Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(runtime-vapor): port tests from rendererComponent.spec.ts #12677

Open
wants to merge 4 commits into
base: vapor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/runtime-core/__tests__/rendererComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand Down
285 changes: 280 additions & 5 deletions packages/runtime-vapor/__tests__/component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,267 @@
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>')
Expand All @@ -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()
})
})
Loading