@esmx/router 3.0.0-rc.27 → 3.0.0-rc.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.zh-CN.md +82 -1
  2. package/dist/index.d.ts +1 -2
  3. package/dist/index.mjs +0 -1
  4. package/package.json +4 -4
  5. package/src/index.ts +0 -3
  6. package/dist/index.test.d.ts +0 -1
  7. package/dist/index.test.mjs +0 -8
  8. package/dist/location.test.d.ts +0 -8
  9. package/dist/location.test.mjs +0 -370
  10. package/dist/matcher.test.d.ts +0 -1
  11. package/dist/matcher.test.mjs +0 -1492
  12. package/dist/micro-app.dom.test.d.ts +0 -1
  13. package/dist/micro-app.dom.test.mjs +0 -532
  14. package/dist/navigation.test.d.ts +0 -1
  15. package/dist/navigation.test.mjs +0 -681
  16. package/dist/route-task.test.d.ts +0 -1
  17. package/dist/route-task.test.mjs +0 -673
  18. package/dist/route-transition.test.d.ts +0 -1
  19. package/dist/route-transition.test.mjs +0 -146
  20. package/dist/route.test.d.ts +0 -1
  21. package/dist/route.test.mjs +0 -1664
  22. package/dist/router-back.test.d.ts +0 -1
  23. package/dist/router-back.test.mjs +0 -361
  24. package/dist/router-forward.test.d.ts +0 -1
  25. package/dist/router-forward.test.mjs +0 -376
  26. package/dist/router-go.test.d.ts +0 -1
  27. package/dist/router-go.test.mjs +0 -73
  28. package/dist/router-guards-cleanup.test.d.ts +0 -1
  29. package/dist/router-guards-cleanup.test.mjs +0 -437
  30. package/dist/router-push.test.d.ts +0 -1
  31. package/dist/router-push.test.mjs +0 -115
  32. package/dist/router-replace.test.d.ts +0 -1
  33. package/dist/router-replace.test.mjs +0 -114
  34. package/dist/router-resolve.test.d.ts +0 -1
  35. package/dist/router-resolve.test.mjs +0 -393
  36. package/dist/router-restart-app.dom.test.d.ts +0 -1
  37. package/dist/router-restart-app.dom.test.mjs +0 -616
  38. package/dist/router-window-navigation.test.d.ts +0 -1
  39. package/dist/router-window-navigation.test.mjs +0 -359
  40. package/dist/util.test.d.ts +0 -1
  41. package/dist/util.test.mjs +0 -1020
  42. package/src/index.test.ts +0 -9
  43. package/src/location.test.ts +0 -406
  44. package/src/matcher.test.ts +0 -1685
  45. package/src/micro-app.dom.test.ts +0 -708
  46. package/src/navigation.test.ts +0 -858
  47. package/src/route-task.test.ts +0 -901
  48. package/src/route-transition.test.ts +0 -178
  49. package/src/route.test.ts +0 -2014
  50. package/src/router-back.test.ts +0 -487
  51. package/src/router-forward.test.ts +0 -506
  52. package/src/router-go.test.ts +0 -91
  53. package/src/router-guards-cleanup.test.ts +0 -595
  54. package/src/router-push.test.ts +0 -140
  55. package/src/router-replace.test.ts +0 -139
  56. package/src/router-resolve.test.ts +0 -475
  57. package/src/router-restart-app.dom.test.ts +0 -783
  58. package/src/router-window-navigation.test.ts +0 -457
  59. package/src/util.test.ts +0 -1262
