From b73c4d100d0ce2d4a5a994d4945e1578bd138103 Mon Sep 17 00:00:00 2001 From: freakzlike Date: Mon, 17 Jan 2022 17:09:43 +0100 Subject: [PATCH 1/3] feat(plugins): Add createStubs plugin hook --- src/config.ts | 2 + src/stubs.ts | 21 +++++-- tests/features/plugins.spec.ts | 101 ++++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index b921e1717..2771ba84b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,12 +1,14 @@ import { GlobalMountOptions } from './types' import { VueWrapper } from './vueWrapper' import { DOMWrapper } from './domWrapper' +import { CustomCreateStub } from './stubs' export interface GlobalConfigOptions { global: Required plugins: { VueWrapper: Pluggable DOMWrapper: Pluggable> + createStubs?: CustomCreateStub } renderStubDefaultSlot: boolean } diff --git a/src/stubs.ts b/src/stubs.ts index 6da5757d1..75a36b98f 100644 --- a/src/stubs.ts +++ b/src/stubs.ts @@ -20,6 +20,12 @@ import { getComponentName, getComponentRegisteredName } from './utils/componentName' +import { config } from './config' + +export type CustomCreateStub = (params: { + name: string + component: ConcreteComponent +}) => ConcreteComponent interface StubOptions { name: string @@ -259,11 +265,16 @@ export function stubComponents( } const newStub = createStubOnce(type, () => - createStub({ - name: stubName, - type, - renderStubDefaultSlot - }) + config.plugins.createStubs + ? config.plugins.createStubs({ + name: stubName, + component: type + }) + : createStub({ + name: stubName, + type, + renderStubDefaultSlot + }) ) registerStub({ source: type, stub: newStub }) return [newStub, props, children, patchFlag, dynamicProps] diff --git a/tests/features/plugins.spec.ts b/tests/features/plugins.spec.ts index 64b33ec1a..556be8f02 100644 --- a/tests/features/plugins.spec.ts +++ b/tests/features/plugins.spec.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance } from 'vue' +import { ComponentPublicInstance, h } from 'vue' import { mount, config, VueWrapper } from '../../src' @@ -92,3 +92,102 @@ describe('Plugin#install', () => { ) }) }) + +describe('createStubs', () => { + const Child1 = { + name: 'child1', + render: () => h('div', 'real child 1') + } + const Child2 = { + name: 'child2', + render: () => h('div', 'real child 2') + } + + const Parent = { + render: () => h('div', [ + h(Child1), + h(Child1), + h(Child2) + ]) + } + + const customCreateStub = jest.fn(({ name }) => h(`${name}-custom-stub`)) + beforeAll(() => { + config.plugins.createStubs = customCreateStub + }) + + afterAll(() => { + config.plugins.createStubs = undefined + }) + + beforeEach(() => { + customCreateStub.mockClear() + }) + + it('should be called for every stub once', () => { + const wrapper = mount(Parent, { + shallow: true + }) + + expect(wrapper.html()).toBe('
\n' + + ' \n' + + ' \n' + + ' \n' + + '
') + + expect(customCreateStub).toHaveBeenCalledTimes(2) + expect(customCreateStub).toHaveBeenCalledWith({ name: 'child1', component: Child1 }) + expect(customCreateStub).toHaveBeenCalledWith({ name: 'child2', component: Child2 }) + }) + + it('should be called only for stubbed components', () => { + const wrapper = mount(Parent, { + global: { + stubs: { + child2: true + } + } + }) + + expect(wrapper.html()).toBe('
\n' + + '
real child 1
\n' + + '
real child 1
\n' + + ' \n' + + '
') + + expect(customCreateStub).toHaveBeenCalledTimes(1) + expect(customCreateStub).toHaveBeenCalledWith({ name: 'child2', component: Child2 }) + }) + + it('should not be called for no stubs', () => { + const wrapper = mount(Parent) + + expect(wrapper.html()).toBe('
\n' + + '
real child 1
\n' + + '
real child 1
\n' + + '
real child 2
\n' + + '
') + + expect(customCreateStub).not.toHaveBeenCalled() + }) + + it('should not be called for manual stubs', () => { + const wrapper = mount(Parent, { + shallow: true, + global: { + stubs: { + child2: () => h('div', 'Child 2 stub') + } + } + }) + + expect(wrapper.html()).toBe('
\n' + + ' \n' + + ' \n' + + '
Child 2 stub
\n' + + '
') + + expect(customCreateStub).toHaveBeenCalledTimes(1) + expect(customCreateStub).toHaveBeenCalledWith({ name: 'child1', component: Child1 }) + }) +}) From 5a46a69cf58e8e659bb4b04b3119b6f3f0e962de Mon Sep 17 00:00:00 2001 From: freakzlike Date: Mon, 17 Jan 2022 17:28:05 +0100 Subject: [PATCH 2/3] Fix lint errors --- tests/features/plugins.spec.ts | 74 +++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/tests/features/plugins.spec.ts b/tests/features/plugins.spec.ts index 556be8f02..78d51701d 100644 --- a/tests/features/plugins.spec.ts +++ b/tests/features/plugins.spec.ts @@ -104,11 +104,7 @@ describe('createStubs', () => { } const Parent = { - render: () => h('div', [ - h(Child1), - h(Child1), - h(Child2) - ]) + render: () => h('div', [h(Child1), h(Child1), h(Child2)]) } const customCreateStub = jest.fn(({ name }) => h(`${name}-custom-stub`)) @@ -129,15 +125,23 @@ describe('createStubs', () => { shallow: true }) - expect(wrapper.html()).toBe('
\n' + - ' \n' + - ' \n' + - ' \n' + - '
') + expect(wrapper.html()).toBe( + '
\n' + + ' \n' + + ' \n' + + ' \n' + + '
' + ) expect(customCreateStub).toHaveBeenCalledTimes(2) - expect(customCreateStub).toHaveBeenCalledWith({ name: 'child1', component: Child1 }) - expect(customCreateStub).toHaveBeenCalledWith({ name: 'child2', component: Child2 }) + expect(customCreateStub).toHaveBeenCalledWith({ + name: 'child1', + component: Child1 + }) + expect(customCreateStub).toHaveBeenCalledWith({ + name: 'child2', + component: Child2 + }) }) it('should be called only for stubbed components', () => { @@ -149,24 +153,31 @@ describe('createStubs', () => { } }) - expect(wrapper.html()).toBe('
\n' + - '
real child 1
\n' + - '
real child 1
\n' + - ' \n' + - '
') + expect(wrapper.html()).toBe( + '
\n' + + '
real child 1
\n' + + '
real child 1
\n' + + ' \n' + + '
' + ) expect(customCreateStub).toHaveBeenCalledTimes(1) - expect(customCreateStub).toHaveBeenCalledWith({ name: 'child2', component: Child2 }) + expect(customCreateStub).toHaveBeenCalledWith({ + name: 'child2', + component: Child2 + }) }) it('should not be called for no stubs', () => { const wrapper = mount(Parent) - expect(wrapper.html()).toBe('
\n' + - '
real child 1
\n' + - '
real child 1
\n' + - '
real child 2
\n' + - '
') + expect(wrapper.html()).toBe( + '
\n' + + '
real child 1
\n' + + '
real child 1
\n' + + '
real child 2
\n' + + '
' + ) expect(customCreateStub).not.toHaveBeenCalled() }) @@ -181,13 +192,18 @@ describe('createStubs', () => { } }) - expect(wrapper.html()).toBe('
\n' + - ' \n' + - ' \n' + - '
Child 2 stub
\n' + - '
') + expect(wrapper.html()).toBe( + '
\n' + + ' \n' + + ' \n' + + '
Child 2 stub
\n' + + '
' + ) expect(customCreateStub).toHaveBeenCalledTimes(1) - expect(customCreateStub).toHaveBeenCalledWith({ name: 'child1', component: Child1 }) + expect(customCreateStub).toHaveBeenCalledWith({ + name: 'child1', + component: Child1 + }) }) }) From 1360c900aa67ba3e3678c1acaf4a0fe6fa738083 Mon Sep 17 00:00:00 2001 From: freakzlike Date: Wed, 26 Jan 2022 16:41:17 +0100 Subject: [PATCH 3/3] docs(plugins): Add docs for createStubs plugin hook --- docs/guide/extending-vtu/plugins.md | 54 ++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/guide/extending-vtu/plugins.md b/docs/guide/extending-vtu/plugins.md index a1ae01f77..1d32a97d9 100644 --- a/docs/guide/extending-vtu/plugins.md +++ b/docs/guide/extending-vtu/plugins.md @@ -10,7 +10,9 @@ Some use cases for plugins: 1. Attaching matchers to the Wrapper instance 1. Attaching functionality to the Wrapper -## Using a Plugin +## Wrapper Plugin + +### Using a Plugin Install plugins by calling the `config.plugins.VueWrapper.install()` method . This has to be done before you call `mount`. @@ -43,12 +45,12 @@ once. Follow the instructions of the plugin you're installing. Check out the [Vue Community Guide](https://vue-community.org/v2/guide/ecosystem/testing.html) or [awesome-vue](https://github.com/vuejs/awesome-vue#test) for a collection of community-contributed plugins and libraries. -## Writing a Plugin +### Writing a Plugin A Vue Test Utils plugin is simply a function that receives the mounted `VueWrapper` or `DOMWrapper` instance and can modify it. -### Basic Plugin +#### Basic Plugin Below is a simple plugin to add a convenient alias to map `wrapper.element` to `wrapper.$el` @@ -75,7 +77,7 @@ const wrapper = mount({ template: `

🔌 Plugin

` }) console.log(wrapper.$el.innerHTML) // 🔌 Plugin ``` -### Data Test ID Plugin +#### Data Test ID Plugin The below plugin adds a method `findByTestId` to the `VueWrapper` instance. This encourages using a selector strategy relying on test-only attributes on your Vue Components. @@ -122,6 +124,50 @@ const DataTestIdPlugin = (wrapper) => { config.plugins.VueWrapper.install(DataTestIdPlugin) ``` +## Stubs Plugin + +The `config.plugins.createStubs` allows to overwrite the default stub creation provided by VTU. + +Some use cases are: +* You want to add more logic into the stubs (for example named slots) +* You want to use different stubs for multiple components (for example stub components from a library) + +### Usage + +```typescript +config.plugins.createStubs = ({ name, component }) => { + return defineComponent({ + render: () => h(`custom-${name}-stub`) + }) +} +``` + +This function will be called everytime VTU generates a stub either from +```typescript +const wrapper = mount(Component, { + global: { + stubs: { + ChildComponent: true + } + } +}) +``` +or +```typescript +const wrapper = shallowMount(Component) +``` + +But will not be called, when you explicit set a stub +```typescript +const wrapper = mount(Component, { + global: { + stubs: { + ChildComponent: { template: '' } + } + } +}) +``` + ## Featuring Your Plugin If you're missing functionality, consider writing a plugin to extend Vue Test