Преглед изворни кода

test: tests for lifecycle api

Evan You пре 6 година
родитељ
комит
b40b7356ef

+ 314 - 1
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(`<div></div>`)
+    })
+
+    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(`<div>0</div>`)
+    })
+
+    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(`<div>1</div>`)
+    })
+
+    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(`<div></div>`)
+    })
+
+    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')
 })

+ 5 - 2
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)

+ 10 - 13
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)
 }

+ 3 - 3
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,

+ 17 - 12
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