test(runtime-vapor): api lifecycle hooks (#215)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
ubugeeei 2024-05-27 03:37:06 +09:00 committed by GitHub
parent 1008199647
commit 969f53f2e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 447 additions and 47 deletions

View File

@ -1,28 +1,468 @@
import { import {
type DebuggerEvent,
type InjectionKey, type InjectionKey,
type Ref, type Ref,
TrackOpTypes,
TriggerOpTypes,
createComponent, createComponent,
createIf,
createTextNode, createTextNode,
getCurrentInstance, getCurrentInstance,
inject, inject,
nextTick, nextTick,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onMounted,
onRenderTracked, onRenderTracked,
onRenderTriggered, onRenderTriggered,
onUnmounted,
onUpdated, onUpdated,
provide, provide,
reactive,
ref, ref,
renderEffect, renderEffect,
setText, setText,
template,
} from '../src' } from '../src'
import { makeRender } from './_utils' import { makeRender } from './_utils'
import { ITERATE_KEY } from '@vue/reactivity'
const define = makeRender<any>() const define = makeRender<any>()
describe('apiLifecycle', () => { describe('api: lifecycle hooks', () => {
// TODO: test it('onBeforeMount', () => {
const fn = vi.fn(() => {
expect(host.innerHTML).toBe(``)
})
const { render, host } = define({
setup() {
onBeforeMount(fn)
return () => template('<div></div>')()
},
})
render()
expect(fn).toHaveBeenCalledTimes(1)
})
it('onMounted', () => {
const fn = vi.fn(() => {
expect(host.innerHTML).toBe(``)
})
const { render, host } = define({
setup() {
onMounted(fn)
return () => template('<div></div>')()
},
})
render()
expect(fn).toHaveBeenCalledTimes(1)
})
it('onBeforeUpdate', async () => {
const count = ref(0)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('0')
})
const { render, host } = define({
setup() {
onBeforeUpdate(fn)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
})
render()
count.value++
await nextTick()
expect(fn).toHaveBeenCalledTimes(1)
expect(host.innerHTML).toBe('1')
})
it('state mutation in onBeforeUpdate', async () => {
const count = ref(0)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('0')
count.value++
})
const renderSpy = vi.fn()
const { render, host } = define({
setup() {
onBeforeUpdate(fn)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
renderSpy()
setText(n0, count.value)
})
return n0
})()
},
})
render()
expect(renderSpy).toHaveBeenCalledTimes(1)
})
it('onUpdated', async () => {
const count = ref(0)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('1')
})
const { render, host } = define({
setup() {
onUpdated(fn)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
})
render()
count.value++
await nextTick()
expect(fn).toHaveBeenCalledTimes(1)
})
it('onBeforeUnmount', async () => {
const toggle = ref(true)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('<div></div>')
})
const { render, host } = define({
setup() {
return (() => {
const n0 = createIf(
() => toggle.value,
() => createComponent(Child),
)
return n0
})()
},
})
const Child = {
setup() {
onBeforeUnmount(fn)
return (() => {
const t0 = template('<div></div>')
const n0 = t0()
return n0
})()
},
}
render()
toggle.value = false
await nextTick()
// expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called
expect(host.innerHTML).toBe('<!--if-->')
})
it('onUnmounted', async () => {
const toggle = ref(true)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('<div></div>')
})
const { render, host } = define({
setup() {
return (() => {
const n0 = createIf(
() => toggle.value,
() => createComponent(Child),
)
return n0
})()
},
})
const Child = {
setup() {
onUnmounted(fn)
return (() => {
const t0 = template('<div></div>')
const n0 = t0()
return n0
})()
},
}
render()
toggle.value = false
await nextTick()
// expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called
expect(host.innerHTML).toBe('<!--if-->')
})
it('onBeforeUnmount in onMounted', async () => {
const toggle = ref(true)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('<div></div>')
})
const { render, host } = define({
setup() {
return (() => {
const n0 = createIf(
() => toggle.value,
() => createComponent(Child),
)
return n0
})()
},
})
const Child = {
setup() {
onMounted(() => {
onBeforeUnmount(fn)
})
return (() => {
const t0 = template('<div></div>')
const n0 = t0()
return n0
})()
},
}
render()
toggle.value = false
await nextTick()
// expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called
expect(host.innerHTML).toBe('<!--if-->')
})
it('lifecycle call order', async () => {
const count = ref(0)
const toggle = ref(true)
const calls: string[] = []
const { render } = define({
setup() {
onBeforeMount(() => calls.push('onBeforeMount'))
onMounted(() => calls.push('onMounted'))
onBeforeUpdate(() => calls.push('onBeforeUpdate'))
onUpdated(() => calls.push('onUpdated'))
onBeforeUnmount(() => calls.push('onBeforeUnmount'))
onUnmounted(() => calls.push('onUnmounted'))
return (() => {
const n0 = createIf(
() => toggle.value,
() => createComponent(Mid, { count: () => count.value }),
)
return n0
})()
},
})
const Mid = {
props: ['count'],
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 (() => {
const n0 = createComponent(Child, { count: () => props.count })
return n0
})()
},
}
const Child = {
props: ['count'],
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 (() => {
const t0 = template('<div></div>')
const n0 = t0()
renderEffect(() => setText(n0, props.count))
return n0
})()
},
}
// mount
render()
expect(calls).toEqual([
'onBeforeMount',
'mid onBeforeMount',
'child onBeforeMount',
'child onMounted',
'mid onMounted',
'onMounted',
])
calls.length = 0
// update
count.value++
await nextTick()
// FIXME: not called
// expect(calls).toEqual([
// 'root onBeforeUpdate',
// 'mid onBeforeUpdate',
// 'child onBeforeUpdate',
// 'child onUpdated',
// 'mid onUpdated',
// 'root onUpdated',
// ])
calls.length = 0
// unmount
toggle.value = false
// FIXME: not called
// expect(calls).toEqual([
// 'root onBeforeUnmount',
// 'mid onBeforeUnmount',
// 'child onBeforeUnmount',
// 'child onUnmounted',
// 'mid onUnmounted',
// 'root onUnmounted',
// ])
})
it('onRenderTracked', async () => {
const events: DebuggerEvent[] = []
const onTrack = vi.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = reactive({ foo: 1, bar: 2 })
const { render } = define({
setup() {
onRenderTracked(onTrack)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
})
return n0
})()
},
})
render()
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('onRenderTrigger', async () => {
const events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = reactive<{
foo: number
bar?: number
}>({ foo: 1, bar: 2 })
const { render } = define({
setup() {
onRenderTriggered(onTrigger)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, [obj.foo, 'bar' in obj, Object.keys(obj).join('')])
})
return n0
})()
},
})
render()
obj.foo++
await nextTick()
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toMatchObject({
type: TriggerOpTypes.SET,
key: 'foo',
oldValue: 1,
newValue: 2,
})
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,
})
})
it('runs shared hook fn for each instance', async () => {
const fn = vi.fn()
const toggle = ref(true)
const { render } = define({
setup() {
return createIf(
() => toggle.value,
() => [createComponent(Child), createComponent(Child)],
)
},
})
const Child = {
setup() {
onBeforeMount(fn)
onBeforeUnmount(fn)
return template('<div></div>')()
},
}
render()
expect(fn).toHaveBeenCalledTimes(2)
toggle.value = false
await nextTick()
// expect(fn).toHaveBeenCalledTimes(4) // FIXME: not called unmounted hook
})
// #136 // #136
test('should trigger updated hooks across components. (parent -> child)', async () => { it('should trigger updated hooks across components. (parent -> child)', async () => {
const handleUpdated = vi.fn() const handleUpdated = vi.fn()
const handleUpdatedChild = vi.fn() const handleUpdatedChild = vi.fn()
@ -67,7 +507,7 @@ describe('apiLifecycle', () => {
}) })
// #136 // #136
test('should trigger updated hooks across components. (child -> parent)', async () => { it('should trigger updated hooks across components. (child -> parent)', async () => {
const handleUpdated = vi.fn() const handleUpdated = vi.fn()
const handleUpdatedChild = vi.fn() const handleUpdatedChild = vi.fn()
@ -114,47 +554,4 @@ describe('apiLifecycle', () => {
expect(handleUpdated).toHaveBeenCalledTimes(1) expect(handleUpdated).toHaveBeenCalledTimes(1)
expect(handleUpdatedChild).toHaveBeenCalledTimes(1) expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
}) })
test('onRenderTracked', async () => {
const onTrackedFn = vi.fn()
const count = ref(0)
const { host, render } = define({
setup() {
onRenderTracked(onTrackedFn)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
})
render()
await nextTick()
expect(onTrackedFn).toBeCalled()
expect(host.innerHTML).toBe('0')
})
test('onRenderTrigger', async () => {
const onRenderTriggerFn = vi.fn()
const count = ref(0)
const { host, render } = define({
setup() {
onRenderTriggered(onRenderTriggerFn)
return (() => {
const n0 = createTextNode()
renderEffect(() => {
setText(n0, count.value)
})
return n0
})()
},
})
render()
count.value++
await nextTick()
expect(onRenderTriggerFn).toBeCalled()
expect(onRenderTriggerFn).toHaveBeenCalledOnce()
expect(host.innerHTML).toBe('1')
})
}) })

View File

@ -4,6 +4,9 @@ export const version = __VERSION__
export { export {
// core // core
type Ref, type Ref,
type DebuggerEvent,
TrackOpTypes,
TriggerOpTypes,
reactive, reactive,
ref, ref,
readonly, readonly,