Skip to content

Commit

Permalink
feat: add parentComponent option (#846)
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyerburgh authored Jul 22, 2018
1 parent 5fecbd2 commit 1951409
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 29 deletions.
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

0 comments on commit 1951409

Please sign in to comment.