Repository: vuejs/composition-api
Pull request #644 (2021-02-10 21:54:44)

feat: add defineAsyncComponent API
This PR adds a implementation for defineAsyncComponent (I used code from the vue3 implementation) and returns a function async component as used in vue2 (see docs link). This should be a quite similar implementation as vue3 uses and should be compatible in most cases.

Some notes:
1. Tests are a copy of the vue3 tests an then modified.
2. ~Not shure of the typing of `Component` used `VueProxy<any,any>` but maybe there are better options~
3. Changed option suspensible  as vue2 has no suspense component
3. Helpfull docs:
* https://vuejs.org/v2/guide/components-dynamic-async.html#Handling-Loading-State
* https://v3.vuejs.org/api/global-api.html#defineasynccomponent

mathe42: Will work in it.

Should be easy with the test from vue3 https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/__tests__/apiAsyncComponent.spec.ts ...
mathe42: Added tests.

Following things are not like vue3:

1. On timeout a Error is displayed.
2. Error component access to thrown Error is not the same.
3. Resolve after timeout doesn't show the component.

Patch:
diff --git a/src/component/defineAsyncComponent.ts b/src/component/defineAsyncComponent.ts
new file mode 100644
index 0000000..9662a2c
--- /dev/null
+++ b/src/component/defineAsyncComponent.ts
@@ -0,0 +1,126 @@
+import { isFunction, isObject, warn } from '../utils'
+import { VueProxy } from './componentProxy'
+import { AsyncComponent } from 'vue'
+
+import {
+  ComponentOptionsWithoutProps,
+  ComponentOptionsWithArrayProps,
+  ComponentOptionsWithProps,
+} from './componentOptions'
+
+type ComponentOptions =
+  | ComponentOptionsWithoutProps
+  | ComponentOptionsWithArrayProps
+  | ComponentOptionsWithProps
+
+type Component = VueProxy<any, any, any, any, any>
+
+type ComponentOrComponentOptions = ComponentOptions | Component
+
+export type AsyncComponentResolveResult<T = ComponentOrComponentOptions> =
+  | T
+  | { default: T } // es modules
+
+export type AsyncComponentLoader = () => Promise<AsyncComponentResolveResult>
+
+export interface AsyncComponentOptions {
+  loader: AsyncComponentLoader
+  loadingComponent?: ComponentOrComponentOptions
+  errorComponent?: ComponentOrComponentOptions
+  delay?: number
+  timeout?: number
+  suspensible?: boolean
+  onError?: (
+    error: Error,
+    retry: () => void,
+    fail: () => void,
+    attempts: number
+  ) => any
+}
+
+export function defineAsyncComponent(
+  source: AsyncComponentLoader | AsyncComponentOptions
+): AsyncComponent {
+  if (isFunction(source)) {
+    source = { loader: source }
+  }
+
+  const {
+    loader,
+    loadingComponent,
+    errorComponent,
+    delay = 200,
+    timeout, // undefined = never times out
+    suspensible = false, // in Vue 3 default is true
+    onError: userOnError,
+  } = source
+
+  if (__DEV__ && suspensible) {
+    warn(
+      `The suspensiblbe option for async components is not supported in Vue2. It is ignored.`
+    )
+  }
+
+  let pendingRequest: Promise<Component> | null = null
+
+  let retries = 0
+  const retry = () => {
+    retries++
+    pendingRequest = null
+    return load()
+  }
+
+  const load = (): Promise<ComponentOrComponentOptions> => {
+    let thisRequest: Promise<ComponentOrComponentOptions>
+    return (
+      pendingRequest ||
+      (thisRequest = pendingRequest = loader()
+        .catch((err) => {
+          err = err instanceof Error ? err : new Error(String(err))
+          if (userOnError) {
+            return new Promise((resolve, reject) => {
+              const userRetry = () => resolve(retry())
+              const userFail = () => reject(err)
+              userOnError(err, userRetry, userFail, retries + 1)
+            })
+          } else {
+            throw err
+          }
+        })
+        .then((comp: any) => {
+          if (thisRequest !== pendingRequest && pendingRequest) {
+            return pendingRequest
+          }
+          if (__DEV__ && !comp) {
+            warn(
+              `Async component loader resolved to undefined. ` +
+                `If you are using retry(), make sure to return its return value.`
+            )
+          }
+          // interop module default
+          if (
+            comp &&
+            (comp.__esModule || comp[Symbol.toStringTag] === 'Module')
+          ) {
+            comp = comp.default
+          }
+          if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
+            throw new Error(`Invalid async component load result: ${comp}`)
+          }
+          return comp
+        }))
+    )
+  }
+
+  return () => {
+    const component = load()
+
+    return {
+      component: component as any, // there is a type missmatch between vue2 type and the docs
+      delay,
+      timeout,
+      error: errorComponent,
+      loading: loadingComponent,
+    }
+  }
+}
diff --git a/src/component/index.ts b/src/component/index.ts
index 639069e..86a79ee 100644
--- a/src/component/index.ts
+++ b/src/component/index.ts
@@ -1,4 +1,5 @@
 export { defineComponent } from './defineComponent'
