Skip to content

Commit 1951409

Browse files
authored
feat: add parentComponent option (#846)
1 parent 5fecbd2 commit 1951409

File tree

11 files changed

+96
-29
lines changed

11 files changed

+96
-29
lines changed

Diff for: docs/api/options.md

+18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Options for `mount` and `shallowMount`. The options object can contain both Vue
1111
- [`attachToDocument`](#attachtodocument)
1212
- [`attrs`](#attrs)
1313
- [`listeners`](#listeners)
14+
- [`parentComponent`](#parentComponent)
1415
- [`provide`](#provide)
1516
- [`sync`](#sync)
1617

@@ -182,6 +183,23 @@ Set the component instance's `$attrs` object.
182183

183184
Set the component instance's `$listeners` object.
184185

186+
## parentComponent
187+
188+
- type: `Object`
189+
190+
Component to use as parent for mounted component.
191+
192+
Example:
193+
194+
```js
195+
import Foo from './Foo.vue'
196+
197+
const wrapper = shallowMount(Component, {
198+
parentComponent: Foo
199+
})
200+
expect(wrapper.vm.$parent.name).toBe('foo')
201+
```
202+
185203
## provide
186204

187205
- type: `Object`

Diff for: flow/options.flow.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ declare type Options = {
1212
context?: Object,
1313
attrs?: { [key: string]: string },
1414
listeners?: { [key: string]: Function | Array<Function> },
15+
parentComponent?: Object,
1516
logModifiedComponents?: boolean,
1617
sync?: boolean
1718
};

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

+27-20
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { throwError, warn, vueVersion } from 'shared/util'
88
import { compileTemplate } from 'shared/compile-template'
99
import extractInstanceOptions from './extract-instance-options'
1010
import createFunctionalComponent from './create-functional-component'
11-
import { componentNeedsCompiling } from 'shared/validators'
11+
import { componentNeedsCompiling, isPlainObject } from 'shared/validators'
1212
import { validateSlots } from './validate-slots'
1313
import createScopedSlots from './create-scoped-slots'
1414

@@ -138,25 +138,32 @@ export default function createInstance (
138138

139139
const scopedSlots = createScopedSlots(options.scopedSlots)
140140

141-
const Parent = _Vue.extend({
142-
provide: options.provide,
143-
render (h) {
144-
const slots = options.slots
145-
? createSlotVNodes(this, options.slots)
146-
: undefined
147-
return h(
148-
Constructor,
149-
{
150-
ref: 'vm',
151-
props: options.propsData,
152-
on: options.listeners,
153-
attrs: options.attrs,
154-
scopedSlots
155-
},
156-
slots
157-
)
158-
}
159-
})
141+
if (options.parentComponent && !isPlainObject(options.parentComponent)) {
142+
throwError(
143+
`options.parentComponent should be a valid Vue component ` +
144+
`options object`
145+
)
146+
}
147+
148+
const parentComponentOptions = options.parentComponent || {}
149+
parentComponentOptions.provide = options.provide
150+
parentComponentOptions.render = function (h) {
151+
const slots = options.slots
152+
? createSlotVNodes(this, options.slots)
153+
: undefined
154+
return h(
155+
Constructor,
156+
{
157+
ref: 'vm',
158+
props: options.propsData,
159+
on: options.listeners,
160+
attrs: options.attrs,
161+
scopedSlots
162+
},
163+
slots
164+
)
165+
}
166+
const Parent = _Vue.extend(parentComponentOptions)
160167

161168
return new Parent()
162169
}

Diff for: packages/shared/util.js

-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,3 @@ export const hyphenate = (str: string): string =>
3434
export const vueVersion = Number(
3535
`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`
3636
)
37-
38-
export function isPlainObject (obj: any): boolean {
39-
return Object.prototype.toString.call(obj) === '[object Object]'
40-
}

Diff for: packages/shared/validators.js

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export function isVueComponent (component: any): boolean {
4141
return true
4242
}
4343

44+
if (typeof component.template === 'string') {
45+
return true
46+
}
47+
4448
return typeof component.render === 'function'
4549
}
4650

@@ -81,3 +85,7 @@ export function templateContainsComponent (
8185
return re.test(template)
8286
})
8387
}
88+
89+
export function isPlainObject (obj: any): boolean {
90+
return Object.prototype.toString.call(obj) === '[object Object]'
91+
}

Diff for: packages/test-utils/src/recursively-set-data.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPlainObject } from 'shared/util'
1+
import { isPlainObject } from 'shared/validators'
22

33
export function recursivelySetData (vm, target, obj) {
44
Object.keys(obj).forEach(key => {

Diff for: packages/test-utils/types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ interface MountOptions<V extends Vue> extends ComponentOptions<V> {
126126
context?: VNodeData
127127
localVue?: typeof Vue
128128
mocks?: object
129+
parentComponent?: Component
129130
slots?: Slots
130131
scopedSlots?: Record<string, string>
131132
stubs?: Stubs,

Diff for: packages/test-utils/types/test/mount.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mount(ClassComponent, {
2828
mocks: {
2929
$store: store
3030
},
31+
parentComponent: normalOptions,
3132
slots: {
3233
default: `<div>Foo</div>`,
3334
foo: [normalOptions, functionalOptions],

Diff for: test/specs/mounting-options/parentComponent.spec.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describeWithMountingMethods } from '~resources/utils'
2+
3+
describeWithMountingMethods('options.parentComponent', mountingMethod => {
4+
it('mounts component with $parent set to options.parentComponent', () => {
5+
const Parent = {
6+
data: () => ({
7+
customName: 'Parent Name'
8+
})
9+
}
10+
const TestComponent = {
11+
template: '<div>{{$parent.customName}}</div>'
12+
}
13+
const wrapper = mountingMethod(TestComponent, {
14+
parentComponent: Parent
15+
})
16+
const HTML = mountingMethod.name === 'renderToString'
17+
? wrapper
18+
: wrapper.html()
19+
expect(HTML).to.contain('Parent Name')
20+
})
21+
22+
it('validates parentComponent option', () => {
23+
;['str', 123, [], () => {}].forEach(invalidParent => {
24+
const TestComponent = {
25+
template: '<div>{{$parent.customName}}</div>'
26+
}
27+
const fn = () => mountingMethod(TestComponent, {
28+
parentComponent: invalidParent
29+
})
30+
const message = '[vue-test-utils]: options.parentComponent should be a valid Vue component options object'
31+
expect(fn).to.throw()
32+
.with.property('message', message)
33+
})
34+
})
35+
})

Diff for: test/specs/mounting-options/stubs.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ describeWithMountingMethods('options.stub', mountingMethod => {
3737
const SubclassedComponent = Vue.extend({ template: '<div></div>' })
3838
mountingMethod(ComponentWithChild, {
3939
stubs: {
40-
ChildComponent: ComponentAsAClass,
41-
ChildComponent2: ComponentWithRender,
40+
ChildComponent: ComponentWithRender,
41+
ChildComponent2: ComponentAsAClass,
4242
ChildComponent3: ComponentWithoutRender,
4343
ChildComponent4: ExtendedComponent,
4444
ChildComponent5: SubclassedComponent

Diff for: test/specs/wrapper/trigger.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ describeWithShallowAndMount('trigger', mountingMethod => {
8181
it('causes DOM to update after clickHandler method that changes components data is called', () => {
8282
const wrapper = mountingMethod(ComponentWithEvents)
8383
const toggle = wrapper.find('.toggle')
84-
expect(toggle.hasClass('active')).to.equal(false)
84+
expect(toggle.classes()).not.to.contain('active')
8585
toggle.trigger('click')
86-
expect(toggle.hasClass('active')).to.equal(true)
86+
expect(toggle.classes()).to.contain('active')
8787
})
8888

8989
it('adds options to event', () => {

0 commit comments

Comments
 (0)