Skip to content

Commit

Permalink
fix: wrap extended child components (#840)
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyerburgh authored Aug 5, 2018
1 parent bee7cb0 commit 4faf5fb
Show file tree
Hide file tree
Showing 15 changed files with 435 additions and 238 deletions.
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

0 comments on commit 4faf5fb

Please sign in to comment.