zero-query 0.9.9 → 1.0.0

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 (97) hide show
  1. package/README.md +33 -32
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +2 -2
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +3 -3
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +1 -2
  50. package/cli/scaffold/ssr/app/components/about.js +5 -5
  51. package/cli/scaffold/ssr/app/components/home.js +2 -2
  52. package/cli/scaffold/ssr/app/components/not-found.js +1 -1
  53. package/cli/scaffold/ssr/app/routes.js +1 -1
  54. package/cli/scaffold/ssr/global.css +2 -2
  55. package/cli/scaffold/ssr/index.html +2 -2
  56. package/cli/scaffold/ssr/server/index.js +4 -4
  57. package/cli/utils.js +6 -6
  58. package/dist/zquery.dist.zip +0 -0
  59. package/dist/zquery.js +508 -227
  60. package/dist/zquery.min.js +2 -2
  61. package/index.d.ts +16 -13
  62. package/index.js +7 -5
  63. package/package.json +2 -2
  64. package/src/component.js +64 -63
  65. package/src/core.js +15 -15
  66. package/src/diff.js +38 -38
  67. package/src/errors.js +17 -17
  68. package/src/expression.js +15 -17
  69. package/src/http.js +4 -4
  70. package/src/reactive.js +75 -9
  71. package/src/router.js +104 -24
  72. package/src/ssr.js +28 -28
  73. package/src/store.js +103 -21
  74. package/src/utils.js +64 -12
  75. package/tests/audit.test.js +143 -15
  76. package/tests/cli.test.js +20 -20
  77. package/tests/component.test.js +121 -121
  78. package/tests/core.test.js +56 -56
  79. package/tests/diff.test.js +42 -42
  80. package/tests/errors.test.js +5 -5
  81. package/tests/expression.test.js +58 -53
  82. package/tests/http.test.js +20 -20
  83. package/tests/reactive.test.js +185 -24
  84. package/tests/router.test.js +501 -74
  85. package/tests/ssr.test.js +15 -13
  86. package/tests/store.test.js +264 -23
  87. package/tests/utils.test.js +163 -26
  88. package/types/collection.d.ts +2 -2
  89. package/types/component.d.ts +5 -5
  90. package/types/errors.d.ts +3 -3
  91. package/types/http.d.ts +3 -3
  92. package/types/misc.d.ts +9 -9
  93. package/types/reactive.d.ts +25 -3
  94. package/types/router.d.ts +10 -6
  95. package/types/ssr.d.ts +2 -2
  96. package/types/store.d.ts +40 -5
  97. package/types/utils.d.ts +1 -1
@@ -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 creation', () => {
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 route matching', () => {
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 navigation', () => {
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 _interpolateParams', () => {
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 z-link-params', () => {
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 guards', () => {
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 onChange', () => {
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 path normalization', () => {
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 destroy', () => {
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 wildcard routes', () => {
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 query parsing', () => {
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 it only normalizes slashes and base prefix
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 multiple guards', () => {
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 multi-param routes', () => {
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 same-path deduplication', () => {
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 should be a no-op
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 same-path / hash-only navigation
462
+ // History mode - same-path / hash-only navigation
463
463
  // ---------------------------------------------------------------------------
464
464
 
465
- describe('Router history mode deduplication', () => {
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 same route, different hash
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 substates', () => {
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 edge cases', () => {
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 route matching priority', () => {
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 route removal', () => {
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 dynamic route addition', () => {
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 navigation chaining', () => {
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 hash mode path parsing', () => {
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 history mode with base path', () => {
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 query string in hash mode', () => {
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 guard edge cases', () => {
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 onChange fires on resolve', () => {
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 multi-param extraction', () => {
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 substates hash mode', () => {
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 _interpolateParams edge cases', () => {
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 destroy completeness', () => {
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 same-route shallow equality', () => {
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 should skip
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 cancel navigation
1211
+ // Guard - cancel navigation
1212
1212
  // ===========================================================================
1213
1213
 
1214
- describe('Router guard returning false cancels navigation', () => {
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 redirect loop detection
1237
+ // Guard - redirect loop detection
1238
1238
  // ===========================================================================
1239
1239
 
1240
- describe('Router guard redirect loop protection', () => {
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 should not infinite loop
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 the guard count > 10 stops it
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 afterEach fires after resolve
1268
+ // Guard - afterEach fires after resolve
1269
1269
  // ===========================================================================
1270
1270
 
1271
- describe('Router afterEach hook', () => {
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 before guard that throws
1297
+ // Guard - before guard that throws
1298
1298
  // ===========================================================================
1299
1299
 
1300
- describe('Router before guard that throws', () => {
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 lazy loading with route.load', () => {
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 fallback 404 route', () => {
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 replace()', () => {
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 query getter', () => {
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() programmatic link generation
1445
+ // resolve() - programmatic link generation
1446
1446
  // ===========================================================================
1447
1447
 
1448
- describe('Router resolve()', () => {
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 back/forward/go wrappers', () => {
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 modified clicks bypass
1514
+ // Link click interception - modified clicks bypass
1515
1515
  // ===========================================================================
1516
1516
 
1517
- describe('Router link click interception', () => {
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 meta key bypasses SPA navigation
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 remove() route
1605
+ // Router - remove() route
1606
1606
  // ===========================================================================
1607
1607
 
1608
- describe('Router remove()', () => {
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 add() chaining
1627
+ // Router - add() chaining
1628
1628
  // ===========================================================================
1629
1629
 
1630
- describe('Router add() chaining', () => {
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 onChange unsubscribe
1646
+ // Router - onChange unsubscribe
1647
1647
  // ===========================================================================
1648
1648
 
1649
- describe('Router onChange unsubscribe', () => {
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 destroy cleans up
1675
+ // Router - destroy cleans up
1676
1676
  // ===========================================================================
1677
1677
 
1678
- describe('Router destroy cleans up', () => {
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 _interpolateParams
1701
+ // Router - _interpolateParams
1702
1702
  // ===========================================================================
1703
1703
 
1704
- describe('Router _interpolateParams', () => {
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 _normalizePath with base stripping
1734
+ // Router - _normalizePath with base stripping
1735
1735
  // ===========================================================================
1736
1736
 
1737
- describe('Router _normalizePath', () => {
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 navigate with options.params
1774
+ // Router - navigate with options.params
1775
1775
  // ===========================================================================
1776
1776
 
1777
- describe('Router navigate with options.params', () => {
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 render function components
1798
+ // Router - render function components
1799
1799
  // ===========================================================================
1800
1800
 
1801
- describe('Router render function component', () => {
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 substate onSubstate unsubscribe
1823
+ // Router - substate onSubstate unsubscribe
1824
1824
  // ===========================================================================
1825
1825
 
1826
- describe('Router onSubstate unsubscribe', () => {
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
+ });