]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(runtime-vapor): port tests from rendererComponent.spec.ts (#12677)
authoredison <daiwei521@126.com>
Wed, 29 Jan 2025 04:12:52 +0000 (12:12 +0800)
committerGitHub <noreply@github.com>
Wed, 29 Jan 2025 04:12:52 +0000 (12:12 +0800)
packages/runtime-core/__tests__/rendererComponent.spec.ts
packages/runtime-vapor/__tests__/component.spec.ts

index fefc4137034f98d1407958330d63a65b596a4272..fa3c192e885f2052c933771a4bb1711fdc68a848 100644 (file)
@@ -46,7 +46,7 @@ describe('renderer: component', () => {
     expect(parentVnode!.el).toBe(childVnode2!.el)
   })
 
-  it('should create an Component with props', () => {
+  it('should create a component with props', () => {
     const Comp = {
       render: () => {
         return h('div')
@@ -57,7 +57,7 @@ describe('renderer: component', () => {
     expect(serializeInner(root)).toBe(`<div id="foo" class="bar"></div>`)
   })
 
-  it('should create an Component with direct text children', () => {
+  it('should create a component with direct text children', () => {
     const Comp = {
       render: () => {
         return h('div', 'test')
index a84125b523206dcc78dc56570a54bd0857115f66..a4af5be7ab192ba5b11628ab889262ce30553f77 100644 (file)
-import { ref, watchEffect } from '@vue/runtime-dom'
-import { renderEffect, setText, template } from '../src'
+import {
+  type Ref,
+  inject,
+  nextTick,
+  onUpdated,
+  provide,
+  ref,
+  watch,
+  watchEffect,
+} from '@vue/runtime-dom'
+import {
+  createComponent,
+  createIf,
+  createTextNode,
+  renderEffect,
+  setText,
+  template,
+} from '../src'
 import { makeRender } from './_utils'
 import type { VaporComponentInstance } from '../src/component'
 
 const define = makeRender()
 
-// TODO port tests from rendererComponent.spec.ts
-
 describe('component', () => {
-  test('unmountComponent', async () => {
+  it('should update parent(hoc) component host el when child component self update', async () => {
+    const value = ref(true)
+    let childNode1: Node | null = null
+    let childNode2: Node | null = null
+
+    const { component: Child } = define({
+      setup() {
+        return createIf(
+          () => value.value,
+          () => (childNode1 = template('<div></div>')()),
+          () => (childNode2 = template('<span></span>')()),
+        )
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child)
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div></div><!--if-->')
+    expect(host.children[0]).toBe(childNode1)
+
+    value.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<span></span><!--if-->')
+    expect(host.children[0]).toBe(childNode2)
+  })
+
+  it('should create a component with props', () => {
+    const { component: Comp } = define({
+      setup() {
+        return template('<div>', true)()
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Comp, { id: () => 'foo', class: () => 'bar' })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div id="foo" class="bar"></div>')
+  })
+
+  it('should not update Component if only changed props are declared emit listeners', async () => {
+    const updatedSyp = vi.fn()
+    const { component: Comp } = define({
+      emits: ['foo'],
+      setup() {
+        onUpdated(updatedSyp)
+        return template('<div>', true)()
+      },
+    })
+
+    const toggle = ref(true)
+    const fn1 = () => {}
+    const fn2 = () => {}
+    define({
+      setup() {
+        const _on_foo = () => (toggle.value ? fn1() : fn2())
+        return createComponent(Comp, { onFoo: () => _on_foo })
+      },
+    }).render()
+    expect(updatedSyp).toHaveBeenCalledTimes(0)
+
+    toggle.value = false
+    await nextTick()
+    expect(updatedSyp).toHaveBeenCalledTimes(0)
+  })
+
+  it('component child synchronously updating parent state should trigger parent re-render', async () => {
+    const { component: Child } = define({
+      setup() {
+        const n = inject<Ref<number>>('foo')!
+        n.value++
+        const n0 = template('<div></div>')()
+        renderEffect(() => setText(n0, n.value))
+        return n0
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        const n = ref(0)
+        provide('foo', n)
+        const n0 = template('<div></div>')()
+        renderEffect(() => setText(n0, n.value))
+        return [n0, createComponent(Child)]
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>0</div><div>1</div>')
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
+  })
+
+  it('component child updating parent state in pre-flush should trigger parent re-render', async () => {
+    const { component: Child } = define({
+      props: ['value'],
+      setup(props: any, { emit }) {
+        watch(
+          () => props.value,
+          val => emit('update', val),
+        )
+        const n0 = template('<div></div>')()
+        renderEffect(() => setText(n0, props.value))
+        return n0
+      },
+    })
+
+    const outer = ref(0)
+    const { host } = define({
+      setup() {
+        const inner = ref(0)
+        const n0 = template('<div></div>')()
+        renderEffect(() => setText(n0, inner.value))
+        const n1 = createComponent(Child, {
+          value: () => outer.value,
+          onUpdate: () => (val: number) => (inner.value = val),
+        })
+        return [n0, n1]
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<div>0</div><div>0</div>')
+    outer.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<div>1</div><div>1</div>')
+  })
+
+  it('child only updates once when triggered in multiple ways', async () => {
+    const a = ref(0)
+    const calls: string[] = []
+
+    const { component: Child } = define({
+      props: ['count'],
+      setup(props: any) {
+        onUpdated(() => calls.push('update child'))
+        return createTextNode(() => [`${props.count} - ${a.value}`])
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Child, { count: () => a.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('0 - 0')
+    expect(calls).toEqual([])
+
+    // This will trigger child rendering directly, as well as via a prop change
+    a.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('1 - 1')
+    expect(calls).toEqual(['update child'])
+  })
+
+  it(`an earlier update doesn't lead to excessive subsequent updates`, async () => {
+    const globalCount = ref(0)
+    const parentCount = ref(0)
+    const calls: string[] = []
+
+    const { component: Child } = define({
+      props: ['count'],
+      setup(props: any) {
+        watch(
+          () => props.count,
+          () => {
+            calls.push('child watcher')
+            globalCount.value = props.count
+          },
+        )
+        onUpdated(() => calls.push('update child'))
+        return []
+      },
+    })
+
+    const { component: Parent } = define({
+      props: ['count'],
+      setup(props: any) {
+        onUpdated(() => calls.push('update parent'))
+        const n1 = createTextNode(() => [
+          `${globalCount.value} - ${props.count}`,
+        ])
+        const n2 = createComponent(Child, { count: () => parentCount.value })
+        return [n1, n2]
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        onUpdated(() => calls.push('update root'))
+        return createComponent(Parent, { count: () => globalCount.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe(`0 - 0`)
+    expect(calls).toEqual([])
+
+    parentCount.value++
+    await nextTick()
+    expect(host.innerHTML).toBe(`1 - 1`)
+    expect(calls).toEqual(['child watcher', 'update parent'])
+  })
+
+  it('child component props update should not lead to double update', async () => {
+    const text = ref(0)
+    const spy = vi.fn()
+
+    const { component: Comp } = define({
+      props: ['text'],
+      setup(props: any) {
+        const n1 = template('<h1></h1>')()
+        renderEffect(() => {
+          spy()
+          setText(n1, props.text)
+        })
+        return n1
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Comp, { text: () => text.value })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('<h1>0</h1>')
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    text.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<h1>1</h1>')
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+
+  it('unmount component', async () => {
     const { host, app, instance } = define(() => {
       const count = ref(0)
       const t0 = template('<div></div>')
@@ -28,4 +281,26 @@ describe('component', () => {
     expect(host.innerHTML).toBe('')
     expect(i.scope.effects.length).toBe(0)
   })
+
+  it('warn if functional vapor component not return a block', () => {
+    define(() => {
+      return () => {}
+    }).render()
+
+    expect(
+      'Functional vapor component must return a block directly',
+    ).toHaveBeenWarned()
+  })
+
+  it('warn if setup return a function and no render function', () => {
+    define({
+      setup() {
+        return () => []
+      },
+    }).render()
+
+    expect(
+      'Vapor component setup() returned non-block value, and has no render function',
+    ).toHaveBeenWarned()
+  })
 })