From 31a3d0bf110f6d52e803a7b57627f87fa1a8d475 Mon Sep 17 00:00:00 2001 From: Edd Yerburgh Date: Sun, 17 Jun 2018 15:37:43 +0100 Subject: [PATCH] fix: stub child components (#723) --- packages/create-instance/add-slots.js | 14 +++--- .../create-functional-component.js | 35 +------------- packages/create-instance/create-instance.js | 22 +++++---- .../server-test-utils/src/renderToString.js | 8 +++- packages/test-utils/src/create-local-vue.js | 8 ++-- packages/test-utils/src/mount.js | 3 +- test/specs/mount.spec.js | 11 ++--- test/specs/mounting-options/localVue.spec.js | 8 ++++ test/specs/mounting-options/slots.spec.js | 19 ++++++++ test/specs/mounting-options/stubs.spec.js | 46 +++++++++++++++++++ test/specs/shallow-mount.spec.js | 15 +++--- 11 files changed, 118 insertions(+), 71 deletions(-) diff --git a/packages/create-instance/add-slots.js b/packages/create-instance/add-slots.js index 746cae35b..6e5381374 100644 --- a/packages/create-instance/add-slots.js +++ b/packages/create-instance/add-slots.js @@ -6,7 +6,7 @@ function startsWithTag (str) { return str && str.trim()[0] === '<' } -function createVNodesForSlot ( +export function createVNodesForSlot ( h: Function, slotValue: SlotValue, name: string @@ -30,12 +30,14 @@ export function createSlotVNodes ( return Object.keys(slots).reduce((acc, key) => { const content = slots[key] if (Array.isArray(content)) { - const nodes = content.reduce((accInner, slotDef) => { - return accInner.concat(createVNodesForSlot(h, slotDef, key)) - }, []) + const nodes = content.map(slotDef => createVNodesForSlot(h, slotDef, key)) return acc.concat(nodes) - } else { - return acc.concat(createVNodesForSlot(h, content, key)) } + + return acc.concat(createVNodesForSlot(h, content, key)) }, []) } + +export function createFunctionalSlotVNodes (h, slots = {}) { + return createSlotVNodes(h, slots) +} diff --git a/packages/create-instance/create-functional-component.js b/packages/create-instance/create-functional-component.js index ab83710c4..f367e6f57 100644 --- a/packages/create-instance/create-functional-component.js +++ b/packages/create-instance/create-functional-component.js @@ -1,39 +1,8 @@ // @flow -import { compileToFunctions } from 'vue-template-compiler' import { throwError } from 'shared/util' import { validateSlots } from './validate-slots' - -function createFunctionalSlots (slots = {}, h) { - if (Array.isArray(slots.default)) { - return slots.default.map(h) - } - - if (typeof slots.default === 'string') { - return [h(compileToFunctions(slots.default))] - } - const children = [] - Object.keys(slots).forEach(slotType => { - if (Array.isArray(slots[slotType])) { - slots[slotType].forEach(slot => { - const component = - typeof slot === 'string' ? compileToFunctions(slot) : slot - const newSlot = h(component) - newSlot.data.slot = slotType - children.push(newSlot) - }) - } else { - const component = - typeof slots[slotType] === 'string' - ? compileToFunctions(slots[slotType]) - : slots[slotType] - const slot = h(component) - slot.data.slot = slotType - children.push(slot) - } - }) - return children -} +import { createFunctionalSlotVNodes } from './add-slots' export default function createFunctionalComponent ( component: Component, @@ -56,7 +25,7 @@ export default function createFunctionalComponent ( mountingOptions.context.children.map( x => (typeof x === 'function' ? x(h) : x) )) || - createFunctionalSlots(mountingOptions.slots, h) + createFunctionalSlotVNodes(h, mountingOptions.slots) ) }, name: component.name, diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 8b693a7e0..b96872853 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -59,7 +59,14 @@ export default function createInstance ( ...stubComponents } } - + _Vue.mixin({ + created () { + Object.assign( + this.$options.components, + stubComponents + ) + } + }) Object.keys(component.components || {}).forEach(c => { if ( component.components[c].extendOptions && @@ -79,14 +86,13 @@ export default function createInstance ( } }) - Object.keys(stubComponents).forEach(c => { - _Vue.component(c, stubComponents[c]) - }) + if (component.options) { + component.options._base = _Vue + } - const Constructor = - vueVersion < 2.3 && typeof component === 'function' - ? component.extend(instanceOptions) - : _Vue.extend(component).extend(instanceOptions) + const Constructor = vueVersion < 2.3 && typeof component === 'function' + ? component.extend(instanceOptions) + : _Vue.extend(component).extend(instanceOptions) Object.keys(instanceOptions.components || {}).forEach(key => { Constructor.component(key, instanceOptions.components[key]) diff --git a/packages/server-test-utils/src/renderToString.js b/packages/server-test-utils/src/renderToString.js index e4c7c0d4b..cfdd8ab7e 100644 --- a/packages/server-test-utils/src/renderToString.js +++ b/packages/server-test-utils/src/renderToString.js @@ -28,8 +28,12 @@ export default function renderToString ( if (options.attachToDocument) { throwError(`you cannot use attachToDocument with ` + `renderToString`) } - const vueClass = options.localVue || testUtils.createLocalVue() - const vm = createInstance(component, mergeOptions(options, config), vueClass) + const vueConstructor = testUtils.createLocalVue(options.localVue) + const vm = createInstance( + component, + mergeOptions(options, config), + vueConstructor + ) let renderedString = '' // $FlowIgnore diff --git a/packages/test-utils/src/create-local-vue.js b/packages/test-utils/src/create-local-vue.js index 80b655a90..2a0979c71 100644 --- a/packages/test-utils/src/create-local-vue.js +++ b/packages/test-utils/src/create-local-vue.js @@ -4,13 +4,13 @@ import Vue from 'vue' import cloneDeep from 'lodash/cloneDeep' import errorHandler from './error-handler' -function createLocalVue (): Component { - const instance = Vue.extend() +function createLocalVue (_Vue: Component = Vue): Component { + const instance = _Vue.extend() // clone global APIs - Object.keys(Vue).forEach(key => { + Object.keys(_Vue).forEach(key => { if (!instance.hasOwnProperty(key)) { - const original = Vue[key] + const original = _Vue[key] instance[key] = typeof original === 'object' ? cloneDeep(original) : original } diff --git a/packages/test-utils/src/mount.js b/packages/test-utils/src/mount.js index 5372c0f0f..1c1b98f60 100644 --- a/packages/test-utils/src/mount.js +++ b/packages/test-utils/src/mount.js @@ -28,8 +28,7 @@ export default function mount ( // Remove cached constructor delete component._Ctor - - const vueConstructor = options.localVue || createLocalVue() + const vueConstructor = createLocalVue(options.localVue) const elm = options.attachToDocument ? createElement() : undefined diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index a13100388..906dfc26b 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -58,20 +58,15 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { } }) - it('returns new VueWrapper with mounted Vue instance initialized with Vue.extend with props, if passed as propsData', () => { - const prop1 = { test: 'TEST' } + it('handles propsData for extended components', () => { + const prop1 = 'test' const TestComponent = Vue.extend(ComponentWithProps) const wrapper = mount(TestComponent, { propsData: { prop1 } }) - expect(wrapper.vm).to.be.an('object') - if (wrapper.vm.$props) { - expect(wrapper.vm.$props.prop1).to.equal(prop1) - } else { - expect(wrapper.vm.$options.propsData.prop1).to.equal(prop1) - } + expect(wrapper.text()).to.contain(prop1) }) it('handles uncompiled extended Vue component', () => { diff --git a/test/specs/mounting-options/localVue.spec.js b/test/specs/mounting-options/localVue.spec.js index 9fbf2bb26..2f51b8e20 100644 --- a/test/specs/mounting-options/localVue.spec.js +++ b/test/specs/mounting-options/localVue.spec.js @@ -69,4 +69,12 @@ describeWithMountingMethods('options.localVue', mountingMethod => { expect(HTML).to.contain('2') } }) + + it('does not add created mixin to localVue', () => { + const localVue = createLocalVue() + mountingMethod({ render: () => {} }, { + localVue + }) + expect(localVue.options.created).to.equal(undefined) + }) }) diff --git a/test/specs/mounting-options/slots.spec.js b/test/specs/mounting-options/slots.spec.js index 8d56654a3..d0e769312 100644 --- a/test/specs/mounting-options/slots.spec.js +++ b/test/specs/mounting-options/slots.spec.js @@ -235,6 +235,25 @@ describeWithMountingMethods('options.slots', mountingMethod => { } }) + it('mounts functional component with text slot', () => { + const TestComponent = { + name: 'component-with-slots', + functional: true, + render: (h, ctx) => h('div', ctx.data, [ctx.slots().default, ctx.slots().header]) + } + const wrapper = mountingMethod(TestComponent, { + slots: { + default: 'hello,', + header: 'world' + } + }) + if (mountingMethod.name === 'renderToString') { + expect(wrapper).contains('hello,world') + } else { + expect(wrapper.text()).to.contain('hello,world') + } + }) + it('mounts component with named slot if passed component in slot object', () => { const wrapper = mountingMethod(ComponentWithSlots, { slots: { diff --git a/test/specs/mounting-options/stubs.spec.js b/test/specs/mounting-options/stubs.spec.js index fe89333f1..9fde1810a 100644 --- a/test/specs/mounting-options/stubs.spec.js +++ b/test/specs/mounting-options/stubs.spec.js @@ -128,6 +128,52 @@ describeWithMountingMethods('options.stub', mountingMethod => { expect(HTML).to.contain('') }) + itDoNotRunIf( + mountingMethod.name === 'shallowMount', + 'stubs nested components', () => { + const GrandchildComponent = { + template: '' + } + const ChildComponent = { + template: '', + components: { GrandchildComponent } + } + const TestComponent = { + template: '', + components: { ChildComponent } + } + const wrapper = mountingMethod(TestComponent, { + stubs: ['grandchild-component'] + }) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).not.to.contain('') + }) + + itDoNotRunIf( + mountingMethod.name === 'shallowMount', + 'stubs nested components on extended components', () => { + const GrandchildComponent = { + template: '' + } + const ChildComponent = { + template: '', + components: { GrandchildComponent } + } + const TestComponent = { + template: '
', + components: { ChildComponent } + } + const wrapper = mountingMethod(Vue.extend(TestComponent), { + stubs: ['grandchild-component'] + }) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).not.to.contain('') + }) + it('stubs components with dummy when passed a boolean', () => { const ComponentWithGlobalComponent = { render: h => h('div', [h('registered-component')]) diff --git a/test/specs/shallow-mount.spec.js b/test/specs/shallow-mount.spec.js index 41f75b41b..6522a85e6 100644 --- a/test/specs/shallow-mount.spec.js +++ b/test/specs/shallow-mount.spec.js @@ -171,14 +171,13 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'shallowMount', () => { expect(wrapper.find('p').exists()).to.equal(false) }) - it('stubs Vue class component children', () => { - if (vueVersion < 2.3) { - return - } - const wrapper = shallowMount(ComponentAsAClassWithChild) - expect(wrapper.find(Component).exists()).to.equal(true) - expect(wrapper.findAll('div').length).to.equal(1) - }) + itDoNotRunIf( + vueVersion < 2.3, + 'stubs Vue class component children', () => { + const wrapper = shallowMount(ComponentAsAClassWithChild) + expect(wrapper.find(Component).exists()).to.equal(true) + expect(wrapper.findAll('div').length).to.equal(1) + }) it('works correctly with find, contains, findAll, and is on unnamed components', () => { const TestComponent = {