@esmx/router 3.0.0-rc.18 → 3.0.0-rc.20

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,595 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import type { Route } from './route';
3
+ import { Router } from './router';
4
+ import { RouterMode } from './types';
5
+ import { removeFromArray } from './util';
6
+
7
+ describe('Router Guards Cleanup Tests', () => {
8
+ let router: Router;
9
+
10
+ beforeEach(async () => {
11
+ router = new Router({
12
+ mode: RouterMode.memory,
13
+ base: new URL('http://localhost:3000/'),
14
+ routes: [
15
+ {
16
+ path: '/',
17
+ component: () => 'Home'
18
+ },
19
+ {
20
+ path: '/test1',
21
+ component: () => 'Test1'
22
+ },
23
+ {
24
+ path: '/test2',
25
+ component: () => 'Test2'
26
+ },
27
+ {
28
+ path: '/test3',
29
+ component: () => 'Test3'
30
+ }
31
+ ]
32
+ });
33
+
34
+ await router.replace('/');
35
+ });
36
+
37
+ afterEach(() => {
38
+ router.destroy();
39
+ });
40
+
41
+ describe('🔥 Guard Cleanup Effect Verification', () => {
42
+ describe('beforeEach cleanup effects', () => {
43
+ test('guard should not execute after cleanup', async () => {
44
+ const spy = vi.fn();
45
+ const unregister = router.beforeEach(spy);
46
+
47
+ await router.push('/test1');
48
+ expect(spy).toHaveBeenCalledTimes(1);
49
+
50
+ // Cleanup guard
51
+ unregister();
52
+
53
+ await router.push('/test2');
54
+ expect(spy).toHaveBeenCalledTimes(1); // Still 1 time, no increase
55
+ });
56
+
57
+ test('guard should not execute after cleanup for multiple navigations', async () => {
58
+ const spy = vi.fn();
59
+ const unregister = router.beforeEach(spy);
60
+
61
+ await router.push('/test1');
62
+ expect(spy).toHaveBeenCalledTimes(1);
63
+
64
+ unregister();
65
+
66
+ await router.push('/test2');
67
+ await router.push('/test3');
68
+ await router.push('/');
69
+ expect(spy).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ test('async guard should not execute after cleanup', async () => {
73
+ const spy = vi.fn();
74
+ const asyncGuard = async (to: Route, from: Route | null) => {
75
+ await new Promise((resolve) => setTimeout(resolve, 10));
76
+ spy(to, from);
77
+ };
78
+
79
+ const unregister = router.beforeEach(asyncGuard);
80
+
81
+ await router.push('/test1');
82
+ expect(spy).toHaveBeenCalledTimes(1);
83
+
84
+ unregister();
85
+
86
+ await router.push('/test2');
87
+ expect(spy).toHaveBeenCalledTimes(1);
88
+ });
89
+ });
90
+
91
+ describe('afterEach cleanup effects', () => {
92
+ test('guard should not execute after cleanup', async () => {
93
+ const spy = vi.fn();
94
+ const unregister = router.afterEach(spy);
95
+
96
+ await router.push('/test1');
97
+ expect(spy).toHaveBeenCalledTimes(1);
98
+
99
+ unregister();
100
+
101
+ await router.push('/test2');
102
+ expect(spy).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ test('guard should not execute after cleanup for multiple navigations', async () => {
106
+ const spy = vi.fn();
107
+ const unregister = router.afterEach(spy);
108
+
109
+ await router.push('/test1');
110
+ expect(spy).toHaveBeenCalledTimes(1);
111
+
112
+ unregister();
113
+
114
+ await router.push('/test2');
115
+ await router.push('/test3');
116
+ await router.push('/');
117
+ expect(spy).toHaveBeenCalledTimes(1);
118
+ });
119
+ });
120
+ });
121
+
122
+ describe('⚡ Multiple Guards Independent Cleanup', () => {
123
+ describe('beforeEach multiple guards cleanup', () => {
124
+ test('cleaning single guard should not affect other guards', async () => {
125
+ const spy1 = vi.fn();
126
+ const spy2 = vi.fn();
127
+ const spy3 = vi.fn();
128
+
129
+ const unregister1 = router.beforeEach(spy1);
130
+ const unregister2 = router.beforeEach(spy2);
131
+ const unregister3 = router.beforeEach(spy3);
132
+
133
+ await router.push('/test1');
134
+ expect(spy1).toHaveBeenCalledTimes(1);
135
+ expect(spy2).toHaveBeenCalledTimes(1);
136
+ expect(spy3).toHaveBeenCalledTimes(1);
137
+
138
+ unregister2();
139
+
140
+ await router.push('/test2');
141
+ expect(spy1).toHaveBeenCalledTimes(2);
142
+ expect(spy2).toHaveBeenCalledTimes(1); // No increase
143
+ expect(spy3).toHaveBeenCalledTimes(2);
144
+
145
+ // Cleanup remaining guards
146
+ unregister1();
147
+ unregister3();
148
+ });
149
+
150
+ test('cleaning multiple guards should work correctly', async () => {
151
+ const spy1 = vi.fn();
152
+ const spy2 = vi.fn();
153
+ const spy3 = vi.fn();
154
+ const spy4 = vi.fn();
155
+
156
+ const unregister1 = router.beforeEach(spy1);
157
+ const unregister2 = router.beforeEach(spy2);
158
+ const unregister3 = router.beforeEach(spy3);
159
+ const unregister4 = router.beforeEach(spy4);
160
+
161
+ await router.push('/test1');
162
+ expect(spy1).toHaveBeenCalledTimes(1);
163
+ expect(spy2).toHaveBeenCalledTimes(1);
164
+ expect(spy3).toHaveBeenCalledTimes(1);
165
+ expect(spy4).toHaveBeenCalledTimes(1);
166
+
167
+ // Cleanup 1st and 3rd guards
168
+ unregister1();
169
+ unregister3();
170
+
171
+ spy1.mockClear();
172
+ spy2.mockClear();
173
+ spy3.mockClear();
174
+ spy4.mockClear();
175
+
176
+ await router.push('/test2');
177
+ expect(spy1).not.toHaveBeenCalled();
178
+ expect(spy2).toHaveBeenCalledTimes(1);
179
+ expect(spy3).not.toHaveBeenCalled();
180
+ expect(spy4).toHaveBeenCalledTimes(1);
181
+
182
+ // Cleanup remaining guards
183
+ unregister2();
184
+ unregister4();
185
+ });
186
+
187
+ test('guard execution order should remain correct after cleanup', async () => {
188
+ const executionOrder: string[] = [];
189
+
190
+ const guard1 = () => {
191
+ executionOrder.push('guard1');
192
+ };
193
+ const guard2 = () => {
194
+ executionOrder.push('guard2');
195
+ };
196
+ const guard3 = () => {
197
+ executionOrder.push('guard3');
198
+ };
199
+
200
+ router.beforeEach(guard1);
201
+ const unregister2 = router.beforeEach(guard2);
202
+ router.beforeEach(guard3);
203
+
204
+ await router.push('/test1');
205
+ expect(executionOrder).toEqual(['guard1', 'guard2', 'guard3']);
206
+
207
+ executionOrder.length = 0;
208
+ unregister2(); // Cleanup middle guard
209
+
210
+ await router.push('/test2');
211
+ expect(executionOrder).toEqual(['guard1', 'guard3']);
212
+ });
213
+ });
214
+
215
+ describe('afterEach multiple guards cleanup', () => {
216
+ test('cleaning single afterEach guard should not affect other guards', async () => {
217
+ const spy1 = vi.fn();
218
+ const spy2 = vi.fn();
219
+ const spy3 = vi.fn();
220
+
221
+ const unregister1 = router.afterEach(spy1);
222
+ const unregister2 = router.afterEach(spy2);
223
+ const unregister3 = router.afterEach(spy3);
224
+
225
+ await router.push('/test1');
226
+ expect(spy1).toHaveBeenCalledTimes(1);
227
+ expect(spy2).toHaveBeenCalledTimes(1);
228
+ expect(spy3).toHaveBeenCalledTimes(1);
229
+
230
+ unregister2();
231
+
232
+ await router.push('/test2');
233
+ expect(spy1).toHaveBeenCalledTimes(2);
234
+ expect(spy2).toHaveBeenCalledTimes(1);
235
+ expect(spy3).toHaveBeenCalledTimes(2);
236
+
237
+ unregister1();
238
+ unregister3();
239
+ });
240
+
241
+ test('afterEach guard execution order should remain correct after cleanup', async () => {
242
+ const executionOrder: string[] = [];
243
+
244
+ const guard1 = () => {
245
+ executionOrder.push('after1');
246
+ };
247
+ const guard2 = () => {
248
+ executionOrder.push('after2');
249
+ };
250
+ const guard3 = () => {
251
+ executionOrder.push('after3');
252
+ };
253
+
254
+ router.afterEach(guard1);
255
+ const unregister2 = router.afterEach(guard2);
256
+ router.afterEach(guard3);
257
+
258
+ await router.push('/test1');
259
+ expect(executionOrder).toEqual(['after1', 'after2', 'after3']);
260
+
261
+ executionOrder.length = 0;
262
+ unregister2();
263
+
264
+ await router.push('/test2');
265
+ expect(executionOrder).toEqual(['after1', 'after3']);
266
+ });
267
+ });
268
+
269
+ describe('mixed guards cleanup', () => {
270
+ test('beforeEach and afterEach guards should cleanup independently', async () => {
271
+ const beforeSpy = vi.fn();
272
+ const afterSpy = vi.fn();
273
+
274
+ const unregisterBefore = router.beforeEach(beforeSpy);
275
+ const unregisterAfter = router.afterEach(afterSpy);
276
+
277
+ await router.push('/test1');
278
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
279
+ expect(afterSpy).toHaveBeenCalledTimes(1);
280
+
281
+ unregisterBefore();
282
+
283
+ await router.push('/test2');
284
+ expect(beforeSpy).toHaveBeenCalledTimes(1); // No increase
285
+ expect(afterSpy).toHaveBeenCalledTimes(2); // Continue to increase
286
+
287
+ // Cleanup afterEach
288
+ unregisterAfter();
289
+
290
+ await router.push('/test3');
291
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
292
+ expect(afterSpy).toHaveBeenCalledTimes(2); // No increase
293
+ });
294
+ });
295
+ });
296
+
297
+ describe('🛡️ Edge Cases Testing', () => {
298
+ describe('repeated cleanup safety', () => {
299
+ test('repeated cleanup function calls should be safe', () => {
300
+ const spy = vi.fn();
301
+ const unregister = router.beforeEach(spy);
302
+
303
+ expect(() => {
304
+ unregister();
305
+ unregister();
306
+ unregister();
307
+ }).not.toThrow();
308
+ });
309
+
310
+ test('guard should still not execute after repeated cleanup', async () => {
311
+ const spy = vi.fn();
312
+ const unregister = router.beforeEach(spy);
313
+
314
+ await router.push('/test1');
315
+ expect(spy).toHaveBeenCalledTimes(1);
316
+
317
+ unregister();
318
+ unregister();
319
+ unregister();
320
+
321
+ await router.push('/test2');
322
+ expect(spy).toHaveBeenCalledTimes(1);
323
+ });
324
+ });
325
+
326
+ describe('cleanup and re-registration', () => {
327
+ test('can re-register same guard after cleanup', async () => {
328
+ const spy = vi.fn();
329
+
330
+ const unregister1 = router.beforeEach(spy);
331
+ await router.push('/test1');
332
+ expect(spy).toHaveBeenCalledTimes(1);
333
+
334
+ unregister1();
335
+
336
+ // Re-register same guard
337
+ const unregister2 = router.beforeEach(spy);
338
+ await router.push('/test2');
339
+ expect(spy).toHaveBeenCalledTimes(2);
340
+
341
+ unregister2();
342
+ });
343
+
344
+ test('re-registration after cleanup should work normally', async () => {
345
+ const spy = vi.fn();
346
+
347
+ let unregister = router.beforeEach(spy);
348
+ await router.push('/test1');
349
+ expect(spy).toHaveBeenCalledTimes(1);
350
+
351
+ // Cleanup
352
+ unregister();
353
+
354
+ // Re-register and navigate
355
+ unregister = router.beforeEach(spy);
356
+ await router.push('/test2');
357
+ expect(spy).toHaveBeenCalledTimes(2);
358
+
359
+ // Cleanup and re-register again
360
+ unregister();
361
+ unregister = router.beforeEach(spy);
362
+ await router.push('/test3');
363
+ expect(spy).toHaveBeenCalledTimes(3);
364
+
365
+ // Final cleanup
366
+ unregister();
367
+ });
368
+ });
369
+
370
+ describe('same guard multiple registrations', () => {
371
+ test('same guard function registered multiple times should handle correctly', async () => {
372
+ const spy = vi.fn();
373
+
374
+ const unregister1 = router.beforeEach(spy);
375
+ const unregister2 = router.beforeEach(spy); // Same function
376
+
377
+ await router.push('/test1');
378
+ expect(spy).toHaveBeenCalledTimes(2); // Should execute twice
379
+
380
+ // Cleanup first registration
381
+ unregister1();
382
+
383
+ await router.push('/test2');
384
+ expect(spy).toHaveBeenCalledTimes(3); // Should execute once more
385
+
386
+ unregister2();
387
+
388
+ await router.push('/test3');
389
+ expect(spy).toHaveBeenCalledTimes(3); // No more execution
390
+ });
391
+
392
+ test('complex scenario with same guard multiple registrations and cleanups', async () => {
393
+ const spy = vi.fn();
394
+
395
+ // Register same guard 3 times
396
+ const unregister1 = router.beforeEach(spy);
397
+ const unregister2 = router.beforeEach(spy);
398
+ const unregister3 = router.beforeEach(spy);
399
+
400
+ await router.push('/test1');
401
+ expect(spy).toHaveBeenCalledTimes(3);
402
+
403
+ // Cleanup middle registration
404
+ unregister2();
405
+
406
+ await router.push('/test2');
407
+ expect(spy).toHaveBeenCalledTimes(5); // 3 + 2
408
+
409
+ // Cleanup remaining
410
+ unregister1();
411
+ unregister3();
412
+
413
+ await router.push('/test3');
414
+ expect(spy).toHaveBeenCalledTimes(5); // No increase
415
+ });
416
+ });
417
+
418
+ describe('empty array and non-existent element handling', () => {
419
+ test('removing element from empty array should be safe', () => {
420
+ const emptyArray: unknown[] = [];
421
+ const element = vi.fn();
422
+
423
+ expect(() => {
424
+ removeFromArray(emptyArray, element);
425
+ }).not.toThrow();
426
+
427
+ expect(emptyArray).toEqual([]);
428
+ });
429
+
430
+ test('removing non-existent element should be safe', () => {
431
+ const array = [vi.fn(), vi.fn(), vi.fn()];
432
+ const nonExistentElement = vi.fn();
433
+ const originalLength = array.length;
434
+
435
+ expect(() => {
436
+ removeFromArray(array, nonExistentElement);
437
+ }).not.toThrow();
438
+
439
+ expect(array).toHaveLength(originalLength);
440
+ });
441
+ });
442
+
443
+ describe('special value handling', () => {
444
+ test('removeFromArray should handle NaN values correctly', () => {
445
+ const arr = [1, Number.NaN, 3, Number.NaN, 5];
446
+ removeFromArray(arr, Number.NaN);
447
+ expect(arr).toEqual([1, 3, Number.NaN, 5]); // Only remove first NaN
448
+ });
449
+
450
+ test('removeFromArray should handle function references correctly', () => {
451
+ const func1 = () => 'func1';
452
+ const func2 = () => 'func2';
453
+ const func3 = () => 'func3';
454
+ const arr = [func1, func2, func3];
455
+
456
+ removeFromArray(arr, func2);
457
+ expect(arr).toEqual([func1, func3]);
458
+ });
459
+
460
+ test('removeFromArray should handle object references correctly', () => {
461
+ const obj1 = { id: 1 };
462
+ const obj2 = { id: 2 };
463
+ const obj3 = { id: 3 };
464
+ const arr = [obj1, obj2, obj3];
465
+
466
+ removeFromArray(arr, obj2);
467
+ expect(arr).toEqual([obj1, obj3]);
468
+ });
469
+ });
470
+
471
+ describe('memory leak protection', () => {
472
+ test('large number of guard registrations and cleanups should not cause memory leaks', () => {
473
+ const unregisters: Array<() => void> = [];
474
+
475
+ // Register many guards
476
+ for (let i = 0; i < 100; i++) {
477
+ const guard = vi.fn();
478
+ const unregister = router.beforeEach(guard);
479
+ unregisters.push(unregister);
480
+ }
481
+
482
+ expect(router.transition.guards.beforeEach).toHaveLength(100);
483
+
484
+ // Cleanup all guards
485
+ unregisters.forEach((unregister) => unregister());
486
+
487
+ expect(router.transition.guards.beforeEach).toHaveLength(0);
488
+ });
489
+
490
+ test('mixed registration and cleanup should not cause memory leaks', () => {
491
+ const guards: Array<() => void> = [];
492
+ const unregisters: Array<() => void> = [];
493
+
494
+ // Mixed registration and cleanup
495
+ for (let i = 0; i < 50; i++) {
496
+ const guard = vi.fn();
497
+ guards.push(guard);
498
+ const unregister = router.beforeEach(guard);
499
+ unregisters.push(unregister);
500
+
501
+ // Cleanup every 5th guard
502
+ if (i % 5 === 0 && i > 0) {
503
+ unregisters[i - 5]();
504
+ }
505
+ }
506
+
507
+ // Cleanup remaining guards
508
+ unregisters.forEach((unregister) => unregister());
509
+
510
+ expect(router.transition.guards.beforeEach).toHaveLength(0);
511
+ expect(router.transition.guards.afterEach).toHaveLength(0);
512
+ });
513
+ });
514
+
515
+ describe('concurrent cleanup safety', () => {
516
+ test('cleaning guard during navigation should be safe', async () => {
517
+ let unregister: (() => void) | null = null;
518
+ let guardExecuted = false;
519
+
520
+ const guard = async () => {
521
+ guardExecuted = true;
522
+ // Cleanup self during guard execution
523
+ unregister?.();
524
+ await new Promise((resolve) => setTimeout(resolve, 10));
525
+ };
526
+
527
+ unregister = router.beforeEach(guard);
528
+
529
+ // Should not throw exception
530
+ await expect(router.push('/test1')).resolves.toBeDefined();
531
+ expect(guardExecuted).toBe(true);
532
+
533
+ // Subsequent navigation guard should not execute
534
+ guardExecuted = false;
535
+ await router.push('/test2');
536
+ expect(guardExecuted).toBe(false);
537
+ });
538
+
539
+ test('cleaning multiple guards simultaneously should be safe', async () => {
540
+ const guards = Array.from({ length: 10 }, () => vi.fn());
541
+ const unregisters = guards.map((guard) =>
542
+ router.beforeEach(guard)
543
+ );
544
+
545
+ await router.push('/test1');
546
+ guards.forEach((guard) =>
547
+ expect(guard).toHaveBeenCalledTimes(1)
548
+ );
549
+
550
+ // Cleanup all guards simultaneously
551
+ unregisters.forEach((unregister) => unregister());
552
+
553
+ await router.push('/test2');
554
+ guards.forEach((guard) =>
555
+ expect(guard).toHaveBeenCalledTimes(1)
556
+ );
557
+ });
558
+ });
559
+ });
560
+
561
+ describe('🔧 Router destroy cleanup verification', () => {
562
+ test('Router destroy should cleanup all guards', async () => {
563
+ const beforeSpy = vi.fn();
564
+ const afterSpy = vi.fn();
565
+
566
+ router.beforeEach(beforeSpy);
567
+ router.afterEach(afterSpy);
568
+
569
+ await router.push('/test1');
570
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
571
+ expect(afterSpy).toHaveBeenCalledTimes(1);
572
+
573
+ // Destroy router
574
+ router.destroy();
575
+
576
+ const newRouter = new Router({
577
+ mode: RouterMode.memory,
578
+ base: new URL('http://localhost:3000/'),
579
+ routes: [
580
+ { path: '/', component: () => 'Home' },
581
+ { path: '/test', component: () => 'Test' }
582
+ ]
583
+ });
584
+
585
+ await newRouter.replace('/');
586
+ await newRouter.push('/test');
587
+
588
+ // Original guards should not be executed
589
+ expect(beforeSpy).toHaveBeenCalledTimes(1);
590
+ expect(afterSpy).toHaveBeenCalledTimes(1);
591
+
592
+ newRouter.destroy();
593
+ });
594
+ });
595
+ });