Skip to content

fix: wrap extended child components #840

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

Merged
merged 14 commits into from
Aug 5, 2018
Merged
49 changes: 15 additions & 34 deletions packages/create-instance/create-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { createSlotVNodes } from './add-slots'
import addMocks from './add-mocks'
import { addEventLogger } from './log-events'
import { createComponentStubs } from 'shared/stub-components'
import { throwError, warn, vueVersion } from 'shared/util'
import { throwError, vueVersion } from 'shared/util'
import { compileTemplate } from 'shared/compile-template'
import extractInstanceOptions from './extract-instance-options'
import createFunctionalComponent from './create-functional-component'
import { componentNeedsCompiling } from 'shared/validators'
import { validateSlots } from './validate-slots'
import createScopedSlots from './create-scoped-slots'
import { extendExtendedComponents } from './extend-extended-components'

export default function createInstance (
component: Component,
Expand All @@ -27,9 +28,6 @@ export default function createInstance (
// 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)

Expand Down Expand Up @@ -59,13 +57,15 @@ export default function createInstance (
// $FlowIgnore
options.stubs
)
if (options.stubs) {
instanceOptions.components = {
...instanceOptions.components,
// $FlowIgnore
...stubComponents
}
}

extendExtendedComponents(
component,
_Vue,
options.logModifiedComponents,
instanceOptions.components
)

// stub components should override every component defined in a component
_Vue.mixin({
created () {
Object.assign(
Expand All @@ -74,26 +74,6 @@ export default function createInstance (
)
}
})
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
Expand All @@ -103,9 +83,10 @@ export default function createInstance (
? 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])
Constructor._vueTestUtilsRoot = component

Object.keys(instanceOptions.components || {}).forEach(c => {
Constructor.component(c, instanceOptions.components[c])
})

if (options.slots) {
Expand Down
100 changes: 100 additions & 0 deletions packages/create-instance/extend-extended-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 will not inherit from
// a localVue constructor by default. To make sure they inherit
// from a localVue constructor, we must create new components by
// extending the original extended components from the localVue constructor.
// The registered original extended components should only be
// overwritten in the component that they are registered on.
// 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(
`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 ` +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't find({ name: 'foo' }) still work in this case? If so maybe that should be added here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it would, I'll add it.

This PR also makes your suggestion of searching by string name a lot more attractive, as it would solve this problem entirely 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice 👍 Lately I (and my team) have decided on the following practice:

  1. when finshing HTML elements, using the querySelector API
  2. when finding components, use the find(Component) API.

which I think is the most intuitive for onboarding people new to Vue and TDD.

`option.`
)
}
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
)
}
}
})
}
}
9 changes: 7 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,18 @@ const MOUNTING_OPTIONS = [
'clone',
'attrs',
'listeners',
'propsData'
'propsData',
'logModifiedComponents',
'sync'
]

