test: tests for lifecycle api

This commit is contained in:
Evan You 2019-08-28 12:13:36 -04:00
parent 2b6ca9a7b6
commit b40b7356ef
5 changed files with 349 additions and 31 deletions

View 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 // reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
describe('api: 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')
}) })

View File

@ -314,7 +314,7 @@ describe('api: watch', () => {
}) })
it('onTrack', async () => { it('onTrack', async () => {
let events: DebuggerEvent[] = [] const events: DebuggerEvent[] = []
let dummy let dummy
const onTrack = jest.fn((e: DebuggerEvent) => { const onTrack = jest.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)
@ -331,14 +331,17 @@ describe('api: watch', () => {
expect(onTrack).toHaveBeenCalledTimes(3) expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toMatchObject([ expect(events).toMatchObject([
{ {
target: obj,
type: OperationTypes.GET, type: OperationTypes.GET,
key: 'foo' key: 'foo'
}, },
{ {
target: obj,
type: OperationTypes.HAS, type: OperationTypes.HAS,
key: 'bar' key: 'bar'
}, },
{ {
target: obj,
type: OperationTypes.ITERATE, type: OperationTypes.ITERATE,
key: ITERATE_KEY key: ITERATE_KEY
} }
@ -346,7 +349,7 @@ describe('api: watch', () => {
}) })
it('onTrigger', async () => { it('onTrigger', async () => {
let events: DebuggerEvent[] = [] const events: DebuggerEvent[] = []
let dummy let dummy
const onTrigger = jest.fn((e: DebuggerEvent) => { const onTrigger = jest.fn((e: DebuggerEvent) => {
events.push(e) events.push(e)

View File

@ -2,7 +2,7 @@ import { ComponentInstance, LifecycleHooks, currentInstance } from './component'
function injectHook( function injectHook(
name: keyof LifecycleHooks, name: keyof LifecycleHooks,
hook: () => void, hook: Function,
target: ComponentInstance | null | void = currentInstance target: ComponentInstance | null | void = currentInstance
) { ) {
if (target) { 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) injectHook('bm', hook, target)
} }
export function onMounted(hook: () => void, target?: ComponentInstance) { export function onMounted(hook: Function, target?: ComponentInstance) {
injectHook('m', hook, target) injectHook('m', hook, target)
} }
export function onBeforeUpdate(hook: () => void, target?: ComponentInstance) { export function onBeforeUpdate(hook: Function, target?: ComponentInstance) {
injectHook('bu', hook, target) injectHook('bu', hook, target)
} }
export function onUpdated(hook: () => void, target?: ComponentInstance) { export function onUpdated(hook: Function, target?: ComponentInstance) {
injectHook('u', hook, target) injectHook('u', hook, target)
} }
export function onBeforeUnmount(hook: () => void, target?: ComponentInstance) { export function onBeforeUnmount(hook: Function, target?: ComponentInstance) {
injectHook('bum', hook, target) injectHook('bum', hook, target)
} }
export function onUnmounted(hook: () => void, target?: ComponentInstance) { export function onUnmounted(hook: Function, target?: ComponentInstance) {
injectHook('um', hook, target) injectHook('um', hook, target)
} }
export function onRenderTriggered( export function onRenderTriggered(hook: Function, target?: ComponentInstance) {
hook: () => void,
target?: ComponentInstance
) {
injectHook('rtg', hook, target) injectHook('rtg', hook, target)
} }
export function onRenderTracked(hook: () => void, target?: ComponentInstance) { export function onRenderTracked(hook: Function, target?: ComponentInstance) {
injectHook('rtc', hook, target) injectHook('rtc', hook, target)
} }
export function onErrorCaptured(hook: () => void, target?: ComponentInstance) { export function onErrorCaptured(hook: Function, target?: ComponentInstance) {
injectHook('ec', hook, target) injectHook('ec', hook, target)
} }

View File

@ -162,14 +162,14 @@ export function createComponent(options: any) {
} }
export function createComponentInstance( export function createComponentInstance(
type: any, vnode: VNode,
parent: ComponentInstance | null parent: ComponentInstance | null
): ComponentInstance { ): ComponentInstance {
const instance = { const instance = {
type, vnode,
parent, parent,
type: vnode.type as any,
root: null as any, // set later so it can point to itself root: null as any, // set later so it can point to itself
vnode: null as any,
next: null, next: null,
subTree: null as any, subTree: null as any,
update: null as any, update: null as any,

View File

@ -565,21 +565,25 @@ export function createRenderer(options: RendererOptions) {
parentComponent: ComponentInstance | null, parentComponent: ComponentInstance | null,
isSVG: boolean isSVG: boolean
) { ) {
const Component = initialVNode.type as any
const instance: ComponentInstance = (initialVNode.component = createComponentInstance( const instance: ComponentInstance = (initialVNode.component = createComponentInstance(
Component, initialVNode,
parentComponent 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() { instance.update = effect(function componentEffect() {
if (instance.vnode === null) { if (!mounted) {
// mountComponent
instance.vnode = initialVNode
resolveProps(instance, initialVNode.props, Component.props)
resolveSlots(instance, initialVNode.children)
// setup stateful
if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
setupStatefulComponent(instance)
}
const subTree = (instance.subTree = renderComponentRoot(instance)) const subTree = (instance.subTree = renderComponentRoot(instance))
// beforeMount hook // beforeMount hook
if (instance.bm !== null) { if (instance.bm !== null) {
@ -591,6 +595,7 @@ export function createRenderer(options: RendererOptions) {
if (instance.m !== null) { if (instance.m !== null) {
queuePostFlushCb(instance.m) queuePostFlushCb(instance.m)
} }
mounted = true
} else { } else {
// updateComponent // updateComponent
// This is triggered by mutation of component's own state (next: null) // This is triggered by mutation of component's own state (next: null)
@ -601,7 +606,7 @@ export function createRenderer(options: RendererOptions) {
next.component = instance next.component = instance
instance.vnode = next instance.vnode = next
instance.next = null instance.next = null
resolveProps(instance, next.props, Component.props) resolveProps(instance, next.props, propsOptions)
resolveSlots(instance, next.children) resolveSlots(instance, next.children)
} }
const prevTree = instance.subTree const prevTree = instance.subTree