@esmx/router 3.0.0-rc.18 → 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 (158) 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 -30
  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/types/index.d.ts +0 -694
  120. package/dist/types/index.mjs +0 -6
  121. package/dist/utils/bom.d.ts +0 -5
  122. package/dist/utils/bom.mjs +0 -10
  123. package/dist/utils/encoding.d.ts +0 -48
  124. package/dist/utils/encoding.mjs +0 -44
  125. package/dist/utils/guards.d.ts +0 -9
  126. package/dist/utils/guards.mjs +0 -12
  127. package/dist/utils/index.d.ts +0 -7
  128. package/dist/utils/index.mjs +0 -27
  129. package/dist/utils/path.d.ts +0 -60
  130. package/dist/utils/path.mjs +0 -282
  131. package/dist/utils/path.spec.mjs +0 -27
  132. package/dist/utils/scroll.d.ts +0 -25
  133. package/dist/utils/scroll.mjs +0 -59
  134. package/dist/utils/utils.d.ts +0 -16
  135. package/dist/utils/utils.mjs +0 -11
  136. package/dist/utils/warn.d.ts +0 -2
  137. package/dist/utils/warn.mjs +0 -12
  138. package/src/history/abstract.ts +0 -149
  139. package/src/history/base.ts +0 -408
  140. package/src/history/html.ts +0 -228
  141. package/src/history/index.ts +0 -20
  142. package/src/matcher/create-matcher.spec.ts +0 -3
  143. package/src/matcher/create-matcher.ts +0 -292
  144. package/src/matcher/index.ts +0 -1
  145. package/src/task-pipe/index.ts +0 -1
  146. package/src/task-pipe/task.ts +0 -97
  147. package/src/types/index.ts +0 -858
  148. package/src/utils/bom.ts +0 -14
  149. package/src/utils/encoding.ts +0 -153
  150. package/src/utils/guards.ts +0 -25
  151. package/src/utils/index.ts +0 -27
  152. package/src/utils/path.spec.ts +0 -32
  153. package/src/utils/path.ts +0 -418
  154. package/src/utils/scroll.ts +0 -120
  155. package/src/utils/utils.ts +0 -30
  156. package/src/utils/warn.ts +0 -13
  157. /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
  158. /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
