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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.zh-CN.md +82 -1
  2. package/dist/index.d.ts +1 -2
  3. package/dist/index.mjs +0 -1
  4. package/package.json +4 -4
  5. package/src/index.ts +0 -3
  6. package/dist/index.test.d.ts +0 -1
  7. package/dist/index.test.mjs +0 -8
  8. package/dist/location.test.d.ts +0 -8
  9. package/dist/location.test.mjs +0 -370
  10. package/dist/matcher.test.d.ts +0 -1
  11. package/dist/matcher.test.mjs +0 -1492
  12. package/dist/micro-app.dom.test.d.ts +0 -1
  13. package/dist/micro-app.dom.test.mjs +0 -532
  14. package/dist/navigation.test.d.ts +0 -1
  15. package/dist/navigation.test.mjs +0 -681
  16. package/dist/route-task.test.d.ts +0 -1
  17. package/dist/route-task.test.mjs +0 -673
  18. package/dist/route-transition.test.d.ts +0 -1
  19. package/dist/route-transition.test.mjs +0 -146
  20. package/dist/route.test.d.ts +0 -1
  21. package/dist/route.test.mjs +0 -1664
  22. package/dist/router-back.test.d.ts +0 -1
  23. package/dist/router-back.test.mjs +0 -361
  24. package/dist/router-forward.test.d.ts +0 -1
  25. package/dist/router-forward.test.mjs +0 -376
  26. package/dist/router-go.test.d.ts +0 -1
  27. package/dist/router-go.test.mjs +0 -73
  28. package/dist/router-guards-cleanup.test.d.ts +0 -1
  29. package/dist/router-guards-cleanup.test.mjs +0 -437
  30. package/dist/router-push.test.d.ts +0 -1
  31. package/dist/router-push.test.mjs +0 -115
  32. package/dist/router-replace.test.d.ts +0 -1
  33. package/dist/router-replace.test.mjs +0 -114
  34. package/dist/router-resolve.test.d.ts +0 -1
  35. package/dist/router-resolve.test.mjs +0 -393
  36. package/dist/router-restart-app.dom.test.d.ts +0 -1
  37. package/dist/router-restart-app.dom.test.mjs +0 -616
  38. package/dist/router-window-navigation.test.d.ts +0 -1
  39. package/dist/router-window-navigation.test.mjs +0 -359
  40. package/dist/util.test.d.ts +0 -1
  41. package/dist/util.test.mjs +0 -1020
  42. package/src/index.test.ts +0 -9
  43. package/src/location.test.ts +0 -406
  44. package/src/matcher.test.ts +0 -1685
  45. package/src/micro-app.dom.test.ts +0 -708
  46. package/src/navigation.test.ts +0 -858
  47. package/src/route-task.test.ts +0 -901
  48. package/src/route-transition.test.ts +0 -178
  49. package/src/route.test.ts +0 -2014
  50. package/src/router-back.test.ts +0 -487
  51. package/src/router-forward.test.ts +0 -506
  52. package/src/router-go.test.ts +0 -91
  53. package/src/router-guards-cleanup.test.ts +0 -595
  54. package/src/router-push.test.ts +0 -140
  55. package/src/router-replace.test.ts +0 -139
  56. package/src/router-resolve.test.ts +0 -475
  57. package/src/router-restart-app.dom.test.ts +0 -783
  58. package/src/router-window-navigation.test.ts +0 -457
  59. package/src/util.test.ts +0 -1262
