test(ssr): hydration suspense tests
This commit is contained in:
parent
1f9c9c14ae
commit
eb1d538ea2
@ -5,7 +5,9 @@ import {
|
|||||||
nextTick,
|
nextTick,
|
||||||
VNode,
|
VNode,
|
||||||
Portal,
|
Portal,
|
||||||
createStaticVNode
|
createStaticVNode,
|
||||||
|
Suspense,
|
||||||
|
onMounted
|
||||||
} from '@vue/runtime-dom'
|
} from '@vue/runtime-dom'
|
||||||
import { renderToString } from '@vue/server-renderer'
|
import { renderToString } from '@vue/server-renderer'
|
||||||
import { mockWarn } from '@vue/shared'
|
import { mockWarn } from '@vue/shared'
|
||||||
@ -28,6 +30,8 @@ const triggerEvent = (type: string, el: Element) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('SSR hydration', () => {
|
describe('SSR hydration', () => {
|
||||||
|
mockWarn()
|
||||||
|
|
||||||
test('text', async () => {
|
test('text', async () => {
|
||||||
const msg = ref('foo')
|
const msg = ref('foo')
|
||||||
const { vnode, container } = mountWithHydration('foo', () => msg.value)
|
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>`)
|
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fragment', async () => {
|
test('Fragment', async () => {
|
||||||
const msg = ref('foo')
|
const msg = ref('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
const { vnode, container } = mountWithHydration(
|
const { vnode, container } = mountWithHydration(
|
||||||
@ -142,7 +146,7 @@ describe('SSR hydration', () => {
|
|||||||
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('portal', async () => {
|
test('Portal', async () => {
|
||||||
const msg = ref('foo')
|
const msg = ref('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
const portalContainer = document.createElement('div')
|
const portalContainer = document.createElement('div')
|
||||||
@ -271,9 +275,109 @@ describe('SSR hydration', () => {
|
|||||||
expect(text.textContent).toBe('bye')
|
expect(text.textContent).toBe('bye')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mismatch handling', () => {
|
test('Suspense', async () => {
|
||||||
mockWarn()
|
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', () => {
|
test('text node', () => {
|
||||||
const { container } = mountWithHydration(`foo`, () => 'bar')
|
const { container } = mountWithHydration(`foo`, () => 'bar')
|
||||||
expect(container.textContent).toBe('bar')
|
expect(container.textContent).toBe('bar')
|
||||||
|
@ -47,7 +47,6 @@ export function createHydrationFunctions(
|
|||||||
const {
|
const {
|
||||||
mt: mountComponent,
|
mt: mountComponent,
|
||||||
p: patch,
|
p: patch,
|
||||||
n: next,
|
|
||||||
o: { patchProp, nextSibling, parentNode }
|
o: { patchProp, nextSibling, parentNode }
|
||||||
} = rendererInternals
|
} = rendererInternals
|
||||||
|
|
||||||
@ -152,16 +151,12 @@ export function createHydrationFunctions(
|
|||||||
parentSuspense,
|
parentSuspense,
|
||||||
isSVGContainer(container)
|
isSVGContainer(container)
|
||||||
)
|
)
|
||||||
const subTree = vnode.component!.subTree
|
// component may be async, so in the case of fragments we cannot rely
|
||||||
if (subTree) {
|
// on component's rendered output to determine the end of the fragment
|
||||||
return next(subTree)
|
// instead, we do a lookahead to find the end anchor node.
|
||||||
} else {
|
return isFragmentStart
|
||||||
// no subTree means this is an async component
|
? locateClosingAsyncAnchor(node)
|
||||||
// try to locate the ending node
|
: nextSibling(node)
|
||||||
return isFragmentStart
|
|
||||||
? locateClosingAsyncAnchor(node)
|
|
||||||
: nextSibling(node)
|
|
||||||
}
|
|
||||||
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
|
@ -212,7 +212,6 @@ export function createVNode(
|
|||||||
): VNode {
|
): VNode {
|
||||||
if (!type) {
|
if (!type) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
debugger
|
|
||||||
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
|
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
|
||||||
}
|
}
|
||||||
type = Comment
|
type = Comment
|
||||||
|
Loading…
x
Reference in New Issue
Block a user