From b30b17d22d325502df663d2139cc2a344ae9dd25 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 11 Sep 2019 23:44:37 -0400 Subject: [PATCH] test: test nested suspense & nested async deps --- .../__tests__/rendererSuspense.spec.ts | 259 +++++++++++++++++- packages/runtime-core/src/component.ts | 8 +- packages/runtime-core/src/createRenderer.ts | 10 +- packages/runtime-core/src/vnode.ts | 10 + 4 files changed, 281 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts index 5e80267b9..fa5b6716a 100644 --- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts +++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts @@ -105,6 +105,53 @@ describe('renderer: suspense', () => { expect(serializeInner(root)).toBe(`
async
`) }) + 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(`
fallback
`) + + await deps[0] + await nextTick() + expect(serializeInner(root)).toBe(`
fallback
`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
inner
`) + }) + test('onResolve', async () => { const Async = createAsyncComponent({ render() { @@ -286,15 +333,219 @@ describe('renderer: suspense', () => { 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(`
fallback
`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
async
`) + 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(`
fallback
`) + + 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(`
fallback outer
`) + + await deps[0] + await nextTick() + expect(serializeInner(root)).toBe( + `
async outer
fallback inner
` + ) + expect(calls).toEqual([`outer mounted`]) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe( + `
async outer
async inner
` + ) + 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(`
fallback outer
`) + + await deps[1] + await nextTick() + expect(serializeInner(root)).toBe(`
fallback outer
`) + expect(calls).toEqual([]) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe( + `
async outer
async inner
` + ) + expect(calls).toEqual([`inner mounted`, `outer mounted`]) + }) test.todo('error handling') + test.todo('new async dep after resolve should cause suspense to restart') + test.todo('portal inside suspense') }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 500ac828a..5802549c4 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1,4 +1,4 @@ -import { VNode, VNodeChild } from './vnode' +import { VNode, VNodeChild, isVNode } from './vnode' import { ReactiveEffect, reactive, readonly } from '@vue/reactivity' import { PublicInstanceProxyHandlers, @@ -279,6 +279,12 @@ export function handleSetupResult( // setup returned an inline render function instance.render = setupResult as RenderFunction } else if (isObject(setupResult)) { + if (__DEV__ && isVNode(setupResult)) { + warn( + `setup() should not return VNodes directly - ` + + `return a render function instead.` + ) + } // setup returned bindings. // assuming a render function compiled from template is present. instance.renderContext = reactive(setupResult) diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 9d665f481..020f7c8fc 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -871,6 +871,7 @@ export function createRenderer< hasUnresolvedAncestor = true break } + parent = parent.parent } // no pending parent suspense, flush all jobs if (!hasUnresolvedAncestor) { @@ -1509,7 +1510,14 @@ export function createRenderer< return } 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 } if (vnode.type === Fragment) { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 8bcafdd7f..589042ec3 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -124,6 +124,12 @@ export function createBlock( return vnode } +const knownVNodes = new WeakSet() + +export function isVNode(value: any): boolean { + return knownVNodes.has(value) +} + export function createVNode( type: VNodeTypes, props: { [key: string]: any } | null | 0 = null, @@ -198,6 +204,10 @@ export function createVNode( trackDynamicNode(vnode) } + if (__DEV__) { + knownVNodes.add(vnode) + } + return vnode }