]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: port tests
authordaiwei <daiwei521@126.com>
Tue, 18 Mar 2025 07:24:45 +0000 (15:24 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 18 Mar 2025 07:24:45 +0000 (15:24 +0800)
packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts

index ef16160b5df14b140a7fa8c116bc6fc0cb80ec37..4c76d8dbc99f86be1a50663bcae71f04f3005ce8 100644 (file)
@@ -2,7 +2,7 @@ import { nextTick, ref } from '@vue/runtime-dom'
 import { type VaporComponent, createComponent } from '../src/component'
 import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
 import { makeRender } from './_utils'
-import { createIf, template } from '@vue/runtime-vapor'
+import { createIf, createTemplateRefSetter, template } from '@vue/runtime-vapor'
 
 const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
 
@@ -45,4 +45,654 @@ describe('api: defineAsyncComponent', () => {
     await nextTick()
     expect(html()).toBe('resolved<!--async component--><!--if-->')
   })
+
+  test('with loading component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+      loadingComponent: () => template('loading')(),
+      delay: 1, // defaults to 200
+    })
+
+    const toggle = ref(true)
+    const { html } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).render()
+
+    // due to the delay, initial mount should be empty
+    expect(html()).toBe('<!--async component--><!--if-->')
+
+    // loading show up after delay
+    await timeout(1)
+    expect(html()).toBe('loading<!--async component--><!--if-->')
+
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(html()).toBe('resolved<!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe('<!--if-->')
+
+    // already resolved component should update on nextTick without loading
+    // state
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('with loading component + explicit delay (0)', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+      loadingComponent: () => template('loading')(),
+      delay: 0,
+    })
+
+    const toggle = ref(true)
+    const { html } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).render()
+
+    // with delay: 0, should show loading immediately
+    expect(html()).toBe('loading<!--async component--><!--if-->')
+
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(html()).toBe('resolved<!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe('<!--if-->')
+
+    // already resolved component should update on nextTick without loading
+    // state
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('error without error component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    let reject: (e: Error) => void
+    const Foo = defineVaporAsyncComponent(
+      () =>
+        new Promise((_resolve, _reject) => {
+          resolve = _resolve as any
+          reject = _reject
+        }),
+    )
+
+    const toggle = ref(true)
+    const { app, mount } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).create()
+
+    const handler = (app.config.errorHandler = vi.fn())
+    const root = document.createElement('div')
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0]).toBe(err)
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--if-->')
+
+    // errored out on previous load, toggle and mock success this time
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    // should render this time
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('error with error component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    let reject: (e: Error) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent: (props: { error: Error }) =>
+        template(props.error.message)(),
+    })
+
+    const toggle = ref(true)
+    const { app, mount } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).create()
+    const handler = (app.config.errorHandler = vi.fn())
+    const root = document.createElement('div')
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--if-->')
+
+    // errored out on previous load, toggle and mock success this time
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    // should render this time
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('error with error component, without global handler', async () => {
+    let resolve: (comp: VaporComponent) => void
+    let reject: (e: Error) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent: (props: { error: Error }) =>
+        template(props.error.message)(),
+    })
+
+    const toggle = ref(true)
+    const { mount } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).create()
+    const root = document.createElement('div')
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout()
+    expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
+    expect(
+      'Unhandled error during execution of async component loader',
+    ).toHaveBeenWarned()
+
+    toggle.value = false
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--if-->')
+
+    // errored out on previous load, toggle and mock success this time
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    // should render this time
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('error with error + loading components', async () => {
+    let resolve: (comp: VaporComponent) => void
+    let reject: (e: Error) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise((_resolve, _reject) => {
+          resolve = _resolve as any
+          reject = _reject
+        }),
+      errorComponent: (props: { error: Error }) =>
+        template(props.error.message)(),
+      loadingComponent: () => template('loading')(),
+      delay: 1,
+    })
+
+    const toggle = ref(true)
+    const { app, mount } = define({
+      setup() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            return createComponent(Foo)
+          },
+        )
+      },
+    }).create()
+    const handler = (app.config.errorHandler = vi.fn())
+    const root = document.createElement('div')
+    mount(root)
+
+    // due to the delay, initial mount should be empty
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    // loading show up after delay
+    await timeout(1)
+    expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
+
+    const err = new Error('errored out')
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--if-->')
+
+    // errored out on previous load, toggle and mock success this time
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+
+    // loading show up after delay
+    await timeout(1)
+    expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
+
+    // should render this time
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+  })
+
+  test('timeout without error component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(_resolve => {
+          resolve = _resolve as any
+        }),
+      timeout: 1,
+    })
+
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+    const handler = vi.fn()
+    app.config.errorHandler = handler
+
+    const root = document.createElement('div')
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+
+    await timeout(1)
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0].message).toMatch(
+      `Async component timed out after 1ms.`,
+    )
+    expect(root.innerHTML).toBe('<!--async component-->')
+
+    // if it resolved after timeout, should still work
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component-->')
+  })
+
+  test('timeout with error component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(_resolve => {
+          resolve = _resolve as any
+        }),
+      timeout: 1,
+      errorComponent: () => template('timed out')(),
+    })
+
+    const root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+
+    const handler = (app.config.errorHandler = vi.fn())
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+
+    await timeout(1)
+    expect(handler).toHaveBeenCalled()
+    expect(root.innerHTML).toBe('timed out<!--async component-->')
+
+    // if it resolved after timeout, should still work
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component-->')
+  })
+
+  test('timeout with error + loading components', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(_resolve => {
+          resolve = _resolve as any
+        }),
+      delay: 1,
+      timeout: 16,
+      errorComponent: () => template('timed out')(),
+      loadingComponent: () => template('loading')(),
+    })
+
+    const root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+    const handler = (app.config.errorHandler = vi.fn())
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+    await timeout(1)
+    expect(root.innerHTML).toBe('loading<!--async component-->')
+
+    await timeout(16)
+    expect(root.innerHTML).toBe('timed out<!--async component-->')
+    expect(handler).toHaveBeenCalled()
+
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component-->')
+  })
+
+  test('timeout without error component, but with loading component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent({
+      loader: () =>
+        new Promise(_resolve => {
+          resolve = _resolve as any
+        }),
+      delay: 1,
+      timeout: 16,
+      loadingComponent: () => template('loading')(),
+    })
+
+    const root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+    const handler = vi.fn()
+    app.config.errorHandler = handler
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+    await timeout(1)
+    expect(root.innerHTML).toBe('loading<!--async component-->')
+
+    await timeout(16)
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0].message).toMatch(
+      `Async component timed out after 16ms.`,
+    )
+    // should still display loading
+    expect(root.innerHTML).toBe('loading<!--async component-->')
+
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component-->')
+  })
+
+  test.todo('with suspense', async () => {})
+
+  test.todo('suspensible: false', async () => {})
+
+  test.todo('suspense with error handling', async () => {})
+
+  test('retry (success)', async () => {
+    let loaderCallCount = 0
+    let resolve: (comp: VaporComponent) => void
+    let reject: (e: Error) => void
+
+    const Foo = defineVaporAsyncComponent({
+      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 root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+
+    const handler = (app.config.errorHandler = vi.fn())
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+    expect(loaderCallCount).toBe(1)
+
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    expect(handler).not.toHaveBeenCalled()
+    expect(loaderCallCount).toBe(2)
+    expect(root.innerHTML).toBe('<!--async component-->')
+
+    // should render this time
+    resolve!(() => template('resolved')())
+    await timeout()
+    expect(handler).not.toHaveBeenCalled()
+    expect(root.innerHTML).toBe('resolved<!--async component-->')
+  })
+
+  test('retry (skipped)', async () => {
+    let loaderCallCount = 0
+    let reject: (e: Error) => void
+
+    const Foo = defineVaporAsyncComponent({
+      loader: () => {
+        loaderCallCount++
+        return new Promise((_resolve, _reject) => {
+          reject = _reject
+        })
+      },
+      onError(error, retry, fail) {
+        if (error.message.match(/bar/)) {
+          retry()
+        } else {
+          fail()
+        }
+      },
+    })
+
+    const root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+
+    const handler = (app.config.errorHandler = vi.fn())
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+    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]).toBe(err)
+    expect(loaderCallCount).toBe(1)
+    expect(root.innerHTML).toBe('<!--async component-->')
+  })
+
+  test('retry (fail w/ max retry attempts)', async () => {
+    let loaderCallCount = 0
+    let reject: (e: Error) => void
+
+    const Foo = defineVaporAsyncComponent({
+      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 root = document.createElement('div')
+    const { app, mount } = define({
+      setup() {
+        return createComponent(Foo)
+      },
+    }).create()
+
+    const handler = (app.config.errorHandler = vi.fn())
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component-->')
+    expect(loaderCallCount).toBe(1)
+
+    // first retry
+    const err = new Error('foo')
+    reject!(err)
+    await timeout()
+    expect(handler).not.toHaveBeenCalled()
+    expect(loaderCallCount).toBe(2)
+    expect(root.innerHTML).toBe('<!--async component-->')
+
+    // 2nd retry, should fail due to reaching maxRetries
+    reject!(err)
+    await timeout()
+    expect(handler).toHaveBeenCalled()
+    expect(handler.mock.calls[0][0]).toBe(err)
+    expect(loaderCallCount).toBe(2)
+    expect(root.innerHTML).toBe('<!--async component-->')
+  })
+
+  test.todo('template ref forwarding', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const Foo = defineVaporAsyncComponent(
+      () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+    )
+
+    const fooRef = ref<any>(null)
+    const toggle = ref(true)
+    const root = document.createElement('div')
+    const { mount } = define({
+      setup() {
+        return { fooRef, toggle }
+      },
+      render() {
+        return createIf(
+          () => toggle.value,
+          () => {
+            const setTemplateRef = createTemplateRefSetter()
+            const n0 = createComponent(Foo, null, null, true)
+            setTemplateRef(n0, 'fooRef')
+            return n0
+          },
+        )
+      },
+    }).create()
+    mount(root)
+    expect(root.innerHTML).toBe('<!--async component--><!--if-->')
+    expect(fooRef.value).toBe(null)
+
+    resolve!({
+      setup: (props, { expose }) => {
+        expose({
+          id: 'foo',
+        })
+        return template('resolved')()
+      },
+    })
+    // first time resolve, wait for macro task since there are multiple
+    // microtasks / .then() calls
+    await timeout()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+    expect(fooRef.value.id).toBe('foo')
+
+    toggle.value = false
+    await nextTick()
+    expect(root.innerHTML).toBe('<!--if-->')
+    expect(fooRef.value).toBe(null)
+
+    // already resolved component should update on nextTick
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
+    expect(fooRef.value.id).toBe('foo')
+  })
+
+  test.todo(
+    'the forwarded template ref should always exist when doing multi patching',
+    async () => {},
+  )
+
+  test.todo('with KeepAlive', async () => {})
+
+  test.todo('with KeepAlive + include', async () => {})
 })