@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,487 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { Router } from './router';
3
+ import { RouteType, RouterMode } from './types';
4
+
5
+ describe('Router.back Tests', () => {
6
+ let router: Router;
7
+ let executionLog: string[];
8
+
9
+ beforeEach(async () => {
10
+ executionLog = [];
11
+
12
+ router = new Router({
13
+ mode: RouterMode.memory,
14
+ base: new URL('http://localhost:3000/'),
15
+ fallback: (to, from) => {
16
+ executionLog.push(`location-handler-${to.path}`);
17
+ },
18
+ routes: [
19
+ {
20
+ path: '/',
21
+ component: () => 'Home'
22
+ },
23
+ {
24
+ path: '/about',
25
+ component: () => 'About'
26
+ },
27
+ {
28
+ path: '/user/:id',
29
+ component: () => 'User',
30
+ beforeEnter: (to) => {
31
+ if (to.params.id === 'blocked') {
32
+ return false; // Block navigation
33
+ }
34
+ if (to.params.id === 'redirect') {
35
+ return '/about'; // Redirect
36
+ }
37
+ }
38
+ },
39
+ {
40
+ path: '/async',
41
+ asyncComponent: () =>
42
+ new Promise((resolve) => {
43
+ setTimeout(() => resolve('AsyncComponent'), 10);
44
+ })
45
+ },
46
+ {
47
+ path: '/async-error',
48
+ asyncComponent: () =>
49
+ new Promise((_, reject) => {
50
+ setTimeout(
51
+ () => reject(new Error('Load failed')),
52
+ 10
53
+ );
54
+ })
55
+ }
56
+ ]
57
+ });
58
+
59
+ await router.push('/');
60
+ });
61
+
62
+ afterEach(() => {
63
+ router.destroy();
64
+ });
65
+
66
+ describe('🎯 Core Behavior', () => {
67
+ test('back should return Promise<Route | null>', async () => {
68
+ await router.push('/about');
69
+ const route = await router.back();
70
+
71
+ expect(route).toBeInstanceOf(Object);
72
+ expect(route?.path).toBe('/');
73
+ expect(route?.handle).not.toBeNull();
74
+ });
75
+
76
+ test('back should navigate to previous route', async () => {
77
+ await router.push('/about');
78
+ await router.push('/user/123');
79
+
80
+ // Go back to /about
81
+ const backRoute = await router.back();
82
+ expect(backRoute?.path).toBe('/about');
83
+ expect(router.route.path).toBe('/about');
84
+ });
85
+
86
+ test('back should update router state', async () => {
87
+ await router.push('/about');
88
+ await router.back();
89
+
90
+ expect(router.route.path).toBe('/');
91
+ expect(router.route.handle).not.toBeNull();
92
+ });
93
+ });
94
+
95
+ describe('🔄 History Navigation Logic', () => {
96
+ test('back should navigate based on history stack', async () => {
97
+ // Establish history: / -> /about -> /user/123
98
+ await router.push('/about');
99
+ await router.push('/user/123');
100
+
101
+ // Go back to /about
102
+ const route1 = await router.back();
103
+ expect(route1?.path).toBe('/about');
104
+ expect(router.route.path).toBe('/about');
105
+
106
+ // Go back again to root path
107
+ const route2 = await router.back();
108
+ expect(route2?.path).toBe('/');
109
+ expect(router.route.path).toBe('/');
110
+ });
111
+
112
+ test('back beyond history boundaries should return null', async () => {
113
+ // In memory mode, create a truly out-of-bounds situation
114
+ const testRouter = new Router({
115
+ mode: RouterMode.memory,
116
+ base: new URL('http://localhost:3000/'),
117
+ routes: [
118
+ { path: '/', component: 'Home' },
119
+ { path: '/about', component: 'About' }
120
+ ]
121
+ });
122
+
123
+ await testRouter.replace('/about');
124
+
125
+ const route = await testRouter.back();
126
+ expect(route).toBe(null);
127
+ expect(testRouter.route.path).toBe('/about'); // Route state unchanged
128
+
129
+ testRouter.destroy();
130
+ });
131
+
132
+ test('back should return correct RouteType', async () => {
133
+ await router.push('/about');
134
+ const route = await router.back();
135
+
136
+ expect(route?.type).toBe(RouteType.back);
137
+ });
138
+
139
+ test('back should keep isPush as false', async () => {
140
+ await router.push('/about');
141
+ const route = await router.back();
142
+
143
+ expect(route?.isPush).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('🏃 Concurrency Control', () => {
148
+ test('later initiated back should cancel earlier back', async () => {
149
+ await router.push('/about');
150
+ await router.push('/user/123');
151
+
152
+ const [firstResult, secondResult] = await Promise.all([
153
+ router.back(), // First operation, should succeed
154
+ router.back() // Second operation, returns null due to first one in progress
155
+ ]);
156
+
157
+ expect(firstResult?.handle).not.toBeNull();
158
+ expect(secondResult).toBe(null);
159
+ expect(router.route.path).toBe('/about'); // First operation result
160
+ });
161
+
162
+ test('cancelled tasks should not affect micro-app state', async () => {
163
+ const updateSpy = vi.spyOn(router.microApp, '_update');
164
+
165
+ await router.push('/about');
166
+ await router.push('/user/123');
167
+
168
+ updateSpy.mockClear();
169
+
170
+ const [firstResult, secondResult] = await Promise.all([
171
+ router.back(), // First operation succeeds
172
+ router.back() // Second operation returns null
173
+ ]);
174
+
175
+ expect(firstResult?.handle).not.toBeNull();
176
+ expect(secondResult).toBe(null);
177
+
178
+ // Micro-app update should only be called by the first successful operation
179
+ expect(updateSpy).toHaveBeenCalledTimes(1);
180
+ });
181
+ });
182
+
183
+ describe('🎭 Micro-app Integration', () => {
184
+ test('back should trigger micro-app update', async () => {
185
+ const updateSpy = vi.spyOn(router.microApp, '_update');
186
+
187
+ await router.push('/about');
188
+ await router.back();
189
+
190
+ expect(updateSpy).toHaveBeenCalled();
191
+ });
192
+
193
+ test('micro-app update should happen after route state update', async () => {
194
+ let routePathWhenUpdated: string | null = null;
195
+
196
+ vi.spyOn(router.microApp, '_update').mockImplementation(() => {
197
+ routePathWhenUpdated = router.route.path;
198
+ });
199
+
200
+ await router.push('/about');
201
+ await router.back();
202
+
203
+ expect(routePathWhenUpdated).toBe('/');
204
+ });
205
+ });
206
+
207
+ describe('⚡ Async Components & Back', () => {
208
+ test('back to async component route should wait for component loading', async () => {
209
+ await router.push('/about');
210
+ await router.push('/async');
211
+
212
+ const route = await router.back();
213
+ expect(route?.path).toBe('/about');
214
+ expect(route?.handle).not.toBeNull();
215
+ });
216
+
217
+ test('back to failed async component route should handle error correctly', async () => {
218
+ await router.push('/about');
219
+
220
+ // Expect async component loading to fail and throw error
221
+ await expect(router.push('/async-error')).rejects.toThrow();
222
+
223
+ // Should still be at /about since navigation failed
224
+ expect(router.route.path).toBe('/about');
225
+
226
+ // Back should work normally
227
+ const route = await router.back();
228
+ expect(route?.path).toBe('/');
229
+ });
230
+ });
231
+
232
+ describe('🛡️ Back Guard Behavior', () => {
233
+ test('back navigation should work normally after blocked route attempt', async () => {
234
+ await router.push('/about');
235
+ await router.push('/user/123');
236
+
237
+ // Attempt to navigate to blocked route (guard returns false)
238
+ const blockedResult = await router.push('/user/blocked');
239
+
240
+ // Navigation still happens but might have different behavior
241
+ expect(blockedResult.path).toBe('/user/blocked');
242
+
243
+ // Current route should now be at blocked location
244
+ expect(router.route.path).toBe('/user/blocked');
245
+
246
+ // Back navigation should work normally
247
+ const route = await router.back();
248
+ expect(route?.path).toBe('/user/123');
249
+ });
250
+
251
+ test('redirect guard should work during navigation', async () => {
252
+ // Navigate to a route that redirects
253
+ const route = await router.push('/user/redirect');
254
+
255
+ // Should be redirected to /about
256
+ expect(route.path).toBe('/about');
257
+ expect(router.route.path).toBe('/about');
258
+
259
+ // Back should go to the original route
260
+ const backRoute = await router.back();
261
+ expect(backRoute?.path).toBe('/');
262
+ });
263
+
264
+ test('afterEach only executes when navigation succeeds', async () => {
265
+ let afterEachCalled = false;
266
+
267
+ const testRouter = new Router({
268
+ mode: RouterMode.memory,
269
+ base: new URL('http://localhost:3000/'),
270
+ routes: [
271
+ { path: '/', component: 'Home' },
272
+ { path: '/about', component: 'About' },
273
+ {
274
+ path: '/blocked',
275
+ component: 'Blocked',
276
+ beforeEnter: () => false
277
+ }
278
+ ]
279
+ });
280
+
281
+ testRouter.afterEach(() => {
282
+ afterEachCalled = true;
283
+ });
284
+
285
+ await testRouter.push('/about');
286
+ afterEachCalled = false; // Reset after successful navigation
287
+
288
+ // This should fail and not trigger afterEach
289
+ await expect(testRouter.push('/blocked')).rejects.toThrow();
290
+ expect(afterEachCalled).toBe(false);
291
+
292
+ testRouter.destroy();
293
+ });
294
+
295
+ test('beforeEach guard should be called during back operation', async () => {
296
+ let beforeEachCalled = false;
297
+
298
+ router.beforeEach(() => {
299
+ beforeEachCalled = true;
300
+ });
301
+
302
+ await router.push('/about');
303
+ await router.back();
304
+
305
+ expect(beforeEachCalled).toBe(true);
306
+ });
307
+ });
308
+
309
+ describe('💾 History Management', () => {
310
+ test('back should navigate correctly in history stack', async () => {
311
+ // Build history stack
312
+ await router.push('/about');
313
+ await router.push('/user/123');
314
+
315
+ const route = await router.back();
316
+ expect(route?.path).toBe('/about');
317
+ expect(router.route.path).toBe('/about');
318
+ });
319
+
320
+ test('back operation should not create new history entries', async () => {
321
+ await router.push('/about');
322
+ await router.push('/user/123');
323
+
324
+ // Go back
325
+ await router.back();
326
+
327
+ // Should be able to go forward again
328
+ const forwardRoute = await router.forward();
329
+ expect(forwardRoute?.path).toBe('/user/123');
330
+ });
331
+ });
332
+
333
+ describe('❌ Error Handling', () => {
334
+ test('back to non-existent route should trigger location handling', async () => {
335
+ // This tests the boundary case where the router falls back to location handling
336
+ const fallbackSpy = vi.fn();
337
+ const testRouter = new Router({
338
+ mode: RouterMode.memory,
339
+ base: new URL('http://localhost:3000/'),
340
+ fallback: fallbackSpy,
341
+ routes: [{ path: '/', component: 'Home' }]
342
+ });
343
+
344
+ // Use replace to avoid creating history
345
+ await testRouter.replace('/');
346
+ const result = await testRouter.back();
347
+
348
+ // Since we used replace and have no history, back should return null
349
+ expect(result).toBe(null);
350
+
351
+ testRouter.destroy();
352
+ });
353
+
354
+ test('exceptions during back process should propagate correctly', async () => {
355
+ const testRouter = new Router({
356
+ mode: RouterMode.memory,
357
+ base: new URL('http://localhost:3000/'),
358
+ routes: [
359
+ { path: '/', component: 'Home' },
360
+ { path: '/about', component: 'About' }
361
+ ]
362
+ });
363
+
364
+ await testRouter.push('/about');
365
+
366
+ // Add guard that throws error after history is established
367
+ testRouter.beforeEach(() => {
368
+ throw new Error('Guard error');
369
+ });
370
+
371
+ await expect(testRouter.back()).rejects.toThrow('Guard error');
372
+
373
+ testRouter.destroy();
374
+ });
375
+ });
376
+
377
+ describe('🔍 Edge Cases', () => {
378
+ test('back should handle special character paths correctly', async () => {
379
+ const testRouter = new Router({
380
+ mode: RouterMode.memory,
381
+ base: new URL('http://localhost:3000/'),
382
+ routes: [
383
+ { path: '/', component: 'Home' },
384
+ { path: '/special', component: 'Special' }
385
+ ]
386
+ });
387
+
388
+ await testRouter.push('/'); // Start at root
389
+ await testRouter.push('/special'); // Navigate to special route
390
+ const route = await testRouter.back(); // Go back to root
391
+
392
+ expect(route?.path).toBe('/');
393
+ expect(testRouter.route.path).toBe('/');
394
+
395
+ testRouter.destroy();
396
+ });
397
+ });
398
+
399
+ describe('🔗 Integration with Other Navigation Methods', () => {
400
+ test('back should behave consistently with go(-1)', async () => {
401
+ await router.push('/about');
402
+ await router.push('/user/123');
403
+
404
+ const backRoute = await router.back();
405
+ const goRoute = await router.go(-1);
406
+
407
+ expect(backRoute?.path).toBe('/about');
408
+ expect(goRoute?.path).toBe('/');
409
+ });
410
+
411
+ test('push after back should handle history correctly', async () => {
412
+ await router.push('/about');
413
+ await router.push('/user/123');
414
+
415
+ // Go back
416
+ await router.back();
417
+
418
+ // Push new route
419
+ await router.push('/user/456');
420
+
421
+ expect(router.route.path).toBe('/user/456');
422
+ });
423
+ });
424
+
425
+ describe('🔧 handleBackBoundary Callback Tests', () => {
426
+ test('should trigger handleBackBoundary when Navigation returns null', async () => {
427
+ let handleBackBoundaryCalled = false;
428
+
429
+ const testRouter = new Router({
430
+ mode: RouterMode.memory,
431
+ base: new URL('http://localhost:3000/'),
432
+ routes: [{ path: '/', component: 'Home' }],
433
+ handleBackBoundary: () => {
434
+ handleBackBoundaryCalled = true;
435
+ }
436
+ });
437
+
438
+ await testRouter.replace('/');
439
+
440
+ const result = await testRouter.back();
441
+ expect(result).toBe(null);
442
+ expect(handleBackBoundaryCalled).toBe(true);
443
+
444
+ testRouter.destroy();
445
+ });
446
+
447
+ test('should not error when no handleBackBoundary callback', async () => {
448
+ const testRouter = new Router({
449
+ mode: RouterMode.memory,
450
+ base: new URL('http://localhost:3000/'),
451
+ routes: [{ path: '/', component: 'Home' }]
452
+ });
453
+
454
+ await testRouter.replace('/');
455
+
456
+ const result = await testRouter.back();
457
+ expect(result).toBe(null);
458
+
459
+ testRouter.destroy();
460
+ });
461
+ });
462
+
463
+ describe('🔄 Navigation Result Handling', () => {
464
+ test('should correctly handle successful navigation result', async () => {
465
+ await router.push('/about');
466
+ const route = await router.back();
467
+
468
+ expect(route?.path).toBe('/');
469
+ expect(router.route.path).toBe('/');
470
+ });
471
+
472
+ test('should return null directly when Navigation returns null', async () => {
473
+ const testRouter = new Router({
474
+ mode: RouterMode.memory,
475
+ base: new URL('http://localhost:3000/'),
476
+ routes: [{ path: '/', component: 'Home' }]
477
+ });
478
+
479
+ await testRouter.replace('/');
480
+
481
+ const result = await testRouter.back();
482
+ expect(result).toBe(null);
483
+
484
+ testRouter.destroy();
485
+ });
486
+ });
487
+ });