Skip to content

Commit ecc1031

Browse files
freakzlikecexbrayat
authored andcommitted
feat: Stub out components prior by key than by name
1 parent 74d2568 commit ecc1031

File tree

5 files changed

+244
-34
lines changed

5 files changed

+244
-34
lines changed

docs/guide/advanced/stubs-shallow-mount.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ test('stubs component', () => {
8383

8484
This will stub out _all_ the `<FetchDataFromApi />` components in the entire render tree, regardless of what level they appear at. That's why it is in the `global` mounting option.
8585

86+
::: tip
87+
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.
88+
:::
89+
8690
## Stubbing all children components
8791

8892
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
137141

138142
## Stubbing an async component
139143

140-
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.
144+
In case you want to stub out an async component, then there are two behaviours. For example, you might have components like this:
141145

142146
```js
143147
// AsyncComponent.js
@@ -153,13 +157,35 @@ const App = defineComponent({
153157
},
154158
template: '<MyComponent/>'
155159
})
160+
```
161+
162+
The first behaviour is using the key defined in your component which loads the async component. In this example we used to key "MyComponent".
163+
It is not required to use `async/await` in the test case, because the component has been stubbed out before resolving.
156164

157-
// App.spec.js
158-
test('stubs async component', async () => {
165+
```js
166+
test('stubs async component without resolving', () => {
167+
const wrapper = mount(App, {
168+
global: {
169+
stubs: {
170+
MyComponent: true
171+
}
172+
}
173+
})
174+
175+
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
176+
})
177+
```
178+
179+
The second behaviour is using the name of the async component. In this example we used to name "AsyncComponent".
180+
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.
181+
182+
**Make sure you define a name in your async component!**
183+
184+
```js
185+
test('stubs async component with resolving', async () => {
159186
const wrapper = mount(App, {
160187
global: {
161188
stubs: {
162-
// Besure to use the name from AsyncComponent and not "MyComponent"
163189
AsyncComponent: true
164190
}
165191
}

src/stubs.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ const resolveComponentStubByName = (
7575
}
7676
}
7777

78+
const getComponentRegisteredName = (
79+
instance: ComponentInternalInstance | null,
80+
type: VNodeTypes
81+
): string | null => {
82+
if (!instance || !instance.parent) return null
83+
84+
// try to infer the name based on local resolution
85+
const registry = (instance.type as any).components
86+
for (const key in registry) {
87+
if (registry[key] === type) {
88+
return key
89+
}
90+
}
91+
92+
return null
93+
}
94+
7895
const isHTMLElement = (type: VNodeTypes) => typeof type === 'string'
7996

8097
const isCommentOrFragment = (type: VNodeTypes) => typeof type === 'symbol'
@@ -139,27 +156,35 @@ export function stubComponents(
139156
}
140157

141158
if (isComponent(type) || isFunctionalComponent(type)) {
142-
let name = type['name'] || type['displayName']
143-
144-
// if no name, then check the locally registered components in the parent
145-
if (!name && instance && instance.parent) {
146-
// try to infer the name based on local resolution
147-
const registry = (instance.type as any).components
148-
for (const key in registry) {
149-
if (registry[key] === type) {
150-
name = key
151-
break
152-
}
153-
}
154-
}
155-
if (!name) {
159+
const registeredName = getComponentRegisteredName(instance, type)
160+
const componentName = type['name'] || type['displayName']
161+
162+
// No name found?
163+
if (!registeredName && !componentName) {
156164
return shallow ? ['stub'] : args
157165
}
158166

159-
const stub = resolveComponentStubByName(name, stubs)
167+
let stub = null
168+
let name = null
169+
170+
// Prio 1 using the key in locally registered components in the parent
171+
if (registeredName) {
172+
stub = resolveComponentStubByName(registeredName, stubs)
173+
if (stub) {
174+
name = registeredName
175+
}
176+
}
177+
178+
// Prio 2 using the name attribute in the component
179+
if (!stub && componentName) {
180+
stub = resolveComponentStubByName(componentName, stubs)
181+
if (stub) {
182+
name = componentName
183+
}
184+
}
160185

161186
// case 2: custom implementation
162-
if (typeof stub === 'object') {
187+
if (stub && typeof stub === 'object') {
163188
// pass the props and children, for advanced stubbing
164189
return [stubs[name], props, children, patchFlag, dynamicProps]
165190
}
@@ -168,6 +193,11 @@ export function stubComponents(
168193
// where the signature is h(Component, props, slots)
169194
// case 1: default stub
170195
if (stub === true || shallow) {
196+
// Set name when using shallow without stub
197+
if (!name) {
198+
name = registeredName || componentName
199+
}
200+
171201
const propsDeclaration = type?.props || {}
172202
const newStub = createStub({ name, propsDeclaration, props })
173203
stubs[name] = newStub
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`shallowMount renders props for stubbed component in a snapshot 1`] = `<my-label-stub val="username"></my-label-stub>`;
3+
exports[`shallowMount renders props for stubbed component in a snapshot 1`] = `
4+
<div>
5+
<my-label-stub val="username"></my-label-stub>
6+
<async-component-stub></async-component-stub>
7+
</div>
8+
`;

tests/mountingOptions/stubs.global.spec.ts

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -393,29 +393,170 @@ describe('mounting options: stubs', () => {
393393
expect(wrapper.find('#content').exists()).toBe(true)
394394
})
395395

396-
it('stubs async component with name', async () => {
397-
const AsyncComponent = defineComponent({
398-
name: 'AsyncComponent',
399-
template: '<span>AsyncComponent</span>'
396+
it('stubs component by key prior before name', () => {
397+
const MyComponent = defineComponent({
398+
name: 'MyComponent',
399+
template: '<span>MyComponent</span>'
400400
})
401+
401402
const TestComponent = defineComponent({
402403
components: {
403-
MyComponent: defineAsyncComponent(async () => AsyncComponent)
404+
MyComponentKey: MyComponent
404405
},
405-
template: '<MyComponent/>'
406+
template: '<MyComponentKey/>'
406407
})
407408

408409
const wrapper = mount(TestComponent, {
409410
global: {
410411
stubs: {
411-
AsyncComponent: true
412+
MyComponentKey: {
413+
template: '<span>MyComponentKey stubbed</span>'
414+
},
415+
MyComponent: {
416+
template: '<span>MyComponent stubbed</span>'
417+
}
412418
}
413419
}
414420
})
415421

416-
await flushPromises()
422+
expect(wrapper.html()).toBe('<span>MyComponentKey stubbed</span>')
423+
})
424+
425+
describe('stub async component', () => {
426+
const AsyncComponent = defineAsyncComponent(async () => ({
427+
name: 'AsyncComponent',
428+
template: '<span>AsyncComponent</span>'
429+
}))
430+
431+
const AsyncComponentWithoutName = defineAsyncComponent(async () => ({
432+
template: '<span>AsyncComponent</span>'
433+
}))
434+
435+
it('stubs async component with name', async () => {
436+
const TestComponent = defineComponent({
437+
components: {
438+
MyComponent: AsyncComponent
439+
},
440+
template: '<MyComponent/>'
441+
})
442+
443+
const wrapper = mount(TestComponent, {
444+
global: {
445+
stubs: {
446+
AsyncComponent: true
447+
}
448+
}
449+
})
450+
451+
// flushPromises required to resolve async component
452+
expect(wrapper.html()).not.toBe(
453+
'<async-component-stub></async-component-stub>'
454+
)
455+
await flushPromises()
456+
457+
expect(wrapper.html()).toBe(
458+
'<async-component-stub></async-component-stub>'
459+
)
460+
})
461+
462+
it('stubs async component with name by alias', () => {
463+
const TestComponent = defineComponent({
464+
components: {
465+
MyComponent: AsyncComponent
466+
},
467+
template: '<MyComponent/>'
468+
})
417469

418-
expect(wrapper.html()).toBe('<async-component-stub></async-component-stub>')
470+
const wrapper = mount(TestComponent, {
471+
global: {
472+
stubs: {
473+
MyComponent: true
474+
}
475+
}
476+
})
477+
478+
// flushPromises no longer required
479+
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
480+
})
481+
482+
it('stubs async component without name', () => {
483+
const TestComponent = defineComponent({
484+
components: {
485+
Foo: {
486+
template: '<div />'
487+
},
488+
MyComponent: AsyncComponentWithoutName
489+
},
490+
template: '<MyComponent/>'
491+
})
492+
493+
const wrapper = mount(TestComponent, {
494+
global: {
495+
stubs: {
496+
MyComponent: true
497+
}
498+
}
499+
})
500+
501+
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
502+
})
503+
504+
it('stubs async component without name and kebab-case', () => {
505+
const TestComponent = defineComponent({
506+
components: {
507+
MyComponent: AsyncComponentWithoutName
508+
},
509+
template: '<MyComponent/>'
510+
})
511+
512+
const wrapper = mount(TestComponent, {
513+
global: {
514+
stubs: {
515+
'my-component': true
516+
}
517+
}
518+
})
519+
520+
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
521+
})
522+
523+
it('stubs async component with string', () => {
524+
const TestComponent = defineComponent({
525+
components: {
526+
MyComponent: AsyncComponentWithoutName
527+
},
528+
template: '<my-component/>'
529+
})
530+
531+
const wrapper = mount(TestComponent, {
532+
global: {
533+
stubs: ['MyComponent']
534+
}
535+
})
536+
537+
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
538+
})
539+
540+
it('stubs async component with other component', () => {
541+
const TestComponent = defineComponent({
542+
components: {
543+
MyComponent: AsyncComponentWithoutName
544+
},
545+
template: '<my-component/>'
546+
})
547+
548+
const wrapper = mount(TestComponent, {
549+
global: {
550+
stubs: {
551+
MyComponent: defineComponent({
552+
template: '<span>StubComponent</span>'
553+
})
554+
}
555+
}
556+
})
557+
558+
expect(wrapper.html()).toBe('<span>StubComponent</span>')
559+
})
419560
})
420561

421562
describe('stub slots', () => {

tests/shallowMount.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineComponent } from 'vue'
1+
import { defineAsyncComponent, defineComponent } from 'vue'
22
import { mount, shallowMount, VueWrapper } from '../src'
33
import ComponentWithChildren from './components/ComponentWithChildren.vue'
44
import ScriptSetupWithChildren from './components/ScriptSetupWithChildren.vue'
@@ -19,9 +19,14 @@ describe('shallowMount', () => {
1919
template: '<label :for="val">{{ val }}</label>'
2020
})
2121

22+
const AsyncComponent = defineAsyncComponent(async () => ({
23+
name: 'AsyncComponentName',
24+
template: '<span>AsyncComponent</span>'
25+
}))
26+
2227
const Component = defineComponent({
23-
components: { MyLabel },
24-
template: '<MyLabel val="username" />',
28+
components: { MyLabel, AsyncComponent },
29+
template: '<div><MyLabel val="username" /><AsyncComponent /></div>',
2530
data() {
2631
return {
2732
foo: 'bar'
@@ -32,7 +37,10 @@ describe('shallowMount', () => {
3237
const wrapper = shallowMount(Component)
3338

3439
expect(wrapper.html()).toBe(
35-
'<my-label-stub val="username"></my-label-stub>'
40+
'<div>\n' +
41+
' <my-label-stub val="username"></my-label-stub>\n' +
42+
' <async-component-stub></async-component-stub>\n' +
43+
'</div>'
3644
)
3745
expect(wrapper).toMatchSnapshot()
3846
})

0 commit comments

Comments
 (0)