export default function extractInstanceOptions (
options: Object
): Object {
const instanceOptions = { ...options }
const instanceOptions = {
...options,
_vueTestUtilsRootExtendOptions: true
}
MOUNTING_OPTIONS.forEach(mountingOption => {
delete instanceOptions[mountingOption]
})
Expand Down
8 changes: 6 additions & 2 deletions packages/shared/stub-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,12 @@ export function createComponentStubsForAll (component: Component): Components {
extended = extended.extends
}

if (component.extendOptions && component.extendOptions.components) {
stubComponents(component.extendOptions.components, stubbedComponents)
let extendOptions = component.extendOptions
while (extendOptions) {
if (extendOptions && extendOptions.components) {
stubComponents(extendOptions.components, stubbedComponents)
}
extendOptions = extendOptions.extendOptions
}

return stubbedComponents
Expand Down
4 changes: 2 additions & 2 deletions packages/test-utils/src/shallow-mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function shallowMount (
component: Component,
options: Options = {}
): VueWrapper {
const vue = options.localVue || Vue
const _Vue = options.localVue || Vue

// remove any recursive components added to the constructor
// in vm._init from previous tests
Expand All @@ -26,7 +26,7 @@ export default function shallowMount (
return mount(component, {
...options,
components: {
...createComponentStubsForGlobals(vue),
...createComponentStubsForGlobals(_Vue),
...createComponentStubsForAll(component)
}
})
Expand Down
131 changes: 96 additions & 35 deletions test/specs/mounting-options/localVue.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,54 +28,115 @@ describeWithMountingMethods('options.localVue', mountingMethod => {
}
)

itSkipIf(vueVersion < 2.3, 'works correctly with extended children', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store({
state: { val: 2 }
})
const ChildComponent = Vue.extend({
template: '<span>{{val}}</span>',
computed: {
val () {
return this.$store.state.val
itSkipIf(
vueVersion < 2.3,
'works correctly with extended children', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store({
state: { val: 2 }
})
const ChildComponent = Vue.extend({
template: '<span>{{val}}</span>',
computed: {
val () {
return this.$store.state.val
}
}
})
const TestComponent = {
template: '<div><child-component /></div>',
components: {
ChildComponent
}
}
})
const TestComponent = {
template: '<div><child-component /></div>',
components: {
ChildComponent
const wrapper = mountingMethod(TestComponent, {
localVue,
store
})
const HTML =
mountingMethod.name === 'renderToString' ? wrapper : wrapper.html()
if (mountingMethod.name === 'shallowMount') {
expect(HTML).to.not.contain('2')
} else {
expect(HTML).to.contain('2')
}
}
const wrapper = mountingMethod(TestComponent, {
localVue,
store
})
const HTML =
mountingMethod.name === 'renderToString' ? wrapper : wrapper.html()
if (mountingMethod.name === 'shallowMount') {
expect(HTML).to.not.contain('2')
} else {
expect(HTML).to.contain('2')
}
})

it('is applies to child extended components', () => {
const ChildComponent = Vue.extend({
template: '<div>{{$route.params}}</div>'
})
const TestComponent = Vue.extend({
template: '<child-component />',
components: { ChildComponent }
itSkipIf(
vueVersion < 2.3,
'is applied to deeply extended components', () => {
const GrandChildComponent = Vue.extend(Vue.extend({
template: '<div>{{$route.params}}</div>'
}))
const ChildComponent = Vue.extend(Vue.extend(Vue.extend({
template: '<div><grand-child-component />{{$route.params}}</div>',
components: {
GrandChildComponent
}
})))
const TestComponent = Vue.extend(Vue.extend({
template: '<child-component />',
components: { ChildComponent }
}))
const localVue = createLocalVue()
localVue.prototype.$route = {}

mountingMethod(TestComponent, {
localVue
})
})

it('is applied to components that extend from other components', () => {
const localVue = createLocalVue()
localVue.prototype.$route = {}

const Extends = {
created () {
console.log(this.$route.params)
}
}
const TestComponent = {
extends: Extends
}
mountingMethod(TestComponent, {
localVue
})
})

itSkipIf(
vueVersion < 2.3,
'is applied to mixed extended components', () => {
const BaseGrandChildComponent = {
created () {
this.$route.params
}
}
const GrandChildComponent = {
created () {
this.$route.params
},
template: '<div/>',
extends: BaseGrandChildComponent
}
const ChildComponent = Vue.extend(({
template: '<div><grand-child-component />{{$route.params}}</div>',
components: {
GrandChildComponent
}
}))
const TestComponent = Vue.extend(Vue.extend({
template: '<div><child-component /></div>',
components: { ChildComponent }
}))
const localVue = createLocalVue()
localVue.prototype.$route = {}

mountingMethod(TestComponent, {
localVue
})
})

it('does not add created mixin to localVue', () => {
const localVue = createLocalVue()
mountingMethod({ render: () => {} }, {
Expand Down
Loading