@@ -1,1685 +0,0 @@
1
- import { assert, describe, test } from 'vitest';
2
- import { createMatcher, joinPathname } from './matcher';
3
- import type { RouteConfirmHook } from './types';
4
-
5
- const BASE_URL = new URL('https://www.esmx.dev');
6
-
7
- describe('joinPathname', () => {
8
- type TestCase = {
9
- path: string;
10
- base?: string;
11
- expected: string;
12
- };
13
- type JoinPathnameTestCase = {
14
- description: string;
15
- cases: TestCase[] | (() => TestCase[]);
16
- };
17
-
18
- // biome-ignore format:
19
- const testCases: JoinPathnameTestCase[] = [
20
- {
21
- description: 'Basic path joining',
22
- cases: [
23
- { path: 'test', expected: '/test' },
24
- { path: '/test', expected: '/test' },
25
- { path: 'test/', expected: '/test' },
26
- { path: '/test/', expected: '/test' },
27
- ]
28
- },
29
- {
30
- description: 'Path joining with a base',
31
- cases: [
32
- { path: 'test', base: '/api', expected: '/api/test' },
33
- { path: '/test', base: '/api', expected: '/api/test' },
34
- { path: 'test', base: 'api', expected: '/api/test' },
35
- { path: '/test', base: 'api', expected: '/api/test' },
36
- ]
37
- },
38
- {
39
- description: 'Multi-level path joining',
40
- cases: [
41
- { path: 'test/path', expected: '/test/path' },
42
- { path: '/test/path', expected: '/test/path' },
43
- { path: 'test/path/', expected: '/test/path' },
44
- { path: '/test/path/', expected: '/test/path' },
45
- ]
46
- },
47
- {
48
- description: 'Multi-level path joining with a base',
49
- cases: [
50
- { path: 'test/path', base: '/api', expected: '/api/test/path' },
51
- { path: '/test/path', base: '/api', expected: '/api/test/path' },
52
- { path: 'test/path', base: 'api', expected: '/api/test/path' },
53
- { path: '/test/path', base: 'api', expected: '/api/test/path' },
54
- ]
55
- },
56
- {
57
- description: 'Handling duplicate slashes',
58
- cases: [
59
- { path: '//test', expected: '/test' },
60
- { path: 'test//path', expected: '/test/path' },
61
- { path: '//test//path//', expected: '/test/path' },
62
- { path: 'test//path', base: '/api//', expected: '/api/test/path' },
63
- ]
64
- },
65
- {
66
- description: 'Handling empty values',
67
- cases: [
68
- { path: '', expected: '/' },
69
- { path: '', base: '', expected: '/' },
70
- { path: 'test', base: '', expected: '/test' },
71
- { path: '', base: 'api', expected: '/api' },
72
- ]
73
- },
74
- {
75
- description: 'Paths with special characters',
76
- cases: [
77
- { path: 'test-path', expected: '/test-path' },
78
- { path: 'test_path', expected: '/test_path' },
79
- { path: 'test.path', expected: '/test.path' },
80
- { path: 'test:path', expected: '/test:path' },
81
- { path: 'test@path', expected: '/test@path' },
82
- ]
83
- },
84
- {
85
- description: 'Support for Chinese characters in paths',
86
- cases: [
87
- { path: '测试', expected: '/测试' },
88
- { path: '测试/路径', expected: '/测试/路径' },
89
- { path: '测试', base: '/api', expected: '/api/测试' },
90
- ]
91
- }
92
- ];
93
- // Test cases for various extreme edge cases
94
- // biome-ignore format:
95
- const edgeCases: JoinPathnameTestCase[] = [
96
- {
97
- description: 'Paths with only slashes or empty strings',
98
- cases: [
99
- { path: '', expected: '/' },
100
- { path: '/', expected: '/' },
101
- { path: '///', expected: '/' },
102
- { path: '/', base: '/', expected: '/' },
103
- { path: '/', base: '//', expected: '/' },
104
- { path: '//', base: '/', expected: '/' },
105
- { path: '//', base: '//', expected: '/' },
106
- ]
107
- },
108
- {
109
- description: 'Extremely long path joining',
110
- cases: () => {
111
- const longSegment =
112
- 'very-long-segment-name-that-could-cause-issues';
113
- const base = Array(10).fill(longSegment).join('/');
114
- const path = Array(10).fill(longSegment).join('/');
115
- const expected = `/${base}/${path}`;
116
- return [{ path, base, expected }];
117
- }
118
- },
119
- {
120
- description: 'Joining paths with special characters',
121
- cases: [
122
- { path: '测试路径', base: '基础', expected: '/基础/测试路径' },
123
- { path: 'path with spaces', base: 'base', expected: '/base/path with spaces' },
124
- { path: 'path-with-dashes', base: 'base_with_underscores', expected: '/base_with_underscores/path-with-dashes' },
125
- ]
126
- },
127
- {
128
- description: 'Handling URL-encoded characters',
129
- cases: [
130
- { path: 'hello%20world', base: 'api', expected: '/api/hello%20world' },
131
- { path: 'user%2Fprofile', base: 'v1', expected: '/v1/user%2Fprofile' },
132
- ]
133
- },
134
- {
135
- description: 'Handling dot segments in paths',
136
- cases: [
137
- { path: '.', expected: '/.' },
138
- { path: '..', expected: '/..' },
139
- { path: './relative', base: 'base', expected: '/base/./relative' },
140
- { path: '../parent', base: 'base', expected: '/base/../parent' },
141
- ]
142
- },
143
- {
144
- description: 'Query parameters and hash do not affect joining',
145
- cases: [
146
- { path: 'path?query=1', base: 'base', expected: '/base/path?query=1' },
147
- { path: 'path#hash', base: 'base', expected: '/base/path#hash' },
148
- { path: 'path?q=1#hash', base: 'base', expected: '/base/path?q=1#hash' },
149
- ]
150
- },
151
- {
152
- description: 'Paths starting with a colon (route parameters)',
153
- cases: [
154
- { path: ':id', base: 'users', expected: '/users/:id' },
155
- { path: ':userId/profile', base: 'api', expected: '/api/:userId/profile' },
156
- ]
157
- },
158
- {
159
- description: 'Paths with wildcard asterisks',
160
- cases: [
161
- { path: ':rest*', base: 'files', expected: '/files/:rest*' },
162
- { path: ':rest*', base: 'assets', expected: '/assets/:rest*' },
163
- { path: 'images/:rest*', base: 'static', expected: '/static/images/:rest*' },
164
- { path: '/*splat', base: 'base', expected: '/base/*splat' },
165
- ]
166
- },
167
- {
168
- description: 'Optional paths',
169
- cases: [
170
- { path: ':id?', base: 'posts', expected: '/posts/:id?' },
171
- { path: 'comments/:commentId?', base: 'articles', expected: '/articles/comments/:commentId?' },
172
- { path: '/users{/:id}/delete?', base: 'base', expected: '/base/users{/:id}/delete?' },
173
- ]
174
- },
175
- {
176
- description: 'Combination of numbers and special symbols',
177
- cases: [
178
- { path: 'v1.2.3', base: 'api', expected: '/api/v1.2.3' },
179
- { path: 'user@domain', base: 'profile', expected: '/profile/user@domain' },
180
- { path: 'item_123', base: 'products', expected: '/products/item_123' },
181
- ]
182
- },
183
- {
184
- description: 'Whitespace character handling',
185
- cases: [
186
- { path: ' path ', base: ' base ', expected: '/ base / path ' },
187
- { path: '\tpath\t', base: '\tbase\t', expected: '/\tbase\t/\tpath\t' },
188
- ]
189
- },
190
- {
191
- description: 'Boolean and numeric paths (boundary test)',
192
- cases: [
193
- { path: 'true', base: 'false', expected: '/false/true' },
194
- { path: '0', base: '1', expected: '/1/0' },
195
- { path: 'NaN', base: 'undefined', expected: '/undefined/NaN' },
196
- ]
197
- },
198
- {
199
- description: 'Extreme cases of path normalization',
200
- cases: [
201
- // Test normalization of multiple slashes
202
- { path: '///path///', base: '///base///', expected: '/base/path' },
203
- { path: 'path////with////slashes', base: 'base////with////slashes', expected: '/base/with/slashes/path/with/slashes' },
204
- ]
205
- },
206
- {
207
- description: 'Handling of non-ASCII character paths',
208
- cases: [
209
- { path: 'путь', base: 'база', expected: '/база/путь' }, // Russian
210
- { path: 'パス', base: 'ベース', expected: '/ベース/パス' }, // Japanese
211
- { path: '경로', base: '기본', expected: '/기본/경로' }, // Korean
212
- { path: 'مسار', base: 'قاعدة', expected: '/قاعدة/مسار' }, // Arabic
213
- ]
214
- },
215
- {
216
- description: 'Handling of special symbols and punctuation',
217
- cases: [
218
- { path: 'path!@#$%^&\\*()', base: 'base!@#$%^&\\*()', expected: '/base!@#$%^&\\*()/path!@#$%^&\\*()' },
219
- { path: 'path\\[]{};:"\'<>\\?', base: 'base\\[]{};:"\'<>\\?', expected: '/base\\[]{};:"\'<>\\?/path\\[]{};:"\'<>\\?' },
220
- { path: 'path\\backslash', base: 'base\\backslash\\', expected: '/base\\backslash\\/path\\backslash' },
221
- ]
222
- },
223
- {
224
- description: 'Paths with combinations of numbers and symbols',
225
- cases: [
226
- { path: '123.456.789', base: 'v1.0.0', expected: '/v1.0.0/123.456.789' },
227
- { path: 'item-123_abc', base: 'category-456_def', expected: '/category-456_def/item-123_abc' },
228
- { path: '2023-12-31', base: '2024-01-01', expected: '/2024-01-01/2023-12-31' },
229
- ]
230
- },
231
- {
232
- description: 'Various forms of whitespace characters',
233
- cases: [
234
- { path: ' ', base: ' ', expected: '/ / ' },
235
- { path: '\n', base: '\t', expected: '/\t/\n' },
236
- { path: '\r\n', base: '\t\r', expected: '/\t\r/\r\n' }, // Test carriage return and line feed
237
- { path: '\u00A0', base: '\u2000', expected: '/\u2000/\u00A0' }, // Non-breaking space and em space
238
- ]
239
- },
240
- {
241
- description: 'Handling of very long paths',
242
- cases: () => {
243
- const veryLongSegment = 'a'.repeat(1000);
244
- const path = veryLongSegment + '/segment';
245
- const base = 'base/' + veryLongSegment;
246
- const expected = '/' + base + '/' + path;
247
- return [{ path, base, expected }];
248
- }
249
- },
250
- {
251
- description: 'Boundary cases for path separators',
252
- cases: [
253
- { path: '/', base: '/', expected: '/' },
254
- { path: '//', base: '//', expected: '/' },
255
- { path: '///', base: '///', expected: '/' },
256
- { path: 'path/', base: '/base', expected: '/base/path' },
257
- { path: '/path/', base: '/base/', expected: '/base/path' },
258
- ]
259
- },
260
- {
261
- description: 'URL-encoded path segments',
262
- cases: [
263
- { path: '%20space%20', base: '%20base%20', expected: '/%20base%20/%20space%20' },
264
- { path: '%2F%2F', base: '%2F', expected: '/%2F/%2F%2F' },
265
- { path: 'path%3Fquery%3D1', base: 'base%23hash', expected: '/base%23hash/path%3Fquery%3D1' },
266
- ]
267
- },
268
- {
269
- description: 'Numeric type paths (type boundary)',
270
- cases: [
271
- { path: '123', base: '456', expected: '/456/123' },
272
- { path: '0', expected: '/0' },
273
- { path: '', base: '0', expected: '/0' },
274
- ]
275
- },
276
- {
277
- description: 'Complex cases with dot notation in paths',
278
- cases: [
279
- { path: '../../../path', base: '../../base', expected: '/../../base/../../../path' },
280
- { path: './././path', base: './././base', expected: '/./././base/./././path' },
281
- { path: 'path/./file', base: 'base/../dir', expected: '/base/../dir/path/./file' },
282
- ]
283
- },
284
- {
285
- description: 'Paths with mixed character sets',
286
- cases: [
287
- { path: '中文/english/русский', base: '日本語/العربية', expected: '/日本語/العربية/中文/english/русский' },
288
- { path: '测试-test-тест', base: '基础-base-база', expected: '/基础-base-база/测试-test-тест' },
289
- ]
290
- },
291
- {
292
- description: 'Handling of control characters',
293
- cases: [
294
- // Test control characters (though uncommon in actual URLs)
295
- { path: '\u0001\u0002', base: '\u0003\u0004', expected: '/\u0003\u0004/\u0001\u0002' },
296
- { path: 'path\u007F', base: 'base\u007F', expected: '/base\u007F/path\u007F' },
297
- ]
298
- },
299
- {
300
- description: 'Various characters at the end of a path',
301
- cases: [
302
- { path: 'path.', base: 'base.', expected: '/base./path.' },
303
- { path: 'path-', base: 'base-', expected: '/base-/path-' },
304
- { path: 'path_', base: 'base_', expected: '/base_/path_' },
305
- { path: 'path~', base: 'base~', expected: '/base~/path~' },
306
- ]
307
- }
308
- ];
309
-
310
- const runTests = (testCases: JoinPathnameTestCase[]) =>
311
- testCases.forEach(({ description, cases }) => {
312
- if (typeof cases === 'function') {
313
- cases = cases();
314
- }
315
- test(description, () => {
316
- for (const { path, base, expected } of cases) {
317
- assert.equal(joinPathname(path, base), expected);
318
- }
319
- });
320
- });
321
-
322
- runTests(testCases);
323
- describe('Edge Cases', () => runTests(edgeCases));
324
- });
325
-
326
- describe('createMatcher', () => {
327
- test('Basic route matching', () => {
328
- const matcher = createMatcher([
329
- { path: '/news' },
330
- { path: '/news/:id' }
331
- ]);
332
- const result = matcher(new URL('/news/123', BASE_URL), BASE_URL);
333
- assert.equal(result.matches.length, 1);
334
- assert.equal(result.matches[0].path, '/news/:id');
335
- assert.equal(result.params.id, '123');
336
- });
337
-
338
- test('Exact route matching priority', () => {
339
- const matcher = createMatcher([
340
- { path: '/news/:id' },
341
- { path: '/news' }
342
- ]);
343
- const result = matcher(new URL('/news', BASE_URL), BASE_URL);
344
- assert.equal(result.matches.length, 1);
345
- assert.equal(result.matches[0].path, '/news');
346
- assert.deepEqual(result.params, {});
347
- });
348
-
349
- test('Nested route matching', () => {
350
- const matcher = createMatcher([
351
- {
352
- path: '/news',
353
- children: [{ path: ':id' }]
354
- }
355
- ]);
356
- const result = matcher(new URL('/news/123', BASE_URL), BASE_URL);
357
- assert.equal(result.matches.length, 2);
358
- assert.equal(result.matches[0].path, '/news');
359
- assert.equal(result.matches[1].path, ':id');
360
- assert.equal(result.params.id, '123');
361
- });
362
-
363
- test('Deeply nested route matching', () => {
364
- const matcher = createMatcher([
365
- {
366
- path: '/user',
367
- children: [
368
- {
369
- path: ':userId',
370
- children: [{ path: 'profile' }, { path: 'settings' }]
371
- }
372
- ]
373
- }
374
- ]);
375
- const result = matcher(
376
- new URL('/user/123/profile', BASE_URL),
377
- BASE_URL
378
- );
379
- assert.equal(result.matches.length, 3);
380
- assert.equal(result.matches[0].path, '/user');
381
- assert.equal(result.matches[1].path, ':userId');
382
- assert.equal(result.matches[2].path, 'profile');
383
- assert.equal(result.params.userId, '123');
384
- });
385
-
386
- test('Multiple parameter route matching', () => {
387
- const matcher = createMatcher([{ path: '/user/:userId/post/:postId' }]);
388
- const result = matcher(
389
- new URL('/user/123/post/456', BASE_URL),
390
- BASE_URL
391
- );
392
- assert.equal(result.matches.length, 1);
393
- assert.equal(result.matches[0].path, '/user/:userId/post/:postId');
394
- assert.equal(result.params.userId, '123');
395
- assert.equal(result.params.postId, '456');
396
- });
397
-
398
- test('Optional parameter route matching', () => {
399
- const matcher = createMatcher([{ path: '/posts/:id?' }]);
400
-
401
- // Match with parameter
402
- const resultWithParam = matcher(
403
- new URL('/posts/123', BASE_URL),
404
- BASE_URL
405
- );
406
- assert.equal(resultWithParam.matches.length, 1);
407
- assert.equal(resultWithParam.params.id, '123');
408
-
409
- // Match without parameter
410
- const resultWithoutParam = matcher(
411
- new URL('/posts', BASE_URL),
412
- BASE_URL
413
- );
414
- assert.equal(resultWithoutParam.matches.length, 1);
415
- assert.equal(resultWithoutParam.params.id, undefined);
416
- });
417
-
418
- test('Numeric parameter route matching', () => {
419
- const matcher = createMatcher([{ path: '/posts/:id(\\d+)' }]);
420
-
421
- // Match numeric parameter
422
- const resultWithParam = matcher(
423
- new URL('/posts/123', BASE_URL),
424
- BASE_URL
425
- );
426
- assert.equal(resultWithParam.matches.length, 1);
427
- assert.equal(resultWithParam.params.id, '123');
428
-
429
- // Match non-numeric parameter
430
- const resultWithoutParam = matcher(
431
- new URL('/posts/123a', BASE_URL),
432
- BASE_URL
433
- );
434
- assert.equal(resultWithoutParam.matches.length, 0);
435
-
436
- // Match NaN parameter
437
- const resultWithNaN = matcher(
438
- new URL('/posts/NaN', BASE_URL),
439
- BASE_URL
440
- );
441
- assert.equal(resultWithNaN.matches.length, 0);
442
- });
443
-
444
- test('Wildcard route matching', () => {
445
- const matcher = createMatcher([{ path: '/files/:rest*' }]);
446
- const result = matcher(
447
- new URL('/files/documents/readme.txt', BASE_URL),
448
- BASE_URL
449
- );
450
- assert.equal(result.matches.length, 1);
451
- assert.equal(result.matches[0].path, '/files/:rest*');
452
- assert.deepEqual(result.params.rest, ['documents', 'readme.txt']);
453
- });
454
-
455
- test('RegExp parameter matching', () => {
456
- const matcher = createMatcher([{ path: '/api/v:version(\\d+)' }]);
457
- const result = matcher(new URL('/api/v1', BASE_URL), BASE_URL);
458
- assert.equal(result.matches.length, 1);
459
- assert.equal(result.params.version, '1');
460
- });
461
-
462
- test('No matching route', () => {
463
- const matcher = createMatcher([{ path: '/news' }]);
464
- const result = matcher(new URL('/blog', BASE_URL), BASE_URL);
465
- assert.equal(result.matches.length, 0);
466
- assert.deepEqual(result.params, {});
467
- });
468
-
469
- test('Empty route configuration', () => {
470
- const matcher = createMatcher([]);
471
- const result = matcher(new URL('/any', BASE_URL), BASE_URL);
472
- assert.equal(result.matches.length, 0);
473
- assert.deepEqual(result.params, {});
474
- });
475
-
476
- test('Route meta information passing', () => {
477
- const matcher = createMatcher([
478
- {
479
- path: '/protected',
480
- meta: { requiresAuth: true }
481
- }
482
- ]);
483
- const result = matcher(new URL('/protected', BASE_URL), BASE_URL);
484
- assert.equal(result.matches.length, 1);
485
- assert.equal(result.matches[0]?.meta?.requiresAuth, true);
486
- });
487
-
488
- test('Complex nested routes with parameters', () => {
489
- const matcher = createMatcher([
490
- {
491
- path: '/admin',
492
- meta: { role: 'admin' },
493
- children: [
494
- {
495
- path: 'users',
496
- children: [
497
- {
498
- path: ':userId',
499
- children: [{ path: 'edit' }]
500
- }
501
- ]
502
- }
503
- ]
504
- }
505
- ]);
506
- const result = matcher(
507
- new URL('/admin/users/123/edit', BASE_URL),
508
- BASE_URL
509
- );
510
- assert.equal(result.matches.length, 4);
511
- assert.equal(result.matches[0].path, '/admin');
512
- assert.equal(result.matches[1].path, 'users');
513
- assert.equal(result.matches[2].path, ':userId');
514
- assert.equal(result.matches[3].path, 'edit');
515
- assert.equal(result.params.userId, '123');
516
- assert.equal(result.matches[0]?.meta?.role, 'admin');
517
- });
518
-
519
- test('baseURL with directory', () => {
520
- const matcher = createMatcher([{ path: '/api' }]);
521
- const customBaseURL = new URL('https://www.esmx.dev/app/');
522
- const result = matcher(
523
- new URL('https://www.esmx.dev/app/api'),
524
- customBaseURL
525
- );
526
- assert.equal(result.matches.length, 1);
527
- assert.equal(result.matches[0].path, '/api');
528
- });
529
-
530
- test('URL-encoded parameter handling', () => {
531
- const matcher = createMatcher([{ path: '/search/:query' }]);
532
- const result = matcher(
533
- new URL('/search/hello world', BASE_URL),
534
- BASE_URL
535
- );
536
- assert.equal(result.matches.length, 1);
537
- // path-to-regexp会编码URL参数
538
- assert.equal(result.params.query, 'hello%20world');
539
- });
540
-
541
- test('Chinese path parameters', () => {
542
- const matcher = createMatcher([
543
- { path: `/${encodeURIComponent('分类')}/:name` }
544
- ]);
545
- const result = matcher(new URL('/分类/技术', BASE_URL), BASE_URL);
546
- assert.equal(result.matches.length, 1);
547
- assert.equal(result.params.name, encodeURIComponent('技术'));
548
- });
549
-
550
- test('Duplicate parameter name handling', () => {
551
- const matcher = createMatcher([
552
- {
553
- path: '/parent/:id',
554
- children: [{ path: 'child/:childId' }]
555
- }
556
- ]);
557
- const result = matcher(
558
- new URL('/parent/123/child/456', BASE_URL),
559
- BASE_URL
560
- );
561
- assert.equal(result.matches.length, 2);
562
- assert.equal(result.params.id, '123');
563
- assert.equal(result.params.childId, '456');
564
- });
565
-
566
- test.todo('Route matching order consistency', () => {
567
- const matcher = createMatcher([
568
- {
569
- path: '/a/:id',
570
- meta: { order: 1 }
571
- },
572
- {
573
- path: '/a/special',
574
- meta: { order: 2 }
575
- }
576
- ]);
577
-
578
- const result1 = matcher(new URL('/a/123', BASE_URL), BASE_URL);
579
- assert.equal(result1.matches.length, 1);
580
- assert.equal(result1.matches[0]?.meta?.order, 1);
581
-
582
- // Exact route should match
583
- const result2 = matcher(new URL('/a/special', BASE_URL), BASE_URL);
584
- assert.equal(result2.matches.length, 1);
585
- assert.equal(result2.matches[0]?.meta?.order, 2);
586
- });
587
-
588
- test('Special characters in path handling', () => {
589
- const routes = [
590
- { path: '/test-path' },
591
- { path: '/test_path' },
592
- { path: '/test.path' }
593
- ];
594
- const matcher = createMatcher(routes);
595
-
596
- for (const { path } of routes) {
597
- const result = matcher(new URL(path, BASE_URL), BASE_URL);
598
- assert.equal(result.matches.length, 1);
599
- assert.equal(result.matches[0].path, path);
600
- }
601
- });
602
-
603
- test('Empty string path handling', () => {
604
- const matcher = createMatcher([
605
- {
606
- path: '',
607
- children: [{ path: 'child' }]
608
- }
609
- ]);
610
- const result = matcher(new URL('/child', BASE_URL), BASE_URL);
611
- assert.equal(result.matches.length, 2);
612
- assert.equal(result.matches[0].path, '');
613
- assert.equal(result.matches[1].path, 'child');
614
- });
615
-
616
- test.todo('Route matching performance verification', () => {
617
- const routes = Array.from({ length: 1000 }, (_, i) => ({
618
- path: `/route${i}/:id`
619
- }));
620
- routes.push({ path: '/target/:id' });
621
-
622
- const matcher = createMatcher(routes);
623
- const startTime = performance.now();
624
- const result = matcher(new URL('/target/123', BASE_URL), BASE_URL);
625
- const endTime = performance.now();
626
-
627
- assert.equal(result.matches.length, 1);
628
- assert.equal(result.params.id, '123');
629
- assert.isTrue(endTime - startTime < 10);
630
- });
631
-
632
- test('Edge case: extremely long path', () => {
633
- const longPath =
634
- '/very/long/path/with/many/segments/that/goes/on/and/on/and/on';
635
- const matcher = createMatcher([{ path: longPath }]);
636
- const result = matcher(new URL(longPath, BASE_URL), BASE_URL);
637
- assert.equal(result.matches.length, 1);
638
- assert.equal(result.matches[0].path, longPath);
639
- });
640
-
641
- test('Edge case: large number of parameters', () => {
642
- const matcher = createMatcher([
643
- { path: '/:a/:b/:c/:d/:e/:f/:g/:h/:i/:j' }
644
- ]);
645
- const result = matcher(
646
- new URL('/1/2/3/4/5/6/7/8/9/10', BASE_URL),
647
- BASE_URL
648
- );
649
- assert.equal(result.matches.length, 1);
650
- assert.equal(result.params.a, '1');
651
- assert.equal(result.params.j, '10');
652
- assert.equal(Object.keys(result.params).length, 10);
653
- });
654
-
655
- test('Path rewriting and encoding', () => {
656
- const matcher = createMatcher([{ path: '/api/:resource' }]);
657
- const result = matcher(
658
- new URL('/api/user%2Fprofile', BASE_URL),
659
- BASE_URL
660
- );
661
- assert.equal(result.matches.length, 1);
662
- // URL-encoded slashes are not automatically decoded as path separators
663
- assert.equal(result.params.resource, 'user%2Fprofile');
664
- });
665
-
666
- test('Query parameters do not affect route matching', () => {
667
- const matcher = createMatcher([{ path: '/search' }]);
668
- const result = matcher(
669
- new URL('/search?q=test&page=1', BASE_URL),
670
- BASE_URL
671
- );
672
- assert.equal(result.matches.length, 1);
673
- assert.equal(result.matches[0].path, '/search');
674
- });
675
-
676
- test('Hash does not affect route matching', () => {
677
- const matcher = createMatcher([{ path: '/page' }]);
678
- const result = matcher(new URL('/page#section1', BASE_URL), BASE_URL);
679
- assert.equal(result.matches.length, 1);
680
- assert.equal(result.matches[0].path, '/page');
681
- });
682
-
683
- test.todo('Case-sensitive matching', () => {
684
- const matcher = createMatcher([{ path: '/API' }, { path: '/api' }]);
685
- const result1 = matcher(new URL('/API', BASE_URL), BASE_URL);
686
- const result2 = matcher(new URL('/api', BASE_URL), BASE_URL);
687
-
688
- assert.equal(result1.matches.length, 1);
689
- assert.equal(result1.matches[0].path, '/API');
690
-
691
- assert.equal(result2.matches.length, 1);
692
- assert.equal(result2.matches[0].path, '/api');
693
- });
694
-
695
- test('Username and password in baseURL should be ignored', () => {
696
- const customBase = new URL('https://uname@pwlocalhost:3000/app/');
697
- const matcher = createMatcher([{ path: '/test' }]);
698
- const result = matcher(
699
- new URL('https://uname2@pw2localhost:3000/app/test'),
700
- customBase
701
- );
702
- assert.equal(result.matches.length, 1);
703
- assert.equal(result.matches[0].path, '/test');
704
- });
705
-
706
- test('Empty string handling in nested routes', () => {
707
- const matcher = createMatcher([
708
- {
709
- path: '/parent',
710
- children: [{ path: '' }, { path: 'child' }]
711
- }
712
- ]);
713
-
714
- const result1 = matcher(new URL('/parent', BASE_URL), BASE_URL);
715
- assert.equal(result1.matches.length, 2);
716
- assert.equal(result1.matches[0].path, '/parent');
717
- assert.equal(result1.matches[1].path, '');
718
-
719
- const result2 = matcher(new URL('/parent/child', BASE_URL), BASE_URL);
720
- assert.equal(result2.matches.length, 2);
721
- assert.equal(result2.matches[0].path, '/parent');
722
- assert.equal(result2.matches[1].path, 'child');
723
- });
724
-
725
- test('Route component configuration persistence', () => {
726
- const TestComponent = () => 'test';
727
- const matcher = createMatcher([
728
- {
729
- path: '/component-test',
730
- component: TestComponent
731
- }
732
- ]);
733
- const result = matcher(new URL('/component-test', BASE_URL), BASE_URL);
734
- assert.equal(result.matches.length, 1);
735
- assert.equal(result.matches[0].component, TestComponent);
736
- });
737
-
738
- test('Route redirect configuration persistence', () => {
739
- const redirectTarget = '/new-path';
740
- const matcher = createMatcher([
741
- {
742
- path: '/old-path',
743
- redirect: redirectTarget
744
- }
745
- ]);
746
- const result = matcher(new URL('/old-path', BASE_URL), BASE_URL);
747
- assert.equal(result.matches.length, 1);
748
- assert.equal(result.matches[0].redirect, redirectTarget);
749
- });
750
-
751
- test('Numeric parameter parsing', () => {
752
- const matcher = createMatcher([{ path: '/user/:id(\\d+)' }]);
753
-
754
- const result1 = matcher(new URL('/user/123', BASE_URL), BASE_URL);
755
- assert.equal(result1.matches.length, 1);
756
- assert.equal(result1.params.id, '123');
757
-
758
- // Non-numeric should not match
759
- const result2 = matcher(new URL('/user/abc', BASE_URL), BASE_URL);
760
- assert.equal(result2.matches.length, 0);
761
- });
762
-
763
- test('Route matching depth-first strategy verification', () => {
764
- const matcher = createMatcher([
765
- {
766
- path: '/level1',
767
- meta: { level: 1 },
768
- children: [
769
- {
770
- path: 'level2',
771
- meta: { level: 2 },
772
- children: [
773
- {
774
- path: 'level3',
775
- meta: { level: 3 }
776
- }
777
- ]
778
- }
779
- ]
780
- }
781
- ]);
782
- const result = matcher(
783
- new URL('/level1/level2/level3', BASE_URL),
784
- BASE_URL
785
- );
786
- assert.equal(result.matches.length, 3);
787
- assert.equal(result.matches[0].meta?.level, 1);
788
- assert.equal(result.matches[1].meta?.level, 2);
789
- assert.equal(result.matches[2].meta?.level, 3);
790
- });
791
-
792
- test('Empty meta object default handling', () => {
793
- const matcher = createMatcher([{ path: '/no-meta' }]);
794
- const result = matcher(new URL('/no-meta', BASE_URL), BASE_URL);
795
- assert.equal(result.matches.length, 1);
796
- assert.isObject(result.matches[0].meta);
797
- assert.deepEqual(result.matches[0].meta, {});
798
- });
799
-
800
- test('Path normalization handling', () => {
801
- const matcher = createMatcher([{ path: '/test//double//slash' }]);
802
- const result = matcher(
803
- new URL('/test/double/slash', BASE_URL),
804
- BASE_URL
805
- );
806
- assert.equal(result.matches.length, 1);
807
- });
808
-
809
- test('Error path configuration handling', () => {
810
- const matcher1 = createMatcher([{ path: '' }]);
811
- const result1 = matcher1(new URL('/', BASE_URL), BASE_URL);
812
- assert.equal(result1.matches.length, 1);
813
-
814
- const matcher2 = createMatcher([{ path: '/' }]);
815
- const result2 = matcher2(new URL('/', BASE_URL), BASE_URL);
816
- assert.equal(result2.matches.length, 1);
817
- });
818
-
819
- test('Empty parameter handling', () => {
820
- const matcher = createMatcher([{ path: '/user/:id' }]);
821
- const result = matcher(new URL('/user/', BASE_URL), BASE_URL);
822
- assert.equal(result.matches.length, 0); // Should not match
823
- });
824
-
825
- test('Route configuration completeness verification', () => {
826
- const TestComponent = () => 'test';
827
- const asyncComponent = async () => TestComponent;
828
- const beforeEnter = async (to: any, from: any, router: any) => void 0; // Correct RouteConfirmHookResult type
829
-
830
- const matcher = createMatcher([
831
- {
832
- path: '/complete',
833
- component: TestComponent,
834
- asyncComponent,
835
- beforeEnter,
836
- meta: {
837
- title: 'Complete Route',
838
- requiresAuth: true,
839
- permissions: ['read', 'write']
840
- },
841
- children: [
842
- {
843
- path: 'child',
844
- component: TestComponent
845
- }
846
- ]
847
- }
848
- ]);
849
-
850
- const result = matcher(new URL('/complete', BASE_URL), BASE_URL);
851
- assert.equal(result.matches.length, 1);
852
- assert.equal(result.matches[0].component, TestComponent);
853
- assert.equal(result.matches[0].asyncComponent, asyncComponent);
854
- assert.equal(result.matches[0].beforeEnter, beforeEnter);
855
- assert.equal(result.matches[0]?.meta?.title, 'Complete Route');
856
- assert.equal(result.matches[0]?.meta?.requiresAuth, true);
857
- assert.deepEqual(result.matches[0]?.meta?.permissions, [
858
- 'read',
859
- 'write'
860
- ]);
861
- assert.equal(result.matches[0].children.length, 1);
862
- });
863
-
864
- test.todo('Route conflict and priority handling', () => {
865
- const matcher = createMatcher([
866
- {
867
- path: '/conflict/:id',
868
- meta: { priority: 1 }
869
- },
870
- {
871
- path: '/conflict/special',
872
- meta: { priority: 2 }
873
- },
874
- {
875
- path: '/conflict/:rest*',
876
- meta: { priority: 3 }
877
- }
878
- ]);
879
-
880
- const result1 = matcher(
881
- new URL('/conflict/special', BASE_URL),
882
- BASE_URL
883
- );
884
- assert.equal(result1.matches.length, 1);
885
- assert.equal(result1.matches[0]?.meta?.priority, 2);
886
-
887
- const result2 = matcher(new URL('/conflict/123', BASE_URL), BASE_URL);
888
- assert.equal(result2.matches.length, 1);
889
- assert.equal(result2.matches[0]?.meta?.priority, 1);
890
- assert.equal(result2.params.id, '123');
891
- });
892
-
893
- test('Multi-level nested parameter extraction', () => {
894
- const matcher = createMatcher([
895
- {
896
- path: '/api',
897
- children: [
898
- {
899
- path: 'v:version',
900
- children: [
901
- {
902
- path: ':resource',
903
- children: [
904
- {
905
- path: ':id',
906
- children: [{ path: ':action' }]
907
- }
908
- ]
909
- }
910
- ]
911
- }
912
- ]
913
- }
914
- ]);
915
-
916
- const result = matcher(
917
- new URL('/api/v1/users/123/edit', BASE_URL),
918
- BASE_URL
919
- );
920
- assert.equal(result.matches.length, 5);
921
- assert.equal(result.params.version, '1');
922
- assert.equal(result.params.resource, 'users');
923
- assert.equal(result.params.id, '123');
924
- assert.equal(result.params.action, 'edit');
925
- });
926
-
927
- test('Route override configuration handling', () => {
928
- // Using proper types instead of any
929
- const overrideHandler: RouteConfirmHook = (to, from, router) => {
930
- return async (toRoute, fromRoute, routerInstance) => ({
931
- data: 'test'
932
- });
933
- };
934
- const matcher = createMatcher([
935
- {
936
- path: '/override-test',
937
- override: overrideHandler,
938
- meta: { type: 'hybrid' }
939
- }
940
- ]);
941
-
942
- const result = matcher(new URL('/override-test', BASE_URL), BASE_URL);
943
- assert.equal(result.matches.length, 1);
944
- assert.equal(result.matches[0].override, overrideHandler);
945
- assert.equal(result.matches[0]?.meta?.type, 'hybrid');
946
- });
947
-
948
- test('Application configuration handling', () => {
949
- const appConfig = 'test-app';
950
- const appCallback = () => ({ mount: () => {}, unmount: () => {} });
951
-
952
- const matcher = createMatcher([
953
- { path: '/app1', app: appConfig },
954
- { path: '/app2', app: appCallback }
955
- ]);
956
-
957
- const result1 = matcher(new URL('/app1', BASE_URL), BASE_URL);
958
- assert.equal(result1.matches.length, 1);
959
- assert.equal(result1.matches[0].app, appConfig);
960
-
961
- const result2 = matcher(new URL('/app2', BASE_URL), BASE_URL);
962
- assert.equal(result2.matches.length, 1);
963
- assert.equal(result2.matches[0].app, appCallback);
964
- });
965
-
966
- test('Complex wildcard and parameter combinations', () => {
967
- const matcher = createMatcher([{ path: '/files/:category/:rest*' }]);
968
- const result = matcher(
969
- new URL('/files/documents/folder1/folder2/view', BASE_URL),
970
- BASE_URL
971
- );
972
- assert.equal(result.matches.length, 1);
973
- assert.equal(result.params.category, 'documents');
974
- assert.deepEqual(result.params.rest, ['folder1', 'folder2', 'view']);
975
- });
976
-
977
- test('Route redirect configuration verification', () => {
978
- const redirectTarget = '/new-location';
979
- const redirectFunction = () => '/dynamic-location';
980
-
981
- const matcher = createMatcher([
982
- {
983
- path: '/redirect-string',
984
- redirect: redirectTarget
985
- },
986
- {
987
- path: '/redirect-function',
988
- redirect: redirectFunction
989
- }
990
- ]);
991
-
992
- const result1 = matcher(
993
- new URL('/redirect-string', BASE_URL),
994
- BASE_URL
995
- );
996
- assert.equal(result1.matches.length, 1);
997
- assert.equal(result1.matches[0].redirect, redirectTarget);
998
-
999
- const result2 = matcher(
1000
- new URL('/redirect-function', BASE_URL),
1001
- BASE_URL
1002
- );
1003
- assert.equal(result2.matches.length, 1);
1004
- assert.equal(result2.matches[0].redirect, redirectFunction);
1005
- });
1006
-
1007
- test('Route guard configuration verification', () => {
1008
- const beforeEnter = async (to: any, from: any, router: any) => void 0; // Correct RouteConfirmHookResult type
1009
- const beforeUpdate = async (to: any, from: any, router: any) => void 0; // Correct void type
1010
- const beforeLeave = async (to: any, from: any, router: any) =>
1011
- '/cancel';
1012
-
1013
- const matcher = createMatcher([
1014
- {
1015
- path: '/guarded',
1016
- beforeEnter,
1017
- beforeUpdate,
1018
- beforeLeave,
1019
- meta: { protected: true }
1020
- }
1021
- ]);
1022
-
1023
- const result = matcher(new URL('/guarded', BASE_URL), BASE_URL);
1024
- assert.equal(result.matches.length, 1);
1025
- assert.equal(result.matches[0].beforeEnter, beforeEnter);
1026
- assert.equal(result.matches[0].beforeUpdate, beforeUpdate);
1027
- assert.equal(result.matches[0].beforeLeave, beforeLeave);
1028
- assert.equal(result.matches[0]?.meta?.protected, true);
1029
- });
1030
-
1031
- test.todo('matcher performance boundary test', () => {
1032
- const routes: Parameters<typeof createMatcher>[0] = [];
1033
- for (let i = 0; i < 500; i++) {
1034
- routes.push({
1035
- path: `/category${i}/:id`,
1036
- children: [
1037
- {
1038
- path: 'subcategory/:subId'
1039
- }
1040
- ]
1041
- });
1042
- }
1043
-
1044
- const matcher = createMatcher(routes);
1045
- const startTime = performance.now();
1046
-
1047
- const result = matcher(new URL('/nonexistent', BASE_URL), BASE_URL);
1048
-
1049
- const endTime = performance.now();
1050
-
1051
- assert.equal(result.matches.length, 0);
1052
- // Even if there is no match, performance should be within a reasonable range
1053
- assert.isTrue(endTime - startTime < 50);
1054
- });
1055
-
1056
- test('params type and value verification', () => {
1057
- const matcher = createMatcher([
1058
- { path: '/typed/:stringParam/:numberParam(\\d+)/:optionalParam?' }
1059
- ]);
1060
-
1061
- const result = matcher(
1062
- new URL('/typed/hello/123/extra', BASE_URL),
1063
- BASE_URL
1064
- );
1065
- assert.equal(result.matches.length, 1);
1066
- assert.equal(typeof result.params.stringParam, 'string');
1067
- assert.equal(result.params.stringParam, 'hello');
1068
- assert.equal(typeof result.params.numberParam, 'string'); // path-to-regexp总是返回字符串
1069
- assert.equal(result.params.numberParam, '123');
1070
- assert.equal(result.params.optionalParam, 'extra');
1071
- });
1072
-
1073
- test('Special URL encoding scenarios', () => {
1074
- const matcher = createMatcher([{ path: '/encoded/:param' }]);
1075
-
1076
- const testCases = [
1077
- { input: '/encoded/hello%20world', expected: 'hello%20world' },
1078
- {
1079
- input: '/encoded/%E4%B8%AD%E6%96%87',
1080
- expected: '%E4%B8%AD%E6%96%87'
1081
- },
1082
- {
1083
- input: '/encoded/user%40domain.com',
1084
- expected: 'user%40domain.com'
1085
- },
1086
- { input: '/encoded/path%2Fto%2Ffile', expected: 'path%2Fto%2Ffile' }
1087
- ];
1088
-
1089
- testCases.forEach(({ input, expected }) => {
1090
- const result = matcher(new URL(input, BASE_URL), BASE_URL);
1091
- assert.equal(result.matches.length, 1);
1092
- assert.equal(result.params.param, expected);
1093
- });
1094
- });
1095
-
1096
- test('Error configuration tolerance handling', () => {
1097
- const emptyMatcher = createMatcher([]);
1098
- const emptyResult = emptyMatcher(new URL('/any', BASE_URL), BASE_URL);
1099
- assert.equal(emptyResult.matches.length, 0);
1100
- assert.deepEqual(emptyResult.params, {});
1101
-
1102
- // Test configuration with undefined path
1103
- const matcher = createMatcher([
1104
- { path: '/valid' },
1105
- ...(process.env.NODE_ENV === 'test' ? [] : [])
1106
- ]);
1107
-
1108
- const result = matcher(new URL('/valid', BASE_URL), BASE_URL);
1109
- assert.equal(result.matches.length, 1);
1110
- });
1111
-
1112
- test('Wildcard route matching - optional wildcard', () => {
1113
- const routes = [
1114
- { path: '/files/:path*', component: 'FilesPage' },
1115
- { path: '/api/:section/data', component: 'ApiDataPage' },
1116
- { path: '/:rest*', component: 'CatchAllPage' }
1117
- ];
1118
- const matcher = createMatcher(routes);
1119
-
1120
- let result = matcher(
1121
- new URL('/files/document.pdf', BASE_URL),
1122
- BASE_URL
1123
- );
1124
- assert.equal(result.matches.length, 1);
1125
- assert.equal(result.matches[0].component, 'FilesPage');
1126
- assert.deepEqual(result.params.path, ['document.pdf']);
1127
-
1128
- result = matcher(
1129
- new URL('/files/images/photo.jpg', BASE_URL),
1130
- BASE_URL
1131
- );
1132
- assert.equal(result.matches.length, 1);
1133
- assert.equal(result.matches[0].component, 'FilesPage');
1134
- assert.deepEqual(result.params.path, ['images', 'photo.jpg']);
1135
-
1136
- result = matcher(new URL('/files/', BASE_URL), BASE_URL);
1137
- assert.equal(result.matches.length, 1);
1138
- assert.equal(result.matches[0].component, 'FilesPage');
1139
- assert.equal(result.params.path, void 0);
1140
-
1141
- result = matcher(new URL('/files', BASE_URL), BASE_URL);
1142
- assert.equal(result.matches.length, 1);
1143
- assert.equal(result.matches[0].component, 'FilesPage');
1144
- assert.equal(result.params.path, void 0);
1145
-
1146
- result = matcher(new URL('/api/v1/data', BASE_URL), BASE_URL);
1147
- assert.equal(result.matches.length, 1);
1148
- assert.equal(result.matches[0].component, 'ApiDataPage');
1149
- assert.equal(result.params.section, 'v1');
1150
-
1151
- result = matcher(new URL('/anything/else', BASE_URL), BASE_URL);
1152
- assert.equal(result.matches.length, 1);
1153
- assert.equal(result.matches[0].component, 'CatchAllPage');
1154
- assert.deepEqual(result.params.rest, ['anything', 'else']);
1155
- });
1156
-
1157
- test('Repeatable parameter route matching - + modifier', () => {
1158
- const routes = [
1159
- { path: '/chapters/:chapters+', component: 'ChaptersPage' },
1160
- {
1161
- path: '/categories/:categories+/items',
1162
- component: 'CategoriesItemsPage'
1163
- },
1164
- { path: '/tags/:tags+/posts/:postId', component: 'TaggedPostPage' }
1165
- ];
1166
- const matcher = createMatcher(routes);
1167
-
1168
- let result = matcher(new URL('/chapters/intro', BASE_URL), BASE_URL);
1169
- assert.equal(result.matches.length, 1);
1170
- assert.equal(result.matches[0].component, 'ChaptersPage');
1171
- assert.deepEqual(result.params.chapters, ['intro']);
1172
-
1173
- result = matcher(
1174
- new URL('/chapters/intro/basics/advanced', BASE_URL),
1175
- BASE_URL
1176
- );
1177
- assert.equal(result.matches.length, 1);
1178
- assert.equal(result.matches[0].component, 'ChaptersPage');
1179
- assert.deepEqual(result.params.chapters, [
1180
- 'intro',
1181
- 'basics',
1182
- 'advanced'
1183
- ]);
1184
-
1185
- result = matcher(
1186
- new URL('/categories/tech/programming/items', BASE_URL),
1187
- BASE_URL
1188
- );
1189
- assert.equal(result.matches.length, 1);
1190
- assert.equal(result.matches[0].component, 'CategoriesItemsPage');
1191
- assert.deepEqual(result.params.categories, ['tech', 'programming']);
1192
-
1193
- result = matcher(
1194
- new URL('/tags/react/typescript/hooks/posts/123', BASE_URL),
1195
- BASE_URL
1196
- );
1197
- assert.equal(result.matches.length, 1);
1198
- assert.equal(result.matches[0].component, 'TaggedPostPage');
1199
- assert.deepEqual(result.params.tags, ['react', 'typescript', 'hooks']);
1200
- assert.equal(result.params.postId, '123');
1201
- });
1202
-
1203
- test('Repeatable parameter route matching - * modifier', () => {
1204
- const routes = [
1205
- { path: '/path/:segments*', component: 'DynamicPathPage' },
1206
- { path: '/files/:path*/download', component: 'DownloadPage' }
1207
- ];
1208
- const matcher = createMatcher(routes);
1209
-
1210
- let result = matcher(new URL('/path', BASE_URL), BASE_URL);
1211
- assert.equal(result.matches.length, 1);
1212
- assert.equal(result.matches[0].component, 'DynamicPathPage');
1213
- assert.equal(result.params.segments, undefined);
1214
-
1215
- result = matcher(new URL('/path/a', BASE_URL), BASE_URL);
1216
- assert.equal(result.matches.length, 1);
1217
- assert.equal(result.matches[0].component, 'DynamicPathPage');
1218
- assert.equal(result.params.segments, 'a');
1219
-
1220
- result = matcher(new URL('/path/a/b/c/d', BASE_URL), BASE_URL);
1221
- assert.equal(result.matches.length, 1);
1222
- assert.equal(result.matches[0].component, 'DynamicPathPage');
1223
- assert.deepEqual(result.params.segments, ['a', 'b', 'c', 'd']);
1224
-
1225
- result = matcher(new URL('/files/download', BASE_URL), BASE_URL);
1226
- assert.equal(result.matches.length, 1);
1227
- assert.equal(result.matches[0].component, 'DownloadPage');
1228
- assert.equal(result.params.path, undefined);
1229
-
1230
- result = matcher(new URL('/files/a/download', BASE_URL), BASE_URL);
1231
- assert.equal(result.matches.length, 1);
1232
- assert.equal(result.matches[0].component, 'DownloadPage');
1233
- assert.equal(result.params.path, 'a');
1234
-
1235
- result = matcher(
1236
- new URL('/files/docs/images/download', BASE_URL),
1237
- BASE_URL
1238
- );
1239
- assert.equal(result.matches.length, 1);
1240
- assert.equal(result.matches[0].component, 'DownloadPage');
1241
- assert.deepEqual(result.params.path, ['docs', 'images']);
1242
- });
1243
-
1244
- test('Custom regular expression route matching', () => {
1245
- const routes = [
1246
- { path: '/order/:orderId(\\d+)', component: 'OrderPage' },
1247
- { path: '/user/:username([a-zA-Z0-9_]+)', component: 'UserPage' },
1248
- { path: '/product/:productName', component: 'ProductPage' },
1249
- { path: '/api/v:version(\\d+)', component: 'ApiPage' },
1250
- { path: '/hex/:color([0-9a-fA-F]{6})', component: 'ColorPage' }
1251
- ];
1252
- const matcher = createMatcher(routes);
1253
-
1254
- let result = matcher(new URL('/order/12345', BASE_URL), BASE_URL);
1255
- assert.equal(result.matches.length, 1);
1256
- assert.equal(result.matches[0].component, 'OrderPage');
1257
- assert.equal(result.params.orderId, '12345');
1258
-
1259
- result = matcher(new URL('/order/abc123', BASE_URL), BASE_URL);
1260
- assert.equal(result.matches.length, 0);
1261
-
1262
- result = matcher(new URL('/user/john_doe123', BASE_URL), BASE_URL);
1263
- assert.equal(result.matches.length, 1);
1264
- assert.equal(result.matches[0].component, 'UserPage');
1265
- assert.equal(result.params.username, 'john_doe123');
1266
-
1267
- result = matcher(new URL('/api/v2', BASE_URL), BASE_URL);
1268
- assert.equal(result.matches.length, 1);
1269
- assert.equal(result.matches[0].component, 'ApiPage');
1270
- assert.equal(result.params.version, '2');
1271
-
1272
- result = matcher(new URL('/hex/FF0000', BASE_URL), BASE_URL);
1273
- assert.equal(result.matches.length, 1);
1274
- assert.equal(result.matches[0].component, 'ColorPage');
1275
- assert.equal(result.params.color, 'FF0000');
1276
-
1277
- result = matcher(new URL('/hex/GGGGGG', BASE_URL), BASE_URL);
1278
- assert.equal(result.matches.length, 0);
1279
-
1280
- result = matcher(new URL('/product/laptop-pro', BASE_URL), BASE_URL);
1281
- assert.equal(result.matches.length, 1);
1282
- assert.equal(result.matches[0].component, 'ProductPage');
1283
- assert.equal(result.params.productName, 'laptop-pro');
1284
- });
1285
-
1286
- test('Repeatable parameter and custom regular expression route matching', () => {
1287
- const routes = [
1288
- { path: '/numbers/:nums(\\d+)+', component: 'NumbersPage' },
1289
- { path: '/codes/:codes([A-Z]{2,3})+', component: 'CodesPage' },
1290
- { path: '/optional/:items(\\d+)*', component: 'OptionalItemsPage' },
1291
- {
1292
- path: '/mixed/:ids(\\d+)+/info/:codes([A-Z]+)*',
1293
- component: 'MixedPage'
1294
- }
1295
- ];
1296
- const matcher = createMatcher(routes);
1297
-
1298
- let result = matcher(new URL('/numbers/123', BASE_URL), BASE_URL);
1299
- assert.equal(result.matches.length, 1);
1300
- assert.equal(result.matches[0].component, 'NumbersPage');
1301
- assert.deepEqual(result.params.nums, ['123']);
1302
-
1303
- result = matcher(new URL('/numbers/123/456/789', BASE_URL), BASE_URL);
1304
- assert.equal(result.matches.length, 1);
1305
- assert.equal(result.matches[0].component, 'NumbersPage');
1306
- assert.deepEqual(result.params.nums, ['123', '456', '789']);
1307
-
1308
- result = matcher(new URL('/codes/US/UK/CA', BASE_URL), BASE_URL);
1309
- assert.equal(result.matches.length, 1);
1310
- assert.equal(result.matches[0].component, 'CodesPage');
1311
- assert.deepEqual(result.params.codes, ['US', 'UK', 'CA']);
1312
-
1313
- result = matcher(new URL('/optional', BASE_URL), BASE_URL);
1314
- assert.equal(result.matches.length, 1);
1315
- assert.equal(result.matches[0].component, 'OptionalItemsPage');
1316
- assert.equal(result.params.items, undefined);
1317
-
1318
- result = matcher(new URL('/optional/100/200/300', BASE_URL), BASE_URL);
1319
- assert.equal(result.matches.length, 1);
1320
- assert.equal(result.matches[0].component, 'OptionalItemsPage');
1321
- assert.deepEqual(result.params.items, ['100', '200', '300']);
1322
-
1323
- result = matcher(
1324
- new URL('/mixed/111/222/info/ABC/DEF', BASE_URL),
1325
- BASE_URL
1326
- );
1327
- assert.equal(result.matches.length, 1);
1328
- assert.equal(result.matches[0].component, 'MixedPage');
1329
- assert.deepEqual(result.params.ids, ['111', '222']);
1330
- assert.deepEqual(result.params.codes, ['ABC', 'DEF']);
1331
-
1332
- result = matcher(new URL('/numbers/abc/123', BASE_URL), BASE_URL);
1333
- assert.equal(result.matches.length, 0);
1334
- });
1335
-
1336
- test('Optional parameter route matching - basic usage', () => {
1337
- const routes = [
1338
- { path: '/users/:userId?', component: 'UsersPage' },
1339
- { path: '/posts/:postId?/comments', component: 'CommentsPage' },
1340
- { path: '/search/:query?/:page?', component: 'SearchPage' },
1341
- {
1342
- path: '/profile/:section?/:subsection?',
1343
- component: 'ProfilePage'
1344
- }
1345
- ];
1346
- const matcher = createMatcher(routes);
1347
-
1348
- // Test no optional parameters
1349
- let result = matcher(new URL('/users', BASE_URL), BASE_URL);
1350
- assert.equal(result.matches.length, 1);
1351
- assert.equal(result.matches[0].component, 'UsersPage');
1352
- assert.equal(result.params.userId, undefined);
1353
-
1354
- result = matcher(new URL('/users/123', BASE_URL), BASE_URL);
1355
- assert.equal(result.matches.length, 1);
1356
- assert.equal(result.matches[0].component, 'UsersPage');
1357
- assert.equal(result.params.userId, '123');
1358
-
1359
- // Test intermediate optional parameters
1360
- result = matcher(new URL('/posts/comments', BASE_URL), BASE_URL);
1361
- assert.equal(result.matches.length, 1);
1362
- assert.equal(result.matches[0].component, 'CommentsPage');
1363
- assert.equal(result.params.postId, undefined);
1364
-
1365
- result = matcher(new URL('/posts/456/comments', BASE_URL), BASE_URL);
1366
- assert.equal(result.matches.length, 1);
1367
- assert.equal(result.matches[0].component, 'CommentsPage');
1368
- assert.equal(result.params.postId, '456');
1369
-
1370
- result = matcher(new URL('/search', BASE_URL), BASE_URL);
1371
- assert.equal(result.matches.length, 1);
1372
- assert.equal(result.matches[0].component, 'SearchPage');
1373
- assert.equal(result.params.query, undefined);
1374
- assert.equal(result.params.page, undefined);
1375
-
1376
- result = matcher(new URL('/search/react', BASE_URL), BASE_URL);
1377
- assert.equal(result.matches.length, 1);
1378
- assert.equal(result.matches[0].component, 'SearchPage');
1379
- assert.equal(result.params.query, 'react');
1380
- assert.equal(result.params.page, undefined);
1381
-
1382
- result = matcher(new URL('/search/react/2', BASE_URL), BASE_URL);
1383
- assert.equal(result.matches.length, 1);
1384
- assert.equal(result.matches[0].component, 'SearchPage');
1385
- assert.equal(result.params.query, 'react');
1386
- assert.equal(result.params.page, '2');
1387
- });
1388
-
1389
- test('Optional parameter and custom regular expression route matching', () => {
1390
- const routes = [
1391
- { path: '/users/:userId(\\d+)?', component: 'UsersPage' },
1392
- {
1393
- path: '/products/:category([a-z]+)?/:productId(\\d+)?',
1394
- component: 'ProductsPage'
1395
- },
1396
- {
1397
- path: '/articles/:year(\\d{4})?/:month(\\d{1,2})?/:slug?',
1398
- component: 'ArticlesPage'
1399
- },
1400
- {
1401
- path: '/api/:version(v\\d+)?/users/:userId(\\d+)?',
1402
- component: 'ApiUsersPage'
1403
- }
1404
- ];
1405
- const matcher = createMatcher(routes);
1406
-
1407
- let result = matcher(new URL('/users', BASE_URL), BASE_URL);
1408
- assert.equal(result.matches.length, 1);
1409
- assert.equal(result.matches[0].component, 'UsersPage');
1410
- assert.equal(result.params.userId, undefined);
1411
-
1412
- result = matcher(new URL('/users/123', BASE_URL), BASE_URL);
1413
- assert.equal(result.matches.length, 1);
1414
- assert.equal(result.matches[0].component, 'UsersPage');
1415
- assert.equal(result.params.userId, '123');
1416
-
1417
- result = matcher(new URL('/users/abc', BASE_URL), BASE_URL);
1418
- assert.equal(result.matches.length, 0);
1419
-
1420
- result = matcher(new URL('/products', BASE_URL), BASE_URL);
1421
- assert.equal(result.matches.length, 1);
1422
- assert.equal(result.matches[0].component, 'ProductsPage');
1423
- assert.equal(result.params.category, undefined);
1424
- assert.equal(result.params.productId, undefined);
1425
-
1426
- result = matcher(new URL('/products/electronics', BASE_URL), BASE_URL);
1427
- assert.equal(result.matches.length, 1);
1428
- assert.equal(result.matches[0].component, 'ProductsPage');
1429
- assert.equal(result.params.category, 'electronics');
1430
- assert.equal(result.params.productId, undefined);
1431
-
1432
- result = matcher(
1433
- new URL('/products/electronics/456', BASE_URL),
1434
- BASE_URL
1435
- );
1436
- assert.equal(result.matches.length, 1);
1437
- assert.equal(result.matches[0].component, 'ProductsPage');
1438
- assert.equal(result.params.category, 'electronics');
1439
- assert.equal(result.params.productId, '456');
1440
-
1441
- // Test article path (year/month/title)
1442
- result = matcher(new URL('/articles/2024', BASE_URL), BASE_URL);
1443
- assert.equal(result.matches.length, 1);
1444
- assert.equal(result.matches[0].component, 'ArticlesPage');
1445
- assert.equal(result.params.year, '2024');
1446
- assert.equal(result.params.month, undefined);
1447
- assert.equal(result.params.slug, undefined);
1448
-
1449
- result = matcher(
1450
- new URL('/articles/2024/03/my-post', BASE_URL),
1451
- BASE_URL
1452
- );
1453
- assert.equal(result.matches.length, 1);
1454
- assert.equal(result.matches[0].component, 'ArticlesPage');
1455
- assert.equal(result.params.year, '2024');
1456
- assert.equal(result.params.month, '03');
1457
- assert.equal(result.params.slug, 'my-post');
1458
-
1459
- // Test API versioned route
1460
- result = matcher(new URL('/api/v2/users/789', BASE_URL), BASE_URL);
1461
- assert.equal(result.matches.length, 1);
1462
- assert.equal(result.matches[0].component, 'ApiUsersPage');
1463
- assert.equal(result.params.version, 'v2');
1464
- assert.equal(result.params.userId, '789');
1465
-
1466
- result = matcher(new URL('/api/users', BASE_URL), BASE_URL);
1467
- assert.equal(result.matches.length, 1);
1468
- assert.equal(result.matches[0].component, 'ApiUsersPage');
1469
- assert.equal(result.params.version, undefined);
1470
- assert.equal(result.params.userId, undefined);
1471
- });
1472
-
1473
- test('Complex route pattern combination matching', () => {
1474
- const routes = [
1475
- {
1476
- path: '/api/v:version(\\d+)/users/:userId(\\d+)/posts/:postIds(\\d+)+',
1477
- component: 'UserPostsPage'
1478
- },
1479
- {
1480
- path: '/files/:folders([a-zA-Z0-9_-]+)*/download/:filename+',
1481
- component: 'FileDownloadPage'
1482
- },
1483
- {
1484
- path: '/shop/:categories([a-z]+)+/items/:itemId(\\d+)?/reviews/:reviewIds(\\d+)*',
1485
- component: 'ShopReviewsPage'
1486
- },
1487
- {
1488
- path: '/admin/users/:userIds(\\d+)+/roles/:roleNames([a-z]+)*',
1489
- component: 'AdminUserRolesPage'
1490
- }
1491
- ];
1492
- const matcher = createMatcher(routes);
1493
-
1494
- let result = matcher(
1495
- new URL('/api/v1/users/123/posts/456/789', BASE_URL),
1496
- BASE_URL
1497
- );
1498
- assert.equal(result.matches.length, 1);
1499
- assert.equal(result.matches[0].component, 'UserPostsPage');
1500
- assert.equal(result.params.version, '1');
1501
- assert.equal(result.params.userId, '123');
1502
- assert.deepEqual(result.params.postIds, ['456', '789']);
1503
-
1504
- // Test file download route
1505
- result = matcher(
1506
- new URL('/files/docs/images/download/photo.jpg', BASE_URL),
1507
- BASE_URL
1508
- );
1509
- assert.equal(result.matches.length, 1);
1510
- assert.equal(result.matches[0].component, 'FileDownloadPage');
1511
- assert.deepEqual(result.params.folders, ['docs', 'images']);
1512
- assert.deepEqual(result.params.filename, ['photo.jpg']);
1513
-
1514
- // Test download without folder
1515
- result = matcher(
1516
- new URL('/files/download/readme.txt', BASE_URL),
1517
- BASE_URL
1518
- );
1519
- assert.equal(result.matches.length, 1);
1520
- assert.equal(result.matches[0].component, 'FileDownloadPage');
1521
- assert.equal(result.params.folders, undefined);
1522
- assert.deepEqual(result.params.filename, ['readme.txt']);
1523
-
1524
- // Test store comment route
1525
- result = matcher(
1526
- new URL('/shop/electronics/computers/items/123/reviews/', BASE_URL),
1527
- BASE_URL
1528
- );
1529
- assert.equal(result.matches.length, 1);
1530
- assert.equal(result.matches[0].component, 'ShopReviewsPage');
1531
- assert.deepEqual(result.params.categories, [
1532
- 'electronics',
1533
- 'computers'
1534
- ]);
1535
- assert.equal(result.params.itemId, '123');
1536
- assert.equal(result.params.reviewIds, undefined);
1537
-
1538
- result = matcher(
1539
- new URL('/shop/books/items/reviews/101/102', BASE_URL),
1540
- BASE_URL
1541
- );
1542
- assert.equal(result.matches.length, 1);
1543
- assert.equal(result.matches[0].component, 'ShopReviewsPage');
1544
- assert.deepEqual(result.params.categories, ['books']);
1545
- assert.equal(result.params.itemId, undefined);
1546
- assert.deepEqual(result.params.reviewIds, ['101', '102']);
1547
-
1548
- // Test admin user role route
1549
- result = matcher(
1550
- new URL('/admin/users/100/200/300/roles/admin/moderator', BASE_URL),
1551
- BASE_URL
1552
- );
1553
- assert.equal(result.matches.length, 1);
1554
- assert.equal(result.matches[0].component, 'AdminUserRolesPage');
1555
- assert.deepEqual(result.params.userIds, ['100', '200', '300']);
1556
- assert.deepEqual(result.params.roleNames, ['admin', 'moderator']);
1557
-
1558
- result = matcher(
1559
- new URL('/admin/users/100/roles/', BASE_URL),
1560
- BASE_URL
1561
- );
1562
- assert.equal(result.matches.length, 1);
1563
- assert.equal(result.matches[0].component, 'AdminUserRolesPage');
1564
- assert.deepEqual(result.params.userIds, ['100']);
1565
- assert.equal(result.params.roleNames, undefined);
1566
- });
1567
-
1568
- test('Advanced route pattern edge cases', () => {
1569
- const routes = [
1570
- {
1571
- path: '/test/:param(\\d+)?/:param2(\\d+)+',
1572
- component: 'TestPage'
1573
- },
1574
- { path: '/empty/:empty*', component: 'EmptyPage' },
1575
- { path: '/strict/:id(\\d{3})', component: 'StrictPage' }
1576
- ];
1577
- const matcher = createMatcher(routes);
1578
-
1579
- let result = matcher(new URL('/test/123/456', BASE_URL), BASE_URL);
1580
- assert.equal(result.matches.length, 1);
1581
- assert.equal(result.matches[0].component, 'TestPage');
1582
- assert.equal(result.params.param, '123');
1583
- assert.deepEqual(result.params.param2, ['456']);
1584
-
1585
- result = matcher(new URL('/test/123/456/789', BASE_URL), BASE_URL);
1586
- assert.equal(result.matches.length, 1);
1587
- assert.equal(result.matches[0].component, 'TestPage');
1588
- assert.equal(result.params.param, '123');
1589
- assert.deepEqual(result.params.param2, ['456', '789']);
1590
-
1591
- result = matcher(new URL('/empty/', BASE_URL), BASE_URL);
1592
- assert.equal(result.matches.length, 1);
1593
- assert.equal(result.matches[0].component, 'EmptyPage');
1594
- assert.equal(result.params.empty, undefined);
1595
-
1596
- result = matcher(new URL('/strict/123', BASE_URL), BASE_URL);
1597
- assert.equal(result.matches.length, 1);
1598
- assert.equal(result.matches[0].component, 'StrictPage');
1599
- assert.equal(result.params.id, '123');
1600
-
1601
- // Test cases that do not match strict regular expression
1602
- result = matcher(new URL('/strict/1234', BASE_URL), BASE_URL);
1603
- assert.equal(result.matches.length, 0);
1604
-
1605
- result = matcher(new URL('/strict/12', BASE_URL), BASE_URL);
1606
- assert.equal(result.matches.length, 0);
1607
- });
1608
-
1609
- test('Empty path wildcard', () => {
1610
- assert.throws(
1611
- () => createMatcher([{ path: '*' }]),
1612
- 'Unexpected MODIFIER'
1613
- );
1614
-
1615
- let result = createMatcher([{ path: '(.*)' }])(
1616
- new URL('/users/a/b/c', BASE_URL),
1617
- BASE_URL
1618
- );
1619
- assert.equal(result.matches.length, 1);
1620
- assert.equal(result.matches[0].path, '(.*)');
1621
-
1622
- result = createMatcher([{ path: '(.*)*' }])(
1623
- new URL('/users/a/b/c', BASE_URL),
1624
- BASE_URL
1625
- );
1626
- assert.equal(result.matches.length, 1);
1627
- assert.equal(result.matches[0].path, '(.*)*');
1628
- });
1629
-
1630
- test.todo('Wildcard route matching - new version', () => {
1631
- const routes = [
1632
- { path: '/files{/*path}/:file{.:ext}', component: 'FilesPage' }, // /files/:path*/:file.:ext?
1633
- { path: '/api/*section/data', component: 'ApiDataPage' }, // /api/:section?/data
1634
- { path: '{/*rest}', component: 'CatchAllPage' } // /:rest*
1635
- ];
1636
- const matcher = createMatcher(routes);
1637
-
1638
- let result = matcher(
1639
- new URL('/files/document.pdf', BASE_URL),
1640
- BASE_URL
1641
- );
1642
- assert.equal(result.matches.length, 1);
1643
- assert.equal(result.matches[0].component, 'FilesPage');
1644
- assert.equal(result.params.path, void 0);
1645
- assert.equal(result.params.file, 'document');
1646
- assert.equal(result.params.ext, 'pdf');
1647
-
1648
- result = matcher(
1649
- new URL('/files/images/photo.jpg', BASE_URL),
1650
- BASE_URL
1651
- );
1652
- assert.equal(result.matches.length, 1);
1653
- assert.equal(result.matches[0].component, 'FilesPage');
1654
- assert.equal(result.params.path, ['images']);
1655
- assert.equal(result.params.file, 'photo');
1656
- assert.equal(result.params.ext, 'jpg');
1657
-
1658
- result = matcher(new URL('/files/images/photo', BASE_URL), BASE_URL);
1659
- assert.equal(result.matches.length, 1);
1660
- assert.equal(result.matches[0].component, 'FilesPage');
1661
- assert.equal(result.params.path, ['images']);
1662
- assert.equal(result.params.file, 'photo');
1663
- assert.equal(result.params.ext, void 0);
1664
-
1665
- result = matcher(new URL('/files/', BASE_URL), BASE_URL);
1666
- assert.equal(result.matches.length, 1);
1667
- assert.equal(result.matches[0].component, 'FilesPage');
1668
- assert.equal(result.params.path, void 0);
1669
-
1670
- result = matcher(new URL('/files', BASE_URL), BASE_URL);
1671
- assert.equal(result.matches.length, 1);
1672
- assert.equal(result.matches[0].component, 'FilesPage');
1673
- assert.equal(result.params.path, void 0);
1674
-
1675
- result = matcher(new URL('/api/v1/data', BASE_URL), BASE_URL);
1676
- assert.equal(result.matches.length, 1);
1677
- assert.equal(result.matches[0].component, 'ApiDataPage');
1678
- assert.equal(result.params.section, 'v1');
1679
-
1680
- result = matcher(new URL('/anything/else', BASE_URL), BASE_URL);
1681
- assert.equal(result.matches.length, 1);
1682
- assert.equal(result.matches[0].component, 'CatchAllPage');
1683
- assert.deepEqual(result.params.rest, ['anything', 'else']);
1684
- });
1685
- });