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)/blog/[post].tsx', 296 dynamic: [{ deep: false, name: 'post' }], 297 route: '(index)/blog/[post]', 298 }, 299 { 300 children: [], 301 contextKey: './(app)/(about)/blog/[post].tsx', 302 dynamic: [{ deep: false, name: 'post' }], 303 route: '(about)/blog/[post]', 304 }, 305 ], 306 contextKey: './(app)/_layout.tsx', 307 dynamic: null, 308 initialRouteName: undefined, 309 route: '(app)', 310 }, 311 ], 312 contextKey: './_layout.tsx', 313 dynamic: null, 314 generated: true, 315 route: '', 316 }); 317 318 expect(dropFunctions(await loadStaticParamsAsync(route))).toEqual({ 319 children: [ 320 { 321 children: [ 322 { 323 children: [], 324 contextKey: './(app)/(index)/blog/[post].tsx', 325 dynamic: [{ deep: false, name: 'post' }], 326 route: '(index)/blog/[post]', 327 }, 328 { 329 children: [], 330 contextKey: './(app)/(index)/blog/123.tsx', 331 dynamic: null, 332 route: '(index)/blog/123', 333 }, 334 { 335 children: [], 336 contextKey: './(app)/(index)/blog/abc.tsx', 337 dynamic: null, 338 route: '(index)/blog/abc', 339 }, 340 { 341 children: [], 342 contextKey: './(app)/(about)/blog/[post].tsx', 343 dynamic: [{ deep: false, name: 'post' }], 344 route: '(about)/blog/[post]', 345 }, 346 { 347 children: [], 348 contextKey: './(app)/(about)/blog/123.tsx', 349 dynamic: null, 350 route: '(about)/blog/123', 351 }, 352 { 353 children: [], 354 contextKey: './(app)/(about)/blog/abc.tsx', 355 dynamic: null, 356 route: '(about)/blog/abc', 357 }, 358 ], 359 contextKey: './(app)/_layout.tsx', 360 dynamic: null, 361 initialRouteName: undefined, 362 route: '(app)', 363 }, 364 ], 365 contextKey: './_layout.tsx', 366 dynamic: null, 367 generated: true, 368 route: '', 369 }); 370 }); 371 372 it(`generateStaticParams with nested dynamic segments`, async () => { 373 const ctx = createMockContextModule({ 374 './post/[post].tsx': { 375 default() {}, 376 async generateStaticParams() { 377 return [{ post: '123' }]; 378 }, 379 }, 380 './a/[b]/c/[d]/[e].tsx': { 381 default() {}, 382 async generateStaticParams() { 383 return [{ b: 'b', d: 'd', e: 'e' }]; 384 }, 385 }, 386 }); 387 388 const route = getExactRoutes(ctx)!; 389 390 expect(dropFunctions(route)).toEqual({ 391 children: [ 392 { 393 children: [], 394 contextKey: './post/[post].tsx', 395 dynamic: [{ deep: false, name: 'post' }], 396 route: 'post/[post]', 397 }, 398 { 399 children: [], 400 contextKey: './a/[b]/c/[d]/[e].tsx', 401 dynamic: [ 402 { 403 deep: false, 404 name: 'b', 405 }, 406 { 407 deep: false, 408 name: 'd', 409 }, 410 { 411 deep: false, 412 name: 'e', 413 }, 414 ], 415 route: 'a/[b]/c/[d]/[e]', 416 }, 417 ], 418 contextKey: './_layout.tsx', 419 dynamic: null, 420 generated: true, 421 route: '', 422 }); 423 424 expect(dropFunctions(await loadStaticParamsAsync(route))).toEqual({ 425 children: [ 426 { 427 children: [], 428 contextKey: './post/[post].tsx', 429 dynamic: [{ deep: false, name: 'post' }], 430 route: 'post/[post]', 431 }, 432 { 433 children: [], 434 contextKey: './post/123.tsx', 435 dynamic: null, 436 route: 'post/123', 437 }, 438 { 439 children: [], 440 contextKey: './a/[b]/c/[d]/[e].tsx', 441 dynamic: [ 442 { 443 deep: false, 444 name: 'b', 445 }, 446 { 447 deep: false, 448 name: 'd', 449 }, 450 { 451 deep: false, 452 name: 'e', 453 }, 454 ], 455 route: 'a/[b]/c/[d]/[e]', 456 }, 457 { 458 children: [], 459 contextKey: './a/b/c/d/e.tsx', 460 dynamic: null, 461 route: 'a/b/c/d/e', 462 }, 463 ], 464 contextKey: './_layout.tsx', 465 dynamic: null, 466 generated: true, 467 route: '', 468 }); 469 }); 470 471 it(`generateStaticParams throws when deep dynamic segments return invalid type`, async () => { 472 const loadWithParam = (params) => 473 loadStaticParamsAsync( 474 getExactRoutes( 475 createMockContextModule({ 476 './post/[...post].tsx': { 477 default() {}, 478 generateStaticParams() { 479 return params; 480 }, 481 }, 482 }) 483 )! 484 ); 485 486 // Passes 487 await loadWithParam([{ post: '123' }]); 488 await loadWithParam([{ post: '123/456' }]); 489 await loadWithParam([{ post: ['123/456', '123'] }]); 490 await loadWithParam([{ post: ['123', '123'] }]); 491 await loadWithParam([{ post: ['123', '/'] }]); 492 await loadWithParam([{ post: [123, '/', '432'] }]); 493 494 await expect(loadWithParam([{ post: ['/'] }])).rejects.toThrowErrorMatchingInlineSnapshot( 495 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing "/"."` 496 ); 497 await expect(loadWithParam([{ post: '' }])).rejects.toThrowErrorMatchingInlineSnapshot( 498 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing ""."` 499 ); 500 await expect( 501 loadWithParam([{ post: ['', '/', ''] }]) 502 ).rejects.toThrowErrorMatchingInlineSnapshot( 503 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" not to be empty while parsing "/"."` 504 ); 505 await expect(loadWithParam([{ post: null }])).rejects.toThrowErrorMatchingInlineSnapshot( 506 `"generateStaticParams() must return an array of params that match the dynamic route. Received {"post":null}"` 507 ); 508 await expect(loadWithParam([{ post: false }])).rejects.toThrowErrorMatchingInlineSnapshot( 509 `"generateStaticParams() for route "./post/[...post].tsx" expected param "post" to be of type string, instead found "boolean" while parsing "false"."` 510 ); 511 }); 512 513 it(`generateStaticParams throws when dynamic segments return invalid type`, async () => { 514 const ctx = createMockContextModule({ 515 './post/[post].tsx': { 516 default() {}, 517 generateStaticParams() { 518 return [{ post: ['123'] }]; 519 }, 520 }, 521 }); 522 const route = getExactRoutes(ctx)!; 523 await expect(loadStaticParamsAsync(route)).rejects.toThrowErrorMatchingInlineSnapshot( 524 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" to be of type string, instead found "object" while parsing "123"."` 525 ); 526 }); 527 528 it(`generateStaticParams throws when dynamic segments return invalid format (multiple slugs)`, async () => { 529 const ctx = createMockContextModule({ 530 './post/[post].tsx': { 531 default() {}, 532 generateStaticParams() { 533 return [{ post: '123/abc' }]; 534 }, 535 }, 536 }); 537 const route = getExactRoutes(ctx)!; 538 await expect(loadStaticParamsAsync(route)).rejects.toThrowErrorMatchingInlineSnapshot( 539 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" to not contain "/" (multiple segments) while parsing "123/abc"."` 540 ); 541 }); 542 543 it(`generateStaticParams throws when dynamic segments return empty string`, async () => { 544 await expect( 545 loadStaticParamsAsync( 546 getExactRoutes( 547 createMockContextModule({ 548 './post/[post].tsx': { 549 default() {}, 550 generateStaticParams() { 551 return [{ post: '/' }]; 552 }, 553 }, 554 }) 555 )! 556 ) 557 ).rejects.toThrowErrorMatchingInlineSnapshot( 558 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" not to be empty while parsing "/"."` 559 ); 560 await expect( 561 loadStaticParamsAsync( 562 getExactRoutes( 563 createMockContextModule({ 564 './post/[post].tsx': { 565 default() {}, 566 generateStaticParams() { 567 return [{ post: '' }]; 568 }, 569 }, 570 }) 571 )! 572 ) 573 ).rejects.toThrowErrorMatchingInlineSnapshot( 574 `"generateStaticParams() for route "./post/[post].tsx" expected param "post" not to be empty while parsing ""."` 575 ); 576 }); 577 578 it(`generateStaticParams allows when dynamic segments return a single slug with a benign slash`, async () => { 579 const ctx = createMockContextModule({ 580 './post/[post].tsx': { 581 default() {}, 582 generateStaticParams() { 583 return [{ post: '123/' }, { post: '/123' }]; 584 }, 585 }, 586 }); 587 // doesn't throw 588 await loadStaticParamsAsync(getExactRoutes(ctx)!); 589 }); 590}); 591