@pyreon/svelte-compat 0.17.0 → 0.26.2
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/package.json +4 -9
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -538
- package/src/jsx-dev-runtime.ts +0 -1
- package/src/jsx-runtime.ts +0 -316
- package/src/store.ts +0 -26
- package/src/svelte-compat.browser.test.ts +0 -67
- package/src/tests/child-instance-leak-repro.test.ts +0 -123
- package/src/tests/lifecycle-cleanup-leak-repro.test.ts +0 -81
- package/src/tests/native-marker-bypass.test.tsx +0 -72
- package/src/tests/setup.ts +0 -3
- package/src/tests/store-entry.test.ts +0 -36
- package/src/tests/svelte-compat.test.ts +0 -547
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import * as storeEntry from '../store'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `@pyreon/svelte-compat/store` is the `svelte/store` import surface —
|
|
6
|
-
* the vite plugin's `compat: 'svelte'` aliases `svelte/store` to it.
|
|
7
|
-
* It must re-export exactly the store API (no lifecycle/context) so the
|
|
8
|
-
* subpath mirrors Svelte's real `svelte/store` shape, and the
|
|
9
|
-
* re-exported functions must be the same identities as `../index`.
|
|
10
|
-
*/
|
|
11
|
-
describe('@pyreon/svelte-compat/store entry', () => {
|
|
12
|
-
it('exposes exactly the store API', () => {
|
|
13
|
-
expect(Object.keys(storeEntry).sort()).toEqual([
|
|
14
|
-
'derived',
|
|
15
|
-
'get',
|
|
16
|
-
'readable',
|
|
17
|
-
'readonly',
|
|
18
|
-
'writable',
|
|
19
|
-
])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('re-exported writable/derived/get round-trip through the subpath', () => {
|
|
23
|
-
const n = storeEntry.writable(2)
|
|
24
|
-
const doubled = storeEntry.derived(n, (v: number) => v * 2)
|
|
25
|
-
expect(storeEntry.get(doubled)).toBe(4)
|
|
26
|
-
n.set(5)
|
|
27
|
-
expect(storeEntry.get(doubled)).toBe(10)
|
|
28
|
-
|
|
29
|
-
const ro = storeEntry.readonly(n)
|
|
30
|
-
expect('set' in ro).toBe(false)
|
|
31
|
-
expect(storeEntry.get(ro)).toBe(5)
|
|
32
|
-
|
|
33
|
-
const r = storeEntry.readable(7)
|
|
34
|
-
expect(storeEntry.get(r)).toBe(7)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
import type { ComponentFn } from '@pyreon/core'
|
|
2
|
-
import { mount as pyreonMount } from '@pyreon/runtime-dom'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import type { RenderContext } from '../jsx-runtime'
|
|
5
|
-
import { beginRender, endRender, getCurrentCtx, jsx } from '../jsx-runtime'
|
|
6
|
-
import {
|
|
7
|
-
afterUpdate,
|
|
8
|
-
beforeUpdate,
|
|
9
|
-
createEventDispatcher,
|
|
10
|
-
derived,
|
|
11
|
-
flushSync,
|
|
12
|
-
get,
|
|
13
|
-
getAllContexts,
|
|
14
|
-
getContext,
|
|
15
|
-
hasContext,
|
|
16
|
-
mount,
|
|
17
|
-
onDestroy,
|
|
18
|
-
onMount,
|
|
19
|
-
readable,
|
|
20
|
-
readonly,
|
|
21
|
-
setContext,
|
|
22
|
-
tick,
|
|
23
|
-
unmount,
|
|
24
|
-
writable,
|
|
25
|
-
} from '../index'
|
|
26
|
-
|
|
27
|
-
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** Runs `fn` inside a fresh RenderContext to exercise hook-index code paths. */
|
|
30
|
-
function createHookRunner() {
|
|
31
|
-
const ctx: RenderContext = {
|
|
32
|
-
hooks: [],
|
|
33
|
-
scheduleRerender: () => {},
|
|
34
|
-
pendingEffects: [],
|
|
35
|
-
pendingLayoutEffects: [],
|
|
36
|
-
unmounted: false,
|
|
37
|
-
unmountCallbacks: [],
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
ctx,
|
|
41
|
-
run<T>(fn: () => T): T {
|
|
42
|
-
beginRender(ctx)
|
|
43
|
-
const result = fn()
|
|
44
|
-
endRender()
|
|
45
|
-
return result
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function container(): HTMLElement {
|
|
51
|
-
const el = document.createElement('div')
|
|
52
|
-
document.body.appendChild(el)
|
|
53
|
-
return el
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe('@pyreon/svelte-compat — svelte/store', () => {
|
|
57
|
-
// ─── writable ──────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
it('writable calls subscriber synchronously with the initial value', () => {
|
|
60
|
-
const store = writable(1)
|
|
61
|
-
const seen: number[] = []
|
|
62
|
-
store.subscribe((v) => seen.push(v))
|
|
63
|
-
expect(seen).toEqual([1])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('writable set notifies subscribers', () => {
|
|
67
|
-
const store = writable(0)
|
|
68
|
-
const seen: number[] = []
|
|
69
|
-
store.subscribe((v) => seen.push(v))
|
|
70
|
-
store.set(5)
|
|
71
|
-
expect(seen).toEqual([0, 5])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('writable update applies the updater to the current value', () => {
|
|
75
|
-
const store = writable(10)
|
|
76
|
-
const seen: number[] = []
|
|
77
|
-
store.subscribe((v) => seen.push(v))
|
|
78
|
-
store.update((n) => n + 3)
|
|
79
|
-
expect(seen).toEqual([10, 13])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('writable supports multiple independent subscribers', () => {
|
|
83
|
-
const store = writable('a')
|
|
84
|
-
const a: string[] = []
|
|
85
|
-
const b: string[] = []
|
|
86
|
-
store.subscribe((v) => a.push(v))
|
|
87
|
-
store.subscribe((v) => b.push(v))
|
|
88
|
-
store.set('b')
|
|
89
|
-
expect(a).toEqual(['a', 'b'])
|
|
90
|
-
expect(b).toEqual(['a', 'b'])
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('writable unsubscribe stops further notifications', () => {
|
|
94
|
-
const store = writable(0)
|
|
95
|
-
const seen: number[] = []
|
|
96
|
-
const unsub = store.subscribe((v) => seen.push(v))
|
|
97
|
-
store.set(1)
|
|
98
|
-
unsub()
|
|
99
|
-
store.set(2)
|
|
100
|
-
expect(seen).toEqual([0, 1])
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('writable invalidate fires before run on change', () => {
|
|
104
|
-
const store = writable(0)
|
|
105
|
-
const order: string[] = []
|
|
106
|
-
store.subscribe(
|
|
107
|
-
() => order.push('run'),
|
|
108
|
-
() => order.push('invalidate'),
|
|
109
|
-
)
|
|
110
|
-
store.set(1)
|
|
111
|
-
// Real Svelte semantics: the initial subscribe calls `run` only (no
|
|
112
|
-
// `invalidate`); each subsequent change is the two-phase
|
|
113
|
-
// `invalidate` → `run`. So: run(0), then invalidate(1), run(1).
|
|
114
|
-
expect(order).toEqual(['run', 'invalidate', 'run'])
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('writable runs start on first subscriber and stop on last', () => {
|
|
118
|
-
const start = vi.fn(() => stop)
|
|
119
|
-
const stop = vi.fn()
|
|
120
|
-
const store = writable<number>(0, start)
|
|
121
|
-
expect(start).not.toHaveBeenCalled()
|
|
122
|
-
const u1 = store.subscribe(() => {})
|
|
123
|
-
expect(start).toHaveBeenCalledTimes(1)
|
|
124
|
-
const u2 = store.subscribe(() => {})
|
|
125
|
-
expect(start).toHaveBeenCalledTimes(1) // not called again
|
|
126
|
-
u1()
|
|
127
|
-
expect(stop).not.toHaveBeenCalled() // still one subscriber
|
|
128
|
-
u2()
|
|
129
|
-
expect(stop).toHaveBeenCalledTimes(1)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('writable start can push values via its set argument', () => {
|
|
133
|
-
const store = writable<number>(0, (set) => {
|
|
134
|
-
set(42)
|
|
135
|
-
return () => {}
|
|
136
|
-
})
|
|
137
|
-
expect(get(store)).toBe(42)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// ─── readable ──────────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
it('readable exposes only subscribe', () => {
|
|
143
|
-
const store = readable(7)
|
|
144
|
-
expect(typeof store.subscribe).toBe('function')
|
|
145
|
-
expect((store as unknown as Record<string, unknown>).set).toBeUndefined()
|
|
146
|
-
expect(get(store)).toBe(7)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('readable start notifier can drive values', () => {
|
|
150
|
-
const store = readable<number>(0, (set) => {
|
|
151
|
-
set(99)
|
|
152
|
-
})
|
|
153
|
-
expect(get(store)).toBe(99)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
// ─── readonly ──────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
it('readonly returns a subscribe-only view of a writable', () => {
|
|
159
|
-
const w = writable(1)
|
|
160
|
-
const ro = readonly(w)
|
|
161
|
-
expect((ro as unknown as Record<string, unknown>).set).toBeUndefined()
|
|
162
|
-
const seen: number[] = []
|
|
163
|
-
ro.subscribe((v) => seen.push(v))
|
|
164
|
-
w.set(2)
|
|
165
|
-
expect(seen).toEqual([1, 2])
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// ─── get ───────────────────────────────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
it('get reads the current value without keeping a subscription', () => {
|
|
171
|
-
const store = writable(123)
|
|
172
|
-
expect(get(store)).toBe(123)
|
|
173
|
-
store.set(456)
|
|
174
|
-
expect(get(store)).toBe(456)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
// ─── derived ───────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
it('derived (single store, sync) maps the source value', () => {
|
|
180
|
-
const n = writable(2)
|
|
181
|
-
const doubled = derived(n, (v: number) => v * 2)
|
|
182
|
-
const seen: number[] = []
|
|
183
|
-
doubled.subscribe((v) => seen.push(v))
|
|
184
|
-
expect(seen).toEqual([4])
|
|
185
|
-
n.set(5)
|
|
186
|
-
expect(seen).toEqual([4, 10])
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('derived (array of stores, sync) combines values', () => {
|
|
190
|
-
const a = writable(1)
|
|
191
|
-
const b = writable(2)
|
|
192
|
-
const sum = derived([a, b], ([x, y]: [number, number]) => x + y)
|
|
193
|
-
const seen: number[] = []
|
|
194
|
-
sum.subscribe((v) => seen.push(v))
|
|
195
|
-
expect(seen).toEqual([3])
|
|
196
|
-
a.set(10)
|
|
197
|
-
expect(seen).toEqual([3, 12])
|
|
198
|
-
b.set(20)
|
|
199
|
-
expect(seen).toEqual([3, 12, 30])
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('derived (async set form) shows the initial value then the async value', async () => {
|
|
203
|
-
const n = writable(1)
|
|
204
|
-
const async = derived(
|
|
205
|
-
n,
|
|
206
|
-
(v: number, set: (x: number) => void) => {
|
|
207
|
-
// Deferred set models a real async source (fetch/timer): the
|
|
208
|
-
// initial value is shown until the async result lands.
|
|
209
|
-
const id = setTimeout(() => set(v + 100), 0)
|
|
210
|
-
return () => clearTimeout(id)
|
|
211
|
-
},
|
|
212
|
-
0,
|
|
213
|
-
)
|
|
214
|
-
const seen: number[] = []
|
|
215
|
-
async.subscribe((v) => seen.push(v))
|
|
216
|
-
expect(seen).toEqual([0]) // initial value, async not yet resolved
|
|
217
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
218
|
-
expect(seen).toEqual([0, 101])
|
|
219
|
-
n.set(2)
|
|
220
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
221
|
-
expect(seen).toEqual([0, 101, 102])
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('derived (async form) runs the returned cleanup before the next run', () => {
|
|
225
|
-
const n = writable(1)
|
|
226
|
-
const cleanup = vi.fn()
|
|
227
|
-
const d = derived(
|
|
228
|
-
n,
|
|
229
|
-
(v: number, set: (x: number) => void) => {
|
|
230
|
-
set(v)
|
|
231
|
-
return cleanup
|
|
232
|
-
},
|
|
233
|
-
0,
|
|
234
|
-
)
|
|
235
|
-
const unsub = d.subscribe(() => {})
|
|
236
|
-
expect(cleanup).not.toHaveBeenCalled()
|
|
237
|
-
n.set(2) // re-runs sync → previous cleanup fires
|
|
238
|
-
expect(cleanup).toHaveBeenCalledTimes(1)
|
|
239
|
-
unsub() // last subscriber → start's stop runs final cleanup
|
|
240
|
-
expect(cleanup).toHaveBeenCalledTimes(2)
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
describe('@pyreon/svelte-compat — svelte lifecycle', () => {
|
|
245
|
-
it('onMount runs after the first render and its cleanup on unmount', async () => {
|
|
246
|
-
const mounted = vi.fn()
|
|
247
|
-
const cleaned = vi.fn()
|
|
248
|
-
const Comp: ComponentFn = () => {
|
|
249
|
-
onMount(() => {
|
|
250
|
-
mounted()
|
|
251
|
-
return cleaned
|
|
252
|
-
})
|
|
253
|
-
return jsx('div', { children: 'hi' })
|
|
254
|
-
}
|
|
255
|
-
const el = container()
|
|
256
|
-
const dispose = pyreonMount(jsx(Comp, {}), el)
|
|
257
|
-
await new Promise((r) => setTimeout(r, 30))
|
|
258
|
-
expect(mounted).toHaveBeenCalledTimes(1)
|
|
259
|
-
dispose()
|
|
260
|
-
expect(cleaned).toHaveBeenCalledTimes(1)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('onMount is hook-index-stable across re-renders (no double registration)', () => {
|
|
264
|
-
const runner = createHookRunner()
|
|
265
|
-
const fn = vi.fn()
|
|
266
|
-
// First render: registers (pendingEffects gets the entry, hook slot set).
|
|
267
|
-
runner.run(() => onMount(fn))
|
|
268
|
-
expect(runner.ctx.pendingEffects).toHaveLength(1)
|
|
269
|
-
expect(runner.ctx.hooks).toHaveLength(1)
|
|
270
|
-
// Re-render: beginRender clears pendingEffects; the hook-index guard
|
|
271
|
-
// must NOT re-push (onMount runs exactly once, Svelte semantics).
|
|
272
|
-
runner.run(() => onMount(fn))
|
|
273
|
-
expect(runner.ctx.pendingEffects).toHaveLength(0)
|
|
274
|
-
expect(runner.ctx.hooks).toHaveLength(1)
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('onMount outside a component falls back to the Pyreon lifecycle', () => {
|
|
278
|
-
// No current ctx → exercises the pyreonOnMount branch (no throw).
|
|
279
|
-
expect(() => onMount(() => {})).not.toThrow()
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
it('onDestroy runs when the component unmounts', async () => {
|
|
283
|
-
const destroyed = vi.fn()
|
|
284
|
-
const Comp: ComponentFn = () => {
|
|
285
|
-
onDestroy(destroyed)
|
|
286
|
-
return jsx('div', { children: 'x' })
|
|
287
|
-
}
|
|
288
|
-
const el = container()
|
|
289
|
-
const dispose = pyreonMount(jsx(Comp, {}), el)
|
|
290
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
291
|
-
dispose()
|
|
292
|
-
expect(destroyed).toHaveBeenCalledTimes(1)
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('onDestroy is hook-index-stable across re-renders', () => {
|
|
296
|
-
const runner = createHookRunner()
|
|
297
|
-
const fn = vi.fn()
|
|
298
|
-
runner.run(() => onDestroy(fn))
|
|
299
|
-
runner.run(() => onDestroy(fn))
|
|
300
|
-
expect(runner.ctx.unmountCallbacks).toHaveLength(1)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('onDestroy outside a component does not throw', () => {
|
|
304
|
-
expect(() => onDestroy(() => {})).not.toThrow()
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('beforeUpdate runs once before the first render commits', () => {
|
|
308
|
-
const runner = createHookRunner()
|
|
309
|
-
const fn = vi.fn()
|
|
310
|
-
runner.run(() => beforeUpdate(fn))
|
|
311
|
-
runner.run(() => beforeUpdate(fn))
|
|
312
|
-
expect(fn).toHaveBeenCalledTimes(1)
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('beforeUpdate outside a component runs immediately', () => {
|
|
316
|
-
const fn = vi.fn()
|
|
317
|
-
beforeUpdate(fn)
|
|
318
|
-
expect(fn).toHaveBeenCalledTimes(1)
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
it('afterUpdate runs after the first render', async () => {
|
|
322
|
-
const fn = vi.fn()
|
|
323
|
-
const Comp: ComponentFn = () => {
|
|
324
|
-
afterUpdate(fn)
|
|
325
|
-
return jsx('div', { children: 'a' })
|
|
326
|
-
}
|
|
327
|
-
const el = container()
|
|
328
|
-
pyreonMount(jsx(Comp, {}), el)
|
|
329
|
-
await new Promise((r) => setTimeout(r, 30))
|
|
330
|
-
expect(fn).toHaveBeenCalledTimes(1)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('tick resolves after the current microtask', async () => {
|
|
334
|
-
let flag = false
|
|
335
|
-
queueMicrotask(() => {
|
|
336
|
-
flag = true
|
|
337
|
-
})
|
|
338
|
-
await tick()
|
|
339
|
-
expect(flag).toBe(true)
|
|
340
|
-
})
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
describe('@pyreon/svelte-compat — svelte context', () => {
|
|
344
|
-
it('setContext/getContext propagate through the component tree', () => {
|
|
345
|
-
const KEY = Symbol('theme')
|
|
346
|
-
const Consumer: ComponentFn = () => {
|
|
347
|
-
const theme = getContext<string>(KEY)
|
|
348
|
-
return jsx('span', { 'data-theme': theme, children: theme })
|
|
349
|
-
}
|
|
350
|
-
const Provider: ComponentFn = () => {
|
|
351
|
-
setContext(KEY, 'dark')
|
|
352
|
-
return jsx(Consumer, {})
|
|
353
|
-
}
|
|
354
|
-
const el = container()
|
|
355
|
-
pyreonMount(jsx(Provider, { children: undefined }), el)
|
|
356
|
-
const span = el.querySelector('span')
|
|
357
|
-
expect(span?.getAttribute('data-theme')).toBe('dark')
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
it('hasContext reports whether a value was provided', () => {
|
|
361
|
-
const KEY = 'k'
|
|
362
|
-
let inside = false
|
|
363
|
-
let outside = true
|
|
364
|
-
const Consumer: ComponentFn = () => {
|
|
365
|
-
inside = hasContext(KEY)
|
|
366
|
-
return jsx('i', {})
|
|
367
|
-
}
|
|
368
|
-
const Provider: ComponentFn = () => {
|
|
369
|
-
setContext(KEY, 1)
|
|
370
|
-
return jsx(Consumer, {})
|
|
371
|
-
}
|
|
372
|
-
const Bare: ComponentFn = () => {
|
|
373
|
-
outside = hasContext('never-set')
|
|
374
|
-
return jsx('i', {})
|
|
375
|
-
}
|
|
376
|
-
const el = container()
|
|
377
|
-
pyreonMount(jsx(Provider, {}), el)
|
|
378
|
-
pyreonMount(jsx(Bare, {}), container())
|
|
379
|
-
expect(inside).toBe(true)
|
|
380
|
-
expect(outside).toBe(false)
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
it('getAllContexts returns a Map (best-effort)', () => {
|
|
384
|
-
expect(getAllContexts()).toBeInstanceOf(Map)
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
it('setContext returns the provided value', () => {
|
|
388
|
-
let returned: unknown
|
|
389
|
-
const Comp: ComponentFn = () => {
|
|
390
|
-
returned = setContext('x', 99)
|
|
391
|
-
return jsx('i', {})
|
|
392
|
-
}
|
|
393
|
-
pyreonMount(jsx(Comp, {}), container())
|
|
394
|
-
expect(returned).toBe(99)
|
|
395
|
-
})
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
describe('@pyreon/svelte-compat — createEventDispatcher', () => {
|
|
399
|
-
it('dispatches a CustomEvent to the on<Type> prop', async () => {
|
|
400
|
-
const handler = vi.fn()
|
|
401
|
-
const Child: ComponentFn = () => {
|
|
402
|
-
const dispatch = createEventDispatcher<{ ping: number }>()
|
|
403
|
-
onMount(() => {
|
|
404
|
-
dispatch('ping', 7)
|
|
405
|
-
})
|
|
406
|
-
return jsx('span', { children: 'child' })
|
|
407
|
-
}
|
|
408
|
-
const Parent: ComponentFn = () => jsx(Child, { onPing: handler })
|
|
409
|
-
const el = container()
|
|
410
|
-
pyreonMount(jsx(Parent, {}), el)
|
|
411
|
-
await new Promise((r) => setTimeout(r, 30))
|
|
412
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
413
|
-
const evt = handler.mock.calls[0]![0] as CustomEvent
|
|
414
|
-
expect(evt.type).toBe('ping')
|
|
415
|
-
expect(evt.detail).toBe(7)
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('returns true when the event is not cancelled', () => {
|
|
419
|
-
let result: boolean | undefined
|
|
420
|
-
const Child: ComponentFn = () => {
|
|
421
|
-
const dispatch = createEventDispatcher<{ go: void }>()
|
|
422
|
-
result = dispatch('go')
|
|
423
|
-
return jsx('i', {})
|
|
424
|
-
}
|
|
425
|
-
pyreonMount(jsx(Child, {}), container())
|
|
426
|
-
expect(result).toBe(true)
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
describe('@pyreon/svelte-compat — mount/unmount/flushSync', () => {
|
|
431
|
-
it('mount renders a component into the target and unmount removes it', () => {
|
|
432
|
-
const App = () => jsx('div', { id: 'svelte-app', children: 'mounted' })
|
|
433
|
-
const target = container()
|
|
434
|
-
const mounted = mount(App, { target })
|
|
435
|
-
expect(target.querySelector('#svelte-app')?.textContent).toBe('mounted')
|
|
436
|
-
unmount(mounted as Record<symbol, unknown>)
|
|
437
|
-
expect(target.querySelector('#svelte-app')).toBeNull()
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it('mount passes props through to the component', () => {
|
|
441
|
-
const App = (props: { label: string }) => jsx('div', { id: 'lbl', children: props.label })
|
|
442
|
-
const target = container()
|
|
443
|
-
mount(App, { target, props: { label: 'hello' } })
|
|
444
|
-
expect(target.querySelector('#lbl')?.textContent).toBe('hello')
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
it('unmount is a no-op on an object never mounted', () => {
|
|
448
|
-
expect(() => unmount({} as Record<symbol, unknown>)).not.toThrow()
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
it('flushSync invokes fn and returns its result', () => {
|
|
452
|
-
expect(flushSync(() => 42)).toBe(42)
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
it('flushSync with no fn returns undefined', () => {
|
|
456
|
-
expect(flushSync()).toBeUndefined()
|
|
457
|
-
})
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
describe('@pyreon/svelte-compat — jsx-runtime coverage', () => {
|
|
461
|
-
it('native components pass through without wrapping', async () => {
|
|
462
|
-
const { Show } = await import('../index')
|
|
463
|
-
const vnode = jsx(Show as ComponentFn, {
|
|
464
|
-
when: () => true,
|
|
465
|
-
children: jsx('span', { children: 'hi' }),
|
|
466
|
-
})
|
|
467
|
-
expect(vnode.type).toBe(Show)
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
it('jsx forwards a key prop', () => {
|
|
471
|
-
const vnode = jsx('div', { children: 'test' }, 'my-key')
|
|
472
|
-
expect(vnode.props.key).toBe('my-key')
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it('jsx handles no children and array children', () => {
|
|
476
|
-
expect(jsx('div', { class: 'empty' })).toBeDefined()
|
|
477
|
-
const list = jsx('ul', {
|
|
478
|
-
children: [jsx('li', { children: 'a' }), jsx('li', { children: 'b' })],
|
|
479
|
-
})
|
|
480
|
-
expect(list).toBeDefined()
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
it('wrapCompatComponent caches the wrapper per component', () => {
|
|
484
|
-
const Comp: ComponentFn = () => jsx('div', { children: 'c' })
|
|
485
|
-
const v1 = jsx(Comp, {})
|
|
486
|
-
const v2 = jsx(Comp, {})
|
|
487
|
-
expect(v1.type).toBe(v2.type)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
it('component re-renders on store change and patches the DOM', async () => {
|
|
491
|
-
const count = writable(0)
|
|
492
|
-
const Comp: ComponentFn = () => {
|
|
493
|
-
let v = 0
|
|
494
|
-
count.subscribe((n) => {
|
|
495
|
-
v = n
|
|
496
|
-
})
|
|
497
|
-
return jsx('span', { id: 'cnt', children: String(v) })
|
|
498
|
-
}
|
|
499
|
-
const el = container()
|
|
500
|
-
const dispose = pyreonMount(jsx(Comp, {}), el)
|
|
501
|
-
expect(el.querySelector('#cnt')?.textContent).toBe('0')
|
|
502
|
-
count.set(3)
|
|
503
|
-
await new Promise((r) => setTimeout(r, 30))
|
|
504
|
-
expect(el.querySelector('#cnt')?.textContent).toBe('3')
|
|
505
|
-
dispose()
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
it('scheduleRerender after unmount does not re-render', async () => {
|
|
509
|
-
const count = writable(0)
|
|
510
|
-
let renders = 0
|
|
511
|
-
const Comp: ComponentFn = () => {
|
|
512
|
-
renders++
|
|
513
|
-
count.subscribe(() => {})
|
|
514
|
-
return jsx('span', { children: 'x' })
|
|
515
|
-
}
|
|
516
|
-
const el = container()
|
|
517
|
-
const dispose = pyreonMount(jsx(Comp, {}), el)
|
|
518
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
519
|
-
const before = renders
|
|
520
|
-
dispose()
|
|
521
|
-
count.set(1)
|
|
522
|
-
await new Promise((r) => setTimeout(r, 30))
|
|
523
|
-
expect(renders).toBe(before)
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
it('layout effects pushed during render run with cleanup', () => {
|
|
527
|
-
const log: string[] = []
|
|
528
|
-
let pushed = false
|
|
529
|
-
const Comp: ComponentFn = () => {
|
|
530
|
-
const ctx = getCurrentCtx()!
|
|
531
|
-
if (!pushed) {
|
|
532
|
-
pushed = true
|
|
533
|
-
ctx.pendingLayoutEffects.push({
|
|
534
|
-
fn: () => {
|
|
535
|
-
log.push('run')
|
|
536
|
-
return () => log.push('cleanup')
|
|
537
|
-
},
|
|
538
|
-
deps: undefined,
|
|
539
|
-
cleanup: undefined,
|
|
540
|
-
})
|
|
541
|
-
}
|
|
542
|
-
return jsx('div', { children: 'lf' })
|
|
543
|
-
}
|
|
544
|
-
pyreonMount(jsx(Comp, {}), container())
|
|
545
|
-
expect(log).toContain('run')
|
|
546
|
-
})
|
|
547
|
-
})
|