| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- /**
- * Unit tests for nextjs-routing-utils.ts
- *
- * This test suite covers:
- * - useNextjsRoutingPageRegister hook functionality
- * - detectNextjsRoutingType function with various routing scenarios
- * - Edge cases and error handling
- *
- * @vitest-environment happy-dom
- */
- import type { GetServerSidePropsContext } from 'next';
- import { renderHook } from '@testing-library/react';
- import Cookies from 'js-cookie';
- import { mock } from 'vitest-mock-extended';
- import {
- detectNextjsRoutingType,
- useNextjsRoutingPageRegister,
- } from './nextjs-routing-utils';
- // Mock js-cookie
- vi.mock('js-cookie', () => ({
- default: {
- set: vi.fn(),
- remove: vi.fn(),
- },
- }));
- const mockCookies = vi.mocked(Cookies);
- describe('nextjs-routing-utils', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
- describe('useNextjsRoutingPageRegister', () => {
- it('should set cookie when nextjsRoutingPage is provided', () => {
- const testPage = '/test-page';
- renderHook(() => useNextjsRoutingPageRegister(testPage));
- expect(mockCookies.set).toHaveBeenCalledWith(
- 'nextjsRoutingPage',
- testPage,
- { path: '/', expires: 1 / 24 },
- );
- });
- it('should remove cookie when nextjsRoutingPage is null', () => {
- renderHook(() => useNextjsRoutingPageRegister(undefined));
- expect(mockCookies.remove).toHaveBeenCalledWith('nextjsRoutingPage');
- });
- it('should update cookie when nextjsRoutingPage changes', () => {
- const { rerender } = renderHook(
- ({ page }) => useNextjsRoutingPageRegister(page),
- { initialProps: { page: '/initial-page' } },
- );
- expect(mockCookies.set).toHaveBeenCalledWith(
- 'nextjsRoutingPage',
- '/initial-page',
- { path: '/', expires: 1 / 24 },
- );
- // Clear mock to check next call
- mockCookies.set.mockClear();
- rerender({ page: '/updated-page' });
- expect(mockCookies.set).toHaveBeenCalledWith(
- 'nextjsRoutingPage',
- '/updated-page',
- { path: '/', expires: 1 / 24 },
- );
- });
- it('should call useEffect cleanup when component unmounts', () => {
- const testPage = '/test-page';
- const { unmount } = renderHook(() =>
- useNextjsRoutingPageRegister(testPage),
- );
- expect(mockCookies.set).toHaveBeenCalledWith(
- 'nextjsRoutingPage',
- testPage,
- { path: '/', expires: 1 / 24 },
- );
- unmount();
- // useEffect cleanup should not cause additional calls
- expect(mockCookies.set).toHaveBeenCalledTimes(1);
- });
- it('should handle rapid prop changes correctly', () => {
- const { rerender } = renderHook(
- ({ page }: { page: string | null }) =>
- useNextjsRoutingPageRegister(page ?? undefined),
- { initialProps: { page: '/page1' as string | null } },
- );
- expect(mockCookies.set).toHaveBeenLastCalledWith(
- 'nextjsRoutingPage',
- '/page1',
- { path: '/', expires: 1 / 24 },
- );
- rerender({ page: '/page2' });
- expect(mockCookies.set).toHaveBeenLastCalledWith(
- 'nextjsRoutingPage',
- '/page2',
- { path: '/', expires: 1 / 24 },
- );
- rerender({ page: null });
- expect(mockCookies.remove).toHaveBeenLastCalledWith('nextjsRoutingPage');
- rerender({ page: '/page3' });
- expect(mockCookies.set).toHaveBeenLastCalledWith(
- 'nextjsRoutingPage',
- '/page3',
- { path: '/', expires: 1 / 24 },
- );
- });
- });
- describe('detectNextjsRoutingType', () => {
- // Type-safe helper function to create mock contexts using vitest-mock-extended
- const createMockContext = (
- hasNextjsHeader: boolean,
- cookieValue?: string,
- ): GetServerSidePropsContext => {
- const mockReq = mock<GetServerSidePropsContext['req']>();
- const mockRes = mock<GetServerSidePropsContext['res']>();
- // Configure mock request headers
- mockReq.headers = hasNextjsHeader ? { 'x-nextjs-data': '1' } : {};
- // Configure mock request cookies
- mockReq.cookies = cookieValue ? { nextjsRoutingPage: cookieValue } : {};
- return {
- req: mockReq,
- res: mockRes,
- query: {},
- params: {},
- resolvedUrl: '/test',
- };
- };
- // Helper function for special edge cases requiring type coercion
- const createMockContextWithSpecialValues = (
- headerValue: string | undefined,
- cookieValue: string | null,
- ): GetServerSidePropsContext => {
- const mockReq = mock<GetServerSidePropsContext['req']>();
- const mockRes = mock<GetServerSidePropsContext['res']>();
- // For edge cases where we need to simulate invalid types that can occur at runtime
- mockReq.headers =
- headerValue !== undefined ? { 'x-nextjs-data': headerValue } : {};
- mockReq.cookies =
- cookieValue !== null
- ? { nextjsRoutingPage: cookieValue as string }
- : {};
- return {
- req: mockReq,
- res: mockRes,
- query: {},
- params: {},
- resolvedUrl: '/test',
- };
- };
- it('should return INITIAL when request is not CSR (no x-nextjs-data header)', () => {
- const context = createMockContext(false);
- const previousRoutingPage = '/previous-page';
- const result = detectNextjsRoutingType(context, previousRoutingPage);
- expect(result).toBe('initial');
- });
- it('should return SAME_ROUTE when CSR and cookie matches previousRoutingPage', () => {
- const previousRoutingPage = '/same-page';
- const context = createMockContext(true, previousRoutingPage);
- const result = detectNextjsRoutingType(context, previousRoutingPage);
- expect(result).toBe('same-route');
- });
- it('should return FROM_OUTSIDE when CSR but no cookie exists', () => {
- const context = createMockContext(true); // No cookie value
- const previousRoutingPage = '/previous-page';
- const result = detectNextjsRoutingType(context, previousRoutingPage);
- expect(result).toBe('from-outside');
- });
- it('should return FROM_OUTSIDE when CSR and cookie does not match previousRoutingPage', () => {
- const context = createMockContext(true, '/different-page');
- const previousRoutingPage = '/previous-page';
- const result = detectNextjsRoutingType(context, previousRoutingPage);
- expect(result).toBe('from-outside');
- });
- it('should return FROM_OUTSIDE when CSR and cookie is empty string', () => {
- const context = createMockContext(true, '');
- const previousRoutingPage = '/previous-page';
- const result = detectNextjsRoutingType(context, previousRoutingPage);
- expect(result).toBe('from-outside');
- });
- it('should handle x-nextjs-data header with different truthy values', () => {
- const context = createMockContext(true, '/test-page');
- // Override the header value to test different truthy values
- const mockReq = context.req as typeof context.req & {
- headers: Record<string, string>;
- };
- mockReq.headers = { 'x-nextjs-data': 'some-value' };
- const result = detectNextjsRoutingType(context, '/test-page');
- expect(result).toBe('same-route');
- });
- it('should handle edge case where cookie value is null but previousRoutingPage exists', () => {
- // Using helper function for edge case where we need to simulate runtime null values
- const context = createMockContextWithSpecialValues('1', null);
- const result = detectNextjsRoutingType(context, '/previous-page');
- expect(result).toBe('from-outside');
- });
- it('should handle missing x-nextjs-data header even when cookies are present', () => {
- const mockReq = mock<GetServerSidePropsContext['req']>();
- const mockRes = mock<GetServerSidePropsContext['res']>();
- mockReq.headers = {}; // No x-nextjs-data header
- mockReq.cookies = { nextjsRoutingPage: '/test-page' };
- const context: GetServerSidePropsContext = {
- req: mockReq,
- res: mockRes,
- query: {},
- params: {},
- resolvedUrl: '/test',
- };
- const result = detectNextjsRoutingType(context, '/test-page');
- expect(result).toBe('initial');
- });
- it('should handle undefined x-nextjs-data header value', () => {
- // Using helper function for edge case where header value is undefined
- const context = createMockContextWithSpecialValues(
- undefined,
- '/test-page',
- );
- const result = detectNextjsRoutingType(context, '/test-page');
- expect(result).toBe('initial');
- });
- describe('when previousRoutingPage is undefined', () => {
- it('should return INITIAL when request is not CSR', () => {
- const context = createMockContext(false);
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('initial');
- });
- it('should return FROM_OUTSIDE when CSR and no cookie exists', () => {
- const context = createMockContext(true); // No cookie value
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('from-outside');
- });
- it('should return FROM_OUTSIDE when CSR and cookie exists', () => {
- const context = createMockContext(true, '/some-page');
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('from-outside');
- });
- it('should return FROM_OUTSIDE when CSR and cookie is empty string', () => {
- const context = createMockContext(true, '');
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('from-outside');
- });
- });
- describe('when both cookie and previousRoutingPage are undefined/null', () => {
- it('should return INITIAL when request is not CSR and both are missing', () => {
- const context = createMockContext(false);
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('initial');
- });
- it('should return FROM_OUTSIDE when CSR and both are missing', () => {
- const context = createMockContext(true); // No cookie
- const result = detectNextjsRoutingType(context);
- expect(result).toBe('from-outside');
- });
- });
- });
- });
|