diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts index 16953bee4..c5d2ad415 100644 --- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts +++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts @@ -6,47 +6,50 @@ import { render, nodeOps, serializeInner, - nextTick + nextTick, + onMounted, + watch, + onUnmounted } from '@vue/runtime-test' describe('renderer: suspense', () => { - it('basic usage (nested + multiple deps)', async () => { - const msg = ref('hello') - const deps: Promise[] = [] + const deps: Promise[] = [] - const createAsyncComponent = (loader: () => Promise) => ({ + beforeEach(() => { + deps.length = 0 + }) + + // a simple async factory for testing purposes only. + function createAsyncComponent( + comp: T, + delay: number = 0 + ) { + return { async setup(props: any, { slots }: any) { - const p = loader() + const p: Promise = new Promise(r => setTimeout(() => r(comp), delay)) deps.push(p) const Inner = await p return () => h(Inner, props, slots) } + } + } + + it('basic usage (nested + multiple deps)', async () => { + const msg = ref('hello') + + const AsyncChild = createAsyncComponent({ + setup(props: { msg: string }) { + return () => h('div', props.msg) + } }) - const AsyncChild = createAsyncComponent( - () => - new Promise(resolve => { - setTimeout(() => { - resolve({ - setup(props: { msg: string }) { - return () => h('div', props.msg) - } - }) - }, 0) - }) - ) - const AsyncChild2 = createAsyncComponent( - () => - new Promise(resolve => { - setTimeout(() => { - resolve({ - setup(props: { msg: string }) { - return () => h('div', props.msg) - } - }) - }, 10) - }) + { + setup(props: { msg: string }) { + return () => h('div', props.msg) + } + }, + 10 ) const Mid = { @@ -77,22 +80,11 @@ describe('renderer: suspense', () => { }) test('fallback content', async () => { - const deps: Promise[] = [] - - const Async = { - async setup() { - const p = new Promise(r => setTimeout(r, 1)) - deps.push(p) - await p - // test resume for returning bindings - return { - msg: 'async' - } - }, - render(this: any) { - return h('div', this.msg) + const Async = createAsyncComponent({ + render() { + return h('div', 'async') } - } + }) const Comp = { setup() { @@ -113,16 +105,112 @@ describe('renderer: suspense', () => { expect(serializeInner(root)).toBe(`
async
`) }) - test.todo('buffer mounted/updated hooks & watch callbacks') + test('onResolve', async () => { + const Async = createAsyncComponent({ + render() { + return h('div', 'async') + } + }) - test.todo('onResolve') + const onResolve = jest.fn() + const Comp = { + setup() { + return () => + h( + Suspense, + { + onResolve + }, + { + default: h(Async), + fallback: h('div', 'fallback') + } + ) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
fallback
`) + expect(onResolve).not.toHaveBeenCalled() + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
async
`) + expect(onResolve).toHaveBeenCalled() + }) + + test('buffer mounted/updated hooks & watch callbacks', async () => { + const deps: Promise[] = [] + const calls: string[] = [] + const toggle = ref(true) + + const Async = { + async setup() { + const p = new Promise(r => setTimeout(r, 1)) + deps.push(p) + + watch(() => { + calls.push('watch callback') + }) + + onMounted(() => { + calls.push('mounted') + }) + + onUnmounted(() => { + calls.push('unmounted') + }) + + await p + // test resume for returning bindings + return { + msg: 'async' + } + }, + render(this: any) { + return h('div', this.msg) + } + } + + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: toggle.value ? h(Async) : null, + fallback: h('div', 'fallback') + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
fallback
`) + expect(calls).toEqual([]) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
async
`) + expect(calls).toEqual([`watch callback`, `mounted`]) + + // effects inside an already resolved suspense should happen at normal timing + toggle.value = false + await nextTick() + expect(serializeInner(root)).toBe(``) + expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted']) + }) + + // should receive updated props/slots when resolved test.todo('content update before suspense resolve') + // mount/unmount hooks should not even fire test.todo('unmount before suspense resolve') test.todo('nested suspense') + test.todo('new async dep after resolve should cause suspense to restart') + test.todo('error handling') test.todo('portal inside suspense')