Skip to content

fix: Stub instance of the same component #1979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions src/createInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -76,6 +76,9 @@ export function createInstance(
let component: ConcreteComponent
const instanceOptions = getInstanceOptions(options ?? {})

const rootComponents: CreateStubComponentsTransformerConfig['rootComponents'] =
{}

if (
isFunctionalComponent(originalComponent) ||
isLegacyFunctionalComponent(originalComponent)
Expand All @@ -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 })
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -329,6 +327,7 @@ export function createInstance(
createVNodeTransformer({
transformers: [
createStubComponentsTransformer({
rootComponents,
stubs: getComponentsFromStubs(global.stubs),
shallow: options?.shallow,
renderStubDefaultSlot: global.renderStubDefaultSlot
Expand Down
27 changes: 18 additions & 9 deletions src/vnodeTransformers/stubComponentsTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ interface StubOptions {
renderStubDefaultSlot?: boolean
}

const doNotStubComponents: WeakSet<ConcreteComponent> = 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
Expand Down Expand Up @@ -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<string, Component | boolean>
shallow?: boolean
renderStubDefaultSlot: boolean
}

export function createStubComponentsTransformer({
rootComponents,
stubs = {},
shallow = false,
renderStubDefaultSlot = false
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -228,7 +237,7 @@ export function createStubComponentsTransformer({
type,
renderStubDefaultSlot
})
return newStub
)
}

return type
Expand Down
14 changes: 14 additions & 0 deletions tests/components/RecursiveComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div>
<Hello />
<RecursiveComponent v-if="first" />
</div>
</template>

<script setup lang="ts">
import Hello from './Hello.vue'

defineProps<{
first?: boolean
}>()
</script>
16 changes: 16 additions & 0 deletions tests/shallowMount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
'<div>\n' +
' <hello-stub></hello-stub>\n' +
' <recursive-component-stub first="false"></recursive-component-stub>\n' +
'</div>'
)
})

it('correctly renders slot content', () => {
const ComponentWithSlot = defineComponent({
template: '<div><slot></slot></div>'
Expand Down