+export { defineAsyncComponent } from './defineAsyncComponent'
 export { SetupFunction, SetupContext } from './componentOptions'
 export { ComponentInstance, ComponentRenderProxy } from './componentProxy'
 export { Data } from './common'


Test patch:
diff --git a/test-dts/defineAsyncComponent.test-d.ts b/test-dts/defineAsyncComponent.test-d.ts
new file mode 100644
index 0000000..cceba81
--- /dev/null
+++ b/test-dts/defineAsyncComponent.test-d.ts
@@ -0,0 +1,74 @@
+import { AsyncComponent } from 'vue'
+import { defineAsyncComponent, defineComponent, expectType } from './index'
+
+function asyncComponent1() {
+  return Promise.resolve().then(() => {
+    return defineComponent({})
+  })
+}
+
+function asyncComponent2() {
+  return Promise.resolve().then(() => {
+    return {
+      template: 'ASYNC',
+    }
+  })
+}
+
+const syncComponent1 = defineComponent({
+  template: '',
+})
+
+const syncComponent2 = {
+  template: '',
+}
+
+defineAsyncComponent(asyncComponent1)
+defineAsyncComponent(asyncComponent2)
+
+defineAsyncComponent({
+  loader: asyncComponent1,
+  delay: 200,
+  timeout: 3000,
+  errorComponent: syncComponent1,
+  loadingComponent: syncComponent1,
+})
+
+defineAsyncComponent({
+  loader: asyncComponent2,
+  delay: 200,
+  timeout: 3000,
+  errorComponent: syncComponent2,
+  loadingComponent: syncComponent2,
+})
+
+defineAsyncComponent(
+  () =>
+    new Promise((resolve, reject) => {
+      resolve(syncComponent1)
+    })
+)
+
+defineAsyncComponent(
+  () =>
+    new Promise((resolve, reject) => {
+      resolve(syncComponent2)
+    })
+)
+
+const component = defineAsyncComponent({
+  loader: asyncComponent1,
+  loadingComponent: defineComponent({}),
+  errorComponent: defineComponent({}),
+  delay: 200,
+  timeout: 3000,
+  suspensible: false,
+  onError(error, retry, fail, attempts) {
+    expectType<() => void>(retry)
+    expectType<() => void>(fail)
+    expectType<number>(attempts)
+    expectType<Error>(error)
+  },
+})
+
+expectType<AsyncComponent>(component)
diff --git a/test/v3/runtime-core/apiAsyncComponent.spec.ts b/test/v3/runtime-core/apiAsyncComponent.spec.ts
new file mode 100644
index 0000000..97f2941
--- /dev/null
+++ b/test/v3/runtime-core/apiAsyncComponent.spec.ts
@@ -0,0 +1,655 @@
+import {
+  h,
+  createApp,
+  defineAsyncComponent,
+  nextTick,
+  ref,
+  defineComponent,
+} from '../../../src'
+
+const resolveComponent = {
+  render() {
+    return h('p', 'resolved')
+  },
+}
+
+const loadingComponent = {
+  render() {
+    return h('p', 'loading')
+  },
+}
+
+const errorComponent = defineComponent({
+  render() {
+    return h('p', 'errored out')
+  },
+})
+
+const timeout = (n: number = 0) => new Promise((r) => setTimeout(r, n))
+
+describe('api: defineAsyncComponent', () => {
+  test('simple usage', async () => {
+    const Foo = defineAsyncComponent(() =>
+      Promise.resolve().then(() => resolveComponent)
+    )
+
+    const toggle = ref(true)
+
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    const vm = app.mount()
+
+    expect(vm.$el.textContent).toBe('')
+
+    // first time resolve, wait for macro task since there are multiple
+    // microtasks / .then() calls
+    await timeout()
+    expect(vm.$el.textContent).toBe('resolved')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // already resolved component should update on nextTick
+    toggle.value = true
+    await nextTick()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('with loading component', async () => {
+    let resolve: (cmp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((res) => {
+          resolve = res
+        }),
+      loadingComponent,
+      delay: 1, // defaults to 200
+    })
+
+    const toggle = ref(true)
+
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    const vm = app.mount()
+
+    // due to the delay, initial mount should be empty
+    expect(vm.$el.textContent).toBe('')
+
+    // loading show up after delay
+    await timeout(2)
+    expect(vm.$el.textContent).toBe('loading')
+
+    resolve!(resolveComponent)
+    await timeout()
+    expect(vm.$el.textContent).toBe('resolved')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // already resolved component should update on nextTick without loading
+    // state
+    toggle.value = true
+    await nextTick()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('with loading component + explicit delay (0)', async () => {
+    let resolve: (comp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((r) => {
+          resolve = r as any
+        }),
+      loadingComponent,
+      delay: 0,
+    })
+
+    const toggle = ref(true)
+
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+    const vm = app.mount()
+
+    // with delay: 0, should show loading immediately
+    expect(vm.$el.textContent).toBe('loading')
+
+    resolve!(resolveComponent)
+    await timeout()
+    expect(vm.$el.textContent).toBe('resolved')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // already resolved component should update on nextTick without loading
+    // state
+    toggle.value = true
+    await nextTick()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('error without error component', async () => {
+    // let resolve: (comp: any) => void
+    let reject: (e: Error) => void
+    const Foo = defineAsyncComponent(
+      () =>
+        new Promise((_resolve, _reject) => {
+          // resolve = _resolve as any
+          reject = _reject
+        })
+    )
+
+    const toggle = ref(true)
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0]).toContain(err.message)
+    expect(vm.$el.textContent).toBe('')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // This retry method doesn't work in Vue2
+
+    // errored out on previous load, toggle and mock success this time
+    // toggle.value = true
+    // await nextTick()
+    // expect(vm.$el.textContent).toBe('')
+
+    // // should render this time
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('error with error component', async () => {
+    // let resolve: (comp: any) => void
+    let reject: (e: Error) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          // resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent,
+    })
+
+    const toggle = ref(true)
+
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout(1)
+    expect(handler).toHaveBeenCalled()
+    expect(vm.$el.textContent).toBe('errored out')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // This doesn't work in vue2
+    // // errored out on previous load, toggle and mock success this time
+    // toggle.value = true
+    // await nextTick()
+    // expect(vm.$el.textContent).toBe('')
+
+    // // should render this time
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  // #2129
+  test('error with error component, without global handler', async () => {
+    // let resolve: (comp: any) => void
+    let reject: (e: Error) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          // resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent,
+    })
+
+    const toggle = ref(true)
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    jest.spyOn(global.console, 'error').mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout()
+    expect(vm.$el.textContent).toBe('errored out')
+    // TODO: Warning check?
+    // expect(
+    //   'Unhandled error during execution of async component loader'
+    // ).toHaveBeenWarned()
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // this doesn't work in vue2
+    // // errored out on previous load, toggle and mock success this time
+    // toggle.value = true
+    // await nextTick()
+    // expect(vm.$el.textContent).toBe('')
+
+    // // should render this time
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('error with error + loading components', async () => {
+    // let resolve: (comp: any) => void
+    let reject: (e: Error) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          // resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent,
+      loadingComponent,
+      delay: 1,
+    })
+
+    const toggle = ref(true)
+    const app = createApp({
+      render: () => (toggle.value ? h(Foo) : null),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    const vm = app.mount()
+
+    // due to the delay, initial mount should be empty
+    expect(vm.$el.textContent).toBe('')
+
+    // loading show up after delay
+    await timeout(1)
+    expect(vm.$el.textContent).toBe('loading')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(vm.$el.textContent).toBe('errored out')
+
+    toggle.value = false
+    await nextTick()
+    expect(vm.$el.textContent).toBe('')
+
+    // Not in vue2
+    // // errored out on previous load, toggle and mock success this time
+    // toggle.value = true
+    // await nextTick()
+    // expect(vm.$el.textContent).toBe('')
+
+    // // loading show up after delay
+    // await timeout(1)
+    // expect(vm.$el.textContent).toBe('loading')
+
+    // // should render this time
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('timeout without error component', async () => {
+    let resolve: (comp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve) => {
+          resolve = _resolve as any
+        }),
+      timeout: 1,
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+
+    await timeout(1)
+    expect(handler).toHaveBeenCalled()
+    // expect(handler.mock.calls[0][0].message).toContain(
+    //   `Async component timed out after 1ms.`
+    // )
+    expect(vm.$el.textContent).toBe('')
+
+    // if it resolved after timeout, should still work
+    resolve!(resolveComponent)
+    await timeout()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('timeout with error component', async () => {
+    // let resolve: (comp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve) => {
+          // resolve = _resolve as any
+        }),
+      timeout: 1,
+      errorComponent,
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+
+    await timeout(1)
+    expect(handler).toHaveBeenCalled()
+    expect(vm.$el.textContent).toBe('errored out')
+
+    // Not in vue2
+    // // if it resolved after timeout, should still work
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('timeout with error + loading components', async () => {
+    // let resolve: (comp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve) => {
+          // resolve = _resolve as any
+        }),
+      delay: 1,
+      timeout: 16,
+      errorComponent,
+      loadingComponent,
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+    const vm = app.mount()
+
+    expect(vm.$el.textContent).toBe('')
+    await timeout(1)
+    expect(vm.$el.textContent).toBe('loading')
+
+    await timeout(16)
+    expect(vm.$el.textContent).toBe('errored out')
+    expect(handler).toHaveBeenCalled()
+
+    // Not in Vue2
+    // resolve!(resolveComponent)
+    // await timeout()
+    // expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('timeout without error component, but with loading component', async () => {
+    let resolve: (comp: any) => void
+    const Foo = defineAsyncComponent({
+      loader: () =>
+        new Promise((_resolve) => {
+          resolve = _resolve as any
+        }),
+      delay: 1,
+      timeout: 16,
+      loadingComponent,
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+    await timeout(1)
+    expect(vm.$el.textContent).toBe('loading')
+
+    await timeout(16)
+    expect(handler).toHaveBeenCalled()
+    // expect(handler.mock.calls[0][0].message).toContain(
+    //   `Async component timed out after 16ms.`
+    // )
+    // should still display loading
+    expect(vm.$el.textContent).toBe('loading')
+
+    resolve!(resolveComponent)
+    await timeout()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('retry (success)', async () => {
+    let loaderCallCount = 0
+    let resolve: (comp: any) => void
+    let reject: (e: Error) => void
+
+    const Foo = defineAsyncComponent({
+      loader: () => {
+        loaderCallCount++
+        return new Promise((_resolve, _reject) => {
+          resolve = _resolve as any
+          reject = _reject
+        })
+      },
+      onError(error, retry, fail) {
+        if (error.message.match(/foo/)) {
+          retry()
+        } else {
+          fail()
+        }
+      },
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+
+    jest.spyOn(global.console, 'error').mockImplementation(() => null)
+
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+    expect(loaderCallCount).toBe(1)
+
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    // expect(handler).toHaveBeenCalled()
+    expect(loaderCallCount).toBe(2)
+    expect(vm.$el.textContent).toBe('')
+
+    // should render this time
+    resolve!(resolveComponent)
+    await timeout()
+    // expect(handler).not.toHaveBeenCalled()
+    expect(vm.$el.textContent).toBe('resolved')
+  })
+
+  test('retry (skipped)', async () => {
+    let loaderCallCount = 0
+    let reject: (e: Error) => void
+
+    const Foo = defineAsyncComponent({
+      loader: () => {
+        loaderCallCount++
+        return new Promise((_resolve, _reject) => {
+          reject = _reject
+        })
+      },
+      onError(error, retry, fail) {
+        if (error.message.match(/bar/)) {
+          retry()
+        } else {
+          fail()
+        }
+      },
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+    expect(loaderCallCount).toBe(1)
+
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    // should fail because retryWhen returns false
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0]).toContain(err.message)
+    expect(loaderCallCount).toBe(1)
+    expect(vm.$el.textContent).toBe('')
+  })
+
+  test('retry (fail w/ max retry attempts)', async () => {
+    let loaderCallCount = 0
+    let reject: (e: Error) => void
+
+    const Foo = defineAsyncComponent({
+      loader: () => {
+        loaderCallCount++
+        return new Promise((_resolve, _reject) => {
+          reject = _reject
+        })
+      },
+      onError(error, retry, fail, attempts) {
+        if (error.message.match(/foo/) && attempts <= 1) {
+          retry()
+        } else {
+          fail()
+        }
+      },
+    })
+
+    const app = createApp({
+      render: () => h(Foo),
+    })
+
+    const handler = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+    const vm = app.mount()
+    expect(vm.$el.textContent).toBe('')
+    expect(loaderCallCount).toBe(1)
+
+    // first retry
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    // expect(handler).not.toHaveBeenCalled()
+    expect(loaderCallCount).toBe(2)
+    expect(vm.$el.textContent).toBe('')
+
+    // 2nd retry, should fail due to reaching maxRetries
+    reject!(err)
+    await timeout()
+    // expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0]).toContain(err.message)
+    expect(loaderCallCount).toBe(2)
+    expect(vm.$el.textContent).toBe('')
+  })
+
+  // test('template ref forwarding', async () => {
+  //   let resolve: (comp: Component) => void
+  //   const Foo = defineAsyncComponent(
+  //     () =>
+  //       new Promise((r) => {
+  //         resolve = r as any
+  //       })
+  //   )
+
+  //   const fooRef = ref()
+  //   const toggle = ref(true)
+  //   const root = nodeOps.createElement('div')
+  //   createApp({
+  //     render: () => (toggle.value ? h(Foo, { ref: fooRef } as any) : null),
+  //   }).mount(root)
+
+  //   expect(vm.$el.textContent).toBe('')
+  //   expect(fooRef.value).toBe(null)
+
+  //   resolve!({
+  //     data() {
+  //       return {
+  //         id: 'foo',
+  //       }
+  //     },
+  //     render: resolveComponent.render,
+  //   })
+  //   // first time resolve, wait for macro task since there are multiple
+  //   // microtasks / .then() calls
+  //   await timeout()
+  //   expect(vm.$el.textContent).toBe('resolved')
+  //   expect(fooRef.value.id).toBe('foo')
+
+  //   toggle.value = false
+  //   await nextTick()
+  //   expect(vm.$el.textContent).toBe('')
+  //   expect(fooRef.value).toBe(null)
+
+  //   // already resolved component should update on nextTick
+  //   toggle.value = true
+  //   await nextTick()
+  //   expect(vm.$el.textContent).toBe('resolved')
+  //   expect(fooRef.value.id).toBe('foo')
+  // })
+})