Skip to content

Commit 6e161f3

Browse files
committed
test: added unit tests for isr route matching and detection
1 parent 6eedcb4 commit 6e161f3

File tree

10 files changed

+363
-0
lines changed

10 files changed

+363
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Nested dynamic ISR page
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Static ISR page
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Optional catchall ISR page
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Required catchall ISR page
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Static ISR page at root
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Dynamic ISR with async function
2+
export const generateStaticParams = async (): Promise<void> => {};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Dynamic ISR page with generateStaticParams
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Regular page without ISR (no generateStaticParams)
2+
export {};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Mixed static-dynamic ISR page
2+
export async function generateStaticParams(): Promise<void> {}
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import path from 'path';
2+
import { describe, expect, test } from 'vitest';
3+
import { createRouteManifest } from '../../../../../src/config/manifest/createRouteManifest';
4+
5+
describe('ISR route detection and matching', () => {
6+
const manifest = createRouteManifest({ appDirPath: path.join(__dirname, 'app') });
7+
8+
describe('ISR detection', () => {
9+
test('should detect static ISR pages with generateStaticParams', () => {
10+
expect(manifest.isrRoutes).toContain('/');
11+
expect(manifest.isrRoutes).toContain('/blog');
12+
});
13+
14+
test('should detect dynamic ISR pages with generateStaticParams', () => {
15+
expect(manifest.isrRoutes).toContain('/products/:id');
16+
expect(manifest.isrRoutes).toContain('/posts/:slug');
17+
});
18+
19+
test('should detect nested dynamic ISR pages', () => {
20+
expect(manifest.isrRoutes).toContain('/articles/:category/:slug');
21+
});
22+
23+
test('should detect optional catchall ISR pages', () => {
24+
expect(manifest.isrRoutes).toContain('/docs/:path*?');
25+
});
26+
27+
test('should detect required catchall ISR pages', () => {
28+
expect(manifest.isrRoutes).toContain('/guides/:segments*');
29+
});
30+
31+
test('should detect mixed static-dynamic ISR pages', () => {
32+
expect(manifest.isrRoutes).toContain('/users/:id/profile');
33+
});
34+
35+
test('should NOT detect pages without generateStaticParams as ISR', () => {
36+
expect(manifest.isrRoutes).not.toContain('/regular');
37+
});
38+
39+
test('should detect both function and const generateStaticParams', () => {
40+
// /blog uses function declaration
41+
// /posts/[slug] uses const declaration
42+
expect(manifest.isrRoutes).toContain('/blog');
43+
expect(manifest.isrRoutes).toContain('/posts/:slug');
44+
});
45+
46+
test('should detect async generateStaticParams', () => {
47+
// Multiple pages use async - this should work
48+
expect(manifest.isrRoutes).toContain('/products/:id');
49+
expect(manifest.isrRoutes).toContain('/posts/:slug');
50+
});
51+
});
52+
53+
describe('Route matching against pathnames', () => {
54+
describe('single dynamic segment ISR routes', () => {
55+
test('should match /products/:id against various product IDs', () => {
56+
const route = manifest.dynamicRoutes.find(r => r.path === '/products/:id');
57+
expect(route).toBeDefined();
58+
const regex = new RegExp(route!.regex!);
59+
60+
// Should match
61+
expect(regex.test('/products/1')).toBe(true);
62+
expect(regex.test('/products/123')).toBe(true);
63+
expect(regex.test('/products/abc-def')).toBe(true);
64+
expect(regex.test('/products/product-with-dashes')).toBe(true);
65+
expect(regex.test('/products/UPPERCASE')).toBe(true);
66+
67+
// Should NOT match
68+
expect(regex.test('/products')).toBe(false);
69+
expect(regex.test('/products/')).toBe(false);
70+
expect(regex.test('/products/123/extra')).toBe(false);
71+
expect(regex.test('/product/123')).toBe(false); // typo
72+
});
73+
74+
test('should match /posts/:slug against various slugs', () => {
75+
const route = manifest.dynamicRoutes.find(r => r.path === '/posts/:slug');
76+
expect(route).toBeDefined();
77+
const regex = new RegExp(route!.regex!);
78+
79+
// Should match
80+
expect(regex.test('/posts/hello')).toBe(true);
81+
expect(regex.test('/posts/world')).toBe(true);
82+
expect(regex.test('/posts/my-awesome-post')).toBe(true);
83+
expect(regex.test('/posts/post_with_underscores')).toBe(true);
84+
85+
// Should NOT match
86+
expect(regex.test('/posts')).toBe(false);
87+
expect(regex.test('/posts/')).toBe(false);
88+
expect(regex.test('/posts/hello/world')).toBe(false);
89+
});
90+
});
91+
92+
describe('nested dynamic segments ISR routes', () => {
93+
test('should match /articles/:category/:slug against various paths', () => {
94+
const route = manifest.dynamicRoutes.find(r => r.path === '/articles/:category/:slug');
95+
expect(route).toBeDefined();
96+
const regex = new RegExp(route!.regex!);
97+
98+
// Should match
99+
expect(regex.test('/articles/tech/nextjs-guide')).toBe(true);
100+
expect(regex.test('/articles/tech/react-tips')).toBe(true);
101+
expect(regex.test('/articles/programming/typescript-advanced')).toBe(true);
102+
expect(regex.test('/articles/news/breaking-news-2024')).toBe(true);
103+
104+
// Should NOT match
105+
expect(regex.test('/articles')).toBe(false);
106+
expect(regex.test('/articles/tech')).toBe(false);
107+
expect(regex.test('/articles/tech/nextjs-guide/extra')).toBe(false);
108+
109+
// Extract parameters
110+
const match = '/articles/tech/nextjs-guide'.match(regex);
111+
expect(match).toBeTruthy();
112+
expect(match?.[1]).toBe('tech');
113+
expect(match?.[2]).toBe('nextjs-guide');
114+
});
115+
});
116+
117+
describe('mixed static-dynamic ISR routes', () => {
118+
test('should match /users/:id/profile against user profile paths', () => {
119+
const route = manifest.dynamicRoutes.find(r => r.path === '/users/:id/profile');
120+
expect(route).toBeDefined();
121+
const regex = new RegExp(route!.regex!);
122+
123+
// Should match
124+
expect(regex.test('/users/user1/profile')).toBe(true);
125+
expect(regex.test('/users/user2/profile')).toBe(true);
126+
expect(regex.test('/users/john-doe/profile')).toBe(true);
127+
expect(regex.test('/users/123/profile')).toBe(true);
128+
129+
// Should NOT match
130+
expect(regex.test('/users/user1')).toBe(false);
131+
expect(regex.test('/users/user1/profile/edit')).toBe(false);
132+
expect(regex.test('/users/profile')).toBe(false);
133+
expect(regex.test('/user/user1/profile')).toBe(false); // typo
134+
135+
// Extract parameter
136+
const match = '/users/john-doe/profile'.match(regex);
137+
expect(match).toBeTruthy();
138+
expect(match?.[1]).toBe('john-doe');
139+
});
140+
});
141+
142+
describe('optional catchall ISR routes', () => {
143+
test('should match /docs/:path*? against various documentation paths', () => {
144+
const route = manifest.dynamicRoutes.find(r => r.path === '/docs/:path*?');
145+
expect(route).toBeDefined();
146+
const regex = new RegExp(route!.regex!);
147+
148+
// Should match - with paths
149+
expect(regex.test('/docs/getting-started')).toBe(true);
150+
expect(regex.test('/docs/api/reference')).toBe(true);
151+
expect(regex.test('/docs/guides/installation/quick-start')).toBe(true);
152+
expect(regex.test('/docs/a')).toBe(true);
153+
expect(regex.test('/docs/a/b/c/d/e')).toBe(true);
154+
155+
// Should match - without path (optional catchall)
156+
expect(regex.test('/docs')).toBe(true);
157+
158+
// Should NOT match
159+
expect(regex.test('/doc')).toBe(false); // typo
160+
expect(regex.test('/')).toBe(false);
161+
expect(regex.test('/documents/test')).toBe(false);
162+
163+
// Extract parameters
164+
const matchWithPath = '/docs/api/reference'.match(regex);
165+
expect(matchWithPath).toBeTruthy();
166+
expect(matchWithPath?.[1]).toBe('api/reference');
167+
168+
const matchNoPath = '/docs'.match(regex);
169+
expect(matchNoPath).toBeTruthy();
170+
// Optional catchall without path
171+
expect(matchNoPath?.[1]).toBeUndefined();
172+
});
173+
});
174+
175+
describe('required catchall ISR routes', () => {
176+
test('should match /guides/:segments* against guide paths', () => {
177+
const route = manifest.dynamicRoutes.find(r => r.path === '/guides/:segments*');
178+
expect(route).toBeDefined();
179+
const regex = new RegExp(route!.regex!);
180+
181+
// Should match - with paths (required)
182+
expect(regex.test('/guides/intro')).toBe(true);
183+
expect(regex.test('/guides/advanced/topics')).toBe(true);
184+
expect(regex.test('/guides/getting-started/installation/setup')).toBe(true);
185+
186+
// Should NOT match - without path (required catchall needs at least one segment)
187+
expect(regex.test('/guides')).toBe(false);
188+
expect(regex.test('/guides/')).toBe(false);
189+
190+
// Should NOT match - wrong path
191+
expect(regex.test('/guide/intro')).toBe(false); // typo
192+
expect(regex.test('/')).toBe(false);
193+
194+
// Extract parameters
195+
const match = '/guides/advanced/topics'.match(regex);
196+
expect(match).toBeTruthy();
197+
expect(match?.[1]).toBe('advanced/topics');
198+
});
199+
});
200+
201+
describe('real-world pathname simulations', () => {
202+
test('should identify ISR pages from window.location.pathname examples', () => {
203+
const testCases = [
204+
{ pathname: '/', isISR: true, matchedRoute: '/' },
205+
{ pathname: '/blog', isISR: true, matchedRoute: '/blog' },
206+
{ pathname: '/products/123', isISR: true, matchedRoute: '/products/:id' },
207+
{ pathname: '/products/gaming-laptop', isISR: true, matchedRoute: '/products/:id' },
208+
{ pathname: '/posts/hello-world', isISR: true, matchedRoute: '/posts/:slug' },
209+
{ pathname: '/articles/tech/nextjs-guide', isISR: true, matchedRoute: '/articles/:category/:slug' },
210+
{ pathname: '/users/john/profile', isISR: true, matchedRoute: '/users/:id/profile' },
211+
{ pathname: '/docs', isISR: true, matchedRoute: '/docs/:path*?' },
212+
{ pathname: '/docs/getting-started', isISR: true, matchedRoute: '/docs/:path*?' },
213+
{ pathname: '/docs/api/reference/advanced', isISR: true, matchedRoute: '/docs/:path*?' },
214+
{ pathname: '/guides/intro', isISR: true, matchedRoute: '/guides/:segments*' },
215+
{ pathname: '/guides/advanced/topics/performance', isISR: true, matchedRoute: '/guides/:segments*' },
216+
{ pathname: '/regular', isISR: false, matchedRoute: null },
217+
];
218+
219+
testCases.forEach(({ pathname, isISR, matchedRoute }) => {
220+
// Check if pathname matches any ISR route
221+
let foundMatch = false;
222+
let foundRoute = null;
223+
224+
// Check static ISR routes
225+
if (manifest.isrRoutes.includes(pathname)) {
226+
foundMatch = true;
227+
foundRoute = pathname;
228+
}
229+
230+
// Check dynamic ISR routes
231+
if (!foundMatch) {
232+
for (const route of manifest.dynamicRoutes) {
233+
if (manifest.isrRoutes.includes(route.path)) {
234+
const regex = new RegExp(route.regex!);
235+
if (regex.test(pathname)) {
236+
foundMatch = true;
237+
foundRoute = route.path;
238+
break;
239+
}
240+
}
241+
}
242+
}
243+
244+
expect(foundMatch).toBe(isISR);
245+
if (matchedRoute) {
246+
expect(foundRoute).toBe(matchedRoute);
247+
}
248+
});
249+
});
250+
});
251+
252+
describe('edge cases and special characters', () => {
253+
test('should handle paths with special characters in dynamic segments', () => {
254+
const route = manifest.dynamicRoutes.find(r => r.path === '/products/:id');
255+
const regex = new RegExp(route!.regex!);
256+
257+
expect(regex.test('/products/product-123')).toBe(true);
258+
expect(regex.test('/products/product_456')).toBe(true);
259+
expect(regex.test('/products/PRODUCT-ABC')).toBe(true);
260+
expect(regex.test('/products/2024-new-product')).toBe(true);
261+
});
262+
263+
test('should handle deeply nested catchall paths', () => {
264+
const route = manifest.dynamicRoutes.find(r => r.path === '/docs/:path*?');
265+
const regex = new RegExp(route!.regex!);
266+
267+
expect(regex.test('/docs/a/b/c/d/e/f/g/h/i/j')).toBe(true);
268+
});
269+
270+
test('should not match paths with trailing slashes if route does not have them', () => {
271+
const route = manifest.dynamicRoutes.find(r => r.path === '/products/:id');
272+
const regex = new RegExp(route!.regex!);
273+
274+
// Most Next.js routes don't match trailing slashes
275+
expect(regex.test('/products/123/')).toBe(false);
276+
});
277+
});
278+
279+
describe('parameter extraction for ISR routes', () => {
280+
test('should extract single parameter from ISR route', () => {
281+
const route = manifest.dynamicRoutes.find(r => r.path === '/products/:id');
282+
const regex = new RegExp(route!.regex!);
283+
284+
const match = '/products/gaming-laptop'.match(regex);
285+
expect(match).toBeTruthy();
286+
expect(route?.paramNames).toEqual(['id']);
287+
expect(match?.[1]).toBe('gaming-laptop');
288+
});
289+
290+
test('should extract multiple parameters from nested ISR route', () => {
291+
const route = manifest.dynamicRoutes.find(r => r.path === '/articles/:category/:slug');
292+
const regex = new RegExp(route!.regex!);
293+
294+
const match = '/articles/programming/typescript-advanced'.match(regex);
295+
expect(match).toBeTruthy();
296+
expect(route?.paramNames).toEqual(['category', 'slug']);
297+
expect(match?.[1]).toBe('programming');
298+
expect(match?.[2]).toBe('typescript-advanced');
299+
});
300+
301+
test('should extract catchall parameter from ISR route', () => {
302+
const route = manifest.dynamicRoutes.find(r => r.path === '/docs/:path*?');
303+
const regex = new RegExp(route!.regex!);
304+
305+
const match = '/docs/api/reference/advanced'.match(regex);
306+
expect(match).toBeTruthy();
307+
expect(route?.paramNames).toEqual(['path']);
308+
expect(match?.[1]).toBe('api/reference/advanced');
309+
});
310+
});
311+
});
312+
313+
describe('complete manifest structure', () => {
314+
test('should have correct structure with all route types', () => {
315+
expect(manifest).toHaveProperty('staticRoutes');
316+
expect(manifest).toHaveProperty('dynamicRoutes');
317+
expect(manifest).toHaveProperty('isrRoutes');
318+
expect(Array.isArray(manifest.staticRoutes)).toBe(true);
319+
expect(Array.isArray(manifest.dynamicRoutes)).toBe(true);
320+
expect(Array.isArray(manifest.isrRoutes)).toBe(true);
321+
});
322+
323+
test('should include both ISR and non-ISR routes in main route lists', () => {
324+
// ISR static routes should be in staticRoutes
325+
expect(manifest.staticRoutes.some(r => r.path === '/')).toBe(true);
326+
expect(manifest.staticRoutes.some(r => r.path === '/blog')).toBe(true);
327+
328+
// Non-ISR static routes should also be in staticRoutes
329+
expect(manifest.staticRoutes.some(r => r.path === '/regular')).toBe(true);
330+
331+
// ISR dynamic routes should be in dynamicRoutes
332+
expect(manifest.dynamicRoutes.some(r => r.path === '/products/:id')).toBe(true);
333+
});
334+
335+
test('should only include ISR routes in isrRoutes list', () => {
336+
// ISR routes should be in the list
337+
expect(manifest.isrRoutes).toContain('/');
338+
expect(manifest.isrRoutes).toContain('/blog');
339+
expect(manifest.isrRoutes).toContain('/products/:id');
340+
341+
// Non-ISR routes should NOT be in the list
342+
expect(manifest.isrRoutes).not.toContain('/regular');
343+
});
344+
});
345+
});

0 commit comments

Comments
 (0)