From 5b288fcbe1f4305b4de37785214702a0a765bf35 Mon Sep 17 00:00:00 2001
From: daiwei <daiwei521@126.com>
Date: Fri, 10 Jan 2025 10:23:23 +0800
Subject: [PATCH 1/4] test(runtime-vapor): port tests from
 rendererComponent.spec.ts

---
 .../runtime-vapor/__tests__/component.spec.ts | 87 ++++++++++++++++++-
 1 file changed, 84 insertions(+), 3 deletions(-)

diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
index a84125b5232..fe76859d3b7 100644
--- a/packages/runtime-vapor/__tests__/component.spec.ts
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -1,5 +1,11 @@
-import { ref, watchEffect } from '@vue/runtime-dom'
-import { renderEffect, setText, template } from '../src'
+import { nextTick, ref, watchEffect } from '@vue/runtime-dom'
+import {
+  createComponent,
+  createIf,
+  renderEffect,
+  setText,
+  template,
+} from '../src'
 import { makeRender } from './_utils'
 import type { VaporComponentInstance } from '../src/component'
 
@@ -8,7 +14,82 @@ 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.todo('should create an Component with props', () => {})
+
+  it.todo('should create an Component with direct text children', () => {})
+
+  it.todo('should update an Component tag which is already mounted', () => {})
+
+  it.todo(
+    'should not update Component if only changed props are declared emit listeners',
+    () => {},
+  )
+
+  it.todo(
+    'component child synchronously updating parent state should trigger parent re-render',
+    async () => {},
+  )
+
+  it.todo('instance.$el should be exposed to watch options', async () => {})
+
+  it.todo(
+    'component child updating parent state in pre-flush should trigger parent re-render',
+    async () => {},
+  )
+
+  it.todo(
+    'child only updates once when triggered in multiple ways',
+    async () => {},
+  )
+
+  it.todo(
+    `an earlier update doesn't lead to excessive subsequent updates`,
+    async () => {},
+  )
+
+  it.todo(
+    'should pause tracking deps when initializing legacy options',
+    async () => {},
+  )
+
+  it.todo(
+    'child component props update should not lead to double update',
+    async () => {},
+  )
+
+  it.todo('should warn accessing `this` in a <script setup> template', () => {})
+
+  it('unmountComponent', async () => {
     const { host, app, instance } = define(() => {
       const count = ref(0)
       const t0 = template('<div></div>')

From 3074d8184456c2ad7c5351aa8362de41cdc99675 Mon Sep 17 00:00:00 2001
From: daiwei <daiwei521@126.com>
Date: Fri, 10 Jan 2025 12:05:14 +0800
Subject: [PATCH 2/4] wip: save

---
 .../__tests__/rendererComponent.spec.ts       |   2 +-
 .../runtime-vapor/__tests__/component.spec.ts | 160 +++++++++++++++---
 2 files changed, 140 insertions(+), 22 deletions(-)

diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts
index fefc4137034..fa43b1015c5 100644
--- a/packages/runtime-core/__tests__/rendererComponent.spec.ts
+++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts
@@ -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')
diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
index fe76859d3b7..dd014519a11 100644
--- a/packages/runtime-vapor/__tests__/component.spec.ts
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -1,7 +1,17 @@
-import { nextTick, ref, watchEffect } from '@vue/runtime-dom'
+import {
+  type Ref,
+  inject,
+  nextTick,
+  onUpdated,
+  provide,
+  ref,
+  watch,
+  watchEffect,
+} from '@vue/runtime-dom'
 import {
   createComponent,
   createIf,
+  createTextNode,
   renderEffect,
   setText,
   template,
@@ -44,33 +54,141 @@ describe('component', () => {
     expect(host.children[0]).toBe(childNode2)
   })
 
-  it.todo('should create an Component with props', () => {})
+  it('should create a Component with props', () => {
+    const { component: Comp } = define({
+      setup() {
+        return template('<div>', true)()
+      },
+    })
 
-  it.todo('should create an Component with direct text children', () => {})
+    const { host } = define({
+      setup() {
+        return createComponent(Comp, { id: () => 'foo', class: () => 'bar' })
+      },
+    }).render()
 
-  it.todo('should update an Component tag which is already mounted', () => {})
+    expect(host.innerHTML).toBe('<div id="foo" class="bar"></div>')
+  })
 
-  it.todo(
-    'should not update Component if only changed props are declared emit listeners',
-    () => {},
-  )
+  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)()
+      },
+    })
 
-  it.todo(
-    'component child synchronously updating parent state should trigger parent re-render',
-    async () => {},
-  )
+    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)
 
