test(ssr): hydration suspense tests

This commit is contained in:
Evan You 2020-03-13 13:04:44 -04:00
parent 1f9c9c14ae
commit eb1d538ea2
3 changed files with 115 additions and 17 deletions

View File

@ -5,7 +5,9 @@ import {
nextTick,
VNode,
Portal,
createStaticVNode
createStaticVNode,
Suspense,
onMounted
} from '@vue/runtime-dom'
import { renderToString } from '@vue/server-renderer'
import { mockWarn } from '@vue/shared'
@ -28,6 +30,8 @@ const triggerEvent = (type: string, el: Element) => {
}
describe('SSR hydration', () => {
mockWarn()
test('text', async () => {
const msg = ref('foo')
const { vnode, container } = mountWithHydration('foo', () => msg.value)
@ -94,7 +98,7 @@ describe('SSR hydration', () => {
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
})
test('fragment', async () => {
test('Fragment', async () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
@ -142,7 +146,7 @@ describe('SSR hydration', () => {
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
})
test('portal', async () => {
test('Portal', async () => {
const msg = ref('foo')
const fn = jest.fn()
const portalContainer = document.createElement('div')
@ -271,9 +275,109 @@ describe('SSR hydration', () => {
expect(text.textContent).toBe('bye')
})
describe('mismatch handling', () => {
mockWarn()
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>`)
})
test('Suspense (full integration)', async () => {
const mountedCalls: number[] = []
const asyncDeps: Promise<any>[] = []
const AsyncChild = {
async setup(props: { n: number }) {
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 = jest.fn()
const App = {
template: `
<Suspense @resolve="done">
<AsyncChild :n="1" />
<AsyncChild :n="2" />
</Suspense>`,
components: {
AsyncChild
},
methods: {
done
}
}
const container = document.createElement('div')
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--[--><span>1</span><span>2</span><!--]-->"`
)
// 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])
// should have removed fragment markers
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
const span1 = container.querySelector('span')!
triggerEvent('click', span1)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`)
const span2 = span1.nextSibling as Element
triggerEvent('click', span2)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
})
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
expect(container.textContent).toBe('bar')

View File

@ -47,7 +47,6 @@ export function createHydrationFunctions(
const {
mt: mountComponent,
p: patch,
n: next,
o: { patchProp, nextSibling, parentNode }
} = rendererInternals
@ -152,16 +151,12 @@ export function createHydrationFunctions(
parentSuspense,
isSVGContainer(container)
)
const subTree = vnode.component!.subTree
if (subTree) {
return next(subTree)
} else {
// no subTree means this is an async component
// try to locate the ending node
return isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)
}
// component may be async, so in the case of fragments we cannot rely
// on component's rendered output to determine the end of the fragment
// instead, we do a lookahead to find the end anchor node.
return isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)
} else if (shapeFlag & ShapeFlags.PORTAL) {
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)

View File

@ -212,7 +212,6 @@ export function createVNode(
): VNode {
if (!type) {
if (__DEV__) {
debugger
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment