diff --git a/src/createInstance.ts b/src/createInstance.ts index 0a2779574..394574963 100644 --- a/src/createInstance.ts +++ b/src/createInstance.ts @@ -32,8 +32,8 @@ import { } from './utils/vueCompatSupport' import { createVNodeTransformer } from './vnodeTransformers/util' import { - addToDoNotStubComponents, - createStubComponentsTransformer + createStubComponentsTransformer, + CreateStubComponentsTransformerConfig } from './vnodeTransformers/stubComponentsTransformer' import { createStubDirectivesTransformer } from './vnodeTransformers/stubDirectivesTransformer' @@ -76,6 +76,9 @@ export function createInstance( let component: ConcreteComponent const instanceOptions = getInstanceOptions(options ?? {}) + const rootComponents: CreateStubComponentsTransformerConfig['rootComponents'] = + {} + if ( isFunctionalComponent(originalComponent) || isLegacyFunctionalComponent(originalComponent) @@ -96,14 +99,14 @@ export function createInstance( h(originalComponent, { ...props, ...attrs }, slots), ...instanceOptions }) - addToDoNotStubComponents(originalComponent) + rootComponents.functional = originalComponent } else if (isObjectComponent(originalComponent)) { component = { ...originalComponent, ...instanceOptions } } else { component = originalComponent } - addToDoNotStubComponents(component) + rootComponents.component = component // We've just replaced our component with its copy // Let's register it as a stub so user can find it registerStub({ source: originalComponent, stub: component }) @@ -217,11 +220,6 @@ export function createInstance( // create the app const app = createApp(Parent) - // the Parent type must not be stubbed - // but we can't add it directly, as createApp creates a copy - // and store it in app._component (since v3.2.32) - // So we store this one instead - addToDoNotStubComponents(app._component) // add tracking for emitted events // this must be done after `createApp`: https://github.com/vuejs/test-utils/issues/436 @@ -329,6 +327,7 @@ export function createInstance( createVNodeTransformer({ transformers: [ createStubComponentsTransformer({ + rootComponents, stubs: getComponentsFromStubs(global.stubs), shallow: options?.shallow, renderStubDefaultSlot: global.renderStubDefaultSlot diff --git a/src/vnodeTransformers/stubComponentsTransformer.ts b/src/vnodeTransformers/stubComponentsTransformer.ts index d2d44e973..78aeae294 100644 --- a/src/vnodeTransformers/stubComponentsTransformer.ts +++ b/src/vnodeTransformers/stubComponentsTransformer.ts @@ -36,11 +36,6 @@ interface StubOptions { renderStubDefaultSlot?: boolean } -const doNotStubComponents: WeakSet = new WeakSet() -const shouldNotStub = (type: ConcreteComponent) => doNotStubComponents.has(type) -export const addToDoNotStubComponents = (type: ConcreteComponent) => - doNotStubComponents.add(type) - const normalizeStubProps = (props: ComponentPropsOptions) => { // props are always normalized to object syntax const $props = props as unknown as ComponentObjectPropsOptions @@ -102,13 +97,20 @@ const resolveComponentStubByName = ( } } -interface CreateStubComponentsTransformerConfig { +export interface CreateStubComponentsTransformerConfig { + rootComponents: { + // Component which has been passed to mount. For functional components it contains a wrapper + component?: Component + // If component is functional then contains the original component otherwise empty + functional?: Component + } stubs?: Record shallow?: boolean renderStubDefaultSlot: boolean } export function createStubComponentsTransformer({ + rootComponents, stubs = {}, shallow = false, renderStubDefaultSlot = false @@ -162,7 +164,14 @@ export function createStubComponentsTransformer({ }) } - if (shouldNotStub(type)) { + if ( + // Don't stub VTU_ROOT component + !instance || + // Don't stub mounted component on root level + (rootComponents.component === type && !instance?.parent) || + // Don't stub component with compat wrapper + (rootComponents.functional && rootComponents.functional === type) + ) { return type } @@ -218,7 +227,7 @@ export function createStubComponentsTransformer({ // Set name when using shallow without stub const stubName = name || registeredName || componentName - const newStub = + return ( config.plugins.createStubs?.({ name: stubName, component: type @@ -228,7 +237,7 @@ export function createStubComponentsTransformer({ type, renderStubDefaultSlot }) - return newStub + ) } return type diff --git a/tests/components/RecursiveComponent.vue b/tests/components/RecursiveComponent.vue new file mode 100644 index 000000000..63b34a344 --- /dev/null +++ b/tests/components/RecursiveComponent.vue @@ -0,0 +1,14 @@ + + + diff --git a/tests/shallowMount.spec.ts b/tests/shallowMount.spec.ts index a852cbca1..21209f04e 100644 --- a/tests/shallowMount.spec.ts +++ b/tests/shallowMount.spec.ts @@ -4,6 +4,7 @@ import { mount, shallowMount, VueWrapper } from '../src' import ComponentWithChildren from './components/ComponentWithChildren.vue' import ScriptSetupWithChildren from './components/ScriptSetupWithChildren.vue' import DynamicComponentWithComputedProperty from './components/DynamicComponentWithComputedProperty.vue' +import RecursiveComponent from './components/RecursiveComponent.vue' describe('shallowMount', () => { it('renders props for stubbed component in a snapshot', () => { @@ -73,6 +74,21 @@ describe('shallowMount', () => { ) }) + it('stub instance of same component', () => { + const wrapper = mount(RecursiveComponent, { + shallow: true, + props: { + first: true + } + }) + expect(wrapper.html()).toEqual( + '
\n' + + ' \n' + + ' \n' + + '
' + ) + }) + it('correctly renders slot content', () => { const ComponentWithSlot = defineComponent({ template: '
'