diff --git a/docs/api/options.md b/docs/api/options.md index d10bf02f8..5f462949f 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -11,6 +11,7 @@ Options for `mount` and `shallowMount`. The options object can contain both Vue - [`attachToDocument`](#attachtodocument) - [`attrs`](#attrs) - [`listeners`](#listeners) +- [`parentComponent`](#parentComponent) - [`provide`](#provide) - [`sync`](#sync) @@ -182,6 +183,23 @@ Set the component instance's `$attrs` object. Set the component instance's `$listeners` object. +## parentComponent + +- type: `Object` + +Component to use as parent for mounted component. + +Example: + +```js +import Foo from './Foo.vue' + +const wrapper = shallowMount(Component, { + parentComponent: Foo +}) +expect(wrapper.vm.$parent.name).toBe('foo') +``` + ## provide - type: `Object` diff --git a/flow/options.flow.js b/flow/options.flow.js index e88ddc9c6..d154c1f14 100644 --- a/flow/options.flow.js +++ b/flow/options.flow.js @@ -12,6 +12,7 @@ declare type Options = { context?: Object, attrs?: { [key: string]: string }, listeners?: { [key: string]: Function | Array }, + parentComponent?: Object, logModifiedComponents?: boolean, sync?: boolean }; diff --git a/packages/create-instance/create-instance.js b/packages/create-instance/create-instance.js index a70f92f6a..cc82ea95a 100644 --- a/packages/create-instance/create-instance.js +++ b/packages/create-instance/create-instance.js @@ -8,7 +8,7 @@ import { throwError, warn, 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 { componentNeedsCompiling, isPlainObject } from 'shared/validators' import { validateSlots } from './validate-slots' import createScopedSlots from './create-scoped-slots' @@ -138,25 +138,32 @@ export default function createInstance ( const scopedSlots = createScopedSlots(options.scopedSlots) - const Parent = _Vue.extend({ - provide: options.provide, - render (h) { - const slots = options.slots - ? createSlotVNodes(this, options.slots) - : undefined - return h( - Constructor, - { - ref: 'vm', - props: options.propsData, - on: options.listeners, - attrs: options.attrs, - scopedSlots - }, - slots - ) - } - }) + if (options.parentComponent && !isPlainObject(options.parentComponent)) { + throwError( + `options.parentComponent should be a valid Vue component ` + + `options object` + ) + } + + const parentComponentOptions = options.parentComponent || {} + parentComponentOptions.provide = options.provide + parentComponentOptions.render = function (h) { + const slots = options.slots + ? createSlotVNodes(this, options.slots) + : undefined + return h( + Constructor, + { + ref: 'vm', + props: options.propsData, + on: options.listeners, + attrs: options.attrs, + scopedSlots + }, + slots + ) + } + const Parent = _Vue.extend(parentComponentOptions) return new Parent() } diff --git a/packages/shared/util.js b/packages/shared/util.js index 730cf8b27..1b78c8506 100644 --- a/packages/shared/util.js +++ b/packages/shared/util.js @@ -34,7 +34,3 @@ export const hyphenate = (str: string): string => export const vueVersion = Number( `${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}` ) - -export function isPlainObject (obj: any): boolean { - return Object.prototype.toString.call(obj) === '[object Object]' -} diff --git a/packages/shared/validators.js b/packages/shared/validators.js index 2ba725dec..6116596d8 100644 --- a/packages/shared/validators.js +++ b/packages/shared/validators.js @@ -41,6 +41,10 @@ export function isVueComponent (component: any): boolean { return true } + if (typeof component.template === 'string') { + return true + } + return typeof component.render === 'function' } @@ -81,3 +85,7 @@ export function templateContainsComponent ( return re.test(template) }) } + +export function isPlainObject (obj: any): boolean { + return Object.prototype.toString.call(obj) === '[object Object]' +} diff --git a/packages/test-utils/src/recursively-set-data.js b/packages/test-utils/src/recursively-set-data.js index bd75f35ae..e76a54612 100644 --- a/packages/test-utils/src/recursively-set-data.js +++ b/packages/test-utils/src/recursively-set-data.js @@ -1,4 +1,4 @@ -import { isPlainObject } from 'shared/util' +import { isPlainObject } from 'shared/validators' export function recursivelySetData (vm, target, obj) { Object.keys(obj).forEach(key => { diff --git a/packages/test-utils/types/index.d.ts b/packages/test-utils/types/index.d.ts index 49c1e9c09..c2cf1dfd6 100644 --- a/packages/test-utils/types/index.d.ts +++ b/packages/test-utils/types/index.d.ts @@ -126,6 +126,7 @@ interface MountOptions extends ComponentOptions { context?: VNodeData localVue?: typeof Vue mocks?: object + parentComponent?: Component slots?: Slots scopedSlots?: Record stubs?: Stubs, diff --git a/packages/test-utils/types/test/mount.ts b/packages/test-utils/types/test/mount.ts index cb847a483..a9ba18482 100644 --- a/packages/test-utils/types/test/mount.ts +++ b/packages/test-utils/types/test/mount.ts @@ -28,6 +28,7 @@ mount(ClassComponent, { mocks: { $store: store }, + parentComponent: normalOptions, slots: { default: `
Foo
`, foo: [normalOptions, functionalOptions], diff --git a/test/specs/mounting-options/parentComponent.spec.js b/test/specs/mounting-options/parentComponent.spec.js new file mode 100644 index 000000000..147b3c7e2 --- /dev/null +++ b/test/specs/mounting-options/parentComponent.spec.js @@ -0,0 +1,35 @@ +import { describeWithMountingMethods } from '~resources/utils' + +describeWithMountingMethods('options.parentComponent', mountingMethod => { + it('mounts component with $parent set to options.parentComponent', () => { + const Parent = { + data: () => ({ + customName: 'Parent Name' + }) + } + const TestComponent = { + template: '
{{$parent.customName}}
' + } + const wrapper = mountingMethod(TestComponent, { + parentComponent: Parent + }) + const HTML = mountingMethod.name === 'renderToString' + ? wrapper + : wrapper.html() + expect(HTML).to.contain('Parent Name') + }) + + it('validates parentComponent option', () => { + ;['str', 123, [], () => {}].forEach(invalidParent => { + const TestComponent = { + template: '
{{$parent.customName}}
' + } + const fn = () => mountingMethod(TestComponent, { + parentComponent: invalidParent + }) + const message = '[vue-test-utils]: options.parentComponent should be a valid Vue component options object' + expect(fn).to.throw() + .with.property('message', message) + }) + }) +}) diff --git a/test/specs/mounting-options/stubs.spec.js b/test/specs/mounting-options/stubs.spec.js index b23a9ab59..24a86cad8 100644 --- a/test/specs/mounting-options/stubs.spec.js +++ b/test/specs/mounting-options/stubs.spec.js @@ -37,8 +37,8 @@ describeWithMountingMethods('options.stub', mountingMethod => { const SubclassedComponent = Vue.extend({ template: '
' }) mountingMethod(ComponentWithChild, { stubs: { - ChildComponent: ComponentAsAClass, - ChildComponent2: ComponentWithRender, + ChildComponent: ComponentWithRender, + ChildComponent2: ComponentAsAClass, ChildComponent3: ComponentWithoutRender, ChildComponent4: ExtendedComponent, ChildComponent5: SubclassedComponent diff --git a/test/specs/wrapper/trigger.spec.js b/test/specs/wrapper/trigger.spec.js index deb8b61d0..be9da5f0a 100644 --- a/test/specs/wrapper/trigger.spec.js +++ b/test/specs/wrapper/trigger.spec.js @@ -81,9 +81,9 @@ describeWithShallowAndMount('trigger', mountingMethod => { it('causes DOM to update after clickHandler method that changes components data is called', () => { const wrapper = mountingMethod(ComponentWithEvents) const toggle = wrapper.find('.toggle') - expect(toggle.hasClass('active')).to.equal(false) + expect(toggle.classes()).not.to.contain('active') toggle.trigger('click') - expect(toggle.hasClass('active')).to.equal(true) + expect(toggle.classes()).to.contain('active') }) it('adds options to event', () => {