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() + }) })