@pyreon/runtime-dom 0.11.4 → 0.11.6

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.
@@ -3,7 +3,7 @@
3
3
  * Covers gaps in: devtools.ts, template.ts, mount.ts, transition.ts,
4
4
  * hydrate.ts, transition-group.ts, nodes.ts, props.ts
5
5
  */
6
- import type { ComponentFn, VNodeChild } from "@pyreon/core"
6
+ import type { ComponentFn, VNodeChild } from '@pyreon/core'
7
7
  import {
8
8
  createRef,
9
9
  defineComponent,
@@ -14,9 +14,9 @@ import {
14
14
  onUnmount,
15
15
  onUpdate,
16
16
  Portal,
17
- } from "@pyreon/core"
18
- import { signal } from "@pyreon/reactivity"
19
- import { installDevTools, registerComponent, unregisterComponent } from "../devtools"
17
+ } from '@pyreon/core'
18
+ import { signal } from '@pyreon/reactivity'
19
+ import { installDevTools, registerComponent, unregisterComponent } from '../devtools'
20
20
  import {
21
21
  Transition as _Transition,
22
22
  TransitionGroup as _TransitionGroup,
@@ -25,53 +25,53 @@ import {
25
25
  mount,
26
26
  sanitizeHtml,
27
27
  setSanitizer,
28
- } from "../index"
29
- import { mountChild } from "../mount"
28
+ } from '../index'
29
+ import { mountChild } from '../mount'
30
30
 
31
31
  const Transition = _Transition as unknown as ComponentFn<Record<string, unknown>>
32
32
  const TransitionGroup = _TransitionGroup as unknown as ComponentFn<Record<string, unknown>>
33
33
 
34
34
  function container(): HTMLElement {
35
- const el = document.createElement("div")
35
+ const el = document.createElement('div')
36
36
  document.body.appendChild(el)
37
37
  return el
38
38
  }
39
39
 
40
40
  // ─── template.ts — _tpl() compiler API (lines 72-80) ─────────────────────────
41
41
 
