]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): more hydration
authorEvan You <evan@vuejs.org>
Fri, 7 Mar 2025 11:45:46 +0000 (19:45 +0800)
committerEvan You <evan@vuejs.org>
Fri, 7 Mar 2025 12:49:21 +0000 (20:49 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/dom/hydration.ts [moved from packages/runtime-vapor/src/dom/hydrate.ts with 83% similarity]
packages/runtime-vapor/src/dom/template.ts

diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
new file mode 100644 (file)
index 0000000..7422f6e
--- /dev/null
@@ -0,0 +1,2314 @@
+// import { type SSRContext, renderToString } from '@vue/server-renderer'
+import { createVaporSSRApp, renderEffect, setText, template } from '../src'
+import { nextTick, ref } from '@vue/runtime-dom'
+
+function mountWithHydration(html: string, setup: () => any) {
+  const container = document.createElement('div')
+  container.innerHTML = html
+  const app = createVaporSSRApp({
+    setup,
+  })
+  app.mount(container)
+  return {
+    container,
+  }
+}
+
+// const triggerEvent = (type: string, el: Element) => {
+//   const event = new Event(type)
+//   el.dispatchEvent(event)
+// }
+
+describe('SSR hydration', () => {
+  beforeEach(() => {
+    document.body.innerHTML = ''
+  })
+
+  test('text', async () => {
+    const msg = ref('foo')
+    const t = template(' ')
+    const { container } = mountWithHydration('foo', () => {
+      const n = t()
+      renderEffect(() => setText(n as Text, msg.value))
+      return n
+    })
+    expect(container.textContent).toBe('foo')
+    msg.value = 'bar'
+    await nextTick()
+    expect(container.textContent).toBe('bar')
+  })
+
+  test('empty text', async () => {
+    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 { container } = mountWithHydration('<!---->', () => t0())
+    expect(container.innerHTML).toBe('<!---->')
+    expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
+  })
+
+  // test('static before text', () => {
+  //   const t0 = template(' A ')
+  //   const t1 = template('<span>foo bar</span>')
+  //   const t2 = template(' ')
+  //   const msg = ref('hello')
+  //   const { container } = mountWithHydration(
+  //     ' A <span>foo bar</span>hello',
+  //     () => {
+  //       const n0 = t0()
+  //       const n1 = t1()
+  //       const n2 = t2()
+  //       const n3 = createTextNode()
+  //       renderEffect(() => setText(n3, toDisplayString(msg.value)))
+  //       return [n0, n1, n2, n3]
+  //     },
+  //   )
+  // })
+
+  // test('static (multiple elements)', () => {
+  //   const staticContent = '<div></div><span>hello</span>'
+  //   const html = `<div><div>hi</div>` + staticContent + `<div>ho</div></div>`
+
+  //   const n1 = h('div', 'hi')
+  //   const s = createStaticVNode('', 2)
+  //   const n2 = h('div', 'ho')
+
+  //   const { container } = mountWithHydration(html, () => h('div', [n1, s, n2]))
+
+  //   const div = container.firstChild!
+
+  //   expect(n1.el).toBe(div.firstChild)
+  //   expect(n2.el).toBe(div.lastChild)
+  //   expect(s.el).toBe(div.childNodes[1])
+  //   expect(s.anchor).toBe(div.childNodes[2])
+  //   expect(s.children).toBe(staticContent)
+  // })
+
+  // // #6008
+  // test('static (with text node as starting node)', () => {
+  //   const html = ` A <span>foo</span> B`
+  //   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', () => {
+  //   const html = ` A <span>foo</span> B`
+  //   const { vnode, container } = mountWithHydration(html, () =>
+  //     createStaticVNode(``, 3),
+  //   )
+  //   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', () => {
+  //   const el = ref()
+  //   const { vnode, container } = mountWithHydration('<div></div>', () =>
+  //     h('div', { ref: el }),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(el.value).toBe(vnode.el)
+  // })
+
+  // test('Fragment', 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.el.innerHTML).toBe(
+  //     `<!--[--><span>foo</span><!--[--><span class="foo"></span><!--]--><!--]-->`,
+  //   )
+
+  //   // start fragment 1
+  //   const fragment1 = (vnode.children as VNode[])[0]
+  //   expect(fragment1.el).toBe(vnode.el.childNodes[0])
+  //   const fragment1Children = fragment1.children as VNode[]
+
+  //   // first <span>
+  //   expect(fragment1Children[0].el!.tagName).toBe('SPAN')
+  //   expect(fragment1Children[0].el).toBe(vnode.el.childNodes[1])
+
+  //   // start fragment 2
+  //   const fragment2 = fragment1Children[1]
+  //   expect(fragment2.el).toBe(vnode.el.childNodes[2])
+  //   const fragment2Children = fragment2.children as VNode[]
+
+  //   // second <span>
+  //   expect(fragment2Children[0].el!.tagName).toBe('SPAN')
+  //   expect(fragment2Children[0].el).toBe(vnode.el.childNodes[3])
+
+  //   // end fragment 2
+  //   expect(fragment2.anchor).toBe(vnode.el.childNodes[4])
+
+  //   // end fragment 1
+  //   expect(fragment1.anchor).toBe(vnode.el.childNodes[5])
+
+  //   // 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><!--]--><!--]-->`,
+  //   )
+  // })
+
+  // // #7285
+  // test('Fragment (multiple continuous text vnodes)', async () => {
+  //   // should no mismatch warning
+  //   const { container } = mountWithHydration('<!--[-->fooo<!--]-->', () => [
+  //     'fo',
+  //     createTextVNode('o'),
+  //     'o',
+  //   ])
+  //   expect(container.textContent).toBe('fooo')
+  // })
+
+  // test('Teleport', async () => {
+  //   const msg = ref('foo')
+  //   const fn = vi.fn()
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport'
+  //   teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
+  //   document.body.appendChild(teleportContainer)
+
+  //   const { vnode, container } = mountWithHydration(
+  //     '<!--teleport start--><!--teleport end-->',
+  //     () =>
+  //       h(Teleport, { to: '#teleport' }, [
+  //         h('span', msg.value),
+  //         h('span', { class: msg.value, onClick: fn }),
+  //       ]),
+  //   )
+
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(vnode.anchor).toBe(container.lastChild)
+
+  //   expect(vnode.target).toBe(teleportContainer)
+  //   expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
+  //   expect((vnode.children as VNode[])[0].el).toBe(
+  //     teleportContainer.childNodes[1],
+  //   )
+  //   expect((vnode.children as VNode[])[1].el).toBe(
+  //     teleportContainer.childNodes[2],
+  //   )
+  //   expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+  //   // event handler
+  //   triggerEvent('click', teleportContainer.querySelector('.foo')!)
+  //   expect(fn).toHaveBeenCalled()
+
+  //   msg.value = 'bar'
+  //   await nextTick()
+  //   expect(teleportContainer.innerHTML).toBe(
+  //     `<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
+  //   )
+  // })
+
+  // test('Teleport (multiple + integration)', async () => {
+  //   const msg = ref('foo')
+  //   const fn1 = vi.fn()
+  //   const fn2 = vi.fn()
+
+  //   const Comp = () => [
+  //     h(Teleport, { to: '#teleport2' }, [
+  //       h('span', msg.value),
+  //       h('span', { class: msg.value, onClick: fn1 }),
+  //     ]),
+  //     h(Teleport, { to: '#teleport2' }, [
+  //       h('span', msg.value + '2'),
+  //       h('span', { class: msg.value + '2', onClick: fn2 }),
+  //     ]),
+  //   ]
+
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport2'
+  //   const ctx: SSRContext = {}
+  //   const mainHtml = await renderToString(h(Comp), ctx)
+  //   expect(mainHtml).toMatchInlineSnapshot(
+  //     `"<!--[--><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--><!--]-->"`,
+  //   )
+
+  //   const teleportHtml = ctx.teleports!['#teleport2']
+  //   expect(teleportHtml).toMatchInlineSnapshot(
+  //     `"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
+  //   )
+
+  //   teleportContainer.innerHTML = teleportHtml
+  //   document.body.appendChild(teleportContainer)
+
+  //   const { vnode, container } = mountWithHydration(mainHtml, Comp)
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   const teleportVnode1 = (vnode.children as VNode[])[0]
+  //   const teleportVnode2 = (vnode.children as VNode[])[1]
+  //   expect(teleportVnode1.el).toBe(container.childNodes[1])
+  //   expect(teleportVnode1.anchor).toBe(container.childNodes[2])
+  //   expect(teleportVnode2.el).toBe(container.childNodes[3])
+  //   expect(teleportVnode2.anchor).toBe(container.childNodes[4])
+
+  //   expect(teleportVnode1.target).toBe(teleportContainer)
+  //   expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
+  //   expect((teleportVnode1 as any).children[0].el).toBe(
+  //     teleportContainer.childNodes[1],
+  //   )
+  //   expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+  //   expect(teleportVnode2.target).toBe(teleportContainer)
+  //   expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
+  //   expect((teleportVnode2 as any).children[0].el).toBe(
+  //     teleportContainer.childNodes[5],
+  //   )
+  //   expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
+
+  //   // // event handler
+  //   triggerEvent('click', teleportContainer.querySelector('.foo')!)
+  //   expect(fn1).toHaveBeenCalled()
+
+  //   triggerEvent('click', teleportContainer.querySelector('.foo2')!)
+  //   expect(fn2).toHaveBeenCalled()
+
+  //   msg.value = 'bar'
+  //   await nextTick()
+  //   expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
+  //     `"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
+  //   )
+  // })
+
+  // test('Teleport (disabled)', async () => {
+  //   const msg = ref('foo')
+  //   const fn1 = vi.fn()
+  //   const fn2 = vi.fn()
+
+  //   const Comp = () => [
+  //     h('div', 'foo'),
+  //     h(Teleport, { to: '#teleport3', disabled: true }, [
+  //       h('span', msg.value),
+  //       h('span', { class: msg.value, onClick: fn1 }),
+  //     ]),
+  //     h('div', { class: msg.value + '2', onClick: fn2 }, 'bar'),
+  //   ]
+
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport3'
+  //   const ctx: SSRContext = {}
+  //   const mainHtml = await renderToString(h(Comp), ctx)
+  //   expect(mainHtml).toMatchInlineSnapshot(
+  //     `"<!--[--><div>foo</div><!--teleport start--><span>foo</span><span class="foo"></span><!--teleport end--><div class="foo2">bar</div><!--]-->"`,
+  //   )
+
+  //   const teleportHtml = ctx.teleports!['#teleport3']
+  //   expect(teleportHtml).toMatchInlineSnapshot(
+  //     `"<!--teleport start anchor--><!--teleport anchor-->"`,
+  //   )
+
+  //   teleportContainer.innerHTML = teleportHtml
+  //   document.body.appendChild(teleportContainer)
+
+  //   const { vnode, container } = mountWithHydration(mainHtml, Comp)
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   const children = vnode.children as VNode[]
+
+  //   expect(children[0].el).toBe(container.childNodes[1])
+
+  //   const teleportVnode = children[1]
+  //   expect(teleportVnode.el).toBe(container.childNodes[2])
+  //   expect((teleportVnode.children as VNode[])[0].el).toBe(
+  //     container.childNodes[3],
+  //   )
+  //   expect((teleportVnode.children as VNode[])[1].el).toBe(
+  //     container.childNodes[4],
+  //   )
+  //   expect(teleportVnode.anchor).toBe(container.childNodes[5])
+  //   expect(children[2].el).toBe(container.childNodes[6])
+
+  //   expect(teleportVnode.target).toBe(teleportContainer)
+  //   expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
+  //   expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
+
+  //   // // event handler
+  //   triggerEvent('click', container.querySelector('.foo')!)
+  //   expect(fn1).toHaveBeenCalled()
+
+  //   triggerEvent('click', container.querySelector('.foo2')!)
+  //   expect(fn2).toHaveBeenCalled()
+
+  //   msg.value = 'bar'
+  //   await nextTick()
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<!--[--><div>foo</div><!--teleport start--><span>bar</span><span class="bar"></span><!--teleport end--><div class="bar2">bar</div><!--]-->"`,
+  //   )
+  // })
+
+  // // #6152
+  // test('Teleport (disabled + as component root)', () => {
+  //   const { container } = mountWithHydration(
+  //     '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
+  //     () => [
+  //       h('div', 'Parent fragment'),
+  //       h(() =>
+  //         h(Teleport, { to: 'body', disabled: true }, [
+  //           h('div', 'Teleport content'),
+  //         ]),
+  //       ),
+  //     ],
+  //   )
+  //   expect(document.body.innerHTML).toBe('')
+  //   expect(container.innerHTML).toBe(
+  //     '<!--[--><div>Parent fragment</div><!--teleport start--><div>Teleport content</div><!--teleport end--><!--]-->',
+  //   )
+  //   expect(
+  //     `Hydration completed but contains mismatches.`,
+  //   ).not.toHaveBeenWarned()
+  // })
+
+  // test('Teleport (as component root)', () => {
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport4'
+  //   teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
+  //   document.body.appendChild(teleportContainer)
+
+  //   const wrapper = {
+  //     render() {
+  //       return h(Teleport, { to: '#teleport4' }, ['hello'])
+  //     },
+  //   }
+
+  //   const { vnode, container } = mountWithHydration(
+  //     '<div><!--teleport start--><!--teleport end--><div></div></div>',
+  //     () => h('div', [h(wrapper), h('div')]),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   // component el
+  //   const wrapperVNode = (vnode as any).children[0]
+  //   const tpStart = container.firstChild?.firstChild
+  //   const tpEnd = tpStart?.nextSibling
+  //   expect(wrapperVNode.el).toBe(tpStart)
+  //   expect(wrapperVNode.component.subTree.el).toBe(tpStart)
+  //   expect(wrapperVNode.component.subTree.anchor).toBe(tpEnd)
+  //   // next node hydrate properly
+  //   const nextVNode = (vnode as any).children[1]
+  //   expect(nextVNode.el).toBe(container.firstChild?.lastChild)
+  // })
+
+  // test('Teleport (nested)', () => {
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport5'
+  //   teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
+  //   document.body.appendChild(teleportContainer)
+
+  //   const { vnode, container } = mountWithHydration(
+  //     '<!--teleport start--><!--teleport end-->',
+  //     () =>
+  //       h(Teleport, { to: '#teleport5' }, [
+  //         h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])]),
+  //       ]),
+  //   )
+
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(vnode.anchor).toBe(container.lastChild)
+
+  //   const childDivVNode = (vnode as any).children[0]
+  //   const div = teleportContainer.childNodes[1]
+  //   expect(childDivVNode.el).toBe(div)
+  //   expect(vnode.targetAnchor).toBe(div?.nextSibling)
+
+  //   const childTeleportVNode = childDivVNode.children[0]
+  //   expect(childTeleportVNode.el).toBe(div?.firstChild)
+  //   expect(childTeleportVNode.anchor).toBe(div?.lastChild)
+
+  //   expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
+  //   expect(childTeleportVNode.children[0].el).toBe(
+  //     teleportContainer.lastChild?.previousSibling,
+  //   )
+  // })
+
+  // test('with data-allow-mismatch component when using onServerPrefetch', async () => {
+  //   const Comp = {
+  //     template: `
+  //       <div>Comp2</div>
+  //     `,
+  //   }
+  //   let foo: any
+  //   const App = {
+  //     setup() {
+  //       const flag = ref(true)
+  //       foo = () => {
+  //         flag.value = false
+  //       }
+  //       onServerPrefetch(() => (flag.value = false))
+  //       return { flag }
+  //     },
+  //     components: {
+  //       Comp,
+  //     },
+  //     template: `
+  //       <span data-allow-mismatch>
+  //         <Comp v-if="flag"></Comp>
+  //       </span>
+  //     `,
+  //   }
+  //   // hydrate
+  //   const container = document.createElement('div')
+  //   container.innerHTML = await renderToString(h(App))
+  //   createSSRApp(App).mount(container)
+  //   expect(container.innerHTML).toBe(
+  //     '<span data-allow-mismatch=""><div>Comp2</div></span>',
+  //   )
+  //   foo()
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe(
+  //     '<span data-allow-mismatch=""><!--v-if--></span>',
+  //   )
+  // })
+
+  // test('Teleport unmount (full integration)', async () => {
+  //   const Comp1 = {
+  //     template: `
+  //       <Teleport to="#target">
+  //         <span>Teleported Comp1</span>
+  //       </Teleport>
+  //     `,
+  //   }
+  //   const Comp2 = {
+  //     template: `
+  //       <div>Comp2</div>
+  //     `,
+  //   }
+
+  //   const toggle = ref(true)
+  //   const App = {
+  //     template: `
+  //       <div>
+  //         <Comp1 v-if="toggle"/>
+  //         <Comp2 v-else/>
+  //       </div>
+  //     `,
+  //     components: {
+  //       Comp1,
+  //       Comp2,
+  //     },
+  //     setup() {
+  //       return { toggle }
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'target'
+  //   document.body.appendChild(teleportContainer)
+
+  //   // server render
+  //   const ctx: SSRContext = {}
+  //   container.innerHTML = await renderToString(h(App), ctx)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   teleportContainer.innerHTML = ctx.teleports!['#target']
+
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   expect(teleportContainer.innerHTML).toBe(
+  //     '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
+  //   )
+  //   expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+
+  //   toggle.value = false
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+  //   expect(teleportContainer.innerHTML).toBe('')
+  // })
+
+  // test('Teleport unmount (mismatch + full integration)', async () => {
+  //   const Comp1 = {
+  //     template: `
+  //       <Teleport to="#target">
+  //         <span>Teleported Comp1</span>
+  //       </Teleport>
+  //     `,
+  //   }
+  //   const Comp2 = {
+  //     template: `
+  //       <div>Comp2</div>
+  //     `,
+  //   }
+
+  //   const toggle = ref(true)
+  //   const App = {
+  //     template: `
+  //       <div>
+  //         <Comp1 v-if="toggle"/>
+  //         <Comp2 v-else/>
+  //       </div>
+  //     `,
+  //     components: {
+  //       Comp1,
+  //       Comp2,
+  //     },
+  //     setup() {
+  //       return { toggle }
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'target'
+  //   document.body.appendChild(teleportContainer)
+
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   expect(teleportContainer.innerHTML).toBe('')
+
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+  //   toggle.value = false
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+  //   expect(teleportContainer.innerHTML).toBe('')
+  // })
+
+  // test('Teleport target change (mismatch + full integration)', async () => {
+  //   const target = ref('#target1')
+  //   const Comp = {
+  //     template: `
+  //       <Teleport :to="target">
+  //         <span>Teleported</span>
+  //       </Teleport>
+  //     `,
+  //     setup() {
+  //       return { target }
+  //     },
+  //   }
+
+  //   const App = {
+  //     template: `
+  //       <div>
+  //         <Comp />
+  //       </div>
+  //     `,
+  //     components: {
+  //       Comp,
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   const teleportContainer1 = document.createElement('div')
+  //   teleportContainer1.id = 'target1'
+  //   const teleportContainer2 = document.createElement('div')
+  //   teleportContainer2.id = 'target2'
+  //   document.body.appendChild(teleportContainer1)
+  //   document.body.appendChild(teleportContainer2)
+
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   expect(teleportContainer1.innerHTML).toBe('')
+  //   expect(teleportContainer2.innerHTML).toBe('')
+
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--teleport start--><!--teleport end--></div>',
+  //   )
+  //   expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
+  //   expect(teleportContainer2.innerHTML).toBe('')
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+  //   target.value = '#target2'
+  //   await nextTick()
+  //   expect(teleportContainer1.innerHTML).toBe('')
+  //   expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
+  // })
+
+  // // compile SSR + client render fn from the same template & hydrate
+  // test('full compiler integration', async () => {
+  //   const mounted: string[] = []
+  //   const log = vi.fn()
+  //   const toggle = ref(true)
+
+  //   const Child = {
+  //     data() {
+  //       return {
+  //         count: 0,
+  //         text: 'hello',
+  //         style: {
+  //           color: 'red',
+  //         },
+  //       }
+  //     },
+  //     mounted() {
+  //       mounted.push('child')
+  //     },
+  //     template: `
+  //     <div>
+  //       <span class="count" :style="style">{{ count }}</span>
+  //       <button class="inc" @click="count++">inc</button>
+  //       <button class="change" @click="style.color = 'green'" >change color</button>
+  //       <button class="emit" @click="$emit('foo')">emit</button>
+  //       <span class="text">{{ text }}</span>
+  //       <input v-model="text">
+  //     </div>
+  //     `,
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return { toggle }
+  //     },
+  //     mounted() {
+  //       mounted.push('parent')
+  //     },
+  //     template: `
+  //       <div>
+  //         <span>hello</span>
+  //         <template v-if="toggle">
+  //           <Child @foo="log('child')"/>
+  //           <template v-if="true">
+  //             <button class="parent-click" @click="log('click')">click me</button>
+  //           </template>
+  //         </template>
+  //         <span>hello</span>
+  //       </div>`,
+  //     components: {
+  //       Child,
+  //     },
+  //     methods: {
+  //       log,
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+
+  //   // assert interactions
+  //   // 1. parent button click
+  //   triggerEvent('click', container.querySelector('.parent-click')!)
+  //   expect(log).toHaveBeenCalledWith('click')
+
+  //   // 2. child inc click + text interpolation
+  //   const count = container.querySelector('.count') as HTMLElement
+  //   expect(count.textContent).toBe(`0`)
+  //   triggerEvent('click', container.querySelector('.inc')!)
+  //   await nextTick()
+  //   expect(count.textContent).toBe(`1`)
+
+  //   // 3. child color click + style binding
+  //   expect(count.style.color).toBe('red')
+  //   triggerEvent('click', container.querySelector('.change')!)
+  //   await nextTick()
+  //   expect(count.style.color).toBe('green')
+
+  //   // 4. child event emit
+  //   triggerEvent('click', container.querySelector('.emit')!)
+  //   expect(log).toHaveBeenCalledWith('child')
+
+  //   // 5. child v-model
+  //   const text = container.querySelector('.text')!
+  //   const input = container.querySelector('input')!
+  //   expect(text.textContent).toBe('hello')
+  //   input.value = 'bye'
+  //   triggerEvent('input', input)
+  //   await nextTick()
+  //   expect(text.textContent).toBe('bye')
+  // })
+
+  // test('handle click error in ssr mode', async () => {
+  //   const App = {
+  //     setup() {
+  //       const throwError = () => {
+  //         throw new Error('Sentry Error')
+  //       }
+  //       return { throwError }
+  //     },
+  //     template: `
+  //       <div>
+  //         <button class="parent-click" @click="throwError">click me</button>
+  //       </div>`,
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   const app = createSSRApp(App)
+  //   const handler = (app.config.errorHandler = vi.fn())
+  //   app.mount(container)
+  //   // assert interactions
+  //   // parent button click
+  //   triggerEvent('click', container.querySelector('.parent-click')!)
+  //   expect(handler).toHaveBeenCalled()
+  // })
+
+  // test('handle blur error in ssr mode', async () => {
+  //   const App = {
+  //     setup() {
+  //       const throwError = () => {
+  //         throw new Error('Sentry Error')
+  //       }
+  //       return { throwError }
+  //     },
+  //     template: `
+  //       <div>
+  //         <input class="parent-click" @blur="throwError"/>
+  //       </div>`,
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   // hydrate
+  //   const app = createSSRApp(App)
+  //   const handler = (app.config.errorHandler = vi.fn())
+  //   app.mount(container)
+  //   // assert interactions
+  //   // parent blur event
+  //   triggerEvent('blur', container.querySelector('.parent-click')!)
+  //   expect(handler).toHaveBeenCalled()
+  // })
+
+  // test('Suspense', async () => {
+  //   const AsyncChild = {
+  //     async setup() {
+  //       const count = ref(0)
+  //       return () =>
+  //         h(
+  //           'span',
+  //           {
+  //             onClick: () => {
+  //               count.value++
+  //             },
+  //           },
+  //           count.value,
+  //         )
+  //     },
+  //   }
+  //   const { vnode, container } = mountWithHydration('<span>0</span>', () =>
+  //     h(Suspense, () => h(AsyncChild)),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   // wait for hydration to finish
+  //   await new Promise(r => setTimeout(r))
+  //   triggerEvent('click', container.querySelector('span')!)
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe(`<span>1</span>`)
+  // })
+
+  // // #6638
+  // test('Suspense + async component', async () => {
+  //   let isSuspenseResolved = false
+  //   let isSuspenseResolvedInChild: any
+  //   const AsyncChild = defineAsyncComponent(() =>
+  //     Promise.resolve(
+  //       defineComponent({
+  //         setup() {
+  //           isSuspenseResolvedInChild = isSuspenseResolved
+  //           const count = ref(0)
+  //           return () =>
+  //             h(
+  //               'span',
+  //               {
+  //                 onClick: () => {
+  //                   count.value++
+  //                 },
+  //               },
+  //               count.value,
+  //             )
+  //         },
+  //       }),
+  //     ),
+  //   )
+  //   const { vnode, container } = mountWithHydration('<span>0</span>', () =>
+  //     h(
+  //       Suspense,
+  //       {
+  //         onResolve() {
+  //           isSuspenseResolved = true
+  //         },
+  //       },
+  //       () => h(AsyncChild),
+  //     ),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   // wait for hydration to finish
+  //   await new Promise(r => setTimeout(r))
+
+  //   expect(isSuspenseResolvedInChild).toBe(false)
+  //   expect(isSuspenseResolved).toBe(true)
+
+  //   // assert interaction
+  //   triggerEvent('click', container.querySelector('span')!)
+  //   await nextTick()
+  //   expect(container.innerHTML).toBe(`<span>1</span>`)
+  // })
+
+  // test('Suspense (full integration)', async () => {
+  //   const mountedCalls: number[] = []
+  //   const asyncDeps: Promise<any>[] = []
+
+  //   const AsyncChild = defineComponent({
+  //     props: ['n'],
+  //     async setup(props) {
+  //       const count = ref(props.n)
+  //       onMounted(() => {
+  //         mountedCalls.push(props.n)
+  //       })
+  //       const p = new Promise(r => setTimeout(r, props.n * 10))
+  //       asyncDeps.push(p)
+  //       await p
+  //       return () =>
+  //         h(
+  //           'span',
+  //           {
+  //             onClick: () => {
+  //               count.value++
+  //             },
+  //           },
+  //           count.value,
+  //         )
+  //     },
+  //   })
+
+  //   const done = vi.fn()
+  //   const App = {
+  //     template: `
+  //     <Suspense @resolve="done">
+  //       <div>
+  //         <AsyncChild :n="1" />
+  //         <AsyncChild :n="2" />
+  //       </div>
+  //     </Suspense>`,
+  //     components: {
+  //       AsyncChild,
+  //     },
+  //     methods: {
+  //       done,
+  //     },
+  //   }
+
+  //   const container = document.createElement('div')
+  //   // server render
+  //   container.innerHTML = await renderToString(h(App))
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<div><span>1</span><span>2</span></div>"`,
+  //   )
+  //   // reset asyncDeps from ssr
+  //   asyncDeps.length = 0
+  //   // hydrate
+  //   createSSRApp(App).mount(container)
+
+  //   expect(mountedCalls.length).toBe(0)
+  //   expect(asyncDeps.length).toBe(2)
+
+  //   // wait for hydration to complete
+  //   await Promise.all(asyncDeps)
+  //   await new Promise(r => setTimeout(r))
+
+  //   // should flush buffered effects
+  //   expect(mountedCalls).toMatchObject([1, 2])
+  //   expect(container.innerHTML).toMatch(
+  //     `<div><span>1</span><span>2</span></div>`,
+  //   )
+
+  //   const span1 = container.querySelector('span')!
+  //   triggerEvent('click', span1)
+  //   await nextTick()
+  //   expect(container.innerHTML).toMatch(
+  //     `<div><span>2</span><span>2</span></div>`,
+  //   )
+
+  //   const span2 = span1.nextSibling as Element
+  //   triggerEvent('click', span2)
+  //   await nextTick()
+  //   expect(container.innerHTML).toMatch(
+  //     `<div><span>2</span><span>3</span></div>`,
+  //   )
+  // })
+
+  // test('async component', async () => {
+  //   const spy = vi.fn()
+  //   const Comp = () =>
+  //     h(
+  //       'button',
+  //       {
+  //         onClick: spy,
+  //       },
+  //       'hello!',
+  //     )
+
+  //   let serverResolve: any
+  //   let AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         serverResolve = r
+  //       }),
+  //   )
+
+  //   const App = {
+  //     render() {
+  //       return ['hello', h(AsyncComp), 'world']
+  //     },
+  //   }
+
+  //   // server render
+  //   const htmlPromise = renderToString(h(App))
+  //   serverResolve(Comp)
+  //   const html = await htmlPromise
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
+  //   )
+
+  //   // hydration
+  //   let clientResolve: any
+  //   AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         clientResolve = r
+  //       }),
+  //   )
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   // hydration not complete yet
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   expect(spy).not.toHaveBeenCalled()
+
+  //   // resolve
+  //   clientResolve(Comp)
+  //   await new Promise(r => setTimeout(r))
+
+  //   // should be hydrated now
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   expect(spy).toHaveBeenCalled()
+  // })
+
+  // test('update async wrapper before resolve', async () => {
+  //   const Comp = {
+  //     render() {
+  //       return h('h1', 'Async component')
+  //     },
+  //   }
+  //   let serverResolve: any
+  //   let AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         serverResolve = r
+  //       }),
+  //   )
+
+  //   const toggle = ref(true)
+  //   const App = {
+  //     setup() {
+  //       onMounted(() => {
+  //         // change state, this makes updateComponent(AsyncComp) execute before
+  //         // the async component is resolved
+  //         toggle.value = false
+  //       })
+
+  //       return () => {
+  //         return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
+  //       }
+  //     },
+  //   }
+
+  //   // server render
+  //   const htmlPromise = renderToString(h(App))
+  //   serverResolve(Comp)
+  //   const html = await htmlPromise
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
+  //   )
+
+  //   // hydration
+  //   let clientResolve: any
+  //   AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         clientResolve = r
+  //       }),
+  //   )
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   // resolve
+  //   clientResolve(Comp)
+  //   await new Promise(r => setTimeout(r))
+
+  //   // should be hydrated now
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<!--[-->world<h1>Async component</h1><!--]-->"`,
+  //   )
+  // })
+
+  // test('hydrate safely when property used by async setup changed before render', async () => {
+  //   const toggle = ref(true)
+
+  //   const AsyncComp = {
+  //     async setup() {
+  //       await new Promise<void>(r => setTimeout(r, 10))
+  //       return () => h('h1', 'Async component')
+  //     },
+  //   }
+
+  //   const AsyncWrapper = {
+  //     render() {
+  //       return h(AsyncComp)
+  //     },
+  //   }
+
+  //   const SiblingComp = {
+  //     setup() {
+  //       toggle.value = false
+  //       return () => h('span')
+  //     },
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return () =>
+  //         h(
+  //           Suspense,
+  //           {},
+  //           {
+  //             default: () => [
+  //               h('main', {}, [
+  //                 h(AsyncWrapper, {
+  //                   prop: toggle.value ? 'hello' : 'world',
+  //                 }),
+  //                 h(SiblingComp),
+  //               ]),
+  //             ],
+  //           },
+  //         )
+  //     },
+  //   }
+
+  //   // server render
+  //   const html = await renderToString(h(App))
+
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
+  //   )
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // hydration
+
+  //   // reset the value
+  //   toggle.value = true
+  //   expect(toggle.value).toBe(true)
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   await new Promise(r => setTimeout(r, 10))
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // should be hydrated now
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
+  //   )
+  // })
+
+  // test('hydrate safely when property used by deep nested async setup changed before render', async () => {
+  //   const toggle = ref(true)
+
+  //   const AsyncComp = {
+  //     async setup() {
+  //       await new Promise<void>(r => setTimeout(r, 10))
+  //       return () => h('h1', 'Async component')
+  //     },
+  //   }
+
+  //   const AsyncWrapper = { render: () => h(AsyncComp) }
+  //   const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
+
+  //   const SiblingComp = {
+  //     setup() {
+  //       toggle.value = false
+  //       return () => h('span')
+  //     },
+  //   }
+
+  //   const App = {
+  //     setup() {
+  //       return () =>
+  //         h(
+  //           Suspense,
+  //           {},
+  //           {
+  //             default: () => [
+  //               h('main', {}, [
+  //                 h(AsyncWrapperWrapper, {
+  //                   prop: toggle.value ? 'hello' : 'world',
+  //                 }),
+  //                 h(SiblingComp),
+  //               ]),
+  //             ],
+  //           },
+  //         )
+  //     },
+  //   }
+
+  //   // server render
+  //   const html = await renderToString(h(App))
+
+  //   expect(html).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
+  //   )
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // hydration
+
+  //   // reset the value
+  //   toggle.value = true
+  //   expect(toggle.value).toBe(true)
+
+  //   const container = document.createElement('div')
+  //   container.innerHTML = html
+  //   createSSRApp(App).mount(container)
+
+  //   await new Promise(r => setTimeout(r, 10))
+
+  //   expect(toggle.value).toBe(false)
+
+  //   // should be hydrated now
+  //   expect(container.innerHTML).toMatchInlineSnapshot(
+  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
+  //   )
+  // })
+
+  // // #3787
+  // test('unmount async wrapper before load', async () => {
+  //   let resolve: any
+  //   const AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         resolve = r
+  //       }),
+  //   )
+
+  //   const show = ref(true)
+  //   const root = document.createElement('div')
+  //   root.innerHTML = '<div><div>async</div></div>'
+
+  //   createSSRApp({
+  //     render() {
+  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+  //     },
+  //   }).mount(root)
+
+  //   show.value = false
+  //   await nextTick()
+  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
+  //   resolve({})
+  // })
+
+  // //#12362
+  // test('nested async wrapper', async () => {
+  //   const Toggle = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         r(
+  //           defineComponent({
+  //             setup(_, { slots }) {
+  //               const show = ref(false)
+  //               onMounted(() => {
+  //                 nextTick(() => {
+  //                   show.value = true
+  //                 })
+  //               })
+  //               return () =>
+  //                 withDirectives(
+  //                   h('div', null, [renderSlot(slots, 'default')]),
+  //                   [[vShow, show.value]],
+  //                 )
+  //             },
+  //           }) as any,
+  //         )
+  //       }),
+  //   )
+
+  //   const Wrapper = defineAsyncComponent(() => {
+  //     return new Promise(r => {
+  //       r(
+  //         defineComponent({
+  //           render(this: any) {
+  //             return renderSlot(this.$slots, 'default')
+  //           },
+  //         }) as any,
+  //       )
+  //     })
+  //   })
+
+  //   const count = ref(0)
+  //   const fn = vi.fn()
+  //   const Child = {
+  //     setup() {
+  //       onMounted(() => {
+  //         fn()
+  //         count.value++
+  //       })
+  //       return () => h('div', count.value)
+  //     },
+  //   }
+
+  //   const App = {
+  //     render() {
+  //       return h(Toggle, null, {
+  //         default: () =>
+  //           h(Wrapper, null, {
+  //             default: () =>
+  //               h(Wrapper, null, {
+  //                 default: () => h(Child),
+  //               }),
+  //           }),
+  //       })
+  //     },
+  //   }
+
+  //   const root = document.createElement('div')
+  //   root.innerHTML = await renderToString(h(App))
+  //   expect(root.innerHTML).toMatchInlineSnapshot(
+  //     `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
+  //   )
+
+  //   createSSRApp(App).mount(root)
+  //   await nextTick()
+  //   await nextTick()
+  //   expect(root.innerHTML).toMatchInlineSnapshot(
+  //     `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
+  //   )
+  //   expect(fn).toBeCalledTimes(1)
+  // })
+
+  // test('unmount async wrapper before load (fragment)', async () => {
+  //   let resolve: any
+  //   const AsyncComp = defineAsyncComponent(
+  //     () =>
+  //       new Promise(r => {
+  //         resolve = r
+  //       }),
+  //   )
+
+  //   const show = ref(true)
+  //   const root = document.createElement('div')
+  //   root.innerHTML = '<div><!--[-->async<!--]--></div>'
+
+  //   createSSRApp({
+  //     render() {
+  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
+  //     },
+  //   }).mount(root)
+
+  //   show.value = false
+  //   await nextTick()
+  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
+  //   resolve({})
+  // })
+
+  // test('elements with camel-case in svg ', () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     '<animateTransform></animateTransform>',
+  //     () => h('animateTransform'),
+  //   )
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('SVG as a mount container', () => {
+  //   const svgContainer = document.createElement('svg')
+  //   svgContainer.innerHTML = '<g></g>'
+  //   const app = createSSRApp({
+  //     render: () => h('g'),
+  //   })
+
+  //   expect(
+  //     (
+  //       app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
+  //         el: Element
+  //       }
+  //     ).el instanceof SVGElement,
+  //   )
+  // })
+
+  // test('force hydrate prop with `.prop` modifier', () => {
+  //   const { container } = mountWithHydration('<input type="checkbox">', () =>
+  //     h('input', {
+  //       type: 'checkbox',
+  //       '.indeterminate': true,
+  //     }),
+  //   )
+  //   expect((container.firstChild! as any).indeterminate).toBe(true)
+  // })
+
+  // test('force hydrate input v-model with non-string value bindings', () => {
+  //   const { container } = mountWithHydration(
+  //     '<input type="checkbox" value="true">',
+  //     () =>
+  //       withDirectives(
+  //         createVNode(
+  //           'input',
+  //           { type: 'checkbox', 'true-value': true },
+  //           null,
+  //           PatchFlags.PROPS,
+  //           ['true-value'],
+  //         ),
+  //         [[vModelCheckbox, true]],
+  //       ),
+  //   )
+  //   expect((container.firstChild as any)._trueValue).toBe(true)
+  // })
+
+  // test('force hydrate checkbox with indeterminate', () => {
+  //   const { container } = mountWithHydration(
+  //     '<input type="checkbox" indeterminate>',
+  //     () =>
+  //       createVNode(
+  //         'input',
+  //         { type: 'checkbox', indeterminate: '' },
+  //         null,
+  //         PatchFlags.CACHED,
+  //       ),
+  //   )
+  //   expect((container.firstChild as any).indeterminate).toBe(true)
+  // })
+
+  // test('force hydrate select option with non-string value bindings', () => {
+  //   const { container } = mountWithHydration(
+  //     '<select><option value="true">ok</option></select>',
+  //     () =>
+  //       h('select', [
+  //         // hoisted because bound value is a constant...
+  //         createVNode('option', { value: true }, null, -1 /* HOISTED */),
+  //       ]),
+  //   )
+  //   expect((container.firstChild!.firstChild as any)._value).toBe(true)
+  // })
+
+  // // #7203
+  // test('force hydrate custom element with dynamic props', () => {
+  //   class MyElement extends HTMLElement {
+  //     foo = ''
+  //     constructor() {
+  //       super()
+  //     }
+  //   }
+  //   customElements.define('my-element-7203', MyElement)
+
+  //   const msg = ref('bar')
+  //   const container = document.createElement('div')
+  //   container.innerHTML = '<my-element-7203></my-element-7203>'
+  //   const app = createSSRApp({
+  //     render: () => h('my-element-7203', { foo: msg.value }),
+  //   })
+  //   app.mount(container)
+  //   expect((container.firstChild as any).foo).toBe(msg.value)
+  // })
+
+  // // #5728
+  // test('empty text node in slot', () => {
+  //   const Comp = {
+  //     render(this: any) {
+  //       return renderSlot(this.$slots, 'default', {}, () => [
+  //         createTextVNode(''),
+  //       ])
+  //     },
+  //   }
+  //   const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
+  //     h(Comp),
+  //   )
+  //   expect(container.childNodes.length).toBe(3)
+  //   const text = container.childNodes[1]
+  //   expect(text.nodeType).toBe(3)
+  //   expect(vnode.el).toBe(container.childNodes[0])
+  //   // component => slot fragment => text node
+  //   expect((vnode as any).component?.subTree.children[0].el).toBe(text)
+  // })
+
+  // // #7215
+  // test('empty text node', () => {
+  //   const Comp = {
+  //     render(this: any) {
+  //       return h('p', [''])
+  //     },
+  //   }
+  //   const { container } = mountWithHydration('<p></p>', () => h(Comp))
+  //   expect(container.childNodes.length).toBe(1)
+  //   const p = container.childNodes[0]
+  //   expect(p.childNodes.length).toBe(1)
+  //   const text = p.childNodes[0]
+  //   expect(text.nodeType).toBe(3)
+  // })
+
+  // // #11372
+  // test('object style value tracking in prod', async () => {
+  //   __DEV__ = false
+  //   try {
+  //     const style = reactive({ color: 'red' })
+  //     const Comp = {
+  //       render(this: any) {
+  //         return (
+  //           openBlock(),
+  //           createElementBlock(
+  //             'div',
+  //             {
+  //               style: normalizeStyle(style),
+  //             },
+  //             null,
+  //             4 /* STYLE */,
+  //           )
+  //         )
+  //       },
+  //     }
+  //     const { container } = mountWithHydration(
+  //       `<div style="color: red;"></div>`,
+  //       () => h(Comp),
+  //     )
+  //     style.color = 'green'
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
+  //   } finally {
+  //     __DEV__ = true
+  //   }
+  // })
+
+  // test('app.unmount()', async () => {
+  //   const container = document.createElement('DIV')
+  //   container.innerHTML = '<button></button>'
+  //   const App = defineComponent({
+  //     setup(_, { expose }) {
+  //       const count = ref(0)
+
+  //       expose({ count })
+
+  //       return () =>
+  //         h('button', {
+  //           onClick: () => count.value++,
+  //         })
+  //     },
+  //   })
+
+  //   const app = createSSRApp(App)
+  //   const vm = app.mount(container)
+  //   await nextTick()
+  //   expect((container as any)._vnode).toBeDefined()
+  //   // @ts-expect-error - expose()'d properties are not available on vm type
+  //   expect(vm.count).toBe(0)
+
+  //   app.unmount()
+  //   expect((container as any)._vnode).toBe(null)
+  // })
+
+  // // #6637
+  // test('stringified root fragment', () => {
+  //   mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
+  //     createStaticVNode(`<div></div>`, 1),
+  //   )
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear', () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div>foo</div></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () => h('div', 'foo'),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear with v-if', () => {
+  //   const show = false
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><!----></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () => (show ? h('div', 'foo') : createCommentVNode('')),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot('<!---->')
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear with v-show', () => {
+  //   const show = false
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div style="display: none;">foo</div></template>`,
+  //     () =>
+  //       h(
+  //         Transition,
+  //         { appear: true },
+  //         {
+  //           default: () =>
+  //             withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
+  //         },
+  //       ),
+  //   )
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //       style="display: none;"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('')
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(`mismatch`).not.toHaveBeenWarned()
+  // })
+
+  // test('transition appear w/ event listener', async () => {
+  //   const container = document.createElement('div')
+  //   container.innerHTML = `<template><button>0</button></template>`
+  //   createSSRApp({
+  //     data() {
+  //       return {
+  //         count: 0,
+  //       }
+  //     },
+  //     template: `
+  //       <Transition appear>
+  //         <button @click="count++">{{count}}</button>
+  //       </Transition>
+  //     `,
+  //   }).mount(container)
+
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <button
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       0
+  //     </button>
+  //   `)
+
+  //   triggerEvent('click', container.querySelector('button')!)
+  //   await nextTick()
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <button
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       1
+  //     </button>
+  //   `)
+  // })
+
+  // test('Suspense + transition appear', async () => {
+  //   const { vnode, container } = mountWithHydration(
+  //     `<template><div>foo</div></template>`,
+  //     () =>
+  //       h(Suspense, {}, () =>
+  //         h(
+  //           Transition,
+  //           { appear: true },
+  //           {
+  //             default: () => h('div', 'foo'),
+  //           },
+  //         ),
+  //       ),
+  //   )
+
+  //   expect(vnode.el).toBe(container.firstChild)
+  //   // wait for hydration to finish
+  //   await new Promise(r => setTimeout(r))
+
+  //   expect(container.firstChild).toMatchInlineSnapshot(`
+  //     <div
+  //       class="v-enter-from v-enter-active"
+  //     >
+  //       foo
+  //     </div>
+  //   `)
+  //   await nextTick()
+  //   expect(vnode.el).toBe(container.firstChild)
+  // })
+
+  // // #10607
+  // test('update component stable slot (prod + optimized mode)', async () => {
+  //   __DEV__ = false
+  //   try {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
+  //     const Comp = {
+  //       render(this: any) {
+  //         return (
+  //           openBlock(),
+  //           createElementBlock('div', null, [
+  //             renderSlot(this.$slots, 'default'),
+  //           ])
+  //         )
+  //       },
+  //     }
+  //     const show = ref(false)
+  //     const clicked = ref(false)
+
+  //     const Wrapper = {
+  //       setup() {
+  //         const items = ref<number[]>([])
+  //         onMounted(() => {
+  //           items.value = [1]
+  //         })
+  //         return () => {
+  //           return (
+  //             openBlock(),
+  //             createBlock(Comp, null, {
+  //               default: withCtx(() => [
+  //                 createElementVNode('div', null, [
+  //                   createElementVNode('div', null, [
+  //                     clicked.value
+  //                       ? (openBlock(),
+  //                         createElementBlock('div', { key: 0 }, 'foo'))
+  //                       : createCommentVNode('v-if', true),
+  //                   ]),
+  //                 ]),
+  //                 createElementVNode(
+  //                   'div',
+  //                   null,
+  //                   items.value.length,
+  //                   1 /* TEXT */,
+  //                 ),
+  //               ]),
+  //               _: 1 /* STABLE */,
+  //             })
+  //           )
+  //         }
+  //       },
+  //     }
+  //     createSSRApp({
+  //       components: { Wrapper },
+  //       data() {
+  //         return { show }
+  //       },
+  //       template: `<Wrapper :show="show"/>`,
+  //     }).mount(container)
+
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(
+  //       `<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
+  //     )
+
+  //     show.value = true
+  //     await nextTick()
+  //     expect(async () => {
+  //       clicked.value = true
+  //       await nextTick()
+  //     }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
+
+  //     await nextTick()
+  //     expect(container.innerHTML).toBe(
+  //       `<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
+  //     )
+  //   } catch (e) {
+  //     throw e
+  //   } finally {
+  //     __DEV__ = true
+  //   }
+  // })
+
+  // describe('mismatch handling', () => {
+  //   test('text node', () => {
+  //     const { container } = mountWithHydration(`foo`, () => 'bar')
+  //     expect(container.textContent).toBe('bar')
+  //     expect(`Hydration text mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('element text content', () => {
+  //     const { container } = mountWithHydration(`<div>foo</div>`, () =>
+  //       h('div', 'bar'),
+  //     )
+  //     expect(container.innerHTML).toBe('<div>bar</div>')
+  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('not enough children', () => {
+  //     const { container } = mountWithHydration(`<div></div>`, () =>
+  //       h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><span>foo</span><span>bar</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('span', 'foo')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><span>foo</span></div>')
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('complete mismatch', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
+  //   })
+
+  //   test('fragment mismatch removal', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //       () => h('div', [h('span', 'replaced')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('fragment not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('fragment too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //     )
+  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //     // as 2nd fragment child.
+  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  //     // excessive children removal
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('Teleport target has empty children', () => {
+  //     const teleportContainer = document.createElement('div')
+  //     teleportContainer.id = 'teleport'
+  //     document.body.appendChild(teleportContainer)
+
+  //     mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
+  //       h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
+  //     )
+  //     expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
+  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (element)', () => {
+  //     const { container } = mountWithHydration(`<div><span></span></div>`, () =>
+  //       h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (text)', () => {
+  //     const { container } = mountWithHydration(`<div>foobar</div>`, () =>
+  //       h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('class mismatch', () => {
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: ['foo', 'bar'] }),
+  //     )
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: { foo: true, bar: true } }),
+  //     )
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'foo bar' }),
+  //     )
+  //     // SVG classes
+  //     mountWithHydration(`<svg class="foo bar"></svg>`, () =>
+  //       h('svg', { class: 'foo bar' }),
+  //     )
+  //     // class with different order
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'bar foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
+  //       h('div', { class: 'foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   test('style mismatch', () => {
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: { color: 'red' } }),
+  //     )
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: `color:red;` }),
+  //     )
+  //     mountWithHydration(
+  //       `<div style="color:red; font-size: 12px;"></div>`,
+  //       () => h('div', { style: `font-size: 12px; color:red;` }),
+  //     )
+  //     mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       h('div', { style: { color: 'green' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('style mismatch when no style attribute is present', () => {
+  //     mountWithHydration(`<div></div>`, () =>
+  //       h('div', { style: { color: 'red' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('style mismatch w/ v-show', () => {
+  //     mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
+  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
+  //         [vShow, false],
+  //       ]),
+  //     )
+  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  //   })
+
+  //   test('attr mismatch', () => {
+  //     mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
+  //     mountWithHydration(`<div spellcheck></div>`, () =>
+  //       h('div', { spellcheck: '' }),
+  //     )
+  //     mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
+  //     // boolean
+  //     mountWithHydration(`<select multiple></div>`, () =>
+  //       h('select', { multiple: true }),
+  //     )
+  //     mountWithHydration(`<select multiple></div>`, () =>
+  //       h('select', { multiple: 'multiple' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
+
+  //     mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
+  //   })
+
+  //   test('attr special case: textarea value', () => {
+  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
+  //       h('textarea', { value: 'foo' }),
+  //     )
+  //     mountWithHydration(`<textarea></textarea>`, () =>
+  //       h('textarea', { value: '' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
+  //       h('textarea', { value: 'bar' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).toHaveBeenWarned()
+  //   })
+
+  //   // #11873
+  //   test('<textarea> with newlines at the beginning', async () => {
+  //     const render = () => h('textarea', null, '\nhello')
+  //     const html = await renderToString(createSSRApp({ render }))
+  //     mountWithHydration(html, render)
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('<pre> with newlines at the beginning', async () => {
+  //     const render = () => h('pre', null, '\n')
+  //     const html = await renderToString(createSSRApp({ render }))
+  //     mountWithHydration(html, render)
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('boolean attr handling', () => {
+  //     mountWithHydration(`<input />`, () => h('input', { readonly: false }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input readonly />`, () =>
+  //       h('input', { readonly: true }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input readonly="readonly" />`, () =>
+  //       h('input', { readonly: true }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('client value is null or undefined', () => {
+  //     mountWithHydration(`<div></div>`, () =>
+  //       h('div', { draggable: undefined }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+  //     mountWithHydration(`<input />`, () => h('input', { type: null }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn against object values', () => {
+  //     mountWithHydration(`<input />`, () => h('input', { from: {} }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn on falsy bindings of non-property keys', () => {
+  //     mountWithHydration(`<button />`, () => h('button', { href: undefined }))
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn on non-renderable option values', () => {
+  //     mountWithHydration(`<select><option>hello</option></select>`, () =>
+  //       h('select', [h('option', { value: ['foo'] }, 'hello')]),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('should not warn css v-bind', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () => h('div', { style: { color: 'var(--foo)' } })
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #10317 - test case from #10325
+  //   test('css vars should only be added to expected on component root dom', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () =>
+  //           h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #11188
+  //   test('css vars support fallthrough', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           foo: 'red',
+  //         }))
+  //         return () => h(Child)
+  //       },
+  //     })
+  //     const Child = {
+  //       setup() {
+  //         return () => h('div', { style: 'padding: 4px' })
+  //       },
+  //     }
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   // #11189
+  //   test('should not warn for directives that mutate DOM in created', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div class="test red"></div>`
+  //     const vColor: ObjectDirective = {
+  //       created(el, binding) {
+  //         el.classList.add(binding.value)
+  //       },
+  //     }
+  //     const app = createSSRApp({
+  //       setup() {
+  //         return () =>
+  //           withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
+  //       },
+  //     })
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('escape css var name', () => {
+  //     const container = document.createElement('div')
+  //     container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
+  //     const app = createSSRApp({
+  //       setup() {
+  //         useCssVars(() => ({
+  //           'foo.bar': 'red',
+  //         }))
+  //         return () => h(Child)
+  //       },
+  //     })
+  //     const Child = {
+  //       setup() {
+  //         return () => h('div', { style: 'padding: 4px' })
+  //       },
+  //     }
+  //     app.mount(container)
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+  // })
+
+  // describe('data-allow-mismatch', () => {
+  //   test('element text content', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="text">foo</div>`,
+  //       () => h('div', 'bar'),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="text">bar</div>',
+  //     )
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"></div>`,
+  //       () => h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('span', 'foo')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>foo</span></div>',
+  //     )
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('complete mismatch', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
+  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment mismatch removal', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //       () => h('div', [h('span', 'replaced')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><span>replaced</span></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment not enough children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('fragment too many children', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //     )
+  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //     // as 2nd fragment child.
+  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //     // excessive children removal
+  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (element)', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children"><span></span></div>`,
+  //       () => h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--hi--></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('comment mismatch (text)', () => {
+  //     const { container } = mountWithHydration(
+  //       `<div data-allow-mismatch="children">foobar</div>`,
+  //       () => h('div', [createCommentVNode('hi')]),
+  //     )
+  //     expect(container.innerHTML).toBe(
+  //       '<div data-allow-mismatch="children"><!--hi--></div>',
+  //     )
+  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('class mismatch', () => {
+  //     mountWithHydration(
+  //       `<div class="foo bar" data-allow-mismatch="class"></div>`,
+  //       () => h('div', { class: 'foo' }),
+  //     )
+  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('style mismatch', () => {
+  //     mountWithHydration(
+  //       `<div style="color:red;" data-allow-mismatch="style"></div>`,
+  //       () => h('div', { style: { color: 'green' } }),
+  //     )
+  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  //   })
+
+  //   test('attr mismatch', () => {
+  //     mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
+  //       h('div', { id: 'foo' }),
+  //     )
+  //     mountWithHydration(
+  //       `<div id="bar" data-allow-mismatch="attribute"></div>`,
+  //       () => h('div', { id: 'foo' }),
+  //     )
+  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  //   })
+  // })
+})
index e1b70cab98281961b9fb25beff5e176db86a0d84..834437ee350b0cd06d3b617ab59afa4bf5d1df8e 100644 (file)
@@ -21,7 +21,7 @@ import {
 import type { RawProps } from './componentProps'
 import { getGlobalThis } from '@vue/shared'
 import { optimizePropertyLookup } from './dom/prop'
-import { withHydration } from './dom/hydrate'
+import { withHydration } from './dom/hydration'
 
 let _createApp: CreateAppFunction<ParentNode, VaporComponent>
 
index 6ec39835bdf4825e2bf1fee2a7a5d4f3be0599ff..b782afd38d35b66c9fed33675d4f4705efbceb3f 100644 (file)
@@ -7,7 +7,7 @@ import {
 } from './component'
 import { createComment, createTextNode } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
-import { isHydrating } from './dom/hydrate'
+import { isHydrating } from './dom/hydration'
 
 export type Block =
   | Node
similarity index 83%
rename from packages/runtime-vapor/src/dom/hydrate.ts
rename to packages/runtime-vapor/src/dom/hydration.ts
index a958f1a42dc82c3def4ad7fc4bea5aa014b6c1d7..3bfc19cbe83232e7a3d1dca8feb7429740ea47e5 100644 (file)
@@ -7,8 +7,16 @@ export function setCurrentHydrationNode(node: Node | null): void {
   currentHydrationNode = node
 }
 
+let isOptimized = false
+
 export function withHydration(container: ParentNode, fn: () => void): void {
   adoptHydrationNode = adoptHydrationNodeImpl
+  if (!isOptimized) {
+    // optimize anchor cache lookup
+    const proto = Comment.prototype as any
+    proto.$p = proto.$e = undefined
+    isOptimized = true
+  }
   isHydrating = true
   currentHydrationNode = child(container)
   const res = fn()
@@ -48,7 +56,9 @@ function adoptHydrationNodeImpl(
   let end: Node | undefined | null
 
   if (template) {
-    while (node.nodeType === 8) node = next(node)
+    if (template[0] !== '<' && template[1] !== '!') {
+      while (node.nodeType === 8) node = next(node)
+    }
     adopted = end = node
   } else if (isComment(node, '[')) {
     // fragment
@@ -96,14 +106,16 @@ function adoptHydrationNodeImpl(
   if (__DEV__ && template) {
     const type = adopted.nodeType
     if (
-      type === 8 ||
+      (type === 8 && !template.startsWith('<!')) ||
       (type === 1 &&
         !template.startsWith(
           `<` + (adopted as Element).tagName.toLowerCase(),
         )) ||
-      (type === 3 && !template.startsWith((adopted as Text).data))
+      (type === 3 &&
+        template.trim() &&
+        !template.startsWith((adopted as Text).data))
     ) {
-      // TODO recover
+      // TODO recover and provide more info
       throw new Error('hydration mismatch!')
     }
   }
index cb0e5ebaa039f0208625407e1ca6f1f122b80973..8069301d2ef464306a964a6368b42aa72acbc754 100644 (file)
@@ -2,7 +2,7 @@ import {
   adoptHydrationNode,
   currentHydrationNode,
   isHydrating,
-} from './hydrate'
+} from './hydration'
 import { child } from './node'
 
 let t: HTMLTemplateElement