+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')
})
function injectHook(
name: keyof LifecycleHooks,
- hook: () => void,
+ hook: Function,
target: ComponentInstance | null | void = currentInstance
) {
if (target) {
}
}
-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)
}
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) {
if (instance.m !== null) {
queuePostFlushCb(instance.m)
}
+ mounted = true
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
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