Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: wrap extended child components #840

Merged
merged 14 commits into from
Aug 5, 2018
Merged
2 changes: 1 addition & 1 deletion packages/create-instance/add-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import $$Vue from 'vue'
import { warn } from 'shared/util'

export default function addMocks (
mockedProperties: Object,
mockedProperties: Object = {},
Vue: Component
): void {
Object.keys(mockedProperties).forEach(key => {
Expand Down
22 changes: 22 additions & 0 deletions packages/create-instance/add-stubs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createComponentStubs } from 'shared/stub-components'

export function addStubs (component, stubs, _Vue) {
const stubComponents = createComponentStubs(
component.components,
stubs
)

function addStubComponentsMixin () {
Object.assign(
this.$options.components,
stubComponents
)
}

_Vue.mixin({
beforeMount: addStubComponentsMixin,
// beforeCreate is for components created in node, which
// never mount
beforeCreate: addStubComponentsMixin
})
}
94 changes: 17 additions & 77 deletions packages/create-instance/create-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
import { createSlotVNodes } from './create-slot-vnodes'
import addMocks from './add-mocks'
import { addEventLogger } from './log-events'
import { createComponentStubs } from 'shared/stub-components'
import { throwError, warn, vueVersion } from 'shared/util'
import { addStubs } from './add-stubs'
import { throwError, vueVersion } from 'shared/util'
import { compileTemplate } from 'shared/compile-template'
import { isRequiredComponent } from 'shared/validators'
import extractInstanceOptions from './extract-instance-options'
import createFunctionalComponent from './create-functional-component'
import { componentNeedsCompiling, isPlainObject } from 'shared/validators'
import { validateSlots } from './validate-slots'
import createScopedSlots from './create-scoped-slots'
import { extendExtendedComponents } from './extend-extended-components'

function compileTemplateForSlots (slots: Object): void {
Object.keys(slots).forEach(key => {
Expand All @@ -33,21 +34,14 @@ export default function createInstance (
// Remove cached constructor
delete component._Ctor

// mounting options are vue-test-utils specific
//
// instance options are options that are passed to the
// root instance when it's instantiated
//
// component options are the root components options
const componentOptions = typeof component === 'function'
? component.extendOptions
: component

const instanceOptions = extractInstanceOptions(options)

if (options.mocks) {
addMocks(options.mocks, _Vue)
}
addEventLogger(_Vue)
addMocks(options.mocks, _Vue)
addStubs(component, options.stubs, _Vue)

if (
(component.options && component.options.functional) ||
component.functional
Expand All @@ -63,8 +57,6 @@ export default function createInstance (
compileTemplate(component)
}

addEventLogger(_Vue)

// Replace globally registered components with components extended
// from localVue. This makes sure the beforeMount mixins to add stubs
// is applied to globally registered components.
Expand All @@ -78,77 +70,25 @@ export default function createInstance (
}
}

const stubComponents = createComponentStubs(
component.components,
// $FlowIgnore
options.stubs
extendExtendedComponents(
component,
_Vue,
options.logModifiedComponents,
instanceOptions.components
)
if (options.stubs) {
instanceOptions.components = {
...instanceOptions.components,
...stubComponents
}
}
function addStubComponentsMixin () {
Object.assign(
this.$options.components,
stubComponents
)
}
_Vue.mixin({
beforeMount: addStubComponentsMixin,
// beforeCreate is for components created in node, which
// never mount
beforeCreate: addStubComponentsMixin
})
Object.keys(componentOptions.components || {}).forEach(c => {
if (
componentOptions.components[c].extendOptions &&
!instanceOptions.components[c]
) {
if (options.logModifiedComponents) {
warn(
`an extended child component <${c}> has been modified ` +
`to ensure it has the correct instance properties. ` +
`This means it is not possible to find the component ` +
`with a component selector. To find the component, ` +
`you must stub it manually using the stubs mounting ` +
`option.`
)
}
instanceOptions.components[c] = _Vue.extend(
componentOptions.components[c]
)
}
})

if (component.options) {
component.options._base = _Vue
}

function getExtendedComponent (component, instanceOptions) {
const extendedComponent = component.extend(instanceOptions)
// to keep the possible overridden prototype and _Vue mixins,
// we need change the proto chains manually
// @see https://github.com/vuejs/vue-test-utils/pull/856
// code below equals to
// `extendedComponent.prototype.__proto__.__proto__ = _Vue.prototype`
const extendedComponentProto =
Object.getPrototypeOf(extendedComponent.prototype)
Object.setPrototypeOf(extendedComponentProto, _Vue.prototype)

return extendedComponent
}

// extend component from _Vue to add properties and mixins
const Constructor = typeof component === 'function'
? getExtendedComponent(component, instanceOptions)
// extend does not work correctly for sub class components in Vue < 2.2
const Constructor = typeof component === 'function' && vueVersion < 2.3
? component.extend(instanceOptions)
: _Vue.extend(component).extend(instanceOptions)

Object.keys(instanceOptions.components || {}).forEach(key => {
Constructor.component(key, instanceOptions.components[key])
_Vue.component(key, instanceOptions.components[key])
})
// Keep reference to component mount was called with
Constructor._vueTestUtilsRoot = component

if (options.slots) {
compileTemplateForSlots(options.slots)
Expand Down
102 changes: 102 additions & 0 deletions packages/create-instance/extend-extended-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { warn } from 'shared/util'

function createdFrom (extendOptions, componentOptions) {
while (extendOptions) {
if (extendOptions === componentOptions) {
return true
}
if (extendOptions._vueTestUtilsRoot === componentOptions) {
return true
}
extendOptions = extendOptions.extendOptions
}
}

function resolveComponents (options = {}, components = {}) {
let extendOptions = options.extendOptions
while (extendOptions) {
resolveComponents(extendOptions, components)
extendOptions = extendOptions.extendOptions
}
let extendsFrom = options.extends
while (extendsFrom) {
resolveComponents(extendsFrom, components)
extendsFrom = extendsFrom.extends
}
Object.keys(options.components || {}).forEach((c) => {
components[c] = options.components[c]
})
return components
}

function shouldExtend (component) {
while (component) {
if (component.extendOptions) {
return true
}
component = component.extends
}
}

// Components created with Vue.extend are not created internally in Vue
// by extending a localVue constructor. To make sure they inherit
// properties add to a localVue constructor, we must create new components by
// extending the original extended components from the localVue constructor.
// We apply a global mixin that overwrites the components original
// components with the extended components when they are created.
export function extendExtendedComponents (
component,
_Vue,
logModifiedComponents,
excludedComponents = { },
stubAllComponents = false
) {
const extendedComponents = Object.create(null)
const components = resolveComponents(component)

Object.keys(components).forEach(c => {
const comp = components[c]
const shouldExtendComponent =
(shouldExtend(comp) &&
!excludedComponents[c]) ||
stubAllComponents
if (shouldExtendComponent) {
if (logModifiedComponents) {
warn(
`The child component <${c}> has been modified to ensure ` +
`it is created with properties injected by Vue Test Utils. \n` +
`This is because the component was created with Vue.extend, ` +
`or uses the Vue Class Component decorator. \n` +
`Because the component has been modified, it is not possible ` +
`to find it with a component selector. To find the ` +
`component, you must stub it manually using the stubs mounting ` +
`option, or use a name or ref selector. \n` +
`You can hide this warning by setting the Vue Test Utils ` +
`config.logModifiedComponents option to false.`
)
}
extendedComponents[c] = _Vue.extend(comp)
}
// If a component has been replaced with an extended component
// all its child components must also be replaced.
extendExtendedComponents(
comp,
_Vue,
logModifiedComponents,
{},
shouldExtendComponent
)
})
if (extendedComponents) {
_Vue.mixin({
created () {
if (createdFrom(this.constructor, component)) {
Object.assign(
this.$options.components,
extendedComponents
)
}
}
})
}
}
8 changes: 6 additions & 2 deletions packages/create-instance/extract-instance-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ const MOUNTING_OPTIONS = [
'clone',
'attrs',
'listeners',
'propsData'
'propsData',
'logModifiedComponents',
'sync'
]

export default function extractInstanceOptions (
options: Object
): Object {
const instanceOptions = { ...options }
const instanceOptions = {
...options
}
MOUNTING_OPTIONS.forEach(mountingOption => {
delete instanceOptions[mountingOption]
})
Expand Down
17 changes: 8 additions & 9 deletions packages/shared/merge-options.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// @flow
import { normalizeStubs } from './normalize'

function getOption (option, config?: Object): any {
if (option || (config && Object.keys(config).length > 0)) {
if (option instanceof Function) {
return option
} else if (Array.isArray(option)) {
return [...option, ...Object.keys(config || {})]
} else if (config instanceof Function) {
}
if (config instanceof Function) {
throw new Error(`Config can't be a Function.`)
} else {
return {
...config,
...option
}
}
return {
...config,
...option
}
}
}
Expand All @@ -25,7 +24,7 @@ export function mergeOptions (options: Options, config: Config): Options {
return {
...options,
logModifiedComponents: config.logModifiedComponents,
stubs: getOption(options.stubs, config.stubs),
stubs: getOption(normalizeStubs(options.stubs), config.stubs),
mocks,
methods,
provide,
Expand Down
18 changes: 18 additions & 0 deletions packages/shared/normalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isPlainObject } from './validators'
import { throwError } from './util'

export function normalizeStubs (stubs = {}) {
if (isPlainObject(stubs)) {
return stubs
}
if (Array.isArray(stubs)) {
return stubs.reduce((acc, stub) => {
if (typeof stub !== 'string') {
throwError('each item in an options.stubs array must be a string')
}
acc[stub] = true
return acc
}, {})
}
throwError('options.stubs must be an object or an Array')
}
Loading