| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import {
- h,
- ref,
- Suspense,
- ComponentOptions,
- render,
- nodeOps,
- serializeInner,
- nextTick,
- onMounted,
- watch,
- onUnmounted
- } from '@vue/runtime-test'
- describe('renderer: suspense', () => {
- const deps: Promise<any>[] = []
- beforeEach(() => {
- deps.length = 0
- })
- // a simple async factory for testing purposes only.
- function createAsyncComponent<T extends ComponentOptions>(
- comp: T,
- delay: number = 0
- ) {
- return {
- async setup(props: any, { slots }: any) {
- const p: Promise<T> = 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 AsyncChild2 = createAsyncComponent(
- {
- setup(props: { msg: string }) {
- return () => h('div', props.msg)
- }
- },
- 10
- )
- const Mid = {
- setup() {
- return () =>
- h(AsyncChild, {
- msg: msg.value
- })
- }
- }
- const Comp = {
- setup() {
- return () =>
- h(Suspense, [msg.value, h(Mid), h(AsyncChild2, { msg: 'child 2' })])
- }
- }
- const root = nodeOps.createElement('div')
- render(h(Comp), root)
- expect(serializeInner(root)).toBe(`<!---->`)
- await Promise.all(deps)
- await nextTick()
- expect(serializeInner(root)).toBe(
- `<!---->hello<div>hello</div><div>child 2</div><!---->`
- )
- })
- test('fallback content', async () => {
- const Async = createAsyncComponent({
- render() {
- return h('div', 'async')
- }
- })
- const Comp = {
- setup() {
- return () =>
- h(Suspense, null, {
- default: h(Async),
- fallback: h('div', 'fallback')
- })
- }
- }
- 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>`)
- })
- test('onResolve', async () => {
- const Async = createAsyncComponent({
- render() {
- return h('div', 'async')
- }
- })
- 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(`<div>fallback</div>`)
- expect(onResolve).not.toHaveBeenCalled()
- await Promise.all(deps)
- await nextTick()
- expect(serializeInner(root)).toBe(`<div>async</div>`)
- expect(onResolve).toHaveBeenCalled()
- })
- test('buffer mounted/updated hooks & watch callbacks', async () => {
- const deps: Promise<any>[] = []
- 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
- return () => h('div', 'async')
- }
- }
- 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(`<div>fallback</div>`)
- expect(calls).toEqual([])
- await Promise.all(deps)
- await nextTick()
- expect(serializeInner(root)).toBe(`<div>async</div>`)
- 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'])
- })
- test('content update before suspense resolve', async () => {
- const Async = createAsyncComponent({
- setup(props: { msg: string }) {
- return () => h('div', props.msg)
- }
- })
- const msg = ref('foo')
- const Comp = {
- setup() {
- return () =>
- h(Suspense, null, {
- default: h(Async, { msg: msg.value }),
- fallback: h('div', `fallback ${msg.value}`)
- })
- }
- }
- const root = nodeOps.createElement('div')
- render(h(Comp), root)
- expect(serializeInner(root)).toBe(`<div>fallback foo</div>`)
- // value changed before resolve
- msg.value = 'bar'
- await nextTick()
- // fallback content should be updated
- expect(serializeInner(root)).toBe(`<div>fallback bar</div>`)
- await Promise.all(deps)
- await nextTick()
- // async component should receive updated props/slots when resolved
- expect(serializeInner(root)).toBe(`<div>bar</div>`)
- })
- // mount/unmount hooks should not even fire
- test('unmount before suspense resolve', async () => {
- const deps: Promise<any>[] = []
- 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
- return () => h('div', 'async')
- }
- }
- 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(`<div>fallback</div>`)
- expect(calls).toEqual([])
- // remvoe the async dep before it's resolved
- toggle.value = false
- await nextTick()
- // should cause the suspense to resolve immediately
- expect(serializeInner(root)).toBe(`<!---->`)
- await Promise.all(deps)
- await nextTick()
- expect(serializeInner(root)).toBe(`<!---->`)
- // should discard effects
- expect(calls).toEqual([])
- })
- test('unmount suspense after resolve', () => {})
- test.todo('unmount suspense before 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')
- })
|