@esmx/router 3.0.0-rc.17 → 3.0.0-rc.19

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 (155) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +70 -0
  3. package/README.zh-CN.md +70 -0
  4. package/dist/error.d.ts +23 -0
  5. package/dist/error.mjs +61 -0
  6. package/dist/increment-id.d.ts +7 -0
  7. package/dist/increment-id.mjs +11 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.mjs +14 -3
  10. package/dist/index.test.mjs +8 -0
  11. package/dist/location.d.ts +15 -0
  12. package/dist/location.mjs +53 -0
  13. package/dist/location.test.d.ts +8 -0
  14. package/dist/location.test.mjs +370 -0
  15. package/dist/matcher.d.ts +3 -0
  16. package/dist/matcher.mjs +44 -0
  17. package/dist/matcher.test.mjs +1492 -0
  18. package/dist/micro-app.d.ts +18 -0
  19. package/dist/micro-app.dom.test.d.ts +1 -0
  20. package/dist/micro-app.dom.test.mjs +532 -0
  21. package/dist/micro-app.mjs +80 -0
  22. package/dist/navigation.d.ts +43 -0
  23. package/dist/navigation.mjs +143 -0
  24. package/dist/navigation.test.d.ts +1 -0
  25. package/dist/navigation.test.mjs +681 -0
  26. package/dist/options.d.ts +4 -0
  27. package/dist/options.mjs +88 -0
  28. package/dist/route-task.d.ts +40 -0
  29. package/dist/route-task.mjs +75 -0
  30. package/dist/route-task.test.d.ts +1 -0
  31. package/dist/route-task.test.mjs +673 -0
  32. package/dist/route-transition.d.ts +53 -0
  33. package/dist/route-transition.mjs +307 -0
  34. package/dist/route-transition.test.d.ts +1 -0
  35. package/dist/route-transition.test.mjs +146 -0
  36. package/dist/route.d.ts +72 -0
  37. package/dist/route.mjs +194 -0
  38. package/dist/route.test.d.ts +1 -0
  39. package/dist/route.test.mjs +1664 -0
  40. package/dist/router-back.test.d.ts +1 -0
  41. package/dist/router-back.test.mjs +361 -0
  42. package/dist/router-forward.test.d.ts +1 -0
  43. package/dist/router-forward.test.mjs +376 -0
  44. package/dist/router-go.test.d.ts +1 -0
  45. package/dist/router-go.test.mjs +73 -0
  46. package/dist/router-guards-cleanup.test.d.ts +1 -0
  47. package/dist/router-guards-cleanup.test.mjs +437 -0
  48. package/dist/router-link.d.ts +10 -0
  49. package/dist/router-link.mjs +126 -0
  50. package/dist/router-push.test.d.ts +1 -0
  51. package/dist/router-push.test.mjs +115 -0
  52. package/dist/router-replace.test.d.ts +1 -0
  53. package/dist/router-replace.test.mjs +114 -0
  54. package/dist/router-resolve.test.d.ts +1 -0
  55. package/dist/router-resolve.test.mjs +393 -0
  56. package/dist/router-restart-app.dom.test.d.ts +1 -0
  57. package/dist/router-restart-app.dom.test.mjs +616 -0
  58. package/dist/router-window-navigation.test.d.ts +1 -0
  59. package/dist/router-window-navigation.test.mjs +359 -0
  60. package/dist/router.d.ts +109 -102
  61. package/dist/router.mjs +260 -361
  62. package/dist/types.d.ts +246 -0
  63. package/dist/types.mjs +18 -0
  64. package/dist/util.d.ts +26 -0
  65. package/dist/util.mjs +53 -0
  66. package/dist/util.test.d.ts +1 -0
  67. package/dist/util.test.mjs +1020 -0
  68. package/package.json +10 -13
  69. package/src/error.ts +84 -0
  70. package/src/increment-id.ts +12 -0
  71. package/src/index.test.ts +9 -0
  72. package/src/index.ts +54 -3
  73. package/src/location.test.ts +406 -0
  74. package/src/location.ts +96 -0
  75. package/src/matcher.test.ts +1685 -0
  76. package/src/matcher.ts +59 -0
  77. package/src/micro-app.dom.test.ts +708 -0
  78. package/src/micro-app.ts +101 -0
  79. package/src/navigation.test.ts +858 -0
  80. package/src/navigation.ts +195 -0
  81. package/src/options.ts +131 -0
  82. package/src/route-task.test.ts +901 -0
  83. package/src/route-task.ts +105 -0
  84. package/src/route-transition.test.ts +178 -0
  85. package/src/route-transition.ts +425 -0
  86. package/src/route.test.ts +2014 -0
  87. package/src/route.ts +308 -0
  88. package/src/router-back.test.ts +487 -0
  89. package/src/router-forward.test.ts +506 -0
  90. package/src/router-go.test.ts +91 -0
  91. package/src/router-guards-cleanup.test.ts +595 -0
  92. package/src/router-link.ts +235 -0
  93. package/src/router-push.test.ts +140 -0
  94. package/src/router-replace.test.ts +139 -0
  95. package/src/router-resolve.test.ts +475 -0
  96. package/src/router-restart-app.dom.test.ts +783 -0
  97. package/src/router-window-navigation.test.ts +457 -0
  98. package/src/router.ts +289 -470
  99. package/src/types.ts +341 -0
  100. package/src/util.test.ts +1262 -0
  101. package/src/util.ts +116 -0
  102. package/dist/history/abstract.d.ts +0 -29
  103. package/dist/history/abstract.mjs +0 -107
  104. package/dist/history/base.d.ts +0 -79
  105. package/dist/history/base.mjs +0 -275
  106. package/dist/history/html.d.ts +0 -22
  107. package/dist/history/html.mjs +0 -183
  108. package/dist/history/index.d.ts +0 -7
  109. package/dist/history/index.mjs +0 -16
  110. package/dist/matcher/create-matcher.d.ts +0 -5
  111. package/dist/matcher/create-matcher.mjs +0 -218
  112. package/dist/matcher/create-matcher.spec.mjs +0 -0
  113. package/dist/matcher/index.d.ts +0 -1
  114. package/dist/matcher/index.mjs +0 -1
  115. package/dist/task-pipe/index.d.ts +0 -1
  116. package/dist/task-pipe/index.mjs +0 -1
  117. package/dist/task-pipe/task.d.ts +0 -30
  118. package/dist/task-pipe/task.mjs +0 -66
  119. package/dist/utils/bom.d.ts +0 -5
  120. package/dist/utils/bom.mjs +0 -10
  121. package/dist/utils/encoding.d.ts +0 -48
  122. package/dist/utils/encoding.mjs +0 -44
  123. package/dist/utils/guards.d.ts +0 -9
  124. package/dist/utils/guards.mjs +0 -12
  125. package/dist/utils/index.d.ts +0 -7
  126. package/dist/utils/index.mjs +0 -27
  127. package/dist/utils/path.d.ts +0 -60
  128. package/dist/utils/path.mjs +0 -281
  129. package/dist/utils/path.spec.mjs +0 -27
  130. package/dist/utils/scroll.d.ts +0 -25
  131. package/dist/utils/scroll.mjs +0 -59
  132. package/dist/utils/utils.d.ts +0 -16
  133. package/dist/utils/utils.mjs +0 -11
  134. package/dist/utils/warn.d.ts +0 -2
  135. package/dist/utils/warn.mjs +0 -12
  136. package/src/history/abstract.ts +0 -149
  137. package/src/history/base.ts +0 -408
  138. package/src/history/html.ts +0 -228
  139. package/src/history/index.ts +0 -20
  140. package/src/matcher/create-matcher.spec.ts +0 -3
  141. package/src/matcher/create-matcher.ts +0 -293
  142. package/src/matcher/index.ts +0 -1
  143. package/src/task-pipe/index.ts +0 -1
  144. package/src/task-pipe/task.ts +0 -97
  145. package/src/utils/bom.ts +0 -14
  146. package/src/utils/encoding.ts +0 -153
  147. package/src/utils/guards.ts +0 -25
  148. package/src/utils/index.ts +0 -27
  149. package/src/utils/path.spec.ts +0 -32
  150. package/src/utils/path.ts +0 -417
  151. package/src/utils/scroll.ts +0 -120
  152. package/src/utils/utils.ts +0 -30
  153. package/src/utils/warn.ts +0 -13
  154. /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
  155. /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
