diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 065898048c3..d822a992816 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -17,9 +17,12 @@ import { onUnmounted, onErrorCaptured, shallowRef, + SuspenseProps, + resolveDynamicComponent, Fragment } from '@vue/runtime-test' import { createApp, defineComponent } from 'vue' +import { type RawSlots } from 'packages/runtime-core/src/componentSlots' describe('Suspense', () => { const deps: Promise[] = [] @@ -1523,4 +1526,75 @@ describe('Suspense', () => { expected = `
outerB
innerB
` expect(serializeInner(root)).toBe(expected) }) + + describe('warnings', () => { + // base function to check if a combination of slots warns or not + function baseCheckWarn( + shouldWarn: boolean, + children: RawSlots, + props: SuspenseProps | null = null + ) { + const Comp = { + setup() { + return () => h(Suspense, props, children) + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + + if (shouldWarn) { + expect(` slots expect a single root node.`).toHaveBeenWarned() + } else { + expect( + ` slots expect a single root node.` + ).not.toHaveBeenWarned() + } + } + + // actual function that we use in tests + const checkWarn = baseCheckWarn.bind(null, true) + const checkNoWarn = baseCheckWarn.bind(null, false) + + test('does not warn on single child', async () => { + checkNoWarn({ + default: h('div'), + fallback: h('div') + }) + }) + + test('does not warn on null', async () => { + checkNoWarn({ + default: null, + fallback: null + }) + }) + + test('does not warn on ', async () => { + checkNoWarn({ + default: () => [resolveDynamicComponent(null)], + fallback: () => null + }) + }) + + test('does not warn on empty array', async () => { + checkNoWarn({ + default: [], + fallback: () => [] + }) + }) + + test('warns on multiple children in default', async () => { + checkWarn({ + default: [h('div'), h('div')] + }) + }) + + test('warns on multiple children in fallback', async () => { + checkWarn({ + default: h('div'), + fallback: [h('div'), h('div')] + }) + }) + }) }) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 3640733d734..ddf21dd2ed4 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -29,6 +29,7 @@ import { assertNumber } from '../warning' import { handleError, ErrorCodes } from '../errorHandling' +import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets' export interface SuspenseProps { onResolve?: () => void @@ -795,7 +796,11 @@ function normalizeSuspenseSlot(s: any) { } if (isArray(s)) { const singleChild = filterSingleRoot(s) - if (__DEV__ && !singleChild) { + if ( + __DEV__ && + !singleChild && + s.filter(child => child !== NULL_DYNAMIC_COMPONENT).length > 0 + ) { warn(` slots expect a single root node.`) } s = singleChild diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index f8cf6652d31..10ee03c29c6 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -114,6 +114,7 @@ export type VNodeProps = { type VNodeChildAtom = | VNode + | typeof NULL_DYNAMIC_COMPONENT | string | number | boolean