diff --git a/packages/create-instance/create-component-stubs.js b/packages/create-instance/create-component-stubs.js index 33470ae76..381e8ce63 100644 --- a/packages/create-instance/create-component-stubs.js +++ b/packages/create-instance/create-component-stubs.js @@ -10,7 +10,9 @@ import { import { componentNeedsCompiling, templateContainsComponent, - isVueComponent + isVueComponent, + isDynamicComponent, + isConstructor } from '../shared/validators' import { compileTemplate, @@ -65,11 +67,11 @@ function createClassString (staticClass, dynamicClass) { } function resolveOptions (component, _Vue) { - if (typeof component === 'function' && !component.cid) { + if (isDynamicComponent(component)) { return {} } - return typeof component === 'function' + return isConstructor(component) ? component.options : _Vue.extend(component).options } @@ -112,19 +114,16 @@ export function createStubFromComponent ( } } -export function createStubFromString ( +function createStubFromString ( templateString: string, originalComponent: Component = {}, - name: string + name: string, + _Vue: Component ): Component { if (templateContainsComponent(templateString, name)) { throwError('options.stub cannot contain a circular reference') } - - const componentOptions = - typeof originalComponent === 'function' && originalComponent.cid - ? originalComponent.extendOptions - : originalComponent + const componentOptions = resolveOptions(originalComponent, _Vue) return { ...getCoreProperties(componentOptions), @@ -167,7 +166,8 @@ export function createStubsFromStubsObject ( acc[stubName] = createStubFromString( stub, component, - stubName + stubName, + _Vue ) return acc } diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index 6e1cdd458..6cd7b2247 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -17,6 +17,7 @@ import { validateSlots } from './validate-slots' 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 ` + @@ -39,13 +40,8 @@ export default function createInstance ( options: Options, _Vue: Component ): Component { - // make sure all extends are based on this instance - _Vue.options._base = _Vue - if ( - VUE_VERSION < 2.3 && - typeof component === 'function' && - component.options + VUE_VERSION < 2.3 && isConstructor(component) ) { UNSUPPORTED_VERSION_OPTIONS.forEach((option) => { if (options[option]) { @@ -54,11 +50,15 @@ export default function createInstance ( }) } + let 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( - component.components, + componentOptions.components, // $FlowIgnore options.stubs, _Vue @@ -69,11 +69,12 @@ export default function createInstance ( addStubs(_Vue, stubComponentsObject) patchCreateElement(_Vue, stubComponentsObject, options.shouldProxy) - if ( - (component.options && component.options.functional) || - component.functional - ) { - component = createFunctionalComponent(component, options, _Vue) + if (componentOptions.functional) { + componentOptions = createFunctionalComponent( + componentOptions, + options, + _Vue + ) } else if (options.context) { throwError( `mount.context can only be used when mounting a ` + @@ -81,19 +82,14 @@ export default function createInstance ( ) } - if (componentNeedsCompiling(component)) { - compileTemplate(component) + if (componentNeedsCompiling(componentOptions)) { + compileTemplate(componentOptions) } - if (component.options) { - component.options._base = _Vue - } + // make sure all extends are based on this instance + componentOptions._base = _Vue - // extend component from _Vue to add properties and mixins - // extend does not work correctly for sub class components in Vue < 2.2 - const Constructor = typeof component === 'function' - ? _Vue.extend(component.options).extend(instanceOptions) - : _Vue.extend(component).extend(instanceOptions) + const Constructor = _Vue.extend(componentOptions).extend(instanceOptions) // used to identify extended component using constructor Constructor.options.$_vueTestUtils_original = component @@ -122,8 +118,7 @@ export default function createInstance ( if (options.parentComponent && !isPlainObject(options.parentComponent)) { throwError( - `options.parentComponent should be a valid Vue component ` + - `options object` + `options.parentComponent should be a valid Vue component options object` ) } diff --git a/packages/create-instance/patch-create-element.js b/packages/create-instance/patch-create-element.js index 43e0a8dd6..741866cf3 100644 --- a/packages/create-instance/patch-create-element.js +++ b/packages/create-instance/patch-create-element.js @@ -1,6 +1,11 @@ import { createStubFromComponent } from './create-component-stubs' import { resolveComponent } from 'shared/util' -import { isReservedTag } from 'shared/validators' +import { + isReservedTag, + isConstructor, + isDynamicComponent, + isComponentOptions +} from 'shared/validators' import { BEFORE_RENDER_LIFECYCLE_HOOK, CREATE_ELEMENT_ALIAS @@ -8,11 +13,10 @@ import { const isWhitelisted = (el, whitelist) => resolveComponent(el, whitelist) const isAlreadyStubbed = (el, stubs) => stubs.has(el) -const isDynamicComponent = cmp => typeof cmp === 'function' && !cmp.cid function shouldExtend (component, _Vue) { return ( - (typeof component === 'function' && !isDynamicComponent(component)) || + isConstructor(component) || (component && component.extends) ) } @@ -21,6 +25,7 @@ function extend (component, _Vue) { const componentOptions = component.options ? component.options : component const stub = _Vue.extend(componentOptions) stub.options.$_vueTestUtils_original = component + stub.options._base = _Vue return stub } @@ -42,14 +47,6 @@ function shouldNotBeStubbed (el, whitelist, modifiedComponents) { ) } -function isConstructor (el) { - return typeof el === 'function' -} - -function isComponentOptions (el) { - return typeof el === 'object' && (el.template || el.render) -} - export function patchCreateElement (_Vue, stubs, stubAllComponents) { // This mixin patches vm.$createElement so that we can stub all components // before they are rendered in shallow mode. We also need to ensure that @@ -87,7 +84,7 @@ export function patchCreateElement (_Vue, stubs, stubAllComponents) { } if (typeof el === 'string') { - let original = resolveComponent(el, originalComponents) + const original = resolveComponent(el, originalComponents) if (!original) { return originalCreateElement(el, ...args) @@ -97,12 +94,6 @@ export function patchCreateElement (_Vue, stubs, stubAllComponents) { return originalCreateElement(el, ...args) } - if ( - original.options && - original.options.$_vueTestUtils_original - ) { - original = original.options.$_vueTestUtils_original - } const stub = createStubIfNeeded(stubAllComponents, original, _Vue, el) if (stub) { diff --git a/packages/shared/validators.js b/packages/shared/validators.js index c24453a2d..3dfb6a7dc 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -28,24 +28,24 @@ export function isDomSelector (selector: any): boolean { } } -export function isVueComponent (component: any): boolean { - if (typeof component === 'function' && component.options) { +export function isVueComponent (c: any): boolean { + if (isConstructor(c)) { return true } - if (component === null || typeof component !== 'object') { + if (c === null || typeof c !== 'object') { return false } - if (component.extends || component._Ctor) { + if (c.extends || c._Ctor) { return true } - if (typeof component.template === 'string') { + if (typeof c.template === 'string') { return true } - return typeof component.render === 'function' + return typeof c.render === 'function' } export function componentNeedsCompiling (component: Component): boolean { @@ -76,6 +76,18 @@ export function isNameSelector (nameOptionsObject: any): boolean { return !!nameOptionsObject.name } +export function isConstructor (c: any) { + return typeof c === 'function' && c.cid +} + +export function isDynamicComponent (c: any) { + return typeof c === 'function' && !c.cid +} + +export function isComponentOptions (c: any) { + return typeof c === 'object' && (c.template || c.render) +} + export function templateContainsComponent ( template: string, name: string @@ -86,8 +98,8 @@ export function templateContainsComponent ( }) } -export function isPlainObject (obj: any): boolean { - return Object.prototype.toString.call(obj) === '[object Object]' +export function isPlainObject (c: any): boolean { + return Object.prototype.toString.call(c) === '[object Object]' } export function isRequiredComponent (name: string): boolean { diff --git a/packages/test-utils/src/matches.js b/packages/test-utils/src/matches.js index 87f95bb33..0739932c6 100644 --- a/packages/test-utils/src/matches.js +++ b/packages/test-utils/src/matches.js @@ -3,6 +3,7 @@ import { COMPONENT_SELECTOR, FUNCTIONAL_OPTIONS } from 'shared/consts' +import { isConstructor } from 'shared/validators' export function vmMatchesName (vm, name) { return !!name && ( @@ -19,7 +20,7 @@ function vmCtorMatches (vm, component) { return true } - const Ctor = typeof component === 'function' + const Ctor = isConstructor(component) ? component.options._Ctor : component._Ctor @@ -46,7 +47,7 @@ export function matches (node, selector) { return element && element.matches && element.matches(selector.value) } - const isFunctionalSelector = typeof selector.value === 'function' + const isFunctionalSelector = isConstructor(selector.value) ? selector.value.options.functional : selector.value.functional @@ -66,7 +67,7 @@ export function matches (node, selector) { // Fallback to name selector for COMPONENT_SELECTOR for Vue < 2.1 const nameSelector = - typeof selector.value === 'function' + isConstructor(selector.value) ? selector.value.extendOptions.name : selector.value.name return vmMatchesName(componentInstance, nameSelector) diff --git a/test/specs/mount.spec.js b/test/specs/mount.spec.js index c1dd1a462..10f15c8d2 100644 --- a/test/specs/mount.spec.js +++ b/test/specs/mount.spec.js @@ -384,6 +384,32 @@ describeRunIf(process.env.TEST_ENV !== 'node', 'mount', () => { expect(wrapper.findAll(ChildComponent).length).to.equal(1) }) + it('handles nested components with extends', () => { + const GrandChildComponent = { + template: '
', + created () { + this.$route.params + } + } + const ChildComponent = Vue.extend({ + template: '