@pyreon/core 0.24.4 → 0.24.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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +53 -31
- package/package.json +2 -6
- package/src/compat-marker.ts +0 -79
- package/src/compat-shared.ts +0 -80
- package/src/component.ts +0 -98
- package/src/context.ts +0 -349
- package/src/defer.ts +0 -279
- package/src/dynamic.ts +0 -32
- package/src/env.d.ts +0 -6
- package/src/error-boundary.ts +0 -90
- package/src/for.ts +0 -51
- package/src/h.ts +0 -80
- package/src/index.ts +0 -80
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -747
- package/src/lazy.ts +0 -25
- package/src/lifecycle.ts +0 -152
- package/src/manifest.ts +0 -579
- package/src/map-array.ts +0 -42
- package/src/portal.ts +0 -39
- package/src/props.ts +0 -269
- package/src/ref.ts +0 -32
- package/src/show.ts +0 -121
- package/src/style.ts +0 -102
- package/src/suspense.ts +0 -52
- package/src/telemetry.ts +0 -120
- package/src/tests/compat-marker.test.ts +0 -96
- package/src/tests/compat-shared.test.ts +0 -99
- package/src/tests/component.test.ts +0 -281
- package/src/tests/context.test.ts +0 -629
- package/src/tests/core.test.ts +0 -1290
- package/src/tests/cx.test.ts +0 -70
- package/src/tests/defer.test.ts +0 -359
- package/src/tests/dynamic.test.ts +0 -87
- package/src/tests/error-boundary.test.ts +0 -181
- package/src/tests/extract-props-overloads.types.test.ts +0 -135
- package/src/tests/for.test.ts +0 -117
- package/src/tests/h.test.ts +0 -221
- package/src/tests/jsx-compat.test.tsx +0 -86
- package/src/tests/lazy.test.ts +0 -100
- package/src/tests/lifecycle.test.ts +0 -350
- package/src/tests/manifest-snapshot.test.ts +0 -100
- package/src/tests/map-array.test.ts +0 -313
- package/src/tests/native-marker-error-boundary.test.ts +0 -12
- package/src/tests/portal.test.ts +0 -48
- package/src/tests/props-extended.test.ts +0 -157
- package/src/tests/props.test.ts +0 -250
- package/src/tests/reactive-context.test.ts +0 -69
- package/src/tests/reactive-props.test.ts +0 -157
- package/src/tests/ref.test.ts +0 -70
- package/src/tests/show.test.ts +0 -314
- package/src/tests/style.test.ts +0 -157
- package/src/tests/suspense.test.ts +0 -139
- package/src/tests/telemetry.test.ts +0 -297
- package/src/types.ts +0 -116
package/src/tests/cx.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { cx } from '../style'
|
|
2
|
-
|
|
3
|
-
describe('cx', () => {
|
|
4
|
-
test('returns empty string for falsy values', () => {
|
|
5
|
-
expect(cx(null)).toBe('')
|
|
6
|
-
expect(cx(undefined)).toBe('')
|
|
7
|
-
expect(cx(false)).toBe('')
|
|
8
|
-
expect(cx(true)).toBe('')
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test('passes through strings', () => {
|
|
12
|
-
expect(cx('foo bar')).toBe('foo bar')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('converts numbers to strings', () => {
|
|
16
|
-
expect(cx(42)).toBe('42')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('filters and joins arrays', () => {
|
|
20
|
-
expect(cx(['foo', false, 'bar', null, 'baz'])).toBe('foo bar baz')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('resolves object keys with truthy values', () => {
|
|
24
|
-
expect(cx({ active: true, hidden: false, bold: true })).toBe('active bold')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('resolves object values that are functions', () => {
|
|
28
|
-
expect(cx({ active: () => true, hidden: () => false })).toBe('active')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('handles nested arrays and objects', () => {
|
|
32
|
-
expect(cx(['base', { active: true }, ['nested', { deep: true }]])).toBe(
|
|
33
|
-
'base active nested deep',
|
|
34
|
-
)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('handles empty inputs', () => {
|
|
38
|
-
expect(cx([])).toBe('')
|
|
39
|
-
expect(cx({})).toBe('')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('object where ALL values are functions returning booleans', () => {
|
|
43
|
-
expect(
|
|
44
|
-
cx({
|
|
45
|
-
active: () => true,
|
|
46
|
-
hidden: () => false,
|
|
47
|
-
bold: () => true,
|
|
48
|
-
italic: () => false,
|
|
49
|
-
}),
|
|
50
|
-
).toBe('active bold')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('deeply nested arrays (3+ levels)', () => {
|
|
54
|
-
expect(cx([[['level3', [['level5']]]]])).toBe('level3 level5')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('mixed: string, object with function, deeply nested array', () => {
|
|
58
|
-
expect(cx(['base', { active: () => true }, [['deeply-nested']]])).toBe(
|
|
59
|
-
'base active deeply-nested',
|
|
60
|
-
)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('empty string values are filtered', () => {
|
|
64
|
-
expect(cx(['foo', '', 'bar', ''])).toBe('foo bar')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('number 0 is a valid class name as string', () => {
|
|
68
|
-
expect(cx(0)).toBe('0')
|
|
69
|
-
})
|
|
70
|
-
})
|
package/src/tests/defer.test.ts
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import { signal } from '@pyreon/reactivity'
|
|
2
|
-
import { _setupIdleTrigger, _setupVisibleTrigger, Defer } from '../defer'
|
|
3
|
-
import { Fragment, h } from '../h'
|
|
4
|
-
import type { ComponentFn, Props, VNode } from '../types'
|
|
5
|
-
|
|
6
|
-
// Helper: pull the render-callback out of Defer's returned VNode shape.
|
|
7
|
-
// `when` and `idle` modes return a Fragment whose `children[0]` is a
|
|
8
|
-
// thunk. `visible` mode returns a div whose `children[0]` is the same
|
|
9
|
-
// thunk. Both shapes return the same `renderContent` accessor.
|
|
10
|
-
function getRenderThunk(vnode: VNode): () => unknown {
|
|
11
|
-
const children = vnode.children as unknown[]
|
|
12
|
-
const thunk = children[0]
|
|
13
|
-
if (typeof thunk !== 'function') throw new Error('Expected render thunk')
|
|
14
|
-
return thunk as () => unknown
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('Defer — common shape', () => {
|
|
18
|
-
test('returns a VNode (Fragment or wrapper div per trigger mode)', () => {
|
|
19
|
-
const result = Defer({
|
|
20
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
21
|
-
when: () => false,
|
|
22
|
-
})
|
|
23
|
-
expect(result).toBeDefined()
|
|
24
|
-
expect((result as VNode).type).toBe(Fragment)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('renders fallback before chunk resolves', () => {
|
|
28
|
-
const fallback = h('span', null, 'loading…')
|
|
29
|
-
const vnode = Defer<Props>({
|
|
30
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}), // never resolves
|
|
31
|
-
when: () => true,
|
|
32
|
-
fallback,
|
|
33
|
-
})
|
|
34
|
-
expect(getRenderThunk(vnode)()).toBe(fallback)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('renders null when no fallback and chunk has not resolved', () => {
|
|
38
|
-
const vnode = Defer<Props>({
|
|
39
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
40
|
-
when: () => true,
|
|
41
|
-
})
|
|
42
|
-
expect(getRenderThunk(vnode)()).toBeNull()
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('Defer — when (signal-driven)', () => {
|
|
47
|
-
test('does NOT load chunk while when is false', () => {
|
|
48
|
-
let calls = 0
|
|
49
|
-
const chunkFn = () => {
|
|
50
|
-
calls++
|
|
51
|
-
return Promise.resolve({ default: (() => null) as ComponentFn<Props> })
|
|
52
|
-
}
|
|
53
|
-
const flag = signal(false)
|
|
54
|
-
Defer<Props>({ chunk: chunkFn, when: flag })
|
|
55
|
-
expect(calls).toBe(0)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('loads chunk when when flips to true', async () => {
|
|
59
|
-
const Inner: ComponentFn<{ msg: string }> = (p) => h('div', null, p.msg)
|
|
60
|
-
let calls = 0
|
|
61
|
-
const chunkFn = () => {
|
|
62
|
-
calls++
|
|
63
|
-
return Promise.resolve({ default: Inner })
|
|
64
|
-
}
|
|
65
|
-
const flag = signal(false)
|
|
66
|
-
const vnode = Defer<{ msg: string }>({
|
|
67
|
-
chunk: chunkFn,
|
|
68
|
-
when: flag,
|
|
69
|
-
children: (Comp) => h(Comp, { msg: 'hi' }),
|
|
70
|
-
})
|
|
71
|
-
expect(calls).toBe(0)
|
|
72
|
-
flag.set(true)
|
|
73
|
-
// Effect schedules synchronously; chunk fetch is microtask-resolved.
|
|
74
|
-
expect(calls).toBe(1)
|
|
75
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
76
|
-
const result = getRenderThunk(vnode)() as VNode
|
|
77
|
-
expect(result.type).toBe(Inner)
|
|
78
|
-
expect(result.props).toEqual({ msg: 'hi' })
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('loads chunk EXACTLY ONCE when signal oscillates', async () => {
|
|
82
|
-
let calls = 0
|
|
83
|
-
const chunkFn = () => {
|
|
84
|
-
calls++
|
|
85
|
-
return Promise.resolve({ default: (() => null) as ComponentFn<Props> })
|
|
86
|
-
}
|
|
87
|
-
const flag = signal(false)
|
|
88
|
-
Defer<Props>({ chunk: chunkFn, when: flag })
|
|
89
|
-
flag.set(true)
|
|
90
|
-
flag.set(false)
|
|
91
|
-
flag.set(true)
|
|
92
|
-
flag.set(false)
|
|
93
|
-
flag.set(true)
|
|
94
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
95
|
-
expect(calls).toBe(1)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('accepts component re-exports without default wrapper', async () => {
|
|
99
|
-
const Inner: ComponentFn = () => h('span', null, 'ok')
|
|
100
|
-
const flag = signal(true)
|
|
101
|
-
const vnode = Defer<Props>({
|
|
102
|
-
chunk: () => Promise.resolve(Inner), // bare ComponentFn
|
|
103
|
-
when: flag,
|
|
104
|
-
children: (Comp) => h(Comp, {}),
|
|
105
|
-
})
|
|
106
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
107
|
-
const result = getRenderThunk(vnode)() as VNode
|
|
108
|
-
expect(result.type).toBe(Inner)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('throws when chunk() rejects (Suspense-style error propagation)', async () => {
|
|
112
|
-
const consoleSpy = (() => {
|
|
113
|
-
const orig = console.error
|
|
114
|
-
console.error = () => {} // silence dev-mode error log
|
|
115
|
-
return () => {
|
|
116
|
-
console.error = orig
|
|
117
|
-
}
|
|
118
|
-
})()
|
|
119
|
-
try {
|
|
120
|
-
const flag = signal(true)
|
|
121
|
-
const vnode = Defer<Props>({
|
|
122
|
-
chunk: () => Promise.reject(new Error('chunk boom')),
|
|
123
|
-
when: flag,
|
|
124
|
-
})
|
|
125
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
126
|
-
expect(() => getRenderThunk(vnode)()).toThrow('chunk boom')
|
|
127
|
-
} finally {
|
|
128
|
-
consoleSpy()
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test('renders default <Comp /> when children render-prop omitted', async () => {
|
|
133
|
-
const Inner: ComponentFn = () => h('div', null, 'no-children-prop')
|
|
134
|
-
const flag = signal(true)
|
|
135
|
-
const vnode = Defer<Props>({
|
|
136
|
-
chunk: () => Promise.resolve({ default: Inner }),
|
|
137
|
-
when: flag,
|
|
138
|
-
})
|
|
139
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
140
|
-
const result = getRenderThunk(vnode)() as VNode
|
|
141
|
-
expect(result.type).toBe(Inner)
|
|
142
|
-
expect(result.props).toEqual({})
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
describe('Defer — on="visible"', () => {
|
|
147
|
-
test('returns a div wrapper with data-pyreon-defer="visible"', () => {
|
|
148
|
-
const vnode = Defer<Props>({
|
|
149
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
150
|
-
on: 'visible',
|
|
151
|
-
})
|
|
152
|
-
expect((vnode as VNode).type).toBe('div')
|
|
153
|
-
expect((vnode as VNode).props['data-pyreon-defer']).toBe('visible')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('uses display: contents so wrapper is layout-transparent', () => {
|
|
157
|
-
const vnode = Defer<Props>({
|
|
158
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
159
|
-
on: 'visible',
|
|
160
|
-
})
|
|
161
|
-
expect((vnode as VNode).props.style).toBe('display: contents')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('default rootMargin is 200px (not exposed via prop spread)', () => {
|
|
165
|
-
// The rootMargin is consumed by onMount; we can't directly observe
|
|
166
|
-
// it from the returned VNode. This test documents the default and
|
|
167
|
-
// would catch a regression in the constant.
|
|
168
|
-
const vnode = Defer<Props>({
|
|
169
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
170
|
-
on: 'visible',
|
|
171
|
-
})
|
|
172
|
-
// Wrapper has the structural attrs but no rootMargin leak to DOM.
|
|
173
|
-
expect((vnode as VNode).props.rootMargin).toBeUndefined()
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe('Defer — on="idle"', () => {
|
|
178
|
-
test('returns a Fragment (no wrapper element)', () => {
|
|
179
|
-
const vnode = Defer<Props>({
|
|
180
|
-
chunk: () => new Promise<{ default: ComponentFn<Props> }>(() => {}),
|
|
181
|
-
on: 'idle',
|
|
182
|
-
})
|
|
183
|
-
expect((vnode as VNode).type).toBe(Fragment)
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
// Browser-API helpers extracted from the onMount callbacks so they're
|
|
188
|
-
// directly testable without happy-dom (core tests run in Node). The
|
|
189
|
-
// onMount wrappers in `defer.ts` just delegate to these.
|
|
190
|
-
describe('_setupIdleTrigger', () => {
|
|
191
|
-
// Use `Reflect.has` + property descriptors instead of plain assignment
|
|
192
|
-
// so the restore is symmetric — if the global wasn't defined at test
|
|
193
|
-
// start (Node), `delete` cleanly returns to the original state.
|
|
194
|
-
const orig = {
|
|
195
|
-
ric: (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback,
|
|
196
|
-
cic: (globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback,
|
|
197
|
-
}
|
|
198
|
-
afterEach(() => {
|
|
199
|
-
if (orig.ric === undefined) delete (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback
|
|
200
|
-
else (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback = orig.ric
|
|
201
|
-
if (orig.cic === undefined) delete (globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback
|
|
202
|
-
else (globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback = orig.cic
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
test('uses requestIdleCallback when available', () => {
|
|
206
|
-
let captured: (() => void) | null = null
|
|
207
|
-
;(globalThis as { requestIdleCallback?: unknown }).requestIdleCallback = (
|
|
208
|
-
cb: () => void,
|
|
209
|
-
): number => {
|
|
210
|
-
captured = cb
|
|
211
|
-
return 42
|
|
212
|
-
}
|
|
213
|
-
let cancelledId: number | null = null
|
|
214
|
-
;(globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback = (
|
|
215
|
-
id: number,
|
|
216
|
-
): void => {
|
|
217
|
-
cancelledId = id
|
|
218
|
-
}
|
|
219
|
-
const startLoad = () => {}
|
|
220
|
-
const teardown = _setupIdleTrigger(startLoad)
|
|
221
|
-
expect(captured).toBe(startLoad)
|
|
222
|
-
teardown()
|
|
223
|
-
expect(cancelledId).toBe(42)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
test('teardown is no-op when cancelIdleCallback is missing', () => {
|
|
227
|
-
;(globalThis as { requestIdleCallback?: unknown }).requestIdleCallback = (
|
|
228
|
-
_cb: () => void,
|
|
229
|
-
): number => 99
|
|
230
|
-
delete (globalThis as { cancelIdleCallback?: unknown }).cancelIdleCallback
|
|
231
|
-
const teardown = _setupIdleTrigger(() => {})
|
|
232
|
-
expect(() => teardown()).not.toThrow()
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
test('falls back to setTimeout when requestIdleCallback is absent', async () => {
|
|
236
|
-
delete (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback
|
|
237
|
-
let loaded = false
|
|
238
|
-
const teardown = _setupIdleTrigger(() => {
|
|
239
|
-
loaded = true
|
|
240
|
-
})
|
|
241
|
-
// setTimeout(fn, 1) fires after a tick — await to observe.
|
|
242
|
-
await new Promise((r) => setTimeout(r, 5))
|
|
243
|
-
expect(loaded).toBe(true)
|
|
244
|
-
teardown() // safe to call after the timer fired
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
test('setTimeout fallback cancels via clearTimeout when teardown fires before tick', () => {
|
|
248
|
-
delete (globalThis as { requestIdleCallback?: unknown }).requestIdleCallback
|
|
249
|
-
let loaded = false
|
|
250
|
-
const teardown = _setupIdleTrigger(() => {
|
|
251
|
-
loaded = true
|
|
252
|
-
})
|
|
253
|
-
teardown() // cancel before the timer fires
|
|
254
|
-
return new Promise<void>((r) =>
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
expect(loaded).toBe(false)
|
|
257
|
-
r()
|
|
258
|
-
}, 10),
|
|
259
|
-
)
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
describe('_setupVisibleTrigger', () => {
|
|
264
|
-
const origObs = (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver
|
|
265
|
-
afterEach(() => {
|
|
266
|
-
if (origObs === undefined) delete (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver
|
|
267
|
-
else (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = origObs
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
test('loads immediately when el is null', () => {
|
|
271
|
-
let loaded = false
|
|
272
|
-
const teardown = _setupVisibleTrigger(null, () => {
|
|
273
|
-
loaded = true
|
|
274
|
-
}, '200px')
|
|
275
|
-
expect(loaded).toBe(true)
|
|
276
|
-
expect(typeof teardown).toBe('function')
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
test('loads immediately when IntersectionObserver is unavailable', () => {
|
|
280
|
-
delete (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver
|
|
281
|
-
// Pass a stub element; without the global the trigger should bail early.
|
|
282
|
-
const stubEl = {} as unknown as HTMLElement
|
|
283
|
-
let loaded = false
|
|
284
|
-
_setupVisibleTrigger(stubEl, () => {
|
|
285
|
-
loaded = true
|
|
286
|
-
}, '200px')
|
|
287
|
-
expect(loaded).toBe(true)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test('creates an observer with the configured rootMargin', () => {
|
|
291
|
-
let capturedOptions: IntersectionObserverInit | undefined
|
|
292
|
-
let observed: Element | null = null
|
|
293
|
-
let disconnected = false
|
|
294
|
-
class StubObserver {
|
|
295
|
-
callback: IntersectionObserverCallback
|
|
296
|
-
constructor(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) {
|
|
297
|
-
this.callback = cb
|
|
298
|
-
capturedOptions = opts
|
|
299
|
-
}
|
|
300
|
-
observe(el: Element) {
|
|
301
|
-
observed = el
|
|
302
|
-
}
|
|
303
|
-
disconnect() {
|
|
304
|
-
disconnected = true
|
|
305
|
-
}
|
|
306
|
-
unobserve() {}
|
|
307
|
-
takeRecords() {
|
|
308
|
-
return []
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
;(globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = StubObserver
|
|
312
|
-
const stubEl = { tagName: 'DIV' } as unknown as HTMLElement
|
|
313
|
-
const teardown = _setupVisibleTrigger(stubEl, () => {}, '300px')
|
|
314
|
-
expect(observed).toBe(stubEl)
|
|
315
|
-
expect(capturedOptions?.rootMargin).toBe('300px')
|
|
316
|
-
teardown()
|
|
317
|
-
expect(disconnected).toBe(true)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
test('fires startLoad on intersection, then disconnects', () => {
|
|
321
|
-
let captured: IntersectionObserverCallback | null = null
|
|
322
|
-
let disconnected = false
|
|
323
|
-
class StubObserver {
|
|
324
|
-
constructor(cb: IntersectionObserverCallback) {
|
|
325
|
-
captured = cb
|
|
326
|
-
}
|
|
327
|
-
observe() {}
|
|
328
|
-
disconnect() {
|
|
329
|
-
disconnected = true
|
|
330
|
-
}
|
|
331
|
-
unobserve() {}
|
|
332
|
-
takeRecords() {
|
|
333
|
-
return []
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
;(globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = StubObserver
|
|
337
|
-
let loaded = false
|
|
338
|
-
const stubEl = {} as unknown as HTMLElement
|
|
339
|
-
_setupVisibleTrigger(stubEl, () => {
|
|
340
|
-
loaded = true
|
|
341
|
-
}, '0px')
|
|
342
|
-
|
|
343
|
-
// Simulate non-intersecting entry — should NOT fire.
|
|
344
|
-
captured!(
|
|
345
|
-
[{ isIntersecting: false } as unknown as IntersectionObserverEntry],
|
|
346
|
-
{} as IntersectionObserver,
|
|
347
|
-
)
|
|
348
|
-
expect(loaded).toBe(false)
|
|
349
|
-
expect(disconnected).toBe(false)
|
|
350
|
-
|
|
351
|
-
// Simulate intersecting entry — fires and disconnects.
|
|
352
|
-
captured!(
|
|
353
|
-
[{ isIntersecting: true } as unknown as IntersectionObserverEntry],
|
|
354
|
-
{} as IntersectionObserver,
|
|
355
|
-
)
|
|
356
|
-
expect(loaded).toBe(true)
|
|
357
|
-
expect(disconnected).toBe(true)
|
|
358
|
-
})
|
|
359
|
-
})
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { Dynamic } from '../dynamic'
|
|
2
|
-
import { h } from '../h'
|
|
3
|
-
import type { ComponentFn, VNode, VNodeChild } from '../types'
|
|
4
|
-
|
|
5
|
-
describe('Dynamic', () => {
|
|
6
|
-
test('renders component function', () => {
|
|
7
|
-
const Greeting: ComponentFn = (props) => h('span', null, (props as { name: string }).name)
|
|
8
|
-
const result = Dynamic({ component: Greeting, name: 'world' })
|
|
9
|
-
expect(result).not.toBeNull()
|
|
10
|
-
expect((result as VNode).type).toBe(Greeting)
|
|
11
|
-
expect((result as VNode).props).toEqual({ name: 'world' })
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('renders string element', () => {
|
|
15
|
-
const result = Dynamic({ component: 'div', class: 'box', id: 'main' })
|
|
16
|
-
expect(result).not.toBeNull()
|
|
17
|
-
expect((result as VNode).type).toBe('div')
|
|
18
|
-
expect((result as VNode).props).toEqual({ class: 'box', id: 'main' })
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('strips component prop from rest props', () => {
|
|
22
|
-
const result = Dynamic({ component: 'span', id: 'x' })
|
|
23
|
-
expect((result as VNode).props.component).toBeUndefined()
|
|
24
|
-
expect((result as VNode).props.id).toBe('x')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('returns null for empty string component', () => {
|
|
28
|
-
const result = Dynamic({ component: '' })
|
|
29
|
-
expect(result).toBeNull()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('warns when component prop is falsy', () => {
|
|
33
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
34
|
-
Dynamic({ component: '' })
|
|
35
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('<Dynamic>'))
|
|
36
|
-
warnSpy.mockRestore()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('passes all extra props to the rendered component', () => {
|
|
40
|
-
const Comp: ComponentFn = (props) => h('div', null, JSON.stringify(props))
|
|
41
|
-
const result = Dynamic({
|
|
42
|
-
component: Comp,
|
|
43
|
-
a: 1,
|
|
44
|
-
b: 'two',
|
|
45
|
-
c: true,
|
|
46
|
-
})
|
|
47
|
-
expect((result as VNode).props).toEqual({ a: 1, b: 'two', c: true })
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('renders with no extra props', () => {
|
|
51
|
-
const result = Dynamic({ component: 'br' })
|
|
52
|
-
expect(result).not.toBeNull()
|
|
53
|
-
expect((result as VNode).type).toBe('br')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('does not leak children as a prop on string-tag mount', () => {
|
|
57
|
-
// Regression: for string `component`, runtime-dom forwards every prop
|
|
58
|
-
// key to setAttribute. If `children` stayed in props it crashed at
|
|
59
|
-
// mount with `setAttribute('children', ...)`. The fix re-emits them
|
|
60
|
-
// as h() rest args, landing them in vnode.children.
|
|
61
|
-
const result = Dynamic({ component: 'h3', children: 'hello' })
|
|
62
|
-
expect((result as VNode).type).toBe('h3')
|
|
63
|
-
expect((result as VNode).props.children).toBeUndefined()
|
|
64
|
-
expect((result as VNode).children).toEqual(['hello'])
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('flattens array children to vnode.children', () => {
|
|
68
|
-
const a = h('span', null, 'a')
|
|
69
|
-
const b = h('span', null, 'b')
|
|
70
|
-
const result = Dynamic({ component: 'div', children: [a, b] })
|
|
71
|
-
expect((result as VNode).props.children).toBeUndefined()
|
|
72
|
-
expect((result as VNode).children).toHaveLength(2)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('component children still reach props.children at mount', () => {
|
|
76
|
-
// For component (not string), the merge happens at mount via
|
|
77
|
-
// mergeChildrenIntoProps — verified end-to-end by mount tests in
|
|
78
|
-
// runtime-dom. Here we just confirm the vnode shape is correct so the
|
|
79
|
-
// merge will fire (children must be on vnode.children, not props).
|
|
80
|
-
const Comp: ComponentFn = (props) =>
|
|
81
|
-
h('div', null, (props as { children?: VNodeChild }).children ?? null)
|
|
82
|
-
const result = Dynamic({ component: Comp, children: 'hi' })
|
|
83
|
-
expect((result as VNode).type).toBe(Comp)
|
|
84
|
-
expect((result as VNode).props.children).toBeUndefined()
|
|
85
|
-
expect((result as VNode).children).toEqual(['hi'])
|
|
86
|
-
})
|
|
87
|
-
})
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { dispatchToErrorBoundary, popErrorBoundary, runWithHooks } from '../component'
|
|
2
|
-
import { ErrorBoundary } from '../error-boundary'
|
|
3
|
-
import { h } from '../h'
|
|
4
|
-
import type { VNodeChild } from '../types'
|
|
5
|
-
|
|
6
|
-
describe('ErrorBoundary', () => {
|
|
7
|
-
// Clean up error boundary stack after each test
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
// Pop all boundaries that tests may have left
|
|
10
|
-
while (dispatchToErrorBoundary('cleanup')) {
|
|
11
|
-
popErrorBoundary()
|
|
12
|
-
}
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('is a function', () => {
|
|
16
|
-
expect(typeof ErrorBoundary).toBe('function')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('returns a reactive getter', () => {
|
|
20
|
-
let result: VNodeChild = null
|
|
21
|
-
runWithHooks(() => {
|
|
22
|
-
result = ErrorBoundary({
|
|
23
|
-
fallback: (err) => `Error: ${err}`,
|
|
24
|
-
children: 'child',
|
|
25
|
-
})
|
|
26
|
-
return null
|
|
27
|
-
}, {})
|
|
28
|
-
expect(typeof result).toBe('function')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('renders children when no error', () => {
|
|
32
|
-
let result: VNodeChild = null
|
|
33
|
-
runWithHooks(() => {
|
|
34
|
-
result = ErrorBoundary({
|
|
35
|
-
fallback: (err) => `Error: ${err}`,
|
|
36
|
-
children: 'child content',
|
|
37
|
-
})
|
|
38
|
-
return null
|
|
39
|
-
}, {})
|
|
40
|
-
const getter = result as unknown as () => VNodeChild
|
|
41
|
-
expect(getter()).toBe('child content')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('renders function children by calling them', () => {
|
|
45
|
-
let result: VNodeChild = null
|
|
46
|
-
runWithHooks(() => {
|
|
47
|
-
result = ErrorBoundary({
|
|
48
|
-
fallback: (err) => `Error: ${err}`,
|
|
49
|
-
children: () => 'dynamic child',
|
|
50
|
-
})
|
|
51
|
-
return null
|
|
52
|
-
}, {})
|
|
53
|
-
const getter = result as unknown as () => VNodeChild
|
|
54
|
-
expect(getter()).toBe('dynamic child')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('renders VNode children', () => {
|
|
58
|
-
let result: VNodeChild = null
|
|
59
|
-
const child = h('div', null, 'content')
|
|
60
|
-
runWithHooks(() => {
|
|
61
|
-
result = ErrorBoundary({
|
|
62
|
-
fallback: (err) => `Error: ${err}`,
|
|
63
|
-
children: child,
|
|
64
|
-
})
|
|
65
|
-
return null
|
|
66
|
-
}, {})
|
|
67
|
-
const getter = result as unknown as () => VNodeChild
|
|
68
|
-
expect(getter()).toBe(child)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('registers unmount cleanup hook', () => {
|
|
72
|
-
const { hooks } = runWithHooks(() => {
|
|
73
|
-
ErrorBoundary({
|
|
74
|
-
fallback: (err) => `Error: ${err}`,
|
|
75
|
-
children: 'child',
|
|
76
|
-
})
|
|
77
|
-
return null
|
|
78
|
-
}, {})
|
|
79
|
-
expect(hooks.unmount!.length).toBeGreaterThanOrEqual(1)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('warns when fallback is not a function', () => {
|
|
83
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
84
|
-
runWithHooks(() => {
|
|
85
|
-
ErrorBoundary({
|
|
86
|
-
fallback: 'not-a-function' as unknown as (err: unknown, reset: () => void) => VNodeChild,
|
|
87
|
-
children: 'child',
|
|
88
|
-
})
|
|
89
|
-
return null
|
|
90
|
-
}, {})
|
|
91
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('<ErrorBoundary>'))
|
|
92
|
-
warnSpy.mockRestore()
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('dispatched error triggers fallback rendering', () => {
|
|
96
|
-
let result: VNodeChild = null
|
|
97
|
-
runWithHooks(() => {
|
|
98
|
-
result = ErrorBoundary({
|
|
99
|
-
fallback: (err) => `Caught: ${err}`,
|
|
100
|
-
children: 'normal',
|
|
101
|
-
})
|
|
102
|
-
return null
|
|
103
|
-
}, {})
|
|
104
|
-
const getter = result as unknown as () => VNodeChild
|
|
105
|
-
expect(getter()).toBe('normal')
|
|
106
|
-
|
|
107
|
-
dispatchToErrorBoundary(new Error('boom'))
|
|
108
|
-
expect(getter()).toBe('Caught: Error: boom')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('fallback receives reset function that clears error', () => {
|
|
112
|
-
let result: VNodeChild = null
|
|
113
|
-
let capturedReset: (() => void) | undefined
|
|
114
|
-
runWithHooks(() => {
|
|
115
|
-
result = ErrorBoundary({
|
|
116
|
-
fallback: (_err, reset) => {
|
|
117
|
-
capturedReset = reset
|
|
118
|
-
return 'error-ui'
|
|
119
|
-
},
|
|
120
|
-
children: 'child',
|
|
121
|
-
})
|
|
122
|
-
return null
|
|
123
|
-
}, {})
|
|
124
|
-
const getter = result as unknown as () => VNodeChild
|
|
125
|
-
expect(getter()).toBe('child')
|
|
126
|
-
|
|
127
|
-
dispatchToErrorBoundary('test-error')
|
|
128
|
-
expect(getter()).toBe('error-ui')
|
|
129
|
-
expect(capturedReset).toBeDefined()
|
|
130
|
-
|
|
131
|
-
capturedReset?.()
|
|
132
|
-
expect(getter()).toBe('child')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('second error while already in error state is not handled', () => {
|
|
136
|
-
let result: VNodeChild = null
|
|
137
|
-
runWithHooks(() => {
|
|
138
|
-
result = ErrorBoundary({
|
|
139
|
-
fallback: (err) => `Error: ${err}`,
|
|
140
|
-
children: 'child',
|
|
141
|
-
})
|
|
142
|
-
return null
|
|
143
|
-
}, {})
|
|
144
|
-
const getter = result as unknown as () => VNodeChild
|
|
145
|
-
|
|
146
|
-
// First error handled
|
|
147
|
-
expect(dispatchToErrorBoundary('first')).toBe(true)
|
|
148
|
-
expect(getter()).toBe('Error: first')
|
|
149
|
-
|
|
150
|
-
// Second error not handled (already in error state)
|
|
151
|
-
expect(dispatchToErrorBoundary('second')).toBe(false)
|
|
152
|
-
// Still showing first error
|
|
153
|
-
expect(getter()).toBe('Error: first')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
test('after reset, new error can be caught again', () => {
|
|
157
|
-
let result: VNodeChild = null
|
|
158
|
-
let capturedReset: (() => void) | undefined
|
|
159
|
-
runWithHooks(() => {
|
|
160
|
-
result = ErrorBoundary({
|
|
161
|
-
fallback: (err, reset) => {
|
|
162
|
-
capturedReset = reset
|
|
163
|
-
return `Error: ${err}`
|
|
164
|
-
},
|
|
165
|
-
children: 'child',
|
|
166
|
-
})
|
|
167
|
-
return null
|
|
168
|
-
}, {})
|
|
169
|
-
const getter = result as unknown as () => VNodeChild
|
|
170
|
-
|
|
171
|
-
dispatchToErrorBoundary('first-error')
|
|
172
|
-
expect(getter()).toBe('Error: first-error')
|
|
173
|
-
|
|
174
|
-
capturedReset?.()
|
|
175
|
-
expect(getter()).toBe('child')
|
|
176
|
-
|
|
177
|
-
// Can catch new error after reset
|
|
178
|
-
expect(dispatchToErrorBoundary('second-error')).toBe(true)
|
|
179
|
-
expect(getter()).toBe('Error: second-error')
|
|
180
|
-
})
|
|
181
|
-
})
|