Reputation: 1705
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