Ver código fonte

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

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
ubugeeei 2 anos atrás
pai
commit
969f53f2e7

+ 444 - 47
packages/runtime-vapor/__tests__/apiLifecycle.spec.ts

@@ -1,28 +1,468 @@
 import {
+  type DebuggerEvent,
   type InjectionKey,
   type Ref,
+  TrackOpTypes,
+  TriggerOpTypes,
   createComponent,
+  createIf,
   createTextNode,
   getCurrentInstance,
   inject,
   nextTick,
+  onBeforeMount,
+  onBeforeUnmount,
+  onBeforeUpdate,
+  onMounted,
   onRenderTracked,
   onRenderTriggered,
+  onUnmounted,
   onUpdated,
   provide,
+  reactive,
   ref,
   renderEffect,
   setText,
+  template,
 } from '../src'
 import { makeRender } from './_utils'
+import { ITERATE_KEY } from '@vue/reactivity'
 
 const define = makeRender<any>()
 
-describe('apiLifecycle', () => {
-  // TODO: test
+describe('api: lifecycle hooks', () => {
+  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
-  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 handleUpdatedChild = vi.fn()
 
@@ -67,7 +507,7 @@ describe('apiLifecycle', () => {
   })
 
   // #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 handleUpdatedChild = vi.fn()
 
@@ -114,47 +554,4 @@ describe('apiLifecycle', () => {
     expect(handleUpdated).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')
-  })
 })

+ 3 - 0
packages/runtime-vapor/src/index.ts

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