nextjs-routing-utils.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /**
  2. * Unit tests for nextjs-routing-utils.ts
  3. *
  4. * This test suite covers:
  5. * - useNextjsRoutingPageRegister hook functionality
  6. * - detectNextjsRoutingType function with various routing scenarios
  7. * - Edge cases and error handling
  8. *
  9. * @vitest-environment happy-dom
  10. */
  11. import type { GetServerSidePropsContext } from 'next';
  12. import { renderHook } from '@testing-library/react';
  13. import Cookies from 'js-cookie';
  14. import { mock } from 'vitest-mock-extended';
  15. import {
  16. detectNextjsRoutingType,
  17. useNextjsRoutingPageRegister,
  18. } from './nextjs-routing-utils';
  19. // Mock js-cookie
  20. vi.mock('js-cookie', () => ({
  21. default: {
  22. set: vi.fn(),
  23. remove: vi.fn(),
  24. },
  25. }));
  26. const mockCookies = vi.mocked(Cookies);
  27. describe('nextjs-routing-utils', () => {
  28. beforeEach(() => {
  29. vi.clearAllMocks();
  30. });
  31. describe('useNextjsRoutingPageRegister', () => {
  32. it('should set cookie when nextjsRoutingPage is provided', () => {
  33. const testPage = '/test-page';
  34. renderHook(() => useNextjsRoutingPageRegister(testPage));
  35. expect(mockCookies.set).toHaveBeenCalledWith(
  36. 'nextjsRoutingPage',
  37. testPage,
  38. { path: '/', expires: 1 / 24 },
  39. );
  40. });
  41. it('should remove cookie when nextjsRoutingPage is null', () => {
  42. renderHook(() => useNextjsRoutingPageRegister(undefined));
  43. expect(mockCookies.remove).toHaveBeenCalledWith('nextjsRoutingPage');
  44. });
  45. it('should update cookie when nextjsRoutingPage changes', () => {
  46. const { rerender } = renderHook(
  47. ({ page }) => useNextjsRoutingPageRegister(page),
  48. { initialProps: { page: '/initial-page' } },
  49. );
  50. expect(mockCookies.set).toHaveBeenCalledWith(
  51. 'nextjsRoutingPage',
  52. '/initial-page',
  53. { path: '/', expires: 1 / 24 },
  54. );
  55. // Clear mock to check next call
  56. mockCookies.set.mockClear();
  57. rerender({ page: '/updated-page' });
  58. expect(mockCookies.set).toHaveBeenCalledWith(
  59. 'nextjsRoutingPage',
  60. '/updated-page',
  61. { path: '/', expires: 1 / 24 },
  62. );
  63. });
  64. it('should call useEffect cleanup when component unmounts', () => {
  65. const testPage = '/test-page';
  66. const { unmount } = renderHook(() =>
  67. useNextjsRoutingPageRegister(testPage),
  68. );
  69. expect(mockCookies.set).toHaveBeenCalledWith(
  70. 'nextjsRoutingPage',
  71. testPage,
  72. { path: '/', expires: 1 / 24 },
  73. );
  74. unmount();
  75. // useEffect cleanup should not cause additional calls
  76. expect(mockCookies.set).toHaveBeenCalledTimes(1);
  77. });
  78. it('should handle rapid prop changes correctly', () => {
  79. const { rerender } = renderHook(
  80. ({ page }: { page: string | null }) =>
  81. useNextjsRoutingPageRegister(page ?? undefined),
  82. { initialProps: { page: '/page1' as string | null } },
  83. );
  84. expect(mockCookies.set).toHaveBeenLastCalledWith(
  85. 'nextjsRoutingPage',
  86. '/page1',
  87. { path: '/', expires: 1 / 24 },
  88. );
  89. rerender({ page: '/page2' });
  90. expect(mockCookies.set).toHaveBeenLastCalledWith(
  91. 'nextjsRoutingPage',
  92. '/page2',
  93. { path: '/', expires: 1 / 24 },
  94. );
  95. rerender({ page: null });
  96. expect(mockCookies.remove).toHaveBeenLastCalledWith('nextjsRoutingPage');
  97. rerender({ page: '/page3' });
  98. expect(mockCookies.set).toHaveBeenLastCalledWith(
  99. 'nextjsRoutingPage',
  100. '/page3',
  101. { path: '/', expires: 1 / 24 },
  102. );
  103. });
  104. });
  105. describe('detectNextjsRoutingType', () => {
  106. // Type-safe helper function to create mock contexts using vitest-mock-extended
  107. const createMockContext = (
  108. hasNextjsHeader: boolean,
  109. cookieValue?: string,
  110. ): GetServerSidePropsContext => {
  111. const mockReq = mock<GetServerSidePropsContext['req']>();
  112. const mockRes = mock<GetServerSidePropsContext['res']>();
  113. // Configure mock request headers
  114. mockReq.headers = hasNextjsHeader ? { 'x-nextjs-data': '1' } : {};
  115. // Configure mock request cookies
  116. mockReq.cookies = cookieValue ? { nextjsRoutingPage: cookieValue } : {};
  117. return {
  118. req: mockReq,
  119. res: mockRes,
  120. query: {},
  121. params: {},
  122. resolvedUrl: '/test',
  123. };
  124. };
  125. // Helper function for special edge cases requiring type coercion
  126. const createMockContextWithSpecialValues = (
  127. headerValue: string | undefined,
  128. cookieValue: string | null,
  129. ): GetServerSidePropsContext => {
  130. const mockReq = mock<GetServerSidePropsContext['req']>();
  131. const mockRes = mock<GetServerSidePropsContext['res']>();
  132. // For edge cases where we need to simulate invalid types that can occur at runtime
  133. mockReq.headers =
  134. headerValue !== undefined ? { 'x-nextjs-data': headerValue } : {};
  135. mockReq.cookies =
  136. cookieValue !== null
  137. ? { nextjsRoutingPage: cookieValue as string }
  138. : {};
  139. return {
  140. req: mockReq,
  141. res: mockRes,
  142. query: {},
  143. params: {},
  144. resolvedUrl: '/test',
  145. };
  146. };
  147. it('should return INITIAL when request is not CSR (no x-nextjs-data header)', () => {
  148. const context = createMockContext(false);
  149. const previousRoutingPage = '/previous-page';
  150. const result = detectNextjsRoutingType(context, previousRoutingPage);
  151. expect(result).toBe('initial');
  152. });
  153. it('should return SAME_ROUTE when CSR and cookie matches previousRoutingPage', () => {
  154. const previousRoutingPage = '/same-page';
  155. const context = createMockContext(true, previousRoutingPage);
  156. const result = detectNextjsRoutingType(context, previousRoutingPage);
  157. expect(result).toBe('same-route');
  158. });
  159. it('should return FROM_OUTSIDE when CSR but no cookie exists', () => {
  160. const context = createMockContext(true); // No cookie value
  161. const previousRoutingPage = '/previous-page';
  162. const result = detectNextjsRoutingType(context, previousRoutingPage);
  163. expect(result).toBe('from-outside');
  164. });
  165. it('should return FROM_OUTSIDE when CSR and cookie does not match previousRoutingPage', () => {
  166. const context = createMockContext(true, '/different-page');
  167. const previousRoutingPage = '/previous-page';
  168. const result = detectNextjsRoutingType(context, previousRoutingPage);
  169. expect(result).toBe('from-outside');
  170. });
  171. it('should return FROM_OUTSIDE when CSR and cookie is empty string', () => {
  172. const context = createMockContext(true, '');
  173. const previousRoutingPage = '/previous-page';
  174. const result = detectNextjsRoutingType(context, previousRoutingPage);
  175. expect(result).toBe('from-outside');
  176. });
  177. it('should handle x-nextjs-data header with different truthy values', () => {
  178. const context = createMockContext(true, '/test-page');
  179. // Override the header value to test different truthy values
  180. const mockReq = context.req as typeof context.req & {
  181. headers: Record<string, string>;
  182. };
  183. mockReq.headers = { 'x-nextjs-data': 'some-value' };
  184. const result = detectNextjsRoutingType(context, '/test-page');
  185. expect(result).toBe('same-route');
  186. });
  187. it('should handle edge case where cookie value is null but previousRoutingPage exists', () => {
  188. // Using helper function for edge case where we need to simulate runtime null values
  189. const context = createMockContextWithSpecialValues('1', null);
  190. const result = detectNextjsRoutingType(context, '/previous-page');
  191. expect(result).toBe('from-outside');
  192. });
  193. it('should handle missing x-nextjs-data header even when cookies are present', () => {
  194. const mockReq = mock<GetServerSidePropsContext['req']>();
  195. const mockRes = mock<GetServerSidePropsContext['res']>();
  196. mockReq.headers = {}; // No x-nextjs-data header
  197. mockReq.cookies = { nextjsRoutingPage: '/test-page' };
  198. const context: GetServerSidePropsContext = {
  199. req: mockReq,
  200. res: mockRes,
  201. query: {},
  202. params: {},
  203. resolvedUrl: '/test',
  204. };
  205. const result = detectNextjsRoutingType(context, '/test-page');
  206. expect(result).toBe('initial');
  207. });
  208. it('should handle undefined x-nextjs-data header value', () => {
  209. // Using helper function for edge case where header value is undefined
  210. const context = createMockContextWithSpecialValues(
  211. undefined,
  212. '/test-page',
  213. );
  214. const result = detectNextjsRoutingType(context, '/test-page');
  215. expect(result).toBe('initial');
  216. });
  217. describe('when previousRoutingPage is undefined', () => {
  218. it('should return INITIAL when request is not CSR', () => {
  219. const context = createMockContext(false);
  220. const result = detectNextjsRoutingType(context);
  221. expect(result).toBe('initial');
  222. });
  223. it('should return FROM_OUTSIDE when CSR and no cookie exists', () => {
  224. const context = createMockContext(true); // No cookie value
  225. const result = detectNextjsRoutingType(context);
  226. expect(result).toBe('from-outside');
  227. });
  228. it('should return FROM_OUTSIDE when CSR and cookie exists', () => {
  229. const context = createMockContext(true, '/some-page');
  230. const result = detectNextjsRoutingType(context);
  231. expect(result).toBe('from-outside');
  232. });
  233. it('should return FROM_OUTSIDE when CSR and cookie is empty string', () => {
  234. const context = createMockContext(true, '');
  235. const result = detectNextjsRoutingType(context);
  236. expect(result).toBe('from-outside');
  237. });
  238. });
  239. describe('when both cookie and previousRoutingPage are undefined/null', () => {
  240. it('should return INITIAL when request is not CSR and both are missing', () => {
  241. const context = createMockContext(false);
  242. const result = detectNextjsRoutingType(context);
  243. expect(result).toBe('initial');
  244. });
  245. it('should return FROM_OUTSIDE when CSR and both are missing', () => {
  246. const context = createMockContext(true); // No cookie
  247. const result = detectNextjsRoutingType(context);
  248. expect(result).toBe('from-outside');
  249. });
  250. });
  251. });
  252. });