From 7ecdc79d5e7ce79319860eda0dde7024d3f3dcd2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 27 Aug 2019 11:35:22 -0400 Subject: [PATCH] test: finish tests for watch api --- packages/reactivity/__tests__/effect.spec.ts | 2 +- packages/reactivity/src/index.ts | 1 + .../runtime-core/__tests__/apiWatch.spec.ts | 236 +++++++++++++++++- packages/runtime-core/src/apiWatch.ts | 16 +- 4 files changed, 245 insertions(+), 10 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index ce8908e26b..eb0e7f4b38 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -531,7 +531,7 @@ describe('reactivity/effect', () => { }) it('events: onTrack', () => { - let events: any[] = [] + let events: DebuggerEvent[] = [] let dummy const onTrack = jest.fn((e: DebuggerEvent) => { events.push(e) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 0ba19b5154..5b9824b801 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -12,6 +12,7 @@ export { computed, ComputedRef, ComputedOptions } from './computed' export { effect, stop, + ITERATE_KEY, ReactiveEffect, ReactiveEffectOptions, DebuggerEvent diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index c3f9c7d22d..579dbf8e40 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1,4 +1,14 @@ -import { watch, reactive, computed, nextTick, ref } from '../src/index' +import { + watch, + reactive, + computed, + nextTick, + ref, + h, + OperationTypes +} from '../src/index' +import { render, nodeOps, serializeInner } from '@vue/runtime-test' +import { ITERATE_KEY, DebuggerEvent } from '@vue/reactivity' // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch @@ -137,17 +147,227 @@ describe('api: watch', () => { expect(cleanup).toHaveBeenCalledTimes(2) }) - it('flush timing: post', () => {}) + it('flush timing: post', async () => { + const count = ref(0) + const assertion = jest.fn(count => { + expect(serializeInner(root)).toBe(`${count}`) + }) + + const Comp = { + setup() { + watch(() => { + assertion(count.value) + }) + return () => count.value + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + await nextTick() + expect(assertion).toHaveBeenCalledTimes(1) + + count.value++ + await nextTick() + expect(assertion).toHaveBeenCalledTimes(2) + }) + + it('flush timing: pre', async () => { + const count = ref(0) + const count2 = ref(0) + + let callCount = 0 + const assertion = jest.fn((count, count2Value) => { + callCount++ + // on mount, the watcher callback should be called before DOM render + // on update, should be called before the count is updated + const expectedDOM = callCount === 1 ? `` : `${count - 1}` + expect(serializeInner(root)).toBe(expectedDOM) + + // in a pre-flush callback, all state should have been updated + const expectedState = callCount === 1 ? 0 : 1 + expect(count2Value).toBe(expectedState) + }) + + const Comp = { + setup() { + watch( + () => { + assertion(count.value, count2.value) + }, + { + flush: 'pre' + } + ) + return () => count.value + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + await nextTick() + expect(assertion).toHaveBeenCalledTimes(1) + + count.value++ + count2.value++ + await nextTick() + // two mutations should result in 1 callback execution + expect(assertion).toHaveBeenCalledTimes(2) + }) + + it('flush timing: sync', async () => { + const count = ref(0) + const count2 = ref(0) - it('flush timing: pre', () => {}) + let callCount = 0 + const assertion = jest.fn(count => { + callCount++ + // on mount, the watcher callback should be called before DOM render + // on update, should be called before the count is updated + const expectedDOM = callCount === 1 ? `` : `${count - 1}` + expect(serializeInner(root)).toBe(expectedDOM) - it('flush timing: sync', () => {}) + // in a sync callback, state mutation on the next line should not have + // executed yet on the 2nd call, but will be on the 3rd call. + const expectedState = callCount < 3 ? 0 : 1 + expect(count2.value).toBe(expectedState) + }) + + const Comp = { + setup() { + watch( + () => { + assertion(count.value) + }, + { + flush: 'sync' + } + ) + return () => count.value + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + await nextTick() + expect(assertion).toHaveBeenCalledTimes(1) - it('deep', () => {}) + count.value++ + count2.value++ + await nextTick() + expect(assertion).toHaveBeenCalledTimes(3) + }) - it('lazy', () => {}) + it('deep', async () => { + const state = reactive({ + nested: { + count: ref(0) + }, + array: [1, 2, 3] + }) - it('onTrack', () => {}) + let dummy + let arr + watch( + () => state, + state => { + dummy = state.nested.count + arr = state.array[2] + }, + { deep: true } + ) - it('onTrigger', () => {}) + await nextTick() + expect(dummy).toBe(0) + expect(arr).toBe(3) + + state.nested.count++ + await nextTick() + expect(dummy).toBe(1) + expect(arr).toBe(3) + + // nested array mutation + state.array[2] = 4 + await nextTick() + expect(dummy).toBe(1) + expect(arr).toBe(4) + }) + + it('lazy', async () => { + const count = ref(0) + const cb = jest.fn() + watch(count, cb, { lazy: true }) + await nextTick() + expect(cb).not.toHaveBeenCalled() + count.value++ + await nextTick() + expect(cb).toHaveBeenCalled() + }) + + it('onTrack', async () => { + let events: DebuggerEvent[] = [] + let dummy + const onTrack = jest.fn((e: DebuggerEvent) => { + events.push(e) + }) + const obj = reactive({ foo: 1, bar: 2 }) + watch( + () => { + dummy = [obj.foo, 'bar' in obj, Object.keys(obj)] + }, + { onTrack } + ) + await nextTick() + expect(dummy).toEqual([1, true, ['foo', 'bar']]) + expect(onTrack).toHaveBeenCalledTimes(3) + expect(events).toMatchObject([ + { + type: OperationTypes.GET, + key: 'foo' + }, + { + type: OperationTypes.HAS, + key: 'bar' + }, + { + type: OperationTypes.ITERATE, + key: ITERATE_KEY + } + ]) + }) + + it('onTrigger', async () => { + let events: DebuggerEvent[] = [] + let dummy + const onTrigger = jest.fn((e: DebuggerEvent) => { + events.push(e) + }) + const obj = reactive({ foo: 1 }) + watch( + () => { + dummy = obj.foo + }, + { onTrigger } + ) + await nextTick() + expect(dummy).toBe(1) + + obj.foo++ + await nextTick() + expect(dummy).toBe(2) + expect(onTrigger).toHaveBeenCalledTimes(1) + expect(events[0]).toMatchObject({ + type: OperationTypes.SET, + key: 'foo', + oldValue: 1, + newValue: 2 + }) + + delete obj.foo + await nextTick() + expect(dummy).toBeUndefined() + expect(onTrigger).toHaveBeenCalledTimes(2) + expect(events[1]).toMatchObject({ + type: OperationTypes.DELETE, + key: 'foo', + oldValue: 2 + }) + }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index bc042173f6..943011f0fe 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -8,6 +8,7 @@ import { import { queueJob, queuePostFlushCb } from './scheduler' import { EMPTY_OBJ, isObject, isArray, isFunction } from '@vue/shared' import { recordEffect } from './apiReactivity' +import { getCurrentInstance } from './component' export interface WatchOptions { lazy?: boolean @@ -113,8 +114,21 @@ function doWatch( } : void 0 + const instance = getCurrentInstance() const scheduler = - flush === 'sync' ? invoke : flush === 'pre' ? queueJob : queuePostFlushCb + flush === 'sync' + ? invoke + : flush === 'pre' + ? (job: () => void) => { + if (!instance || instance.vnode.el != null) { + queueJob(job) + } else { + // with 'pre' option, the first call must happen before + // the component is mounted so it is called synchronously. + job() + } + } + : queuePostFlushCb const runner = effect(getter, { lazy: true, -- 2.47.3