test: test nested suspense & nested async deps
This commit is contained in:
parent
bbc3442c52
commit
b30b17d22d
@ -105,6 +105,53 @@ describe('renderer: suspense', () => {
|
|||||||
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('nested async deps', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
const AsyncOuter = createAsyncComponent({
|
||||||
|
setup() {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('outer mounted')
|
||||||
|
})
|
||||||
|
return () => h(AsyncInner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const AsyncInner = createAsyncComponent(
|
||||||
|
{
|
||||||
|
setup() {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('inner mounted')
|
||||||
|
})
|
||||||
|
return () => h('div', 'inner')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h(AsyncOuter),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
|
|
||||||
|
await deps[0]
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<div>inner</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
test('onResolve', async () => {
|
test('onResolve', async () => {
|
||||||
const Async = createAsyncComponent({
|
const Async = createAsyncComponent({
|
||||||
render() {
|
render() {
|
||||||
@ -286,15 +333,219 @@ describe('renderer: suspense', () => {
|
|||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unmount suspense after resolve', () => {})
|
test('unmount suspense after resolve', async () => {
|
||||||
|
const toggle = ref(true)
|
||||||
|
const unmounted = jest.fn()
|
||||||
|
|
||||||
test.todo('unmount suspense before resolve')
|
const Async = createAsyncComponent({
|
||||||
|
setup() {
|
||||||
|
onUnmounted(unmounted)
|
||||||
|
return () => h('div', 'async')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test.todo('nested suspense')
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
toggle.value
|
||||||
|
? h(Suspense, null, {
|
||||||
|
default: h(Async),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test.todo('new async dep after resolve should cause suspense to restart')
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
||||||
|
expect(unmounted).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
|
expect(unmounted).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unmount suspense before resolve', async () => {
|
||||||
|
const toggle = ref(true)
|
||||||
|
const mounted = jest.fn()
|
||||||
|
const unmounted = jest.fn()
|
||||||
|
|
||||||
|
const Async = createAsyncComponent({
|
||||||
|
setup() {
|
||||||
|
onMounted(mounted)
|
||||||
|
onUnmounted(unmounted)
|
||||||
|
return () => h('div', 'async')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
toggle.value
|
||||||
|
? h(Suspense, null, {
|
||||||
|
default: h(Async),
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
|
expect(mounted).not.toHaveBeenCalled()
|
||||||
|
expect(unmounted).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
// should not resolve and cause unmount
|
||||||
|
expect(mounted).not.toHaveBeenCalled()
|
||||||
|
expect(unmounted).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested suspense (parent resolves first)', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
const AsyncOuter = createAsyncComponent(
|
||||||
|
{
|
||||||
|
setup: () => {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('outer mounted')
|
||||||
|
})
|
||||||
|
return () => h('div', 'async outer')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
const AsyncInner = createAsyncComponent(
|
||||||
|
{
|
||||||
|
setup: () => {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('inner mounted')
|
||||||
|
})
|
||||||
|
return () => h('div', 'async inner')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
const Inner = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h(AsyncInner),
|
||||||
|
fallback: h('div', 'fallback inner')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: [h(AsyncOuter), h(Inner)],
|
||||||
|
fallback: h('div', 'fallback outer')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
|
||||||
|
|
||||||
|
await deps[0]
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(
|
||||||
|
`<!----><div>async outer</div><div>fallback inner</div><!---->`
|
||||||
|
)
|
||||||
|
expect(calls).toEqual([`outer mounted`])
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(
|
||||||
|
`<!----><div>async outer</div><div>async inner</div><!---->`
|
||||||
|
)
|
||||||
|
expect(calls).toEqual([`outer mounted`, `inner mounted`])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested suspense (child resolves first)', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
|
||||||
|
const AsyncOuter = createAsyncComponent(
|
||||||
|
{
|
||||||
|
setup: () => {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('outer mounted')
|
||||||
|
})
|
||||||
|
return () => h('div', 'async outer')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
const AsyncInner = createAsyncComponent(
|
||||||
|
{
|
||||||
|
setup: () => {
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('inner mounted')
|
||||||
|
})
|
||||||
|
return () => h('div', 'async inner')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
const Inner = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h(AsyncInner),
|
||||||
|
fallback: h('div', 'fallback inner')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: [h(AsyncOuter), h(Inner)],
|
||||||
|
fallback: h('div', 'fallback outer')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
|
||||||
|
|
||||||
|
await deps[1]
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback outer</div>`)
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(
|
||||||
|
`<!----><div>async outer</div><div>async inner</div><!---->`
|
||||||
|
)
|
||||||
|
expect(calls).toEqual([`inner mounted`, `outer mounted`])
|
||||||
|
})
|
||||||
|
|
||||||
test.todo('error handling')
|
test.todo('error handling')
|
||||||
|
|
||||||
|
test.todo('new async dep after resolve should cause suspense to restart')
|
||||||
|
|
||||||
test.todo('portal inside suspense')
|
test.todo('portal inside suspense')
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VNode, VNodeChild } from './vnode'
|
import { VNode, VNodeChild, isVNode } from './vnode'
|
||||||
import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
|
import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
PublicInstanceProxyHandlers,
|
PublicInstanceProxyHandlers,
|
||||||
@ -279,6 +279,12 @@ export function handleSetupResult(
|
|||||||
// setup returned an inline render function
|
// setup returned an inline render function
|
||||||
instance.render = setupResult as RenderFunction
|
instance.render = setupResult as RenderFunction
|
||||||
} else if (isObject(setupResult)) {
|
} else if (isObject(setupResult)) {
|
||||||
|
if (__DEV__ && isVNode(setupResult)) {
|
||||||
|
warn(
|
||||||
|
`setup() should not return VNodes directly - ` +
|
||||||
|
`return a render function instead.`
|
||||||
|
)
|
||||||
|
}
|
||||||
// setup returned bindings.
|
// setup returned bindings.
|
||||||
// assuming a render function compiled from template is present.
|
// assuming a render function compiled from template is present.
|
||||||
instance.renderContext = reactive(setupResult)
|
instance.renderContext = reactive(setupResult)
|
||||||
|
@ -871,6 +871,7 @@ export function createRenderer<
|
|||||||
hasUnresolvedAncestor = true
|
hasUnresolvedAncestor = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
parent = parent.parent
|
||||||
}
|
}
|
||||||
// no pending parent suspense, flush all jobs
|
// no pending parent suspense, flush all jobs
|
||||||
if (!hasUnresolvedAncestor) {
|
if (!hasUnresolvedAncestor) {
|
||||||
@ -1509,7 +1510,14 @@ export function createRenderer<
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
|
if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
|
||||||
move((vnode.suspense as any).subTree, container, anchor)
|
const suspense = vnode.suspense as SuspenseBoundary
|
||||||
|
move(
|
||||||
|
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
|
||||||
|
container,
|
||||||
|
anchor
|
||||||
|
)
|
||||||
|
suspense.container = container
|
||||||
|
// suspense.anchor = anchor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (vnode.type === Fragment) {
|
if (vnode.type === Fragment) {
|
||||||
|
@ -124,6 +124,12 @@ export function createBlock(
|
|||||||
return vnode
|
return vnode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const knownVNodes = new WeakSet<VNode>()
|
||||||
|
|
||||||
|
export function isVNode(value: any): boolean {
|
||||||
|
return knownVNodes.has(value)
|
||||||
|
}
|
||||||
|
|
||||||
export function createVNode(
|
export function createVNode(
|
||||||
type: VNodeTypes,
|
type: VNodeTypes,
|
||||||
props: { [key: string]: any } | null | 0 = null,
|
props: { [key: string]: any } | null | 0 = null,
|
||||||
@ -198,6 +204,10 @@ export function createVNode(
|
|||||||
trackDynamicNode(vnode)
|
trackDynamicNode(vnode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
knownVNodes.add(vnode)
|
||||||
|
}
|
||||||
|
|
||||||
return vnode
|
return vnode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user