@@ -1,858 +0,0 @@
1
- import {
2
- assert,
3
- afterEach,
4
- beforeEach,
5
- describe,
6
- expect,
7
- it,
8
- test,
9
- vi
10
- } from 'vitest';
11
- import { MemoryHistory, Navigation } from './navigation';
12
- import { parsedOptions } from './options';
13
- import { Route } from './route';
14
- import type { RouterOptions } from './types';
15
- import { RouteType, RouterMode } from './types';
16
-
17
- const sleep = (ms?: number) => new Promise((s) => setTimeout(s, ms));
18
- const awaitGo = (history: MemoryHistory, delta: number) => {
19
- const p = Promise.withResolvers<void>();
20
- const un = history.onPopState(() => {
21
- un();
22
- p.resolve();
23
- });
24
- history.go(delta);
25
- return p.promise;
26
- };
27
-
28
- describe('MemoryHistory', () => {
29
- test('should initialize with root path', () => {
30
- const history = new MemoryHistory();
31
- assert.equal(history.length, 1);
32
- assert.equal(history.state, null);
33
- });
34
-
35
- describe('pushState', () => {
36
- test('should add new entry', () => {
37
- const history = new MemoryHistory();
38
- history.pushState({ id: 1 }, '', '/page1');
39
-
40
- assert.equal(history.length, 2);
41
- assert.deepEqual(history.state, { id: 1 });
42
- });
43
-
44
- test('should clear forward history when pushing new state', () => {
45
- const history = new MemoryHistory();
46
- history.pushState({ id: 1 }, '', '/page1');
47
- history.pushState({ id: 2 }, '', '/page2');
48
- history.back();
49
- history.pushState({ id: 3 }, '', '/page3');
50
-
51
- assert.equal(history.length, 3);
52
- assert.deepEqual(history.state, { id: 3 });
53
- });
54
- });
55
-
56
- describe('replaceState', () => {
57
- test('should replace current entry', () => {
58
- const history = new MemoryHistory();
59
- history.pushState({ id: 1 }, '', '/page1');
60
- history.replaceState({ id: 2 }, '', '/page1-updated');
61
-
62
- assert.equal(history.length, 2);
63
- assert.deepEqual(history.state, { id: 2 });
64
- });
65
- });
66
-
67
- describe('navigation', () => {
68
- test('back() should navigate to previous entry', () => {
69
- const history = new MemoryHistory();
70
- history.pushState({ id: 1 }, '', '/page1');
71
- history.pushState({ id: 2 }, '', '/page2');
72
-
73
- history.back();
74
- assert.deepEqual(history.state, { id: 1 });
75
- });
76
-
77
- test('forward() should navigate to next entry', () => {
78
- const history = new MemoryHistory();
79
- history.pushState({ id: 1 }, '', '/page1');
80
- history.pushState({ id: 2 }, '', '/page2');
81
-
82
- history.back();
83
- history.forward();
84
- assert.deepEqual(history.state, { id: 2 });
85
- });
86
-
87
- test('go() should navigate to specific delta', () => {
88
- const history = new MemoryHistory();
89
- history.pushState({ id: 1 }, '', '/page1');
90
- history.pushState({ id: 2 }, '', '/page2');
91
- history.pushState({ id: 3 }, '', '/page3');
92
-
93
- history.go(-2);
94
- assert.deepEqual(history.state, { id: 1 });
95
-
96
- history.go(2);
97
- assert.deepEqual(history.state, { id: 3 });
98
- });
99
-
100
- test('go() should not navigate beyond bounds', () => {
101
- const history = new MemoryHistory();
102
- history.pushState({ id: 1 }, '', '/page1');
103
-
104
- const originalState = history.state;
105
- history.go(-2); // Try to go below lower bound
106
- assert.deepEqual(history.state, originalState);
107
-
108
- history.go(2); // Try to go above upper bound
109
- assert.deepEqual(history.state, originalState);
110
- });
111
-
112
- test('go() should do nothing when delta is 0 or undefined', () => {
113
- const history = new MemoryHistory();
114
- history.pushState({ id: 1 }, '', '/page1');
115
-
116
- const originalState = history.state;
117
- history.go(0);
118
- assert.deepEqual(history.state, originalState);
119
-
120
- history.go();
121
- assert.deepEqual(history.state, originalState);
122
- });
123
- });
124
- });
125
-
126
- describe.concurrent('subscribeMemory', () => {
127
- // Keep original functionality test - original features should work after subscription
128
- test('should preserve original go functionality after subscription', async () => {
129
- const history = new MemoryHistory();
130
- history.pushState({ id: 1 }, '', '/page1');
131
- history.pushState({ id: 2 }, '', '/page2');
132
-
133
- void history.onPopState(() => {});
134
-
135
- history.go(-1);
136
- await sleep(); // Wait for callback execution
137
- assert.equal(history.url, '/page1');
138
- assert.deepEqual(history.state, { id: 1 });
139
-
140
- history.go(1);
141
- await sleep(); // Wait for callback execution
142
- assert.equal(history.url, '/page2');
143
- assert.deepEqual(history.state, { id: 2 });
144
- });
145
-
146
- // Basic callback functionality test - callback should trigger on navigation
147
- test('should trigger callback only on actual navigation (like popstate)', async () => {
148
- const history = new MemoryHistory();
149
- history.pushState({ id: 1 }, '', '/page1');
150
- history.pushState({ id: 2 }, '', '/page2');
151
-
152
- const callbacks: Array<{ url: string; state: any }> = [];
153
- void history.onPopState((url, state) => {
154
- callbacks.push({ url, state });
155
- });
156
-
157
- // Navigate to the previous page - should trigger callback
158
- await awaitGo(history, -1);
159
-
160
- assert.equal(callbacks.length, 1);
161
- assert.equal(callbacks[0].url, '/page1');
162
- assert.deepEqual(callbacks[0].state, { id: 1 });
163
- });
164
-
165
- test('should call callback multiple times for multiple navigation', async () => {
166
- const history = new MemoryHistory();
167
- history.pushState({ id: 1 }, '', '/page1');
168
- history.pushState({ id: 2 }, '', '/page2');
169
- history.pushState({ id: 3 }, '', '/page3');
170
-
171
- const callbacks: Array<{ url: string; state: any }> = [];
172
- void history.onPopState((url, state) => {
173
- callbacks.push({ url, state });
174
- });
175
-
176
- history.go(-1); // to page2
177
- history.go(-1); // to page1
178
- history.go(2); // to page3
179
- await sleep(); // Wait for callback execution
180
-
181
- assert.equal(callbacks.length, 3);
182
- assert.deepEqual(callbacks, [
183
- { url: '/page2', state: { id: 2 } },
184
- { url: '/page1', state: { id: 1 } },
185
- { url: '/page3', state: { id: 3 } }
186
- ]);
187
- });
188
-
189
- // Test pushState and replaceState - these operations should not trigger callbacks
190
- test('should NOT trigger callback on pushState/replaceState (like real popstate)', async () => {
191
- const history = new MemoryHistory();
192
-
193
- const callbacks: Array<{ url: string; state: any }> = [];
194
- void history.onPopState((url, state) => {
195
- callbacks.push({ url, state });
196
- });
197
-
198
- // pushState and replaceState should not trigger popstate events
199
- history.pushState({ id: 1 }, '', '/page1');
200
- history.replaceState({ id: 2 }, '', '/page1-updated');
201
- await sleep(); // Wait for callback execution
202
-
203
- assert.equal(callbacks.length, 0);
204
- });
205
-
206
- // Edge case test - test go(0) and go(undefined) - these should not trigger callbacks
207
- test('should NOT trigger callback when go() with delta 0 (no actual navigation)', async () => {
208
- const history = new MemoryHistory();
209
- history.pushState({ id: 1 }, '', '/page1');
210
-
211
- const callbacks: Array<{ url: string; state: any }> = [];
212
- void history.onPopState((url, state) => {
213
- callbacks.push({ url, state });
214
- });
215
-
216
- // go(0) and go(undefined) do not change history state and should not trigger callbacks
217
- history.go(0);
218
- history.go();
219
- await sleep(); // Wait for callback execution
220
-
221
- assert.equal(callbacks.length, 0);
222
- });
223
-
224
- // Out-of-bounds navigation test - navigation beyond history bounds should not trigger callbacks
225
- test('should NOT trigger callback when navigation is out of bounds', async () => {
226
- const history = new MemoryHistory();
227
- history.pushState({ id: 1 }, '', '/page1');
228
-
229
- const callbacks: Array<{ url: string; state: any }> = [];
230
- void history.onPopState((url, state) => {
231
- callbacks.push({ url, state });
232
- });
233
-
234
- // Attempting out-of-bounds navigation
235
- history.go(-10); // Below lower bound
236
- await sleep(); // Wait for callback execution
237
- history.go(10); // Above upper bound
238
- await sleep(); // Wait for callback execution
239
-
240
- assert.equal(callbacks.length, 0);
241
- });
242
-
243
- // Legal navigation callback test - valid back/forward/go navigations should all trigger callbacks
244
- test('should trigger callback for valid back/forward navigation', async () => {
245
- const history = new MemoryHistory();
246
- history.pushState({ id: 1 }, '', '/page1');
247
- history.pushState({ id: 2 }, '', '/page2');
248
- history.pushState({ id: 3 }, '', '/page3');
249
-
250
- const callbacks: Array<{ url: string; state: any }> = [];
251
- void history.onPopState((url, state) => {
252
- callbacks.push({ url, state });
253
- });
254
-
255
- // These should all trigger callbacks as they change the history state
256
- history.back(); // to page2
257
- history.back(); // to page1
258
- history.forward(); // to page2
259
- history.go(1); // to page3
260
- await sleep(); // Wait for callback execution
261
-
262
- assert.equal(callbacks.length, 4);
263
- assert.deepEqual(callbacks, [
264
- { url: '/page2', state: { id: 2 } },
265
- { url: '/page1', state: { id: 1 } },
266
- { url: '/page2', state: { id: 2 } },
267
- { url: '/page3', state: { id: 3 } }
268
- ]);
269
- });
270
-
271
- // Test state and URL provided in callback - ensure they are correct
272
- test('should provide correct state and url in callback (like popstate event)', async () => {
273
- const history = new MemoryHistory();
274
- history.pushState({ userId: 123, page: 'profile' }, '', '/user/123');
275
- history.pushState(
276
- { userId: 456, page: 'settings' },
277
- '',
278
- '/user/456/settings'
279
- );
280
-
281
- type Data = { url: string; state: any };
282
- let callbackData: Data | null = null;
283
- void history.onPopState((url, state) => {
284
- callbackData = { url, state };
285
- });
286
-
287
- await awaitGo(history, -1);
288
-
289
- assert.isNotNull(callbackData);
290
- assert.equal((callbackData as Data).url, '/user/123');
291
- assert.deepEqual((callbackData as Data).state, {
292
- userId: 123,
293
- page: 'profile'
294
- });
295
- });
296
-
297
- test('should support multiple subscribers like multiple event listeners', async () => {
298
- const history = new MemoryHistory();
299
- history.pushState({ id: 1 }, '', '/page1');
300
- history.pushState({ id: 2 }, '', '/page2');
301
-
302
- const callbacks1: Array<{ url: string; state: any }> = [];
303
- const callbacks2: Array<{ url: string; state: any }> = [];
304
-
305
- void history.onPopState((url, state) => {
306
- callbacks1.push({ url, state });
307
- });
308
- void history.onPopState((url, state) => {
309
- callbacks2.push({ url, state });
310
- });
311
-
312
- await awaitGo(history, -1);
313
-
314
- assert.equal(callbacks1.length, 1);
315
- assert.deepEqual(callbacks1, [{ url: '/page1', state: { id: 1 } }]);
316
- assert.equal(callbacks2.length, 1);
317
- assert.deepEqual(callbacks2, [{ url: '/page1', state: { id: 1 } }]);
318
- });
319
-
320
- // Test returning cleanup function - check if it returns a function to unsubscribe
321
- test('should return cleanup function to unsubscribe', () => {
322
- const history = new MemoryHistory();
323
- const unsubscribe = history.onPopState(() => {});
324
-
325
- assert.equal(typeof unsubscribe, 'function');
326
- });
327
-
328
- // Test cleanup functionality - ensure subscription can be cancelled correctly
329
- test('cleanup function should stop triggering callbacks', async () => {
330
- const history = new MemoryHistory();
331
- history.pushState({ id: 1 }, '', '/page1');
332
- history.pushState({ id: 2 }, '', '/page2');
333
-
334
- const callbacks: Array<{ url: string; state: any }> = [];
335
- const unsubscribe = history.onPopState((url, state) => {
336
- callbacks.push({ url, state });
337
- });
338
-
339
- // Navigate once, should trigger callback
340
- await awaitGo(history, -1);
341
- assert.equal(callbacks.length, 1);
342
-
343
- // Cleanup subscription
344
- unsubscribe();
345
-
346
- // Navigate again, should not trigger callback
347
- await awaitGo(history, 1);
348
-
349
- assert.equal(callbacks.length, 1);
350
- });
351
-
352
- // Cleanup multiple subscribers - ensure cleanup function only cancels the current subscription
353
- test('should allow multiple subscriptions and cleanup only the current one', async () => {
354
- const history = new MemoryHistory();
355
- history.pushState({ id: 1 }, '', '/page1');
356
- history.pushState({ id: 2 }, '', '/page2');
357
-
358
- const callbacks1: Array<{ url: string; state: any }> = [];
359
- const callbacks2: Array<{ url: string; state: any }> = [];
360
- const callbacks3: Array<{ url: string; state: any }> = [];
361
-
362
- void history.onPopState((url, state) => {
363
- callbacks1.push({ url, state });
364
- });
365
- const unsubscribe2 = history.onPopState((url, state) => {
366
- callbacks2.push({ url, state });
367
- });
368
- void history.onPopState((url, state) => {
369
- callbacks3.push({ url, state });
370
- });
371
-
372
- // Navigate once, all subscribers should be triggered
373
- await awaitGo(history, -1);
374
- assert.equal(callbacks1.length, 1);
375
- assert.deepEqual(callbacks1, [{ url: '/page1', state: { id: 1 } }]);
376
- assert.equal(callbacks2.length, 1);
377
- assert.deepEqual(callbacks2, [{ url: '/page1', state: { id: 1 } }]);
378
- assert.equal(callbacks3.length, 1);
379
- assert.deepEqual(callbacks3, [{ url: '/page1', state: { id: 1 } }]);
380
-
381
- // Cleanup subscriber 2
382
- unsubscribe2();
383
-
384
- // Navigate again, remaining subscribers should be triggered, but the cleaned one should not
385
- await awaitGo(history, 1);
386
- assert.equal(callbacks1.length, 2); // Subscriber 1 should be triggered
387
- assert.equal(callbacks2.length, 1); // Subscriber 2 should NOT be triggered
388
- assert.equal(callbacks3.length, 2); // Subscriber 3 should be triggered
389
- });
390
-
391
- test('should not subscribe the same callback multiple times', async () => {
392
- const history = new MemoryHistory();
393
- history.pushState({ id: 1 }, '', '/page1');
394
- history.pushState({ id: 2 }, '', '/page2');
395
-
396
- const callbacks: Array<{ url: string; state: any }> = [];
397
- const callback = (url: string, state: any) => {
398
- callbacks.push({ url, state });
399
- };
400
-
401
- const unSub1 = history.onPopState(callback);
402
- const unSub2 = history.onPopState(callback);
403
-
404
- // Navigate once, should trigger callback only once
405
- await awaitGo(history, -1);
406
- assert.equal(callbacks.length, 1);
407
- assert.deepEqual(callbacks[0], { url: '/page1', state: { id: 1 } });
408
-
409
- // Cleanup subscription
410
- unSub1();
411
- unSub2(); // Cleanup for the second subscription should have no effect
412
-
413
- // Navigate again, should not trigger callback
414
- await awaitGo(history, 1);
415
- assert.equal(callbacks.length, 1); // Callback count is still one
416
-
417
- void history.onPopState(callback);
418
- unSub1(); // It should be possible to call the unsubscribe function multiple times for the same listener
419
-
420
- // Navigate again, still should not trigger callback
421
- await awaitGo(history, -1);
422
- assert.equal(callbacks.length, 1); // Callback count is still one
423
- });
424
- });
425
-
426
- describe('Navigation', () => {
427
- const createTestOptions = () => {
428
- const baseOptions: RouterOptions = {
429
- context: {},
430
- routes: [],
431
- mode: RouterMode.memory,
432
- base: new URL('http://test.com'),
433
- req: null,
434
- res: null,
435
- apps: {},
436
- normalizeURL: (url: URL) => url,
437
- fallback: () => {},
438
- rootStyle: false,
439
- handleBackBoundary: () => {},
440
- handleLayerClose: () => {}
441
- };
442
- return parsedOptions(baseOptions);
443
- };
444
-
445
- test('should provide access to history length', () => {
446
- const nav = new Navigation({ mode: RouterMode.memory } as any);
447
-
448
- // Initial length should be 1 (root entry)
449
- assert.equal(nav.length, 1);
450
-
451
- // Push operations should increase length
452
- const route1 = new Route({
453
- options: createTestOptions(),
454
- toType: RouteType.push,
455
- toInput: { path: '/foo', state: { a: 1 } }
456
- });
457
- nav.push(route1);
458
- assert.equal(nav.length, 2);
459
-
460
- const route2 = new Route({
461
- options: createTestOptions(),
462
- toType: RouteType.push,
463
- toInput: { path: '/bar', state: { b: 2 } }
464
- });
465
- nav.push(route2);
466
- assert.equal(nav.length, 3);
467
-
468
- // Replace operation should not change length
469
- const route3 = new Route({
470
- options: createTestOptions(),
471
- toType: RouteType.push,
472
- toInput: { path: '/baz', state: { c: 3 } }
473
- });
474
- nav.replace(route3);
475
- assert.equal(nav.length, 3);
476
-
477
- nav.destroy();
478
- });
479
-
480
- test('should push and replace state correctly', () => {
481
- const nav = new Navigation({ mode: RouterMode.memory } as any);
482
- const state1 = nav.push({ state: { a: 1 } }, '/foo');
483
- assert.deepEqual((state1 as any).state.a, 1);
484
- const state2 = nav.replace({ state: { b: 2 } }, '/bar');
485
- assert.deepEqual((state2 as any).state.b, 2);
486
- nav.destroy();
487
- });
488
-
489
- test('should resolve go/back/forward with correct url and state', async () => {
490
- const nav = new Navigation({ mode: RouterMode.memory } as any);
491
- nav.push({ state: { a: 1 } }, '/a');
492
- nav.push({ state: { b: 2 } }, '/b');
493
- nav.push({ state: { c: 3 } }, '/c');
494
-
495
- // go(-2) 回到 /a
496
- const res1 = await nav.go(-2);
497
- assert.equal(res1?.url, '/a');
498
- assert.deepEqual((res1?.state as any).state.a, 1);
499
- // forward 到 /b
500
- const res2 = await nav.forward();
501
- assert.equal(res2?.url, '/b');
502
- assert.deepEqual((res2?.state as any).state.b, 2);
503
- // back 到 /a
504
- const res3 = await nav.back();
505
- assert.equal(res3?.url, '/a');
506
- assert.deepEqual((res3?.state as any).state.a, 1);
507
- nav.destroy();
508
- });
509
-
510
- test('should call onUpdated callback on navigation', async () => {
511
- const updates: Array<{ url: string; state: any }> = [];
512
- const nav = new Navigation(
513
- { mode: RouterMode.memory } as any,
514
- (url, state) => updates.push({ url, state })
515
- );
516
- nav.push({ state: { a: 1 } }, '/a');
517
- nav.push({ state: { b: 2 } }, '/b');
518
-
519
- ((nav as any)._history as MemoryHistory).back();
520
- await sleep(100); // Wait for callback execution
521
- assert.ok(updates.length > 0);
522
- assert.equal(updates[0].url, '/a');
523
- nav.destroy();
524
- });
525
-
526
- test('should resolve null if go is called while pending', async () => {
527
- const nav = new Navigation({ mode: RouterMode.memory } as any);
528
- nav.push(
529
- new Route({
530
- options: createTestOptions(),
531
- toType: RouteType.push,
532
- toInput: { path: '/a', state: { a: 1 } }
533
- })
534
- );
535
- nav.push(
536
- new Route({
537
- options: createTestOptions(),
538
- toType: RouteType.push,
539
- toInput: { path: '/b', state: { b: 2 } }
540
- })
541
- );
542
-
543
- const p1 = nav.go(-1);
544
- const p2 = nav.go(1); // Second `go` should resolve with null immediately
545
- const r2 = await p2;
546
- assert.equal(r2, null);
547
- await p1;
548
- nav.destroy();
549
- });
550
-
551
- test('should cleanup listeners on destroy', async () => {
552
- const updates: Array<{ url: string; state: any }> = [];
553
- const nav = new Navigation(
554
- { mode: RouterMode.memory } as any,
555
- (url, state) => updates.push({ url, state })
556
- );
557
- nav.push(
558
- new Route({
559
- options: createTestOptions(),
560
- toType: RouteType.push,
561
- toInput: { path: '/a', state: { a: 1 } }
562
- })
563
- );
564
- nav.push(
565
- new Route({
566
- options: createTestOptions(),
567
- toType: RouteType.push,
568
- toInput: { path: '/b', state: { b: 2 } }
569
- })
570
- );
571
- nav.destroy();
572
- // `go` after destroy should not throw an error
573
- nav.go(-1);
574
- const res = await nav.go(1);
575
- assert.equal(res, null);
576
- assert.equal(updates.length, 0);
577
- });
578
-
579
- describe('in history mode', () => {
580
- let mockHistory: any;
581
- let popstateHandler: any;
582
-
583
- beforeEach(() => {
584
- // Mock window.history
585
- mockHistory = new MemoryHistory();
586
- mockHistory.replaceState({ startPoint: true }, '', '/startPoint');
587
- mockHistory.pushState = vi.fn(
588
- mockHistory.pushState.bind(mockHistory)
589
- );
590
- mockHistory.replaceState = vi.fn(
591
- mockHistory.replaceState.bind(mockHistory)
592
- );
593
- mockHistory.go = vi.fn(mockHistory.go.bind(mockHistory));
594
- mockHistory.onPopState(() => popstateHandler?.());
595
- // Mock location
596
- const mockLocation = {
597
- get href() {
598
- return mockHistory.url;
599
- }
600
- };
601
- // Mock window.addEventListener
602
- const mockAddEventListener = vi.fn((type, handler) => {
603
- if (type === 'popstate') popstateHandler = handler;
604
- });
605
- const mockRemoveEventListener = vi.fn((type, handler) => {
606
- if (type === 'popstate' && popstateHandler === handler) {
607
- popstateHandler = null;
608
- }
609
- });
610
- vi.stubGlobal('window', {
611
- get history() {
612
- return mockHistory;
613
- },
614
- get location() {
615
- return mockLocation;
616
- },
617
- addEventListener: mockAddEventListener,
618
- removeEventListener: mockRemoveEventListener
619
- });
620
- vi.stubGlobal('location', mockLocation);
621
- vi.stubGlobal('history', mockHistory);
622
- });
623
- afterEach(() => {
624
- vi.unstubAllGlobals();
625
- popstateHandler = null;
626
- mockHistory = null;
627
- });
628
-
629
- it('should call pushState/replaceState and handle go/back/forward', async () => {
630
- const nav = new Navigation({ mode: RouterMode.history } as any);
631
- // push
632
- const state1 = nav.push({ state: { a: 1 } }, '/foo');
633
- expect(mockHistory.pushState).toHaveBeenCalledWith(
634
- expect.objectContaining({ state: { a: 1 } }),
635
- '',
636
- '/foo'
637
- );
638
- // replace
639
- const state2 = nav.replace({ state: { b: 2 } }, '/bar');
640
- expect(mockHistory.replaceState).toHaveBeenCalledWith(
641
- expect.objectContaining({ state: { b: 2 } }),
642
- '',
643
- '/bar'
644
- );
645
- // go/back/forward
646
- const res = await nav.go(-1);
647
- expect(res).toEqual({
648
- type: 'success',
649
- url: '/startPoint',
650
- state: { startPoint: true }
651
- });
652
- nav.destroy();
653
- });
654
- });
655
-
656
- // New test cases to cover untested branches
657
- describe('Uncovered branches tests', () => {
658
- test('should handle null/undefined route.state in push method (line 43)', () => {
659
- const nav = new Navigation({ mode: RouterMode.memory } as any);
660
-
661
- const routeWithNullState = new Route({
662
- options: createTestOptions(),
663
- toType: RouteType.push,
664
- toInput: { path: '/test', state: null as any }
665
- });
666
- const state1 = nav.push(routeWithNullState);
667
- assert.ok(state1);
668
- assert.ok('__pageId__' in state1);
669
-
670
- const routeWithUndefinedState = new Route({
671
- options: createTestOptions(),
672
- toType: RouteType.push,
673
- toInput: { path: '/test2', state: undefined }
674
- });
675
- const state2 = nav.push(routeWithUndefinedState);
676
- assert.ok(state2);
677
- assert.ok('__pageId__' in state2);
678
-
679
- nav.destroy();
680
- });
681
-
682
- test('should handle null/undefined route.state in replace method (line 53)', () => {
683
- const nav = new Navigation({ mode: RouterMode.memory } as any);
684
- nav.push(
685
- new Route({
686
- options: createTestOptions(),
687
- toType: RouteType.push,
688
- toInput: '/initial'
689
- })
690
- );
691
-
692
- const routeWithNullState = new Route({
693
- options: createTestOptions(),
694
- toType: RouteType.push,
695
- toInput: { path: '/test', state: null as any }
696
- });
697
- const state1 = nav.replace(routeWithNullState);
698
- assert.ok(state1);
699
- assert.ok('__pageId__' in state1);
700
-
701
- const routeWithUndefinedState = new Route({
702
- options: createTestOptions(),
703
- toType: RouteType.push,
704
- toInput: { path: '/test2', state: undefined }
705
- });
706
- const state2 = nav.replace(routeWithUndefinedState);
707
- assert.ok(state2);
708
- assert.ok('__pageId__' in state2);
709
-
710
- nav.destroy();
711
- });
712
-
713
- test('should call _promiseResolve when destroying with pending promise (line 82)', async () => {
714
- const nav = new Navigation({ mode: RouterMode.memory } as any);
715
- nav.push(
716
- new Route({
717
- options: createTestOptions(),
718
- toType: RouteType.push,
719
- toInput: '/test1'
720
- })
721
- );
722
- nav.push(
723
- new Route({
724
- options: createTestOptions(),
725
- toType: RouteType.push,
726
- toInput: '/test2'
727
- })
728
- );
729
-
730
- // Start a 'go' operation without awaiting it
731
- const goPromise = nav.go(-1);
732
-
733
- // Destroy immediately, which should call _promiseResolve?.()
734
- nav.destroy();
735
-
736
- const result = await goPromise;
737
- assert.equal(result, null);
738
- });
739
-
740
- test('should return null when _curEntry index is out of bounds (line 92)', () => {
741
- const history = new MemoryHistory();
742
-
743
- (history as any)._index = -1;
744
- assert.equal((history as any)._curEntry, null);
745
-
746
- (history as any)._index = 999; // Exceeds length
747
- assert.equal((history as any)._curEntry, null);
748
- });
749
-
750
- test('should return empty string when _curEntry.url is null/undefined (line 101)', () => {
751
- const history = new MemoryHistory();
752
-
753
- const mockEntry = { state: {}, url: undefined };
754
- (history as any)._entries = [mockEntry];
755
- (history as any)._index = 0;
756
-
757
- assert.equal(history.url, '');
758
- });
759
-
760
- test('should use this.url when pushState url parameter is null/undefined (line 108)', () => {
761
- const history = new MemoryHistory();
762
- const currentUrl = history.url; // Get current URL
763
-
764
- history.pushState({ test: 1 }, '', null);
765
- assert.equal(history.url, currentUrl);
766
-
767
- history.pushState({ test: 2 }, '', undefined);
768
- assert.equal(history.url, currentUrl);
769
- });
770
-
771
- test('should return early when curEntry is null in replaceState (line 128)', () => {
772
- const history = new MemoryHistory();
773
-
774
- (history as any)._index = -1; // Set to an invalid index
775
-
776
- // Calling replaceState should return early without doing anything
777
- const originalEntries = [...(history as any)._entries];
778
- history.replaceState({ test: 1 }, '', '/test');
779
-
780
- // entries should remain unchanged
781
- assert.deepEqual((history as any)._entries, originalEntries);
782
- });
783
-
784
- test('should return empty function when cb is not a function in onPopState (line 152)', () => {
785
- const history = new MemoryHistory();
786
-
787
- const unsubscribe1 = history.onPopState(null as any);
788
- const unsubscribe2 = history.onPopState(undefined as any);
789
- const unsubscribe3 = history.onPopState('not a function' as any);
790
- const unsubscribe4 = history.onPopState(123 as any);
791
-
792
- assert.equal(typeof unsubscribe1, 'function');
793
- assert.equal(typeof unsubscribe2, 'function');
794
- assert.equal(typeof unsubscribe3, 'function');
795
- assert.equal(typeof unsubscribe4, 'function');
796
-
797
- // These should be empty functions that don't throw when called
798
- assert.doesNotThrow(() => {
799
- unsubscribe1();
800
- unsubscribe2();
801
- unsubscribe3();
802
- unsubscribe4();
803
- });
804
- });
805
-
806
- test('should handle null history.state in subscribeHtmlHistory (line 159)', () => {
807
- const mockHistory = {
808
- state: null // Simulate history.state being null
809
- };
810
- const mockLocation = {
811
- href: 'http://test.com/page'
812
- };
813
-
814
- const mockWindow = {
815
- addEventListener: vi.fn(),
816
- removeEventListener: vi.fn()
817
- };
818
-
819
- let capturedCallback: any = null;
820
- mockWindow.addEventListener.mockImplementation(
821
- (event, callback) => {
822
- if (event === 'popstate') {
823
- capturedCallback = callback;
824
- }
825
- }
826
- );
827
-
828
- vi.stubGlobal('window', mockWindow);
829
- vi.stubGlobal('history', mockHistory);
830
- vi.stubGlobal('location', mockLocation);
831
-
832
- const callbackData: Array<{ url: string; state: any }> = [];
833
-
834
- const nav = new Navigation(
835
- { mode: RouterMode.history } as any,
836
- (url: string, state: any) => {
837
- callbackData.push({ url, state });
838
- }
839
- );
840
-
841
- expect(mockWindow.addEventListener).toHaveBeenCalledWith(
842
- 'popstate',
843
- expect.any(Function)
844
- );
845
-
846
- if (capturedCallback) {
847
- capturedCallback(); // This will trigger line 160: history.state || {}
848
- }
849
-
850
- expect(callbackData.length).toBe(1);
851
- expect(callbackData[0].url).toBe('http://test.com/page');
852
- expect(callbackData[0].state).toEqual({}); // Should be {} when history.state is null
853
-
854
- nav.destroy();
855
- vi.unstubAllGlobals();
856
- });
857
- });
858
- });