-import type { Ref } from '@vue/reactivity'
import {
- EffectScope,
+ currentInstance,
+ effectScope,
nextTick,
- onWatcherCleanup,
+ onMounted,
+ onUpdated,
ref,
+ watch,
watchEffect,
- watchSyncEffect,
-} from '../src'
-
-describe.todo('watchEffect and onWatcherCleanup', () => {
- test('basic', async () => {
- let dummy = 0
- let source: Ref<number>
- const scope = new EffectScope()
-
- scope.run(() => {
- source = ref(0)
- watchEffect(onCleanup => {
- source.value
-
- onCleanup(() => (dummy += 2))
- onWatcherCleanup(() => (dummy += 3))
- onWatcherCleanup(() => (dummy += 5))
+} from '@vue/runtime-dom'
+import { createComponent, defineVaporComponent, renderEffect } from '../src'
+import { makeRender } from './_utils'
+import type { VaporComponentInstance } from '../src/component'
+
+const define = makeRender()
+
+// only need to port test cases related to in-component usage
+describe('apiWatch', () => {
+ // #7030
+ it.todo(
+ // need if support
+ 'should not fire on child component unmount w/ flush: pre',
+ async () => {
+ const visible = ref(true)
+ const cb = vi.fn()
+ const Parent = defineVaporComponent({
+ props: ['visible'],
+ setup() {
+ // @ts-expect-error
+ return visible.value ? h(Comp) : null
+ },
+ })
+ const Comp = {
+ setup() {
+ watch(visible, cb, { flush: 'pre' })
+ return []
+ },
+ }
+ define(Parent).render({
+ visible: () => visible.value,
})
+ expect(cb).not.toHaveBeenCalled()
+ visible.value = false
+ await nextTick()
+ expect(cb).not.toHaveBeenCalled()
+ },
+ )
+
+ // #7030
+ it('flush: pre watcher in child component should not fire before parent update', async () => {
+ const b = ref(0)
+ const calls: string[] = []
+
+ const Comp = {
+ setup() {
+ watch(
+ () => b.value,
+ val => {
+ calls.push('watcher child')
+ },
+ { flush: 'pre' },
+ )
+ renderEffect(() => {
+ b.value
+ calls.push('render child')
+ })
+ return []
+ },
+ }
+
+ const Parent = {
+ props: ['a'],
+ setup() {
+ watch(
+ () => b.value,
+ val => {
+ calls.push('watcher parent')
+ },
+ { flush: 'pre' },
+ )
+ renderEffect(() => {
+ b.value
+ calls.push('render parent')
+ })
+
+ return createComponent(Comp)
+ },
+ }
+
+ define(Parent).render({
+ a: () => b.value,
})
+
+ expect(calls).toEqual(['render parent', 'render child'])
+
+ b.value++
await nextTick()
- expect(dummy).toBe(0)
+ expect(calls).toEqual([
+ 'render parent',
+ 'render child',
+ 'watcher parent',
+ 'render parent',
+ 'watcher child',
+ 'render child',
+ ])
+ })
+
+ // #1763
+ it('flush: pre watcher watching props should fire before child update', async () => {
+ const a = ref(0)
+ const b = ref(0)
+ const c = ref(0)
+ const calls: string[] = []
+
+ const Comp = {
+ props: ['a', 'b'],
+ setup(props: any) {
+ watch(
+ () => props.a + props.b,
+ () => {
+ calls.push('watcher 1')
+ c.value++
+ },
+ { flush: 'pre' },
+ )
- scope.run(() => {
- source.value++
+ // #1777 chained pre-watcher
+ watch(
+ c,
+ () => {
+ calls.push('watcher 2')
+ },
+ { flush: 'pre' },
+ )
+ renderEffect(() => {
+ c.value
+ calls.push('render')
+ })
+ return []
+ },
+ }
+
+ define(Comp).render({
+ a: () => a.value,
+ b: () => b.value,
})
+
+ expect(calls).toEqual(['render'])
+
+ // both props are updated
+ // should trigger pre-flush watcher first and only once
+ // then trigger child render
+ a.value++
+ b.value++
await nextTick()
- expect(dummy).toBe(10)
+ expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
+ })
+
+ // #5721
+ it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
+ const count = ref(0)
+ const calls: string[] = []
+ const App = {
+ setup() {
+ watch(
+ count,
+ () => {
+ calls.push('watch ' + count.value)
+ },
+ { flush: 'pre' },
+ )
+ onMounted(() => {
+ calls.push('mounted')
+ })
+ // mutate multiple times
+ count.value++
+ count.value++
+ count.value++
+ return []
+ },
+ }
+ define(App).render()
+ expect(calls).toMatchObject(['watch 3', 'mounted'])
+ })
+
+ // #1852
+ it.todo(
+ // need if + templateRef
+ 'flush: post watcher should fire after template refs updated',
+ async () => {
+ const toggle = ref(false)
+ let dom: Element | null = null
- scope.run(() => {
- source.value++
+ const App = {
+ setup() {
+ const domRef = ref<Element | null>(null)
+
+ watch(
+ toggle,
+ () => {
+ dom = domRef.value
+ },
+ { flush: 'post' },
+ )
+
+ return () => {
+ // @ts-expect-error
+ return toggle.value ? h('p', { ref: domRef }) : null
+ }
+ },
+ }
+
+ // @ts-expect-error
+ render(h(App), nodeOps.createElement('div'))
+ expect(dom).toBe(null)
+
+ toggle.value = true
+ await nextTick()
+ expect(dom!.tagName).toBe('P')
+ },
+ )
+
+ test('should not leak `this.proxy` to setup()', () => {
+ const source = vi.fn()
+
+ const Comp = defineVaporComponent({
+ setup() {
+ watch(source, () => {})
+ return []
+ },
})
- await nextTick()
- expect(dummy).toBe(20)
- scope.stop()
- await nextTick()
- expect(dummy).toBe(30)
+ define(Comp).render()
+
+ // should not have any arguments
+ expect(source.mock.calls[0]).toMatchObject([])
})
- test('nested call to watchEffect', async () => {
- let dummy = 0
- let source: Ref<number>
- let double: Ref<number>
- const scope = new EffectScope()
-
- scope.run(() => {
- source = ref(0)
- double = ref(0)
- watchEffect(() => {
- double.value = source.value * 2
- onWatcherCleanup(() => (dummy += 2))
- })
- watchSyncEffect(() => {
- double.value
- onWatcherCleanup(() => (dummy += 3))
- })
+ // #2728
+ test('pre watcher callbacks should not track dependencies', async () => {
+ const a = ref(0)
+ const b = ref(0)
+ const updated = vi.fn()
+
+ const Comp = defineVaporComponent({
+ props: ['a'],
+ setup(props) {
+ onUpdated(updated)
+ watch(
+ () => props.a,
+ () => {
+ b.value
+ },
+ )
+ renderEffect(() => {
+ props.a
+ })
+ return []
+ },
})
+
+ define(Comp).render({
+ a: () => a.value,
+ })
+
+ a.value++
await nextTick()
- expect(dummy).toBe(0)
+ expect(updated).toHaveBeenCalledTimes(1)
- scope.run(() => source.value++)
+ b.value++
await nextTick()
- expect(dummy).toBe(5)
+ // should not track b as dependency of Child
+ expect(updated).toHaveBeenCalledTimes(1)
+ })
+
+ // #4158
+ test('watch should not register in owner component if created inside detached scope', () => {
+ let instance: VaporComponentInstance
+ const Comp = {
+ setup() {
+ instance = currentInstance as VaporComponentInstance
+ effectScope(true).run(() => {
+ watch(
+ () => 1,
+ () => {},
+ )
+ })
+ return []
+ },
+ }
+ define(Comp).render()
+ // should not record watcher in detached scope
+ expect(instance!.scope.effects.length).toBe(0)
+ })
- scope.run(() => source.value++)
+ test('watchEffect should keep running if created in a detached scope', async () => {
+ const trigger = ref(0)
+ let countWE = 0
+ let countW = 0
+ const Comp = {
+ setup() {
+ effectScope(true).run(() => {
+ watchEffect(() => {
+ trigger.value
+ countWE++
+ })
+ watch(trigger, () => countW++)
+ })
+ return []
+ },
+ }
+ const { app } = define(Comp).render()
+ // only watchEffect as ran so far
+ expect(countWE).toBe(1)
+ expect(countW).toBe(0)
+ trigger.value++
await nextTick()
- expect(dummy).toBe(10)
+ // both watchers run while component is mounted
+ expect(countWE).toBe(2)
+ expect(countW).toBe(1)
- scope.stop()
+ app.unmount()
+ await nextTick()
+ trigger.value++
await nextTick()
- expect(dummy).toBe(15)
+ // both watchers run again event though component has been unmounted
+ expect(countWE).toBe(3)
+ expect(countW).toBe(2)
})
})