Skip to content

Commit 4faf5fb

Browse files
authored
fix: wrap extended child components (#840)
1 parent bee7cb0 commit 4faf5fb

15 files changed

+435
-238
lines changed

Diff for: packages/create-instance/add-mocks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import $$Vue from 'vue'
33
import { warn } from 'shared/util'
44

55
export default function addMocks (
6-
mockedProperties: Object,
6+
mockedProperties: Object = {},
77
Vue: Component
88
): void {
99
Object.keys(mockedProperties).forEach(key => {

Diff for: packages/create-instance/add-stubs.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createComponentStubs } from 'shared/stub-components'
2+
3+
export function addStubs (component, stubs, _Vue) {
4+
const stubComponents = createComponentStubs(
5+
component.components,
6+
stubs
7+
)
8+
9+
function addStubComponentsMixin () {
10+
Object.assign(
11+
this.$options.components,
12+
stubComponents
13+
)
14+
}
15+
16+
_Vue.mixin({
17+
beforeMount: addStubComponentsMixin,
18+
// beforeCreate is for components created in node, which
19+
// never mount
20+
beforeCreate: addStubComponentsMixin
21+
})
22+
}

Diff for: packages/create-instance/create-instance.js

+17-77
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
import { createSlotVNodes } from './create-slot-vnodes'
44
import addMocks from './add-mocks'
55
import { addEventLogger } from './log-events'
6-
import { createComponentStubs } from 'shared/stub-components'
7-
import { throwError, warn, vueVersion } from 'shared/util'
6+
import { addStubs } from './add-stubs'
7+
import { throwError, vueVersion } from 'shared/util'
88
import { compileTemplate } from 'shared/compile-template'
99
import { isRequiredComponent } from 'shared/validators'
1010
import extractInstanceOptions from './extract-instance-options'
1111
import createFunctionalComponent from './create-functional-component'
1212
import { componentNeedsCompiling, isPlainObject } from 'shared/validators'
1313
import { validateSlots } from './validate-slots'
1414
import createScopedSlots from './create-scoped-slots'
15+
import { extendExtendedComponents } from './extend-extended-components'
1516

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

36-
// mounting options are vue-test-utils specific
37-
//
3837
// instance options are options that are passed to the
3938
// root instance when it's instantiated
40-
//
41-
// component options are the root components options
42-
const componentOptions = typeof component === 'function'
43-
? component.extendOptions
44-
: component
45-
4639
const instanceOptions = extractInstanceOptions(options)
4740

48-
if (options.mocks) {
49-
addMocks(options.mocks, _Vue)
50-
}
41+
addEventLogger(_Vue)
42+
addMocks(options.mocks, _Vue)
43+
addStubs(component, options.stubs, _Vue)
44+
5145
if (
5246
(component.options && component.options.functional) ||
5347
component.functional
@@ -63,8 +57,6 @@ export default function createInstance (
6357
compileTemplate(component)
6458
}
6559

66-
addEventLogger(_Vue)
67-
6860
// Replace globally registered components with components extended
6961
// from localVue. This makes sure the beforeMount mixins to add stubs
7062
// is applied to globally registered components.
@@ -78,77 +70,25 @@ export default function createInstance (
7870
}
7971
}
8072

81-
const stubComponents = createComponentStubs(
82-
component.components,
83-
// $FlowIgnore
84-
options.stubs
73+
extendExtendedComponents(
74+
component,
75+
_Vue,
76+
options.logModifiedComponents,
77+
instanceOptions.components
8578
)
86-
if (options.stubs) {
87-
instanceOptions.components = {
88-
...instanceOptions.components,
89-
...stubComponents
90-
}
91-
}
92-
function addStubComponentsMixin () {
93-
Object.assign(
94-
this.$options.components,
95-
stubComponents
96-
)
97-
}
98-
_Vue.mixin({
99-
beforeMount: addStubComponentsMixin,
100-
// beforeCreate is for components created in node, which
101-
// never mount
102-
beforeCreate: addStubComponentsMixin
103-
})
104-
Object.keys(componentOptions.components || {}).forEach(c => {
105-
if (
106-
componentOptions.components[c].extendOptions &&
107-
!instanceOptions.components[c]
108-
) {
109-
if (options.logModifiedComponents) {
110-
warn(
111-
`an extended child component <${c}> has been modified ` +
112-
`to ensure it has the correct instance properties. ` +
113-
`This means it is not possible to find the component ` +
114-
`with a component selector. To find the component, ` +
115-
`you must stub it manually using the stubs mounting ` +
116-
`option.`
117-
)
118-
}
119-
instanceOptions.components[c] = _Vue.extend(
120-
componentOptions.components[c]
121-
)
122-
}
123-
})
12479

12580
if (component.options) {
12681
component.options._base = _Vue
12782
}
12883

129-
function getExtendedComponent (component, instanceOptions) {
130-
const extendedComponent = component.extend(instanceOptions)
131-
// to keep the possible overridden prototype and _Vue mixins,
132-
// we need change the proto chains manually
133-
// @see https://github.com/vuejs/vue-test-utils/pull/856
134-
// code below equals to
135-
// `extendedComponent.prototype.__proto__.__proto__ = _Vue.prototype`
136-
const extendedComponentProto =
137-
Object.getPrototypeOf(extendedComponent.prototype)
138-
Object.setPrototypeOf(extendedComponentProto, _Vue.prototype)
139-
140-
return extendedComponent
141-
}
142-
14384
// extend component from _Vue to add properties and mixins
144-
const Constructor = typeof component === 'function'
145-
? getExtendedComponent(component, instanceOptions)
85+
// extend does not work correctly for sub class components in Vue < 2.2
86+
const Constructor = typeof component === 'function' && vueVersion < 2.3
87+
? component.extend(instanceOptions)
14688
: _Vue.extend(component).extend(instanceOptions)
14789

148-
Object.keys(instanceOptions.components || {}).forEach(key => {
149-
Constructor.component(key, instanceOptions.components[key])
150-
_Vue.component(key, instanceOptions.components[key])
151-
})
90+
// Keep reference to component mount was called with
91+
Constructor._vueTestUtilsRoot = component
15292

15393
if (options.slots) {
15494
compileTemplateForSlots(options.slots)
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { warn } from 'shared/util'
2+
3+
function createdFrom (extendOptions, componentOptions) {
4+
while (extendOptions) {
5+
if (extendOptions === componentOptions) {
6+
return true
7+
}
8+
if (extendOptions._vueTestUtilsRoot === componentOptions) {
9+
return true
10+
}
11+
extendOptions = extendOptions.extendOptions
12+
}
13+
}
14+
15+
function resolveComponents (options = {}, components = {}) {
16+
let extendOptions = options.extendOptions
17+
while (extendOptions) {
18+
resolveComponents(extendOptions, components)
19+
extendOptions = extendOptions.extendOptions
20+
}
21+
let extendsFrom = options.extends
22+
while (extendsFrom) {
23+
resolveComponents(extendsFrom, components)
24+
extendsFrom = extendsFrom.extends
25+
}
26+
Object.keys(options.components || {}).forEach((c) => {
27+
components[c] = options.components[c]
28+
})
29+
return components
30+
}
31+
32+
function shouldExtend (component) {
33+
while (component) {
34+
if (component.extendOptions) {
35+
return true
36+
}
37+
component = component.extends
38+
}
39+
}
40+
41+
// Components created with Vue.extend are not created internally in Vue
42+
// by extending a localVue constructor. To make sure they inherit
43+
// properties add to a localVue constructor, we must create new components by
44+
// extending the original extended components from the localVue constructor.
45+
// We apply a global mixin that overwrites the components original
46+
// components with the extended components when they are created.
47+
export function extendExtendedComponents (
48+
component,
49+
_Vue,
50+
logModifiedComponents,
51+
excludedComponents = { },
52+
stubAllComponents = false
53+
) {
54+
const extendedComponents = Object.create(null)
55+
const components = resolveComponents(component)
56+
57+
Object.keys(components).forEach(c => {
58+
const comp = components[c]
59+
const shouldExtendComponent =
60+
(shouldExtend(comp) &&
61+
!excludedComponents[c]) ||
62+
stubAllComponents
63+
if (shouldExtendComponent) {
64+
if (logModifiedComponents) {
65+
warn(
66+
`The child component <${c}> has been modified to ensure ` +
67+
`it is created with properties injected by Vue Test Utils. \n` +
68+
`This is because the component was created with Vue.extend, ` +
69+
`or uses the Vue Class Component decorator. \n` +
70+
`Because the component has been modified, it is not possible ` +
71+
`to find it with a component selector. To find the ` +
72+
`component, you must stub it manually using the stubs mounting ` +
73+
`option, or use a name or ref selector. \n` +
74+
`You can hide this warning by setting the Vue Test Utils ` +
75+
`config.logModifiedComponents option to false.`
76+
)
77+
}
78+
extendedComponents[c] = _Vue.extend(comp)
79+
}
80+
// If a component has been replaced with an extended component
81+
// all its child components must also be replaced.
82+
extendExtendedComponents(
83+
comp,
84+
_Vue,
85+
logModifiedComponents,
86+
{},
87+
shouldExtendComponent
88+
)
89+
})
90+
if (extendedComponents) {
91+
_Vue.mixin({
92+
created () {
93+
if (createdFrom(this.constructor, component)) {
94+
Object.assign(
95+
this.$options.components,
96+
extendedComponents
97+
)
98+
}
99+
}
100+
})
101+
}
102+
}

Diff for: packages/create-instance/extract-instance-options.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ const MOUNTING_OPTIONS = [
1010
'clone',
1111
'attrs',
1212
'listeners',
13-
'propsData'
13+
'propsData',
14+
'logModifiedComponents',
15+
'sync'
1416
]
1517

1618
export default function extractInstanceOptions (
1719
options: Object
1820
): Object {
19-
const instanceOptions = { ...options }
21+
const instanceOptions = {
22+
...options
23+
}
2024
MOUNTING_OPTIONS.forEach(mountingOption => {
2125
delete instanceOptions[mountingOption]
2226
})

Diff for: packages/shared/merge-options.js

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
// @flow
2+
import { normalizeStubs } from './normalize'
23

34
function getOption (option, config?: Object): any {
45
if (option || (config && Object.keys(config).length > 0)) {
56
if (option instanceof Function) {
67
return option
7-
} else if (Array.isArray(option)) {
8-
return [...option, ...Object.keys(config || {})]
9-
} else if (config instanceof Function) {
8+
}
9+
if (config instanceof Function) {
1010
throw new Error(`Config can't be a Function.`)
11-
} else {
12-
return {
13-
...config,
14-
...option
15-
}
11+
}
12+
return {
13+
...config,
14+
...option
1615
}
1716
}
1817
}
@@ -25,7 +24,7 @@ export function mergeOptions (options: Options, config: Config): Options {
2524
return {
2625
...options,
2726
logModifiedComponents: config.logModifiedComponents,
28-
stubs: getOption(options.stubs, config.stubs),
27+
stubs: getOption(normalizeStubs(options.stubs), config.stubs),
2928
mocks,
3029
methods,
3130
provide,

Diff for: packages/shared/normalize.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { isPlainObject } from './validators'
2+
import { throwError } from './util'
3+
4+
export function normalizeStubs (stubs = {}) {
5+
if (isPlainObject(stubs)) {
6+
return stubs
7+
}
8+
if (Array.isArray(stubs)) {
9+
return stubs.reduce((acc, stub) => {
10+
if (typeof stub !== 'string') {
11+
throwError('each item in an options.stubs array must be a string')
12+
}
13+
acc[stub] = true
14+
return acc
15+
}, {})
16+
}
17+
throwError('options.stubs must be an object or an Array')
18+
}

0 commit comments

Comments
 (0)