Prajwal Kulkarni
Prajwal Kulkarni

Reputation: 1705

`usePathname` returning `null` despite mocking the return value - vitest

I'm currently writing UTs using vitest & react-testing library for my Next.js 14 application and I'm facing issues with the usePathname hook from next/navigation.

The usePathname hook is returning null despite mocking it to return a non-null value

vi.mock('next/navigation', () => ({
    usePathname: () => vi.fn(()=>'/abc'), // Also tried usePathname: () => '/earnings'
    useRouter: () => mockRouter
}));

Alternate implementation of the mock:

vi.mock('next/navigation', async () => {
            const actual =
                await vi.importActual<typeof import('next/navigation')>(
                    'next/navigation'
                );
            return {
                ...actual,
                usePathname: vi.fn().mockReturnValue('/abc'),
            };
});
        options
    );
});

Test case:

 it('should render earnings tabs correctly', async () => {
        customRender(<EarningsWrapper>XYZ</EarningsWrapper>);

        await waitFor(
            async () => {
                const pendingPaymentsTab =
                    await screen.findByText('Pending Payments');
                const pastPaymentsTab =
                    await screen.findByText('Past Payments');

                expect(pendingPaymentsTab).toBeInTheDocument();
                expect(pastPaymentsTab).toBeInTheDocument();
            },
            { timeout: 4000 }
        );
    });

Implementation of customRender fn:

import { render } from '@testing-library/react';
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';

import React from 'react';

export const mockRouter = {
    push: vitest.vi.fn(),
    replace: vitest.vi.fn(),
    prefetch: vitest.vi.fn(),
    ...
};

export const customRender = (ui: React.ReactElement, options = {}) =>
    render(
        <AppRouterContext.Provider value={mockRouter}>
            {ui}
        </AppRouterContext.Provider>,
        options
    );
});

One of the nested components within the EarningsWrapper component uses the usePathname hook.

EarningsWrapper

export const EarningsWrapper = ({children}) => {

   return (
        <PageLayout
            appBar={{ heading: EARNINGS_HEADING, showRightContent: true }}
        >
            <StyledEarningsContainer>
                <EarningsInfo />
                <Divider />
                <HorizontalTabs
                    tabs={EARNINGS_TABS}
                    onClick={setActiveTab}
                    activeTabIndex={activeTabIndex}
                />
                <Divider />
                {children}
            </StyledEarningsContainer>
        </PageLayout>
    );
}

PageLayout

const PageLayout = ({
    appBar,
    children,
    footer,
}: TPageLayoutProps): ReactElement => {
    const { data: unreadCount, handleNotificationIconClick } =
        useNotificationUnreadCount(appBar?.showRightContent ?? false);

    const showFooter = !footer?.hide;
    const showIndicatorOnNotificationIcon = unreadCount > 0;

    const NOTIFICATION_ICON = showIndicatorOnNotificationIcon
        ? {
              src: IMAGE_PATHS.COMMON.NOTIFICATION_UNREAD_ICON.src,
              alt: IMAGE_PATHS.COMMON.NOTIFICATION_UNREAD_ICON.alt,
          }
        : {
              src: IMAGE_PATHS.COMMON.NOTFICATION_ICON.src,
              alt: IMAGE_PATHS.COMMON.NOTFICATION_ICON.alt,
          };

    

    return (
        <StyledPageLayoutContainer>
            {appBar && (
                <>
                    <AppBar
                        heading={appBar.heading}
                        showBackIcon={appBar?.showBackIcon}
                        rightContent={getRightContent()}
                        onBackClick={appBar?.onBackClick}
                        leftIcon={appBar?.leftIcon}
                    />
                    <Divider />
                </>
            )}

            <StyledPageLayoutContent>{children}</StyledPageLayoutContent>
            {showFooter && <Footer customFooter={footer?.customFooter} />}
        </StyledPageLayoutContainer>
    );
};

Footer

const Footer = ({ customFooter }: TFooterProps): ReactElement => {
    const pathname = usePathname();
    const showEarnings = pathname.includes(ROUTES.EARNINGS.BASE);

    const [activeTab, setActiveTab] = useState<string>(
        'XYZ'
    );
    const router = useRouter();

    const handleBottomTabClick = (tab: TBottomNavTab) => {
        setActiveTab(tab.title);
       
        router.push(tab.title)
    };

    return (
        <StyledFooterContainer>
            {customFooter || (
                <BottomNavTabs
                    tabs={getFooterDefaultTabs(activeTab)}
                    onClick={handleBottomTabClick}
                    defaultActiveTabIndex={showEarnings ? 0 : 1}
                />
            )}
        </StyledFooterContainer>
    );
};

Note that redundant parts of the code that are out of the context for this issue have been removed from the above snippets. The error that I'm getting is:

TypeError: Cannot read properties of null (reading 'includes')

Footer src/components/shared/Footer/index.tsx:19:35
     17| const Footer = ({ customFooter }: TFooterProps): ReactElement => {
     18|     const pathname = usePathname();
     19|     const showEarnings = pathname.includes(ROUTES.EARNINGS.BASE);
       |                                   ^
     20| 
     21|     const [activeTab, setActiveTab] = useState<string>(

Further, I tried mocking the component that uses usePathname (essentially skipping to deal with usePathname) but that didn't work either and the hook somehow still returned null.

What exactly is causing this issue and what changes or additions could possibly fix this?

Upvotes: 0

Views: 51

Answers (0)

Related Questions