@@ -0,0 +1,2014 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { parsedOptions } from './options';
3
+ import { NON_ENUMERABLE_PROPERTIES, Route, applyRouteParams } from './route';
4
+ import type { Router } from './router';
5
+ import { RouteType, RouterMode } from './types';
6
+ import type {
7
+ RouteConfig,
8
+ RouteConfirmHook,
9
+ RouteHandleHook,
10
+ RouterOptions,
11
+ RouterParsedOptions
12
+ } from './types';
13
+
14
+ describe('Route Class Complete Test Suite', () => {
15
+ const createOptions = (
16
+ overrides: Partial<RouterOptions> = {}
17
+ ): RouterParsedOptions => {
18
+ const base = new URL('http://localhost:3000/app/');
19
+ const mockRoutes: RouteConfig[] = [
20
+ {
21
+ path: '/users/:id',
22
+ meta: { title: 'User Detail', requireAuth: true }
23
+ },
24
+ {
25
+ path: '/posts/:postId/comments/:commentId',
26
+ meta: { title: 'Comment Detail' }
27
+ },
28
+ {
29
+ path: '/admin/(.*)',
30
+ meta: { title: 'Admin', role: 'admin' }
31
+ }
32
+ ];
33
+
34
+ const routerOptions: RouterOptions = {
35
+ root: '#test',
36
+ context: { version: '1.0.0' },
37
+ routes: mockRoutes,
38
+ mode: RouterMode.history,
39
+ base,
40
+ req: null,
41
+ res: null,
42
+ apps: {},
43
+ normalizeURL: (url: URL) => url,
44
+ fallback: () => {},
45
+ rootStyle: false,
46
+ handleBackBoundary: () => {},
47
+ handleLayerClose: () => {},
48
+ ...overrides
49
+ };
50
+
51
+ return parsedOptions(routerOptions);
52
+ };
53
+
54
+ describe('🏗️ Constructor Tests', () => {
55
+ describe('Basic Construction', () => {
56
+ it('should create route with default options', () => {
57
+ const route = new Route();
58
+
59
+ expect(route.type).toBe(RouteType.push);
60
+ expect(route.isPush).toBe(true);
61
+ expect(route.path).toBe('/');
62
+ expect(route.state).toEqual({});
63
+ expect(route.params).toEqual({});
64
+ expect(route.query).toEqual({});
65
+ expect(route.queryArray).toEqual({});
66
+ });
67
+
68
+ it('should correctly handle string path', () => {
69
+ const options = createOptions();
70
+ const route = new Route({
71
+ options,
72
+ toType: RouteType.push,
73
+ toInput: '/users/123'
74
+ });
75
+
76
+ expect(route.path).toBe('/users/123');
77
+ expect(route.params.id).toBe('123');
78
+ expect(route.type).toBe(RouteType.push);
79
+ expect(route.isPush).toBe(true);
80
+ });
81
+
82
+ it('should correctly handle object-form route location', () => {
83
+ const options = createOptions();
84
+ const route = new Route({
85
+ options,
86
+ toType: RouteType.replace,
87
+ toInput: {
88
+ path: '/users/456',
89
+ query: { tab: 'profile' },
90
+ state: { fromPage: 'dashboard' },
91
+ keepScrollPosition: true
92
+ }
93
+ });
94
+
95
+ expect(route.path).toBe('/users/456');
96
+ expect(route.params.id).toBe('456');
97
+ expect(route.query.tab).toBe('profile');
98
+ expect(route.state.fromPage).toBe('dashboard');
99
+ expect(route.keepScrollPosition).toBe(true);
100
+ expect(route.isPush).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('URL Parsing and Matching', () => {
105
+ it('should correctly parse complex URL', () => {
106
+ const options = createOptions();
107
+ const route = new Route({
108
+ options,
109
+ toType: RouteType.push,
110
+ toInput: '/users/123?tab=profile&sort=name#section1'
111
+ });
112
+
113
+ expect(route.path).toBe('/users/123');
114
+ expect(route.fullPath).toBe(
115
+ '/users/123?tab=profile&sort=name#section1'
116
+ );
117
+ expect(route.query.tab).toBe('profile');
118
+ expect(route.query.sort).toBe('name');
119
+ expect(route.url.hash).toBe('#section1');
120
+ });
121
+
122
+ it('should handle multi-value query parameters', () => {
123
+ const options = createOptions();
124
+ const route = new Route({
125
+ options,
126
+ toType: RouteType.push,
127
+ toInput: '/users/123?tags=js&tags=react&tags=vue'
128
+ });
129
+
130
+ expect(route.query.tags).toBe('js'); // First value
131
+ expect(route.queryArray.tags).toEqual(['js', 'react', 'vue']);
132
+ });
133
+
134
+ it('should correctly match nested route parameters', () => {
135
+ const options = createOptions();
136
+ const route = new Route({
137
+ options,
138
+ toType: RouteType.push,
139
+ toInput: '/posts/456/comments/789'
140
+ });
141
+
142
+ expect(route.params.postId).toBe('456');
143
+ expect(route.params.commentId).toBe('789');
144
+ expect(route.matched.length).toBeGreaterThan(0);
145
+ });
146
+
147
+ it('should handle unmatched routes', () => {
148
+ const options = createOptions();
149
+ const route = new Route({
150
+ options,
151
+ toType: RouteType.push,
152
+ toInput: '/unknown/path'
153
+ });
154
+
155
+ expect(route.matched).toHaveLength(0);
156
+ expect(route.config).toBeNull();
157
+ expect(route.meta).toEqual({});
158
+ });
159
+ });
160
+
161
+ describe('State and Metadata Handling', () => {
162
+ it('should correctly set route metadata', () => {
163
+ const options = createOptions();
164
+ const route = new Route({
165
+ options,
166
+ toType: RouteType.push,
167
+ toInput: '/users/123'
168
+ });
169
+
170
+ expect(route.meta.title).toBe('User Detail');
171
+ expect(route.meta.requireAuth).toBe(true);
172
+ });
173
+
174
+ it('should correctly initialize state object', () => {
175
+ const options = createOptions();
176
+ const initialState = {
177
+ userId: 123,
178
+ permissions: ['read', 'write']
179
+ };
180
+ const route = new Route({
181
+ options,
182
+ toType: RouteType.push,
183
+ toInput: { path: '/users/123', state: initialState }
184
+ });
185
+
186
+ expect(route.state).toEqual(initialState);
187
+ expect(route.state).not.toBe(initialState); // Should be new object
188
+ });
189
+ });
190
+
191
+ describe('🔍 Cross-domain and Path Calculation Tests', () => {
192
+ it('should handle cross-domain URLs (different origin)', () => {
193
+ const options = createOptions({
194
+ base: new URL('http://localhost:3000/app/')
195
+ });
196
+ const route = new Route({
197
+ options,
198
+ toType: RouteType.push,
199
+ toInput: 'https://external.com/api/data'
200
+ });
201
+
202
+ // Cross-domain should not match routes
203
+ expect(route.matched).toHaveLength(0);
204
+ expect(route.config).toBeNull();
205
+ expect(route.path).toBe('/api/data'); // Use original pathname
206
+ expect(route.fullPath).toBe('/api/data'); // Use original path calculation
207
+ });
208
+
209
+ it('should handle URLs with different base paths', () => {
210
+ const options = createOptions({
211
+ base: new URL('http://localhost:3000/app/')
212
+ });
213
+ const route = new Route({
214
+ options,
215
+ toType: RouteType.push,
216
+ toInput: 'http://localhost:3000/other/path'
217
+ });
218
+
219
+ expect(route.matched).toHaveLength(0);
220
+ expect(route.config).toBeNull();
221
+ expect(route.path).toBe('/other/path'); // Use original pathname
222
+ });
223
+
224
+ it('should correctly calculate path when matched', () => {
225
+ const options = createOptions({
226
+ base: new URL('http://localhost:3000/app/')
227
+ });
228
+ const route = new Route({
229
+ options,
230
+ toType: RouteType.push,
231
+ toInput: 'http://localhost:3000/app/users/123'
232
+ });
233
+
234
+ expect(route.path).toBe('/users/123');
235
+ expect(route.matched.length).toBeGreaterThan(0);
236
+ });
237
+
238
+ it('should correctly calculate fullPath when unmatched', () => {
239
+ const options = createOptions();
240
+ const route = new Route({
241
+ options,
242
+ toType: RouteType.push,
243
+ toInput: 'https://external.com/api/data?key=value#section'
244
+ });
245
+
246
+ expect(route.fullPath).toBe('/api/data?key=value#section');
247
+ expect(route.path).toBe('/api/data');
248
+ });
249
+ });
250
+
251
+ describe('🔧 normalizeURL Integration Tests', () => {
252
+ it('should use custom normalizeURL function', () => {
253
+ const customNormalizeURL = vi.fn(
254
+ (url: URL, from: URL | null) => {
255
+ // Custom logic: convert path to lowercase
256
+ url.pathname = url.pathname.toLowerCase();
257
+ return url;
258
+ }
259
+ );
260
+
261
+ const options = createOptions({
262
+ normalizeURL: customNormalizeURL
263
+ });
264
+ const route = new Route({
265
+ options,
266
+ toType: RouteType.push,
267
+ toInput: '/USERS/123'
268
+ });
269
+
270
+ expect(customNormalizeURL).toHaveBeenCalled();
271
+ expect(route.path).toBe('/users/123');
272
+ });
273
+
274
+ it('should pass from parameter to normalizeURL', () => {
275
+ const customNormalizeURL = vi.fn(
276
+ (url: URL, from: URL | null) => url
277
+ );
278
+ const options = createOptions({
279
+ normalizeURL: customNormalizeURL
280
+ });
281
+
282
+ const fromURL = new URL('http://localhost:3000/app/previous');
283
+ const route = new Route({
284
+ options,
285
+ toType: RouteType.push,
286
+ toInput: '/users/123',
287
+ from: fromURL
288
+ });
289
+
290
+ expect(customNormalizeURL).toHaveBeenCalledWith(
291
+ expect.any(URL),
292
+ fromURL
293
+ );
294
+ });
295
+ });
296
+
297
+ describe('Property Enumerability', () => {
298
+ it('should correctly set non-enumerable properties', () => {
299
+ const options = createOptions();
300
+ const route = new Route({
301
+ options,
302
+ toType: RouteType.push,
303
+ toInput: '/users/123'
304
+ });
305
+
306
+ NON_ENUMERABLE_PROPERTIES.forEach((prop) => {
307
+ const descriptor = Object.getOwnPropertyDescriptor(
308
+ route,
309
+ prop
310
+ );
311
+ expect(descriptor?.enumerable).toBe(false);
312
+ });
313
+ });
314
+
315
+ it('should keep user properties enumerable', () => {
316
+ const options = createOptions();
317
+ const route = new Route({
318
+ options,
319
+ toType: RouteType.push,
320
+ toInput: '/users/123'
321
+ });
322
+
323
+ const userProperties = [
324
+ 'path',
325
+ 'fullPath',
326
+ 'params',
327
+ 'query',
328
+ 'meta',
329
+ 'state'
330
+ ];
331
+ userProperties.forEach((prop) => {
332
+ const descriptor = Object.getOwnPropertyDescriptor(
333
+ route,
334
+ prop
335
+ );
336
+ expect(descriptor?.enumerable).toBe(true);
337
+ });
338
+ });
339
+ });
340
+ });
341
+
342
+ describe('🔧 Property Tests', () => {
343
+ describe('Read-only Property Validation', () => {
344
+ it('should validate property existence', () => {
345
+ const options = createOptions();
346
+ const route = new Route({
347
+ options,
348
+ toType: RouteType.push,
349
+ toInput: '/users/123'
350
+ });
351
+
352
+ // Validate property existence
353
+ expect(route.path).toBeDefined();
354
+ expect(route.fullPath).toBeDefined();
355
+ expect(route.url).toBeDefined();
356
+ expect(route.params).toBeDefined();
357
+ expect(route.query).toBeDefined();
358
+ expect(route.matched).toBeDefined();
359
+ expect(route.config).toBeDefined();
360
+ expect(route.meta).toBeDefined();
361
+ expect(route.confirm).toBeDefined();
362
+ });
363
+ });
364
+
365
+ describe('Computed Property Correctness', () => {
366
+ it('should correctly calculate isPush property', () => {
367
+ const options = createOptions();
368
+
369
+ const pushRoute = new Route({
370
+ options,
371
+ toType: RouteType.push,
372
+ toInput: '/test'
373
+ });
374
+ expect(pushRoute.isPush).toBe(true);
375
+
376
+ const pushWindowRoute = new Route({
377
+ options,
378
+ toType: RouteType.pushWindow,
379
+ toInput: '/test'
380
+ });
381
+ expect(pushWindowRoute.isPush).toBe(true);
382
+
383
+ const replaceRoute = new Route({
384
+ options,
385
+ toType: RouteType.replace,
386
+ toInput: '/test'
387
+ });
388
+ expect(replaceRoute.isPush).toBe(false);
389
+
390
+ const goRoute = new Route({
391
+ options,
392
+ toType: RouteType.go,
393
+ toInput: '/test'
394
+ });
395
+ expect(goRoute.isPush).toBe(false);
396
+ });
397
+
398
+ it('should correctly calculate fullPath', () => {
399
+ const options = createOptions();
400
+ const route = new Route({
401
+ options,
402
+ toType: RouteType.push,
403
+ toInput: '/users/123?tab=profile#section1'
404
+ });
405
+
406
+ expect(route.fullPath).toBe('/users/123?tab=profile#section1');
407
+ expect(route.path).toBe('/users/123');
408
+ });
409
+ });
410
+
411
+ describe('Type Validation', () => {
412
+ it('should correctly set all RouteType', () => {
413
+ const options = createOptions();
414
+
415
+ Object.values(RouteType).forEach((type) => {
416
+ const route = new Route({
417
+ options,
418
+ toType: type,
419
+ toInput: '/test'
420
+ });
421
+ expect(route.type).toBe(type);
422
+ });
423
+ });
424
+ });
425
+
426
+ describe('Confirm Handler Tests', () => {
427
+ it('should initialize confirm as null when not provided', () => {
428
+ const options = createOptions();
429
+ const route = new Route({
430
+ options,
431
+ toType: RouteType.push,
432
+ toInput: '/users/123'
433
+ });
434
+
435
+ expect(route.confirm).toBeNull();
436
+ });
437
+
438
+ it('should set confirm handler when provided in RouteLocation', () => {
439
+ const options = createOptions();
440
+ const mockConfirmHandler = vi.fn();
441
+ const route = new Route({
442
+ options,
443
+ toType: RouteType.push,
444
+ toInput: {
445
+ path: '/users/123',
446
+ confirm: mockConfirmHandler
447
+ }
448
+ });
449
+
450
+ expect(route.confirm).toBe(mockConfirmHandler);
451
+ expect(typeof route.confirm).toBe('function');
452
+ });
453
+
454
+ it('should handle confirm as null when toInput is string', () => {
455
+ const options = createOptions();
456
+ const route = new Route({
457
+ options,
458
+ toType: RouteType.push,
459
+ toInput: '/users/123'
460
+ });
461
+
462
+ expect(route.confirm).toBeNull();
463
+ });
464
+
465
+ it('should preserve confirm handler during route cloning', () => {
466
+ const options = createOptions();
467
+ const mockConfirmHandler = vi.fn();
468
+ const originalRoute = new Route({
469
+ options,
470
+ toType: RouteType.push,
471
+ toInput: {
472
+ path: '/users/123',
473
+ confirm: mockConfirmHandler
474
+ }
475
+ });
476
+
477
+ const clonedRoute = originalRoute.clone();
478
+
479
+ expect(clonedRoute.confirm).toBe(mockConfirmHandler);
480
+ expect(clonedRoute.confirm).toBe(originalRoute.confirm);
481
+ });
482
+
483
+ it('should handle null confirm during route cloning', () => {
484
+ const options = createOptions();
485
+ const originalRoute = new Route({
486
+ options,
487
+ toType: RouteType.push,
488
+ toInput: '/users/123'
489
+ });
490
+
491
+ const clonedRoute = originalRoute.clone();
492
+
493
+ expect(clonedRoute.confirm).toBeNull();
494
+ expect(clonedRoute.confirm).toBe(originalRoute.confirm);
495
+ });
496
+
497
+ it('should make confirm property non-enumerable', () => {
498
+ const options = createOptions();
499
+ const mockConfirmHandler = vi.fn();
500
+ const route = new Route({
501
+ options,
502
+ toType: RouteType.push,
503
+ toInput: {
504
+ path: '/users/123',
505
+ confirm: mockConfirmHandler
506
+ }
507
+ });
508
+
509
+ const keys = Object.keys(route);
510
+ const propertyNames = Object.getOwnPropertyNames(route);
511
+ const descriptor = Object.getOwnPropertyDescriptor(
512
+ route,
513
+ 'confirm'
514
+ );
515
+
516
+ expect(keys).not.toContain('confirm');
517
+ expect(propertyNames).toContain('confirm');
518
+ expect(descriptor?.enumerable).toBe(false);
519
+ });
520
+
521
+ it('should be included in NON_ENUMERABLE_PROPERTIES list', () => {
522
+ expect(NON_ENUMERABLE_PROPERTIES).toContain('confirm');
523
+ });
524
+ });
525
+ });
526
+
527
+ describe('🎯 Handle mechanism tests', () => {
528
+ describe('Handle setting and getting', () => {
529
+ it('should correctly set and get handle function', () => {
530
+ const options = createOptions();
531
+ const route = new Route({
532
+ options,
533
+ toType: RouteType.push,
534
+ toInput: '/users/123'
535
+ });
536
+ const mockHandle: RouteHandleHook = vi.fn(
537
+ (to, from, router) => ({
538
+ result: 'test'
539
+ })
540
+ );
541
+
542
+ route.handle = mockHandle;
543
+ expect(route.handle).toBeDefined();
544
+ expect(typeof route.handle).toBe('function');
545
+ });
546
+
547
+ it('should handle null handle', () => {
548
+ const options = createOptions();
549
+ const route = new Route({
550
+ options,
551
+ toType: RouteType.push,
552
+ toInput: '/users/123'
553
+ });
554
+
555
+ route.handle = null;
556
+ expect(route.handle).toBeNull();
557
+ });
558
+
559
+ it('should handle non-function type handle', () => {
560
+ const options = createOptions();
561
+ const route = new Route({
562
+ options,
563
+ toType: RouteType.push,
564
+ toInput: '/users/123'
565
+ });
566
+
567
+ route.handle = 'not a function' as any;
568
+ expect(route.handle).toBeNull();
569
+ });
570
+ });
571
+
572
+ describe('Handle execution validation', () => {
573
+ it('should execute handle in correct state', () => {
574
+ const options = createOptions();
575
+ const route = new Route({
576
+ options,
577
+ toType: RouteType.push,
578
+ toInput: '/users/123'
579
+ });
580
+ const mockRouter = {} as Router;
581
+ const mockHandle: RouteHandleHook = vi.fn(
582
+ (to, from, router) => ({
583
+ result: 'success'
584
+ })
585
+ );
586
+
587
+ route.handle = mockHandle;
588
+
589
+ const result = route.handle!(route, null, mockRouter);
590
+ expect(result).toEqual({ result: 'success' });
591
+ expect(mockHandle).toHaveBeenCalledWith(
592
+ route,
593
+ null,
594
+ mockRouter
595
+ );
596
+ });
597
+
598
+ it('should throw exception in error state', () => {
599
+ const options = createOptions();
600
+ const route = new Route({
601
+ options,
602
+ toType: RouteType.push,
603
+ toInput: '/users/123'
604
+ });
605
+ const mockRouter = {} as Router;
606
+ const mockHandle: RouteHandleHook = vi.fn();
607
+
608
+ route.handle = mockHandle;
609
+
610
+ // Since there's no error status anymore, handle should work normally
611
+ expect(() => {
612
+ route.handle!(route, null, mockRouter);
613
+ }).not.toThrow();
614
+ });
615
+
616
+ it('should prevent repeated handle calls', () => {
617
+ const options = createOptions();
618
+ const route = new Route({
619
+ options,
620
+ toType: RouteType.push,
621
+ toInput: '/users/123'
622
+ });
623
+ const mockRouter = {} as Router;
624
+ const mockHandle: RouteHandleHook = vi.fn(
625
+ (to, from, router) => ({
626
+ result: 'test'
627
+ })
628
+ );
629
+
630
+ route.handle = mockHandle;
631
+
632
+ route.handle!(route, null, mockRouter);
633
+
634
+ expect(() => {
635
+ route.handle!(route, null, mockRouter);
636
+ }).toThrow(
637
+ 'Route handle hook can only be called once per navigation'
638
+ );
639
+ });
640
+ });
641
+
642
+ describe('HandleResult management', () => {
643
+ it('should correctly set and get handleResult', () => {
644
+ const options = createOptions();
645
+ const route = new Route({
646
+ options,
647
+ toType: RouteType.push,
648
+ toInput: '/users/123'
649
+ });
650
+ const result = { data: 'test', status: 'ok' };
651
+
652
+ route.handleResult = result;
653
+ expect(route.handleResult).toBe(result);
654
+
655
+ route.handleResult = null;
656
+ expect(route.handleResult).toBeNull();
657
+ });
658
+ });
659
+
660
+ describe('Handle wrapper function tests', () => {
661
+ it('should test handle calls with double-call prevention', () => {
662
+ const options = createOptions();
663
+ const route = new Route({
664
+ options,
665
+ toType: RouteType.push,
666
+ toInput: '/users/123'
667
+ });
668
+ const mockRouter = {} as Router;
669
+ const mockHandle: RouteHandleHook = vi.fn(
670
+ (to, from, router) => ({
671
+ result: 'test'
672
+ })
673
+ );
674
+
675
+ route.handle = mockHandle;
676
+
677
+ // Since status concept is removed, the handle should be callable initially
678
+ const firstResult = route.handle!(route, null, mockRouter);
679
+ expect(firstResult).toEqual({ result: 'test' });
680
+
681
+ // After first call, subsequent calls should throw due to double-call prevention
682
+ expect(() => route.handle!(route, null, mockRouter)).toThrow(
683
+ 'Route handle hook can only be called once per navigation'
684
+ );
685
+ });
686
+
687
+ it('should correctly pass this context and parameters', () => {
688
+ const options = createOptions();
689
+ const route = new Route({
690
+ options,
691
+ toType: RouteType.push,
692
+ toInput: '/users/123'
693
+ });
694
+ const mockRouter = {} as Router;
695
+ const mockHandle: RouteHandleHook = vi.fn(function (
696
+ this: Route,
697
+ to: Route,
698
+ from: Route | null,
699
+ router: Router
700
+ ) {
701
+ expect(this).toBe(route);
702
+ return { context: this, to, from, router };
703
+ });
704
+
705
+ route.handle = mockHandle;
706
+
707
+ const fromRoute = new Route({
708
+ options,
709
+ toType: RouteType.push,
710
+ toInput: '/home'
711
+ });
712
+ const result = route.handle!(route, fromRoute, mockRouter);
713
+
714
+ expect(mockHandle).toHaveBeenCalledWith(
715
+ route,
716
+ fromRoute,
717
+ mockRouter
718
+ );
719
+ expect(result).toEqual({
720
+ context: route,
721
+ to: route,
722
+ from: fromRoute,
723
+ router: mockRouter
724
+ });
725
+ });
726
+
727
+ it('should handle handle function exceptions', () => {
728
+ const options = createOptions();
729
+ const route = new Route({
730
+ options,
731
+ toType: RouteType.push,
732
+ toInput: '/users/123'
733
+ });
734
+ const mockRouter = {} as Router;
735
+ const errorHandle: RouteHandleHook = vi.fn(() => {
736
+ throw new Error('Handle execution failed');
737
+ });
738
+
739
+ route.handle = errorHandle;
740
+
741
+ expect(() => route.handle!(route, null, mockRouter)).toThrow(
742
+ 'Handle execution failed'
743
+ );
744
+ expect(errorHandle).toHaveBeenCalledOnce();
745
+ });
746
+
747
+ it('should handle setHandle boundary cases', () => {
748
+ const options = createOptions();
749
+ const route = new Route({
750
+ options,
751
+ toType: RouteType.push,
752
+ toInput: '/users/123'
753
+ });
754
+
755
+ route.setHandle(undefined as any);
756
+ expect(route.handle).toBeNull();
757
+
758
+ route.setHandle(123 as any);
759
+ expect(route.handle).toBeNull();
760
+
761
+ route.setHandle('string' as any);
762
+ expect(route.handle).toBeNull();
763
+
764
+ route.setHandle({} as any);
765
+ expect(route.handle).toBeNull();
766
+
767
+ route.setHandle([] as any);
768
+ expect(route.handle).toBeNull();
769
+ });
770
+ });
771
+ });
772
+
773
+ describe('📊 State management tests', () => {
774
+ describe('Navigation state application', () => {
775
+ it('should correctly apply navigation state', () => {
776
+ const options = createOptions();
777
+ const route = new Route({
778
+ options,
779
+ toType: RouteType.push,
780
+ toInput: { path: '/users/123', state: { a: 1, b: 2 } }
781
+ });
782
+
783
+ route.applyNavigationState({ b: 3, c: 4 });
784
+ expect(route.state).toEqual({ a: 1, b: 3, c: 4 });
785
+ });
786
+
787
+ it('should handle empty navigation state application', () => {
788
+ const options = createOptions();
789
+ const route = new Route({
790
+ options,
791
+ toType: RouteType.push,
792
+ toInput: '/users/123'
793
+ });
794
+
795
+ route.applyNavigationState({ __pageId__: 123 });
796
+ expect(route.state).toEqual({ __pageId__: 123 });
797
+ });
798
+ });
799
+
800
+ describe('Direct state assignment', () => {
801
+ it('should correctly set single state value', () => {
802
+ const options = createOptions();
803
+ const route = new Route({
804
+ options,
805
+ toType: RouteType.push,
806
+ toInput: '/users/123'
807
+ });
808
+
809
+ route.state.userId = 123;
810
+ route.state.userName = 'john';
811
+
812
+ expect(route.state.userId).toBe(123);
813
+ expect(route.state.userName).toBe('john');
814
+ });
815
+
816
+ it('should overwrite existing state value', () => {
817
+ const options = createOptions();
818
+ const route = new Route({
819
+ options,
820
+ toType: RouteType.push,
821
+ toInput: { path: '/users/123', state: { count: 1 } }
822
+ });
823
+
824
+ route.state.count = 2;
825
+ expect(route.state.count).toBe(2);
826
+ });
827
+ });
828
+
829
+ describe('State isolation', () => {
830
+ it("should ensure different routes' states are independent", () => {
831
+ const options = createOptions();
832
+ const route1 = new Route({
833
+ options,
834
+ toType: RouteType.push,
835
+ toInput: { path: '/route1', state: { shared: 'value1' } }
836
+ });
837
+ const route2 = new Route({
838
+ options,
839
+ toType: RouteType.push,
840
+ toInput: { path: '/route2', state: { shared: 'value2' } }
841
+ });
842
+
843
+ route1.state.shared = 'modified1';
844
+ expect(route2.state.shared).toBe('value2');
845
+ });
846
+ });
847
+
848
+ describe('StatusCode tests', () => {
849
+ it('should correctly set default statusCode', () => {
850
+ const options = createOptions();
851
+
852
+ // No statusCode input should default to null
853
+ const routeWithoutCode = new Route({
854
+ options,
855
+ toType: RouteType.push,
856
+ toInput: '/users/123'
857
+ });
858
+ expect(routeWithoutCode.statusCode).toBe(null);
859
+
860
+ // Unmatched routes should also default to null
861
+ const unmatchedRoute = new Route({
862
+ options,
863
+ toType: RouteType.push,
864
+ toInput: '/completely/unknown/path/that/does/not/match'
865
+ });
866
+ expect(unmatchedRoute.statusCode).toBe(null);
867
+ });
868
+
869
+ it('should support statusCode input from RouteLocation', () => {
870
+ const options = createOptions();
871
+
872
+ // Pass number statusCode
873
+ const routeWithCode = new Route({
874
+ options,
875
+ toType: RouteType.push,
876
+ toInput: { path: '/users/123', statusCode: 201 }
877
+ });
878
+ expect(routeWithCode.statusCode).toBe(201);
879
+
880
+ // Pass null statusCode
881
+ const routeWithNull = new Route({
882
+ options,
883
+ toType: RouteType.push,
884
+ toInput: { path: '/users/123', statusCode: null }
885
+ });
886
+ expect(routeWithNull.statusCode).toBe(null);
887
+ });
888
+
889
+ it('should set statusCode as non-enumerable', () => {
890
+ const options = createOptions();
891
+ const route = new Route({
892
+ options,
893
+ toType: RouteType.push,
894
+ toInput: '/users/123'
895
+ });
896
+
897
+ const descriptor = Object.getOwnPropertyDescriptor(
898
+ route,
899
+ 'statusCode'
900
+ );
901
+ expect(descriptor?.enumerable).toBe(false);
902
+
903
+ const keys = Object.keys(route);
904
+ expect(keys).not.toContain('statusCode');
905
+ });
906
+
907
+ it('should correctly copy statusCode in clone', () => {
908
+ const options = createOptions();
909
+ const originalRoute = new Route({
910
+ options,
911
+ toType: RouteType.push,
912
+ toInput: { path: '/users/123', statusCode: 500 }
913
+ });
914
+
915
+ const clonedRoute = originalRoute.clone();
916
+ expect(clonedRoute.statusCode).toBe(500);
917
+
918
+ // statusCode should be readonly, cloned route keeps original value
919
+ expect(clonedRoute.statusCode).toBe(500);
920
+ expect(originalRoute.statusCode).toBe(500);
921
+ });
922
+ });
923
+ });
924
+
925
+ describe('🔄 Clone function tests', () => {
926
+ describe('Object independence', () => {
927
+ it('should create completely independent cloned object', () => {
928
+ const options = createOptions();
929
+ const original = new Route({
930
+ options,
931
+ toType: RouteType.push,
932
+ toInput: { path: '/users/123', state: { test: 'value' } }
933
+ });
934
+
935
+ const cloned = original.clone();
936
+
937
+ expect(cloned).not.toBe(original);
938
+ expect(cloned.state).not.toBe(original.state);
939
+ expect(cloned.params).not.toBe(original.params);
940
+ });
941
+
942
+ it('should keep attribute values equal', () => {
943
+ const options = createOptions();
944
+ const original = new Route({
945
+ options,
946
+ toType: RouteType.push,
947
+ toInput: {
948
+ path: '/users/123',
949
+ state: { userId: 123, preferences: { theme: 'dark' } }
950
+ }
951
+ });
952
+
953
+ const cloned = original.clone();
954
+
955
+ expect(cloned.path).toBe(original.path);
956
+ expect(cloned.type).toBe(original.type);
957
+ expect(cloned.state).toEqual(original.state);
958
+ expect(cloned.params).toEqual(original.params);
959
+ });
960
+ });
961
+
962
+ describe('State deep copy', () => {
963
+ it('should deep copy state object', () => {
964
+ const options = createOptions();
965
+ const original = new Route({
966
+ options,
967
+ toType: RouteType.push,
968
+ toInput: {
969
+ path: '/users/123',
970
+ state: {
971
+ user: { id: 123, name: 'John' },
972
+ settings: { theme: 'dark' }
973
+ }
974
+ }
975
+ });
976
+
977
+ const cloned = original.clone();
978
+
979
+ // Modify cloned object's state should not affect original
980
+ cloned.state.newProp = 'newValue';
981
+ expect(original.state.newProp).toBeUndefined();
982
+ });
983
+ });
984
+
985
+ describe('Attribute completeness', () => {
986
+ it('should keep all important attributes', () => {
987
+ const options = createOptions();
988
+ const original = new Route({
989
+ options,
990
+ toType: RouteType.pushWindow,
991
+ toInput: '/users/123?tab=profile#section1'
992
+ });
993
+
994
+ const cloned = original.clone();
995
+
996
+ expect(cloned.type).toBe(original.type);
997
+ expect(cloned.isPush).toBe(original.isPush);
998
+ expect(cloned.path).toBe(original.path);
999
+ expect(cloned.fullPath).toBe(original.fullPath);
1000
+ expect(cloned.query).toEqual(original.query);
1001
+ expect(cloned.params).toEqual(original.params);
1002
+ expect(cloned.meta).toEqual(original.meta);
1003
+ });
1004
+ });
1005
+ });
1006
+
1007
+ describe('⚠️ Edge case tests', () => {
1008
+ describe('Exception input handling', () => {
1009
+ it('should handle invalid route type', () => {
1010
+ const options = createOptions();
1011
+ expect(() => {
1012
+ new Route({
1013
+ options,
1014
+ toType: 'invalid' as any,
1015
+ toInput: '/test'
1016
+ });
1017
+ }).not.toThrow();
1018
+ });
1019
+
1020
+ it('should handle empty string path', () => {
1021
+ const options = createOptions();
1022
+ const route = new Route({
1023
+ options,
1024
+ toType: RouteType.push,
1025
+ toInput: ''
1026
+ });
1027
+
1028
+ expect(route.path).toBeDefined();
1029
+ expect(route.fullPath).toBeDefined();
1030
+ });
1031
+
1032
+ it('should handle special character path', () => {
1033
+ const options = createOptions();
1034
+ const route = new Route({
1035
+ options,
1036
+ toType: RouteType.push,
1037
+ toInput: '/users/测试用户/profile?name=张三'
1038
+ });
1039
+
1040
+ // URL-encoded path should not contain original Chinese characters
1041
+ expect(route.path).toContain('users');
1042
+ expect(route.path).toContain('profile');
1043
+ expect(route.query.name).toBe('张三');
1044
+ });
1045
+ });
1046
+
1047
+ describe('Extreme value tests', () => {
1048
+ it('should handle very long path', () => {
1049
+ const options = createOptions();
1050
+ const longPath = '/users/' + 'a'.repeat(1000);
1051
+
1052
+ expect(() => {
1053
+ new Route({
1054
+ options,
1055
+ toType: RouteType.push,
1056
+ toInput: longPath
1057
+ });
1058
+ }).not.toThrow();
1059
+ });
1060
+
1061
+ it('should handle large number of query parameters', () => {
1062
+ const options = createOptions();
1063
+ const queryParams = Array.from(
1064
+ { length: 100 },
1065
+ (_, i) => `param${i}=value${i}`
1066
+ ).join('&');
1067
+ const path = `/test?${queryParams}`;
1068
+
1069
+ const route = new Route({
1070
+ options,
1071
+ toType: RouteType.push,
1072
+ toInput: path
1073
+ });
1074
+
1075
+ expect(Object.keys(route.query)).toHaveLength(100);
1076
+ expect(route.query.param0).toBe('value0');
1077
+ expect(route.query.param99).toBe('value99');
1078
+ });
1079
+ });
1080
+ });
1081
+
1082
+ describe('🔧 Tool function tests', () => {
1083
+ describe('applyRouteParams function', () => {
1084
+ it('should correctly apply route parameters', () => {
1085
+ const base = new URL('http://localhost:3000/app/');
1086
+ const options = createOptions({ base });
1087
+ const to = new URL('http://localhost:3000/app/users/old-id');
1088
+ const match = options.matcher(to, base);
1089
+ const toInput = {
1090
+ path: '/users/old-id',
1091
+ params: { id: 'new-id' }
1092
+ };
1093
+
1094
+ applyRouteParams(match, toInput, base, to);
1095
+
1096
+ expect(to.pathname).toBe('/app/users/new-id');
1097
+ expect(match.params.id).toBe('new-id');
1098
+ });
1099
+
1100
+ it('should handle multiple parameters', () => {
1101
+ const base = new URL('http://localhost:3000/app/');
1102
+ const options = createOptions({
1103
+ base,
1104
+ routes: [{ path: '/posts/:postId/comments/:commentId' }]
1105
+ });
1106
+ const to = new URL(
1107
+ 'http://localhost:3000/app/posts/123/comments/456'
1108
+ );
1109
+ const match = options.matcher(to, base);
1110
+ const toInput = {
1111
+ path: '/posts/123/comments/456',
1112
+ params: { postId: 'post-999', commentId: 'comment-888' }
1113
+ };
1114
+
1115
+ applyRouteParams(match, toInput, base, to);
1116
+
1117
+ expect(to.pathname).toBe(
1118
+ '/app/posts/post-999/comments/comment-888'
1119
+ );
1120
+ expect(match.params.postId).toBe('post-999');
1121
+ expect(match.params.commentId).toBe('comment-888');
1122
+ });
1123
+
1124
+ it('should return directly when unmatched', () => {
1125
+ const base = new URL('http://localhost:3000/app/');
1126
+ const options = createOptions({ routes: [] });
1127
+ const to = new URL('http://localhost:3000/app/unknown');
1128
+ const originalPathname = to.pathname;
1129
+ const match = options.matcher(to, base);
1130
+ const toInput = { path: '/unknown', params: { id: 'test' } };
1131
+
1132
+ applyRouteParams(match, toInput, base, to);
1133
+
1134
+ expect(to.pathname).toBe(originalPathname);
1135
+ });
1136
+
1137
+ it('should handle non-object toInput parameters', () => {
1138
+ const base = new URL('http://localhost:3000/app/');
1139
+ const options = createOptions();
1140
+ const to = new URL('http://localhost:3000/app/users/123');
1141
+ const originalPathname = to.pathname;
1142
+ const match = options.matcher(to, base);
1143
+
1144
+ // Test string type
1145
+ applyRouteParams(match, '/users/123', base, to);
1146
+ expect(to.pathname).toBe(originalPathname);
1147
+
1148
+ // Test null
1149
+ applyRouteParams(match, null as any, base, to);
1150
+ expect(to.pathname).toBe(originalPathname);
1151
+
1152
+ // Test undefined
1153
+ applyRouteParams(match, undefined as any, base, to);
1154
+ expect(to.pathname).toBe(originalPathname);
1155
+ });
1156
+
1157
+ it('should handle empty params object', () => {
1158
+ const base = new URL('http://localhost:3000/app/');
1159
+ const options = createOptions();
1160
+ const to = new URL('http://localhost:3000/app/users/123');
1161
+ const originalPathname = to.pathname;
1162
+ const match = options.matcher(to, base);
1163
+
1164
+ const toInput = { path: '/users/123', params: {} };
1165
+ applyRouteParams(match, toInput, base, to);
1166
+ expect(to.pathname).toBe(originalPathname);
1167
+
1168
+ // Test undefined params
1169
+ const toInput2 = {
1170
+ path: '/users/123',
1171
+ params: undefined as any
1172
+ };
1173
+ applyRouteParams(match, toInput2, base, to);
1174
+ expect(to.pathname).toBe(originalPathname);
1175
+ });
1176
+
1177
+ it('should handle complex path replacement logic', () => {
1178
+ const base = new URL('http://localhost:3000/app/');
1179
+ const options = createOptions({
1180
+ base,
1181
+ routes: [{ path: '/users/:id/posts/:postId' }]
1182
+ });
1183
+ const to = new URL(
1184
+ 'http://localhost:3000/app/users/123/posts/456'
1185
+ );
1186
+ const match = options.matcher(to, base);
1187
+ const toInput = {
1188
+ path: '/users/123/posts/456',
1189
+ params: { id: 'user-999', postId: 'post-888' }
1190
+ };
1191
+
1192
+ applyRouteParams(match, toInput, base, to);
1193
+
1194
+ expect(to.pathname).toBe('/app/users/user-999/posts/post-888');
1195
+ expect(match.params.id).toBe('user-999');
1196
+ expect(match.params.postId).toBe('post-888');
1197
+ });
1198
+
1199
+ it('should handle path segment empty cases', () => {
1200
+ const base = new URL('http://localhost:3000/app/');
1201
+ const options = createOptions({
1202
+ base,
1203
+ routes: [{ path: '/users/:id' }]
1204
+ });
1205
+ const to = new URL('http://localhost:3000/app/users/123');
1206
+ const match = options.matcher(to, base);
1207
+
1208
+ const originalCompile = match.matches[0].compile;
1209
+ match.matches[0].compile = vi.fn(() => '/users/'); // Return empty id part
1210
+
1211
+ const toInput = { path: '/users/123', params: { id: '' } };
1212
+ applyRouteParams(match, toInput, base, to);
1213
+
1214
+ // Should keep original path fragment
1215
+ expect(to.pathname).toBe('/app/users/123');
1216
+
1217
+ match.matches[0].compile = originalCompile;
1218
+ });
1219
+ });
1220
+ });
1221
+
1222
+ describe('🔗 Integration tests', () => {
1223
+ describe('With router options integration', () => {
1224
+ it('should correctly use custom normalizeURL', () => {
1225
+ const customNormalizeURL = vi.fn((url: URL) => {
1226
+ url.pathname = url.pathname.toLowerCase();
1227
+ return url;
1228
+ });
1229
+
1230
+ const options = createOptions({
1231
+ normalizeURL: customNormalizeURL
1232
+ });
1233
+ const route = new Route({
1234
+ options,
1235
+ toType: RouteType.push,
1236
+ toInput: '/USERS/123'
1237
+ });
1238
+
1239
+ expect(customNormalizeURL).toHaveBeenCalled();
1240
+ expect(route.path).toBe('/users/123');
1241
+ });
1242
+
1243
+ it('should correctly handle SSR related attributes', () => {
1244
+ const mockReq = {} as any;
1245
+ const mockRes = {} as any;
1246
+ const options = createOptions({ req: mockReq, res: mockRes });
1247
+
1248
+ const route = new Route({
1249
+ options,
1250
+ toType: RouteType.push,
1251
+ toInput: '/users/123'
1252
+ });
1253
+
1254
+ expect(route.req).toBe(mockReq);
1255
+ expect(route.res).toBe(mockRes);
1256
+ });
1257
+ });
1258
+
1259
+ describe('With route configuration integration', () => {
1260
+ it('should correctly handle nested route configuration', () => {
1261
+ const nestedRoutes: RouteConfig[] = [
1262
+ {
1263
+ path: '/admin',
1264
+ meta: { requireAuth: true },
1265
+ children: [
1266
+ {
1267
+ path: '/users',
1268
+ meta: { title: 'User Management' }
1269
+ }
1270
+ ]
1271
+ }
1272
+ ];
1273
+
1274
+ const options = createOptions({ routes: nestedRoutes });
1275
+ const route = new Route({
1276
+ options,
1277
+ toType: RouteType.push,
1278
+ toInput: '/admin/users'
1279
+ });
1280
+
1281
+ expect(route.matched.length).toBeGreaterThan(0);
1282
+ expect(route.meta.title).toBe('User Management');
1283
+ });
1284
+ });
1285
+ });
1286
+
1287
+ describe('🎭 Performance tests', () => {
1288
+ it('should create a large number of route instances within reasonable time', () => {
1289
+ const options = createOptions();
1290
+ const startTime = performance.now();
1291
+
1292
+ for (let i = 0; i < 1000; i++) {
1293
+ new Route({
1294
+ options,
1295
+ toType: RouteType.push,
1296
+ toInput: `/users/${i}`
1297
+ });
1298
+ }
1299
+
1300
+ const endTime = performance.now();
1301
+ const duration = endTime - startTime;
1302
+
1303
+ // 1000 instances should be created within 100ms
1304
+ expect(duration).toBeLessThan(100);
1305
+ });
1306
+
1307
+ it('should efficiently handle state operations', () => {
1308
+ const options = createOptions();
1309
+ const route = new Route({
1310
+ options,
1311
+ toType: RouteType.push,
1312
+ toInput: '/test'
1313
+ });
1314
+
1315
+ const startTime = performance.now();
1316
+
1317
+ for (let i = 0; i < 1000; i++) {
1318
+ route.state[`key${i}`] = `value${i}`;
1319
+ }
1320
+
1321
+ const endTime = performance.now();
1322
+ const duration = endTime - startTime;
1323
+
1324
+ // 1000 state setting should be completed within 50ms
1325
+ expect(duration).toBeLessThan(50);
1326
+ expect(Object.keys(route.state)).toHaveLength(1000);
1327
+ });
1328
+ });
1329
+ });
1330
+
1331
+ // Supplement missing test cases
1332
+ describe('🔍 Route Class Depth Test - Missing Scenario Supplement', () => {
1333
+ const createOptions = (
1334
+ overrides: Partial<RouterOptions> = {}
1335
+ ): RouterParsedOptions => {
1336
+ const base = new URL('http://localhost:3000/app/');
1337
+ const mockRoutes: RouteConfig[] = [
1338
+ {
1339
+ path: '/users/:id',
1340
+ meta: { title: 'User Detail', requireAuth: true }
1341
+ },
1342
+ {
1343
+ path: '/posts/:postId/comments/:commentId',
1344
+ meta: { title: 'Comment Detail' }
1345
+ }
1346
+ ];
1347
+
1348
+ const routerOptions: RouterOptions = {
1349
+ root: '#test',
1350
+ context: { version: '1.0.0' },
1351
+ routes: mockRoutes,
1352
+ mode: RouterMode.history,
1353
+ base,
1354
+ req: null,
1355
+ res: null,
1356
+ apps: {},
1357
+ normalizeURL: (url: URL) => url,
1358
+ fallback: () => {},
1359
+ rootStyle: false,
1360
+ handleBackBoundary: () => {},
1361
+ handleLayerClose: () => {},
1362
+ ...overrides
1363
+ };
1364
+
1365
+ return parsedOptions(routerOptions);
1366
+ };
1367
+
1368
+ describe('🔧 applyRouteParams Boundary Condition Tests', () => {
1369
+ it('should handle non-object toInput parameters', () => {
1370
+ const base = new URL('http://localhost:3000/app/');
1371
+ const options = createOptions();
1372
+ const to = new URL('http://localhost:3000/app/users/123');
1373
+ const originalPathname = to.pathname;
1374
+ const match = options.matcher(to, base);
1375
+
1376
+ // Test string type
1377
+ applyRouteParams(match, '/users/123', base, to);
1378
+ expect(to.pathname).toBe(originalPathname);
1379
+
1380
+ // Test null
1381
+ applyRouteParams(match, null as any, base, to);
1382
+ expect(to.pathname).toBe(originalPathname);
1383
+
1384
+ // Test undefined
1385
+ applyRouteParams(match, undefined as any, base, to);
1386
+ expect(to.pathname).toBe(originalPathname);
1387
+ });
1388
+
1389
+ it('should handle empty params object', () => {
1390
+ const base = new URL('http://localhost:3000/app/');
1391
+ const options = createOptions();
1392
+ const to = new URL('http://localhost:3000/app/users/123');
1393
+ const originalPathname = to.pathname;
1394
+ const match = options.matcher(to, base);
1395
+
1396
+ const toInput = { path: '/users/123', params: {} };
1397
+ applyRouteParams(match, toInput, base, to);
1398
+ expect(to.pathname).toBe(originalPathname);
1399
+
1400
+ // Test undefined params
1401
+ const toInput2 = { path: '/users/123', params: undefined as any };
1402
+ applyRouteParams(match, toInput2, base, to);
1403
+ expect(to.pathname).toBe(originalPathname);
1404
+ });
1405
+
1406
+ it('should handle path segment empty cases', () => {
1407
+ const base = new URL('http://localhost:3000/app/');
1408
+ const options = createOptions({
1409
+ base,
1410
+ routes: [{ path: '/users/:id' }]
1411
+ });
1412
+ const to = new URL('http://localhost:3000/app/users/123');
1413
+ const match = options.matcher(to, base);
1414
+
1415
+ const originalCompile = match.matches[0].compile;
1416
+ match.matches[0].compile = vi.fn(() => '/users/'); // Return empty id part
1417
+
1418
+ const toInput = { path: '/users/123', params: { id: '' } };
1419
+ applyRouteParams(match, toInput, base, to);
1420
+
1421
+ // Should keep original path fragment
1422
+ expect(to.pathname).toBe('/app/users/123');
1423
+
1424
+ match.matches[0].compile = originalCompile;
1425
+ });
1426
+ });
1427
+
1428
+ describe('🎯 Query Parameter Processing Depth Test', () => {
1429
+ it('should handle query parameter de-duplication logic', () => {
1430
+ const options = createOptions();
1431
+ const route = new Route({
1432
+ options,
1433
+ toType: RouteType.push,
1434
+ toInput: '/users/123?name=john&name=jane&age=25&name=bob'
1435
+ });
1436
+
1437
+ // query should only contain the first value
1438
+ expect(route.query.name).toBe('john');
1439
+ expect(route.query.age).toBe('25');
1440
+
1441
+ // queryArray should contain all values
1442
+ expect(route.queryArray.name).toEqual(['john', 'jane', 'bob']);
1443
+ expect(route.queryArray.age).toEqual(['25']);
1444
+ });
1445
+
1446
+ it('should handle empty query parameter values', () => {
1447
+ const options = createOptions();
1448
+ const route = new Route({
1449
+ options,
1450
+ toType: RouteType.push,
1451
+ toInput: '/users/123?empty=&name=john&blank&value=test'
1452
+ });
1453
+
1454
+ expect(route.query.empty).toBe('');
1455
+ expect(route.query.name).toBe('john');
1456
+ expect(route.query.blank).toBe('');
1457
+ expect(route.query.value).toBe('test');
1458
+ });
1459
+
1460
+ it('should handle special character query parameters', () => {
1461
+ const options = createOptions();
1462
+ const route = new Route({
1463
+ options,
1464
+ toType: RouteType.push,
1465
+ toInput:
1466
+ '/users/123?name=%E5%BC%A0%E4%B8%89&symbol=%26%3D%3F%23'
1467
+ });
1468
+
1469
+ expect(route.query.name).toBe('张三');
1470
+ expect(route.query.symbol).toBe('&=?#');
1471
+ });
1472
+ });
1473
+
1474
+ describe('🔄 Clone Function Depth Test', () => {
1475
+ it('should correctly clone complex state object', () => {
1476
+ const options = createOptions();
1477
+ const complexState = {
1478
+ user: { id: 123, name: 'John', roles: ['admin', 'user'] },
1479
+ settings: { theme: 'dark', notifications: true },
1480
+ metadata: { created: new Date(), version: 1.0 }
1481
+ };
1482
+
1483
+ const original = new Route({
1484
+ options,
1485
+ toType: RouteType.push,
1486
+ toInput: { path: '/users/123', state: complexState }
1487
+ });
1488
+
1489
+ const cloned = original.clone();
1490
+
1491
+ expect(cloned.state).toEqual(original.state);
1492
+ expect(cloned.state).not.toBe(original.state);
1493
+
1494
+ // Modify cloned object should not affect original
1495
+ cloned.state.newProp = 'newValue';
1496
+ expect(original.state.newProp).toBeUndefined();
1497
+ });
1498
+
1499
+ it('should keep cloned object _options reference', () => {
1500
+ const options = createOptions();
1501
+ const original = new Route({
1502
+ options,
1503
+ toType: RouteType.push,
1504
+ toInput: '/users/123'
1505
+ });
1506
+
1507
+ const cloned = original.clone();
1508
+
1509
+ // _options should be the same reference
1510
+ expect((cloned as any)._options).toBe((original as any)._options);
1511
+ });
1512
+
1513
+ it('should correctly clone routes with query parameters and hash', () => {
1514
+ const options = createOptions();
1515
+ const original = new Route({
1516
+ options,
1517
+ toType: RouteType.pushWindow,
1518
+ toInput: '/users/123?tab=profile&sort=name#section1'
1519
+ });
1520
+
1521
+ const cloned = original.clone();
1522
+
1523
+ expect(cloned.fullPath).toBe(original.fullPath);
1524
+ expect(cloned.query).toEqual(original.query);
1525
+ expect(cloned.type).toBe(original.type);
1526
+ expect(cloned.isPush).toBe(original.isPush);
1527
+ });
1528
+ });
1529
+
1530
+ describe('🏗️ Constructor Boundary Condition Tests', () => {
1531
+ it('should handle keepScrollPosition various values', () => {
1532
+ const options = createOptions();
1533
+
1534
+ // Test true value
1535
+ const route1 = new Route({
1536
+ options,
1537
+ toType: RouteType.push,
1538
+ toInput: { path: '/test', keepScrollPosition: true }
1539
+ });
1540
+ expect(route1.keepScrollPosition).toBe(true);
1541
+
1542
+ // Test false value
1543
+ const route2 = new Route({
1544
+ options,
1545
+ toType: RouteType.push,
1546
+ toInput: { path: '/test', keepScrollPosition: false }
1547
+ });
1548
+ expect(route2.keepScrollPosition).toBe(false);
1549
+
1550
+ // Test truthy value
1551
+ const route3 = new Route({
1552
+ options,
1553
+ toType: RouteType.push,
1554
+ toInput: { path: '/test', keepScrollPosition: 'yes' as any }
1555
+ });
1556
+ expect(route3.keepScrollPosition).toBe(true);
1557
+
1558
+ // Test falsy value
1559
+ const route4 = new Route({
1560
+ options,
1561
+ toType: RouteType.push,
1562
+ toInput: { path: '/test', keepScrollPosition: 0 as any }
1563
+ });
1564
+ expect(route4.keepScrollPosition).toBe(false);
1565
+
1566
+ // Test string path (should be false)
1567
+ const route5 = new Route({
1568
+ options,
1569
+ toType: RouteType.push,
1570
+ toInput: '/test'
1571
+ });
1572
+ expect(route5.keepScrollPosition).toBe(false);
1573
+ });
1574
+
1575
+ it('should correctly handle config and meta calculation', () => {
1576
+ const options = createOptions();
1577
+
1578
+ // Matched route
1579
+ const matchedRoute = new Route({
1580
+ options,
1581
+ toType: RouteType.push,
1582
+ toInput: '/users/123'
1583
+ });
1584
+ expect(matchedRoute.config).not.toBeNull();
1585
+ expect(matchedRoute.meta.title).toBe('User Detail');
1586
+
1587
+ // Unmatched route
1588
+ const unmatchedRoute = new Route({
1589
+ options,
1590
+ toType: RouteType.push,
1591
+ toInput: '/unknown'
1592
+ });
1593
+ expect(unmatchedRoute.config).toBeNull();
1594
+ expect(unmatchedRoute.meta).toEqual({});
1595
+ });
1596
+
1597
+ it('should correctly handle matched array freezing', () => {
1598
+ const options = createOptions();
1599
+ const route = new Route({
1600
+ options,
1601
+ toType: RouteType.push,
1602
+ toInput: '/users/123'
1603
+ });
1604
+
1605
+ // matched array should be frozen
1606
+ expect(Object.isFrozen(route.matched)).toBe(true);
1607
+
1608
+ expect(() => {
1609
+ (route.matched as any).push({});
1610
+ }).toThrow();
1611
+ });
1612
+ });
1613
+
1614
+ describe('🔒 Property Immutable Test', () => {
1615
+ it('should verify read-only property behavior', () => {
1616
+ const options = createOptions();
1617
+ const route = new Route({
1618
+ options,
1619
+ toType: RouteType.push,
1620
+ toInput: '/users/123'
1621
+ });
1622
+
1623
+ expect(route.params).toBeDefined();
1624
+ expect(route.query).toBeDefined();
1625
+ expect(route.url).toBeDefined();
1626
+
1627
+ expect(typeof route.params).toBe('object');
1628
+ expect(typeof route.query).toBe('object');
1629
+ expect(route.url instanceof URL).toBe(true);
1630
+ });
1631
+ });
1632
+
1633
+ describe('🎨 State Management Special Cases', () => {
1634
+ it('should handle state object special keys', () => {
1635
+ const options = createOptions();
1636
+ const route = new Route({
1637
+ options,
1638
+ toType: RouteType.push,
1639
+ toInput: {
1640
+ path: '/test',
1641
+ state: {
1642
+ normalKey: 'value',
1643
+ specialKey: 'specialValue'
1644
+ }
1645
+ }
1646
+ });
1647
+
1648
+ expect(route.state.normalKey).toBe('value');
1649
+ expect(route.state.specialKey).toBe('specialValue');
1650
+ });
1651
+
1652
+ it('should handle state synchronization special keys', () => {
1653
+ const options = createOptions();
1654
+
1655
+ const sourceRoute = new Route({
1656
+ options,
1657
+ toType: RouteType.push,
1658
+ toInput: {
1659
+ path: '/source',
1660
+ state: {
1661
+ normal: 'source',
1662
+ special: 'sourceSpecial'
1663
+ }
1664
+ }
1665
+ });
1666
+
1667
+ const targetRoute = new Route({
1668
+ options,
1669
+ toType: RouteType.push,
1670
+ toInput: {
1671
+ path: '/target',
1672
+ state: {
1673
+ existing: 'target',
1674
+ special: 'targetSpecial'
1675
+ }
1676
+ }
1677
+ });
1678
+
1679
+ sourceRoute.syncTo(targetRoute);
1680
+
1681
+ expect(targetRoute.state.normal).toBe('source');
1682
+ expect(targetRoute.state.existing).toBeUndefined();
1683
+ expect(targetRoute.state.special).toBe('sourceSpecial');
1684
+ });
1685
+ });
1686
+
1687
+ describe('🔄 syncTo Method Tests', () => {
1688
+ it('should fully synchronize all route attributes', () => {
1689
+ const options = createOptions();
1690
+
1691
+ const sourceRoute = new Route({
1692
+ options,
1693
+ toType: RouteType.push,
1694
+ toInput: {
1695
+ path: '/users/456',
1696
+ state: { userId: 456, name: 'Jane' },
1697
+ statusCode: 200
1698
+ }
1699
+ });
1700
+
1701
+ const targetRoute = new Route({
1702
+ options,
1703
+ toType: RouteType.replace,
1704
+ toInput: {
1705
+ path: '/old/path',
1706
+ state: { oldData: 'old' }
1707
+ }
1708
+ });
1709
+
1710
+ sourceRoute.syncTo(targetRoute);
1711
+
1712
+ expect(targetRoute.statusCode).toBe(200);
1713
+
1714
+ expect(targetRoute.state.userId).toBe(456);
1715
+ expect(targetRoute.state.name).toBe('Jane');
1716
+ expect(targetRoute.state.oldData).toBeUndefined();
1717
+
1718
+ expect(targetRoute.type).toBe(RouteType.push);
1719
+ expect(targetRoute.path).toBe('/users/456');
1720
+ expect(targetRoute.fullPath).toBe('/users/456');
1721
+ expect(targetRoute.params.id).toBe('456');
1722
+ });
1723
+
1724
+ it('should synchronize params object', () => {
1725
+ const options = createOptions();
1726
+
1727
+ const sourceRoute = new Route({
1728
+ options,
1729
+ toType: RouteType.push,
1730
+ toInput: '/users/789'
1731
+ });
1732
+
1733
+ const targetRoute = new Route({
1734
+ options,
1735
+ toType: RouteType.push,
1736
+ toInput: '/posts/123'
1737
+ });
1738
+
1739
+ sourceRoute.syncTo(targetRoute);
1740
+
1741
+ expect(targetRoute.params.id).toBe('789');
1742
+ expect(targetRoute.params.postId).toBeUndefined();
1743
+ });
1744
+
1745
+ it('should synchronize query parameters', () => {
1746
+ const options = createOptions();
1747
+
1748
+ const sourceRoute = new Route({
1749
+ options,
1750
+ toType: RouteType.push,
1751
+ toInput: '/search?q=test&page=2'
1752
+ });
1753
+
1754
+ const targetRoute = new Route({
1755
+ options,
1756
+ toType: RouteType.push,
1757
+ toInput: '/old?old=value'
1758
+ });
1759
+
1760
+ sourceRoute.syncTo(targetRoute);
1761
+
1762
+ expect(targetRoute.query.q).toBe('test');
1763
+ expect(targetRoute.query.page).toBe('2');
1764
+ expect(targetRoute.query.old).toBeUndefined();
1765
+ });
1766
+
1767
+ it('should synchronize handle related attributes', () => {
1768
+ const options = createOptions();
1769
+
1770
+ const sourceRoute = new Route({
1771
+ options,
1772
+ toType: RouteType.push,
1773
+ toInput: '/test'
1774
+ });
1775
+
1776
+ const mockRouter = {} as Router;
1777
+ const mockHandle = vi.fn();
1778
+ sourceRoute.setHandle(mockHandle);
1779
+ (sourceRoute as any)._handleResult = { success: true };
1780
+ (sourceRoute as any)._handled = true;
1781
+
1782
+ const targetRoute = new Route({
1783
+ options,
1784
+ toType: RouteType.push,
1785
+ toInput: '/other'
1786
+ });
1787
+
1788
+ sourceRoute.syncTo(targetRoute);
1789
+
1790
+ expect((targetRoute as any)._handle).toBe(
1791
+ (sourceRoute as any)._handle
1792
+ );
1793
+ expect((targetRoute as any)._handleResult).toEqual({
1794
+ success: true
1795
+ });
1796
+ expect((targetRoute as any)._handled).toBe(true);
1797
+ });
1798
+ });
1799
+
1800
+ describe('🏗️ Layer Field Tests', () => {
1801
+ describe('Layer Field Initialization', () => {
1802
+ it('should initialize layer as null for non-pushLayer route types', () => {
1803
+ const options = createOptions();
1804
+
1805
+ // Test different route types should all have null layer
1806
+ const routeTypes = [
1807
+ RouteType.push,
1808
+ RouteType.replace,
1809
+ RouteType.back,
1810
+ RouteType.forward,
1811
+ RouteType.go,
1812
+ RouteType.pushWindow,
1813
+ RouteType.replaceWindow,
1814
+ RouteType.restartApp,
1815
+ RouteType.unknown
1816
+ ];
1817
+
1818
+ routeTypes.forEach((routeType) => {
1819
+ const route = new Route({
1820
+ options,
1821
+ toType: routeType,
1822
+ toInput: {
1823
+ path: '/test',
1824
+ layer: { zIndex: 1000 }
1825
+ }
1826
+ });
1827
+
1828
+ expect(route.layer).toBeNull();
1829
+ });
1830
+ });
1831
+
1832
+ it('should set layer value for pushLayer route type', () => {
1833
+ const options = createOptions();
1834
+ const layerConfig = {
1835
+ zIndex: 1000,
1836
+ params: { userId: 123, mode: 'edit' },
1837
+ autoPush: false,
1838
+ push: true
1839
+ };
1840
+
1841
+ const route = new Route({
1842
+ options,
1843
+ toType: RouteType.pushLayer,
1844
+ toInput: {
1845
+ path: '/user/123',
1846
+ layer: layerConfig
1847
+ }
1848
+ });
1849
+
1850
+ expect(route.layer).toBe(layerConfig);
1851
+ expect(route.layer?.zIndex).toBe(1000);
1852
+ expect(route.layer?.autoPush).toBe(false);
1853
+ });
1854
+
1855
+ it('should set layer as null for pushLayer without layer config', () => {
1856
+ const options = createOptions();
1857
+
1858
+ const route = new Route({
1859
+ options,
1860
+ toType: RouteType.pushLayer,
1861
+ toInput: '/test'
1862
+ });
1863
+
1864
+ expect(route.layer).toBeNull();
1865
+ });
1866
+
1867
+ it('should set layer as null for pushLayer with string toInput', () => {
1868
+ const options = createOptions();
1869
+
1870
+ const route = new Route({
1871
+ options,
1872
+ toType: RouteType.pushLayer,
1873
+ toInput: '/test'
1874
+ });
1875
+
1876
+ expect(route.layer).toBeNull();
1877
+ });
1878
+ });
1879
+
1880
+ describe('Layer Field Non-Enumerable Property', () => {
1881
+ it('should make layer property non-enumerable', () => {
1882
+ const options = createOptions();
1883
+ const layerConfig = { zIndex: 1000 };
1884
+
1885
+ const route = new Route({
1886
+ options,
1887
+ toType: RouteType.pushLayer,
1888
+ toInput: {
1889
+ path: '/test',
1890
+ layer: layerConfig
1891
+ }
1892
+ });
1893
+
1894
+ const keys = Object.keys(route);
1895
+ const propertyNames = Object.getOwnPropertyNames(route);
1896
+ const descriptor = Object.getOwnPropertyDescriptor(
1897
+ route,
1898
+ 'layer'
1899
+ );
1900
+
1901
+ expect(keys).not.toContain('layer');
1902
+ expect(propertyNames).toContain('layer');
1903
+ expect(descriptor?.enumerable).toBe(false);
1904
+ });
1905
+
1906
+ it('should be included in NON_ENUMERABLE_PROPERTIES list', () => {
1907
+ expect(NON_ENUMERABLE_PROPERTIES).toContain('layer');
1908
+ });
1909
+ });
1910
+
1911
+ describe('Layer Field in Route Operations', () => {
1912
+ it('should preserve layer during route cloning', () => {
1913
+ const options = createOptions();
1914
+ const layerConfig = {
1915
+ zIndex: 2000,
1916
+ params: { modal: true }
1917
+ };
1918
+
1919
+ const originalRoute = new Route({
1920
+ options,
1921
+ toType: RouteType.pushLayer,
1922
+ toInput: {
1923
+ path: '/modal',
1924
+ layer: layerConfig
1925
+ }
1926
+ });
1927
+
1928
+ const clonedRoute = originalRoute.clone();
1929
+
1930
+ expect(clonedRoute.layer).toBe(layerConfig);
1931
+ expect(clonedRoute.layer).toBe(originalRoute.layer);
1932
+ });
1933
+
1934
+ it('should preserve null layer during route cloning', () => {
1935
+ const options = createOptions();
1936
+
1937
+ const originalRoute = new Route({
1938
+ options,
1939
+ toType: RouteType.push,
1940
+ toInput: '/test'
1941
+ });
1942
+
1943
+ const clonedRoute = originalRoute.clone();
1944
+
1945
+ expect(clonedRoute.layer).toBeNull();
1946
+ expect(clonedRoute.layer).toBe(originalRoute.layer);
1947
+ });
1948
+
1949
+ it('should synchronize layer field during syncTo operation', () => {
1950
+ const options = createOptions();
1951
+ const layerConfig = {
1952
+ zIndex: 3000,
1953
+ params: { popup: true }
1954
+ };
1955
+
1956
+ const sourceRoute = new Route({
1957
+ options,
1958
+ toType: RouteType.pushLayer,
1959
+ toInput: {
1960
+ path: '/popup',
1961
+ layer: layerConfig
1962
+ }
1963
+ });
1964
+
1965
+ const targetRoute = new Route({
1966
+ options,
1967
+ toType: RouteType.push,
1968
+ toInput: '/target'
1969
+ });
1970
+
1971
+ sourceRoute.syncTo(targetRoute);
1972
+
1973
+ expect(targetRoute.layer).toBe(layerConfig);
1974
+ expect(targetRoute.layer).toBe(sourceRoute.layer);
1975
+ });
1976
+
1977
+ it('should handle complex layer configuration', () => {
1978
+ const options = createOptions();
1979
+ const complexLayerConfig = {
1980
+ zIndex: 5000,
1981
+ params: {
1982
+ userId: 456,
1983
+ permissions: ['read', 'write'],
1984
+ metadata: {
1985
+ title: 'User Settings',
1986
+ description: 'Edit user profile'
1987
+ }
1988
+ },
1989
+ shouldClose: () => true,
1990
+ autoPush: true,
1991
+ push: false,
1992
+ routerOptions: {
1993
+ mode: RouterMode.memory
1994
+ }
1995
+ };
1996
+
1997
+ const route = new Route({
1998
+ options,
1999
+ toType: RouteType.pushLayer,
2000
+ toInput: {
2001
+ path: '/settings',
2002
+ layer: complexLayerConfig
2003
+ }
2004
+ });
2005
+
2006
+ expect(route.layer).toBe(complexLayerConfig);
2007
+ expect(typeof route.layer?.shouldClose).toBe('function');
2008
+ expect(route.layer?.routerOptions?.mode).toBe(
2009
+ RouterMode.memory
2010
+ );
2011
+ });
2012
+ });
2013
+ });
2014
+ });