wip(vapor): text hydration tests

This commit is contained in:
Evan You 2025-03-09 20:14:03 +08:00
parent 97c40a69fb
commit a2415de7bf
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
3 changed files with 101 additions and 117 deletions

View File

@ -69,7 +69,6 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
const anchor = (prevDynamics[0].anchor = context.increaseId()) const anchor = (prevDynamics[0].anchor = context.increaseId())
context.registerOperation({ context.registerOperation({
type: IRNodeTypes.INSERT_NODE, type: IRNodeTypes.INSERT_NODE,
elements: prevDynamics.map(child => child.id!), elements: prevDynamics.map(child => child.id!),

View File

@ -1,9 +1,19 @@
// import { type SSRContext, renderToString } from '@vue/server-renderer' // import { type SSRContext, renderToString } from '@vue/server-renderer'
import { createVaporSSRApp, renderEffect, setText, template } from '../src' import {
import { nextTick, ref } from '@vue/runtime-dom' child,
createVaporSSRApp,
delegateEvents,
next,
renderEffect,
setClass,
setText,
template,
} from '../src'
import { nextTick, ref, toDisplayString } from '@vue/runtime-dom'
function mountWithHydration(html: string, setup: () => any) { function mountWithHydration(html: string, setup: () => any) {
const container = document.createElement('div') const container = document.createElement('div')
document.body.appendChild(container)
container.innerHTML = html container.innerHTML = html
const app = createVaporSSRApp({ const app = createVaporSSRApp({
setup, setup,
@ -14,17 +24,19 @@ function mountWithHydration(html: string, setup: () => any) {
} }
} }
// const triggerEvent = (type: string, el: Element) => { const triggerEvent = (type: string, el: Element) => {
// const event = new Event(type) const event = new Event(type, { bubbles: true })
// el.dispatchEvent(event) el.dispatchEvent(event)
// } }
describe('SSR hydration', () => { describe('SSR hydration', () => {
delegateEvents('click')
beforeEach(() => { beforeEach(() => {
document.body.innerHTML = '' document.body.innerHTML = ''
}) })
test('text', async () => { test('root text', async () => {
const msg = ref('foo') const msg = ref('foo')
const t = template(' ') const t = template(' ')
const { container } = mountWithHydration('foo', () => { const { container } = mountWithHydration('foo', () => {
@ -38,128 +50,99 @@ describe('SSR hydration', () => {
expect(container.textContent).toBe('bar') expect(container.textContent).toBe('bar')
}) })
test('empty text', async () => { test('root comment', () => {
const t0 = template('<div></div>', true)
const { container } = mountWithHydration('<div></div>', () => t0())
expect(container.innerHTML).toBe('<div></div>')
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
})
test('comment', () => {
const t0 = template('<!---->') const t0 = template('<!---->')
const { container } = mountWithHydration('<!---->', () => t0()) const { container } = mountWithHydration('<!---->', () => t0())
expect(container.innerHTML).toBe('<!---->') expect(container.innerHTML).toBe('<!---->')
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned() expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
}) })
// test('static before text', () => { test('root with mixed element and text', async () => {
// const t0 = template(' A ') const t0 = template(' A')
// const t1 = template('<span>foo bar</span>') const t1 = template('<span>foo bar</span>')
// const t2 = template(' ') const t2 = template(' ')
// const msg = ref('hello') const msg = ref('hello')
// const { container } = mountWithHydration( const { container } = mountWithHydration(
// ' A <span>foo bar</span>hello', ' A<span>foo bar</span>hello',
// () => { () => {
// const n0 = t0() const n0 = t0()
// const n1 = t1() const n1 = t1()
// const n2 = t2() const n2 = t2()
// const n3 = createTextNode() renderEffect(() => setText(n2 as Text, toDisplayString(msg.value)))
// renderEffect(() => setText(n3, toDisplayString(msg.value))) return [n0, n1, n2]
// return [n0, n1, n2, n3] },
// }, )
// ) expect(container.innerHTML).toBe(' A<span>foo bar</span>hello')
// }) msg.value = 'bar'
await nextTick()
expect(container.innerHTML).toBe(' A<span>foo bar</span>bar')
})
// test('static (multiple elements)', () => { test('empty element', async () => {
// const staticContent = '<div></div><span>hello</span>' const t0 = template('<div></div>', true)
// const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>` const { container } = mountWithHydration('<div></div>', () => t0())
expect(container.innerHTML).toBe('<div></div>')
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
})
// const n1 = h('div', 'hi') test('element with text children', async () => {
// const s = createStaticVNode('', 2) const t0 = template('<div> </div>', true)
// const n2 = h('div', 'ho') const msg = ref('foo')
const { container } = mountWithHydration(
'<div class="foo">foo</div>',
() => {
const n0 = t0() as Element
const x0 = child(n0) as Text
renderEffect(() => {
const _msg = msg.value
// const { container } = mountWithHydration(html, () => h('div', [n1, s, n2])) setText(x0, toDisplayString(_msg))
setClass(n0, _msg)
})
return n0
},
)
expect(container.innerHTML).toBe(`<div class="foo">foo</div>`)
msg.value = 'bar'
await nextTick()
expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
})
// const div = container.firstChild! test('element with elements children', async () => {
const t0 = template('<div><span> </span><span></span></div>', true)
const msg = ref('foo')
const fn = vi.fn()
const { container } = mountWithHydration(
'<div><span>foo</span><span class="foo"></span></div>',
() => {
const n2 = t0() as Element
const n0 = child(n2) as Element
const n1 = next(n0) as Element
const x0 = child(n0) as Text
;(n1 as any).$evtclick = fn
renderEffect(() => {
const _msg = msg.value
// expect(n1.el).toBe(div.firstChild) setText(x0, toDisplayString(_msg))
// expect(n2.el).toBe(div.lastChild) setClass(n1, _msg)
// expect(s.el).toBe(div.childNodes[1]) })
// expect(s.anchor).toBe(div.childNodes[2]) return n2
// expect(s.children).toBe(staticContent) },
// }) )
expect(container.innerHTML).toBe(
`<div><span>foo</span><span class="foo"></span></div>`,
)
// // #6008 // event handler
// test('static (with text node as starting node)', () => { triggerEvent('click', container.querySelector('.foo')!)
// const html = ` A <span>foo</span> B` expect(fn).toHaveBeenCalled()
// const { vnode, container } = mountWithHydration(html, () =>
// createStaticVNode(` A <span>foo</span> B`, 3),
// )
// expect(vnode.el).toBe(container.firstChild)
// expect(vnode.anchor).toBe(container.lastChild)
// expect(`Hydration node mismatch`).not.toHaveBeenWarned()
// })
// test('static with content adoption', () => { msg.value = 'bar'
// const html = ` A <span>foo</span> B` await nextTick()
// const { vnode, container } = mountWithHydration(html, () => expect(container.innerHTML).toBe(
// createStaticVNode(``, 3), `<div><span>bar</span><span class="bar"></span></div>`,
// ) )
// expect(vnode.el).toBe(container.firstChild) })
// expect(vnode.anchor).toBe(container.lastChild)
// expect(vnode.children).toBe(html)
// expect(`Hydration node mismatch`).not.toHaveBeenWarned()
// })
// test('element with text children', async () => {
// const msg = ref('foo')
// const { vnode, container } = mountWithHydration(
// '<div class="foo">foo</div>',
// () => h('div', { class: msg.value }, msg.value),
// )
// expect(vnode.el).toBe(container.firstChild)
// expect(container.firstChild!.textContent).toBe('foo')
// msg.value = 'bar'
// await nextTick()
// expect(container.innerHTML).toBe(`<div class="bar">bar</div>`)
// })
// // #7285
// test('element with multiple continuous text vnodes', async () => {
// // should no mismatch warning
// const { container } = mountWithHydration('<div>foo0o</div>', () =>
// h('div', ['fo', createTextVNode('o'), 0, 'o']),
// )
// expect(container.textContent).toBe('foo0o')
// })
// test('element with elements children', async () => {
// const msg = ref('foo')
// const fn = vi.fn()
// const { vnode, container } = mountWithHydration(
// '<div><span>foo</span><span class="foo"></span></div>',
// () =>
// h('div', [
// h('span', msg.value),
// h('span', { class: msg.value, onClick: fn }),
// ]),
// )
// expect(vnode.el).toBe(container.firstChild)
// expect((vnode.children as VNode[])[0].el).toBe(
// container.firstChild!.childNodes[0],
// )
// expect((vnode.children as VNode[])[1].el).toBe(
// container.firstChild!.childNodes[1],
// )
// // event handler
// triggerEvent('click', vnode.el.querySelector('.foo')!)
// expect(fn).toHaveBeenCalled()
// msg.value = 'bar'
// await nextTick()
// expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
// })
// test('element with ref', () => { // test('element with ref', () => {
// const el = ref() // const el = ref()

View File

@ -116,6 +116,8 @@ function adoptHydrationNodeImpl(
!template.startsWith((adopted as Text).data)) !template.startsWith((adopted as Text).data))
) { ) {
// TODO recover and provide more info // TODO recover and provide more info
console.error(`adopted: `, adopted)
console.error(`template: ${template}`)
throw new Error('hydration mismatch!') throw new Error('hydration mismatch!')
} }
} }