@@ -0,0 +1,783 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { Router } from './router';
6
+ import { RouteType, RouterMode } from './types';
7
+ import type { Route, RouteLocationInput, RouterOptions } from './types';
8
+
9
+ describe('Router.restartApp Focused Tests', () => {
10
+ let router: Router;
11
+ let mockApps: Record<string, ReturnType<typeof vi.fn>>;
12
+
13
+ beforeEach(async () => {
14
+ mockApps = {
15
+ home: vi.fn(() => ({ mount: vi.fn(), unmount: vi.fn() })),
16
+ about: vi.fn(() => ({ mount: vi.fn(), unmount: vi.fn() })),
17
+ user: vi.fn(() => ({ mount: vi.fn(), unmount: vi.fn() })),
18
+ products: vi.fn(() => ({ mount: vi.fn(), unmount: vi.fn() }))
19
+ };
20
+
21
+ const options: RouterOptions = {
22
+ routes: [
23
+ { path: '/', app: 'home' },
24
+ { path: '/about', app: 'about' },
25
+ { path: '/user/:id', app: 'user' },
26
+ { path: '/products/:category', app: 'products' }
27
+ ],
28
+ apps: mockApps
29
+ };
30
+
31
+ router = new Router(options);
32
+ await router.push('/');
33
+ });
34
+
35
+ afterEach(() => {
36
+ router.destroy();
37
+ vi.clearAllMocks();
38
+ });
39
+
40
+ describe('🎯 Core Functionality Tests', () => {
41
+ it('should support parameterless restart (restart to current path)', async () => {
42
+ await router.push('/user/123');
43
+ expect(router.route.url.pathname).toBe('/user/123');
44
+
45
+ // Parameterless restart
46
+ const result = await router.restartApp();
47
+
48
+ expect(result.type).toBe(RouteType.restartApp);
49
+ expect(result.url.pathname).toBe('/user/123');
50
+ expect(result.handle).not.toBeNull();
51
+ expect(router.route).toBe(result);
52
+ });
53
+
54
+ it('should support string path restart', async () => {
55
+ const result = await router.restartApp('/about');
56
+
57
+ expect(result.type).toBe(RouteType.restartApp);
58
+ expect(result.url.pathname).toBe('/about');
59
+ expect(result.handle).not.toBeNull();
60
+ expect(router.route).toBe(result);
61
+ });
62
+
63
+ it('should support object parameter restart', async () => {
64
+ const result = await router.restartApp({
65
+ path: '/user/456',
66
+ query: { tab: 'profile', mode: 'edit' },
67
+ hash: '#section1'
68
+ });
69
+
70
+ expect(result.type).toBe(RouteType.restartApp);
71
+ expect(result.url.pathname).toBe('/user/456');
72
+ expect(result.query.tab).toBe('profile');
73
+ expect(result.query.mode).toBe('edit');
74
+ expect(result.url.hash).toBe('#section1');
75
+ expect(result.params.id).toBe('456');
76
+ expect(result.handle).not.toBeNull();
77
+ });
78
+
79
+ it('should support restart with state', async () => {
80
+ const customState = { userId: 123, preferences: { theme: 'dark' } };
81
+ const result = await router.restartApp({
82
+ path: '/about',
83
+ state: customState
84
+ });
85
+
86
+ // State will be merged, including system-added fields
87
+ expect(result.state).toEqual(expect.objectContaining(customState));
88
+ const state = result.state as typeof customState;
89
+ expect(state.userId).toBe(123);
90
+ expect(state.preferences.theme).toBe('dark');
91
+ });
92
+
93
+ it('should correctly handle route parameters', async () => {
94
+ const result = await router.restartApp('/user/789');
95
+
96
+ expect(result.params.id).toBe('789');
97
+ expect(result.matched.length).toBeGreaterThan(0);
98
+ expect(result.matched[0].path).toBe('/user/:id');
99
+ });
100
+
101
+ it('should correctly handle query parameters', async () => {
102
+ const result = await router.restartApp('/about?tab=info&mode=edit');
103
+
104
+ expect(result.query.tab).toBe('info');
105
+ expect(result.query.mode).toBe('edit');
106
+ expect(result.url.search).toBe('?tab=info&mode=edit');
107
+ });
108
+
109
+ it('should correctly handle hash', async () => {
110
+ const result = await router.restartApp('/about#section2');
111
+
112
+ expect(result.url.hash).toBe('#section2');
113
+ });
114
+ });
115
+
116
+ describe('🔄 restartApp-Specific Behavior Tests', () => {
117
+ it('should force update MicroApp (even with same application)', async () => {
118
+ await router.push('/about');
119
+ const firstCallCount = mockApps.about.mock.calls.length;
120
+
121
+ // Restart to same path, should force update
122
+ await router.restartApp('/about');
123
+ const secondCallCount = mockApps.about.mock.calls.length;
124
+
125
+ expect(secondCallCount).toBeGreaterThan(firstCallCount);
126
+ });
127
+
128
+ it('should call navigation.replace instead of push', async () => {
129
+ const replaceSpy = vi.spyOn(router.navigation, 'replace');
130
+ const pushSpy = vi.spyOn(router.navigation, 'push');
131
+
132
+ await router.restartApp('/about');
133
+
134
+ expect(replaceSpy).toHaveBeenCalled();
135
+ expect(pushSpy).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it('should correctly update router current route state', async () => {
139
+ const result = await router.restartApp('/about');
140
+
141
+ expect(router.route.url.pathname).toBe('/about');
142
+ expect(router.route.type).toBe(RouteType.restartApp);
143
+ expect(router.route).toBe(result);
144
+ });
145
+
146
+ it('should differentiate from other routing method behaviors', async () => {
147
+ // push navigation
148
+ await router.push('/user/123');
149
+ expect(router.route.type).toBe(RouteType.push);
150
+
151
+ // replace navigation
152
+ await router.replace('/about');
153
+ expect(router.route.type).toBe(RouteType.replace);
154
+
155
+ // restartApp navigation
156
+ const result = await router.restartApp('/products/electronics');
157
+ expect(result.type).toBe(RouteType.restartApp);
158
+ expect(router.route.type).toBe(RouteType.restartApp);
159
+ });
160
+ });
161
+
162
+ describe('🎭 Edge Cases Tests', () => {
163
+ it('should handle non-existent routes', async () => {
164
+ const result = await router.restartApp('/nonexistent');
165
+
166
+ expect(result.matched.length).toBe(0);
167
+ expect(result.config).toBeNull();
168
+ expect(result.url.pathname).toBe('/nonexistent');
169
+ expect(result.type).toBe(RouteType.restartApp);
170
+ });
171
+
172
+ it('should handle empty string path', async () => {
173
+ const result = await router.restartApp('');
174
+
175
+ expect(result.url.pathname).toBe('/');
176
+ expect(result.type).toBe(RouteType.restartApp);
177
+ });
178
+
179
+ it('should handle root path', async () => {
180
+ const result = await router.restartApp('/');
181
+
182
+ expect(result.url.pathname).toBe('/');
183
+ expect(result.matched.length).toBeGreaterThan(0);
184
+ expect(result.type).toBe(RouteType.restartApp);
185
+ });
186
+
187
+ it('should handle complex query parameters', async () => {
188
+ const result = await router.restartApp('/about?a=1&b=2&a=3&c=');
189
+
190
+ // Query parameter handling may vary by implementation, testing basic functionality here
191
+ expect(result.query.b).toBe('2');
192
+ expect(result.query.c).toBe('');
193
+ expect(result.url.search).toContain('a=');
194
+ expect(result.url.search).toContain('b=2');
195
+ });
196
+
197
+ it('should handle special character paths', async () => {
198
+ const result = await router.restartApp('/user/测试用户');
199
+
200
+ expect(result.url.pathname).toContain('/user/');
201
+ // Parameters may be URL encoded, need to decode
202
+ expect(decodeURIComponent(result.params.id)).toBe('测试用户');
203
+ });
204
+ });
205
+
206
+ describe('🔗 URL Parsing Tests', () => {
207
+ it('should correctly parse absolute paths', async () => {
208
+ await router.push('/user/123');
209
+
210
+ // Restart to absolute path
211
+ const result = await router.restartApp('/about');
212
+
213
+ expect(result.url.pathname).toBe('/about');
214
+ expect(result.url.href).toMatch(/\/about$/);
215
+ });
216
+
217
+ it('should correctly handle relative paths', async () => {
218
+ await router.push('/user/123');
219
+
220
+ const result = await router.restartApp('456');
221
+
222
+ // Relative path handling depends on current base URL implementation
223
+ expect(result.url.pathname).toContain('456');
224
+ if (
225
+ result.matched.length > 0 &&
226
+ result.matched[0].path === '/user/:id'
227
+ ) {
228
+ expect(result.params.id).toBe('456');
229
+ }
230
+ });
231
+
232
+ it('should correctly handle complete URLs', async () => {
233
+ const result = await router.restartApp('http://example.com/test');
234
+
235
+ expect(result.url.href).toBe('http://example.com/test');
236
+ expect(result.url.pathname).toBe('/test');
237
+ });
238
+ });
239
+
240
+ describe('🎯 Type Overload Tests', () => {
241
+ it('should support parameterless calls', async () => {
242
+ await router.push('/user/123');
243
+
244
+ const result: Route = await router.restartApp();
245
+
246
+ expect(result.url.pathname).toBe('/user/123');
247
+ expect(result.type).toBe(RouteType.restartApp);
248
+ });
249
+
250
+ it('should support string parameter calls', async () => {
251
+ const result: Route = await router.restartApp('/about');
252
+
253
+ expect(result.url.pathname).toBe('/about');
254
+ expect(result.type).toBe(RouteType.restartApp);
255
+ });
256
+
257
+ it('should support object parameter calls', async () => {
258
+ const routeLocation: RouteLocationInput = {
259
+ path: '/user/456',
260
+ query: { tab: 'settings' }
261
+ };
262
+
263
+ const result: Route = await router.restartApp(routeLocation);
264
+
265
+ expect(result.url.pathname).toBe('/user/456');
266
+ expect(result.query.tab).toBe('settings');
267
+ expect(result.type).toBe(RouteType.restartApp);
268
+ });
269
+ });
270
+
271
+ describe('🔄 Multiple Restart Tests', () => {
272
+ it('should support consecutive multiple restarts', async () => {
273
+ const paths = ['/about', '/user/123', '/products/electronics', '/'];
274
+
275
+ for (const path of paths) {
276
+ const result = await router.restartApp(path);
277
+ expect(result.type).toBe(RouteType.restartApp);
278
+ expect(result.handle).not.toBeNull();
279
+ expect(router.route).toBe(result);
280
+ }
281
+ });
282
+
283
+ it('should create new application instances on each restart', async () => {
284
+ await router.restartApp('/about');
285
+ const firstCallCount = mockApps.about.mock.calls.length;
286
+
287
+ await router.restartApp('/about');
288
+ const secondCallCount = mockApps.about.mock.calls.length;
289
+
290
+ await router.restartApp('/about');
291
+ const thirdCallCount = mockApps.about.mock.calls.length;
292
+
293
+ expect(secondCallCount).toBeGreaterThan(firstCallCount);
294
+ expect(thirdCallCount).toBeGreaterThan(secondCallCount);
295
+ });
296
+ });
297
+
298
+ describe('🎨 Status Code Handling Tests', () => {
299
+ it('should support custom status codes', async () => {
300
+ const result = await router.restartApp({
301
+ path: '/about',
302
+ statusCode: 201
303
+ });
304
+
305
+ expect(result.statusCode).toBe(201);
306
+ });
307
+
308
+ it('should maintain default status code as null', async () => {
309
+ const result = await router.restartApp('/about');
310
+
311
+ expect(result.statusCode).toBeNull();
312
+ });
313
+ });
314
+
315
+ describe('🔧 Consistency Tests with resolve Method', () => {
316
+ it('should maintain consistency with resolve method results in URL parsing', async () => {
317
+ const resolvedRoute = router.resolve('/user/789');
318
+ const restartedRoute = await router.restartApp('/user/789');
319
+
320
+ expect(restartedRoute.url.href).toBe(resolvedRoute.url.href);
321
+ expect(restartedRoute.params).toEqual(resolvedRoute.params);
322
+ expect(restartedRoute.matched).toEqual(resolvedRoute.matched);
323
+ expect(restartedRoute.type).toBe(RouteType.restartApp);
324
+ expect(resolvedRoute.type).toBe(RouteType.push);
325
+ });
326
+ });
327
+
328
+ describe('🛡️ Route Guards Integration Tests', () => {
329
+ let guardExecutionLog: string[];
330
+
331
+ beforeEach(() => {
332
+ guardExecutionLog = [];
333
+ });
334
+
335
+ it('should correctly execute beforeEach guards', async () => {
336
+ const unregister = router.beforeEach(async (to, from) => {
337
+ guardExecutionLog.push(
338
+ `beforeEach-${to.path}-from-${from?.path || 'null'}`
339
+ );
340
+ });
341
+
342
+ await router.restartApp('/about');
343
+
344
+ expect(guardExecutionLog).toContain('beforeEach-/about-from-/');
345
+ unregister();
346
+ });
347
+
348
+ it('should correctly execute afterEach guards', async () => {
349
+ const unregister = router.afterEach((to, from) => {
350
+ guardExecutionLog.push(
351
+ `afterEach-${to.path}-from-${from?.path || 'null'}`
352
+ );
353
+ });
354
+
355
+ await router.restartApp('/about');
356
+
357
+ expect(guardExecutionLog).toContain('afterEach-/about-from-/');
358
+ unregister();
359
+ });
360
+
361
+ it('should abort restart when beforeEach guard returns false', async () => {
362
+ const unregister = router.beforeEach(async (to, from) => {
363
+ if (to.path === '/about') {
364
+ return false;
365
+ }
366
+ });
367
+
368
+ await expect(router.restartApp('/about')).rejects.toThrow();
369
+ expect(router.route.path).toBe('/'); // Should maintain original route
370
+ unregister();
371
+ });
372
+
373
+ it('should support guard redirects', async () => {
374
+ const unregister = router.beforeEach(async (to, from) => {
375
+ if (to.path === '/about') {
376
+ return '/user/redirected';
377
+ }
378
+ });
379
+
380
+ const result = await router.restartApp('/about');
381
+
382
+ expect(result.path).toBe('/user/redirected');
383
+ expect(result.params.id).toBe('redirected');
384
+ expect(result.handle).not.toBeNull();
385
+ unregister();
386
+ });
387
+ });
388
+
389
+ describe('🧩 Async Component Handling Tests', () => {
390
+ let asyncRouter: Router;
391
+
392
+ beforeEach(async () => {
393
+ const asyncOptions: RouterOptions = {
394
+ routes: [
395
+ {
396
+ path: '/',
397
+ app: 'home',
398
+ component: () => 'HomeComponent'
399
+ },
400
+ {
401
+ path: '/async',
402
+ app: 'async',
403
+ asyncComponent: async () => {
404
+ await new Promise((resolve) =>
405
+ setTimeout(resolve, 10)
406
+ );
407
+ return () => 'AsyncComponent';
408
+ }
409
+ },
410
+ {
411
+ path: '/async-error',
412
+ app: 'async-error',
413
+ asyncComponent: async () => {
414
+ throw new Error('Component load failed');
415
+ }
416
+ }
417
+ ],
418
+ apps: mockApps
419
+ };
420
+
421
+ asyncRouter = new Router(asyncOptions);
422
+ await asyncRouter.push('/');
423
+ });
424
+
425
+ afterEach(() => {
426
+ asyncRouter.destroy();
427
+ });
428
+
429
+ it('should correctly handle async component loading', async () => {
430
+ const result = await asyncRouter.restartApp('/async');
431
+
432
+ expect(result.handle).not.toBeNull();
433
+ expect(result.matched[0].component).toBeDefined();
434
+ expect(typeof result.matched[0].component).toBe('function');
435
+ });
436
+
437
+ it('should handle async component loading failures', async () => {
438
+ await expect(
439
+ asyncRouter.restartApp('/async-error')
440
+ ).rejects.toThrow();
441
+ });
442
+ });
443
+
444
+ describe('⚡ Task Cancellation and Concurrency Control Tests', () => {
445
+ it('should cancel tasks interrupted by new restartApp calls', async () => {
446
+ const unregister = router.beforeEach(async (to, from) => {
447
+ if (to.path === '/user/slow') {
448
+ await new Promise((resolve) => setTimeout(resolve, 50));
449
+ }
450
+ });
451
+
452
+ // Rapidly consecutive restartApp calls
453
+ const promises = [
454
+ router.restartApp('/user/slow').catch((err) => err),
455
+ router.restartApp('/about').catch((err) => err)
456
+ ];
457
+ const results = await Promise.all(promises);
458
+
459
+ expect(results[0] instanceof Error).toBe(true);
460
+ expect(results[1].handle).not.toBeNull();
461
+ expect(router.route.path).toBe('/about');
462
+
463
+ unregister();
464
+ });
465
+
466
+ it('should correctly handle multiple concurrent restartApp calls', async () => {
467
+ const paths = ['/user/1', '/user/2', '/user/3'];
468
+ const promises = paths.map((path) =>
469
+ router.restartApp(path).catch((err) => err)
470
+ );
471
+ const results = await Promise.all(promises);
472
+
473
+ expect(results[0] instanceof Error).toBe(true);
474
+ expect(results[1] instanceof Error).toBe(true);
475
+ expect(results[2].handle).not.toBeNull();
476
+ expect(router.route.path).toBe('/user/3');
477
+ });
478
+ });
479
+
480
+ describe('🌍 Route Override Tests', () => {
481
+ let overrideRouter: Router;
482
+
483
+ beforeEach(async () => {
484
+ const overrideOptions: RouterOptions = {
485
+ routes: [
486
+ {
487
+ path: '/',
488
+ app: 'home',
489
+ component: () => 'HomeComponent'
490
+ },
491
+ {
492
+ path: '/override-test',
493
+ app: 'override',
494
+ component: () => 'OverrideComponent',
495
+ override: (to, from) => {
496
+ if (to.query.native === 'true') {
497
+ return async () => {
498
+ return { native: true, path: to.path };
499
+ };
500
+ }
501
+ }
502
+ },
503
+ {
504
+ path: '/hybrid-page',
505
+ app: 'hybrid',
506
+ component: () => 'HybridComponent',
507
+ override: (to, from) => {
508
+ // Always override for this test
509
+ return async () => {
510
+ return {
511
+ hybrid: 'native',
512
+ component: 'NativeComponent'
513
+ };
514
+ };
515
+ }
516
+ }
517
+ ],
518
+ apps: mockApps
519
+ };
520
+
521
+ overrideRouter = new Router(overrideOptions);
522
+ await overrideRouter.push('/');
523
+ });
524
+
525
+ afterEach(() => {
526
+ overrideRouter.destroy();
527
+ });
528
+
529
+ it('should use override when condition is met', async () => {
530
+ const result = await overrideRouter.restartApp(
531
+ '/override-test?native=true'
532
+ );
533
+
534
+ expect(result.handle).not.toBeNull();
535
+ // restartApp doesn't execute override tasks, so handleResult should be undefined
536
+ expect(result.handleResult).toBeUndefined();
537
+ expect(result.config?.override).toBeDefined();
538
+ });
539
+
540
+ it('should use default behavior when override returns nothing', async () => {
541
+ const result = await overrideRouter.restartApp(
542
+ '/override-test?native=false'
543
+ );
544
+
545
+ expect(result.handle).not.toBeNull();
546
+ expect(result.handle).not.toBeNull();
547
+ });
548
+
549
+ it('should always use override when function always returns handler', async () => {
550
+ const result = await overrideRouter.restartApp('/hybrid-page');
551
+
552
+ expect(result.handle).not.toBeNull();
553
+ // restartApp doesn't execute override tasks, so handleResult should be undefined
554
+ expect(result.handleResult).toBeUndefined();
555
+ expect(result.config?.override).toBeDefined();
556
+ });
557
+ });
558
+
559
+ describe('❌ Error Handling and Exception Scenario Tests', () => {
560
+ it('should handle exceptions thrown in guards', async () => {
561
+ const unregister = router.beforeEach(async (to, from) => {
562
+ if (to.path === '/about') {
563
+ throw new Error('Guard error');
564
+ }
565
+ });
566
+
567
+ await expect(router.restartApp('/about')).rejects.toThrow(
568
+ 'Guard error'
569
+ );
570
+ unregister();
571
+ });
572
+
573
+ it('should handle MicroApp update exceptions', async () => {
574
+ const originalUpdate = router.microApp._update;
575
+ router.microApp._update = vi.fn().mockImplementation(() => {
576
+ throw new Error('MicroApp update failed');
577
+ });
578
+
579
+ // MicroApp update exceptions will cause entire route handling to fail
580
+ await expect(router.restartApp('/about')).rejects.toThrow(
581
+ 'MicroApp update failed'
582
+ );
583
+
584
+ router.microApp._update = originalUpdate;
585
+ });
586
+
587
+ it('should handle navigation.replace exceptions', async () => {
588
+ const originalReplace = router.navigation.replace;
589
+ router.navigation.replace = vi.fn().mockImplementation(() => {
590
+ throw new Error('Navigation replace failed');
591
+ });
592
+
593
+ // navigation.replace exceptions will cause entire route handling to fail
594
+ await expect(router.restartApp('/about')).rejects.toThrow(
595
+ 'Navigation replace failed'
596
+ );
597
+
598
+ router.navigation.replace = originalReplace;
599
+ });
600
+ });
601
+
602
+ describe('🔄 Route Lifecycle Integrity Tests', () => {
603
+ let lifecycleRouter: Router;
604
+ let lifecycleLog: string[];
605
+
606
+ beforeEach(async () => {
607
+ lifecycleLog = [];
608
+
609
+ const lifecycleOptions: RouterOptions = {
610
+ routes: [
611
+ {
612
+ path: '/',
613
+ app: 'home',
614
+ component: () => 'HomeComponent',
615
+ beforeLeave: async (to, from) => {
616
+ lifecycleLog.push('home-beforeLeave');
617
+ }
618
+ },
619
+ {
620
+ path: '/lifecycle',
621
+ app: 'lifecycle',
622
+ component: () => 'LifecycleComponent',
623
+ beforeEnter: async (to, from) => {
624
+ lifecycleLog.push('lifecycle-beforeEnter');
625
+ },
626
+ beforeUpdate: async (to, from) => {
627
+ lifecycleLog.push('lifecycle-beforeUpdate');
628
+ },
629
+ beforeLeave: async (to, from) => {
630
+ lifecycleLog.push('lifecycle-beforeLeave');
631
+ }
632
+ }
633
+ ],
634
+ apps: mockApps
635
+ };
636
+
637
+ lifecycleRouter = new Router(lifecycleOptions);
638
+ await lifecycleRouter.push('/');
639
+ });
640
+
641
+ afterEach(() => {
642
+ lifecycleRouter.destroy();
643
+ });
644
+
645
+ it('should correctly execute complete route lifecycle', async () => {
646
+ // Global guards
647
+ const unregisterBefore = lifecycleRouter.beforeEach((to, from) => {
648
+ lifecycleLog.push(`global-beforeEach-${to.path}`);
649
+ });
650
+ const unregisterAfter = lifecycleRouter.afterEach((to, from) => {
651
+ lifecycleLog.push(`global-afterEach-${to.path}`);
652
+ });
653
+
654
+ await lifecycleRouter.restartApp('/lifecycle');
655
+
656
+ expect(lifecycleLog).toEqual([
657
+ 'home-beforeLeave',
658
+ 'global-beforeEach-/lifecycle',
659
+ 'lifecycle-beforeEnter',
660
+ 'global-afterEach-/lifecycle'
661
+ ]);
662
+
663
+ unregisterBefore();
664
+ unregisterAfter();
665
+ });
666
+
667
+ it('should execute beforeUpdate when restarting same route', async () => {
668
+ await lifecycleRouter.push('/lifecycle');
669
+ lifecycleLog = []; // Clear log
670
+
671
+ // Restart to same route with different parameters
672
+ await lifecycleRouter.restartApp('/lifecycle?version=2');
673
+
674
+ expect(lifecycleLog).toContain('lifecycle-beforeUpdate');
675
+ expect(lifecycleLog).not.toContain('lifecycle-beforeEnter');
676
+ });
677
+ });
678
+
679
+ describe('🎯 Special Route Configuration Tests', () => {
680
+ it('should handle routes with custom location handler', async () => {
681
+ let locationCalled = false;
682
+ const customLocationRouter = new Router({
683
+ routes: [{ path: '/', app: 'home' }],
684
+ apps: mockApps,
685
+ fallback: (to, from) => {
686
+ locationCalled = true;
687
+ return { customLocation: true, path: to.path };
688
+ }
689
+ });
690
+
691
+ await customLocationRouter.push('/');
692
+ const result =
693
+ await customLocationRouter.restartApp('/nonexistent');
694
+
695
+ // Location handler should be called because route doesn't exist
696
+ expect(locationCalled).toBe(true);
697
+ expect(result.matched.length).toBe(0); // Non-existent route
698
+ expect(typeof result.handle).toBe('function');
699
+ expect(result.handleResult).toEqual({
700
+ customLocation: true,
701
+ path: '/nonexistent'
702
+ });
703
+ customLocationRouter.destroy();
704
+ });
705
+
706
+ it('should handle complex nested route restarts', async () => {
707
+ const nestedRouter = new Router({
708
+ routes: [
709
+ {
710
+ path: '/',
711
+ app: 'home',
712
+ children: [
713
+ {
714
+ path: 'nested/:id',
715
+ app: 'nested',
716
+ children: [
717
+ {
718
+ path: 'deep/:subId',
719
+ app: 'deep'
720
+ }
721
+ ]
722
+ }
723
+ ]
724
+ }
725
+ ],
726
+ apps: mockApps
727
+ });
728
+
729
+ await nestedRouter.push('/');
730
+ const result = await nestedRouter.restartApp(
731
+ '/nested/123/deep/456'
732
+ );
733
+
734
+ expect(result.params.id).toBe('123');
735
+ expect(result.params.subId).toBe('456');
736
+ expect(result.matched.length).toBe(3); // Three levels of nesting
737
+ nestedRouter.destroy();
738
+ });
739
+ });
740
+
741
+ describe('📊 Performance and Memory Tests', () => {
742
+ it('should correctly clean up resources after extensive restarts', async () => {
743
+ const initialAppsCallCount = Object.values(mockApps).reduce(
744
+ (sum, app) => sum + app.mock.calls.length,
745
+ 0
746
+ );
747
+
748
+ // Execute extensive restart operations
749
+ for (let i = 0; i < 50; i++) {
750
+ await router.restartApp(`/user/${i}`);
751
+ }
752
+
753
+ const finalAppsCallCount = Object.values(mockApps).reduce(
754
+ (sum, app) => sum + app.mock.calls.length,
755
+ 0
756
+ );
757
+
758
+ expect(finalAppsCallCount).toBeGreaterThan(initialAppsCallCount);
759
+
760
+ expect(router.route.params.id).toBe('49');
761
+ });
762
+
763
+ it('should correctly handle rapid consecutive restart calls', async () => {
764
+ const startTime = Date.now();
765
+
766
+ // Rapid consecutive calls
767
+ const promises = Array.from({ length: 10 }, (_, i) =>
768
+ router.restartApp(`/user/${i}`).catch((err) => err)
769
+ );
770
+
771
+ const results = await Promise.all(promises);
772
+ const endTime = Date.now();
773
+
774
+ const successfulResults = results.filter(
775
+ (r) => !(r instanceof Error) && r.handle !== null
776
+ );
777
+ expect(successfulResults).toHaveLength(1);
778
+ expect(successfulResults[0].params.id).toBe('9');
779
+
780
+ expect(endTime - startTime).toBeLessThan(1000);
781
+ });
782
+ });
783
+ });