import {
onBeforeMount,
h,
nodeOps,
render,
serializeInner,
onMounted,
ref,
onBeforeUpdate,
nextTick,
onUpdated,
onBeforeUnmount,
onUnmounted,
onRenderTracked,
reactive,
TrackOpTypes,
onRenderTriggered
} from '@vue/runtime-test'
import { ITERATE_KEY, DebuggerEvent, TriggerOpTypes } from '@vue/reactivity'
// reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
describe('api: lifecycle hooks', () => {
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)
expect(serializeInner(root)).toBe(`1
`)
})
it('state mutation in 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
`)
count.value++
})
const renderSpy = jest.fn()
const Comp = {
setup() {
onBeforeUpdate(fn)
return () => {
renderSpy()
return h('div', count.value)
}
}
}
render(h(Comp), root)
expect(renderSpy).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(fn).toHaveBeenCalledTimes(1)
expect(renderSpy).toHaveBeenCalledTimes(2)
expect(serializeInner(root)).toBe(`2
`)
})
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('onBeforeUnmount in onMounted', 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() {
onMounted(() => {
onBeforeUnmount(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: TrackOpTypes.GET,
key: 'foo'
},
{
target: obj,
type: TrackOpTypes.HAS,
key: 'bar'
},
{
target: obj,
type: TrackOpTypes.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: TriggerOpTypes.SET,
key: 'foo',
oldValue: 1,
newValue: 2
})
// @ts-ignore
delete obj.bar
await nextTick()
expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toMatchObject({
type: TriggerOpTypes.DELETE,
key: 'bar',
oldValue: 2
})
;(obj as any).baz = 3
await nextTick()
expect(onTrigger).toHaveBeenCalledTimes(3)
expect(events[2]).toMatchObject({
type: TriggerOpTypes.ADD,
key: 'baz',
newValue: 3
})
})
})