child: {{ props.y }}
` + + // `x` goes away while Child (or a component around it) shows its `y`: + // neither mode may see the watcher fire or the read throw + async function expectGuardedRead(srcs: Recordx is gone
+ `, + })) + + test('v-for row created after the branch rendered', async () => { + const seen: Recordx is gone
+ `, + }, + () => refx is gone
+ `, + })) + + test('source fixed again within the same tick', async () => { + const seen: Recordx is gone
+ `, + }, + () => refx is gone
+ `, + }, + () => refchild: {{ y }}
`, + App: ` +x is gone
+ `, + }, + () => refchild: {{ props.y }}
`, + App: ` +x is gone
+ `, + })) + + test('slot content guarded by the slot owner', () => + expectGuardedRead({ + Wrapper: `x is gone
+ `, + })) + + test('slot content guarded inside the slot host', () => + expectGuardedRead({ + Wrapper: ` +x is gone
+ `, + App: ` +child: {{ model }}
`, + App: ` +x is gone
+ `, + }, + () => refb: {{ y }}
`, + App: ` +x is gone
+ `, + data, + components, + { vapor: false }, + ) + const root = document.createElement('div') + const app = createApp(App) + app.use(vaporInteropPlugin).mount(root) + expect(root.textContent).toBe('child: a') + data.value.x = undefined + await nextTick() + expect(data.value.seen).toEqual([]) + expect(root.textContent).toBe('x is gone') + app.unmount() + }) + + test('vdom child with a sync watcher under a vapor v-if', async () => { + const data = refx is gone
+ `, + data, + components, + { vapor: true }, + ) + const root = document.createElement('div') + const app = createVaporApp(App) + app.use(vaporInteropPlugin).mount(root) + expect(root.textContent).toBe('child: a') + data.value.x = undefined + await nextTick() + expect(data.value.seen).toEqual([]) + expect(root.textContent).toBe('x is gone') + app.unmount() + }) + + // #15228: deactivation freezes parent inputs while local effects stay live. + test('kept-alive child with a sync watcher', async () => { + const seen: Recordx is gone
+x is gone
+ `, + }, + () => refchild: {{ y }}
`, + App: ` +x is gone
+ `, + }, + () => ref{{ y.value }}
`, + App: ` +{{ props.y[0] }}:{{ count }}
`, + App: `gone
+ `, + }, + { show: true }, + true, + )) + + test('dynamic slot name read by a sync watcher', () => + expectGuarded( + { + Child: Child('Object.keys(slots).join()'), + App: ` +gone
+ `, + }, + { name: 'foo' }, + 'foo', + )) + + test('v-for slots read by a sync watcher', () => + expectGuarded( + { + Child: Child('Object.keys(slots).join()'), + App: ` +gone
+ `, + }, + { names: ['foo', 'bar'] }, + 'foo,bar', + )) + + test('vdom child under a vapor parent', async () => { + const data = refgone
+ `, + data, + components, + { vapor: true }, + ) + const root = document.createElement('div') + const app = createVaporApp(App) + app.use(vaporInteropPlugin).mount(root) + expect(root.textContent).toBe('foo') + data.value.x = undefined + expect(data.value.seen).toEqual([true]) + await nextTick() + expect(root.textContent).toBe('gone') + app.unmount() + }) + + // vdom slots are not reactive, so only the timing is comparable: nothing + // reaches the child before the parent's update + test('a slot toggle reaches the child in the flush', async () => { + const seen: Recordother
')()) + // Compiled branch keys or fallthrough sources could mask no rawProps. + const Parent = defineVaporComponent(() => + createComponent(VaporKeepAlive, null, { + default: () => + createIf( + () => page.value === 0, + () => createComponent(Child, initialRawProps), + () => + createIf( + () => page.value === 1, + () => createComponent(Other), + () => createComponent(Child, { title: () => title.value }), + ), + ), + }), + ) + const { host, app } = define(Parent).render() + const node = host.querySelector('div')! + expect(Object.keys(attrs)).toEqual([]) + expect(node.hasAttribute('title')).toBe(false) + + page.value = 1 + await nextTick() + expect(host.textContent).toBe('other') + page.value = 2 + await nextTick() + expect(setup).toHaveBeenCalledTimes(1) + expect(host.querySelector('div')).toBe(node) + expect(attrs.title).toBe('from-new-call-site') + expect(Object.keys(attrs)).toEqual(['title']) + expect(node.getAttribute('title')).toBe('from-new-call-site') + + title.value = 'updated' + await nextTick() + expect(attrs.title).toBe('updated') + expect(node.getAttribute('title')).toBe('updated') + app.unmount() + }, + ) + }) }) diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index 20271409244..952f5ffbb77 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -305,6 +305,34 @@ describe('defineVaporCustomElement', () => { expect(e.getAttribute('baz-qux')).toBe('four') }) + test('preserves function-valued properties without calling them during delivery', async () => { + const first = vi.fn(() => 'first') + const second = vi.fn(() => 'second') + let readCallback!: () => Function | undefined + const FunctionElement = defineVaporCustomElement({ + props: { callback: Function }, + setup(props) { + readCallback = () => props.callback + return template('{{ props.y.value }}
`, + data, + ), + } + const App = compile( + `{{ props.y.value }}
`, + data, + ), + } + const App = compile( + ` +