zero-query 0.9.8 → 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 (99) hide show
  1. package/README.md +55 -31
  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 +41 -7
  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 +4 -2
  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 +29 -0
  50. package/cli/scaffold/ssr/app/components/about.js +28 -0
  51. package/cli/scaffold/ssr/app/components/home.js +37 -0
  52. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  53. package/cli/scaffold/ssr/app/routes.js +6 -0
  54. package/cli/scaffold/ssr/global.css +113 -0
  55. package/cli/scaffold/ssr/index.html +31 -0
  56. package/cli/scaffold/ssr/package.json +8 -0
  57. package/cli/scaffold/ssr/server/index.js +118 -0
  58. package/cli/utils.js +6 -6
  59. package/dist/zquery.dist.zip +0 -0
  60. package/dist/zquery.js +565 -228
  61. package/dist/zquery.min.js +2 -2
  62. package/index.d.ts +25 -12
  63. package/index.js +11 -7
  64. package/package.json +9 -3
  65. package/src/component.js +64 -63
  66. package/src/core.js +15 -15
  67. package/src/diff.js +38 -38
  68. package/src/errors.js +72 -18
  69. package/src/expression.js +15 -17
  70. package/src/http.js +4 -4
  71. package/src/package.json +1 -0
  72. package/src/reactive.js +75 -9
  73. package/src/router.js +104 -24
  74. package/src/ssr.js +133 -39
  75. package/src/store.js +103 -21
  76. package/src/utils.js +64 -12
  77. package/tests/audit.test.js +143 -15
  78. package/tests/cli.test.js +20 -20
  79. package/tests/component.test.js +121 -121
  80. package/tests/core.test.js +56 -56
  81. package/tests/diff.test.js +42 -42
  82. package/tests/errors.test.js +425 -147
  83. package/tests/expression.test.js +58 -53
  84. package/tests/http.test.js +20 -20
  85. package/tests/reactive.test.js +185 -24
  86. package/tests/router.test.js +501 -74
  87. package/tests/ssr.test.js +444 -10
  88. package/tests/store.test.js +264 -23
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +36 -4
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +22 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
@@ -22,7 +22,7 @@ function morphAndGet(oldHTML, newHTML) {
22
22
  // Basic morphing
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
- describe('morph basic', () => {
25
+ describe('morph - basic', () => {
26
26
  it('updates text content', () => {
27
27
  const root = morphAndGet('<p>old</p>', '<p>new</p>');
28
28
  expect(root.innerHTML).toBe('<p>new</p>');
@@ -87,7 +87,7 @@ describe('morph — basic', () => {
87
87
  // Attribute morphing
88
88
  // ---------------------------------------------------------------------------
89
89
 
90
- describe('morph attributes', () => {
90
+ describe('morph - attributes', () => {
91
91
  it('adds new attributes', () => {
92
92
  const root = morphAndGet('<div></div>', '<div class="active"></div>');
93
93
  expect(root.children[0].className).toBe('active');
@@ -133,7 +133,7 @@ describe('morph — attributes', () => {
133
133
  // Keyed reconciliation
134
134
  // ---------------------------------------------------------------------------
135
135
 
136
- describe('morph keyed', () => {
136
+ describe('morph - keyed', () => {
137
137
  it('matches elements by z-key', () => {
138
138
  const root = el('<div z-key="a">A</div><div z-key="b">B</div>');
139
139
  morph(root, '<div z-key="b">B-updated</div><div z-key="a">A-updated</div>');
@@ -206,7 +206,7 @@ describe('morph — keyed', () => {
206
206
  // Input / form element handling
207
207
  // ---------------------------------------------------------------------------
208
208
 
209
- describe('morph form elements', () => {
209
+ describe('morph - form elements', () => {
210
210
  it('syncs input value', () => {
211
211
  const root = el('<input value="old">');
212
212
  morph(root, '<input value="new">');
@@ -253,7 +253,7 @@ describe('morph — form elements', () => {
253
253
  // Text and comment nodes
254
254
  // ---------------------------------------------------------------------------
255
255
 
256
- describe('morph text nodes', () => {
256
+ describe('morph - text nodes', () => {
257
257
  it('updates text nodes', () => {
258
258
  const root = document.createElement('div');
259
259
  root.textContent = 'old';
@@ -283,7 +283,7 @@ describe('morph — text nodes', () => {
283
283
  // Nested children
284
284
  // ---------------------------------------------------------------------------
285
285
 
286
- describe('morph nested', () => {
286
+ describe('morph - nested', () => {
287
287
  it('recursively morphs nested elements', () => {
288
288
  const root = morphAndGet(
289
289
  '<ul><li>a</li><li>b</li></ul>',
@@ -329,7 +329,7 @@ describe('morph — nested', () => {
329
329
  // Preserves unchanged nodes (identity)
330
330
  // ---------------------------------------------------------------------------
331
331
 
332
- describe('morph preservation', () => {
332
+ describe('morph - preservation', () => {
333
333
  it('does not replace elements that have not changed', () => {
334
334
  const root = el('<p>same</p><p>will-change</p>');
335
335
  const firstP = root.children[0];
@@ -351,7 +351,7 @@ describe('morph — preservation', () => {
351
351
  // z-skip directive
352
352
  // ---------------------------------------------------------------------------
353
353
 
354
- describe('morph z-skip', () => {
354
+ describe('morph - z-skip', () => {
355
355
  it('skips subtrees with z-skip attribute', () => {
356
356
  const root = el('<div z-skip><p>original</p></div>');
357
357
  const p = root.querySelector('p');
@@ -374,7 +374,7 @@ describe('morph — z-skip', () => {
374
374
  // isEqualNode fast bail-out
375
375
  // ---------------------------------------------------------------------------
376
376
 
377
- describe('morph fast bail-out', () => {
377
+ describe('morph - fast bail-out', () => {
378
378
  it('uses isEqualNode to skip identical elements', () => {
379
379
  const root = el('<div><p class="a" id="b">text</p></div>');
380
380
  const p = root.querySelector('p');
@@ -389,7 +389,7 @@ describe('morph — fast bail-out', () => {
389
389
  // Edge cases & stress
390
390
  // ---------------------------------------------------------------------------
391
391
 
392
- describe('morph edge cases', () => {
392
+ describe('morph - edge cases', () => {
393
393
  it('handles whitespace-only text nodes', () => {
394
394
  const root = morphAndGet('<div> </div>', '<div>text</div>');
395
395
  expect(root.children[0].textContent).toBe('text');
@@ -432,7 +432,7 @@ describe('morph — edge cases', () => {
432
432
  // Auto-keyed reconciliation (id / data-id / data-key)
433
433
  // ---------------------------------------------------------------------------
434
434
 
435
- describe('morph auto-keyed via id attribute', () => {
435
+ describe('morph - auto-keyed via id attribute', () => {
436
436
  it('reorders elements by id without z-key', () => {
437
437
  const root = el(
438
438
  '<div id="a">A</div><div id="b">B</div><div id="c">C</div>'
@@ -440,7 +440,7 @@ describe('morph — auto-keyed via id attribute', () => {
440
440
  const refA = root.children[0];
441
441
  const refB = root.children[1];
442
442
  morph(root, '<div id="c">C</div><div id="a">A</div><div id="b">B</div>');
443
- // Node identity preserved same DOM nodes, different order
443
+ // Node identity preserved - same DOM nodes, different order
444
444
  expect(root.children[1]).toBe(refA);
445
445
  expect(root.children[2]).toBe(refB);
446
446
  expect([...root.children].map(c => c.id)).toEqual(['c', 'a', 'b']);
@@ -486,7 +486,7 @@ describe('morph — auto-keyed via id attribute', () => {
486
486
 
487
487
 
488
488
  // ---------------------------------------------------------------------------
489
- // morphElement single-element morph
489
+ // morphElement - single-element morph
490
490
  // ---------------------------------------------------------------------------
491
491
 
492
492
  describe('morphElement', () => {
@@ -529,7 +529,7 @@ describe('morphElement', () => {
529
529
  // Deep keyed reconciliation edge cases
530
530
  // ---------------------------------------------------------------------------
531
531
 
532
- describe('morph keyed edge cases', () => {
532
+ describe('morph - keyed edge cases', () => {
533
533
  it('handles single keyed element swap', () => {
534
534
  const root = el('<div z-key="a">A</div>');
535
535
  const nodeA = root.children[0];
@@ -620,7 +620,7 @@ describe('morph — keyed edge cases', () => {
620
620
  // Boolean attribute morphing
621
621
  // ---------------------------------------------------------------------------
622
622
 
623
- describe('morph boolean attributes', () => {
623
+ describe('morph - boolean attributes', () => {
624
624
  it('adds hidden attribute', () => {
625
625
  const root = morphAndGet('<div></div>', '<div hidden></div>');
626
626
  expect(root.children[0].hasAttribute('hidden')).toBe(true);
@@ -654,7 +654,7 @@ describe('morph — boolean attributes', () => {
654
654
  // Complex form element morphing
655
655
  // ---------------------------------------------------------------------------
656
656
 
657
- describe('morph complex form elements', () => {
657
+ describe('morph - complex form elements', () => {
658
658
  it('syncs input type change', () => {
659
659
  const root = morphAndGet('<input type="text" value="hello">', '<input type="password" value="secret">');
660
660
  const input = root.querySelector('input');
@@ -706,7 +706,7 @@ describe('morph — complex form elements', () => {
706
706
  // Text / comment node edge cases
707
707
  // ---------------------------------------------------------------------------
708
708
 
709
- describe('morph text and comment edge cases', () => {
709
+ describe('morph - text and comment edge cases', () => {
710
710
  it('morphs comment to text node', () => {
711
711
  const root = document.createElement('div');
712
712
  root.appendChild(document.createComment('comment'));
@@ -750,7 +750,7 @@ describe('morph — text and comment edge cases', () => {
750
750
  // Nested structure edge cases
751
751
  // ---------------------------------------------------------------------------
752
752
 
753
- describe('morph nested structure edge cases', () => {
753
+ describe('morph - nested structure edge cases', () => {
754
754
  it('handles 5-level deep nesting change', () => {
755
755
  const root = morphAndGet(
756
756
  '<div><div><div><div><div>deep old</div></div></div></div></div>',
@@ -800,7 +800,7 @@ describe('morph — nested structure edge cases', () => {
800
800
  // z-skip with keyed children
801
801
  // ---------------------------------------------------------------------------
802
802
 
803
- describe('morph z-skip + keyed interactions', () => {
803
+ describe('morph - z-skip + keyed interactions', () => {
804
804
  it('z-skip on parent skips entire keyed children', () => {
805
805
  const root = el('<div z-skip><p z-key="a">A</p><p z-key="b">B</p></div>');
806
806
  const pA = root.querySelector('p[z-key="a"]');
@@ -827,7 +827,7 @@ describe('morph — z-skip + keyed interactions', () => {
827
827
  // morphElement advanced cases
828
828
  // ---------------------------------------------------------------------------
829
829
 
830
- describe('morphElement advanced', () => {
830
+ describe('morphElement - advanced', () => {
831
831
  it('morphs only attributes without child changes', () => {
832
832
  const root = el('<p class="old" data-x="1">text</p>');
833
833
  const target = root.children[0];
@@ -862,7 +862,7 @@ describe('morphElement — advanced', () => {
862
862
  // Stress tests
863
863
  // ---------------------------------------------------------------------------
864
864
 
865
- describe('morph stress tests', () => {
865
+ describe('morph - stress tests', () => {
866
866
  it('handles 500 unkeyed children update', () => {
867
867
  const oldItems = Array.from({ length: 500 }, (_, i) => `<span>${i}</span>`).join('');
868
868
  const newItems = Array.from({ length: 500 }, (_, i) => `<span>${i + 1}</span>`).join('');
@@ -914,7 +914,7 @@ describe('morph — stress tests', () => {
914
914
  // BUG FIX: keyed morph cursor after node replacement
915
915
  // ---------------------------------------------------------------------------
916
916
 
917
- describe('morph keyed cursor after replaceChild', () => {
917
+ describe('morph - keyed cursor after replaceChild', () => {
918
918
  it('correctly positions nodes when tag changes during keyed morph', () => {
919
919
  const root = el(
920
920
  '<div z-key="a">A</div>' +
@@ -942,7 +942,7 @@ describe('morph — keyed cursor after replaceChild', () => {
942
942
  // BUG FIX: attribute removal on live NamedNodeMap
943
943
  // ---------------------------------------------------------------------------
944
944
 
945
- describe('morph attribute removal stability', () => {
945
+ describe('morph - attribute removal stability', () => {
946
946
  it('removes multiple stale attributes without index errors', () => {
947
947
  const root = el('<div class="a" data-x="1" data-y="2" data-z="3" id="t1">hi</div>');
948
948
  morph(root, '<div id="t1">hi</div>');
@@ -967,10 +967,10 @@ describe('morph — attribute removal stability', () => {
967
967
 
968
968
 
969
969
  // ---------------------------------------------------------------------------
970
- // Round 3 Extended coverage
970
+ // Round 3 - Extended coverage
971
971
  // ---------------------------------------------------------------------------
972
972
 
973
- describe('morph __zqMorphHook performance hook', () => {
973
+ describe('morph - __zqMorphHook performance hook', () => {
974
974
  it('calls the hook with root element and elapsed time for morph()', () => {
975
975
  const calls = [];
976
976
  window.__zqMorphHook = (el, ms) => calls.push({ el, ms });
@@ -1018,7 +1018,7 @@ describe('morph — __zqMorphHook performance hook', () => {
1018
1018
  });
1019
1019
  });
1020
1020
 
1021
- describe('morph element to text node type change', () => {
1021
+ describe('morph - element to text node type change', () => {
1022
1022
  it('replaces element with text node at same position', () => {
1023
1023
  const root = el('<p>hello</p><div>world</div>');
1024
1024
  morph(root, 'just text');
@@ -1047,7 +1047,7 @@ describe('morph — element to text node type change', () => {
1047
1047
  });
1048
1048
  });
1049
1049
 
1050
- describe('morph attributes fast-path bypass', () => {
1050
+ describe('morph - attributes fast-path bypass', () => {
1051
1051
  it('skips attribute update when count and values are identical but children differ', () => {
1052
1052
  const root = el('<div class="a" data-x="1"><p>old child</p></div>');
1053
1053
  const child = root.firstElementChild;
@@ -1071,7 +1071,7 @@ describe('morph — attributes fast-path bypass', () => {
1071
1071
  });
1072
1072
  });
1073
1073
 
1074
- describe('morph textarea edge cases', () => {
1074
+ describe('morph - textarea edge cases', () => {
1075
1075
  it('syncs textarea with null-like textContent', () => {
1076
1076
  const root = el('<textarea>old value</textarea>');
1077
1077
  morph(root, '<textarea></textarea>');
@@ -1085,7 +1085,7 @@ describe('morph — textarea edge cases', () => {
1085
1085
  });
1086
1086
  });
1087
1087
 
1088
- describe('morph select value sync', () => {
1088
+ describe('morph - select value sync', () => {
1089
1089
  it('syncs select value after morphing options', () => {
1090
1090
  const root = el('<select><option value="a">A</option><option value="b">B</option></select>');
1091
1091
  root.querySelector('select').value = 'a';
@@ -1101,7 +1101,7 @@ describe('morph — select value sync', () => {
1101
1101
  });
1102
1102
  });
1103
1103
 
1104
- describe('morph input sync edge cases', () => {
1104
+ describe('morph - input sync edge cases', () => {
1105
1105
  it('syncs input value when new element has no value attribute', () => {
1106
1106
  const root = el('<input type="text" value="old">');
1107
1107
  morph(root, '<input type="text">');
@@ -1133,7 +1133,7 @@ describe('morph — input sync edge cases', () => {
1133
1133
  });
1134
1134
  });
1135
1135
 
1136
- describe('morph keyed with multiple unkeyed leftover', () => {
1136
+ describe('morph - keyed with multiple unkeyed leftover', () => {
1137
1137
  it('consumes some unkeyed and removes remaining', () => {
1138
1138
  const root = el(
1139
1139
  '<div z-key="a">A</div><p>unkeyed1</p><p>unkeyed2</p><p>unkeyed3</p><div z-key="b">B</div>'
@@ -1160,7 +1160,7 @@ describe('morph — keyed with multiple unkeyed leftover', () => {
1160
1160
  });
1161
1161
  });
1162
1162
 
1163
- describe('morph LIS edge cases', () => {
1163
+ describe('morph - LIS edge cases', () => {
1164
1164
  it('handles all entries being unmatched (-1)', () => {
1165
1165
  // All new keyed elements with no matching old keys
1166
1166
  const root = el('<div z-key="x">X</div><div z-key="y">Y</div>');
@@ -1184,7 +1184,7 @@ describe('morph — LIS edge cases', () => {
1184
1184
  morph(root,
1185
1185
  '<div z-key="a">A2</div><div z-key="b">B2</div><div z-key="c">C2</div>'
1186
1186
  );
1187
- // All in order identity preserved, no moves
1187
+ // All in order - identity preserved, no moves
1188
1188
  expect(root.children[0]).toBe(refs[0]);
1189
1189
  expect(root.children[1]).toBe(refs[1]);
1190
1190
  expect(root.children[2]).toBe(refs[2]);
@@ -1204,7 +1204,7 @@ describe('morph — LIS edge cases', () => {
1204
1204
  });
1205
1205
  });
1206
1206
 
1207
- describe('morph mixed content type transitions', () => {
1207
+ describe('morph - mixed content type transitions', () => {
1208
1208
  it('morphs from elements to mixed text and elements', () => {
1209
1209
  const root = el('<p>one</p><p>two</p>');
1210
1210
  morph(root, 'text<p>element</p>more text');
@@ -1230,7 +1230,7 @@ describe('morph — mixed content type transitions', () => {
1230
1230
  });
1231
1231
  });
1232
1232
 
1233
- describe('morph deeply nested keyed reorder', () => {
1233
+ describe('morph - deeply nested keyed reorder', () => {
1234
1234
  it('reorders keyed elements inside a nested structure', () => {
1235
1235
  const root = el('<ul><li z-key="c">C</li><li z-key="a">A</li><li z-key="b">B</li></ul>');
1236
1236
  const ul = root.querySelector('ul');
@@ -1244,7 +1244,7 @@ describe('morph — deeply nested keyed reorder', () => {
1244
1244
  });
1245
1245
  });
1246
1246
 
1247
- describe('morph large scale stress tests', () => {
1247
+ describe('morph - large scale stress tests', () => {
1248
1248
  it('handles 1000 unkeyed elements', () => {
1249
1249
  const genItems = n => Array.from({ length: n }, (_, i) => `<li>${i}</li>`).join('');
1250
1250
  const root = el(genItems(1000));
@@ -1273,7 +1273,7 @@ describe('morph — large scale stress tests', () => {
1273
1273
  });
1274
1274
  });
1275
1275
 
1276
- describe('morph auto-key edge cases', () => {
1276
+ describe('morph - auto-key edge cases', () => {
1277
1277
  it('handles mixed auto-key types (id, data-id, data-key)', () => {
1278
1278
  const root = el(
1279
1279
  '<div id="x">X</div><div data-id="y">Y</div><div data-key="z">Z</div>'
@@ -1295,7 +1295,7 @@ describe('morph — auto-key edge cases', () => {
1295
1295
  });
1296
1296
  });
1297
1297
 
1298
- describe('morph whitespace and boundary edge cases', () => {
1298
+ describe('morph - whitespace and boundary edge cases', () => {
1299
1299
  it('handles only whitespace in new HTML', () => {
1300
1300
  const root = el('<p>content</p>');
1301
1301
  morph(root, ' \n\t ');
@@ -1316,7 +1316,7 @@ describe('morph — whitespace and boundary edge cases', () => {
1316
1316
  });
1317
1317
  });
1318
1318
 
1319
- describe('morph z-skip edge cases', () => {
1319
+ describe('morph - z-skip edge cases', () => {
1320
1320
  it('preserves z-skip subtree even when parent changes', () => {
1321
1321
  const root = el('<div class="outer"><div z-skip>KEEP ME</div><p>old</p></div>');
1322
1322
  const skipDiv = root.querySelector('[z-skip]');
@@ -1337,7 +1337,7 @@ describe('morph — z-skip edge cases', () => {
1337
1337
  });
1338
1338
  });
1339
1339
 
1340
- describe('morph morphElement edge cases', () => {
1340
+ describe('morph - morphElement edge cases', () => {
1341
1341
  it('handles morphElement with complex nested content', () => {
1342
1342
  const node = document.createElement('div');
1343
1343
  node.innerHTML = '<ul><li>a</li><li>b</li></ul>';
@@ -1375,7 +1375,7 @@ describe('morph — morphElement edge cases', () => {
1375
1375
  });
1376
1376
  });
1377
1377
 
1378
- describe('morph keyed nodes with tag changes during reorder', () => {
1378
+ describe('morph - keyed nodes with tag changes during reorder', () => {
1379
1379
  it('handles keyed nodes where matched node has different tag (replaceChild path)', () => {
1380
1380
  // This tests the cursor stability fix: capture nextSibling before _morphNode
1381
1381
  const root = el(
@@ -1391,7 +1391,7 @@ describe('morph — keyed nodes with tag changes during reorder', () => {
1391
1391
  });
1392
1392
  });
1393
1393
 
1394
- describe('morph identity preservation across morphs', () => {
1394
+ describe('morph - identity preservation across morphs', () => {
1395
1395
  it('preserves element identity through multiple consecutive morphs', () => {
1396
1396
  const root = el('<div id="target"><p>v1</p></div>');
1397
1397
  const target = root.firstElementChild;