test: tests for lifecycle api
This commit is contained in:
parent
2b6ca9a7b6
commit
b40b7356ef
@ -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')
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user