diff --git a/docs/guide/advanced/stubs-shallow-mount.md b/docs/guide/advanced/stubs-shallow-mount.md index 25490fab5..d56166d1b 100644 --- a/docs/guide/advanced/stubs-shallow-mount.md +++ b/docs/guide/advanced/stubs-shallow-mount.md @@ -83,6 +83,10 @@ test('stubs component', () => { This will stub out _all_ the `` components in the entire render tree, regardless of what level they appear at. That's why it is in the `global` mounting option. +::: tip +To stub out you can either use the key in `components` or the name of your component. If both are given in `global.stubs` the key will be used first. +::: + ## Stubbing all children components Sometimes you might want to stub out _all_ the custom components. For example you might have a component like this: @@ -137,7 +141,7 @@ If you used VTU V1, you may remember this as `shallowMount`. That method is stil ## Stubbing an async component -In case you want to stub out an async component, then make sure to provide a name for the component and use this name as stubs key. +In case you want to stub out an async component, then there are two behaviours. For example, you might have components like this: ```js // AsyncComponent.js @@ -153,13 +157,35 @@ const App = defineComponent({ }, template: '' }) +``` + +The first behaviour is using the key defined in your component which loads the async component. In this example we used to key "MyComponent". +It is not required to use `async/await` in the test case, because the component has been stubbed out before resolving. -// App.spec.js -test('stubs async component', async () => { +```js +test('stubs async component without resolving', () => { + const wrapper = mount(App, { + global: { + stubs: { + MyComponent: true + } + } + }) + + expect(wrapper.html()).toBe('') +}) +``` + +The second behaviour is using the name of the async component. In this example we used to name "AsyncComponent". +Now it is required to use `async/await`, because the async component needs to be resolved and then can be stubbed out by the name defined in the async component. + +**Make sure you define a name in your async component!** + +```js +test('stubs async component with resolving', async () => { const wrapper = mount(App, { global: { stubs: { - // Besure to use the name from AsyncComponent and not "MyComponent" AsyncComponent: true } } diff --git a/src/stubs.ts b/src/stubs.ts index f50ca084d..2443df0ec 100644 --- a/src/stubs.ts +++ b/src/stubs.ts @@ -75,6 +75,23 @@ const resolveComponentStubByName = ( } } +const getComponentRegisteredName = ( + instance: ComponentInternalInstance | null, + type: VNodeTypes +): string | null => { + if (!instance || !instance.parent) return null + + // try to infer the name based on local resolution + const registry = (instance.type as any).components + for (const key in registry) { + if (registry[key] === type) { + return key + } + } + + return null +} + const isHTMLElement = (type: VNodeTypes) => typeof type === 'string' const isCommentOrFragment = (type: VNodeTypes) => typeof type === 'symbol' @@ -139,27 +156,35 @@ export function stubComponents( } if (isComponent(type) || isFunctionalComponent(type)) { - let name = type['name'] || type['displayName'] - - // if no name, then check the locally registered components in the parent - if (!name && instance && instance.parent) { - // try to infer the name based on local resolution - const registry = (instance.type as any).components - for (const key in registry) { - if (registry[key] === type) { - name = key - break - } - } - } - if (!name) { + const registeredName = getComponentRegisteredName(instance, type) + const componentName = type['name'] || type['displayName'] + + // No name found? + if (!registeredName && !componentName) { return shallow ? ['stub'] : args } - const stub = resolveComponentStubByName(name, stubs) + let stub = null + let name = null + + // Prio 1 using the key in locally registered components in the parent + if (registeredName) { + stub = resolveComponentStubByName(registeredName, stubs) + if (stub) { + name = registeredName + } + } + + // Prio 2 using the name attribute in the component + if (!stub && componentName) { + stub = resolveComponentStubByName(componentName, stubs) + if (stub) { + name = componentName + } + } // case 2: custom implementation - if (typeof stub === 'object') { + if (stub && typeof stub === 'object') { // pass the props and children, for advanced stubbing return [stubs[name], props, children, patchFlag, dynamicProps] } @@ -168,6 +193,11 @@ export function stubComponents( // where the signature is h(Component, props, slots) // case 1: default stub if (stub === true || shallow) { + // Set name when using shallow without stub + if (!name) { + name = registeredName || componentName + } + const propsDeclaration = type?.props || {} const newStub = createStub({ name, propsDeclaration, props }) stubs[name] = newStub diff --git a/tests/__snapshots__/shallowMount.spec.ts.snap b/tests/__snapshots__/shallowMount.spec.ts.snap index dc4213466..a757d11b8 100644 --- a/tests/__snapshots__/shallowMount.spec.ts.snap +++ b/tests/__snapshots__/shallowMount.spec.ts.snap @@ -1,3 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`shallowMount renders props for stubbed component in a snapshot 1`] = ``; +exports[`shallowMount renders props for stubbed component in a snapshot 1`] = ` +
+ + +
+`; diff --git a/tests/mountingOptions/stubs.global.spec.ts b/tests/mountingOptions/stubs.global.spec.ts index 434518253..95d045225 100644 --- a/tests/mountingOptions/stubs.global.spec.ts +++ b/tests/mountingOptions/stubs.global.spec.ts @@ -393,29 +393,170 @@ describe('mounting options: stubs', () => { expect(wrapper.find('#content').exists()).toBe(true) }) - it('stubs async component with name', async () => { - const AsyncComponent = defineComponent({ - name: 'AsyncComponent', - template: 'AsyncComponent' + it('stubs component by key prior before name', () => { + const MyComponent = defineComponent({ + name: 'MyComponent', + template: 'MyComponent' }) + const TestComponent = defineComponent({ components: { - MyComponent: defineAsyncComponent(async () => AsyncComponent) + MyComponentKey: MyComponent }, - template: '' + template: '' }) const wrapper = mount(TestComponent, { global: { stubs: { - AsyncComponent: true + MyComponentKey: { + template: 'MyComponentKey stubbed' + }, + MyComponent: { + template: 'MyComponent stubbed' + } } } }) - await flushPromises() + expect(wrapper.html()).toBe('MyComponentKey stubbed') + }) + + describe('stub async component', () => { + const AsyncComponent = defineAsyncComponent(async () => ({ + name: 'AsyncComponent', + template: 'AsyncComponent' + })) + + const AsyncComponentWithoutName = defineAsyncComponent(async () => ({ + template: 'AsyncComponent' + })) + + it('stubs async component with name', async () => { + const TestComponent = defineComponent({ + components: { + MyComponent: AsyncComponent + }, + template: '' + }) + + const wrapper = mount(TestComponent, { + global: { + stubs: { + AsyncComponent: true + } + } + }) + + // flushPromises required to resolve async component + expect(wrapper.html()).not.toBe( + '' + ) + await flushPromises() + + expect(wrapper.html()).toBe( + '' + ) + }) + + it('stubs async component with name by alias', () => { + const TestComponent = defineComponent({ + components: { + MyComponent: AsyncComponent + }, + template: '' + }) - expect(wrapper.html()).toBe('') + const wrapper = mount(TestComponent, { + global: { + stubs: { + MyComponent: true + } + } + }) + + // flushPromises no longer required + expect(wrapper.html()).toBe('') + }) + + it('stubs async component without name', () => { + const TestComponent = defineComponent({ + components: { + Foo: { + template: '
' + }, + MyComponent: AsyncComponentWithoutName + }, + template: '' + }) + + const wrapper = mount(TestComponent, { + global: { + stubs: { + MyComponent: true + } + } + }) + + expect(wrapper.html()).toBe('') + }) + + it('stubs async component without name and kebab-case', () => { + const TestComponent = defineComponent({ + components: { + MyComponent: AsyncComponentWithoutName + }, + template: '' + }) + + const wrapper = mount(TestComponent, { + global: { + stubs: { + 'my-component': true + } + } + }) + + expect(wrapper.html()).toBe('') + }) + + it('stubs async component with string', () => { + const TestComponent = defineComponent({ + components: { + MyComponent: AsyncComponentWithoutName + }, + template: '' + }) + + const wrapper = mount(TestComponent, { + global: { + stubs: ['MyComponent'] + } + }) + + expect(wrapper.html()).toBe('') + }) + + it('stubs async component with other component', () => { + const TestComponent = defineComponent({ + components: { + MyComponent: AsyncComponentWithoutName + }, + template: '' + }) + + const wrapper = mount(TestComponent, { + global: { + stubs: { + MyComponent: defineComponent({ + template: 'StubComponent' + }) + } + } + }) + + expect(wrapper.html()).toBe('StubComponent') + }) }) describe('stub slots', () => { diff --git a/tests/shallowMount.spec.ts b/tests/shallowMount.spec.ts index 4b8f5eb01..759828c2f 100644 --- a/tests/shallowMount.spec.ts +++ b/tests/shallowMount.spec.ts @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue' +import { defineAsyncComponent, defineComponent } from 'vue' import { mount, shallowMount, VueWrapper } from '../src' import ComponentWithChildren from './components/ComponentWithChildren.vue' import ScriptSetupWithChildren from './components/ScriptSetupWithChildren.vue' @@ -19,9 +19,14 @@ describe('shallowMount', () => { template: '' }) + const AsyncComponent = defineAsyncComponent(async () => ({ + name: 'AsyncComponentName', + template: 'AsyncComponent' + })) + const Component = defineComponent({ - components: { MyLabel }, - template: '', + components: { MyLabel, AsyncComponent }, + template: '
', data() { return { foo: 'bar' @@ -32,7 +37,10 @@ describe('shallowMount', () => { const wrapper = shallowMount(Component) expect(wrapper.html()).toBe( - '' + '
\n' + + ' \n' + + ' \n' + + '
' ) expect(wrapper).toMatchSnapshot() })