@pyreon/runtime-dom 0.11.5 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -22
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/package.json +12 -12
- package/src/delegate.ts +25 -25
- package/src/devtools.ts +37 -37
- package/src/hydrate.ts +30 -30
- package/src/hydration-debug.ts +2 -2
- package/src/index.ts +20 -20
- package/src/keep-alive.ts +6 -6
- package/src/mount.ts +46 -46
- package/src/nodes.ts +31 -19
- package/src/props.ts +93 -93
- package/src/template.ts +6 -6
- package/src/tests/coverage-gaps.test.ts +669 -669
- package/src/tests/coverage.test.ts +299 -299
- package/src/tests/mount.test.ts +1183 -1183
- package/src/tests/props.test.ts +219 -219
- package/src/tests/setup.ts +1 -1
- package/src/tests/show-context.test.ts +43 -43
- package/src/tests/template.test.ts +71 -71
- package/src/tests/transition.test.ts +124 -124
- package/src/transition-group.ts +22 -22
- package/src/transition.ts +18 -18
|
@@ -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
|
|
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
|
|
18
|
-
import { signal } from
|
|
19
|
-
import { installDevTools, registerComponent, unregisterComponent } from
|
|
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
|
|
29
|
-
import { mountChild } from
|
|
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(
|
|
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(
|
|
43
|
-
test(
|
|
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(
|
|
47
|
-
span.textContent =
|
|
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(
|
|
53
|
+
expect(el.querySelector('.box span')?.textContent).toBe('hello')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
test(
|
|
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(
|
|
59
|
+
root.querySelector('em')!.textContent = 'first'
|
|
60
60
|
return null
|
|
61
61
|
})
|
|
62
62
|
const n2 = _tpl(html, (root) => {
|
|
63
|
-
root.querySelector(
|
|
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(
|
|
69
|
-
expect((n2.el as HTMLElement).querySelector(
|
|
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(
|
|
72
|
+
test('bind function can return a cleanup', () => {
|
|
73
73
|
let cleaned = false
|
|
74
|
-
const native = _tpl(
|
|
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(
|
|
84
|
+
test('mountChild handles NativeItem from _tpl', () => {
|
|
85
85
|
const el = container()
|
|
86
|
-
const native = _tpl(
|
|
86
|
+
const native = _tpl('<span>tpl</span>', () => null)
|
|
87
87
|
const cleanup = mountChild(native as unknown as VNodeChild, el, null)
|
|
88
|
-
expect(el.querySelector(
|
|
88
|
+
expect(el.querySelector('span')?.textContent).toBe('tpl')
|
|
89
89
|
cleanup()
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test(
|
|
92
|
+
test('mountChild handles NativeItem with cleanup from _tpl', () => {
|
|
93
93
|
const el = container()
|
|
94
94
|
let cleaned = false
|
|
95
|
-
const native = _tpl(
|
|
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(
|
|
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(
|
|
107
|
+
describe('DevTools — overlay and $p console helpers', () => {
|
|
108
108
|
beforeAll(() => {
|
|
109
109
|
installDevTools()
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
test(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
140
|
+
expect(document.getElementById('__pyreon-overlay')).not.toBeNull()
|
|
141
141
|
devtools.disableOverlay()
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
test(
|
|
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(
|
|
152
|
+
const target = document.createElement('div')
|
|
153
153
|
document.body.appendChild(target)
|
|
154
|
-
const event = new MouseEvent(
|
|
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(
|
|
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(
|
|
168
|
-
target.style.cssText =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
183
|
+
unregisterComponent('overlay-test')
|
|
184
184
|
target.remove()
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
test(
|
|
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(
|
|
194
|
-
target.style.cssText =
|
|
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(
|
|
199
|
-
registerComponent(
|
|
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(
|
|
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(
|
|
212
|
-
unregisterComponent(
|
|
211
|
+
unregisterComponent('click-test')
|
|
212
|
+
unregisterComponent('click-parent')
|
|
213
213
|
target.remove()
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
test(
|
|
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(
|
|
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(
|
|
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(
|
|
241
|
+
expect(document.body.style.cursor).toBe('crosshair')
|
|
242
242
|
|
|
243
|
-
const event = new KeyboardEvent(
|
|
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(
|
|
248
|
+
test('Ctrl+Shift+P toggles overlay', () => {
|
|
249
249
|
// Enable via Ctrl+Shift+P
|
|
250
|
-
const enableEvent = new KeyboardEvent(
|
|
251
|
-
key:
|
|
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(
|
|
257
|
+
expect(document.body.style.cursor).toBe('crosshair')
|
|
258
258
|
|
|
259
259
|
// Disable via Ctrl+Shift+P
|
|
260
|
-
const disableEvent = new KeyboardEvent(
|
|
261
|
-
key:
|
|
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(
|
|
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(
|
|
277
|
-
target.style.cssText =
|
|
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(
|
|
281
|
-
registerComponent(
|
|
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(
|
|
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(
|
|
290
|
-
unregisterComponent(
|
|
289
|
+
unregisterComponent('child-ov-1')
|
|
290
|
+
unregisterComponent('parent-ov')
|
|
291
291
|
target.remove()
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
test(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
328
|
+
expect(typeof stats.roots).toBe('number')
|
|
329
329
|
|
|
330
330
|
// $p.help()
|
|
331
331
|
$p.help()
|
|
332
332
|
|
|
333
|
-
unregisterComponent(
|
|
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(
|
|
341
|
+
registerComponent('sole-root', 'SoleRoot', null, null)
|
|
342
342
|
const stats = $p.stats()
|
|
343
343
|
expect(stats.roots).toBeGreaterThanOrEqual(1)
|
|
344
|
-
unregisterComponent(
|
|
344
|
+
unregisterComponent('sole-root')
|
|
345
345
|
})
|
|
346
346
|
})
|
|
347
347
|
|
|
348
348
|
// ─── mount.ts — uncovered branches ───────────────────────────────────────────
|
|
349
349
|
|
|
350
|
-
describe(
|
|
351
|
-
test(
|
|
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,
|
|
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(
|
|
359
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('returned an invalid value'))
|
|
360
360
|
warnSpy.mockRestore()
|
|
361
361
|
})
|
|
362
362
|
|
|
363
|
-
test(
|
|
363
|
+
test('component returning Promise triggers dev warning', () => {
|
|
364
364
|
const el = container()
|
|
365
|
-
const warnSpy = vi.spyOn(console,
|
|
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(
|
|
370
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('returned a Promise'))
|
|
371
371
|
warnSpy.mockRestore()
|
|
372
372
|
})
|
|
373
373
|
|
|
374
|
-
test(
|
|
374
|
+
test('void element with children triggers dev warning', () => {
|
|
375
375
|
const el = container()
|
|
376
|
-
const warnSpy = vi.spyOn(console,
|
|
376
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
377
377
|
|
|
378
|
-
mount(h(
|
|
378
|
+
mount(h('img', null, 'child text'), el)
|
|
379
379
|
|
|
380
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(
|
|
380
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('void element'))
|
|
381
381
|
warnSpy.mockRestore()
|
|
382
382
|
})
|
|
383
383
|
|
|
384
|
-
test(
|
|
384
|
+
test('Portal with falsy target warns', () => {
|
|
385
385
|
const el = container()
|
|
386
|
-
const warnSpy = vi.spyOn(console,
|
|
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(
|
|
390
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Portal'))
|
|
391
391
|
warnSpy.mockRestore()
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
-
test(
|
|
394
|
+
test('component subtree mount error with propagateError (lines 298-309)', () => {
|
|
395
395
|
const el = container()
|
|
396
|
-
const errorSpy = vi.spyOn(console,
|
|
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(
|
|
404
|
-
throw new 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(
|
|
414
|
+
test('mountChildren with >2 children and cleanups (line 387)', () => {
|
|
415
415
|
const el = container()
|
|
416
|
-
const s1 = signal(
|
|
417
|
-
const s2 = signal(
|
|
418
|
-
const s3 = signal(
|
|
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
|
-
|
|
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(
|
|
433
|
-
expect(el.querySelector(
|
|
434
|
-
expect(el.querySelector(
|
|
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(
|
|
439
|
+
test('mountElement with ref + propCleanup at _elementDepth > 0', () => {
|
|
440
440
|
const el = container()
|
|
441
441
|
const ref = createRef<HTMLElement>()
|
|
442
|
-
const cls = signal(
|
|
442
|
+
const cls = signal('foo')
|
|
443
443
|
|
|
444
444
|
// Nested element with ref AND reactive prop — exercises the combined cleanup path
|
|
445
|
-
mount(h(
|
|
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(
|
|
448
|
+
expect(ref.current?.className).toBe('foo')
|
|
449
449
|
})
|
|
450
450
|
|
|
451
|
-
test(
|
|
451
|
+
test('mountElement with propCleanup only at _elementDepth > 0', () => {
|
|
452
452
|
const el = container()
|
|
453
|
-
const cls = signal(
|
|
453
|
+
const cls = signal('bar')
|
|
454
454
|
|
|
455
455
|
// Nested element with reactive prop but no ref
|
|
456
|
-
const unmount = mount(h(
|
|
456
|
+
const unmount = mount(h('div', null, h('span', { class: () => cls() }, 'inner')), el)
|
|
457
457
|
|
|
458
|
-
expect(el.querySelector(
|
|
459
|
-
cls.set(
|
|
460
|
-
expect(el.querySelector(
|
|
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(
|
|
464
|
+
test('reactive text at _elementDepth > 0 returns just dispose', () => {
|
|
465
465
|
const el = container()
|
|
466
|
-
const text = signal(
|
|
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(
|
|
470
|
+
h('div', null, () => text()),
|
|
471
471
|
el,
|
|
472
472
|
)
|
|
473
473
|
|
|
474
|
-
expect(el.querySelector(
|
|
475
|
-
text.set(
|
|
476
|
-
expect(el.querySelector(
|
|
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(
|
|
480
|
+
test('NativeItem without cleanup at _elementDepth > 0', () => {
|
|
481
481
|
const el = container()
|
|
482
|
-
const native = _tpl(
|
|
482
|
+
const native = _tpl('<b>native</b>', () => null)
|
|
483
483
|
|
|
484
484
|
// Mount NativeItem inside a parent element
|
|
485
|
-
mount(h(
|
|
486
|
-
expect(el.querySelector(
|
|
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(
|
|
489
|
+
test('NativeItem with cleanup at _elementDepth > 0', () => {
|
|
490
490
|
const el = container()
|
|
491
491
|
let _cleaned = false
|
|
492
|
-
const native = _tpl(
|
|
492
|
+
const native = _tpl('<b>native2</b>', () => () => {
|
|
493
493
|
_cleaned = true
|
|
494
494
|
})
|
|
495
495
|
|
|
496
|
-
mount(h(
|
|
497
|
-
expect(el.querySelector(
|
|
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(
|
|
504
|
-
test(
|
|
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:
|
|
512
|
-
children: h(
|
|
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(
|
|
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:
|
|
532
|
+
children: 'just text' as unknown as VNodeChild,
|
|
533
533
|
}),
|
|
534
534
|
el,
|
|
535
535
|
)
|
|
536
536
|
})
|
|
537
537
|
|
|
538
|
-
test(
|
|
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(
|
|
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(
|
|
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,
|
|
555
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
556
556
|
|
|
557
|
-
const Inner = defineComponent(() => h(
|
|
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(
|
|
567
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Transition child is a component'))
|
|
568
568
|
warnSpy.mockRestore()
|
|
569
569
|
})
|
|
570
570
|
|
|
571
|
-
test(
|
|
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(
|
|
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(
|
|
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:
|
|
598
|
+
name: 'fade',
|
|
599
599
|
onAfterEnter: () => {
|
|
600
600
|
afterEnterCalled = true
|
|
601
601
|
},
|
|
602
|
-
children: h(
|
|
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(
|
|
612
|
+
const target = el.querySelector('#after-enter-test')
|
|
613
613
|
if (target) {
|
|
614
|
-
target.dispatchEvent(new Event(
|
|
614
|
+
target.dispatchEvent(new Event('transitionend'))
|
|
615
615
|
}
|
|
616
616
|
expect(afterEnterCalled).toBe(true)
|
|
617
617
|
})
|
|
618
618
|
|
|
619
|
-
test(
|
|
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:
|
|
627
|
+
name: 'fade',
|
|
628
628
|
onAfterLeave: () => {
|
|
629
629
|
afterLeaveCalled = true
|
|
630
630
|
},
|
|
631
|
-
children: h(
|
|
631
|
+
children: h('div', { id: 'after-leave-test' }, 'content'),
|
|
632
632
|
}),
|
|
633
633
|
el,
|
|
634
634
|
)
|
|
635
635
|
|
|
636
|
-
const target = el.querySelector(
|
|
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(
|
|
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(
|
|
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(
|
|
653
|
+
const target = document.createElement('div')
|
|
654
654
|
// Position near top so tooltip moves below element (rect.top < 35)
|
|
655
|
-
target.style.cssText =
|
|
655
|
+
target.style.cssText = 'width:100px;height:20px;position:fixed;top:10px;left:10px;'
|
|
656
656
|
document.body.appendChild(target)
|
|
657
|
-
registerComponent(
|
|
657
|
+
registerComponent('top-comp', 'TopComp', target, null)
|
|
658
658
|
|
|
659
659
|
devtools.enableOverlay()
|
|
660
660
|
|
|
661
|
-
const event = new MouseEvent(
|
|
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(
|
|
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(
|
|
673
|
-
test(
|
|
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 =
|
|
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:
|
|
679
|
-
{ id: 2, label:
|
|
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(
|
|
689
|
+
children: (r: { id: number; label: string }) => h('li', null, r.label),
|
|
690
690
|
}),
|
|
691
|
-
h(
|
|
691
|
+
h('p', null, 'after'),
|
|
692
692
|
),
|
|
693
693
|
)
|
|
694
694
|
cleanup()
|
|
695
695
|
})
|
|
696
696
|
|
|
697
|
-
test(
|
|
697
|
+
test('component with onUpdate hooks during hydration (line 338)', () => {
|
|
698
698
|
const el = container()
|
|
699
|
-
el.innerHTML =
|
|
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(
|
|
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(
|
|
713
|
+
test('component with onUnmount hook during hydration cleanup', () => {
|
|
714
714
|
const el = container()
|
|
715
|
-
el.innerHTML =
|
|
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(
|
|
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(
|
|
730
|
+
test('component with mount cleanup during hydration', () => {
|
|
731
731
|
const el = container()
|
|
732
|
-
el.innerHTML =
|
|
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(
|
|
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(
|
|
747
|
+
test('hydrates component with children merge', () => {
|
|
748
748
|
const el = container()
|
|
749
|
-
el.innerHTML =
|
|
749
|
+
el.innerHTML = '<div><b>child</b></div>'
|
|
750
750
|
|
|
751
751
|
const Wrapper = defineComponent((props: { children?: VNodeChild }) =>
|
|
752
|
-
h(
|
|
752
|
+
h('div', null, props.children),
|
|
753
753
|
)
|
|
754
|
-
const cleanup = hydrateRoot(el, h(Wrapper, null, h(
|
|
754
|
+
const cleanup = hydrateRoot(el, h(Wrapper, null, h('b', null, 'child')))
|
|
755
755
|
cleanup()
|
|
756
756
|
})
|
|
757
757
|
|
|
758
|
-
test(
|
|
758
|
+
test('hydrates reactive accessor returning VNode with domNode present', () => {
|
|
759
759
|
const el = container()
|
|
760
|
-
el.innerHTML =
|
|
761
|
-
const content = signal<VNodeChild>(h(
|
|
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(
|
|
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(
|
|
771
|
-
test(
|
|
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:
|
|
775
|
-
{ id: 2, label:
|
|
776
|
-
{ id: 3, label:
|
|
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:
|
|
782
|
-
name:
|
|
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(
|
|
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(
|
|
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:
|
|
796
|
-
{ id: 1, label:
|
|
797
|
-
{ id: 2, label:
|
|
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(
|
|
807
|
+
const spans = el.querySelectorAll('span.flip-item')
|
|
808
808
|
for (const span of spans) {
|
|
809
|
-
span.dispatchEvent(new Event(
|
|
809
|
+
span.dispatchEvent(new Event('transitionend'))
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
// Items should be reordered
|
|
813
|
-
const reorderedSpans = el.querySelectorAll(
|
|
814
|
-
expect(reorderedSpans[0]?.textContent).toBe(
|
|
815
|
-
expect(reorderedSpans[1]?.textContent).toBe(
|
|
816
|
-
expect(reorderedSpans[2]?.textContent).toBe(
|
|
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(
|
|
823
|
-
test(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
896
|
+
const items = signal<R[]>([{ id: 1, label: 'old' }])
|
|
897
897
|
|
|
898
898
|
mount(
|
|
899
899
|
h(
|
|
900
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
928
|
-
test(
|
|
927
|
+
describe('props.ts — uncovered branches', () => {
|
|
928
|
+
test('multiple prop cleanups chain correctly (line 213)', () => {
|
|
929
929
|
const el = container()
|
|
930
|
-
const cls = signal(
|
|
931
|
-
const title = signal(
|
|
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(
|
|
934
|
+
const unmount = mount(h('div', { class: () => cls(), title: () => title() }), el)
|
|
935
935
|
|
|
936
|
-
const div = el.querySelector(
|
|
937
|
-
expect(div.className).toBe(
|
|
938
|
-
expect(div.title).toBe(
|
|
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(
|
|
941
|
-
title.set(
|
|
942
|
-
expect(div.className).toBe(
|
|
943
|
-
expect(div.title).toBe(
|
|
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(
|
|
948
|
+
test('non-function event handler triggers dev warning', () => {
|
|
949
949
|
const el = container()
|
|
950
|
-
const warnSpy = vi.spyOn(console,
|
|
950
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
951
951
|
|
|
952
|
-
mount(h(
|
|
952
|
+
mount(h('button', { onClick: 'not a function' }), el)
|
|
953
953
|
|
|
954
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(
|
|
954
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('non-function value'))
|
|
955
955
|
warnSpy.mockRestore()
|
|
956
956
|
})
|
|
957
957
|
|
|
958
|
-
test(
|
|
958
|
+
test('innerHTML with setHTML method (line 242)', async () => {
|
|
959
959
|
const el = container()
|
|
960
|
-
const div = document.createElement(
|
|
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(
|
|
972
|
-
applyProp(div,
|
|
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(
|
|
974
|
+
expect(div.innerHTML).toBe('<b>via setHTML</b>')
|
|
975
975
|
})
|
|
976
976
|
|
|
977
|
-
test(
|
|
977
|
+
test('multiple chained prop cleanups (3+ reactive props)', () => {
|
|
978
978
|
const el = container()
|
|
979
|
-
const a = signal(
|
|
980
|
-
const b = signal(
|
|
981
|
-
const c = signal(
|
|
979
|
+
const a = signal('a')
|
|
980
|
+
const b = signal('b')
|
|
981
|
+
const c = signal('c')
|
|
982
982
|
|
|
983
983
|
const unmount = mount(
|
|
984
|
-
h(
|
|
984
|
+
h('div', {
|
|
985
985
|
class: () => a(),
|
|
986
986
|
title: () => b(),
|
|
987
|
-
|
|
987
|
+
'data-x': () => c(),
|
|
988
988
|
}),
|
|
989
989
|
el,
|
|
990
990
|
)
|
|
991
991
|
|
|
992
|
-
const div = el.querySelector(
|
|
993
|
-
expect(div.className).toBe(
|
|
994
|
-
expect(div.title).toBe(
|
|
995
|
-
expect(div.getAttribute(
|
|
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(
|
|
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(
|
|
1005
|
-
expect(result).toBe(
|
|
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(
|
|
1009
|
+
test('dangerouslySetInnerHTML warns in dev mode', () => {
|
|
1010
1010
|
const el = container()
|
|
1011
|
-
const warnSpy = vi.spyOn(console,
|
|
1011
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
1012
1012
|
|
|
1013
|
-
mount(h(
|
|
1013
|
+
mount(h('div', { dangerouslySetInnerHTML: { __html: '<em>raw</em>' } }), el)
|
|
1014
1014
|
|
|
1015
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(
|
|
1015
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('dangerouslySetInnerHTML'))
|
|
1016
1016
|
warnSpy.mockRestore()
|
|
1017
1017
|
})
|
|
1018
1018
|
|
|
1019
|
-
test(
|
|
1019
|
+
test('style as null/undefined does nothing', () => {
|
|
1020
1020
|
const el = container()
|
|
1021
|
-
mount(h(
|
|
1021
|
+
mount(h('div', { style: null as unknown as string }), el)
|
|
1022
1022
|
// Should not throw
|
|
1023
|
-
expect(el.querySelector(
|
|
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(
|
|
1030
|
-
test(
|
|
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(
|
|
1034
|
-
expect(el.querySelector(
|
|
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(
|
|
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(
|
|
1042
|
-
mount(h(
|
|
1043
|
-
expect(el.querySelectorAll(
|
|
1044
|
-
expect(el.querySelector(
|
|
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(
|
|
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(
|
|
1051
|
-
expect(el.querySelector(
|
|
1052
|
-
expect(el.querySelector(
|
|
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(
|
|
1055
|
+
test('mountChildren 2-child path where first cleanup is noop', () => {
|
|
1056
1056
|
const el = container()
|
|
1057
|
-
const cls = signal(
|
|
1057
|
+
const cls = signal('x')
|
|
1058
1058
|
// First child static (noop), second child reactive
|
|
1059
|
-
mount(h(
|
|
1059
|
+
mount(h('div', null, 'text', h('b', { class: () => cls() }, 'reactive')), el)
|
|
1060
1060
|
})
|
|
1061
1061
|
|
|
1062
|
-
test(
|
|
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(
|
|
1067
|
+
h('div', null, () => items().map((it) => h('span', { key: it.id }))),
|
|
1068
1068
|
el,
|
|
1069
1069
|
)
|
|
1070
|
-
expect(el.querySelector(
|
|
1070
|
+
expect(el.querySelector('span')).toBeNull()
|
|
1071
1071
|
})
|
|
1072
1072
|
|
|
1073
|
-
test(
|
|
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(
|
|
1078
|
+
h('div', null, () => items().map((n) => h('span', null, String(n)))),
|
|
1079
1079
|
el,
|
|
1080
1080
|
)
|
|
1081
|
-
expect(el.querySelectorAll(
|
|
1081
|
+
expect(el.querySelectorAll('span').length).toBe(3)
|
|
1082
1082
|
})
|
|
1083
1083
|
})
|
|
1084
1084
|
|
|
1085
1085
|
// ─── hydrate.ts — additional branches ────────────────────────────────────────
|
|
1086
1086
|
|
|
1087
|
-
describe(
|
|
1088
|
-
test(
|
|
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(
|
|
1096
|
+
test('hydrates element mismatch — element found but wrong tag', () => {
|
|
1097
1097
|
const el = container()
|
|
1098
|
-
el.innerHTML =
|
|
1098
|
+
el.innerHTML = '<div>wrong tag</div>'
|
|
1099
1099
|
// Expect a <p> but find <div>
|
|
1100
|
-
const cleanup = hydrateRoot(el, h(
|
|
1100
|
+
const cleanup = hydrateRoot(el, h('p', null, 'right'))
|
|
1101
1101
|
cleanup()
|
|
1102
1102
|
})
|
|
1103
1103
|
|
|
1104
|
-
test(
|
|
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 =
|
|
1108
|
-
const items = signal([{ id: 1, label:
|
|
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(
|
|
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(
|
|
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(
|
|
1126
|
-
const cleanup = hydrateRoot(el, Portal({ target, children: h(
|
|
1127
|
-
expect(target.querySelector(
|
|
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(
|
|
1131
|
+
test('reactive accessor with complex VNode and existing domNode inserts marker before domNode', () => {
|
|
1132
1132
|
const el = container()
|
|
1133
|
-
el.innerHTML =
|
|
1134
|
-
const content = signal<VNodeChild>(h(
|
|
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
|
})
|