Skip to content

Commit

Permalink
feat: Stub out components prior by key than by name
Browse files Browse the repository at this point in the history
  • Loading branch information
freakzlike authored and cexbrayat committed Apr 5, 2021
1 parent 74d2568 commit ecc1031
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 34 deletions.
34 changes: 30 additions & 4 deletions docs/guide/advanced/stubs-shallow-mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ test('stubs component', () => {

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.

::: 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:
Expand Down Expand Up @@ -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
Expand All @@ -153,13 +157,35 @@ const App = defineComponent({
},
template: '<MyComponent/>'
})
```

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('<my-component-stub></my-component-stub>')
})
```

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
}
}
Expand Down
62 changes: 46 additions & 16 deletions src/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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]
}
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion tests/__snapshots__/shallowMount.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`shallowMount renders props for stubbed component in a snapshot 1`] = `<my-label-stub val="username"></my-label-stub>`;
exports[`shallowMount renders props for stubbed component in a snapshot 1`] = `
<div>
<my-label-stub val="username"></my-label-stub>
<async-component-stub></async-component-stub>
</div>
`;
159 changes: 150 additions & 9 deletions tests/mountingOptions/stubs.global.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<span>AsyncComponent</span>'
it('stubs component by key prior before name', () => {
const MyComponent = defineComponent({
name: 'MyComponent',
template: '<span>MyComponent</span>'
})

const TestComponent = defineComponent({
components: {
MyComponent: defineAsyncComponent(async () => AsyncComponent)
MyComponentKey: MyComponent
},
template: '<MyComponent/>'
template: '<MyComponentKey/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: {
AsyncComponent: true
MyComponentKey: {
template: '<span>MyComponentKey stubbed</span>'
},
MyComponent: {
template: '<span>MyComponent stubbed</span>'
}
}
}
})

await flushPromises()
expect(wrapper.html()).toBe('<span>MyComponentKey stubbed</span>')
})

describe('stub async component', () => {
const AsyncComponent = defineAsyncComponent(async () => ({
name: 'AsyncComponent',
template: '<span>AsyncComponent</span>'
}))

const AsyncComponentWithoutName = defineAsyncComponent(async () => ({
template: '<span>AsyncComponent</span>'
}))

it('stubs async component with name', async () => {
const TestComponent = defineComponent({
components: {
MyComponent: AsyncComponent
},
template: '<MyComponent/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: {
AsyncComponent: true
}
}
})

// flushPromises required to resolve async component
expect(wrapper.html()).not.toBe(
'<async-component-stub></async-component-stub>'
)
await flushPromises()

expect(wrapper.html()).toBe(
'<async-component-stub></async-component-stub>'
)
})

it('stubs async component with name by alias', () => {
const TestComponent = defineComponent({
components: {
MyComponent: AsyncComponent
},
template: '<MyComponent/>'
})

expect(wrapper.html()).toBe('<async-component-stub></async-component-stub>')
const wrapper = mount(TestComponent, {
global: {
stubs: {
MyComponent: true
}
}
})

// flushPromises no longer required
expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

it('stubs async component without name', () => {
const TestComponent = defineComponent({
components: {
Foo: {
template: '<div />'
},
MyComponent: AsyncComponentWithoutName
},
template: '<MyComponent/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: {
MyComponent: true
}
}
})

expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

it('stubs async component without name and kebab-case', () => {
const TestComponent = defineComponent({
components: {
MyComponent: AsyncComponentWithoutName
},
template: '<MyComponent/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: {
'my-component': true
}
}
})

expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

it('stubs async component with string', () => {
const TestComponent = defineComponent({
components: {
MyComponent: AsyncComponentWithoutName
},
template: '<my-component/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: ['MyComponent']
}
})

expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

it('stubs async component with other component', () => {
const TestComponent = defineComponent({
components: {
MyComponent: AsyncComponentWithoutName
},
template: '<my-component/>'
})

const wrapper = mount(TestComponent, {
global: {
stubs: {
MyComponent: defineComponent({
template: '<span>StubComponent</span>'
})
}
}
})

expect(wrapper.html()).toBe('<span>StubComponent</span>')
})
})

describe('stub slots', () => {
Expand Down
16 changes: 12 additions & 4 deletions tests/shallowMount.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,9 +19,14 @@ describe('shallowMount', () => {
template: '<label :for="val">{{ val }}</label>'
})

const AsyncComponent = defineAsyncComponent(async () => ({
name: 'AsyncComponentName',
template: '<span>AsyncComponent</span>'
}))

const Component = defineComponent({
components: { MyLabel },
template: '<MyLabel val="username" />',
components: { MyLabel, AsyncComponent },
template: '<div><MyLabel val="username" /><AsyncComponent /></div>',
data() {
return {
foo: 'bar'
Expand All @@ -32,7 +37,10 @@ describe('shallowMount', () => {
const wrapper = shallowMount(Component)

expect(wrapper.html()).toBe(
'<my-label-stub val="username"></my-label-stub>'
'<div>\n' +
' <my-label-stub val="username"></my-label-stub>\n' +
' <async-component-stub></async-component-stub>\n' +
'</div>'
)
expect(wrapper).toMatchSnapshot()
})
Expand Down

0 comments on commit ecc1031

Please sign in to comment.