From 0c4a8110b4aa72653cbfce9568a07a3a870a0825 Mon Sep 17 00:00:00 2001 From: Jamie S Date: Thu, 26 Mar 2020 06:48:30 -0500 Subject: [PATCH] fix #1377 string stubs dropping props (#1473) * test(issue #1377): add test for props on stubbed child component * feat(create-instance): convert string stubs to template close #1377 This also fixes the component name being dropped for template stubs * docs(api): update stubs documentation * test(stubs): limit stub props to 2.2 and above * docs(api): updated docs wording --- docs/api/mount.md | 8 ++- docs/api/options.md | 8 ++- .../create-instance/create-component-stubs.js | 43 ++++++++----- packages/shared/compile-template.js | 24 +++---- ...nt-with-nested-childern-and-attributes.vue | 22 +++++++ test/specs/mounting-options/stubs.spec.js | 63 +++++++++++++++++-- 6 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 test/resources/components/component-with-nested-childern-and-attributes.vue diff --git a/docs/api/mount.md b/docs/api/mount.md index 6744186e5..240c15b97 100644 --- a/docs/api/mount.md +++ b/docs/api/mount.md @@ -116,9 +116,9 @@ describe('Foo', () => { it('renders a div', () => { const wrapper = mount(Foo, { stubs: { - Bar: '
', BarFoo: true, - FooBar: Faz + FooBar: Faz, + Bar: { template: '
' } } }) expect(wrapper.contains('.stubbed')).toBe(true) @@ -127,4 +127,8 @@ describe('Foo', () => { }) ``` +**Deprecation Notice:** + +When stubbing components, supplying a string (`ComponentToStub: '
`) is no longer supported. + - **See also:** [Wrapper](wrapper/) diff --git a/docs/api/options.md b/docs/api/options.md index 1704ed31b..f3b13fce5 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -208,9 +208,13 @@ const wrapper = mount(WrapperComp).find(ComponentUnderTest) ## stubs -- type: `{ [name: string]: Component | boolean } | Array` +- type: `{ [name: string]: Component | string | boolean } | Array` -Stubs child components. Can be an Array of component names to stub, or an object. If `stubs` is an Array, every stub is `<${component name}-stub>`. +Stubs child components can be an Array of component names to stub, or an object. If `stubs` is an Array, every stub is `<${component name}-stub>`. + +**Deprecation Notice:** + +When stubbing components, supplying a string (`ComponentToStub: '
`) is no longer supported. Example: diff --git a/packages/create-instance/create-component-stubs.js b/packages/create-instance/create-component-stubs.js index efca560d6..a5de8e315 100644 --- a/packages/create-instance/create-component-stubs.js +++ b/packages/create-instance/create-component-stubs.js @@ -6,7 +6,8 @@ import { camelize, capitalize, hyphenate, - keys + keys, + warn } from '../shared/util' import { componentNeedsCompiling, @@ -15,7 +16,7 @@ import { isDynamicComponent, isConstructor } from '../shared/validators' -import { compileTemplate, compileFromString } from '../shared/compile-template' +import { compileTemplate } from '../shared/compile-template' function isVueComponentStub(comp): boolean { return (comp && comp.template) || isVueComponent(comp) @@ -156,24 +157,31 @@ export function createStubFromComponent( } } -function createStubFromString( - templateString: string, - originalComponent: Component = {}, - name: string, - _Vue: Component -): Component { +// DEPRECATED: converts string stub to template stub. +function createStubFromString(templateString: string, name: string): Component { + warn('String stubs are deprecated and will be removed in future versions') + if (templateContainsComponent(templateString, name)) { throwError('options.stub cannot contain a circular reference') } - const componentOptions = resolveOptions(originalComponent, _Vue) return { - ...getCoreProperties(componentOptions), - $_doNotStubChildren: true, - ...compileFromString(templateString) + template: templateString, + $_doNotStubChildren: true } } +function setStubComponentName( + stub: Object, + originalComponent: Component = {}, + _Vue: Component +) { + if (stub.name) return + + const componentOptions = resolveOptions(originalComponent, _Vue) + stub.name = getCoreProperties(componentOptions).name +} + function validateStub(stub) { if (!isValidStub(stub)) { throwError(`options.stub values must be passed a string or ` + `component`) @@ -186,7 +194,7 @@ export function createStubsFromStubsObject( _Vue: Component ): Components { return Object.keys(stubs || {}).reduce((acc, stubName) => { - const stub = stubs[stubName] + let stub = stubs[stubName] validateStub(stub) @@ -194,18 +202,19 @@ export function createStubsFromStubsObject( return acc } + const component = resolveComponent(originalComponents, stubName) + if (stub === true) { - const component = resolveComponent(originalComponents, stubName) acc[stubName] = createStubFromComponent(component, stubName, _Vue) return acc } if (typeof stub === 'string') { - const component = resolveComponent(originalComponents, stubName) - acc[stubName] = createStubFromString(stub, component, stubName, _Vue) - return acc + stub = createStubFromString(stub, stubName) + stubs[stubName] } + setStubComponentName(stub, component, _Vue) if (componentNeedsCompiling(stub)) { compileTemplate(stub) } diff --git a/packages/shared/compile-template.js b/packages/shared/compile-template.js index 7999894bc..49bbdeb91 100644 --- a/packages/shared/compile-template.js +++ b/packages/shared/compile-template.js @@ -4,19 +4,16 @@ import { compileToFunctions } from 'vue-template-compiler' import { componentNeedsCompiling } from './validators' import { throwError } from './util' -export function compileFromString(str: string) { - if (!compileToFunctions) { - throwError( - `vueTemplateCompiler is undefined, you must pass ` + - `precompiled components if vue-template-compiler is ` + - `undefined` - ) - } - return compileToFunctions(str) -} - export function compileTemplate(component: Component): void { if (component.template) { + if (!compileToFunctions) { + throwError( + `vueTemplateCompiler is undefined, you must pass ` + + `precompiled components if vue-template-compiler is ` + + `undefined` + ) + } + if (component.template.charAt('#') === '#') { var el = document.querySelector(component.template) if (!el) { @@ -27,7 +24,10 @@ export function compileTemplate(component: Component): void { component.template = el.innerHTML } - Object.assign(component, compileToFunctions(component.template)) + Object.assign(component, { + ...compileToFunctions(component.template), + name: component.name + }) } if (component.components) { diff --git a/test/resources/components/component-with-nested-childern-and-attributes.vue b/test/resources/components/component-with-nested-childern-and-attributes.vue new file mode 100644 index 000000000..6f04db2f9 --- /dev/null +++ b/test/resources/components/component-with-nested-childern-and-attributes.vue @@ -0,0 +1,22 @@ + + + diff --git a/test/specs/mounting-options/stubs.spec.js b/test/specs/mounting-options/stubs.spec.js index 905c369a7..8c8715462 100644 --- a/test/specs/mounting-options/stubs.spec.js +++ b/test/specs/mounting-options/stubs.spec.js @@ -2,6 +2,7 @@ import ComponentWithChild from '~resources/components/component-with-child.vue' import ComponentWithNestedChildren from '~resources/components/component-with-nested-children.vue' import Component from '~resources/components/component.vue' import ComponentAsAClass from '~resources/components/component-as-a-class.vue' +import ComponentWithNestedChildrenAndAttributes from '~resources/components/component-with-nested-childern-and-attributes.vue' import { createLocalVue, config } from '@vue/test-utils' import { config as serverConfig } from '@vue/server-test-utils' import Vue from 'vue' @@ -18,6 +19,7 @@ describeWithShallowAndMount('options.stub', mountingMethod => { serverConfigSave = serverConfig.stubs config.stubs = {} serverConfig.stubs = {} + sandbox.stub(console, 'error').callThrough() }) afterEach(() => { @@ -32,21 +34,24 @@ describeWithShallowAndMount('options.stub', mountingMethod => { const ComponentWithoutRender = { template: '
' } const ExtendedComponent = { extends: ComponentWithRender } const SubclassedComponent = Vue.extend({ template: '
' }) + const StringComponent = '
' mountingMethod(ComponentWithChild, { stubs: { ChildComponent: ComponentWithRender, ChildComponent2: ComponentAsAClass, ChildComponent3: ComponentWithoutRender, ChildComponent4: ExtendedComponent, - ChildComponent5: SubclassedComponent + ChildComponent5: SubclassedComponent, + ChildComponent6: StringComponent } }) }) it('replaces component with template string ', () => { + const Stub = { template: '
' } const wrapper = mountingMethod(ComponentWithChild, { stubs: { - ChildComponent: '
' + ChildComponent: Stub } }) expect(wrapper.findAll('.stub').length).to.equal(1) @@ -321,7 +326,7 @@ describeWithShallowAndMount('options.stub', mountingMethod => { const wrapper = mountingMethod(TestComponent, { stubs: { - 'span-component': '

' + 'span-component': { template: '

' } }, localVue }) @@ -342,7 +347,7 @@ describeWithShallowAndMount('options.stub', mountingMethod => { const wrapper = mountingMethod(TestComponent, { stubs: { - 'time-component': '' + 'time-component': { template: '' } }, localVue }) @@ -414,7 +419,7 @@ describeWithShallowAndMount('options.stub', mountingMethod => { expect(wrapper.html()).contains('No render function') }) - it('throws an error when passed a circular reference', () => { + it('throws an error when passed a circular reference for string stubs', () => { const names = ['child-component', 'ChildComponent', 'childComponent'] const validValues = [ '', @@ -590,4 +595,52 @@ describeWithShallowAndMount('options.stub', mountingMethod => { expect(result.props().propA).to.equal('A') delete Vue.options.components['child-component'] }) + + itRunIf( + vueVersion >= 2.2, + 'renders props in the element as attributes', + () => { + const ComponentStub = { template: '

' } + const StringStub = '
' + const BooleanStub = true + + const wrapper = mountingMethod(ComponentWithNestedChildrenAndAttributes, { + stubs: { + SlotComponent: ComponentStub, + ChildComponent: StringStub, + OriginalComponent: BooleanStub + } + }) + + expect(wrapper.find('#component-stub').attributes()).to.eql({ + id: 'component-stub', + prop1: 'foobar', + prop2: 'fizzbuzz' + }) + expect(wrapper.find('#string-stub').attributes()).to.eql({ + id: 'string-stub', + prop1: 'foobar', + prop2: 'fizzbuzz' + }) + expect(wrapper.find('originalcomponent-stub').attributes()).to.eql({ + prop1: 'foobar', + prop2: 'fizzbuzz' + }) + } + ) + + it('warns when passing a string', () => { + const StringComponent = '
' + mountingMethod(ComponentWithChild, { + stubs: { + ChildComponent6: StringComponent + } + }) + + expect(console.error).calledWith( + sandbox.match( + '[vue-test-utils]: String stubs are deprecated and will be removed in future versions' + ) + ) + }) })