zero-query 0.9.9 → 1.0.1
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.md +34 -33
- package/cli/args.js +1 -1
- package/cli/commands/build.js +2 -2
- package/cli/commands/bundle.js +21 -18
- package/cli/commands/create.js +9 -2
- package/cli/commands/dev/devtools/index.js +1 -1
- package/cli/commands/dev/devtools/js/core.js +14 -14
- package/cli/commands/dev/devtools/js/elements.js +4 -4
- package/cli/commands/dev/devtools/js/stats.js +1 -1
- package/cli/commands/dev/devtools/styles.css +2 -2
- package/cli/commands/dev/index.js +2 -2
- package/cli/commands/dev/logger.js +1 -1
- package/cli/commands/dev/overlay.js +21 -14
- package/cli/commands/dev/server.js +5 -5
- package/cli/commands/dev/validator.js +7 -7
- package/cli/commands/dev/watcher.js +6 -6
- package/cli/help.js +3 -3
- package/cli/index.js +1 -1
- package/cli/scaffold/default/app/app.js +17 -18
- package/cli/scaffold/default/app/components/about.js +9 -9
- package/cli/scaffold/default/app/components/api-demo.js +6 -6
- package/cli/scaffold/default/app/components/contact-card.js +4 -4
- package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
- package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
- package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
- package/cli/scaffold/default/app/components/counter.js +8 -8
- package/cli/scaffold/default/app/components/home.js +13 -13
- package/cli/scaffold/default/app/components/not-found.js +1 -1
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +11 -11
- package/cli/scaffold/default/app/components/playground/playground.js +11 -11
- package/cli/scaffold/default/app/components/todos.js +8 -8
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
- package/cli/scaffold/default/app/routes.js +1 -1
- package/cli/scaffold/default/app/store.js +1 -1
- package/cli/scaffold/default/global.css +2 -2
- package/cli/scaffold/default/index.html +2 -2
- package/cli/scaffold/minimal/app/app.js +6 -7
- package/cli/scaffold/minimal/app/components/about.js +5 -5
- package/cli/scaffold/minimal/app/components/counter.js +6 -6
- package/cli/scaffold/minimal/app/components/home.js +8 -8
- package/cli/scaffold/minimal/app/components/not-found.js +1 -1
- package/cli/scaffold/minimal/app/routes.js +1 -1
- package/cli/scaffold/minimal/app/store.js +1 -1
- package/cli/scaffold/minimal/global.css +2 -2
- package/cli/scaffold/minimal/index.html +1 -1
- package/cli/scaffold/ssr/app/app.js +1 -2
- package/cli/scaffold/ssr/app/components/about.js +5 -5
- package/cli/scaffold/ssr/app/components/home.js +2 -2
- package/cli/scaffold/ssr/app/components/not-found.js +2 -2
- package/cli/scaffold/ssr/app/routes.js +1 -1
- package/cli/scaffold/ssr/global.css +3 -4
- package/cli/scaffold/ssr/index.html +2 -2
- package/cli/scaffold/ssr/server/index.js +26 -25
- package/cli/utils.js +6 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +508 -227
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +16 -13
- package/index.js +7 -5
- package/package.json +3 -3
- package/src/component.js +64 -63
- package/src/core.js +15 -15
- package/src/diff.js +38 -38
- package/src/errors.js +17 -17
- package/src/expression.js +15 -17
- package/src/http.js +4 -4
- package/src/reactive.js +75 -9
- package/src/router.js +104 -24
- package/src/ssr.js +28 -28
- package/src/store.js +103 -21
- package/src/utils.js +64 -12
- package/tests/audit.test.js +143 -15
- package/tests/cli.test.js +20 -20
- package/tests/component.test.js +121 -121
- package/tests/core.test.js +56 -56
- package/tests/diff.test.js +42 -42
- package/tests/errors.test.js +5 -5
- package/tests/expression.test.js +58 -53
- package/tests/http.test.js +20 -20
- package/tests/reactive.test.js +185 -24
- package/tests/router.test.js +501 -74
- package/tests/ssr.test.js +15 -13
- package/tests/store.test.js +264 -23
- package/tests/test-minifier.js +153 -0
- package/tests/test-ssr.js +27 -0
- package/tests/utils.test.js +163 -26
- package/types/collection.d.ts +2 -2
- package/types/component.d.ts +5 -5
- package/types/errors.d.ts +3 -3
- package/types/http.d.ts +3 -3
- package/types/misc.d.ts +9 -9
- package/types/reactive.d.ts +25 -3
- package/types/router.d.ts +10 -6
- package/types/ssr.d.ts +2 -2
- package/types/store.d.ts +40 -5
- package/types/utils.d.ts +1 -1
package/tests/router.test.js
CHANGED
|
@@ -13,7 +13,7 @@ component('docs-page', { render: () => '<p>docs</p>' });
|
|
|
13
13
|
// Router creation and basic API
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
-
describe('Router
|
|
16
|
+
describe('Router - creation', () => {
|
|
17
17
|
beforeEach(() => {
|
|
18
18
|
document.body.innerHTML = '<div id="app"></div>';
|
|
19
19
|
});
|
|
@@ -58,7 +58,7 @@ describe('Router — creation', () => {
|
|
|
58
58
|
// Route matching
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
|
-
describe('Router
|
|
61
|
+
describe('Router - route matching', () => {
|
|
62
62
|
it('compiles path params', () => {
|
|
63
63
|
const router = createRouter({
|
|
64
64
|
mode: 'hash',
|
|
@@ -89,7 +89,7 @@ describe('Router — route matching', () => {
|
|
|
89
89
|
// Navigation
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
|
|
92
|
-
describe('Router
|
|
92
|
+
describe('Router - navigation', () => {
|
|
93
93
|
let router;
|
|
94
94
|
|
|
95
95
|
beforeEach(() => {
|
|
@@ -151,7 +151,7 @@ describe('Router — navigation', () => {
|
|
|
151
151
|
// _interpolateParams
|
|
152
152
|
// ---------------------------------------------------------------------------
|
|
153
153
|
|
|
154
|
-
describe('Router
|
|
154
|
+
describe('Router - _interpolateParams', () => {
|
|
155
155
|
let router;
|
|
156
156
|
|
|
157
157
|
beforeEach(() => {
|
|
@@ -190,7 +190,7 @@ describe('Router — _interpolateParams', () => {
|
|
|
190
190
|
// z-link-params
|
|
191
191
|
// ---------------------------------------------------------------------------
|
|
192
192
|
|
|
193
|
-
describe('Router
|
|
193
|
+
describe('Router - z-link-params', () => {
|
|
194
194
|
let router;
|
|
195
195
|
|
|
196
196
|
beforeEach(() => {
|
|
@@ -233,7 +233,7 @@ describe('Router — z-link-params', () => {
|
|
|
233
233
|
// Guards
|
|
234
234
|
// ---------------------------------------------------------------------------
|
|
235
235
|
|
|
236
|
-
describe('Router
|
|
236
|
+
describe('Router - guards', () => {
|
|
237
237
|
it('beforeEach registers a guard', () => {
|
|
238
238
|
const router = createRouter({
|
|
239
239
|
mode: 'hash',
|
|
@@ -260,7 +260,7 @@ describe('Router — guards', () => {
|
|
|
260
260
|
// onChange listener
|
|
261
261
|
// ---------------------------------------------------------------------------
|
|
262
262
|
|
|
263
|
-
describe('Router
|
|
263
|
+
describe('Router - onChange', () => {
|
|
264
264
|
it('onChange registers and returns unsubscribe', () => {
|
|
265
265
|
const router = createRouter({
|
|
266
266
|
mode: 'hash',
|
|
@@ -279,7 +279,7 @@ describe('Router — onChange', () => {
|
|
|
279
279
|
// Path normalization
|
|
280
280
|
// ---------------------------------------------------------------------------
|
|
281
281
|
|
|
282
|
-
describe('Router
|
|
282
|
+
describe('Router - path normalization', () => {
|
|
283
283
|
it('normalizes relative paths', () => {
|
|
284
284
|
const router = createRouter({
|
|
285
285
|
mode: 'hash',
|
|
@@ -316,7 +316,7 @@ describe('Router — path normalization', () => {
|
|
|
316
316
|
// Destroy
|
|
317
317
|
// ---------------------------------------------------------------------------
|
|
318
318
|
|
|
319
|
-
describe('Router
|
|
319
|
+
describe('Router - destroy', () => {
|
|
320
320
|
it('clears routes, guards, and listeners', () => {
|
|
321
321
|
const router = createRouter({
|
|
322
322
|
mode: 'hash',
|
|
@@ -336,7 +336,7 @@ describe('Router — destroy', () => {
|
|
|
336
336
|
// Wildcard / catch-all routes
|
|
337
337
|
// ---------------------------------------------------------------------------
|
|
338
338
|
|
|
339
|
-
describe('Router
|
|
339
|
+
describe('Router - wildcard routes', () => {
|
|
340
340
|
it('compiles wildcard route', () => {
|
|
341
341
|
const router = createRouter({
|
|
342
342
|
mode: 'hash',
|
|
@@ -355,13 +355,13 @@ describe('Router — wildcard routes', () => {
|
|
|
355
355
|
// Query string handling
|
|
356
356
|
// ---------------------------------------------------------------------------
|
|
357
357
|
|
|
358
|
-
describe('Router
|
|
358
|
+
describe('Router - query parsing', () => {
|
|
359
359
|
it('_normalizePath strips query string for route matching', () => {
|
|
360
360
|
const router = createRouter({
|
|
361
361
|
mode: 'hash',
|
|
362
362
|
routes: [],
|
|
363
363
|
});
|
|
364
|
-
// _normalizePath does not strip query params
|
|
364
|
+
// _normalizePath does not strip query params - it only normalizes slashes and base prefix
|
|
365
365
|
const path = router._normalizePath('/docs?section=intro');
|
|
366
366
|
expect(path).toBe('/docs?section=intro');
|
|
367
367
|
});
|
|
@@ -372,7 +372,7 @@ describe('Router — query parsing', () => {
|
|
|
372
372
|
// Multiple guards
|
|
373
373
|
// ---------------------------------------------------------------------------
|
|
374
374
|
|
|
375
|
-
describe('Router
|
|
375
|
+
describe('Router - multiple guards', () => {
|
|
376
376
|
it('registers multiple beforeEach guards', () => {
|
|
377
377
|
const router = createRouter({
|
|
378
378
|
mode: 'hash',
|
|
@@ -405,7 +405,7 @@ describe('Router — multiple guards', () => {
|
|
|
405
405
|
// Route with multiple params
|
|
406
406
|
// ---------------------------------------------------------------------------
|
|
407
407
|
|
|
408
|
-
describe('Router
|
|
408
|
+
describe('Router - multi-param routes', () => {
|
|
409
409
|
it('compiles route with multiple params', () => {
|
|
410
410
|
const router = createRouter({
|
|
411
411
|
mode: 'hash',
|
|
@@ -424,7 +424,7 @@ describe('Router — multi-param routes', () => {
|
|
|
424
424
|
// Same-path deduplication
|
|
425
425
|
// ---------------------------------------------------------------------------
|
|
426
426
|
|
|
427
|
-
describe('Router
|
|
427
|
+
describe('Router - same-path deduplication', () => {
|
|
428
428
|
let router;
|
|
429
429
|
|
|
430
430
|
beforeEach(() => {
|
|
@@ -443,7 +443,7 @@ describe('Router — same-path deduplication', () => {
|
|
|
443
443
|
it('skips duplicate hash navigation to the same path', () => {
|
|
444
444
|
router.navigate('/about');
|
|
445
445
|
expect(window.location.hash).toBe('#/about');
|
|
446
|
-
// Navigate to the same path again
|
|
446
|
+
// Navigate to the same path again - should be a no-op
|
|
447
447
|
const result = router.navigate('/about');
|
|
448
448
|
expect(window.location.hash).toBe('#/about');
|
|
449
449
|
expect(result).toBe(router); // still returns the router chain
|
|
@@ -459,10 +459,10 @@ describe('Router — same-path deduplication', () => {
|
|
|
459
459
|
|
|
460
460
|
|
|
461
461
|
// ---------------------------------------------------------------------------
|
|
462
|
-
// History mode
|
|
462
|
+
// History mode - same-path / hash-only navigation
|
|
463
463
|
// ---------------------------------------------------------------------------
|
|
464
464
|
|
|
465
|
-
describe('Router
|
|
465
|
+
describe('Router - history mode deduplication', () => {
|
|
466
466
|
let router;
|
|
467
467
|
let pushSpy, replaceSpy;
|
|
468
468
|
|
|
@@ -499,7 +499,7 @@ describe('Router — history mode deduplication', () => {
|
|
|
499
499
|
router.navigate('/docs');
|
|
500
500
|
pushSpy.mockClear();
|
|
501
501
|
replaceSpy.mockClear();
|
|
502
|
-
// Navigate to /docs#section
|
|
502
|
+
// Navigate to /docs#section - same route, different hash
|
|
503
503
|
router.navigate('/docs#section');
|
|
504
504
|
expect(pushSpy).not.toHaveBeenCalled();
|
|
505
505
|
expect(replaceSpy).toHaveBeenCalledTimes(1);
|
|
@@ -511,7 +511,7 @@ describe('Router — history mode deduplication', () => {
|
|
|
511
511
|
// Sub-route history substates
|
|
512
512
|
// ---------------------------------------------------------------------------
|
|
513
513
|
|
|
514
|
-
describe('Router
|
|
514
|
+
describe('Router - substates', () => {
|
|
515
515
|
let router;
|
|
516
516
|
let pushSpy;
|
|
517
517
|
|
|
@@ -635,7 +635,7 @@ describe('Router — substates', () => {
|
|
|
635
635
|
// Edge cases
|
|
636
636
|
// ---------------------------------------------------------------------------
|
|
637
637
|
|
|
638
|
-
describe('Router
|
|
638
|
+
describe('Router - edge cases', () => {
|
|
639
639
|
it('handles empty routes array', () => {
|
|
640
640
|
const router = createRouter({ mode: 'hash', routes: [] });
|
|
641
641
|
expect(router._routes.length).toBe(0);
|
|
@@ -653,7 +653,7 @@ describe('Router — edge cases', () => {
|
|
|
653
653
|
// Route matching priority (first match wins)
|
|
654
654
|
// ---------------------------------------------------------------------------
|
|
655
655
|
|
|
656
|
-
describe('Router
|
|
656
|
+
describe('Router - route matching priority', () => {
|
|
657
657
|
it('first matching route wins', () => {
|
|
658
658
|
const router = createRouter({
|
|
659
659
|
mode: 'hash',
|
|
@@ -701,7 +701,7 @@ describe('Router — route matching priority', () => {
|
|
|
701
701
|
// Route removal
|
|
702
702
|
// ---------------------------------------------------------------------------
|
|
703
703
|
|
|
704
|
-
describe('Router
|
|
704
|
+
describe('Router - route removal', () => {
|
|
705
705
|
it('remove() deletes the matching route', () => {
|
|
706
706
|
const router = createRouter({
|
|
707
707
|
mode: 'hash',
|
|
@@ -740,7 +740,7 @@ describe('Router — route removal', () => {
|
|
|
740
740
|
// Dynamic route addition
|
|
741
741
|
// ---------------------------------------------------------------------------
|
|
742
742
|
|
|
743
|
-
describe('Router
|
|
743
|
+
describe('Router - dynamic route addition', () => {
|
|
744
744
|
it('add() returns the router for chaining', () => {
|
|
745
745
|
const router = createRouter({ mode: 'hash', routes: [] });
|
|
746
746
|
const result = router.add({ path: '/new', component: 'home-page' });
|
|
@@ -770,7 +770,7 @@ describe('Router — dynamic route addition', () => {
|
|
|
770
770
|
// Navigation chaining
|
|
771
771
|
// ---------------------------------------------------------------------------
|
|
772
772
|
|
|
773
|
-
describe('Router
|
|
773
|
+
describe('Router - navigation chaining', () => {
|
|
774
774
|
let router;
|
|
775
775
|
beforeEach(() => {
|
|
776
776
|
document.body.innerHTML = '<div id="app"></div>';
|
|
@@ -813,7 +813,7 @@ describe('Router — navigation chaining', () => {
|
|
|
813
813
|
// Hash mode path parsing
|
|
814
814
|
// ---------------------------------------------------------------------------
|
|
815
815
|
|
|
816
|
-
describe('Router
|
|
816
|
+
describe('Router - hash mode path parsing', () => {
|
|
817
817
|
let router;
|
|
818
818
|
beforeEach(() => {
|
|
819
819
|
router = createRouter({
|
|
@@ -843,7 +843,7 @@ describe('Router — hash mode path parsing', () => {
|
|
|
843
843
|
// History mode path handling with base
|
|
844
844
|
// ---------------------------------------------------------------------------
|
|
845
845
|
|
|
846
|
-
describe('Router
|
|
846
|
+
describe('Router - history mode with base path', () => {
|
|
847
847
|
it('resolve includes base prefix', () => {
|
|
848
848
|
const router = createRouter({
|
|
849
849
|
mode: 'history',
|
|
@@ -879,7 +879,7 @@ describe('Router — history mode with base path', () => {
|
|
|
879
879
|
// Navigate with query strings in hash mode
|
|
880
880
|
// ---------------------------------------------------------------------------
|
|
881
881
|
|
|
882
|
-
describe('Router
|
|
882
|
+
describe('Router - query string in hash mode', () => {
|
|
883
883
|
let router;
|
|
884
884
|
beforeEach(() => {
|
|
885
885
|
document.body.innerHTML = '<div id="app"></div>';
|
|
@@ -905,7 +905,7 @@ describe('Router — query string in hash mode', () => {
|
|
|
905
905
|
// Guard edge cases
|
|
906
906
|
// ---------------------------------------------------------------------------
|
|
907
907
|
|
|
908
|
-
describe('Router
|
|
908
|
+
describe('Router - guard edge cases', () => {
|
|
909
909
|
it('beforeEach returns the router for chaining', () => {
|
|
910
910
|
const router = createRouter({
|
|
911
911
|
mode: 'hash',
|
|
@@ -967,7 +967,7 @@ describe('Router — guard edge cases', () => {
|
|
|
967
967
|
// onChange with navigation
|
|
968
968
|
// ---------------------------------------------------------------------------
|
|
969
969
|
|
|
970
|
-
describe('Router
|
|
970
|
+
describe('Router - onChange fires on resolve', () => {
|
|
971
971
|
it('fires onChange listener after route resolution', async () => {
|
|
972
972
|
document.body.innerHTML = '<div id="app"></div>';
|
|
973
973
|
const listener = vi.fn();
|
|
@@ -997,7 +997,7 @@ describe('Router — onChange fires on resolve', () => {
|
|
|
997
997
|
// Multi-param extraction
|
|
998
998
|
// ---------------------------------------------------------------------------
|
|
999
999
|
|
|
1000
|
-
describe('Router
|
|
1000
|
+
describe('Router - multi-param extraction', () => {
|
|
1001
1001
|
it('extracts multiple params from URL', () => {
|
|
1002
1002
|
const router = createRouter({
|
|
1003
1003
|
mode: 'hash',
|
|
@@ -1019,7 +1019,7 @@ describe('Router — multi-param extraction', () => {
|
|
|
1019
1019
|
// Substate in hash mode
|
|
1020
1020
|
// ---------------------------------------------------------------------------
|
|
1021
1021
|
|
|
1022
|
-
describe('Router
|
|
1022
|
+
describe('Router - substates hash mode', () => {
|
|
1023
1023
|
let router, pushSpy;
|
|
1024
1024
|
|
|
1025
1025
|
beforeEach(() => {
|
|
@@ -1058,7 +1058,7 @@ describe('Router — substates hash mode', () => {
|
|
|
1058
1058
|
// _interpolateParams edge cases
|
|
1059
1059
|
// ---------------------------------------------------------------------------
|
|
1060
1060
|
|
|
1061
|
-
describe('Router
|
|
1061
|
+
describe('Router - _interpolateParams edge cases', () => {
|
|
1062
1062
|
let router;
|
|
1063
1063
|
beforeEach(() => {
|
|
1064
1064
|
router = createRouter({ mode: 'hash', routes: [] });
|
|
@@ -1099,7 +1099,7 @@ describe('Router — _interpolateParams edge cases', () => {
|
|
|
1099
1099
|
// Router.destroy cleans up everything
|
|
1100
1100
|
// ---------------------------------------------------------------------------
|
|
1101
1101
|
|
|
1102
|
-
describe('Router
|
|
1102
|
+
describe('Router - destroy completeness', () => {
|
|
1103
1103
|
it('clears instance, routes, guards, listeners, and substates', () => {
|
|
1104
1104
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1105
1105
|
const router = createRouter({
|
|
@@ -1178,7 +1178,7 @@ describe('Router — destroy completeness', () => {
|
|
|
1178
1178
|
// PERF: same-route comparison uses shallow equality (no JSON.stringify)
|
|
1179
1179
|
// ---------------------------------------------------------------------------
|
|
1180
1180
|
|
|
1181
|
-
describe('Router
|
|
1181
|
+
describe('Router - same-route shallow equality', () => {
|
|
1182
1182
|
it('skips re-render when navigating to same route with same params', async () => {
|
|
1183
1183
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1184
1184
|
let renderCount = 0;
|
|
@@ -1196,7 +1196,7 @@ describe('Router — same-route shallow equality', () => {
|
|
|
1196
1196
|
await new Promise(r => setTimeout(r, 50));
|
|
1197
1197
|
const firstCount = renderCount;
|
|
1198
1198
|
|
|
1199
|
-
// Navigate to the same route
|
|
1199
|
+
// Navigate to the same route - should skip
|
|
1200
1200
|
router.navigate('/user/42');
|
|
1201
1201
|
await new Promise(r => setTimeout(r, 50));
|
|
1202
1202
|
// Hash mode prevents same-hash navigation at URL level,
|
|
@@ -1208,10 +1208,10 @@ describe('Router — same-route shallow equality', () => {
|
|
|
1208
1208
|
|
|
1209
1209
|
|
|
1210
1210
|
// ===========================================================================
|
|
1211
|
-
// Guard
|
|
1211
|
+
// Guard - cancel navigation
|
|
1212
1212
|
// ===========================================================================
|
|
1213
1213
|
|
|
1214
|
-
describe('Router
|
|
1214
|
+
describe('Router - guard returning false cancels navigation', () => {
|
|
1215
1215
|
it('does not resolve route when guard returns false', async () => {
|
|
1216
1216
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1217
1217
|
const router = createRouter({
|
|
@@ -1234,10 +1234,10 @@ describe('Router — guard returning false cancels navigation', () => {
|
|
|
1234
1234
|
|
|
1235
1235
|
|
|
1236
1236
|
// ===========================================================================
|
|
1237
|
-
// Guard
|
|
1237
|
+
// Guard - redirect loop detection
|
|
1238
1238
|
// ===========================================================================
|
|
1239
1239
|
|
|
1240
|
-
describe('Router
|
|
1240
|
+
describe('Router - guard redirect loop protection', () => {
|
|
1241
1241
|
it('stops after more than 10 redirects', async () => {
|
|
1242
1242
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1243
1243
|
const router = createRouter({
|
|
@@ -1255,20 +1255,20 @@ describe('Router — guard redirect loop protection', () => {
|
|
|
1255
1255
|
if (to.path === '/b') return '/a';
|
|
1256
1256
|
});
|
|
1257
1257
|
await new Promise(r => setTimeout(r, 10));
|
|
1258
|
-
// Navigate to /a
|
|
1258
|
+
// Navigate to /a - should not infinite loop
|
|
1259
1259
|
window.location.hash = '#/a';
|
|
1260
1260
|
await router._resolve();
|
|
1261
|
-
// Just verify it doesn't hang
|
|
1261
|
+
// Just verify it doesn't hang - the guard count > 10 stops it
|
|
1262
1262
|
router.destroy();
|
|
1263
1263
|
});
|
|
1264
1264
|
});
|
|
1265
1265
|
|
|
1266
1266
|
|
|
1267
1267
|
// ===========================================================================
|
|
1268
|
-
// Guard
|
|
1268
|
+
// Guard - afterEach fires after resolve
|
|
1269
1269
|
// ===========================================================================
|
|
1270
1270
|
|
|
1271
|
-
describe('Router
|
|
1271
|
+
describe('Router - afterEach hook', () => {
|
|
1272
1272
|
it('fires afterEach with to and from after route resolves', async () => {
|
|
1273
1273
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1274
1274
|
const afterFn = vi.fn();
|
|
@@ -1294,10 +1294,10 @@ describe('Router — afterEach hook', () => {
|
|
|
1294
1294
|
|
|
1295
1295
|
|
|
1296
1296
|
// ===========================================================================
|
|
1297
|
-
// Guard
|
|
1297
|
+
// Guard - before guard that throws
|
|
1298
1298
|
// ===========================================================================
|
|
1299
1299
|
|
|
1300
|
-
describe('Router
|
|
1300
|
+
describe('Router - before guard that throws', () => {
|
|
1301
1301
|
it('catches the error and does not crash', async () => {
|
|
1302
1302
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1303
1303
|
const router = createRouter({
|
|
@@ -1321,7 +1321,7 @@ describe('Router — before guard that throws', () => {
|
|
|
1321
1321
|
// Lazy loading via route.load
|
|
1322
1322
|
// ===========================================================================
|
|
1323
1323
|
|
|
1324
|
-
describe('Router
|
|
1324
|
+
describe('Router - lazy loading with route.load', () => {
|
|
1325
1325
|
it('calls load() before mounting component', async () => {
|
|
1326
1326
|
const loadFn = vi.fn().mockResolvedValue(undefined);
|
|
1327
1327
|
document.body.innerHTML = '<div id="app"></div>';
|
|
@@ -1365,7 +1365,7 @@ describe('Router — lazy loading with route.load', () => {
|
|
|
1365
1365
|
// Fallback / 404 route
|
|
1366
1366
|
// ===========================================================================
|
|
1367
1367
|
|
|
1368
|
-
describe('Router
|
|
1368
|
+
describe('Router - fallback 404 route', () => {
|
|
1369
1369
|
it('resolves to fallback component for unknown paths', async () => {
|
|
1370
1370
|
component('notfound-page', { render: () => '<p>404</p>' });
|
|
1371
1371
|
document.body.innerHTML = '<div id="app"></div>';
|
|
@@ -1390,7 +1390,7 @@ describe('Router — fallback 404 route', () => {
|
|
|
1390
1390
|
// replace()
|
|
1391
1391
|
// ===========================================================================
|
|
1392
1392
|
|
|
1393
|
-
describe('Router
|
|
1393
|
+
describe('Router - replace()', () => {
|
|
1394
1394
|
it('returns router for chaining in hash mode', async () => {
|
|
1395
1395
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1396
1396
|
window.location.hash = '#/';
|
|
@@ -1414,7 +1414,7 @@ describe('Router — replace()', () => {
|
|
|
1414
1414
|
// query getter
|
|
1415
1415
|
// ===========================================================================
|
|
1416
1416
|
|
|
1417
|
-
describe('Router
|
|
1417
|
+
describe('Router - query getter', () => {
|
|
1418
1418
|
it('returns parsed query params from hash', () => {
|
|
1419
1419
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1420
1420
|
const router = createRouter({
|
|
@@ -1442,10 +1442,10 @@ describe('Router — query getter', () => {
|
|
|
1442
1442
|
|
|
1443
1443
|
|
|
1444
1444
|
// ===========================================================================
|
|
1445
|
-
// resolve()
|
|
1445
|
+
// resolve() - programmatic link generation
|
|
1446
1446
|
// ===========================================================================
|
|
1447
1447
|
|
|
1448
|
-
describe('Router
|
|
1448
|
+
describe('Router - resolve()', () => {
|
|
1449
1449
|
it('returns full URL path with base prefix', () => {
|
|
1450
1450
|
const router = createRouter({
|
|
1451
1451
|
mode: 'hash',
|
|
@@ -1471,7 +1471,7 @@ describe('Router — resolve()', () => {
|
|
|
1471
1471
|
// back/forward/go wrappers
|
|
1472
1472
|
// ===========================================================================
|
|
1473
1473
|
|
|
1474
|
-
describe('Router
|
|
1474
|
+
describe('Router - back/forward/go wrappers', () => {
|
|
1475
1475
|
it('calls window.history.back', () => {
|
|
1476
1476
|
const spy = vi.spyOn(window.history, 'back').mockImplementation(() => {});
|
|
1477
1477
|
const router = createRouter({
|
|
@@ -1511,10 +1511,10 @@ describe('Router — back/forward/go wrappers', () => {
|
|
|
1511
1511
|
|
|
1512
1512
|
|
|
1513
1513
|
// ===========================================================================
|
|
1514
|
-
// Link click interception
|
|
1514
|
+
// Link click interception - modified clicks bypass
|
|
1515
1515
|
// ===========================================================================
|
|
1516
1516
|
|
|
1517
|
-
describe('Router
|
|
1517
|
+
describe('Router - link click interception', () => {
|
|
1518
1518
|
it('intercepts normal clicks on z-link elements', async () => {
|
|
1519
1519
|
document.body.innerHTML = '<div id="app"></div><a z-link="/about">About</a>';
|
|
1520
1520
|
const router = createRouter({
|
|
@@ -1555,7 +1555,7 @@ describe('Router — link click interception', () => {
|
|
|
1555
1555
|
const e = new MouseEvent('click', { bubbles: true, metaKey: true });
|
|
1556
1556
|
link.dispatchEvent(e);
|
|
1557
1557
|
await new Promise(r => setTimeout(r, 10));
|
|
1558
|
-
// Route should remain unchanged
|
|
1558
|
+
// Route should remain unchanged - meta key bypasses SPA navigation
|
|
1559
1559
|
expect(router.current?.path).toBe(currentBefore);
|
|
1560
1560
|
router.destroy();
|
|
1561
1561
|
});
|
|
@@ -1602,10 +1602,10 @@ describe('Router — link click interception', () => {
|
|
|
1602
1602
|
|
|
1603
1603
|
|
|
1604
1604
|
// ===========================================================================
|
|
1605
|
-
// Router
|
|
1605
|
+
// Router - remove() route
|
|
1606
1606
|
// ===========================================================================
|
|
1607
1607
|
|
|
1608
|
-
describe('Router
|
|
1608
|
+
describe('Router - remove()', () => {
|
|
1609
1609
|
it('removes route by path', () => {
|
|
1610
1610
|
const router = createRouter({
|
|
1611
1611
|
mode: 'hash',
|
|
@@ -1624,10 +1624,10 @@ describe('Router — remove()', () => {
|
|
|
1624
1624
|
|
|
1625
1625
|
|
|
1626
1626
|
// ===========================================================================
|
|
1627
|
-
// Router
|
|
1627
|
+
// Router - add() chaining
|
|
1628
1628
|
// ===========================================================================
|
|
1629
1629
|
|
|
1630
|
-
describe('Router
|
|
1630
|
+
describe('Router - add() chaining', () => {
|
|
1631
1631
|
it('supports fluent chaining of add calls', () => {
|
|
1632
1632
|
const router = createRouter({
|
|
1633
1633
|
mode: 'hash',
|
|
@@ -1643,10 +1643,10 @@ describe('Router — add() chaining', () => {
|
|
|
1643
1643
|
|
|
1644
1644
|
|
|
1645
1645
|
// ===========================================================================
|
|
1646
|
-
// Router
|
|
1646
|
+
// Router - onChange unsubscribe
|
|
1647
1647
|
// ===========================================================================
|
|
1648
1648
|
|
|
1649
|
-
describe('Router
|
|
1649
|
+
describe('Router - onChange unsubscribe', () => {
|
|
1650
1650
|
it('stops calling listener after unsubscribe', async () => {
|
|
1651
1651
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1652
1652
|
const listener = vi.fn();
|
|
@@ -1672,10 +1672,10 @@ describe('Router — onChange unsubscribe', () => {
|
|
|
1672
1672
|
|
|
1673
1673
|
|
|
1674
1674
|
// ===========================================================================
|
|
1675
|
-
// Router
|
|
1675
|
+
// Router - destroy cleans up
|
|
1676
1676
|
// ===========================================================================
|
|
1677
1677
|
|
|
1678
|
-
describe('Router
|
|
1678
|
+
describe('Router - destroy cleans up', () => {
|
|
1679
1679
|
it('clears listeners, guards, and routes on destroy', () => {
|
|
1680
1680
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1681
1681
|
const router = createRouter({
|
|
@@ -1698,10 +1698,10 @@ describe('Router — destroy cleans up', () => {
|
|
|
1698
1698
|
|
|
1699
1699
|
|
|
1700
1700
|
// ===========================================================================
|
|
1701
|
-
// Router
|
|
1701
|
+
// Router - _interpolateParams
|
|
1702
1702
|
// ===========================================================================
|
|
1703
1703
|
|
|
1704
|
-
describe('Router
|
|
1704
|
+
describe('Router - _interpolateParams', () => {
|
|
1705
1705
|
it('replaces :param with provided values', () => {
|
|
1706
1706
|
const router = createRouter({
|
|
1707
1707
|
mode: 'hash',
|
|
@@ -1731,10 +1731,10 @@ describe('Router — _interpolateParams', () => {
|
|
|
1731
1731
|
|
|
1732
1732
|
|
|
1733
1733
|
// ===========================================================================
|
|
1734
|
-
// Router
|
|
1734
|
+
// Router - _normalizePath with base stripping
|
|
1735
1735
|
// ===========================================================================
|
|
1736
1736
|
|
|
1737
|
-
describe('Router
|
|
1737
|
+
describe('Router - _normalizePath', () => {
|
|
1738
1738
|
it('strips base prefix if accidentally included', () => {
|
|
1739
1739
|
const router = createRouter({
|
|
1740
1740
|
mode: 'hash',
|
|
@@ -1771,10 +1771,10 @@ describe('Router — _normalizePath', () => {
|
|
|
1771
1771
|
|
|
1772
1772
|
|
|
1773
1773
|
// ===========================================================================
|
|
1774
|
-
// Router
|
|
1774
|
+
// Router - navigate with options.params
|
|
1775
1775
|
// ===========================================================================
|
|
1776
1776
|
|
|
1777
|
-
describe('Router
|
|
1777
|
+
describe('Router - navigate with options.params', () => {
|
|
1778
1778
|
it('interpolates params in path', async () => {
|
|
1779
1779
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1780
1780
|
const router = createRouter({
|
|
@@ -1795,10 +1795,10 @@ describe('Router — navigate with options.params', () => {
|
|
|
1795
1795
|
|
|
1796
1796
|
|
|
1797
1797
|
// ===========================================================================
|
|
1798
|
-
// Router
|
|
1798
|
+
// Router - render function components
|
|
1799
1799
|
// ===========================================================================
|
|
1800
1800
|
|
|
1801
|
-
describe('Router
|
|
1801
|
+
describe('Router - render function component', () => {
|
|
1802
1802
|
it('renders HTML from a function component', async () => {
|
|
1803
1803
|
document.body.innerHTML = '<div id="app"></div>';
|
|
1804
1804
|
window.location.hash = '#/';
|
|
@@ -1820,10 +1820,10 @@ describe('Router — render function component', () => {
|
|
|
1820
1820
|
|
|
1821
1821
|
|
|
1822
1822
|
// ===========================================================================
|
|
1823
|
-
// Router
|
|
1823
|
+
// Router - substate onSubstate unsubscribe
|
|
1824
1824
|
// ===========================================================================
|
|
1825
1825
|
|
|
1826
|
-
describe('Router
|
|
1826
|
+
describe('Router - onSubstate unsubscribe', () => {
|
|
1827
1827
|
it('removes listener after unsubscribe', () => {
|
|
1828
1828
|
const router = createRouter({ mode: 'hash', routes: [] });
|
|
1829
1829
|
const fn = vi.fn();
|
|
@@ -1834,3 +1834,430 @@ describe('Router — onSubstate unsubscribe', () => {
|
|
|
1834
1834
|
router.destroy();
|
|
1835
1835
|
});
|
|
1836
1836
|
});
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
// ===========================================================================
|
|
1840
|
+
// Router - substate history restoration after navigation away
|
|
1841
|
+
// ===========================================================================
|
|
1842
|
+
|
|
1843
|
+
describe('Router - substate restoration after navigating away and back', () => {
|
|
1844
|
+
let router;
|
|
1845
|
+
|
|
1846
|
+
beforeEach(() => {
|
|
1847
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
1848
|
+
window.location.hash = '#/';
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
afterEach(() => {
|
|
1852
|
+
if (router) router.destroy();
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
/**
|
|
1856
|
+
* Reproduces the bug: tabbing through substates in a component (e.g.
|
|
1857
|
+
* compare-page), navigating to a different route (e.g. /about), then
|
|
1858
|
+
* pressing back should return to the LAST substate tab — not the default.
|
|
1859
|
+
*
|
|
1860
|
+
* Steps to reproduce:
|
|
1861
|
+
* 1. Navigate to /compare, push substates: tab-a → tab-b → tab-c
|
|
1862
|
+
* 2. Navigate to /about (component with listener is destroyed)
|
|
1863
|
+
* 3. Simulate popstate back → lands on substate entry for tab-c
|
|
1864
|
+
* 4. BUG: no listener exists (it was destroyed), so the substate is
|
|
1865
|
+
* ignored and the component remounts with its default state.
|
|
1866
|
+
* 5. EXPECTED: the router should re-fire the substate after the new
|
|
1867
|
+
* component mounts so the listener can restore the correct tab.
|
|
1868
|
+
*/
|
|
1869
|
+
it('restores last substate when pressing back after navigating away', async () => {
|
|
1870
|
+
router = createRouter({
|
|
1871
|
+
el: '#app',
|
|
1872
|
+
mode: 'history',
|
|
1873
|
+
routes: [
|
|
1874
|
+
{ path: '/', component: 'home-page' },
|
|
1875
|
+
{ path: '/compare', component: 'about-page' },
|
|
1876
|
+
{ path: '/about', component: 'user-page' },
|
|
1877
|
+
],
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// Wait for initial resolve
|
|
1881
|
+
await new Promise(r => setTimeout(r, 10));
|
|
1882
|
+
|
|
1883
|
+
// 1. Navigate to /compare and register a substate listener
|
|
1884
|
+
// (simulates what compare-page does in its mounted() hook)
|
|
1885
|
+
window.history.pushState({ __zq: 'route' }, '', '/compare');
|
|
1886
|
+
await router._resolve();
|
|
1887
|
+
expect(router.current.path).toBe('/compare');
|
|
1888
|
+
|
|
1889
|
+
// Simulate component mounting and registering a substate listener
|
|
1890
|
+
let activeTab = 'overview';
|
|
1891
|
+
const listener = vi.fn((key, data, action) => {
|
|
1892
|
+
if (key === 'compare-tab') { activeTab = data.tab; return true; }
|
|
1893
|
+
if (action === 'reset') { activeTab = 'overview'; return true; }
|
|
1894
|
+
});
|
|
1895
|
+
const unsub = router.onSubstate(listener);
|
|
1896
|
+
|
|
1897
|
+
// 2. Push several substates (tab switches)
|
|
1898
|
+
router.pushSubstate('compare-tab', { tab: 'components' });
|
|
1899
|
+
activeTab = 'components';
|
|
1900
|
+
router.pushSubstate('compare-tab', { tab: 'directives' });
|
|
1901
|
+
activeTab = 'directives';
|
|
1902
|
+
router.pushSubstate('compare-tab', { tab: 'reactivity' });
|
|
1903
|
+
activeTab = 'reactivity';
|
|
1904
|
+
|
|
1905
|
+
// 3. Navigate away to /about — this destroys compare-page
|
|
1906
|
+
// so we unsubscribe the listener (like destroyed() would)
|
|
1907
|
+
unsub();
|
|
1908
|
+
window.history.pushState({ __zq: 'route' }, '', '/about');
|
|
1909
|
+
await router._resolve();
|
|
1910
|
+
expect(router.current.path).toBe('/about');
|
|
1911
|
+
|
|
1912
|
+
// 4. Register a NEW listener after _resolve, simulating what the freshly
|
|
1913
|
+
// mounted compare-page would do. We use a one-time setup that defers
|
|
1914
|
+
// registration until _resolve has run (like mounted() in the component).
|
|
1915
|
+
let restoredTab = 'overview';
|
|
1916
|
+
const freshListener = vi.fn((key, data, action) => {
|
|
1917
|
+
if (key === 'compare-tab') { restoredTab = data.tab; return true; }
|
|
1918
|
+
if (action === 'reset') { restoredTab = 'overview'; return true; }
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
// Intercept _resolve to register the listener after mount (simulating
|
|
1922
|
+
// the component's mounted() lifecycle hook)
|
|
1923
|
+
const origResolve = router.__resolve.bind(router);
|
|
1924
|
+
router.__resolve = async function () {
|
|
1925
|
+
await origResolve();
|
|
1926
|
+
// After resolve mounts the component, register the substate listener
|
|
1927
|
+
router.onSubstate(freshListener);
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// 5. Simulate pressing back — popstate lands on the last substate
|
|
1931
|
+
const evt = new PopStateEvent('popstate', {
|
|
1932
|
+
state: { __zq: 'substate', key: 'compare-tab', data: { tab: 'reactivity' } }
|
|
1933
|
+
});
|
|
1934
|
+
window.dispatchEvent(evt);
|
|
1935
|
+
|
|
1936
|
+
// Allow async _resolve to complete and the retry to fire
|
|
1937
|
+
await new Promise(r => setTimeout(r, 50));
|
|
1938
|
+
|
|
1939
|
+
// EXPECTED: the fresh listener should have been called with the substate
|
|
1940
|
+
// data so that restoredTab is 'reactivity', NOT 'overview'
|
|
1941
|
+
expect(freshListener).toHaveBeenCalledWith('compare-tab', { tab: 'reactivity' }, 'pop');
|
|
1942
|
+
expect(restoredTab).toBe('reactivity');
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
it('handles second back correctly after substate restoration', async () => {
|
|
1946
|
+
router = createRouter({
|
|
1947
|
+
el: '#app',
|
|
1948
|
+
mode: 'history',
|
|
1949
|
+
routes: [
|
|
1950
|
+
{ path: '/', component: 'home-page' },
|
|
1951
|
+
{ path: '/compare', component: 'about-page' },
|
|
1952
|
+
{ path: '/about', component: 'user-page' },
|
|
1953
|
+
],
|
|
1954
|
+
});
|
|
1955
|
+
await new Promise(r => setTimeout(r, 10));
|
|
1956
|
+
|
|
1957
|
+
// Navigate to /compare
|
|
1958
|
+
window.history.pushState({ __zq: 'route' }, '', '/compare');
|
|
1959
|
+
await router._resolve();
|
|
1960
|
+
|
|
1961
|
+
let activeTab = 'overview';
|
|
1962
|
+
const listener = vi.fn((key, data, action) => {
|
|
1963
|
+
if (key === 'compare-tab') { activeTab = data.tab; return true; }
|
|
1964
|
+
if (action === 'reset') { activeTab = 'overview'; return true; }
|
|
1965
|
+
});
|
|
1966
|
+
const unsub = router.onSubstate(listener);
|
|
1967
|
+
|
|
1968
|
+
// Push two substates
|
|
1969
|
+
router.pushSubstate('compare-tab', { tab: 'components' });
|
|
1970
|
+
router.pushSubstate('compare-tab', { tab: 'directives' });
|
|
1971
|
+
|
|
1972
|
+
// Navigate away and unsubscribe (simulating component destroy)
|
|
1973
|
+
unsub();
|
|
1974
|
+
window.history.pushState({ __zq: 'route' }, '', '/about');
|
|
1975
|
+
await router._resolve();
|
|
1976
|
+
|
|
1977
|
+
// Back: first pop hits 'directives' substate — no listener yet
|
|
1978
|
+
let restoredTab = 'overview';
|
|
1979
|
+
const freshListener = vi.fn((key, data, action) => {
|
|
1980
|
+
if (key === 'compare-tab') { restoredTab = data.tab; return true; }
|
|
1981
|
+
if (action === 'reset') { restoredTab = 'overview'; return true; }
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
const origResolve = router.__resolve.bind(router);
|
|
1985
|
+
let resolveCount = 0;
|
|
1986
|
+
router.__resolve = async function () {
|
|
1987
|
+
await origResolve();
|
|
1988
|
+
if (resolveCount === 0) router.onSubstate(freshListener);
|
|
1989
|
+
resolveCount++;
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
// First back: lands on 'directives' substate
|
|
1993
|
+
window.dispatchEvent(new PopStateEvent('popstate', {
|
|
1994
|
+
state: { __zq: 'substate', key: 'compare-tab', data: { tab: 'directives' } }
|
|
1995
|
+
}));
|
|
1996
|
+
await new Promise(r => setTimeout(r, 50));
|
|
1997
|
+
|
|
1998
|
+
expect(restoredTab).toBe('directives');
|
|
1999
|
+
|
|
2000
|
+
// Second back: lands on 'components' substate — listener IS registered now
|
|
2001
|
+
window.dispatchEvent(new PopStateEvent('popstate', {
|
|
2002
|
+
state: { __zq: 'substate', key: 'compare-tab', data: { tab: 'components' } }
|
|
2003
|
+
}));
|
|
2004
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2005
|
+
|
|
2006
|
+
expect(restoredTab).toBe('components');
|
|
2007
|
+
});
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
// ===========================================================================
|
|
2012
|
+
// Router - <z-outlet> auto-detection
|
|
2013
|
+
// ===========================================================================
|
|
2014
|
+
|
|
2015
|
+
describe('Router - z-outlet auto-detection', () => {
|
|
2016
|
+
beforeEach(() => {
|
|
2017
|
+
window.location.hash = '#/';
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
it('auto-detects <z-outlet> when no el: is provided', async () => {
|
|
2021
|
+
document.body.innerHTML = '<z-outlet></z-outlet>';
|
|
2022
|
+
const router = createRouter({
|
|
2023
|
+
mode: 'hash',
|
|
2024
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2025
|
+
});
|
|
2026
|
+
expect(router._el).toBe(document.querySelector('z-outlet'));
|
|
2027
|
+
// Wait for initial resolve and verify component was mounted
|
|
2028
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2029
|
+
expect(router._el.innerHTML).not.toBe('');
|
|
2030
|
+
router.destroy();
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
it('prefers explicit el: over <z-outlet>', () => {
|
|
2034
|
+
document.body.innerHTML = '<div id="app"></div><z-outlet></z-outlet>';
|
|
2035
|
+
const router = createRouter({
|
|
2036
|
+
mode: 'hash',
|
|
2037
|
+
el: '#app',
|
|
2038
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2039
|
+
});
|
|
2040
|
+
expect(router._el).toBe(document.getElementById('app'));
|
|
2041
|
+
router.destroy();
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
it('reads fallback attribute from <z-outlet>', () => {
|
|
2045
|
+
document.body.innerHTML = '<z-outlet fallback="about-page"></z-outlet>';
|
|
2046
|
+
const router = createRouter({
|
|
2047
|
+
mode: 'hash',
|
|
2048
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2049
|
+
});
|
|
2050
|
+
expect(router._fallback).toBe('about-page');
|
|
2051
|
+
router.destroy();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
it('config fallback takes priority over <z-outlet> fallback attribute', () => {
|
|
2055
|
+
document.body.innerHTML = '<z-outlet fallback="about-page"></z-outlet>';
|
|
2056
|
+
const router = createRouter({
|
|
2057
|
+
mode: 'hash',
|
|
2058
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2059
|
+
fallback: 'user-page',
|
|
2060
|
+
});
|
|
2061
|
+
expect(router._fallback).toBe('user-page');
|
|
2062
|
+
router.destroy();
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
it('reads mode attribute from <z-outlet>', () => {
|
|
2066
|
+
document.body.innerHTML = '<z-outlet mode="hash"></z-outlet>';
|
|
2067
|
+
const router = createRouter({
|
|
2068
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2069
|
+
});
|
|
2070
|
+
expect(router._mode).toBe('hash');
|
|
2071
|
+
router.destroy();
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
it('reads base attribute from <z-outlet>', () => {
|
|
2075
|
+
document.body.innerHTML = '<z-outlet base="/my-app"></z-outlet>';
|
|
2076
|
+
const router = createRouter({
|
|
2077
|
+
mode: 'hash',
|
|
2078
|
+
routes: [],
|
|
2079
|
+
});
|
|
2080
|
+
expect(router._base).toBe('/my-app');
|
|
2081
|
+
router.destroy();
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
it('config base takes priority over <z-outlet> base attribute', () => {
|
|
2085
|
+
document.body.innerHTML = '<z-outlet base="/outlet-base"></z-outlet>';
|
|
2086
|
+
const router = createRouter({
|
|
2087
|
+
mode: 'hash',
|
|
2088
|
+
base: '/config-base',
|
|
2089
|
+
routes: [],
|
|
2090
|
+
});
|
|
2091
|
+
expect(router._base).toBe('/config-base');
|
|
2092
|
+
router.destroy();
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
it('falls back gracefully when no <z-outlet> and no el:', () => {
|
|
2096
|
+
document.body.innerHTML = '<div>no outlet here</div>';
|
|
2097
|
+
const router = createRouter({
|
|
2098
|
+
mode: 'hash',
|
|
2099
|
+
routes: [{ path: '/', component: 'home-page' }],
|
|
2100
|
+
});
|
|
2101
|
+
expect(router._el).toBeNull();
|
|
2102
|
+
router.destroy();
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
it('mounts and navigates using <z-outlet>', async () => {
|
|
2106
|
+
document.body.innerHTML = '<z-outlet></z-outlet>';
|
|
2107
|
+
const router = createRouter({
|
|
2108
|
+
mode: 'hash',
|
|
2109
|
+
routes: [
|
|
2110
|
+
{ path: '/', component: 'home-page' },
|
|
2111
|
+
{ path: '/about', component: 'about-page' },
|
|
2112
|
+
],
|
|
2113
|
+
});
|
|
2114
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2115
|
+
expect(router.current.path).toBe('/');
|
|
2116
|
+
router.navigate('/about');
|
|
2117
|
+
await router._resolve();
|
|
2118
|
+
expect(router.current.path).toBe('/about');
|
|
2119
|
+
router.destroy();
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
// ===========================================================================
|
|
2125
|
+
// z-active-route directive
|
|
2126
|
+
// ===========================================================================
|
|
2127
|
+
|
|
2128
|
+
describe('Router - z-active-route', () => {
|
|
2129
|
+
let router;
|
|
2130
|
+
|
|
2131
|
+
beforeEach(() => {
|
|
2132
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
2133
|
+
router = createRouter({
|
|
2134
|
+
el: '#app',
|
|
2135
|
+
mode: 'hash',
|
|
2136
|
+
routes: [
|
|
2137
|
+
{ path: '/', component: 'home-page' },
|
|
2138
|
+
{ path: '/about', component: 'about-page' },
|
|
2139
|
+
{ path: '/docs/:section', component: 'docs-page' },
|
|
2140
|
+
],
|
|
2141
|
+
});
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
afterEach(() => {
|
|
2145
|
+
if (router) router.destroy();
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
it('adds "active" class to matching element (prefix match)', () => {
|
|
2149
|
+
document.body.innerHTML += `
|
|
2150
|
+
<a z-link="/docs/intro" z-active-route="/docs">Docs</a>
|
|
2151
|
+
<a z-link="/about" z-active-route="/about">About</a>
|
|
2152
|
+
`;
|
|
2153
|
+
router._updateActiveRoutes('/docs/intro');
|
|
2154
|
+
const docsLink = document.querySelector('[z-active-route="/docs"]');
|
|
2155
|
+
const aboutLink = document.querySelector('[z-active-route="/about"]');
|
|
2156
|
+
expect(docsLink.classList.contains('active')).toBe(true);
|
|
2157
|
+
expect(aboutLink.classList.contains('active')).toBe(false);
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
it('removes "active" class when route no longer matches', () => {
|
|
2161
|
+
document.body.innerHTML += '<a z-active-route="/about">About</a>';
|
|
2162
|
+
const el = document.querySelector('[z-active-route="/about"]');
|
|
2163
|
+
|
|
2164
|
+
router._updateActiveRoutes('/about');
|
|
2165
|
+
expect(el.classList.contains('active')).toBe(true);
|
|
2166
|
+
|
|
2167
|
+
router._updateActiveRoutes('/docs');
|
|
2168
|
+
expect(el.classList.contains('active')).toBe(false);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
it('supports custom class via z-active-class', () => {
|
|
2172
|
+
document.body.innerHTML += '<a z-active-route="/about" z-active-class="selected">About</a>';
|
|
2173
|
+
const el = document.querySelector('[z-active-route="/about"]');
|
|
2174
|
+
|
|
2175
|
+
router._updateActiveRoutes('/about');
|
|
2176
|
+
expect(el.classList.contains('selected')).toBe(true);
|
|
2177
|
+
expect(el.classList.contains('active')).toBe(false);
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
it('supports exact matching with z-active-exact', () => {
|
|
2181
|
+
document.body.innerHTML += `
|
|
2182
|
+
<a z-active-route="/" z-active-exact>Home</a>
|
|
2183
|
+
<a z-active-route="/docs">Docs</a>
|
|
2184
|
+
`;
|
|
2185
|
+
const home = document.querySelector('[z-active-route="/"]');
|
|
2186
|
+
const docs = document.querySelector('[z-active-route="/docs"]');
|
|
2187
|
+
|
|
2188
|
+
// At root - home is exact match, docs should not match
|
|
2189
|
+
router._updateActiveRoutes('/');
|
|
2190
|
+
expect(home.classList.contains('active')).toBe(true);
|
|
2191
|
+
expect(docs.classList.contains('active')).toBe(false);
|
|
2192
|
+
|
|
2193
|
+
// At /docs - home exact should NOT match, docs prefix should match
|
|
2194
|
+
router._updateActiveRoutes('/docs');
|
|
2195
|
+
expect(home.classList.contains('active')).toBe(false);
|
|
2196
|
+
expect(docs.classList.contains('active')).toBe(true);
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
it('root "/" only matches itself, not all paths (prefix match)', () => {
|
|
2200
|
+
document.body.innerHTML += '<a z-active-route="/">Home</a>';
|
|
2201
|
+
const el = document.querySelector('[z-active-route="/"]');
|
|
2202
|
+
|
|
2203
|
+
router._updateActiveRoutes('/about');
|
|
2204
|
+
expect(el.classList.contains('active')).toBe(false);
|
|
2205
|
+
|
|
2206
|
+
router._updateActiveRoutes('/');
|
|
2207
|
+
expect(el.classList.contains('active')).toBe(true);
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
it('handles multiple elements simultaneously', () => {
|
|
2211
|
+
document.body.innerHTML += `
|
|
2212
|
+
<nav>
|
|
2213
|
+
<a z-active-route="/" z-active-exact>Home</a>
|
|
2214
|
+
<a z-active-route="/about">About</a>
|
|
2215
|
+
<a z-active-route="/docs">Docs</a>
|
|
2216
|
+
<a z-active-route="/docs" z-active-class="highlight">Docs Alt</a>
|
|
2217
|
+
</nav>
|
|
2218
|
+
`;
|
|
2219
|
+
|
|
2220
|
+
router._updateActiveRoutes('/docs/getting-started');
|
|
2221
|
+
|
|
2222
|
+
const home = document.querySelector('[z-active-route="/"]');
|
|
2223
|
+
const about = document.querySelector('[z-active-route="/about"]');
|
|
2224
|
+
const docs = document.querySelectorAll('[z-active-route="/docs"]');
|
|
2225
|
+
|
|
2226
|
+
expect(home.classList.contains('active')).toBe(false);
|
|
2227
|
+
expect(about.classList.contains('active')).toBe(false);
|
|
2228
|
+
expect(docs[0].classList.contains('active')).toBe(true);
|
|
2229
|
+
expect(docs[1].classList.contains('highlight')).toBe(true);
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
it('z-active-exact does not match child routes', () => {
|
|
2233
|
+
document.body.innerHTML += '<a z-active-route="/docs" z-active-exact>Docs</a>';
|
|
2234
|
+
const el = document.querySelector('[z-active-route="/docs"]');
|
|
2235
|
+
|
|
2236
|
+
router._updateActiveRoutes('/docs/intro');
|
|
2237
|
+
expect(el.classList.contains('active')).toBe(false);
|
|
2238
|
+
|
|
2239
|
+
router._updateActiveRoutes('/docs');
|
|
2240
|
+
expect(el.classList.contains('active')).toBe(true);
|
|
2241
|
+
});
|
|
2242
|
+
|
|
2243
|
+
it('toggles class correctly across navigation changes', () => {
|
|
2244
|
+
document.body.innerHTML += `
|
|
2245
|
+
<a z-active-route="/about">About</a>
|
|
2246
|
+
<a z-active-route="/docs">Docs</a>
|
|
2247
|
+
`;
|
|
2248
|
+
const about = document.querySelector('[z-active-route="/about"]');
|
|
2249
|
+
const docs = document.querySelector('[z-active-route="/docs"]');
|
|
2250
|
+
|
|
2251
|
+
router._updateActiveRoutes('/about');
|
|
2252
|
+
expect(about.classList.contains('active')).toBe(true);
|
|
2253
|
+
expect(docs.classList.contains('active')).toBe(false);
|
|
2254
|
+
|
|
2255
|
+
router._updateActiveRoutes('/docs/selectors');
|
|
2256
|
+
expect(about.classList.contains('active')).toBe(false);
|
|
2257
|
+
expect(docs.classList.contains('active')).toBe(true);
|
|
2258
|
+
|
|
2259
|
+
router._updateActiveRoutes('/');
|
|
2260
|
+
expect(about.classList.contains('active')).toBe(false);
|
|
2261
|
+
expect(docs.classList.contains('active')).toBe(false);
|
|
2262
|
+
});
|
|
2263
|
+
});
|