From b40b7356ef585cd4221e0cfb768e6fd9d23a569c Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 28 Aug 2019 12:13:36 -0400 Subject: [PATCH] test: tests for lifecycle api --- .../__tests__/apiLifecycle.spec.ts | 315 +++++++++++++++++- .../runtime-core/__tests__/apiWatch.spec.ts | 7 +- packages/runtime-core/src/apiLifecycle.ts | 23 +- packages/runtime-core/src/component.ts | 6 +- packages/runtime-core/src/createRenderer.ts | 29 +- 5 files changed, 349 insertions(+), 31 deletions(-) diff --git a/packages/runtime-core/__tests__/apiLifecycle.spec.ts b/packages/runtime-core/__tests__/apiLifecycle.spec.ts index dd0c8fba..8798b5ae 100644 --- a/packages/runtime-core/__tests__/apiLifecycle.spec.ts +++ b/packages/runtime-core/__tests__/apiLifecycle.spec.ts @@ -1,5 +1,318 @@ +import { + onBeforeMount, + h, + nodeOps, + render, + serializeInner, + onMounted, + ref, + onBeforeUpdate, + nextTick, + onUpdated, + onBeforeUnmount, + onUnmounted, + onRenderTracked, + reactive, + OperationTypes, + onRenderTriggered +} from '@vue/runtime-test' +import { ITERATE_KEY, DebuggerEvent } from '@vue/reactivity' + // reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks describe('api: lifecycle hooks', () => { - test.todo('should work') + it('onBeforeMount', () => { + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called before inner div is rendered + expect(serializeInner(root)).toBe(``) + }) + + const Comp = { + setup() { + onBeforeMount(fn) + return () => h('div') + } + } + render(h(Comp), root) + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onMounted', () => { + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called after inner div is rendered + expect(serializeInner(root)).toBe(`
`) + }) + + const Comp = { + setup() { + onMounted(fn) + return () => h('div') + } + } + render(h(Comp), root) + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onBeforeUpdate', async () => { + const count = ref(0) + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called before inner div is updated + expect(serializeInner(root)).toBe(`
0
`) + }) + + const Comp = { + setup() { + onBeforeUpdate(fn) + return () => h('div', count.value) + } + } + render(h(Comp), root) + + count.value++ + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onUpdated', async () => { + const count = ref(0) + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called after inner div is updated + expect(serializeInner(root)).toBe(`
1
`) + }) + + const Comp = { + setup() { + onUpdated(fn) + return () => h('div', count.value) + } + } + render(h(Comp), root) + + count.value++ + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onBeforeUnmount', async () => { + const toggle = ref(true) + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called before inner div is removed + expect(serializeInner(root)).toBe(`
`) + }) + + const Comp = { + setup() { + return () => (toggle.value ? h(Child) : null) + } + } + + const Child = { + setup() { + onBeforeUnmount(fn) + return () => h('div') + } + } + + render(h(Comp), root) + + toggle.value = false + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('onUnmounted', async () => { + const toggle = ref(true) + const root = nodeOps.createElement('div') + const fn = jest.fn(() => { + // should be called after inner div is removed + expect(serializeInner(root)).toBe(``) + }) + + const Comp = { + setup() { + return () => (toggle.value ? h(Child) : null) + } + } + + const Child = { + setup() { + onUnmounted(fn) + return () => h('div') + } + } + + render(h(Comp), root) + + toggle.value = false + await nextTick() + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('lifecycle call order', async () => { + const count = ref(0) + const root = nodeOps.createElement('div') + const calls: string[] = [] + + const Root = { + setup() { + onBeforeMount(() => calls.push('root onBeforeMount')) + onMounted(() => calls.push('root onMounted')) + onBeforeUpdate(() => calls.push('root onBeforeUpdate')) + onUpdated(() => calls.push('root onUpdated')) + onBeforeUnmount(() => calls.push('root onBeforeUnmount')) + onUnmounted(() => calls.push('root onUnmounted')) + return () => h(Mid, { count: count.value }) + } + } + + const Mid = { + setup(props: any) { + onBeforeMount(() => calls.push('mid onBeforeMount')) + onMounted(() => calls.push('mid onMounted')) + onBeforeUpdate(() => calls.push('mid onBeforeUpdate')) + onUpdated(() => calls.push('mid onUpdated')) + onBeforeUnmount(() => calls.push('mid onBeforeUnmount')) + onUnmounted(() => calls.push('mid onUnmounted')) + return () => h(Child, { count: props.count }) + } + } + + const Child = { + setup(props: any) { + onBeforeMount(() => calls.push('child onBeforeMount')) + onMounted(() => calls.push('child onMounted')) + onBeforeUpdate(() => calls.push('child onBeforeUpdate')) + onUpdated(() => calls.push('child onUpdated')) + onBeforeUnmount(() => calls.push('child onBeforeUnmount')) + onUnmounted(() => calls.push('child onUnmounted')) + return () => h('div', props.count) + } + } + + // mount + render(h(Root), root) + expect(calls).toEqual([ + 'root onBeforeMount', + 'mid onBeforeMount', + 'child onBeforeMount', + 'child onMounted', + 'mid onMounted', + 'root onMounted' + ]) + + calls.length = 0 + + // update + count.value++ + await nextTick() + expect(calls).toEqual([ + 'root onBeforeUpdate', + 'mid onBeforeUpdate', + 'child onBeforeUpdate', + 'child onUpdated', + 'mid onUpdated', + 'root onUpdated' + ]) + + calls.length = 0 + + // unmount + render(null, root) + expect(calls).toEqual([ + 'root onBeforeUnmount', + 'mid onBeforeUnmount', + 'child onBeforeUnmount', + 'child onUnmounted', + 'mid onUnmounted', + 'root onUnmounted' + ]) + }) + + it('onRenderTracked', () => { + const events: DebuggerEvent[] = [] + const onTrack = jest.fn((e: DebuggerEvent) => { + events.push(e) + }) + const obj = reactive({ foo: 1, bar: 2 }) + + const Comp = { + setup() { + onRenderTracked(onTrack) + return () => + h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]) + } + } + + render(h(Comp), nodeOps.createElement('div')) + expect(onTrack).toHaveBeenCalledTimes(3) + expect(events).toMatchObject([ + { + target: obj, + type: OperationTypes.GET, + key: 'foo' + }, + { + target: obj, + type: OperationTypes.HAS, + key: 'bar' + }, + { + target: obj, + type: OperationTypes.ITERATE, + key: ITERATE_KEY + } + ]) + }) + + it('onRenderTriggered', async () => { + const events: DebuggerEvent[] = [] + const onTrigger = jest.fn((e: DebuggerEvent) => { + events.push(e) + }) + const obj = reactive({ foo: 1, bar: 2 }) + + const Comp = { + setup() { + onRenderTriggered(onTrigger) + return () => + h('div', [obj.foo, 'bar' in obj, Object.keys(obj).join('')]) + } + } + + render(h(Comp), nodeOps.createElement('div')) + + obj.foo++ + await nextTick() + expect(onTrigger).toHaveBeenCalledTimes(1) + expect(events[0]).toMatchObject({ + type: OperationTypes.SET, + key: 'foo', + oldValue: 1, + newValue: 2 + }) + + delete obj.bar + await nextTick() + expect(onTrigger).toHaveBeenCalledTimes(2) + expect(events[1]).toMatchObject({ + type: OperationTypes.DELETE, + key: 'bar', + oldValue: 2 + }) + ;(obj as any).baz = 3 + await nextTick() + expect(onTrigger).toHaveBeenCalledTimes(3) + expect(events[2]).toMatchObject({ + type: OperationTypes.ADD, + key: 'baz', + newValue: 3 + }) + }) + + test.todo('onErrorCaptured') }) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index d8854f0a..9af70349 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -314,7 +314,7 @@ describe('api: watch', () => { }) it('onTrack', async () => { - let events: DebuggerEvent[] = [] + const events: DebuggerEvent[] = [] let dummy const onTrack = jest.fn((e: DebuggerEvent) => { events.push(e) @@ -331,14 +331,17 @@ describe('api: watch', () => { expect(onTrack).toHaveBeenCalledTimes(3) expect(events).toMatchObject([ { + target: obj, type: OperationTypes.GET, key: 'foo' }, { + target: obj, type: OperationTypes.HAS, key: 'bar' }, { + target: obj, type: OperationTypes.ITERATE, key: ITERATE_KEY } @@ -346,7 +349,7 @@ describe('api: watch', () => { }) it('onTrigger', async () => { - let events: DebuggerEvent[] = [] + const events: DebuggerEvent[] = [] let dummy const onTrigger = jest.fn((e: DebuggerEvent) => { events.push(e) diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index ff8fdb84..c5976015 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -2,7 +2,7 @@ import { ComponentInstance, LifecycleHooks, currentInstance } from './component' function injectHook( name: keyof LifecycleHooks, - hook: () => void, + hook: Function, target: ComponentInstance | null | void = currentInstance ) { if (target) { @@ -14,41 +14,38 @@ function injectHook( } } -export function onBeforeMount(hook: () => void, target?: ComponentInstance) { +export function onBeforeMount(hook: Function, target?: ComponentInstance) { injectHook('bm', hook, target) } -export function onMounted(hook: () => void, target?: ComponentInstance) { +export function onMounted(hook: Function, target?: ComponentInstance) { injectHook('m', hook, target) } -export function onBeforeUpdate(hook: () => void, target?: ComponentInstance) { +export function onBeforeUpdate(hook: Function, target?: ComponentInstance) { injectHook('bu', hook, target) } -export function onUpdated(hook: () => void, target?: ComponentInstance) { +export function onUpdated(hook: Function, target?: ComponentInstance) { injectHook('u', hook, target) } -export function onBeforeUnmount(hook: () => void, target?: ComponentInstance) { +export function onBeforeUnmount(hook: Function, target?: ComponentInstance) { injectHook('bum', hook, target) } -export function onUnmounted(hook: () => void, target?: ComponentInstance) { +export function onUnmounted(hook: Function, target?: ComponentInstance) { injectHook('um', hook, target) } -export function onRenderTriggered( - hook: () => void, - target?: ComponentInstance -) { +export function onRenderTriggered(hook: Function, target?: ComponentInstance) { injectHook('rtg', hook, target) } -export function onRenderTracked(hook: () => void, target?: ComponentInstance) { +export function onRenderTracked(hook: Function, target?: ComponentInstance) { injectHook('rtc', hook, target) } -export function onErrorCaptured(hook: () => void, target?: ComponentInstance) { +export function onErrorCaptured(hook: Function, target?: ComponentInstance) { injectHook('ec', hook, target) } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 21e5b443..8b27fe86 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -162,14 +162,14 @@ export function createComponent(options: any) { } export function createComponentInstance( - type: any, + vnode: VNode, parent: ComponentInstance | null ): ComponentInstance { const instance = { - type, + vnode, parent, + type: vnode.type as any, root: null as any, // set later so it can point to itself - vnode: null as any, next: null, subTree: null as any, update: null as any, diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 2dd6c6ab..0552a838 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -565,21 +565,25 @@ export function createRenderer(options: RendererOptions) { parentComponent: ComponentInstance | null, isSVG: boolean ) { - const Component = initialVNode.type as any const instance: ComponentInstance = (initialVNode.component = createComponentInstance( - Component, + initialVNode, parentComponent )) + + // resolve props and slots for setup context + const propsOptions = (initialVNode.type as any).props + resolveProps(instance, initialVNode.props, propsOptions) + resolveSlots(instance, initialVNode.children) + + // setup stateful logic + if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { + setupStatefulComponent(instance) + } + + // create reactive effect for rendering + let mounted = false instance.update = effect(function componentEffect() { - if (instance.vnode === null) { - // mountComponent - instance.vnode = initialVNode - resolveProps(instance, initialVNode.props, Component.props) - resolveSlots(instance, initialVNode.children) - // setup stateful - if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { - setupStatefulComponent(instance) - } + if (!mounted) { const subTree = (instance.subTree = renderComponentRoot(instance)) // beforeMount hook if (instance.bm !== null) { @@ -591,6 +595,7 @@ export function createRenderer(options: RendererOptions) { if (instance.m !== null) { queuePostFlushCb(instance.m) } + mounted = true } else { // updateComponent // This is triggered by mutation of component's own state (next: null) @@ -601,7 +606,7 @@ export function createRenderer(options: RendererOptions) { next.component = instance instance.vnode = next instance.next = null - resolveProps(instance, next.props, Component.props) + resolveProps(instance, next.props, propsOptions) resolveSlots(instance, next.children) } const prevTree = instance.subTree