1import { RouteNode } from '../Route'; 2import { getExactRoutes } from '../getRoutes'; 3import { loadStaticParamsAsync } from '../loadStaticParamsAsync'; 4import { RequireContext } from '../types'; 5 6function createMockContextModule(map: Record<string, Record<string, any>> = {}) { 7 const contextModule = jest.fn((key) => map[key]); 8 9 Object.defineProperty(contextModule, 'keys', { 10 value: () => Object.keys(map), 11 }); 12 13 return contextModule as unknown as RequireContext; 14} 15 16function dropFunctions({ loadRoute, ...node }: RouteNode) { 17 return { 18 ...node, 19 children: node.children.map(dropFunctions), 20 }; 21} 22 23describe(loadStaticParamsAsync, () => { 24 it(`evaluates a single dynamic param`, async () => { 25 const route = getExactRoutes( 26 createMockContextModule({ 27 './[color].tsx': { 28 default() {}, 29 unstable_settings: { initialRouteName: 'index' }, 30 generateStaticParams() { 31 return ['red', 'blue'].map((color) => ({ color })); 32 }, 33 }, 34 }) 35 )!; 36 37 expect(dropFunctions(route)).toEqual({ 38 children: [ 39 { 40 children: [], 41 contextKey: './[color].tsx', 42 dynamic: [{ deep: false, name: 'color' }], 43 route: '[color]', 44 }, 45 ], 46 contextKey: './_layout.tsx', 47 dynamic: null, 48 generated: true, 49 route: '', 50 }); 51 52 const r = await loadStaticParamsAsync(route); 53 54 expect(dropFunctions(r)).toEqual({ 55 children: [ 56 { 57 children: [], 58 contextKey: './[color].tsx', 59 dynamic: [{ deep: false, name: 'color' }], 60 route: '[color]', 61 }, 62 { children: [], contextKey: './red.tsx', dynamic: null, route: 'red' }, 63 { 64 children: [], 65 contextKey: './blue.tsx', 66 dynamic: null, 67 route: 'blue', 68 }, 69 ], 70 contextKey: './_layout.tsx', 71 dynamic: null, 72 generated: true, 73 route: '', 74 }); 75 }); 76 77 it(`evaluates with nested dynamic routes`, async () => { 78 const ctx = createMockContextModule({ 79 './_layout.tsx': { default() {} }, 80 './[color]/[shape].tsx': { 81 default() {}, 82 async generateStaticParams({ params }) { 83 return ['square', 'triangle'].map((shape) => ({ 84 ...params, 85 shape, 86 })); 87 }, 88 }, 89 './[color]/_layout.tsx': { 90 default() {}, 91 generateStaticParams() { 92 return ['red', 'blue'].map((color) => ({ color })); 93 }, 94 }, 95 }); 96 const route = getExactRoutes(ctx); 97 98 expect(dropFunctions(route!)).toEqual({ 99 children: [ 100 { 101 children: [ 102 { 103 children: [], 104 contextKey: './[color]/[shape].tsx', 105 dynamic: [{ deep: false, name: 'shape' }], 106 route: '[shape]', 107 }, 108 ], 109 contextKey: './[color]/_layout.tsx', 110 dynamic: [{ deep: false, name: 'color' }], 111 initialRouteName: undefined, 112 route: '[color]', 113 }, 114 ], 115 contextKey: './_layout.tsx', 116 dynamic: null, 117 initialRouteName: undefined, 118 route: '', 119 }); 120 121 const r = await loadStaticParamsAsync(route!); 122 123 expect(dropFunctions(r)).toEqual({ 124 children: [ 125 { 126 children: [ 127 { 128 children: [], 129 contextKey: './[color]/[shape].tsx', 130 dynamic: [{ deep: false, name: 'shape' }], 131 route: '[shape]', 132 }, 133 { 134 children: [], 135 contextKey: './[color]/square.tsx', 136 dynamic: null, 137 route: 'square', 138 }, 139 { 140 children: [], 141 contextKey: './[color]/triangle.tsx', 142 dynamic: null, 143 route: 'triangle', 144 }, 145 ], 146 contextKey: './[color]/_layout.tsx', 147 dynamic: [{ deep: false, name: 'color' }], 148 initialRouteName: undefined, 149 route: '[color]', 150 }, 151 { 152 children: [ 153 { 154 children: [], 155 contextKey: './[color]/[shape].tsx', 156 dynamic: [{ deep: false, name: 'shape' }], 157 route: '[shape]', 158 }, 159 { 160 children: [], 161 contextKey: './[color]/square.tsx', 162 dynamic: null, 163 route: 'square', 164 }, 165 { 166 children: [], 167 contextKey: './[color]/triangle.tsx', 168 dynamic: null, 169 route: 'triangle', 170 }, 171 ], 172 contextKey: './red/_layout.tsx', 173 dynamic: null, 174 initialRouteName: undefined, 175 route: 'red', 176 }, 177 { 178 children: [ 179 { 180 children: [], 181 contextKey: './[color]/[shape].tsx', 182 dynamic: [{ deep: false, name: 'shape' }], 183 route: '[shape]', 184 }, 185 { 186 children: [], 187 contextKey: './[color]/square.tsx', 188 dynamic: null, 189 route: 'square', 190 }, 191 { 192 children: [], 193 contextKey: './[color]/triangle.tsx', 194 dynamic: null, 195 route: 'triangle', 196 }, 197 ], 198 contextKey: './blue/_layout.tsx', 199 dynamic: null, 200 initialRouteName: undefined, 201 route: 'blue', 202 }, 203 ], 204 contextKey: './_layout.tsx', 205 dynamic: null, 206 initialRouteName: undefined, 207 route: '', 208 }); 209 }); 210 211 it(`throws when required parameter is missing`, async () => { 212 const routes = getExactRoutes( 213 createMockContextModule({ 214 './post/[post].tsx': { 215 default() {}, 216 generateStaticParams() { 217 return [{}]; 218 }, 219 }, 220 }) 221 )!; 222 await expect(loadStaticParamsAsync(routes)).rejects.toThrowErrorMatchingInlineSnapshot( 223 `"generateStaticParams() must return an array of params that match the dynamic route. Received {}"` 224 ); 225 }); 226 227 it(`evaluates with nested deep dynamic segments`, async () => { 228 const ctx = createMockContextModule({ 229 './post/[...post].tsx': { 230 default() {}, 231 async generateStaticParams() { 232 return [{ post: ['123', '456'] }]; 233 }, 234 }, 235 }); 236 237 const route = getExactRoutes(ctx)!; 238 239 expect(dropFunctions(route)).toEqual({ 240 children: [ 241 { 242 children: [], 243 contextKey: './post/[...post].tsx', 244 dynamic: [{ deep: true, name: 'post' }], 245 route: 'post/[...post]', 246 }, 247 ], 248 contextKey: './_layout.tsx', 249 dynamic: null, 250 generated: true, 251 route: '', 252 }); 253 254 expect(dropFunctions(await loadStaticParamsAsync(route))).toEqual({ 255 children: [ 256 { 257 children: [], 258 contextKey: './post/[...post].tsx', 259 dynamic: [{ deep: true, name: 'post' }], 260 route: 'post/[...post]', 261 }, 262 { 263 children: [], 264 contextKey: './post/123/456.tsx', 265 dynamic: null, 266 route: 'post/123/456', 267 }, 268 ], 269 contextKey: './_layout.tsx', 270 dynamic: null, 271 generated: true, 272 route: '', 273 }); 274 }); 275 276 it(`evaluates with nested clone syntax`, async () => { 277 const ctx = createMockContextModule({ 278 './(app)/_layout.tsx': { default() {} }, 279 './(app)/(index,about)/blog/[post].tsx': { 280 default() {}, 281 async generateStaticParams() { 282 return [{ post: '123' }, { post: 'abc' }]; 283 }, 284 }, 285 }); 286 287 const route = getExactRoutes(ctx)!; 288 289 expect(dropFunctions(route)).toEqual({ 290 children: [ 291 { 292 children: [ 293 { 294 children: [], 295 contextKey: './(app)/(index,about)/blog/[post].tsx', 296 dynamic: [{ deep: false, name: 'post' }], 297 route: '(index,about)/blog/[post]', 298 }, 299 ], 300 contextKey: './(app)/_layout.tsx', 301 dynamic: null, 302 initialRouteName: undefined, 303 route: '(app)', 304 }, 305 ], 306 contextKey: './_layout.tsx', 307 dynamic: null, 308 generated: true, 309 route: '', 310 }); 311 312 expect(dropFunctions(await loadStaticParamsAsync(route))).toEqual({ 313 children: [ 314 { 315 children: [ 316 { 317 children: [], 318 contextKey: './(app)/(index,about)/blog/[post].tsx', 319 dynamic: [{ deep: false, name: 'post' }], 320 route: '(index,about)/blog/[post]', 321 }, 322 { 323 children: [], 324 contextKey: './(app)/(index,about)/blog/123.tsx', 325 dynamic: null, 326 route: '(index,about)/blog/123', 327 }, 328 { 329 children: [], 330 contextKey: './(app)/(index,about)/blog/abc.tsx', 331 dynamic: null, 332 route: '(index,about)/blog/abc', 333 }, 334 ], 335 contextKey: './(app)/_layout.tsx', 336 dynamic: null, 337 initialRouteName: undefined, 338 route: '(app)', 339 }, 340 ], 341 contextKey: './_layout.tsx', 342 dynamic: null, 343 generated: true, 344 route: '', 345 }); 346 }); 347 348 it(`generateStaticParams with nested dynamic segments`, async () => { 349 const ctx = createMockContextModule({ 350 './post/[post].tsx': { 351 default() {}, 352 async generateStaticParams() { 353 return [{ post: '123' }]; 354 }, 355 }, 356 './a/[b]/c/[d]/[e].tsx': { 357 default() {}, 358 async generateStaticParams() { 359 return [{ b: 'b', d: 'd', e: 'e' }]; 360 }, 361 }, 362 }); 363 364 const route = getExactRoutes(ctx)!; 365 366 expect(dropFunctions(route)).toEqual({ 367 children: [ 368 { 369 children: [], 370 contextKey: './post/[post].tsx', 371 dynamic: [{ deep: false, name: 'post' }], 372 route: 'post/[post]', 373 }, 374 { 375 children: [], 376 contextKey: './a/[b]/c/[d]/[e].tsx', 377 dynamic: [ 378 { 379 deep: false, 380 name: 'b', 381 }, 382 { 383 deep: false, 384 name: 'd', 385 }, 386 { 387 deep: false, 388 name: 'e', 389 }, 390 ], 391 route: 'a/[b]/c/[d]/[e]', 392 }, 393 ], 394 contextKey: './_layout.tsx', 395 dynamic: null, 396 generated: true, 397 route: '', 398 }); 399 400 expect(dropFunctions(await loadStaticParamsAsync(route))).toEqual({ 401 children: [ 402 { 403 children: [], 404 contextKey: './post/[post].tsx', 405 dynamic: [{ deep: false, name: 'post' }], 406 route: 'post/[post]', 407 }, 408 { 409 children: [], 410 contextKey: './post/123.tsx', 411 dynamic: null, 412 route: 'post/123', 413 }, 414 { 415 children: [], 416 contextKey: './a/[b]/c/[d]/[e].tsx', 417 dynamic: [ 418 { 419 deep: false, 420 name: 'b', 421 }, 422 { 423 deep: false, 424 name: 'd', 425 }, 426 { 427 deep: false, 428 name: 'e', 429 }, 430 ], 431 route: 'a/[b]/c/[d]/[e]', 432 }, 433 { 434 children: [], 435 contextKey: './a/b/c/d/e.tsx', 436 dynamic: null, 437 route: 'a/b/c/d/e', 438 }, 439 ], 440 contextKey: './_layout.tsx', 441 dynamic: null, 442 generated: true, 443 route: '', 444 }); 445 }); 446 447 it(`generateStaticParams throws when deep dynamic segments return invalid type`, async () => { 448 const loadWithParam = (params) => 449 loadStaticParamsAsync( 450 getExactRoutes( 451 createMockContextModule({ 452 './post/[...post].tsx': { 453 default() {}, 454 generateStaticParams() { 455 return params; 456 }, 457 }, 458 }) 459 )! 460 ); 461 462 // Passes 463 await loadWithParam([{ post: '123' }]); 464 await loadWithParam([{ post: '123/456' }]); 465 await loadWithParam([{ post: ['123/456', '123'] }]); 466 await loadWithParam([{ post: ['123', '123'] }]); 467 await loadWithParam([{ post: ['123', '/'] }]); 468 await loadWithParam([{ post: [123, '/', '432'] }]); 469 470 await expect(loadWithParam([{ post: ['/'] }])).rejects.toThrowErrorMatchingInlineSnapshot( 471 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing "/"."` 472 ); 473 await expect(loadWithParam([{ post: '' }])).rejects.toThrowErrorMatchingInlineSnapshot( 474 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing ""."` 475 ); 476 await expect( 477 loadWithParam([{ post: ['', '/', ''] }]) 478 ).rejects.toThrowErrorMatchingInlineSnapshot( 479 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing "/"."` 480 ); 481 await expect(loadWithParam([{ post: null }])).rejects.toThrowErrorMatchingInlineSnapshot( 482 `"generateStaticParams() must return an array of params that match the dynamic route. Received {"post":null}"` 483 ); 484 await expect(loadWithParam([{ post: false }])).rejects.toThrowErrorMatchingInlineSnapshot( 485 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" to be of type string, instead found "boolean" while parsing "false"."` 486 ); 487 }); 488 489 it(`generateStaticParams throws when dynamic segments return invalid type`, async () => { 490 const ctx = createMockContextModule({ 491 './post/[post].tsx': { 492 default() {}, 493 generateStaticParams() { 494 return [{ post: ['123'] }]; 495 }, 496 }, 497 }); 498 const route = getExactRoutes(ctx)!; 499 await expect(loadStaticParamsAsync(route)).rejects.toThrowErrorMatchingInlineSnapshot( 500 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" to be of type string, instead found "object" while parsing "123"."` 501 ); 502 }); 503 504 it(`generateStaticParams throws when dynamic segments return invalid format (multiple slugs)`, async () => { 505 const ctx = createMockContextModule({ 506 './post/[post].tsx': { 507 default() {}, 508 generateStaticParams() { 509 return [{ post: '123/abc' }]; 510 }, 511 }, 512 }); 513 const route = getExactRoutes(ctx)!; 514 await expect(loadStaticParamsAsync(route)).rejects.toThrowErrorMatchingInlineSnapshot( 515 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" to not contain "/" (multiple segments) while parsing "123/abc"."` 516 ); 517 }); 518 519 it(`generateStaticParams throws when dynamic segments return empty string`, async () => { 520 await expect( 521 loadStaticParamsAsync( 522 getExactRoutes( 523 createMockContextModule({ 524 './post/[post].tsx': { 525 default() {}, 526 generateStaticParams() { 527 return [{ post: '/' }]; 528 }, 529 }, 530 }) 531 )! 532 ) 533 ).rejects.toThrowErrorMatchingInlineSnapshot( 534 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" not to be empty while parsing "/"."` 535 ); 536 await expect( 537 loadStaticParamsAsync( 538 getExactRoutes( 539 createMockContextModule({ 540 './post/[post].tsx': { 541 default() {}, 542 generateStaticParams() { 543 return [{ post: '' }]; 544 }, 545 }, 546 }) 547 )! 548 ) 549 ).rejects.toThrowErrorMatchingInlineSnapshot( 550 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" not to be empty while parsing ""."` 551 ); 552 }); 553 554 it(`generateStaticParams allows when dynamic segments return a single slug with a benign slash`, async () => { 555 const ctx = createMockContextModule({ 556 './post/[post].tsx': { 557 default() {}, 558 generateStaticParams() { 559 return [{ post: '123/' }, { post: '/123' }]; 560 }, 561 }, 562 }); 563 // doesn't throw 564 await loadStaticParamsAsync(getExactRoutes(ctx)!); 565 }); 566}); 567