feat: update Suspense usage (#2099)

See https://github.com/vuejs/vue-next/pull/2099 for details.
This commit is contained in:
Evan You
2020-09-15 12:45:06 -04:00
committed by GitHub
parent 37e686f25e
commit 5ae7380b4a
17 changed files with 815 additions and 247 deletions

View File

@@ -406,7 +406,7 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
@@ -416,7 +416,7 @@ describe('api: defineAsyncComponent', () => {
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved & resolved')
expect(serializeInner(root)).toBe('<div>resolved & resolved</div>')
})
test('suspensible: false', async () => {
@@ -433,18 +433,18 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
app.mount(root)
// should not show suspense fallback
expect(serializeInner(root)).toBe('<!----> & <!---->')
expect(serializeInner(root)).toBe('<div><!----> & <!----></div>')
resolve!(() => 'resolved')
await timeout()
expect(serializeInner(root)).toBe('resolved & resolved')
expect(serializeInner(root)).toBe('<div>resolved & resolved</div>')
})
test('suspense with error handling', async () => {
@@ -460,7 +460,7 @@ describe('api: defineAsyncComponent', () => {
const app = createApp({
render: () =>
h(Suspense, null, {
default: () => [h(Foo), ' & ', h(Foo)],
default: () => h('div', [h(Foo), ' & ', h(Foo)]),
fallback: () => 'loading'
})
})
@@ -472,7 +472,7 @@ describe('api: defineAsyncComponent', () => {
reject!(new Error('no'))
await timeout()
expect(handler).toHaveBeenCalled()
expect(serializeInner(root)).toBe('<!----> & <!---->')
expect(serializeInner(root)).toBe('<div><!----> & <!----></div>')
})
test('retry (success)', async () => {

View File

@@ -11,7 +11,8 @@ import {
watch,
watchEffect,
onUnmounted,
onErrorCaptured
onErrorCaptured,
shallowRef
} from '@vue/runtime-test'
describe('Suspense', () => {
@@ -490,7 +491,7 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
default: [h(AsyncOuter), h(Inner)],
default: h('div', [h(AsyncOuter), h(Inner)]),
fallback: h('div', 'fallback outer')
})
}
@@ -503,14 +504,14 @@ describe('Suspense', () => {
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(
`<div>async outer</div><div>fallback inner</div>`
`<div><div>async outer</div><div>fallback inner</div></div>`
)
expect(calls).toEqual([`outer mounted`])
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<div>async outer</div><div>async inner</div>`
`<div><div>async outer</div><div>async inner</div></div>`
)
expect(calls).toEqual([`outer mounted`, `inner mounted`])
})
@@ -556,7 +557,7 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
default: [h(AsyncOuter), h(Inner)],
default: h('div', [h(AsyncOuter), h(Inner)]),
fallback: h('div', 'fallback outer')
})
}
@@ -574,7 +575,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<div>async outer</div><div>async inner</div>`
`<div><div>async outer</div><div>async inner</div></div>`
)
expect(calls).toEqual([`inner mounted`, `outer mounted`])
})
@@ -683,12 +684,12 @@ describe('Suspense', () => {
setup() {
return () =>
h(Suspense, null, {
default: [
default: h('div', [
h(MiddleComponent),
h(AsyncChildParent, {
msg: 'root async'
})
],
]),
fallback: h('div', 'root fallback')
})
}
@@ -722,7 +723,7 @@ describe('Suspense', () => {
await deps[3]
await nextTick()
expect(serializeInner(root)).toBe(
`<div>nested fallback</div><div>root async</div>`
`<div><div>nested fallback</div><div>root async</div></div>`
)
expect(calls).toEqual([0, 1, 3])
@@ -733,7 +734,7 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(
`<div>nested changed</div><div>root async</div>`
`<div><div>nested changed</div><div>root async</div></div>`
)
expect(calls).toEqual([0, 1, 3, 2])
@@ -741,51 +742,316 @@ describe('Suspense', () => {
msg.value = 'nested changed again'
await nextTick()
expect(serializeInner(root)).toBe(
`<div>nested changed again</div><div>root async</div>`
`<div><div>nested changed again</div><div>root async</div></div>`
)
})
test('new async dep after resolve should cause suspense to restart', async () => {
const toggle = ref(false)
test('switching branches', async () => {
const calls: string[] = []
const toggle = ref(true)
const ChildA = defineAsyncComponent({
const Foo = defineAsyncComponent({
setup() {
return () => h('div', 'Child A')
onMounted(() => {
calls.push('foo mounted')
})
onUnmounted(() => {
calls.push('foo unmounted')
})
return () => h('div', ['foo', h(FooNested)])
}
})
const ChildB = defineAsyncComponent({
setup() {
return () => h('div', 'Child B')
}
})
const FooNested = defineAsyncComponent(
{
setup() {
onMounted(() => {
calls.push('foo nested mounted')
})
onUnmounted(() => {
calls.push('foo nested unmounted')
})
return () => h('div', 'foo nested')
}
},
10
)
const Bar = defineAsyncComponent(
{
setup() {
onMounted(() => {
calls.push('bar mounted')
})
onUnmounted(() => {
calls.push('bar unmounted')
})
return () => h('div', 'bar')
}
},
10
)
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: [h(ChildA), toggle.value ? h(ChildB) : null],
fallback: h('div', 'root fallback')
default: toggle.value ? h(Foo) : h(Bar),
fallback: h('div', 'fallback')
})
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(`<div>Child A</div><!---->`)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
await Promise.all(deps)
await nextTick()
expect(calls).toEqual([`foo mounted`, `foo nested mounted`])
expect(serializeInner(root)).toBe(`<div>foo<div>foo nested</div></div>`)
// toggle
toggle.value = false
await nextTick()
expect(deps.length).toBe(3)
// should remain on current view
expect(calls).toEqual([`foo mounted`, `foo nested mounted`])
expect(serializeInner(root)).toBe(`<div>foo<div>foo nested</div></div>`)
await Promise.all(deps)
await nextTick()
const tempCalls = [
`foo mounted`,
`foo nested mounted`,
`bar mounted`,
`foo nested unmounted`,
`foo unmounted`
]
expect(calls).toEqual(tempCalls)
expect(serializeInner(root)).toBe(`<div>bar</div>`)
// toggle back
toggle.value = true
await nextTick()
expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
// should remain
expect(calls).toEqual(tempCalls)
expect(serializeInner(root)).toBe(`<div>bar</div>`)
await deps[3]
await nextTick()
// still pending...
expect(calls).toEqual(tempCalls)
expect(serializeInner(root)).toBe(`<div>bar</div>`)
await Promise.all(deps)
await nextTick()
expect(calls).toEqual([
...tempCalls,
`foo mounted`,
`foo nested mounted`,
`bar unmounted`
])
expect(serializeInner(root)).toBe(`<div>foo<div>foo nested</div></div>`)
})
test('branch switch to 3rd branch before resolve', async () => {
const calls: string[] = []
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
{
setup() {
onMounted(() => {
calls.push(`${name} mounted`)
})
onUnmounted(() => {
calls.push(`${name} unmounted`)
})
return () => h('div', [name])
}
},
delay
)
const One = makeComp('one')
const Two = makeComp('two', 10)
const Three = makeComp('three', 20)
const view = shallowRef(One)
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: h(view.value),
fallback: h('div', 'fallback')
})
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
view.value = Two
await nextTick()
expect(deps.length).toBe(2)
// switch before two resovles
view.value = Three
await nextTick()
expect(deps.length).toBe(3)
// dep for two resolves
await deps[1]
await nextTick()
// should still be on view one
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
await deps[2]
await nextTick()
expect(serializeInner(root)).toBe(`<div>three</div>`)
expect(calls).toEqual([`one mounted`, `three mounted`, `one unmounted`])
})
test('branch switch back before resolve', async () => {
const calls: string[] = []
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
{
setup() {
onMounted(() => {
calls.push(`${name} mounted`)
})
onUnmounted(() => {
calls.push(`${name} unmounted`)
})
return () => h('div', [name])
}
},
delay
)
const One = makeComp('one')
const Two = makeComp('two', 10)
const view = shallowRef(One)
const Comp = {
setup() {
return () =>
h(Suspense, null, {
default: h(view.value),
fallback: h('div', 'fallback')
})
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
view.value = Two
await nextTick()
expect(deps.length).toBe(2)
// switch back before two resovles
view.value = One
await nextTick()
expect(deps.length).toBe(2)
// dep for two resolves
await deps[1]
await nextTick()
// should still be on view one
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
})
test('branch switch timeout + fallback', async () => {
const calls: string[] = []
const makeComp = (name: string, delay = 0) =>
defineAsyncComponent(
{
setup() {
onMounted(() => {
calls.push(`${name} mounted`)
})
onUnmounted(() => {
calls.push(`${name} unmounted`)
})
return () => h('div', [name])
}
},
delay
)
const One = makeComp('one')
const Two = makeComp('two', 20)
const view = shallowRef(One)
const Comp = {
setup() {
return () =>
h(
Suspense,
{
timeout: 10
},
{
default: h(view.value),
fallback: h('div', 'fallback')
}
)
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([])
await deps[0]
await nextTick()
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
view.value = Two
await nextTick()
expect(serializeInner(root)).toBe(`<div>one</div>`)
expect(calls).toEqual([`one mounted`])
await new Promise(r => setTimeout(r, 10))
await nextTick()
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([`one mounted`, `one unmounted`])
await deps[1]
await nextTick()
expect(serializeInner(root)).toBe(`<div>Child A</div><div>Child B</div>`)
expect(serializeInner(root)).toBe(`<div>two</div>`)
expect(calls).toEqual([`one mounted`, `one unmounted`, `two mounted`])
})
test.todo('teleport inside suspense')
})

View File

@@ -506,8 +506,10 @@ describe('SSR hydration', () => {
const App = {
template: `
<Suspense @resolve="done">
<AsyncChild :n="1" />
<AsyncChild :n="2" />
<div>
<AsyncChild :n="1" />
<AsyncChild :n="2" />
</div>
</Suspense>`,
components: {
AsyncChild
@@ -521,7 +523,7 @@ describe('SSR hydration', () => {
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--[--><span>1</span><span>2</span><!--]-->"`
`"<div><span>1</span><span>2</span></div>"`
)
// reset asyncDeps from ssr
asyncDeps.length = 0
@@ -537,17 +539,23 @@ describe('SSR hydration', () => {
// should flush buffered effects
expect(mountedCalls).toMatchObject([1, 2])
expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`)
expect(container.innerHTML).toMatch(
`<div><span>1</span><span>2</span></div>`
)
const span1 = container.querySelector('span')!
triggerEvent('click', span1)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`)
expect(container.innerHTML).toMatch(
`<div><span>2</span><span>2</span></div>`
)
const span2 = span1.nextSibling as Element
triggerEvent('click', span2)
await nextTick()
expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`)
expect(container.innerHTML).toMatch(
`<div><span>2</span><span>3</span></div>`
)
})
test('async component', async () => {