Skip to content

feat: add parentComponent option #846

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 3 commits into from
Jul 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions flow/options.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare type Options = {
context?: Object,
attrs?: { [key: string]: string },
listeners?: { [key: string]: Function | Array<Function> },
parentComponent?: Object,
logModifiedComponents?: boolean,
sync?: boolean
};
Expand Down
47 changes: 27 additions & 20 deletions packages/create-instance/create-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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()
}
4 changes: 0 additions & 4 deletions packages/shared/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
}
8 changes: 8 additions & 0 deletions packages/shared/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export function isVueComponent (component: any): boolean {
return true
}

if (typeof component.template === 'string') {
return true
}

return typeof component.render === 'function'
}

Expand Down Expand Up @@ -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]'
}
2 changes: 1 addition & 1 deletion packages/test-utils/src/recursively-set-data.js
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
context?: VNodeData
localVue?: typeof Vue
mocks?: object
parentComponent?: Component
slots?: Slots
scopedSlots?: Record<string, string>
stubs?: Stubs,
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/types/test/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mount(ClassComponent, {
mocks: {
$store: store
},
parentComponent: normalOptions,
slots: {
default: `<div>Foo</div>`,
foo: [normalOptions, functionalOptions],
Expand Down
35 changes: 35 additions & 0 deletions test/specs/mounting-options/parentComponent.spec.js
Original file line number Diff line number Diff line change
@@ -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: '<div>{{$parent.customName}}</div>'
}
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: '<div>{{$parent.customName}}</div>'
}
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)
})
})
})
4 changes: 2 additions & 2 deletions test/specs/mounting-options/stubs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ describeWithMountingMethods('options.stub', mountingMethod => {
const SubclassedComponent = Vue.extend({ template: '<div></div>' })
mountingMethod(ComponentWithChild, {
stubs: {
ChildComponent: ComponentAsAClass,
ChildComponent2: ComponentWithRender,
ChildComponent: ComponentWithRender,
ChildComponent2: ComponentAsAClass,
ChildComponent3: ComponentWithoutRender,
ChildComponent4: ExtendedComponent,
ChildComponent5: SubclassedComponent
Expand Down
4 changes: 2 additions & 2 deletions test/specs/wrapper/trigger.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down