@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.
- package/README.zh-CN.md +82 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.mjs +0 -1
- package/package.json +4 -4
- package/src/index.ts +0 -3
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.mjs +0 -8
- package/dist/location.test.d.ts +0 -8
- package/dist/location.test.mjs +0 -370
- package/dist/matcher.test.d.ts +0 -1
- package/dist/matcher.test.mjs +0 -1492
- package/dist/micro-app.dom.test.d.ts +0 -1
- package/dist/micro-app.dom.test.mjs +0 -532
- package/dist/navigation.test.d.ts +0 -1
- package/dist/navigation.test.mjs +0 -681
- package/dist/route-task.test.d.ts +0 -1
- package/dist/route-task.test.mjs +0 -673
- package/dist/route-transition.test.d.ts +0 -1
- package/dist/route-transition.test.mjs +0 -146
- package/dist/route.test.d.ts +0 -1
- package/dist/route.test.mjs +0 -1664
- package/dist/router-back.test.d.ts +0 -1
- package/dist/router-back.test.mjs +0 -361
- package/dist/router-forward.test.d.ts +0 -1
- package/dist/router-forward.test.mjs +0 -376
- package/dist/router-go.test.d.ts +0 -1
- package/dist/router-go.test.mjs +0 -73
- package/dist/router-guards-cleanup.test.d.ts +0 -1
- package/dist/router-guards-cleanup.test.mjs +0 -437
- package/dist/router-push.test.d.ts +0 -1
- package/dist/router-push.test.mjs +0 -115
- package/dist/router-replace.test.d.ts +0 -1
- package/dist/router-replace.test.mjs +0 -114
- package/dist/router-resolve.test.d.ts +0 -1
- package/dist/router-resolve.test.mjs +0 -393
- package/dist/router-restart-app.dom.test.d.ts +0 -1
- package/dist/router-restart-app.dom.test.mjs +0 -616
- package/dist/router-window-navigation.test.d.ts +0 -1
- package/dist/router-window-navigation.test.mjs +0 -359
- package/dist/util.test.d.ts +0 -1
- package/dist/util.test.mjs +0 -1020
- package/src/index.test.ts +0 -9
- package/src/location.test.ts +0 -406
- package/src/matcher.test.ts +0 -1685
- package/src/micro-app.dom.test.ts +0 -708
- package/src/navigation.test.ts +0 -858
- package/src/route-task.test.ts +0 -901
- package/src/route-transition.test.ts +0 -178
- package/src/route.test.ts +0 -2014
- package/src/router-back.test.ts +0 -487
- package/src/router-forward.test.ts +0 -506
- package/src/router-go.test.ts +0 -91
- package/src/router-guards-cleanup.test.ts +0 -595
- package/src/router-push.test.ts +0 -140
- package/src/router-replace.test.ts +0 -139
- package/src/router-resolve.test.ts +0 -475
- package/src/router-restart-app.dom.test.ts +0 -783
- package/src/router-window-navigation.test.ts +0 -457
- package/src/util.test.ts +0 -1262
package/src/navigation.test.ts
DELETED
|
@@ -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
|
-
});
|