]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: tests for lifecycle api
authorEvan You <yyx990803@gmail.com>
Wed, 28 Aug 2019 16:13:36 +0000 (12:13 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 28 Aug 2019 16:13:36 +0000 (12:13 -0400)
packages/runtime-core/__tests__/apiLifecycle.spec.ts
packages/runtime-core/__tests__/apiWatch.spec.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/createRenderer.ts

index dd0c8fbac1482a1eea7e473ba25dc72705cb92ff..8798b5ae90b2937bc60c8135234dc801a65d41b3 100644 (file)
@@ -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')
 })
index d8854f0a510798e783e115528fd50634a7bbca6e..9af703499e2abdc9c4937e5ee6a5c307f2420e4c 100644 (file)
@@ -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)
index ff8fdb849fbbd05b5c6567c3c5d946924494cab5..c59760158afd6372495ce143f0f9f4ccb9f21279 100644 (file)
@@ -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)
 }
index 21e5b443d6e221baaec30d0a792953d9d1e28fcb..8b27fe8631d5976a6adf2f709efaadb108a70db4 100644 (file)
@@ -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,
index 2dd6c6abeffe2aa7ba7ea2b4484fcbbf15c75c63..0552a8381e68308f5ab67635d7276e93c65e6009 100644 (file)
@@ -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