@poveste/plugin-vue 0.18.3 → 0.19.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.
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { inferredType, passedValue } from '../auto-props.js'
3
-
4
- /*
5
- * A production build erases the runtime types Vue infers from a type-only
6
- * `defineProps`, so the declaration says nothing and the value has to (#490).
7
- */
8
- describe('the type a value implies', () => {
9
- it.each([
10
- ['a string', 'hello', 'string'],
11
- ['a number', 42, 'number'],
12
- ['zero', 0, 'number'],
13
- ['a boolean', false, 'boolean'],
14
- ['an array', [1, 2], 'array'],
15
- ['an object', { a: 1 }, 'object'],
16
- ])('reads %s', (_, value, expected) => {
17
- expect(inferredType(value)).toBe(expected)
18
- })
19
-
20
- // Nothing to go on, and guessing would pick a control the prop cannot hold.
21
- // A factory default arrives here as the function it is, which is why nothing
22
- // upstream has to strip it first.
23
- it.each([
24
- ['null', null],
25
- ['undefined', undefined],
26
- ['a factory default', () => []],
27
- ])('declines to guess from %s', (_, value) => {
28
- expect(inferredType(value)).toBe('unknown')
29
- })
30
- })
31
-
32
- describe('the value a vnode passed', () => {
33
- it('finds a prop under the name it was declared with', () => {
34
- expect(passedValue({ props: { label: 'x' } }, 'label')).toBe('x')
35
- })
36
-
37
- // `vnode.type.props` is keyed as declared, `vnode.props` as authored.
38
- it('finds a multi-word prop written in kebab case', () => {
39
- expect(passedValue({ props: { 'my-label': 'x' } }, 'myLabel')).toBe('x')
40
- })
41
-
42
- it('prefers the declared spelling when both are present', () => {
43
- expect(passedValue({ props: { 'myLabel': 'camel', 'my-label': 'kebab' } }, 'myLabel')).toBe('camel')
44
- })
45
-
46
- it('is undefined when the component was given no props at all', () => {
47
- expect(passedValue({}, 'label')).toBeUndefined()
48
- })
49
- })
@@ -1,76 +0,0 @@
1
- import type { Story as StoryData, Variant as VariantData } from '@poveste/shared'
2
- import type { Component } from 'vue'
3
- import { describe, expect, it } from 'vitest'
4
- import { createSSRApp, defineComponent, h, reactive } from 'vue'
5
- import { renderToString } from 'vue/server-renderer'
6
- import { provideRenderContext } from '../render-context.js'
7
- import Story from '../Story.js'
8
- import Variant from '../Variant.js'
9
-
10
- const COUNT = 50
11
-
12
- function storyData(): StoryData {
13
- const variants = Array.from({ length: COUNT }, (_, i) => ({ id: `v${i + 1}`, title: `Variant ${i + 1}`, state: {} })) as VariantData[]
14
- // `hasVariantChildComponents` is what every explicit <Variant> sets as it renders,
15
- // so this is the story as a retarget finds it: past its first render.
16
- return reactive({ id: 'grid', title: 'Grid', variants, meta: { hasVariantChildComponents: true } }) as unknown as StoryData
17
- }
18
-
19
- /** Renders `storyComponent` for `target` in a render context, and counts the Variant components created. */
20
- async function renderPreview(storyComponent: Component, story: StoryData, target: string) {
21
- let variantsCreated = 0
22
- const currentVariant = story.variants.find(variant => variant.id === target) ?? null
23
- const app = createSSRApp(defineComponent({
24
- setup() {
25
- provideRenderContext(reactive({ mode: 'render', slotName: 'default', currentVariant, externalState: {}, nextVariantIndex: { value: 0 } }))
26
- return () => h(storyComponent, { story })
27
- },
28
- }))
29
- app.mixin({
30
- beforeCreate() {
31
- if (this.$options.name === 'Variant') {
32
- variantsCreated++
33
- }
34
- },
35
- })
36
- const html = await renderToString(app)
37
- return { html, variantsCreated }
38
- }
39
-
40
- const button = (n: number) => h('button', `Button ${n}`)
41
-
42
- describe('a preview render of one variant', () => {
43
- // #873: gated on `hasVariantChildComponents`, the filter below never ran after a
44
- // story's first render, and every retarget rendered all N Variant components.
45
- it('creates only the target Variant when every variant is a direct child', async () => {
46
- const DirectStory = defineComponent({
47
- props: { story: { type: Object, required: true } },
48
- setup: props => () => h(Story, { story: props.story }, {
49
- default: () => Array.from({ length: COUNT }, (_, i) => h(Variant, { key: i }, { default: () => button(i + 1) })),
50
- }),
51
- })
52
-
53
- const { html, variantsCreated } = await renderPreview(DirectStory, storyData(), 'v37')
54
-
55
- expect(variantsCreated).toBe(1)
56
- expect(html).toContain('Button 37')
57
- expect(html).not.toContain('Button 36<')
58
- })
59
-
60
- // A walk of the slot's vnodes cannot see a <Variant> inside a child component,
61
- // so positions would not line up with `story.variants`, and every one renders.
62
- it('still renders every Variant when they live in a child component', async () => {
63
- const Rows = defineComponent({
64
- setup: () => () => Array.from({ length: COUNT }, (_, i) => h(Variant, { key: i }, { default: () => button(i + 1) })),
65
- })
66
- const WrappedStory = defineComponent({
67
- props: { story: { type: Object, required: true } },
68
- setup: props => () => h(Story, { story: props.story }, { default: () => h(Rows) }),
69
- })
70
-
71
- const { html, variantsCreated } = await renderPreview(WrappedStory, storyData(), 'v37')
72
-
73
- expect(variantsCreated).toBe(COUNT)
74
- expect(html).toContain('Button 37')
75
- })
76
- })
@@ -1,498 +0,0 @@
1
- import { autoPropsStateKeys } from '@poveste/shared'
2
- import { nextTick as bundledNextTick, reactive as bundledReactive, ref as bundledRef } from '@poveste/vendors/vue'
3
- import { describe, expect, it } from 'vitest'
4
- import { markRaw, nextTick, reactive, ref, watch } from 'vue'
5
- import { syncStateBundledAndExternal } from '../util.js'
6
-
7
- // These run against both Vue copies for real: `@poveste/vendors/vue` is the one
8
- // poveste bundles, `vue` is the one a user's story would run on. That is the
9
- // whole point — the sync exists because the two have separate reactivity graphs
10
- // and separate schedulers, and nothing about it can be tested with one of them.
11
-
12
- // Each side flushes on its own scheduler and a write can bounce between them a
13
- // few times before settling, so drain both alternately rather than once.
14
- async function settle() {
15
- for (let i = 0; i < 6; i++) {
16
- await nextTick()
17
- await bundledNextTick()
18
- }
19
- }
20
-
21
- function setup(initial: Record<string, any>, omit: string[] = []) {
22
- const bundled = bundledReactive(structuredClone(initial))
23
- const external = reactive(structuredClone(initial))
24
- const sync = syncStateBundledAndExternal(bundled, external, omit)
25
- return { bundled, external, sync }
26
- }
27
-
28
- describe('syncStateBundledAndExternal', () => {
29
- it('mirrors an edit in each direction', async () => {
30
- const { bundled, external, sync } = setup({ count: 0 })
31
- await settle()
32
-
33
- external.count = 1
34
- await settle()
35
- expect(bundled.count).toBe(1)
36
-
37
- bundled.count = 2
38
- await settle()
39
- expect(external.count).toBe(2)
40
-
41
- sync.stop()
42
- })
43
-
44
- // Each side's walker recurses with its own copy's `isRef`, and the bundled
45
- // one used to hand its whole subtree to the external walker — so below the
46
- // first level, bundled state was walked by the wrong copy of Vue. Nothing
47
- // observable came of it, because `isRef` reads `__v_isRef` rather than an
48
- // identity and either copy answers for the other's ref; this pins the
49
- // recursion either way, which is coverage the bridge did not have.
50
- it('unwraps a ref nested below the first level, on both sides', async () => {
51
- const { bundled, external, sync } = setup({ count: 0 })
52
- await settle()
53
-
54
- bundled.deep = { inner: { value: bundledRef('from bundled') } }
55
- await settle()
56
- expect(external.deep.inner.value).toBe('from bundled')
57
-
58
- external.other = { inner: { value: ref('from external') } }
59
- await settle()
60
- expect(bundled.other.inner.value).toBe('from external')
61
-
62
- sync.stop()
63
- })
64
-
65
- // Both walkers rebuilt every object from its own enumerable keys, so a `Date`
66
- // crossed as `{}` — and the baseline's `isEquivalent`, which refuses to
67
- // compare anything that is not plain precisely so two distinct `Date`s are
68
- // never called equal, was handed two empty plain objects instead (#977).
69
- it('carries a Date across, rather than emptying it', async () => {
70
- const { bundled, external, sync } = setup({ count: 0 })
71
- await settle()
72
-
73
- external.at = new Date(1790071200000)
74
- await settle()
75
-
76
- expect(bundled.at).toBeInstanceOf(Date)
77
- expect(bundled.at.getTime()).toBe(1790071200000)
78
-
79
- sync.stop()
80
- })
81
-
82
- it('carries a Map and a class instance the same way', async () => {
83
- class Point {
84
- constructor(public x: number, public y: number) {}
85
- get len() { return Math.hypot(this.x, this.y) }
86
- }
87
- const { bundled, external, sync } = setup({ count: 0 })
88
- await settle()
89
-
90
- external.m = new Map([['a', 1]])
91
- external.p = new Point(3, 4)
92
- await settle()
93
-
94
- expect(bundled.m).toBeInstanceOf(Map)
95
- expect(bundled.m.get('a')).toBe(1)
96
- // In one realm the reference crosses, so the prototype survives too — which
97
- // the sandbox bridge cannot promise and does not (#977).
98
- expect(bundled.p.len).toBe(5)
99
-
100
- sync.stop()
101
- })
102
-
103
- // The half the walker was destroying upstream of: two different dates must
104
- // not settle as equivalent, or an edit is dropped rather than mirrored.
105
- it('mirrors a change from one Date to another', async () => {
106
- const { bundled, external, sync } = setup({ count: 0 })
107
- await settle()
108
-
109
- external.at = new Date(1790071200000)
110
- await settle()
111
-
112
- external.at = new Date(0)
113
- await settle()
114
-
115
- expect(bundled.at.getTime()).toBe(0)
116
-
117
- sync.stop()
118
- })
119
-
120
- it('stops mirroring once stopped', async () => {
121
- const { bundled, external, sync } = setup({ count: 0 })
122
- await settle()
123
- sync.stop()
124
-
125
- external.count = 1
126
- await settle()
127
- expect(bundled.count).toBe(0)
128
- })
129
-
130
- // The #95 regression. `applyState` iterates the incoming keys, so it has no
131
- // way to express "this key is gone" — the write it performs for a removal
132
- // changes nothing on the far side, and the far side's watcher never fires.
133
- //
134
- // That matters because the two watchers coordinate through a `syncing` boolean
135
- // meaning "ignore the next firing, it is my own echo", and the counterpart
136
- // firing is what clears it. No counterpart, no clear: the flag stayed set, and
137
- // the next edit was mistaken for an echo and dropped on the floor.
138
- //
139
- // Worth being exact about how bad this was in the shipped app, because it is
140
- // the reason the issue had no repro: it never bit here. Vue stories always
141
- // carry `_hPropState` and `_hPropDefs`, and `applyState` sent every
142
- // `_h`-prefixed key down the plain-assignment branch, so those two were
143
- // re-identified on every apply and a counterpart firing was guaranteed. The
144
- // invariant held by luck. These tests use a state without that bookkeeping,
145
- // which is what the function actually promises to handle — and which is the
146
- // shape the sandbox bridge and `plugin-svelte` were relying on luck for too.
147
- it('does not drop the edit after a removal it cannot mirror', async () => {
148
- const { bundled, external, sync } = setup({ count: 0, gone: 1 })
149
- await settle()
150
-
151
- delete external.gone
152
- await settle()
153
-
154
- external.count = 1
155
- await settle()
156
- expect(bundled.count).toBe(1)
157
-
158
- sync.stop()
159
- })
160
-
161
- it('does not drop the edit after a nested removal it cannot mirror', async () => {
162
- // The same shape one level down, and the more plausible one: a story that
163
- // drops an entry out of a map in state, then touches a control.
164
- const { bundled, external, sync } = setup({ count: 0, items: { a: 1, b: 2 } })
165
- await settle()
166
-
167
- delete external.items.b
168
- await settle()
169
-
170
- external.count = 1
171
- await settle()
172
- expect(bundled.count).toBe(1)
173
-
174
- sync.stop()
175
- })
176
-
177
- it('does not drop the edit after a change that nets out to nothing', async () => {
178
- const { bundled, external, sync } = setup({ count: 0, list: [1, 2] })
179
- await settle()
180
-
181
- external.list.push(9)
182
- external.list.pop()
183
- await settle()
184
-
185
- external.count = 1
186
- await settle()
187
- expect(bundled.count).toBe(1)
188
-
189
- sync.stop()
190
- })
191
-
192
- it('leaves object identity alone on both sides when a sync changes nothing', async () => {
193
- // The other half of the fix, and the reason the flag can now be trusted.
194
- // `toRawDeep` rebuilds every object it passes, so mirroring `count` used to
195
- // drop a fresh `nested` and `list` onto the receiving side as well, and each
196
- // of those re-triggered its watchers. Every sync was a burst of firings that
197
- // happened to keep the count even; none of them meant anything.
198
- const { bundled, external, sync } = setup({ count: 0, nested: { a: 1 }, list: [1, 2] })
199
- await settle()
200
-
201
- const externalNested = external.nested
202
- const externalList = external.list
203
- const bundledNested = bundled.nested
204
- const bundledList = bundled.list
205
-
206
- external.count = 1
207
- await settle()
208
-
209
- expect(bundled.count).toBe(1)
210
- expect(external.nested).toBe(externalNested)
211
- expect(external.list).toBe(externalList)
212
- // The receiving side is the one that used to get churned.
213
- expect(bundled.nested).toBe(bundledNested)
214
- expect(bundled.list).toBe(bundledList)
215
-
216
- sync.stop()
217
- })
218
-
219
- // The #96 regression, and the reason the `syncing` flag is gone rather than
220
- // repaired. Both watchers mirrored the whole of their side, so the second one
221
- // to fire carried its own edit *and* its copy of the key the first side had
222
- // just changed — one edit stale, because the mirror had not reached it yet.
223
- // Last writer won, and it won across every key rather than just its own.
224
- it('keeps both edits when the two sides change in the same tick', async () => {
225
- const { bundled, external, sync } = setup({ a: 0, b: 0 })
226
- await settle()
227
-
228
- external.a = 1
229
- bundled.b = 1
230
- await settle()
231
-
232
- expect({ a: external.a, b: external.b }).toEqual({ a: 1, b: 1 })
233
- expect({ a: bundled.a, b: bundled.b }).toEqual({ a: 1, b: 1 })
234
-
235
- sync.stop()
236
- })
237
-
238
- it('keeps both edits when the two sides change in the same tick, bundled first', async () => {
239
- // Which watcher happens to flush first decided who lost, so pin both orders.
240
- const { bundled, external, sync } = setup({ a: 0, b: 0 })
241
- await settle()
242
-
243
- bundled.b = 1
244
- external.a = 1
245
- await settle()
246
-
247
- expect({ a: external.a, b: external.b }).toEqual({ a: 1, b: 1 })
248
- expect({ a: bundled.a, b: bundled.b }).toEqual({ a: 1, b: 1 })
249
-
250
- sync.stop()
251
- })
252
-
253
- it('keeps both edits when they land in different keys of the same object', async () => {
254
- // The nested form: two entries of one map, which whole-object mirroring
255
- // could not separate even in principle.
256
- const { bundled, external, sync } = setup({ items: { a: 0, b: 0 } })
257
- await settle()
258
-
259
- external.items.a = 1
260
- bundled.items.b = 1
261
- await settle()
262
-
263
- expect(external.items).toEqual({ a: 1, b: 1 })
264
- expect(bundled.items).toEqual({ a: 1, b: 1 })
265
-
266
- sync.stop()
267
- })
268
-
269
- it('keeps a concurrent edit made while the other side is mid-flight', async () => {
270
- // Not the same tick, and it passed before the fix too — kept because it
271
- // pins the interleaving the flag was most delicate about: the far side edits
272
- // after a mirror has landed but before its echo has been observed.
273
- const { bundled, external, sync } = setup({ a: 0, b: 0 })
274
- await settle()
275
-
276
- external.a = 1
277
- await nextTick()
278
- bundled.b = 1
279
- await settle()
280
-
281
- expect({ a: external.a, b: external.b }).toEqual({ a: 1, b: 1 })
282
- expect({ a: bundled.a, b: bundled.b }).toEqual({ a: 1, b: 1 })
283
-
284
- sync.stop()
285
- })
286
-
287
- it('mirrors an edit made to a key the other side removed', async () => {
288
- // A removal cannot cross, so the two sides disagree about that key from then
289
- // on. The baseline must not mistake that standing disagreement for a change
290
- // and start replaying it — nor let it mask the next real edit.
291
- const { bundled, external, sync } = setup({ count: 0, gone: 1 })
292
- await settle()
293
-
294
- delete external.gone
295
- await settle()
296
-
297
- bundled.count = 5
298
- await settle()
299
- expect(external.count).toBe(5)
300
-
301
- external.count = 6
302
- await settle()
303
- expect(bundled.count).toBe(6)
304
-
305
- sync.stop()
306
- })
307
-
308
- it('does not let a mirrored array alias the baseline', async () => {
309
- // An array always crosses whole — `applyState` assigns it rather than
310
- // merging — so the receiving side ends up holding the very object the
311
- // baseline recorded. Reactive writes go through to the raw target, so
312
- // without a copy the next `push` lands in the baseline as well, and the sync
313
- // reads the edit back as already agreed and drops it.
314
- const { bundled, external, sync } = setup({ list: [1, 2] })
315
- await settle()
316
-
317
- external.list = [1, 2, 3]
318
- await settle()
319
- expect(bundled.list).toEqual([1, 2, 3])
320
-
321
- bundled.list.push(4)
322
- await settle()
323
- expect(external.list).toEqual([1, 2, 3, 4])
324
-
325
- sync.stop()
326
- })
327
-
328
- it('does not let a mirrored object alias the baseline', async () => {
329
- // Same hazard by the other route into the wholesale branch: a key the far
330
- // side does not have yet is assigned rather than merged.
331
- const { bundled, external, sync } = setup({ count: 0 })
332
- await settle()
333
-
334
- external.added = { a: 1 }
335
- await settle()
336
- expect(bundled.added).toEqual({ a: 1 })
337
-
338
- bundled.added.a = 2
339
- await settle()
340
- expect(external.added.a).toBe(2)
341
-
342
- sync.stop()
343
- })
344
-
345
- it('mirrors a write the story makes in reaction to a control edit', async () => {
346
- // The realistic form of #96, and the one a book actually hits: a story that
347
- // derives one state key from another. Editing `a` in the controls panel is
348
- // one edit, but the story answering it with `b` is a second, and it lands
349
- // while the first is still in flight — so the two sides are changing at the
350
- // same time without anyone having to do anything unusual.
351
- //
352
- // The story's watcher is installed before the sync here, and that ordering
353
- // is the whole of it: Vue flushes by creation order, so the story's write
354
- // lands before the sync watcher runs and rides in on the very firing the
355
- // flag was waiting to discard. Install the sync first and the same code
356
- // survived, which is why this never showed up as a reliable bug.
357
- //
358
- // Both orders occur. `Variant` attaches its sync in `setup()`, before the
359
- // story mounts; `RenderStory` tears the sync down and re-attaches it when
360
- // the variant changes, by which time the story's watchers are long since
361
- // registered. A flag whose correctness turns on which of those you got is
362
- // not correct, it is lucky.
363
- const bundled = bundledReactive({ a: 0, b: 0 })
364
- const external = reactive({ a: 0, b: 0 })
365
-
366
- const stop = watch(() => external.a, (value) => {
367
- external.b = value * 2
368
- })
369
- const sync = syncStateBundledAndExternal(bundled, external)
370
- await settle()
371
-
372
- bundled.a = 1
373
- await settle()
374
-
375
- expect(external.b).toBe(2)
376
- expect(bundled.b).toBe(2)
377
-
378
- stop()
379
- sync.stop()
380
- })
381
-
382
- it('mirrors only what moved without taking the neighbouring keys with it', async () => {
383
- // Sending less than the whole object is what stops the two sides clobbering
384
- // each other, and it is only safe as far as `applyState` merges — one level,
385
- // and never for an `_h` key, which it replaces outright for the sandbox's
386
- // sake. Narrow past either and the subset lands *as* the object.
387
- //
388
- // Every Vue story carries `_hPropState`, so getting that wrong empties the
389
- // auto-props of every story in the book rather than failing somewhere quiet.
390
- const initial = { _hPropState: { a: 1, b: 2 }, deep: { inner: { x: 1, y: 2 } } }
391
- const bundled = bundledReactive(structuredClone(initial))
392
- const external = reactive(structuredClone(initial))
393
- const sync = syncStateBundledAndExternal(bundled, external)
394
- await settle()
395
-
396
- external._hPropState.a = 9
397
- external.deep.inner.x = 9
398
- await settle()
399
-
400
- expect(bundled._hPropState).toEqual({ a: 9, b: 2 })
401
- expect(bundled.deep.inner).toEqual({ x: 9, y: 2 })
402
-
403
- sync.stop()
404
- })
405
-
406
- it('mirrors many edits in a row without falling behind', async () => {
407
- const { bundled, external, sync } = setup({ count: 0 })
408
- await settle()
409
-
410
- for (let i = 1; i <= 10; i++) {
411
- external.count = i
412
- await settle()
413
- expect(bundled.count).toBe(i)
414
- }
415
-
416
- sync.stop()
417
- })
418
- })
419
-
420
- /*
421
- * A variant's state is synced with the story's implicit state, which every
422
- * variant shares — so anything that crosses reaches all of them. `_hPropState`
423
- * is keyed by a component's index within one variant, so it must not (#473).
424
- */
425
- describe('keys the sync is told to omit', () => {
426
- it('does not carry an auto-props edit to the other side', async () => {
427
- const { bundled, external, sync } = setup({ _hPropState: {}, label: 'a' }, autoPropsStateKeys)
428
- await settle()
429
-
430
- external._hPropState = { 0: { name: 'Bender' } }
431
- await settle()
432
-
433
- expect(bundled._hPropState).toEqual({})
434
-
435
- sync.stop()
436
- })
437
-
438
- it('goes on carrying everything else', async () => {
439
- const { bundled, external, sync } = setup({ _hPropState: {}, label: 'a' }, autoPropsStateKeys)
440
- await settle()
441
-
442
- external.label = 'edited'
443
- await settle()
444
-
445
- expect(bundled.label).toBe('edited')
446
-
447
- sync.stop()
448
- })
449
-
450
- // The reason the sync exists: a variant with no `initState` picks up the
451
- // story's own state, and omitting the bookkeeping must not cost that.
452
- it('still carries a key the story owns', async () => {
453
- const { bundled, external, sync } = setup({}, autoPropsStateKeys)
454
- await settle()
455
-
456
- bundled.fromStory = 'yes'
457
- await settle()
458
-
459
- expect(external.fromStory).toBe('yes')
460
-
461
- sync.stop()
462
- })
463
- })
464
-
465
- describe('a binding Vue has marked raw', () => {
466
- // Not `setup()` above: that clones its argument, and `structuredClone` is
467
- // exactly what a mark does not survive.
468
- function setupWithGraph() {
469
- const graph = markRaw({ nodes: { deep: 1 } })
470
- const bundled = bundledReactive<Record<string, any>>({ text: '', graph })
471
- const external = reactive<Record<string, any>>({ text: '' })
472
- const sync = syncStateBundledAndExternal(bundled, external)
473
- return { bundled, external, sync, graph }
474
- }
475
-
476
- it('crosses between the two Vues as itself, not as a rebuilt copy', async () => {
477
- const { external, sync, graph } = setupWithGraph()
478
- await settle()
479
-
480
- // Both sides live in one realm, so the reference is the useful thing to
481
- // hand over — and handing it over is what lets the compare settle on
482
- // identity instead of walking whatever is behind it (#957).
483
- expect(external.graph).toBe(graph)
484
- sync.stop()
485
- })
486
-
487
- it('stays that object over later passes, and does not hold up the keys beside it', async () => {
488
- const { bundled, external, sync, graph } = setupWithGraph()
489
- await settle()
490
-
491
- bundled.text = 'a'
492
- await settle()
493
-
494
- expect(external.graph).toBe(graph)
495
- expect(external.text).toBe('a')
496
- sync.stop()
497
- })
498
- })
@@ -1,39 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { computed, readonly, ref, shallowRef, useTemplateRef } from 'vue'
3
- import { isWritableBinding } from '../util.js'
4
-
5
- // `useTemplateRef` returns a readonly ref in a dev build and a writable one in a
6
- // production build, so this file is the only place the defect in #959 can be
7
- // caught: vitest runs the dev build, and the e2e suites serve built books where
8
- // the bug does not exist.
9
-
10
- describe('isWritableBinding', () => {
11
- it('rejects the ref useTemplateRef returns, which is what loops the sync', () => {
12
- expect(isWritableBinding(useTemplateRef('el'))).toBe(false)
13
- })
14
-
15
- it('rejects a readonly ref however it was made', () => {
16
- expect(isWritableBinding(readonly(ref(0)))).toBe(false)
17
- expect(isWritableBinding(readonly(shallowRef(null)))).toBe(false)
18
- })
19
-
20
- // Same rule, and the reason is the same: a control that cannot write is not a
21
- // control. Called out because it is a behaviour change beyond the defect.
22
- it('rejects a computed, which could never take the write either', () => {
23
- expect(isWritableBinding(computed(() => 1))).toBe(false)
24
- })
25
-
26
- // The shape every existing story uses for a template ref, which has to keep
27
- // working — a story that writes its own state through one still syncs.
28
- it('accepts the pre-3.5 ref(null) form', () => {
29
- expect(isWritableBinding(ref(null))).toBe(true)
30
- expect(isWritableBinding(shallowRef(null))).toBe(true)
31
- })
32
-
33
- it('accepts ordinary bindings, ref and plain alike', () => {
34
- expect(isWritableBinding(ref(0))).toBe(true)
35
- expect(isWritableBinding({ a: 1 })).toBe(true)
36
- expect(isWritableBinding('text')).toBe(true)
37
- expect(isWritableBinding(undefined)).toBe(true)
38
- })
39
- })