@pyreon/vue-compat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +309 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +309 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +189 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +481 -0
- package/src/tests/setup.ts +3 -0
- package/src/tests/vue-compat.test.ts +698 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import type { ComponentFn } from "@pyreon/core"
|
|
2
|
+
import { mount } from "@pyreon/runtime-dom"
|
|
3
|
+
import {
|
|
4
|
+
batch,
|
|
5
|
+
computed,
|
|
6
|
+
createApp,
|
|
7
|
+
defineComponent,
|
|
8
|
+
Fragment,
|
|
9
|
+
h,
|
|
10
|
+
inject,
|
|
11
|
+
isRef,
|
|
12
|
+
nextTick,
|
|
13
|
+
onBeforeMount,
|
|
14
|
+
onBeforeUnmount,
|
|
15
|
+
onMounted,
|
|
16
|
+
onUnmounted,
|
|
17
|
+
onUpdated,
|
|
18
|
+
provide,
|
|
19
|
+
reactive,
|
|
20
|
+
readonly,
|
|
21
|
+
ref,
|
|
22
|
+
shallowReactive,
|
|
23
|
+
shallowRef,
|
|
24
|
+
toRaw,
|
|
25
|
+
toRef,
|
|
26
|
+
toRefs,
|
|
27
|
+
triggerRef,
|
|
28
|
+
unref,
|
|
29
|
+
watch,
|
|
30
|
+
watchEffect,
|
|
31
|
+
} from "../index"
|
|
32
|
+
|
|
33
|
+
function container(): HTMLElement {
|
|
34
|
+
const el = document.createElement("div")
|
|
35
|
+
document.body.appendChild(el)
|
|
36
|
+
return el
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("@pyreon/vue-compat", () => {
|
|
40
|
+
// ─── ref ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
it("ref() creates reactive ref with .value", () => {
|
|
43
|
+
const count = ref(0)
|
|
44
|
+
expect(count.value).toBe(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("ref().value setter updates value", () => {
|
|
48
|
+
const count = ref(0)
|
|
49
|
+
count.value = 5
|
|
50
|
+
expect(count.value).toBe(5)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// ─── shallowRef ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
it("shallowRef() creates a ref (same as ref in Pyreon)", () => {
|
|
56
|
+
const r = shallowRef(42)
|
|
57
|
+
expect(r.value).toBe(42)
|
|
58
|
+
expect(isRef(r)).toBe(true)
|
|
59
|
+
r.value = 100
|
|
60
|
+
expect(r.value).toBe(100)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// ─── triggerRef ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
it("triggerRef forces subscribers to re-run", () => {
|
|
66
|
+
const r = ref(0)
|
|
67
|
+
let runs = 0
|
|
68
|
+
|
|
69
|
+
const stop = watchEffect(() => {
|
|
70
|
+
void r.value
|
|
71
|
+
runs++
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(runs).toBe(1)
|
|
75
|
+
triggerRef(r)
|
|
76
|
+
expect(runs).toBe(3) // set undefined then set back = 2 triggers
|
|
77
|
+
stop()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// ─── isRef ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
it("isRef() detects refs", () => {
|
|
83
|
+
const r = ref(0)
|
|
84
|
+
expect(isRef(r)).toBe(true)
|
|
85
|
+
expect(isRef(0)).toBe(false)
|
|
86
|
+
expect(isRef({ value: 0 })).toBe(false)
|
|
87
|
+
expect(isRef(null)).toBe(false)
|
|
88
|
+
|
|
89
|
+
const c = computed(() => 42)
|
|
90
|
+
expect(isRef(c)).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ─── unref ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
it("unref() unwraps refs", () => {
|
|
96
|
+
const r = ref(42)
|
|
97
|
+
expect(unref(r)).toBe(42)
|
|
98
|
+
expect(unref(99)).toBe(99)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ─── computed ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
it("computed() derives from ref", () => {
|
|
104
|
+
const count = ref(2)
|
|
105
|
+
const doubled = computed(() => count.value * 2)
|
|
106
|
+
expect(doubled.value).toBe(4)
|
|
107
|
+
|
|
108
|
+
count.value = 10
|
|
109
|
+
expect(doubled.value).toBe(20)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("computed().value is readonly", () => {
|
|
113
|
+
const c = computed(() => 42)
|
|
114
|
+
expect(() => {
|
|
115
|
+
;(c as { value: number }).value = 99
|
|
116
|
+
}).toThrow("readonly")
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ─── reactive ──────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
it("reactive() creates deep reactive object", () => {
|
|
122
|
+
const state = reactive({ count: 0, nested: { value: "hello" } })
|
|
123
|
+
const values: number[] = []
|
|
124
|
+
|
|
125
|
+
const stop = watchEffect(() => {
|
|
126
|
+
values.push(state.count)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
state.count = 1
|
|
130
|
+
state.count = 2
|
|
131
|
+
|
|
132
|
+
expect(values).toEqual([0, 1, 2])
|
|
133
|
+
stop()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// ─── shallowReactive ──────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("shallowReactive() creates reactive object (same as reactive)", () => {
|
|
139
|
+
const state = shallowReactive({ count: 0 })
|
|
140
|
+
const values: number[] = []
|
|
141
|
+
|
|
142
|
+
const stop = watchEffect(() => {
|
|
143
|
+
values.push(state.count)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
state.count = 5
|
|
147
|
+
expect(values).toEqual([0, 5])
|
|
148
|
+
stop()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ─── readonly ──────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
it("readonly() prevents mutations", () => {
|
|
154
|
+
const obj = readonly({ count: 0 })
|
|
155
|
+
expect(obj.count).toBe(0)
|
|
156
|
+
expect(() => {
|
|
157
|
+
;(obj as { count: number }).count = 5
|
|
158
|
+
}).toThrow("readonly")
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("readonly() prevents delete", () => {
|
|
162
|
+
const obj = readonly({ count: 0 }) as Record<string, unknown>
|
|
163
|
+
expect(() => {
|
|
164
|
+
delete obj.count
|
|
165
|
+
}).toThrow("Cannot delete")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it("readonly() throws on symbol property set", () => {
|
|
169
|
+
const obj = readonly({ count: 0 })
|
|
170
|
+
const sym = Symbol("test")
|
|
171
|
+
// Only internal symbols (V_IS_READONLY, V_RAW) are allowed; all others throw
|
|
172
|
+
expect(() => {
|
|
173
|
+
;(obj as Record<symbol, unknown>)[sym] = "value"
|
|
174
|
+
}).toThrow("readonly")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("readonly() exposes V_IS_READONLY symbol", () => {
|
|
178
|
+
const obj = readonly({ count: 0 })
|
|
179
|
+
// The readonly proxy should have the V_IS_READONLY symbol accessible
|
|
180
|
+
expect(typeof obj).toBe("object")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// ─── toRaw ─────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
it("toRaw() returns raw object for reactive", () => {
|
|
186
|
+
const original = { count: 0 }
|
|
187
|
+
const state = reactive(original)
|
|
188
|
+
const raw = toRaw(state)
|
|
189
|
+
expect(raw).toBe(original)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("toRaw() returns raw object for readonly", () => {
|
|
193
|
+
const original = { count: 0 }
|
|
194
|
+
const ro = readonly(original)
|
|
195
|
+
const raw = toRaw(ro)
|
|
196
|
+
expect(raw).toBe(original)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("toRaw() returns same object for plain object", () => {
|
|
200
|
+
const obj = { a: 1 }
|
|
201
|
+
expect(toRaw(obj)).toBe(obj)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ─── toRef ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
it("toRef() creates ref linked to reactive property", () => {
|
|
207
|
+
const state = reactive({ count: 0 })
|
|
208
|
+
const countRef = toRef(state, "count")
|
|
209
|
+
|
|
210
|
+
expect(isRef(countRef)).toBe(true)
|
|
211
|
+
expect(countRef.value).toBe(0)
|
|
212
|
+
|
|
213
|
+
// Writing through ref updates original
|
|
214
|
+
countRef.value = 10
|
|
215
|
+
expect(state.count).toBe(10)
|
|
216
|
+
|
|
217
|
+
// Writing to original updates ref
|
|
218
|
+
state.count = 20
|
|
219
|
+
expect(countRef.value).toBe(20)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ─── toRefs ────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
it("toRefs() converts reactive to refs", () => {
|
|
225
|
+
const state = reactive({ a: 1, b: "hello" })
|
|
226
|
+
const refs = toRefs(state)
|
|
227
|
+
|
|
228
|
+
expect(isRef(refs.a)).toBe(true)
|
|
229
|
+
expect(refs.a.value).toBe(1)
|
|
230
|
+
expect(refs.b.value).toBe("hello")
|
|
231
|
+
|
|
232
|
+
refs.a.value = 10
|
|
233
|
+
expect(state.a).toBe(10)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// ─── watch ──────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
it("watch() fires on ref change", () => {
|
|
239
|
+
const count = ref(0)
|
|
240
|
+
const calls: number[] = []
|
|
241
|
+
|
|
242
|
+
const stop = watch(count, (newVal) => {
|
|
243
|
+
calls.push(newVal)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
count.value = 1
|
|
247
|
+
count.value = 2
|
|
248
|
+
|
|
249
|
+
expect(calls).toEqual([1, 2])
|
|
250
|
+
stop()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("watch() provides old and new values", () => {
|
|
254
|
+
const count = ref(10)
|
|
255
|
+
const history: [number, number | undefined][] = []
|
|
256
|
+
|
|
257
|
+
const stop = watch(count, (newVal, oldVal) => {
|
|
258
|
+
history.push([newVal, oldVal])
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
count.value = 20
|
|
262
|
+
count.value = 30
|
|
263
|
+
|
|
264
|
+
expect(history).toEqual([
|
|
265
|
+
[20, 10],
|
|
266
|
+
[30, 20],
|
|
267
|
+
])
|
|
268
|
+
stop()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it("watch() with immediate fires synchronously", () => {
|
|
272
|
+
const count = ref(5)
|
|
273
|
+
const calls: [number, number | undefined][] = []
|
|
274
|
+
|
|
275
|
+
const stop = watch(
|
|
276
|
+
count,
|
|
277
|
+
(newVal, oldVal) => {
|
|
278
|
+
calls.push([newVal, oldVal])
|
|
279
|
+
},
|
|
280
|
+
{ immediate: true },
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
expect(calls.length).toBeGreaterThanOrEqual(1)
|
|
284
|
+
expect(calls[0]).toEqual([5, undefined])
|
|
285
|
+
stop()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("watch() with getter function as source", () => {
|
|
289
|
+
const count = ref(0)
|
|
290
|
+
const calls: number[] = []
|
|
291
|
+
|
|
292
|
+
const stop = watch(
|
|
293
|
+
() => count.value * 2,
|
|
294
|
+
(newVal) => {
|
|
295
|
+
calls.push(newVal)
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
count.value = 5
|
|
300
|
+
expect(calls).toContain(10)
|
|
301
|
+
stop()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("watch() stop function disposes watcher", () => {
|
|
305
|
+
const count = ref(0)
|
|
306
|
+
const calls: number[] = []
|
|
307
|
+
|
|
308
|
+
const stop = watch(count, (newVal) => {
|
|
309
|
+
calls.push(newVal)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
count.value = 1
|
|
313
|
+
expect(calls).toEqual([1])
|
|
314
|
+
|
|
315
|
+
stop()
|
|
316
|
+
count.value = 2
|
|
317
|
+
expect(calls).toEqual([1]) // no more updates
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// ─── watchEffect ───────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
it("watchEffect() tracks dependencies", () => {
|
|
323
|
+
const count = ref(0)
|
|
324
|
+
const values: number[] = []
|
|
325
|
+
|
|
326
|
+
const stop = watchEffect(() => {
|
|
327
|
+
values.push(count.value)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
count.value = 1
|
|
331
|
+
count.value = 2
|
|
332
|
+
|
|
333
|
+
expect(values).toEqual([0, 1, 2])
|
|
334
|
+
|
|
335
|
+
stop()
|
|
336
|
+
count.value = 3
|
|
337
|
+
expect(values).toEqual([0, 1, 2])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// ─── nextTick ──────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
it("nextTick() resolves after flush", async () => {
|
|
343
|
+
const count = ref(0)
|
|
344
|
+
count.value = 42
|
|
345
|
+
await nextTick()
|
|
346
|
+
expect(count.value).toBe(42)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// ─── lifecycle ─────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
it("onMounted/onUnmounted lifecycle hooks work", () => {
|
|
352
|
+
const mounted: string[] = []
|
|
353
|
+
const unmounted: string[] = []
|
|
354
|
+
|
|
355
|
+
const Comp = defineComponent({
|
|
356
|
+
name: "TestComp",
|
|
357
|
+
setup() {
|
|
358
|
+
onMounted(() => {
|
|
359
|
+
mounted.push("mounted")
|
|
360
|
+
})
|
|
361
|
+
onUnmounted(() => {
|
|
362
|
+
unmounted.push("unmounted")
|
|
363
|
+
})
|
|
364
|
+
return () => h("div", null, "test")
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const el = container()
|
|
369
|
+
const unmount = mount(h(Comp, null), el)
|
|
370
|
+
|
|
371
|
+
expect(mounted).toEqual(["mounted"])
|
|
372
|
+
expect(unmounted).toEqual([])
|
|
373
|
+
|
|
374
|
+
unmount()
|
|
375
|
+
expect(unmounted).toEqual(["unmounted"])
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it("onBeforeMount works (maps to onMount)", () => {
|
|
379
|
+
const calls: string[] = []
|
|
380
|
+
|
|
381
|
+
const Comp = defineComponent({
|
|
382
|
+
setup() {
|
|
383
|
+
onBeforeMount(() => {
|
|
384
|
+
calls.push("beforeMount")
|
|
385
|
+
})
|
|
386
|
+
return () => h("div", null, "test")
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const el = container()
|
|
391
|
+
const unmount = mount(h(Comp, null), el)
|
|
392
|
+
expect(calls).toEqual(["beforeMount"])
|
|
393
|
+
unmount()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("onBeforeUnmount works (maps to onUnmount)", () => {
|
|
397
|
+
const calls: string[] = []
|
|
398
|
+
|
|
399
|
+
const Comp = defineComponent({
|
|
400
|
+
setup() {
|
|
401
|
+
onBeforeUnmount(() => {
|
|
402
|
+
calls.push("beforeUnmount")
|
|
403
|
+
})
|
|
404
|
+
return () => h("div", null, "test")
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const el = container()
|
|
409
|
+
const unmount = mount(h(Comp, null), el)
|
|
410
|
+
expect(calls).toEqual([])
|
|
411
|
+
unmount()
|
|
412
|
+
expect(calls).toEqual(["beforeUnmount"])
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it("onUpdated is a function", () => {
|
|
416
|
+
expect(typeof onUpdated).toBe("function")
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// ─── provide / inject ─────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
it("provide/inject with string key", () => {
|
|
422
|
+
provide("theme", "dark")
|
|
423
|
+
expect(inject("theme")).toBe("dark")
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it("provide/inject with symbol key", () => {
|
|
427
|
+
const key = Symbol("test-key")
|
|
428
|
+
provide(key, { value: 42 })
|
|
429
|
+
expect((inject(key) as { value: number }).value).toBe(42)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it("inject returns default value when not provided", () => {
|
|
433
|
+
const key = Symbol("missing-key")
|
|
434
|
+
expect(inject(key, "fallback")).toBe("fallback")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("inject returns undefined when not provided and no default", () => {
|
|
438
|
+
const key = Symbol("no-default")
|
|
439
|
+
expect(inject(key)).toBeUndefined()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// ─── defineComponent ──────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
it("defineComponent with setup function returning render fn", () => {
|
|
445
|
+
const Comp = defineComponent({
|
|
446
|
+
name: "TestComp",
|
|
447
|
+
setup() {
|
|
448
|
+
const count = ref(0)
|
|
449
|
+
return () => h("div", null, String(count.value))
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const el = container()
|
|
454
|
+
const unmount = mount(h(Comp, null), el)
|
|
455
|
+
expect(el.textContent).toBe("0")
|
|
456
|
+
unmount()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it("defineComponent with setup returning VNode directly", () => {
|
|
460
|
+
const Comp = defineComponent({
|
|
461
|
+
setup() {
|
|
462
|
+
return h("span", null, "direct")
|
|
463
|
+
},
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const el = container()
|
|
467
|
+
const unmount = mount(h(Comp, null), el)
|
|
468
|
+
expect(el.textContent).toBe("direct")
|
|
469
|
+
unmount()
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it("defineComponent with function shorthand", () => {
|
|
473
|
+
const Comp = defineComponent(() => h("div", null, "shorthand"))
|
|
474
|
+
const el = container()
|
|
475
|
+
const unmount = mount(h(Comp, null), el)
|
|
476
|
+
expect(el.textContent).toBe("shorthand")
|
|
477
|
+
unmount()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("defineComponent with name sets function name", () => {
|
|
481
|
+
const Comp = defineComponent({
|
|
482
|
+
name: "MyComponent",
|
|
483
|
+
setup() {
|
|
484
|
+
return h("div", null, "named")
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
expect(Comp.name).toBe("MyComponent")
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it("defineComponent without name", () => {
|
|
491
|
+
const Comp = defineComponent({
|
|
492
|
+
setup() {
|
|
493
|
+
return h("div", null, "unnamed")
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
expect(typeof Comp).toBe("function")
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
// ─── h / Fragment ─────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
it("h is re-exported", () => {
|
|
502
|
+
expect(typeof h).toBe("function")
|
|
503
|
+
const vnode = h("div", null, "test")
|
|
504
|
+
expect(vnode.type).toBe("div")
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it("Fragment is re-exported", () => {
|
|
508
|
+
expect(typeof Fragment).toBe("symbol")
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// ─── createApp ────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
it("createApp().mount mounts to element", () => {
|
|
514
|
+
const Comp = () => h("div", null, "app")
|
|
515
|
+
const el = container()
|
|
516
|
+
const app = createApp(Comp)
|
|
517
|
+
const unmount = app.mount(el)
|
|
518
|
+
expect(el.textContent).toBe("app")
|
|
519
|
+
unmount()
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it("createApp().mount with string selector", () => {
|
|
523
|
+
const el = container()
|
|
524
|
+
el.id = "test-app-mount"
|
|
525
|
+
document.body.appendChild(el)
|
|
526
|
+
|
|
527
|
+
const Comp = () => h("div", null, "selector-app")
|
|
528
|
+
const app = createApp(Comp)
|
|
529
|
+
const unmount = app.mount("#test-app-mount")
|
|
530
|
+
expect(el.textContent).toBe("selector-app")
|
|
531
|
+
unmount()
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it("createApp().mount throws for missing selector", () => {
|
|
535
|
+
const Comp = () => h("div", null, "app")
|
|
536
|
+
const app = createApp(Comp)
|
|
537
|
+
expect(() => app.mount("#nonexistent-element")).toThrow("Cannot find mount target")
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it("createApp with props passes them to component", () => {
|
|
541
|
+
const Comp = ((props: { name: string }) => h("div", null, props.name)) as ComponentFn
|
|
542
|
+
const el = container()
|
|
543
|
+
const app = createApp(Comp, { name: "world" })
|
|
544
|
+
const unmount = app.mount(el)
|
|
545
|
+
expect(el.textContent).toBe("world")
|
|
546
|
+
unmount()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// ─── batch ────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
it("batch is re-exported and coalesces updates", () => {
|
|
552
|
+
const count = ref(0)
|
|
553
|
+
const values: number[] = []
|
|
554
|
+
|
|
555
|
+
const stop = watchEffect(() => {
|
|
556
|
+
values.push(count.value)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
batch(() => {
|
|
560
|
+
count.value = 1
|
|
561
|
+
count.value = 2
|
|
562
|
+
count.value = 3
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// Should have initial (0) and then final batch result (3)
|
|
566
|
+
expect(values[0]).toBe(0)
|
|
567
|
+
expect(values[values.length - 1]).toBe(3)
|
|
568
|
+
stop()
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// ─── triggerRef edge: no _signal ────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
it("triggerRef is a no-op if ref has no _signal", () => {
|
|
574
|
+
// Create a fake ref without _signal
|
|
575
|
+
const fakeRef = { value: 42 } as unknown as ReturnType<typeof ref>
|
|
576
|
+
// Should not throw
|
|
577
|
+
expect(() => triggerRef(fakeRef)).not.toThrow()
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
// ─── isRef edge cases ──────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
it("isRef returns false for undefined", () => {
|
|
583
|
+
expect(isRef(undefined)).toBe(false)
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
it("isRef returns false for string", () => {
|
|
587
|
+
expect(isRef("hello")).toBe(false)
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
// ─── readonly get V_IS_READONLY ────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
it("readonly proxy reports V_IS_READONLY via symbol", () => {
|
|
593
|
+
const obj = readonly({ count: 0 })
|
|
594
|
+
// Access the internal V_IS_READONLY symbol via a known property read
|
|
595
|
+
// The proxy get trap handles this symbol
|
|
596
|
+
const _V_IS_READONLY = Symbol("__v_isReadonly")
|
|
597
|
+
// We can't access the private symbol directly, but we can verify it doesn't throw
|
|
598
|
+
// when accessing regular properties
|
|
599
|
+
expect(obj.count).toBe(0)
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// ─── readonly get V_RAW ─────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
it("toRaw retrieves raw from readonly proxy", () => {
|
|
605
|
+
const original = { a: 1, b: 2 }
|
|
606
|
+
const ro = readonly(original)
|
|
607
|
+
const raw = toRaw(ro)
|
|
608
|
+
expect(raw).toBe(original)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// ─── watch with immediate + subsequent changes ─────────────────────────
|
|
612
|
+
|
|
613
|
+
it("watch with immediate tracks subsequent changes too", () => {
|
|
614
|
+
const count = ref(0)
|
|
615
|
+
const calls: [number, number | undefined][] = []
|
|
616
|
+
|
|
617
|
+
const stop = watch(
|
|
618
|
+
count,
|
|
619
|
+
(newVal, oldVal) => {
|
|
620
|
+
calls.push([newVal, oldVal])
|
|
621
|
+
},
|
|
622
|
+
{ immediate: true },
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// First call from immediate
|
|
626
|
+
expect(calls[0]).toEqual([0, undefined])
|
|
627
|
+
|
|
628
|
+
count.value = 10
|
|
629
|
+
// Should have the change tracked
|
|
630
|
+
const lastCall = calls[calls.length - 1]!
|
|
631
|
+
expect(lastCall[0]).toBe(10)
|
|
632
|
+
|
|
633
|
+
stop()
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
// ─── watch with getter and immediate ──────────────────────────────────
|
|
637
|
+
|
|
638
|
+
it("watch with getter function and immediate", () => {
|
|
639
|
+
const count = ref(5)
|
|
640
|
+
const calls: [number, number | undefined][] = []
|
|
641
|
+
|
|
642
|
+
const stop = watch(
|
|
643
|
+
() => count.value * 2,
|
|
644
|
+
(newVal, oldVal) => {
|
|
645
|
+
calls.push([newVal, oldVal])
|
|
646
|
+
},
|
|
647
|
+
{ immediate: true },
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
expect(calls[0]).toEqual([10, undefined])
|
|
651
|
+
stop()
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
// ─── createApp with no props ───────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
it("createApp with no props", () => {
|
|
657
|
+
const Comp = () => h("div", null, "no-props")
|
|
658
|
+
const el = container()
|
|
659
|
+
const app = createApp(Comp)
|
|
660
|
+
const unmount = app.mount(el)
|
|
661
|
+
expect(el.textContent).toBe("no-props")
|
|
662
|
+
unmount()
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
// ─── defineComponent setup returning function ──────────────────────────
|
|
666
|
+
|
|
667
|
+
it("defineComponent setup returning VNodeChild (non-function) renders", () => {
|
|
668
|
+
const Comp = defineComponent({
|
|
669
|
+
setup() {
|
|
670
|
+
return h("p", null, "static-vnode")
|
|
671
|
+
},
|
|
672
|
+
})
|
|
673
|
+
const el = container()
|
|
674
|
+
const unmount = mount(h(Comp, null), el)
|
|
675
|
+
expect(el.querySelector("p")?.textContent).toBe("static-vnode")
|
|
676
|
+
unmount()
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// ─── provide overwrite ──────────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
it("provide overwrites previously provided value", () => {
|
|
682
|
+
const key = "overwrite-test"
|
|
683
|
+
provide(key, "first")
|
|
684
|
+
expect(inject(key)).toBe("first")
|
|
685
|
+
provide(key, "second")
|
|
686
|
+
expect(inject(key)).toBe("second")
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// ─── getOrCreateContext reuses existing ─────────────────────────────────
|
|
690
|
+
|
|
691
|
+
it("inject returns provided value for existing string key", () => {
|
|
692
|
+
const key = "reuse-context-test"
|
|
693
|
+
provide(key, 123)
|
|
694
|
+
expect(inject(key)).toBe(123)
|
|
695
|
+
// Call again to ensure it reuses
|
|
696
|
+
expect(inject(key)).toBe(123)
|
|
697
|
+
})
|
|
698
|
+
})
|