-  it.todo('instance.$el should be exposed to watch options', async () => {})
+    toggle.value = false
+    await nextTick()
+    expect(updatedSyp).toHaveBeenCalledTimes(0)
+  })
 
-  it.todo(
-    'component child updating parent state in pre-flush should trigger parent re-render',
-    async () => {},
-  )
+  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
+      },
+    })
 
-  it.todo(
-    'child only updates once when triggered in multiple ways',
-    async () => {},
-  )
+    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 },
+          {
+            default: () => createTextNode(() => [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.todo(
     `an earlier update doesn't lead to excessive subsequent updates`,

From a5acb4acd0902e1762b4e5bfafe9af069edddbe5 Mon Sep 17 00:00:00 2001
From: daiwei <daiwei521@126.com>
Date: Fri, 10 Jan 2025 14:40:42 +0800
Subject: [PATCH 3/4] test: update test

---
 .../__tests__/rendererComponent.spec.ts       |   4 +-
 .../runtime-vapor/__tests__/component.spec.ts | 102 +++++++++++++-----
 2 files changed, 80 insertions(+), 26 deletions(-)

diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts
index fa43b1015c5..fa3c192e885 100644
--- a/packages/runtime-core/__tests__/rendererComponent.spec.ts
+++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts
@@ -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 a Component with direct text children', () => {
+  it('should create a component with direct text children', () => {
     const Comp = {
       render: () => {
         return h('div', 'test')
diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
index dd014519a11..3a428c0e31c 100644
--- a/packages/runtime-vapor/__tests__/component.spec.ts
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -21,8 +21,6 @@ import type { VaporComponentInstance } from '../src/component'
 
 const define = makeRender()
 
-// TODO port tests from rendererComponent.spec.ts
-
 describe('component', () => {
   it('should update parent(hoc) component host el when child component self update', async () => {
     const value = ref(true)
@@ -54,7 +52,7 @@ describe('component', () => {
     expect(host.children[0]).toBe(childNode2)
   })
 
-  it('should create a Component with props', () => {
+  it('should create a component with props', () => {
     const { component: Comp } = define({
       setup() {
         return template('<div>', true)()
@@ -170,13 +168,7 @@ describe('component', () => {
 
     const { host } = define({
       setup() {
-        return createComponent(
-          Child,
-          { count: () => a.value },
-          {
-            default: () => createTextNode(() => [a.value]),
-          },
-        )
+        return createComponent(Child, { count: () => a.value })
       },
     }).render()
 
@@ -190,24 +182,86 @@ describe('component', () => {
     expect(calls).toEqual(['update child'])
   })
 
-  it.todo(
-    `an earlier update doesn't lead to excessive subsequent updates`,
-    async () => {},
-  )
+  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()
 
-  it.todo(
-    'should pause tracking deps when initializing legacy options',
-    async () => {},
-  )
+    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()
 
-  it.todo(
-    'child component props update should not lead to double update',
-    async () => {},
-  )
+    expect(host.innerHTML).toBe('<h1>0</h1>')
+    expect(spy).toHaveBeenCalledTimes(1)
 
-  it.todo('should warn accessing `this` in a <script setup> template', () => {})
+    text.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<h1>1</h1>')
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
 
-  it('unmountComponent', async () => {
+  it('unmount component', async () => {
     const { host, app, instance } = define(() => {
       const count = ref(0)
       const t0 = template('<div></div>')

From e1c5980ed4b70bc99023f5b38aacfe1bcc65f903 Mon Sep 17 00:00:00 2001
From: daiwei <daiwei521@126.com>
Date: Fri, 10 Jan 2025 14:48:40 +0800
Subject: [PATCH 4/4] test: add more cases

---
 .../runtime-vapor/__tests__/component.spec.ts | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts
index 3a428c0e31c..a4af5be7ab1 100644
--- a/packages/runtime-vapor/__tests__/component.spec.ts
+++ b/packages/runtime-vapor/__tests__/component.spec.ts
@@ -281,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()
+  })
 })