diff --git a/packages/create-instance/create-functional-component.js b/packages/create-instance/create-functional-component.js deleted file mode 100644 index 8e41f2bdb..000000000 --- a/packages/create-instance/create-functional-component.js +++ /dev/null @@ -1,51 +0,0 @@ -// @flow - -import { throwError } from 'shared/util' -import { validateSlots } from './validate-slots' -import { createSlotVNodes } from './create-slot-vnodes' -import createScopedSlots from './create-scoped-slots' - -export default function createFunctionalComponent ( - component: Component, - mountingOptions: Options, - _Vue: Component -): Component { - if (mountingOptions.context && typeof mountingOptions.context !== 'object') { - throwError('mount.context must be an object') - } - if (mountingOptions.slots) { - validateSlots(mountingOptions.slots) - } - - const context = - mountingOptions.context || - component.FunctionalRenderContext || - {} - - const listeners = mountingOptions.listeners - - if (listeners) { - Object.keys(listeners).forEach(key => { - context.on[key] = listeners[key] - }) - } - - context.scopedSlots = createScopedSlots(mountingOptions.scopedSlots, _Vue) - - return { - render (h: Function) { - return h( - component, - context, - (mountingOptions.context && - mountingOptions.context.children && - mountingOptions.context.children.map( - x => (typeof x === 'function' ? x(h) : x) - )) || - createSlotVNodes(this, mountingOptions.slots || {}) - ) - }, - name: component.name, - _isFunctionalContainer: true - } -} diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 6cd7b2247..039986f93 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -4,59 +4,58 @@ import { createSlotVNodes } from './create-slot-vnodes' import addMocks from './add-mocks' import { addEventLogger } from './log-events' import { addStubs } from './add-stubs' -import { throwError } from 'shared/util' -import { VUE_VERSION } from 'shared/consts' -import { - compileTemplate, - compileTemplateForSlots -} from 'shared/compile-template' +import { compileTemplate } from 'shared/compile-template' import extractInstanceOptions from './extract-instance-options' -import createFunctionalComponent from './create-functional-component' -import { componentNeedsCompiling, isPlainObject } from 'shared/validators' -import { validateSlots } from './validate-slots' +import { + componentNeedsCompiling, + isConstructor +} from 'shared/validators' import createScopedSlots from './create-scoped-slots' import { createStubsFromStubsObject } from './create-component-stubs' import { patchCreateElement } from './patch-create-element' -import { isConstructor } from 'shared/validators' -function vueExtendUnsupportedOption (option: string) { - return `options.${option} is not supported for ` + - `components created with Vue.extend in Vue < 2.3. ` + - `You can set ${option} to false to mount the component.` +function createContext (options, scopedSlots) { + const on = { + ...(options.context && options.context.on), + ...options.listeners + } + return { + attrs: { + ...options.attrs, + // pass as attrs so that inheritAttrs works correctly + // propsData should take precedence over attrs + ...options.propsData + }, + ...(options.context || {}), + on, + scopedSlots + } } -// these options aren't supported if Vue is version < 2.3 -// for components using Vue.extend. This is due to a bug -// that means the mixins we use to add properties are not applied -// correctly -const UNSUPPORTED_VERSION_OPTIONS = [ - 'mocks', - 'stubs', - 'localVue' -] +function createChildren (vm, h, { slots, context }) { + const slotVNodes = slots + ? createSlotVNodes(vm, slots) + : undefined + return ( + context && + context.children && + context.children.map(x => (typeof x === 'function' ? x(h) : x)) + ) || slotVNodes +} export default function createInstance ( component: Component, options: Options, _Vue: Component ): Component { - if ( - VUE_VERSION < 2.3 && isConstructor(component) - ) { - UNSUPPORTED_VERSION_OPTIONS.forEach((option) => { - if (options[option]) { - throwError(vueExtendUnsupportedOption(option)) - } - }) - } - - let componentOptions = isConstructor(component) + const componentOptions = isConstructor(component) ? component.options : component // instance options are options that are passed to the // root instance when it's instantiated const instanceOptions = extractInstanceOptions(options) + const stubComponentsObject = createStubsFromStubsObject( componentOptions.components, // $FlowIgnore @@ -69,81 +68,30 @@ export default function createInstance ( addStubs(_Vue, stubComponentsObject) patchCreateElement(_Vue, stubComponentsObject, options.shouldProxy) - if (componentOptions.functional) { - componentOptions = createFunctionalComponent( - componentOptions, - options, - _Vue - ) - } else if (options.context) { - throwError( - `mount.context can only be used when mounting a ` + - `functional component` - ) - } - if (componentNeedsCompiling(componentOptions)) { compileTemplate(componentOptions) } + // used to identify extended component using constructor + componentOptions.$_vueTestUtils_original = component + // make sure all extends are based on this instance componentOptions._base = _Vue const Constructor = _Vue.extend(componentOptions).extend(instanceOptions) - // used to identify extended component using constructor - Constructor.options.$_vueTestUtils_original = component - - if (options.slots) { - compileTemplateForSlots(options.slots) - // validate slots outside of the createSlots function so - // that we can throw an error without it being caught by - // the Vue error handler - // $FlowIgnore - validateSlots(options.slots) - } - - // Objects are not resolved in extended components in Vue < 2.5 - // https://github.com/vuejs/vue/issues/6436 - if ( - options.provide && - typeof options.provide === 'object' && - VUE_VERSION < 2.5 - ) { - const obj = { ...options.provide } - options.provide = () => obj - } - const scopedSlots = createScopedSlots(options.scopedSlots, _Vue) - if (options.parentComponent && !isPlainObject(options.parentComponent)) { - throwError( - `options.parentComponent should be a valid Vue component options object` - ) - } - const parentComponentOptions = options.parentComponent || {} + parentComponentOptions.provide = options.provide parentComponentOptions.$_doNotStubChildren = true - + parentComponentOptions._isFunctionalContainer = componentOptions.functional parentComponentOptions.render = function (h) { - const slots = options.slots - ? createSlotVNodes(this, options.slots) - : undefined return h( Constructor, - { - ref: 'vm', - on: options.listeners, - attrs: { - ...options.attrs, - // pass as attrs so that inheritAttrs works correctly - // propsData should take precedence over attrs - ...options.propsData - }, - scopedSlots - }, - slots + createContext(options, scopedSlots), + createChildren(this, h, options) ) } const Parent = _Vue.extend(parentComponentOptions) diff --git a/packages/server-test-utils/src/renderToString.js b/packages/server-test-utils/src/renderToString.js index cec6b7707..a9ce19a53 100644 --- a/packages/server-test-utils/src/renderToString.js +++ b/packages/server-test-utils/src/renderToString.js @@ -7,6 +7,7 @@ import { createRenderer } from 'vue-server-renderer' import { mergeOptions } from 'shared/merge-options' import config from './config' import testUtils from '@vue/test-utils' +import { validateOptions } from 'shared/validate-options' Vue.config.productionTip = false Vue.config.devtools = false @@ -27,9 +28,12 @@ export default function renderToString ( throwError(`you cannot use attachToDocument with ` + `renderToString`) } + const mergedOptions = mergeOptions(options, config) + validateOptions(mergedOptions, component) + const vm = createInstance( component, - mergeOptions(options, config), + mergedOptions, testUtils.createLocalVue(options.localVue) ) let renderedString = '' diff --git a/packages/shared/merge-options.js b/packages/shared/merge-options.js index 7c4b71b92..21daf94d3 100644 --- a/packages/shared/merge-options.js +++ b/packages/shared/merge-options.js @@ -1,5 +1,5 @@ // @flow -import { normalizeStubs } from './normalize' +import { normalizeStubs, normalizeProvide } from './normalize' function getOption (option, config?: Object): any { if (option === false) { @@ -26,11 +26,11 @@ export function mergeOptions (options: Options, config: Config): Options { const provide = ((getOption(options.provide, config.provide)): Object) return { ...options, + provide: normalizeProvide(provide), logModifiedComponents: config.logModifiedComponents, stubs: getOption(normalizeStubs(options.stubs), config.stubs), mocks, methods, - provide, sync: !!(options.sync || options.sync === undefined) } } diff --git a/packages/shared/normalize.js b/packages/shared/normalize.js index 1791cff97..e86017da7 100644 --- a/packages/shared/normalize.js +++ b/packages/shared/normalize.js @@ -1,5 +1,6 @@ import { isPlainObject } from './validators' import { throwError } from './util' +import { VUE_VERSION } from './consts' export function normalizeStubs (stubs = {}) { if (stubs === false) { @@ -19,3 +20,16 @@ export function normalizeStubs (stubs = {}) { } throwError('options.stubs must be an object or an Array') } + +export function normalizeProvide (provide) { + // Objects are not resolved in extended components in Vue < 2.5 + // https://github.com/vuejs/vue/issues/6436 + if ( + typeof provide === 'object' && + VUE_VERSION < 2.5 + ) { + const obj = { ...provide } + return () => obj + } + return provide +} diff --git a/packages/shared/validate-options.js b/packages/shared/validate-options.js new file mode 100644 index 000000000..b52faad57 --- /dev/null +++ b/packages/shared/validate-options.js @@ -0,0 +1,61 @@ +import { + isPlainObject, + isFunctionalComponent, + isConstructor +} from './validators' +import { VUE_VERSION } from './consts' +import { compileTemplateForSlots } from './compile-template' +import { throwError } from './util' +import { validateSlots } from './validate-slots' + +function vueExtendUnsupportedOption (option) { + return `options.${option} is not supported for ` + + `components created with Vue.extend in Vue < 2.3. ` + + `You can set ${option} to false to mount the component.` +} +// these options aren't supported if Vue is version < 2.3 +// for components using Vue.extend. This is due to a bug +// that means the mixins we use to add properties are not applied +// correctly +const UNSUPPORTED_VERSION_OPTIONS = [ + 'mocks', + 'stubs', + 'localVue' +] + +export function validateOptions (options, component) { + if (options.parentComponent && !isPlainObject(options.parentComponent)) { + throwError( + `options.parentComponent should be a valid Vue component options object` + ) + } + + if (!isFunctionalComponent(component) && options.context) { + throwError( + `mount.context can only be used when mounting a functional component` + ) + } + + if (options.context && !isPlainObject(options.context)) { + throwError('mount.context must be an object') + } + + if ( + VUE_VERSION < 2.3 && isConstructor(component) + ) { + UNSUPPORTED_VERSION_OPTIONS.forEach((option) => { + if (options[option]) { + throwError(vueExtendUnsupportedOption(option)) + } + }) + } + + if (options.slots) { + compileTemplateForSlots(options.slots) + // validate slots outside of the createSlots function so + // that we can throw an error without it being caught by + // the Vue error handler + // $FlowIgnore + validateSlots(options.slots) + } +} diff --git a/packages/create-instance/validate-slots.js b/packages/shared/validate-slots.js similarity index 94% rename from packages/create-instance/validate-slots.js rename to packages/shared/validate-slots.js index 34b5432d2..f212767a2 100644 --- a/packages/create-instance/validate-slots.js +++ b/packages/shared/validate-slots.js @@ -2,7 +2,7 @@ import { throwError } from 'shared/util' import { compileToFunctions } from 'vue-template-compiler' -import { isVueComponent } from '../shared/validators' +import { isVueComponent } from './validators' function isValidSlot (slot: any): boolean { return ( diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 3dfb6a7dc..1840f241d 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -88,6 +88,16 @@ export function isComponentOptions (c: any) { return typeof c === 'object' && (c.template || c.render) } +export function isFunctionalComponent (c: any) { + if (!isVueComponent(c)) { + return false + } + if (isConstructor(c)) { + return c.options.functional + } + return c.functional +} + export function templateContainsComponent ( template: string, name: string diff --git a/packages/test-utils/src/mount.js b/packages/test-utils/src/mount.js index e89b9cb0d..01c2ec7da 100644 --- a/packages/test-utils/src/mount.js +++ b/packages/test-utils/src/mount.js @@ -15,6 +15,7 @@ import config from './config' import warnIfNoWindow from './warn-if-no-window' import createWrapper from './create-wrapper' import createLocalVue from './create-local-vue' +import { validateOptions } from 'shared/validate-options' Vue.config.productionTip = false Vue.config.devtools = false @@ -31,6 +32,8 @@ export default function mount ( const mergedOptions = mergeOptions(options, config) + validateOptions(mergedOptions, component) + const parentVm = createInstance( component, mergedOptions, @@ -38,7 +41,7 @@ export default function mount ( ) const el = options.attachToDocument ? createElement() : undefined - const vm = parentVm.$mount(el).$refs.vm + const vm = parentVm.$mount(el) component._Ctor = {} @@ -49,9 +52,9 @@ export default function mount ( sync: mergedOptions.sync } - const root = vm.$options._isFunctionalContainer + const root = parentVm.$options._isFunctionalContainer ? vm._vnode - : vm + : vm.$children[0] return createWrapper(root, wrapperOptions) } diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index 10f15c8d2..57e74a671 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -27,23 +27,18 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.vm).to.be.an('object') }) - it('returns new VueWrapper when root is functional component', () => { - const FunctionalComponent = { + it('handles root functional component', () => { + const TestComponent = { functional: true, render (h) { - return h('div', {}, [ - h('p', { - class: { - foo: true - } - }), + return h('div', [ + h('p'), h('p') ]) - }, - name: 'common' + } } - const wrapper = mount(FunctionalComponent) + const wrapper = mount(TestComponent) expect(wrapper.findAll('p').length).to.equal(2) }) @@ -258,7 +253,9 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { } ) if (injectSupported) { - expect(typeof wrapper.vm.$options.provide).to.equal('object') + expect(typeof wrapper.vm.$options.provide).to.equal( + vueVersion < 2.5 ? 'function' : 'object' + ) } expect(wrapper.vm.$options.attachToDocument).to.equal(undefined)