42
- describe("_tpl — compiler-facing template API", () => {
43
- test("creates a NativeItem from HTML string and bind function", () => {
42
+ describe('_tpl — compiler-facing template API', () => {
43
+ test('creates a NativeItem from HTML string and bind function', () => {
44
44
  const el = container()
45
45
  const native = _tpl('<div class="box"><span></span></div>', (root) => {
46
- const span = root.querySelector("span")!
47
- span.textContent = "hello"
46
+ const span = root.querySelector('span')!
47
+ span.textContent = 'hello'
48
48
  return null
49
49
  })
50
50
  expect(native.__isNative).toBe(true)
51
51
  expect(native.el).toBeInstanceOf(HTMLElement)
52
52
  el.appendChild(native.el)
53
- expect(el.querySelector(".box span")?.textContent).toBe("hello")
53
+ expect(el.querySelector('.box span')?.textContent).toBe('hello')
54
54
  })
55
55
 
56
- test("caches template elements — same HTML string reuses template", () => {
56
+ test('caches template elements — same HTML string reuses template', () => {
57
57
  const html = '<p class="cached"><em></em></p>'
58
58
  const n1 = _tpl(html, (root) => {
59
- root.querySelector("em")!.textContent = "first"
59
+ root.querySelector('em')!.textContent = 'first'
60
60
  return null
61
61
  })
62
62
  const n2 = _tpl(html, (root) => {
63
- root.querySelector("em")!.textContent = "second"
63
+ root.querySelector('em')!.textContent = 'second'
64
64
  return null
65
65
  })
66
66
  // Both produce valid elements but they are separate clones
67
67
  expect(n1.el).not.toBe(n2.el)
68
- expect((n1.el as HTMLElement).querySelector("em")?.textContent).toBe("first")
69
- expect((n2.el as HTMLElement).querySelector("em")?.textContent).toBe("second")
68
+ expect((n1.el as HTMLElement).querySelector('em')?.textContent).toBe('first')
69
+ expect((n2.el as HTMLElement).querySelector('em')?.textContent).toBe('second')
70
70
  })
71
71
 
72
- test("bind function can return a cleanup", () => {
72
+ test('bind function can return a cleanup', () => {
73
73
  let cleaned = false
74
- const native = _tpl("<div></div>", () => {
74
+ const native = _tpl('<div></div>', () => {
75
75
  return () => {
76
76
  cleaned = true
77
77
  }
@@ -81,22 +81,22 @@ describe("_tpl — compiler-facing template API", () => {
81
81
  expect(cleaned).toBe(true)
82
82
  })
83
83
 
84
- test("mountChild handles NativeItem from _tpl", () => {
84
+ test('mountChild handles NativeItem from _tpl', () => {
85
85
  const el = container()
86
- const native = _tpl("<span>tpl</span>", () => null)
86
+ const native = _tpl('<span>tpl</span>', () => null)
87
87
  const cleanup = mountChild(native as unknown as VNodeChild, el, null)
88
- expect(el.querySelector("span")?.textContent).toBe("tpl")
88
+ expect(el.querySelector('span')?.textContent).toBe('tpl')
89
89
  cleanup()
90
90
  })
91
91
 
92
- test("mountChild handles NativeItem with cleanup from _tpl", () => {
92
+ test('mountChild handles NativeItem with cleanup from _tpl', () => {
93
93
  const el = container()
94
94
  let cleaned = false
95
- const native = _tpl("<span>tpl2</span>", () => () => {
95
+ const native = _tpl('<span>tpl2</span>', () => () => {
96
96
  cleaned = true
97
97
  })
98
98
  const cleanup = mountChild(native as unknown as VNodeChild, el, null)
99
- expect(el.querySelector("span")?.textContent).toBe("tpl2")
99
+ expect(el.querySelector('span')?.textContent).toBe('tpl2')
100
100
  cleanup()
101
101
  expect(cleaned).toBe(true)
102
102
  })
@@ -104,44 +104,44 @@ describe("_tpl — compiler-facing template API", () => {
104
104
 
105
105
  // ─── devtools.ts — overlay and $p helpers (lines 155-258, 267-290) ────────────
106
106
 
107
- describe("DevTools — overlay and $p console helpers", () => {
107
+ describe('DevTools — overlay and $p console helpers', () => {
108
108
  beforeAll(() => {
109
109
  installDevTools()
110
110
  })
111
111
 
112
- test("enableOverlay and disableOverlay toggle overlay state", () => {
112
+ test('enableOverlay and disableOverlay toggle overlay state', () => {
113
113
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
114
114
  enableOverlay: () => void
115
115
  disableOverlay: () => void
116
116
  }
117
117
  // Enable overlay
118
118
  devtools.enableOverlay()
119
- expect(document.body.style.cursor).toBe("crosshair")
119
+ expect(document.body.style.cursor).toBe('crosshair')
120
120
 
121
121
  // Enable again — should be noop (already active)
122
122
  devtools.enableOverlay()
123
- expect(document.body.style.cursor).toBe("crosshair")
123
+ expect(document.body.style.cursor).toBe('crosshair')
124
124
 
125
125
  // Disable overlay
126
126
  devtools.disableOverlay()
127
- expect(document.body.style.cursor).toBe("")
127
+ expect(document.body.style.cursor).toBe('')
128
128
 
129
129
  // Disable again — should be noop (already disabled)
130
130
  devtools.disableOverlay()
131
- expect(document.body.style.cursor).toBe("")
131
+ expect(document.body.style.cursor).toBe('')
132
132
  })
133
133
 
134
- test("overlay creates overlay and tooltip elements", () => {
134
+ test('overlay creates overlay and tooltip elements', () => {
135
135
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
136
136
  enableOverlay: () => void
137
137
  disableOverlay: () => void
138
138
  }
139
139
  devtools.enableOverlay()
140
- expect(document.getElementById("__pyreon-overlay")).not.toBeNull()
140
+ expect(document.getElementById('__pyreon-overlay')).not.toBeNull()
141
141
  devtools.disableOverlay()
142
142
  })
143
143
 
144
- test("overlay mousemove with no registered component hides overlay", () => {
144
+ test('overlay mousemove with no registered component hides overlay', () => {
145
145
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
146
146
  enableOverlay: () => void
147
147
  disableOverlay: () => void
@@ -149,71 +149,71 @@ describe("DevTools — overlay and $p console helpers", () => {
149
149
  devtools.enableOverlay()
150
150
 
151
151
  // Simulate mousemove over an unregistered element
152
- const target = document.createElement("div")
152
+ const target = document.createElement('div')
153
153
  document.body.appendChild(target)
154
- const event = new MouseEvent("mousemove", { clientX: 10, clientY: 10, bubbles: true })
154
+ const event = new MouseEvent('mousemove', { clientX: 10, clientY: 10, bubbles: true })
155
155
  document.dispatchEvent(event)
156
156
 
157
157
  devtools.disableOverlay()
158
158
  target.remove()
159
159
  })
160
160
 
161
- test("overlay mousemove highlights registered component", () => {
161
+ test('overlay mousemove highlights registered component', () => {
162
162
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
163
163
  enableOverlay: () => void
164
164
  disableOverlay: () => void
165
165
  }
166
166
 
167
- const target = document.createElement("div")
168
- target.style.cssText = "width:100px;height:100px;position:fixed;top:0;left:0;"
167
+ const target = document.createElement('div')
168
+ target.style.cssText = 'width:100px;height:100px;position:fixed;top:0;left:0;'
169
169
  document.body.appendChild(target)
170
- registerComponent("overlay-test", "OverlayComp", target, null)
170
+ registerComponent('overlay-test', 'OverlayComp', target, null)
171
171
 
172
172
  devtools.enableOverlay()
173
173
 
174
174
  // Simulate mousemove over the registered element
175
- const event = new MouseEvent("mousemove", { clientX: 50, clientY: 50, bubbles: true })
175
+ const event = new MouseEvent('mousemove', { clientX: 50, clientY: 50, bubbles: true })
176
176
  document.dispatchEvent(event)
177
177
 
178
178
  // Simulate same element again — should be noop (same _currentHighlight)
179
- const event2 = new MouseEvent("mousemove", { clientX: 50, clientY: 50, bubbles: true })
179
+ const event2 = new MouseEvent('mousemove', { clientX: 50, clientY: 50, bubbles: true })
180
180
  document.dispatchEvent(event2)
181
181
 
182
182
  devtools.disableOverlay()
183
- unregisterComponent("overlay-test")
183
+ unregisterComponent('overlay-test')
184
184
  target.remove()
185
185
  })
186
186
 
187
- test("overlay click logs component and disables overlay", () => {
187
+ test('overlay click logs component and disables overlay', () => {
188
188
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
189
189
  enableOverlay: () => void
190
190
  disableOverlay: () => void
191
191
  }
192
192
 
193
- const target = document.createElement("div")
194
- target.style.cssText = "width:100px;height:100px;position:fixed;top:0;left:0;"
193
+ const target = document.createElement('div')
194
+ target.style.cssText = 'width:100px;height:100px;position:fixed;top:0;left:0;'
195
195
  document.body.appendChild(target)
196
196
 
197
197
  // Register parent and child for parent logging branch
198
- registerComponent("click-parent", "ClickParent", null, null)
199
- registerComponent("click-test", "ClickComp", target, "click-parent")
198
+ registerComponent('click-parent', 'ClickParent', null, null)
199
+ registerComponent('click-test', 'ClickComp', target, 'click-parent')
200
200
 
201
201
  devtools.enableOverlay()
202
202
 
203
- const event = new MouseEvent("click", { clientX: 50, clientY: 50, bubbles: true })
203
+ const event = new MouseEvent('click', { clientX: 50, clientY: 50, bubbles: true })
204
204
  document.dispatchEvent(event)
205
205
 
206
206
  // In happy-dom elementFromPoint returns null, so the click handler
207
207
  // returns early without calling disableOverlay. Manually disable.
208
208
  devtools.disableOverlay()
209
- expect(document.body.style.cursor).toBe("")
209
+ expect(document.body.style.cursor).toBe('')
210
210
 
211
- unregisterComponent("click-test")
212
- unregisterComponent("click-parent")
211
+ unregisterComponent('click-test')
212
+ unregisterComponent('click-parent')
213
213
  target.remove()
214
214
  })
215
215
 
216
- test("overlay click on unregistered element — no console log", () => {
216
+ test('overlay click on unregistered element — no console log', () => {
217
217
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
218
218
  enableOverlay: () => void
219
219
  disableOverlay: () => void
@@ -222,76 +222,76 @@ describe("DevTools — overlay and $p console helpers", () => {
222
222
  devtools.enableOverlay()
223
223
 
224
224
  // Click on area with no component
225
- const event = new MouseEvent("click", { clientX: 0, clientY: 0, bubbles: true })
225
+ const event = new MouseEvent('click', { clientX: 0, clientY: 0, bubbles: true })
226
226
  document.dispatchEvent(event)
227
227
 
228
228
  // In happy-dom elementFromPoint returns null, so click handler returns
229
229
  // early. Manually disable overlay and verify cursor is restored.
230
230
  devtools.disableOverlay()
231
- expect(document.body.style.cursor).toBe("")
231
+ expect(document.body.style.cursor).toBe('')
232
232
  })
233
233
 
234
- test("Escape key disables overlay", () => {
234
+ test('Escape key disables overlay', () => {
235
235
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
236
236
  enableOverlay: () => void
237
237
  disableOverlay: () => void
238
238
  }
239
239
 
240
240
  devtools.enableOverlay()
241
- expect(document.body.style.cursor).toBe("crosshair")
241
+ expect(document.body.style.cursor).toBe('crosshair')
242
242
 
243
- const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true })
243
+ const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
244
244
  document.dispatchEvent(event)
245
- expect(document.body.style.cursor).toBe("")
245
+ expect(document.body.style.cursor).toBe('')
246
246
  })
247
247
 
248
- test("Ctrl+Shift+P toggles overlay", () => {
248
+ test('Ctrl+Shift+P toggles overlay', () => {
249
249
  // Enable via Ctrl+Shift+P
250
- const enableEvent = new KeyboardEvent("keydown", {
251
- key: "P",
250
+ const enableEvent = new KeyboardEvent('keydown', {
251
+ key: 'P',
252
252
  ctrlKey: true,
253
253
  shiftKey: true,
254
254
  bubbles: true,
255
255
  })
256
256
  window.dispatchEvent(enableEvent)
257
- expect(document.body.style.cursor).toBe("crosshair")
257
+ expect(document.body.style.cursor).toBe('crosshair')
258
258
 
259
259
  // Disable via Ctrl+Shift+P
260
- const disableEvent = new KeyboardEvent("keydown", {
261
- key: "P",
260
+ const disableEvent = new KeyboardEvent('keydown', {
261
+ key: 'P',
262
262
  ctrlKey: true,
263
263
  shiftKey: true,
264
264
  bubbles: true,
265
265
  })
266
266
  window.dispatchEvent(disableEvent)
267
- expect(document.body.style.cursor).toBe("")
267
+ expect(document.body.style.cursor).toBe('')
268
268
  })
269
269
 
270
- test("overlay with component that has children shows child count", () => {
270
+ test('overlay with component that has children shows child count', () => {
271
271
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
272
272
  enableOverlay: () => void
273
273
  disableOverlay: () => void
274
274
  }
275
275
 
276
- const target = document.createElement("div")
277
- target.style.cssText = "width:100px;height:100px;position:fixed;top:50px;left:50px;"
276
+ const target = document.createElement('div')
277
+ target.style.cssText = 'width:100px;height:100px;position:fixed;top:50px;left:50px;'
278
278
  document.body.appendChild(target)
279
279
 
280
- registerComponent("parent-ov", "ParentOv", target, null)
281
- registerComponent("child-ov-1", "ChildOv1", null, "parent-ov")
280
+ registerComponent('parent-ov', 'ParentOv', target, null)
281
+ registerComponent('child-ov-1', 'ChildOv1', null, 'parent-ov')
282
282
 
283
283
  devtools.enableOverlay()
284
284
 
285
- const event = new MouseEvent("mousemove", { clientX: 75, clientY: 75, bubbles: true })
285
+ const event = new MouseEvent('mousemove', { clientX: 75, clientY: 75, bubbles: true })
286
286
  document.dispatchEvent(event)
287
287
 
288
288
  devtools.disableOverlay()
289
- unregisterComponent("child-ov-1")
290
- unregisterComponent("parent-ov")
289
+ unregisterComponent('child-ov-1')
290
+ unregisterComponent('parent-ov')
291
291
  target.remove()
292
292
  })
293
293
 
294
- test("$p console helpers exist and work", () => {
294
+ test('$p console helpers exist and work', () => {
295
295
  const $p = (window as unknown as Record<string, unknown>).$p as {
296
296
  components: () => unknown[]
297
297
  tree: () => unknown[]
@@ -304,7 +304,7 @@ describe("DevTools — overlay and $p console helpers", () => {
304
304
  expect($p).toBeDefined()
305
305
 
306
306
  // $p.components()
307
- registerComponent("$p-test", "$pTest", null, null)
307
+ registerComponent('$p-test', '$pTest', null, null)
308
308
  const comps = $p.components()
309
309
  expect(comps.length).toBeGreaterThan(0)
310
310
 
@@ -313,24 +313,24 @@ describe("DevTools — overlay and $p console helpers", () => {
313
313
  expect(Array.isArray(tree)).toBe(true)
314
314
 
315
315
  // $p.highlight()
316
- $p.highlight("$p-test")
316
+ $p.highlight('$p-test')
317
317
 
318
318
  // $p.inspect() — toggles overlay on
319
319
  $p.inspect()
320
- expect(document.body.style.cursor).toBe("crosshair")
320
+ expect(document.body.style.cursor).toBe('crosshair')
321
321
  // $p.inspect() — toggles overlay off
322
322
  $p.inspect()
323
- expect(document.body.style.cursor).toBe("")
323
+ expect(document.body.style.cursor).toBe('')
324
324
 
325
325
  // $p.stats()
326
326
  const stats = $p.stats()
327
327
  expect(stats.total).toBeGreaterThan(0)
328
- expect(typeof stats.roots).toBe("number")
328
+ expect(typeof stats.roots).toBe('number')
329
329
 
330
330
  // $p.help()
331
331
  $p.help()
332
332
 
333
- unregisterComponent("$p-test")
333
+ unregisterComponent('$p-test')
334
334
  })
335
335
 
336
336
  test("$p.stats shows singular 'root' for 1 root", () => {
@@ -338,70 +338,70 @@ describe("DevTools — overlay and $p console helpers", () => {
338
338
  const $p = (window as unknown as Record<string, unknown>).$p as {
339
339
  stats: () => { total: number; roots: number }
340
340
  }
341
- registerComponent("sole-root", "SoleRoot", null, null)
341
+ registerComponent('sole-root', 'SoleRoot', null, null)
342
342
  const stats = $p.stats()
343
343
  expect(stats.roots).toBeGreaterThanOrEqual(1)
344
- unregisterComponent("sole-root")
344
+ unregisterComponent('sole-root')
345
345
  })
346
346
  })
347
347
 
348
348
  // ─── mount.ts — uncovered branches ───────────────────────────────────────────
349
349
 
350
- describe("mount.ts — uncovered branches", () => {
351
- test("component returning invalid value triggers dev warning (line 283)", () => {
350
+ describe('mount.ts — uncovered branches', () => {
351
+ test('component returning invalid value triggers dev warning (line 283)', () => {
352
352
  const el = container()
353
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
353
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
354
354
 
355
355
  // Component returns an object without 'type' property — triggers invalid return warning
356
356
  const BadComp = (() => ({ weird: true })) as unknown as ComponentFn
357
357
  mount(h(BadComp, null), el)
358
358
 
359
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("returned an invalid value"))
359
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('returned an invalid value'))
360
360
  warnSpy.mockRestore()
361
361
  })
362
362
 
363
- test("component returning Promise triggers dev warning", () => {
363
+ test('component returning Promise triggers dev warning', () => {
364
364
  const el = container()
365
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
365
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
366
366
 
367
367
  const AsyncComp = (() => Promise.resolve(null)) as unknown as ComponentFn
368
368
  mount(h(AsyncComp, null), el)
369
369
 
370
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("returned a Promise"))
370
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('returned a Promise'))
371
371
  warnSpy.mockRestore()
372
372
  })
373
373
 
374
- test("void element with children triggers dev warning", () => {
374
+ test('void element with children triggers dev warning', () => {
375
375
  const el = container()
376
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
376
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
377
377
 
378
- mount(h("img", null, "child text"), el)
378
+ mount(h('img', null, 'child text'), el)
379
379
 
380
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("void element"))
380
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('void element'))
381
381
  warnSpy.mockRestore()
382
382
  })
383
383
 
384
- test("Portal with falsy target warns", () => {
384
+ test('Portal with falsy target warns', () => {
385
385
  const el = container()
386
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
386
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
387
387
 
388
388
  mount(h(Portal, { target: null }), el)
389
389
 
390
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Portal"))
390
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Portal'))
391
391
  warnSpy.mockRestore()
392
392
  })
393
393
 
394
- test("component subtree mount error with propagateError (lines 298-309)", () => {
394
+ test('component subtree mount error with propagateError (lines 298-309)', () => {
395
395
  const el = container()
396
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
396
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
397
397
 
398
398
  // Component whose subtree throws during mount
399
399
  const Outer = defineComponent(() => {
400
400
  // Inner component throws during mount (not setup)
401
401
  const Inner = defineComponent(() => {
402
402
  // Return a VNode that will throw when mounted
403
- return h("div", null, (() => {
404
- throw new Error("subtree mount error")
403
+ return h('div', null, (() => {
404
+ throw new Error('subtree mount error')
405
405
  }) as unknown as VNodeChild)
406
406
  })
407
407
  return h(Inner, null)
@@ -411,16 +411,16 @@ describe("mount.ts — uncovered branches", () => {
411
411
  errorSpy.mockRestore()
412
412
  })
413
413
 
414
- test("mountChildren with >2 children and cleanups (line 387)", () => {
414
+ test('mountChildren with >2 children and cleanups (line 387)', () => {
415
415
  const el = container()
416
- const s1 = signal("a")
417
- const s2 = signal("b")
418
- const s3 = signal("c")
416
+ const s1 = signal('a')
417
+ const s2 = signal('b')
418
+ const s3 = signal('c')
419
419
 
420
420
  // 3 reactive children will all have cleanups, hitting the map+cleanup path
421
421
  const unmount = mount(
422
422
  h(
423
- "div",
423
+ 'div',
424
424
  null,
425
425
  () => s1(),
426
426
  () => s2(),
@@ -429,87 +429,87 @@ describe("mount.ts — uncovered branches", () => {
429
429
  el,
430
430
  )
431
431
 
432
- expect(el.querySelector("div")?.textContent).toContain("a")
433
- expect(el.querySelector("div")?.textContent).toContain("b")
434
- expect(el.querySelector("div")?.textContent).toContain("c")
432
+ expect(el.querySelector('div')?.textContent).toContain('a')
433
+ expect(el.querySelector('div')?.textContent).toContain('b')
434
+ expect(el.querySelector('div')?.textContent).toContain('c')
435
435
 
436
436
  unmount()
437
437
  })
438
438
 
439
- test("mountElement with ref + propCleanup at _elementDepth > 0", () => {
439
+ test('mountElement with ref + propCleanup at _elementDepth > 0', () => {
440
440
  const el = container()
441
441
  const ref = createRef<HTMLElement>()
442
- const cls = signal("foo")
442
+ const cls = signal('foo')
443
443
 
444
444
  // Nested element with ref AND reactive prop — exercises the combined cleanup path
445
- mount(h("div", null, h("span", { ref, class: () => cls() }, "inner")), el)
445
+ mount(h('div', null, h('span', { ref, class: () => cls() }, 'inner')), el)
446
446
 
447
447
  expect(ref.current).not.toBeNull()
448
- expect(ref.current?.className).toBe("foo")
448
+ expect(ref.current?.className).toBe('foo')
449
449
  })
450
450
 
451
- test("mountElement with propCleanup only at _elementDepth > 0", () => {
451
+ test('mountElement with propCleanup only at _elementDepth > 0', () => {
452
452
  const el = container()
453
- const cls = signal("bar")
453
+ const cls = signal('bar')
454
454
 
455
455
  // Nested element with reactive prop but no ref
456
- const unmount = mount(h("div", null, h("span", { class: () => cls() }, "inner")), el)
456
+ const unmount = mount(h('div', null, h('span', { class: () => cls() }, 'inner')), el)
457
457
 
458
- expect(el.querySelector("span")?.className).toBe("bar")
459
- cls.set("baz")
460
- expect(el.querySelector("span")?.className).toBe("baz")
458
+ expect(el.querySelector('span')?.className).toBe('bar')
459
+ cls.set('baz')
460
+ expect(el.querySelector('span')?.className).toBe('baz')
461
461
  unmount()
462
462
  })
463
463
 
464
- test("reactive text at _elementDepth > 0 returns just dispose", () => {
464
+ test('reactive text at _elementDepth > 0 returns just dispose', () => {
465
465
  const el = container()
466
- const text = signal("nested")
466
+ const text = signal('nested')
467
467
 
468
468
  // Reactive text inside a parent element — should skip DOM removal closure
469
469
  const unmount = mount(
470
- h("div", null, () => text()),
470
+ h('div', null, () => text()),
471
471
  el,
472
472
  )
473
473
 
474
- expect(el.querySelector("div")?.textContent).toBe("nested")
475
- text.set("updated")
476
- expect(el.querySelector("div")?.textContent).toBe("updated")
474
+ expect(el.querySelector('div')?.textContent).toBe('nested')
475
+ text.set('updated')
476
+ expect(el.querySelector('div')?.textContent).toBe('updated')
477
477
  unmount()
478
478
  })
479
479
 
480
- test("NativeItem without cleanup at _elementDepth > 0", () => {
480
+ test('NativeItem without cleanup at _elementDepth > 0', () => {
481
481
  const el = container()
482
- const native = _tpl("<b>native</b>", () => null)
482
+ const native = _tpl('<b>native</b>', () => null)
483
483
 
484
484
  // Mount NativeItem inside a parent element
485
- mount(h("div", null, native as unknown as VNodeChild), el)
486
- expect(el.querySelector("b")?.textContent).toBe("native")
485
+ mount(h('div', null, native as unknown as VNodeChild), el)
486
+ expect(el.querySelector('b')?.textContent).toBe('native')
487
487
  })
488
488
 
489
- test("NativeItem with cleanup at _elementDepth > 0", () => {
489
+ test('NativeItem with cleanup at _elementDepth > 0', () => {
490
490
  const el = container()
491
491
  let _cleaned = false
492
- const native = _tpl("<b>native2</b>", () => () => {
492
+ const native = _tpl('<b>native2</b>', () => () => {
493
493
  _cleaned = true
494
494
  })
495
495
 
496
- mount(h("div", null, native as unknown as VNodeChild), el)
497
- expect(el.querySelector("b")?.textContent).toBe("native2")
496
+ mount(h('div', null, native as unknown as VNodeChild), el)
497
+ expect(el.querySelector('b')?.textContent).toBe('native2')
498
498
  })
499
499
  })
500
500
 
501
501
  // ─── transition.ts — uncovered branches (lines 152, 165, 170-175) ────────────
502
502
 
503
- describe("Transition — uncovered branches", () => {
504
- test("onUnmount cancels pending leave (line 152)", async () => {
503
+ describe('Transition — uncovered branches', () => {
504
+ test('onUnmount cancels pending leave (line 152)', async () => {
505
505
  const el = container()
506
506
  const visible = signal(true)
507
507
 
508
508
  const unmount = mount(
509
509
  h(Transition, {
510
510
  show: visible,
511
- name: "fade",
512
- children: h("div", { id: "unmount-leave" }, "content"),
511
+ name: 'fade',
512
+ children: h('div', { id: 'unmount-leave' }, 'content'),
513
513
  }),
514
514
  el,
515
515
  )
@@ -521,7 +521,7 @@ describe("Transition — uncovered branches", () => {
521
521
  unmount()
522
522
  })
523
523
 
524
- test("non-object/array child returns rawChild (line 165)", () => {
524
+ test('non-object/array child returns rawChild (line 165)', () => {
525
525
  const el = container()
526
526
  const visible = signal(true)
527
527
 
@@ -529,13 +529,13 @@ describe("Transition — uncovered branches", () => {
529
529
  mount(
530
530
  h(Transition, {
531
531
  show: visible,
532
- children: "just text" as unknown as VNodeChild,
532
+ children: 'just text' as unknown as VNodeChild,
533
533
  }),
534
534
  el,
535
535
  )
536
536
  })
537
537
 
538
- test("array child returns rawChild (line 164)", () => {
538
+ test('array child returns rawChild (line 164)', () => {
539
539
  const el = container()
540
540
  const visible = signal(true)
541
541
 
@@ -543,18 +543,18 @@ describe("Transition — uncovered branches", () => {
543
543
  mount(
544
544
  h(Transition, {
545
545
  show: visible,
546
- children: [h("span", null, "a"), h("span", null, "b")] as unknown as VNodeChild,
546
+ children: [h('span', null, 'a'), h('span', null, 'b')] as unknown as VNodeChild,
547
547
  }),
548
548
  el,
549
549
  )
550
550
  })
551
551
 
552
- test("component child warns and returns vnode (lines 170-175)", () => {
552
+ test('component child warns and returns vnode (lines 170-175)', () => {
553
553
  const el = container()
554
554
  const visible = signal(true)
555
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
555
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
556
556
 
557
- const Inner = defineComponent(() => h("span", null, "comp child"))
557
+ const Inner = defineComponent(() => h('span', null, 'comp child'))
558
558
 
559
559
  mount(
560
560
  h(Transition, {
@@ -564,16 +564,16 @@ describe("Transition — uncovered branches", () => {
564
564
  el,
565
565
  )
566
566
 
567
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Transition child is a component"))
567
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Transition child is a component'))
568
568
  warnSpy.mockRestore()
569
569
  })
570
570
 
571
- test("leave with no ref.current sets isMounted false immediately (line 142-144)", async () => {
571
+ test('leave with no ref.current sets isMounted false immediately (line 142-144)', async () => {
572
572
  const el = container()
573
573
  const visible = signal(true)
574
574
 
575
575
  // Use a non-string type (like a component) so ref won't be injected
576
- const Comp = () => h("span", null, "no-ref")
576
+ const Comp = () => h('span', null, 'no-ref')
577
577
  mount(
578
578
  h(Transition, {
579
579
  show: visible,
@@ -587,7 +587,7 @@ describe("Transition — uncovered branches", () => {
587
587
  await new Promise<void>((r) => queueMicrotask(r))
588
588
  })
589
589
 
590
- test("onAfterEnter callback fires after enter transition", async () => {
590
+ test('onAfterEnter callback fires after enter transition', async () => {
591
591
  const el = container()
592
592
  const visible = signal(false)
593
593
  let afterEnterCalled = false
@@ -595,11 +595,11 @@ describe("Transition — uncovered branches", () => {
595
595
  mount(
596
596
  h(Transition, {
597
597
  show: visible,
598
- name: "fade",
598
+ name: 'fade',
599
599
  onAfterEnter: () => {
600
600
  afterEnterCalled = true
601
601
  },
602
- children: h("div", { id: "after-enter-test" }, "content"),
602
+ children: h('div', { id: 'after-enter-test' }, 'content'),
603
603
  }),
604
604
  el,
605
605
  )
@@ -609,14 +609,14 @@ describe("Transition — uncovered branches", () => {
609
609
 
610
610
  // Trigger rAF
611
611
  await new Promise<void>((r) => requestAnimationFrame(() => r()))
612
- const target = el.querySelector("#after-enter-test")
612
+ const target = el.querySelector('#after-enter-test')
613
613
  if (target) {
614
- target.dispatchEvent(new Event("transitionend"))
614
+ target.dispatchEvent(new Event('transitionend'))
615
615
  }
616
616
  expect(afterEnterCalled).toBe(true)
617
617
  })
618
618
 
619
- test("onAfterLeave callback fires after leave transition", async () => {
619
+ test('onAfterLeave callback fires after leave transition', async () => {
620
620
  const el = container()
621
621
  const visible = signal(true)
622
622
  let afterLeaveCalled = false
@@ -624,59 +624,59 @@ describe("Transition — uncovered branches", () => {
624
624
  mount(
625
625
  h(Transition, {
626
626
  show: visible,
627
- name: "fade",
627
+ name: 'fade',
628
628
  onAfterLeave: () => {
629
629
  afterLeaveCalled = true
630
630
  },
631
- children: h("div", { id: "after-leave-test" }, "content"),
631
+ children: h('div', { id: 'after-leave-test' }, 'content'),
632
632
  }),
633
633
  el,
634
634
  )
635
635
 
636
- const target = el.querySelector("#after-leave-test")
636
+ const target = el.querySelector('#after-leave-test')
637
637
  visible.set(false)
638
638
 
639
639
  await new Promise<void>((r) => requestAnimationFrame(() => r()))
640
640
  if (target) {
641
- target.dispatchEvent(new Event("transitionend"))
641
+ target.dispatchEvent(new Event('transitionend'))
642
642
  }
643
643
  await new Promise<void>((r) => queueMicrotask(r))
644
644
  expect(afterLeaveCalled).toBe(true)
645
645
  })
646
646
 
647
- test("tooltip repositions when near top of viewport", () => {
647
+ test('tooltip repositions when near top of viewport', () => {
648
648
  const devtools = (window as unknown as Record<string, unknown>).__PYREON_DEVTOOLS__ as {
649
649
  enableOverlay: () => void
650
650
  disableOverlay: () => void
651
651
  }
652
652
 
653
- const target = document.createElement("div")
653
+ const target = document.createElement('div')
654
654
  // Position near top so tooltip moves below element (rect.top < 35)
655
- target.style.cssText = "width:100px;height:20px;position:fixed;top:10px;left:10px;"
655
+ target.style.cssText = 'width:100px;height:20px;position:fixed;top:10px;left:10px;'
656
656
  document.body.appendChild(target)
657
- registerComponent("top-comp", "TopComp", target, null)
657
+ registerComponent('top-comp', 'TopComp', target, null)
658
658
 
659
659
  devtools.enableOverlay()
660
660
 
661
- const event = new MouseEvent("mousemove", { clientX: 50, clientY: 15, bubbles: true })
661
+ const event = new MouseEvent('mousemove', { clientX: 50, clientY: 15, bubbles: true })
662
662
  document.dispatchEvent(event)
663
663
 
664
664
  devtools.disableOverlay()
665
- unregisterComponent("top-comp")
665
+ unregisterComponent('top-comp')
666
666
  target.remove()
667
667
  })
668
668
  })
669
669
 
670
670
  // ─── hydrate.ts — uncovered branches (lines 162-183, 338) ────────────────────
671
671
 
672
- describe("hydrate.ts — uncovered branches", () => {
673
- test("For hydration with SSR markers — full path with afterEnd (lines 162-183)", () => {
672
+ describe('hydrate.ts — uncovered branches', () => {
673
+ test('For hydration with SSR markers — full path with afterEnd (lines 162-183)', () => {
674
674
  const el = container()
675
675
  // SSR markers with content after the end marker
676
- el.innerHTML = "<!--pyreon-for--><li>a</li><li>b</li><!--/pyreon-for--><p>after</p>"
676
+ el.innerHTML = '<!--pyreon-for--><li>a</li><li>b</li><!--/pyreon-for--><p>after</p>'
677
677
  const items = signal([
678
- { id: 1, label: "a" },
679
- { id: 2, label: "b" },
678
+ { id: 1, label: 'a' },
679
+ { id: 2, label: 'b' },
680
680
  ])
681
681
  const cleanup = hydrateRoot(
682
682
  el,
@@ -686,40 +686,40 @@ describe("hydrate.ts — uncovered branches", () => {
686
686
  For({
687
687
  each: items,
688
688
  by: (r: { id: number }) => r.id,
689
- children: (r: { id: number; label: string }) => h("li", null, r.label),
689
+ children: (r: { id: number; label: string }) => h('li', null, r.label),
690
690
  }),
691
- h("p", null, "after"),
691
+ h('p', null, 'after'),
692
692
  ),
693
693
  )
694
694
  cleanup()
695
695
  })
696
696
 
697
- test("component with onUpdate hooks during hydration (line 338)", () => {
697
+ test('component with onUpdate hooks during hydration (line 338)', () => {
698
698
  const el = container()
699
- el.innerHTML = "<span>update-test</span>"
699
+ el.innerHTML = '<span>update-test</span>'
700
700
  let _updateCalled = false
701
701
 
702
702
  const Comp = defineComponent(() => {
703
703
  onUpdate(() => {
704
704
  _updateCalled = true
705
705
  })
706
- return h("span", null, "update-test")
706
+ return h('span', null, 'update-test')
707
707
  })
708
708
 
709
709
  const cleanup = hydrateRoot(el, h(Comp, null))
710
710
  cleanup()
711
711
  })
712
712
 
713
- test("component with onUnmount hook during hydration cleanup", () => {
713
+ test('component with onUnmount hook during hydration cleanup', () => {
714
714
  const el = container()
715
- el.innerHTML = "<span>unmount-test</span>"
715
+ el.innerHTML = '<span>unmount-test</span>'
716
716
  let unmountCalled = false
717
717
 
718
718
  const Comp = defineComponent(() => {
719
719
  onUnmount(() => {
720
720
  unmountCalled = true
721
721
  })
722
- return h("span", null, "unmount-test")
722
+ return h('span', null, 'unmount-test')
723
723
  })
724
724
 
725
725
  const cleanup = hydrateRoot(el, h(Comp, null))
@@ -727,16 +727,16 @@ describe("hydrate.ts — uncovered branches", () => {
727
727
  expect(unmountCalled).toBe(true)
728
728
  })
729
729
 
730
- test("component with mount cleanup during hydration", () => {
730
+ test('component with mount cleanup during hydration', () => {
731
731
  const el = container()
732
- el.innerHTML = "<span>mount-cleanup</span>"
732
+ el.innerHTML = '<span>mount-cleanup</span>'
733
733
  let mountCleanupCalled = false
734
734
 
735
735
  const Comp = defineComponent(() => {
736
736
  onMount(() => () => {
737
737
  mountCleanupCalled = true
738
738
  })
739
- return h("span", null, "mount-cleanup")
739
+ return h('span', null, 'mount-cleanup')
740
740
  })
741
741
 
742
742
  const cleanup = hydrateRoot(el, h(Comp, null))
@@ -744,57 +744,57 @@ describe("hydrate.ts — uncovered branches", () => {
744
744
  expect(mountCleanupCalled).toBe(true)
745
745
  })
746
746
 
747
- test("hydrates component with children merge", () => {
747
+ test('hydrates component with children merge', () => {
748
748
  const el = container()
749
- el.innerHTML = "<div><b>child</b></div>"
749
+ el.innerHTML = '<div><b>child</b></div>'
750
750
 
751
751
  const Wrapper = defineComponent((props: { children?: VNodeChild }) =>
752
- h("div", null, props.children),
752
+ h('div', null, props.children),
753
753
  )
754
- const cleanup = hydrateRoot(el, h(Wrapper, null, h("b", null, "child")))
754
+ const cleanup = hydrateRoot(el, h(Wrapper, null, h('b', null, 'child')))
755
755
  cleanup()
756
756
  })
757
757
 
758
- test("hydrates reactive accessor returning VNode with domNode present", () => {
758
+ test('hydrates reactive accessor returning VNode with domNode present', () => {
759
759
  const el = container()
760
- el.innerHTML = "<div><span>initial</span></div>"
761
- const content = signal<VNodeChild>(h("span", null, "initial"))
760
+ el.innerHTML = '<div><span>initial</span></div>'
761
+ const content = signal<VNodeChild>(h('span', null, 'initial'))
762
762
  // Reactive accessor returns a VNode — goes through the complex reactive path with marker
763
- const cleanup = hydrateRoot(el, h("div", null, (() => content()) as unknown as VNodeChild))
763
+ const cleanup = hydrateRoot(el, h('div', null, (() => content()) as unknown as VNodeChild))
764
764
  cleanup()
765
765
  })
766
766
  })
767
767
 
768
768
  // ─── transition-group.ts — FLIP move animation (lines 209-218) ───────────────
769
769
 
770
- describe("TransitionGroup — FLIP move animation", () => {
771
- test("FLIP animation fires for moved items", async () => {
770
+ describe('TransitionGroup — FLIP move animation', () => {
771
+ test('FLIP animation fires for moved items', async () => {
772
772
  const el = container()
773
773
  const items = signal([
774
- { id: 1, label: "a" },
775
- { id: 2, label: "b" },
776
- { id: 3, label: "c" },
774
+ { id: 1, label: 'a' },
775
+ { id: 2, label: 'b' },
776
+ { id: 3, label: 'c' },
777
777
  ])
778
778
 
779
779
  mount(
780
780
  h(TransitionGroup, {
781
- tag: "div",
782
- name: "list",
781
+ tag: 'div',
782
+ name: 'list',
783
783
  items: () => items(),
784
784
  keyFn: (item: { id: number }) => item.id,
785
785
  render: (item: { id: number; label: string }) =>
786
- h("span", { class: "flip-item" }, item.label),
786
+ h('span', { class: 'flip-item' }, item.label),
787
787
  }),
788
788
  el,
789
789
  )
790
790
  await new Promise<void>((r) => queueMicrotask(r))
791
- expect(el.querySelectorAll("span.flip-item").length).toBe(3)
791
+ expect(el.querySelectorAll('span.flip-item').length).toBe(3)
792
792
 
793
793
  // Reorder to trigger FLIP
794
794
  items.set([
795
- { id: 3, label: "c" },
796
- { id: 1, label: "a" },
797
- { id: 2, label: "b" },
795
+ { id: 3, label: 'c' },
796
+ { id: 1, label: 'a' },
797
+ { id: 2, label: 'b' },
798
798
  ])
799
799
 
800
800
  // Wait for the effect and rAF chains
@@ -804,23 +804,23 @@ describe("TransitionGroup — FLIP move animation", () => {
804
804
  await new Promise<void>((r) => requestAnimationFrame(() => r()))
805
805
 
806
806
  // Fire transitionend to clean up move class
807
- const spans = el.querySelectorAll("span.flip-item")
807
+ const spans = el.querySelectorAll('span.flip-item')
808
808
  for (const span of spans) {
809
- span.dispatchEvent(new Event("transitionend"))
809
+ span.dispatchEvent(new Event('transitionend'))
810
810
  }
811
811
 
812
812
  // Items should be reordered
813
- const reorderedSpans = el.querySelectorAll("span.flip-item")
814
- expect(reorderedSpans[0]?.textContent).toBe("c")
815
- expect(reorderedSpans[1]?.textContent).toBe("a")
816
- expect(reorderedSpans[2]?.textContent).toBe("b")
813
+ const reorderedSpans = el.querySelectorAll('span.flip-item')
814
+ expect(reorderedSpans[0]?.textContent).toBe('c')
815
+ expect(reorderedSpans[1]?.textContent).toBe('a')
816
+ expect(reorderedSpans[2]?.textContent).toBe('b')
817
817
  })
818
818
  })
819
819
 
820
820
  // ─── nodes.ts — empty mount placeholder paths (lines 433-435, 493-496) ───────
821
821
 
822
- describe("nodes.ts — placeholder comment paths", () => {
823
- test("mountFor fresh render — component returning null uses placeholder", () => {
822
+ describe('nodes.ts — placeholder comment paths', () => {
823
+ test('mountFor fresh render — component returning null uses placeholder', () => {
824
824
  const el = container()
825
825
  const items = signal([{ id: 1 }, { id: 2 }])
826
826
 
@@ -830,7 +830,7 @@ describe("nodes.ts — placeholder comment paths", () => {
830
830
 
831
831
  mount(
832
832
  h(
833
- "div",
833
+ 'div',
834
834
  null,
835
835
  For({
836
836
  each: items,
@@ -842,7 +842,7 @@ describe("nodes.ts — placeholder comment paths", () => {
842
842
  )
843
843
  })
844
844
 
845
- test("mountFor replace-all — component returning null uses placeholder", () => {
845
+ test('mountFor replace-all — component returning null uses placeholder', () => {
846
846
  const el = container()
847
847
  const items = signal([{ id: 1 }])
848
848
 
@@ -850,7 +850,7 @@ describe("nodes.ts — placeholder comment paths", () => {
850
850
 
851
851
  mount(
852
852
  h(
853
- "div",
853
+ 'div',
854
854
  null,
855
855
  For({
856
856
  each: items,
@@ -865,7 +865,7 @@ describe("nodes.ts — placeholder comment paths", () => {
865
865
  items.set([{ id: 10 }, { id: 11 }])
866
866
  })
867
867
 
868
- test("mountFor step 3 — new entries with component returning null (lines 493-496)", () => {
868
+ test('mountFor step 3 — new entries with component returning null (lines 493-496)', () => {
869
869
  const el = container()
870
870
  const items = signal([{ id: 1 }, { id: 2 }])
871
871
 
@@ -873,7 +873,7 @@ describe("nodes.ts — placeholder comment paths", () => {
873
873
 
874
874
  mount(
875
875
  h(
876
- "div",
876
+ 'div',
877
877
  null,
878
878
  For({
879
879
  each: items,
@@ -888,22 +888,22 @@ describe("nodes.ts — placeholder comment paths", () => {
888
888
  items.set([{ id: 1 }, { id: 2 }, { id: 3 }])
889
889
  })
890
890
 
891
- test("mountFor with NativeItem having cleanup in replace-all path", () => {
891
+ test('mountFor with NativeItem having cleanup in replace-all path', () => {
892
892
  const el = container()
893
893
  type R = { id: number; label: string }
894
894
  let cleanupCount = 0
895
895
 
896
- const items = signal<R[]>([{ id: 1, label: "old" }])
896
+ const items = signal<R[]>([{ id: 1, label: 'old' }])
897
897
 
898
898
  mount(
899
899
  h(
900
- "div",
900
+ 'div',
901
901
  null,
902
902
  For({
903
903
  each: items,
904
904
  by: (r) => r.id,
905
905
  children: (r) => {
906
- const native = _tpl("<b></b>", (root) => {
906
+ const native = _tpl('<b></b>', (root) => {
907
907
  root.textContent = r.label
908
908
  return () => {
909
909
  cleanupCount++
@@ -917,47 +917,47 @@ describe("nodes.ts — placeholder comment paths", () => {
917
917
  )
918
918
 
919
919
  // Replace all — should call cleanup on old entries
920
- items.set([{ id: 10, label: "new" }])
920
+ items.set([{ id: 10, label: 'new' }])
921
921
  expect(cleanupCount).toBe(1)
922
922
  })
923
923
  })
924
924
 
925
925
  // ─── props.ts — uncovered branches (lines 213, 242, 273-277) ─────────────────
926
926
 
927
- describe("props.ts — uncovered branches", () => {
928
- test("multiple prop cleanups chain correctly (line 213)", () => {
927
+ describe('props.ts — uncovered branches', () => {
928
+ test('multiple prop cleanups chain correctly (line 213)', () => {
929
929
  const el = container()
930
- const cls = signal("a")
931
- const title = signal("t")
930
+ const cls = signal('a')
931
+ const title = signal('t')
932
932
 
933
933
  // Two reactive props => two cleanups that chain
934
- const unmount = mount(h("div", { class: () => cls(), title: () => title() }), el)
934
+ const unmount = mount(h('div', { class: () => cls(), title: () => title() }), el)
935
935
 
936
- const div = el.querySelector("div") as HTMLElement
937
- expect(div.className).toBe("a")
938
- expect(div.title).toBe("t")
936
+ const div = el.querySelector('div') as HTMLElement
937
+ expect(div.className).toBe('a')
938
+ expect(div.title).toBe('t')
939
939
 
940
- cls.set("b")
941
- title.set("u")
942
- expect(div.className).toBe("b")
943
- expect(div.title).toBe("u")
940
+ cls.set('b')
941
+ title.set('u')
942
+ expect(div.className).toBe('b')
943
+ expect(div.title).toBe('u')
944
944
 
945
945
  unmount()
946
946
  })
947
947
 
948
- test("non-function event handler triggers dev warning", () => {
948
+ test('non-function event handler triggers dev warning', () => {
949
949
  const el = container()
950
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
950
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
951
951
 
952
- mount(h("button", { onClick: "not a function" }), el)
952
+ mount(h('button', { onClick: 'not a function' }), el)
953
953
 
954
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("non-function value"))
954
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('non-function value'))
955
955
  warnSpy.mockRestore()
956
956
  })
957
957
 
958
- test("innerHTML with setHTML method (line 242)", async () => {
958
+ test('innerHTML with setHTML method (line 242)', async () => {
959
959
  const el = container()
960
- const div = document.createElement("div")
960
+ const div = document.createElement('div')
961
961
  el.appendChild(div)
962
962
 
963
963
  // Mock setHTML on the element
@@ -968,170 +968,170 @@ describe("props.ts — uncovered branches", () => {
968
968
  }
969
969
 
970
970
  // Use applyProp directly for this test
971
- const { applyProp } = await import("../props")
972
- applyProp(div, "innerHTML", "<b>via setHTML</b>")
971
+ const { applyProp } = await import('../props')
972
+ applyProp(div, 'innerHTML', '<b>via setHTML</b>')
973
973
  expect(setHTMLCalled).toBe(true)
974
- expect(div.innerHTML).toBe("<b>via setHTML</b>")
974
+ expect(div.innerHTML).toBe('<b>via setHTML</b>')
975
975
  })
976
976
 
977
- test("multiple chained prop cleanups (3+ reactive props)", () => {
977
+ test('multiple chained prop cleanups (3+ reactive props)', () => {
978
978
  const el = container()
979
- const a = signal("a")
980
- const b = signal("b")
981
- const c = signal("c")
979
+ const a = signal('a')
980
+ const b = signal('b')
981
+ const c = signal('c')
982
982
 
983
983
  const unmount = mount(
984
- h("div", {
984
+ h('div', {
985
985
  class: () => a(),
986
986
  title: () => b(),
987
- "data-x": () => c(),
987
+ 'data-x': () => c(),
988
988
  }),
989
989
  el,
990
990
  )
991
991
 
992
- const div = el.querySelector("div") as HTMLElement
993
- expect(div.className).toBe("a")
994
- expect(div.title).toBe("b")
995
- expect(div.getAttribute("data-x")).toBe("c")
992
+ const div = el.querySelector('div') as HTMLElement
993
+ expect(div.className).toBe('a')
994
+ expect(div.title).toBe('b')
995
+ expect(div.getAttribute('data-x')).toBe('c')
996
996
 
997
997
  unmount()
998
998
  })
999
999
 
1000
- test("sanitizeHtml with no DOMParser or Sanitizer falls back to tag stripping", () => {
1000
+ test('sanitizeHtml with no DOMParser or Sanitizer falls back to tag stripping', () => {
1001
1001
  // This path is hard to test in happy-dom since DOMParser exists,
1002
1002
  // but we can test the custom sanitizer path
1003
- setSanitizer((html) => html.replace(/<[^>]*>/g, ""))
1004
- const result = sanitizeHtml("<b>bold</b><script>bad</script>")
1005
- expect(result).toBe("boldbad")
1003
+ setSanitizer((html) => html.replace(/<[^>]*>/g, ''))
1004
+ const result = sanitizeHtml('<b>bold</b><script>bad</script>')
1005
+ expect(result).toBe('boldbad')
1006
1006
  setSanitizer(null)
1007
1007
  })
1008
1008
 
1009
- test("dangerouslySetInnerHTML warns in dev mode", () => {
1009
+ test('dangerouslySetInnerHTML warns in dev mode', () => {
1010
1010
  const el = container()
1011
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
1011
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
1012
1012
 
1013
- mount(h("div", { dangerouslySetInnerHTML: { __html: "<em>raw</em>" } }), el)
1013
+ mount(h('div', { dangerouslySetInnerHTML: { __html: '<em>raw</em>' } }), el)
1014
1014
 
1015
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("dangerouslySetInnerHTML"))
1015
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('dangerouslySetInnerHTML'))
1016
1016
  warnSpy.mockRestore()
1017
1017
  })
1018
1018
 
1019
- test("style as null/undefined does nothing", () => {
1019
+ test('style as null/undefined does nothing', () => {
1020
1020
  const el = container()
1021
- mount(h("div", { style: null as unknown as string }), el)
1021
+ mount(h('div', { style: null as unknown as string }), el)
1022
1022
  // Should not throw
1023
- expect(el.querySelector("div")).not.toBeNull()
1023
+ expect(el.querySelector('div')).not.toBeNull()
1024
1024
  })
1025
1025
  })
1026
1026
 
1027
1027
  // ─── Additional edge cases for mount.ts ──────────────────────────────────────
1028
1028
 
1029
- describe("mount.ts — additional edge cases", () => {
1030
- test("mountElement no reactive work and no ref at depth > 0 returns noop", () => {
1029
+ describe('mount.ts — additional edge cases', () => {
1030
+ test('mountElement no reactive work and no ref at depth > 0 returns noop', () => {
1031
1031
  const el = container()
1032
1032
  // Static nested element with no reactive props, no ref — returns noop at _elementDepth > 0
1033
- const unmount = mount(h("div", null, h("span", null, "static")), el)
1034
- expect(el.querySelector("span")?.textContent).toBe("static")
1033
+ const unmount = mount(h('div', null, h('span', null, 'static')), el)
1034
+ expect(el.querySelector('span')?.textContent).toBe('static')
1035
1035
  unmount()
1036
1036
  })
1037
1037
 
1038
- test("mountChildren 2-child path where one cleanup is noop", () => {
1038
+ test('mountChildren 2-child path where one cleanup is noop', () => {
1039
1039
  const el = container()
1040
1040
  // 2 children: one static (noop cleanup) and one with cleanup
1041
- const cls = signal("x")
1042
- mount(h("div", null, h("span", null, "static"), h("b", { class: () => cls() }, "reactive")), el)
1043
- expect(el.querySelectorAll("span").length).toBe(1)
1044
- expect(el.querySelector("b")?.className).toBe("x")
1041
+ const cls = signal('x')
1042
+ mount(h('div', null, h('span', null, 'static'), h('b', { class: () => cls() }, 'reactive')), el)
1043
+ expect(el.querySelectorAll('span').length).toBe(1)
1044
+ expect(el.querySelector('b')?.className).toBe('x')
1045
1045
  })
1046
1046
 
1047
- test("mountChildren 2-child path where both cleanups are noop", () => {
1047
+ test('mountChildren 2-child path where both cleanups are noop', () => {
1048
1048
  const el = container()
1049
1049
  // 2 static children — both noop cleanup
1050
- mount(h("div", null, h("span", null, "a"), h("b", null, "b")), el)
1051
- expect(el.querySelector("span")?.textContent).toBe("a")
1052
- expect(el.querySelector("b")?.textContent).toBe("b")
1050
+ mount(h('div', null, h('span', null, 'a'), h('b', null, 'b')), el)
1051
+ expect(el.querySelector('span')?.textContent).toBe('a')
1052
+ expect(el.querySelector('b')?.textContent).toBe('b')
1053
1053
  })
1054
1054
 
1055
- test("mountChildren 2-child path where first cleanup is noop", () => {
1055
+ test('mountChildren 2-child path where first cleanup is noop', () => {
1056
1056
  const el = container()
1057
- const cls = signal("x")
1057
+ const cls = signal('x')
1058
1058
  // First child static (noop), second child reactive
1059
- mount(h("div", null, "text", h("b", { class: () => cls() }, "reactive")), el)
1059
+ mount(h('div', null, 'text', h('b', { class: () => cls() }, 'reactive')), el)
1060
1060
  })
1061
1061
 
1062
- test("isKeyedArray returns false for empty array", () => {
1062
+ test('isKeyedArray returns false for empty array', () => {
1063
1063
  const el = container()
1064
1064
  const items = signal<{ id: number }[]>([])
1065
1065
  // Reactive accessor returning empty array — should not use keyed reconciler
1066
1066
  mount(
1067
- h("div", null, () => items().map((it) => h("span", { key: it.id }))),
1067
+ h('div', null, () => items().map((it) => h('span', { key: it.id }))),
1068
1068
  el,
1069
1069
  )
1070
- expect(el.querySelector("span")).toBeNull()
1070
+ expect(el.querySelector('span')).toBeNull()
1071
1071
  })
1072
1072
 
1073
- test("isKeyedArray returns false for non-keyed vnodes", () => {
1073
+ test('isKeyedArray returns false for non-keyed vnodes', () => {
1074
1074
  const el = container()
1075
1075
  const items = signal([1, 2, 3])
1076
1076
  // VNodes without keys — should NOT use keyed reconciler
1077
1077
  mount(
1078
- h("div", null, () => items().map((n) => h("span", null, String(n)))),
1078
+ h('div', null, () => items().map((n) => h('span', null, String(n)))),
1079
1079
  el,
1080
1080
  )
1081
- expect(el.querySelectorAll("span").length).toBe(3)
1081
+ expect(el.querySelectorAll('span').length).toBe(3)
1082
1082
  })
1083
1083
  })
1084
1084
 
1085
1085
  // ─── hydrate.ts — additional branches ────────────────────────────────────────
1086
1086
 
1087
- describe("hydrate.ts — additional branches", () => {
1088
- test("hydrates component returning null", () => {
1087
+ describe('hydrate.ts — additional branches', () => {
1088
+ test('hydrates component returning null', () => {
1089
1089
  const el = container()
1090
- el.innerHTML = ""
1090
+ el.innerHTML = ''
1091
1091
  const NullComp = defineComponent(() => null)
1092
1092
  const cleanup = hydrateRoot(el, h(NullComp, null))
1093
1093
  cleanup()
1094
1094
  })
1095
1095
 
1096
- test("hydrates element mismatch — element found but wrong tag", () => {
1096
+ test('hydrates element mismatch — element found but wrong tag', () => {
1097
1097
  const el = container()
1098
- el.innerHTML = "<div>wrong tag</div>"
1098
+ el.innerHTML = '<div>wrong tag</div>'
1099
1099
  // Expect a <p> but find <div>
1100
- const cleanup = hydrateRoot(el, h("p", null, "right"))
1100
+ const cleanup = hydrateRoot(el, h('p', null, 'right'))
1101
1101
  cleanup()
1102
1102
  })
1103
1103
 
1104
- test("hydrates For without SSR markers but with existing domNode (non-comment)", () => {
1104
+ test('hydrates For without SSR markers but with existing domNode (non-comment)', () => {
1105
1105
  const el = container()
1106
1106
  // Existing element (not a comment) — takes the no-markers path
1107
- el.innerHTML = "<span>not a for marker</span>"
1108
- const items = signal([{ id: 1, label: "a" }])
1107
+ el.innerHTML = '<span>not a for marker</span>'
1108
+ const items = signal([{ id: 1, label: 'a' }])
1109
1109
  const cleanup = hydrateRoot(
1110
1110
  el,
1111
1111
  For({
1112
1112
  each: items,
1113
1113
  by: (r: { id: number }) => r.id,
1114
- children: (r: { id: number; label: string }) => h("li", null, r.label),
1114
+ children: (r: { id: number; label: string }) => h('li', null, r.label),
1115
1115
  }),
1116
1116
  )
1117
1117
  cleanup()
1118
1118
  })
1119
1119
 
1120
- test("hydrates PortalSymbol — always remounts", async () => {
1120
+ test('hydrates PortalSymbol — always remounts', async () => {
1121
1121
  const el = container()
1122
1122
  const target = container()
1123
- el.innerHTML = ""
1123
+ el.innerHTML = ''
1124
1124
 
1125
- const { Portal } = await import("@pyreon/core")
1126
- const cleanup = hydrateRoot(el, Portal({ target, children: h("span", null, "portal") }))
1127
- expect(target.querySelector("span")?.textContent).toBe("portal")
1125
+ const { Portal } = await import('@pyreon/core')
1126
+ const cleanup = hydrateRoot(el, Portal({ target, children: h('span', null, 'portal') }))
1127
+ expect(target.querySelector('span')?.textContent).toBe('portal')
1128
1128
  cleanup()
1129
1129
  })
1130
1130
 
1131
- test("reactive accessor with complex VNode and existing domNode inserts marker before domNode", () => {
1131
+ test('reactive accessor with complex VNode and existing domNode inserts marker before domNode', () => {
1132
1132
  const el = container()
1133
- el.innerHTML = "<span>existing</span>"
1134
- const content = signal<VNodeChild>(h("b", null, "complex"))
1133
+ el.innerHTML = '<span>existing</span>'
1134
+ const content = signal<VNodeChild>(h('b', null, 'complex'))
1135
1135
  const cleanup = hydrateRoot(el, (() => content()) as unknown as VNodeChild)
1136
1136
  cleanup()
1137
1137
  })