Skip to content

Commit e730945

Browse files
authored
Merge pull request #51 from vuejs/lachlan/poc-set-props
feat: setProps
2 parents ac33be0 + b8209a4 commit e730945

File tree

3 files changed

+162
-21
lines changed

3 files changed

+162
-21
lines changed

src/mount.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@ import {
44
VNode,
55
defineComponent,
66
VNodeNormalizedChildren,
7-
VNodeProps,
87
ComponentOptions,
98
Plugin,
109
Directive,
11-
Component
10+
Component,
11+
reactive
1212
} from 'vue'
1313

14-
import { VueWrapper, createWrapper } from './vue-wrapper'
14+
import { createWrapper } from './vue-wrapper'
1515
import { createEmitMixin } from './emitMixin'
1616
import { createDataMixin } from './dataMixin'
1717
import { MOUNT_ELEMENT_ID } from './constants'
1818

1919
type Slot = VNode | string | { render: Function }
2020

21-
interface MountingOptions<Props> {
21+
interface MountingOptions {
2222
data?: () => Record<string, unknown>
23-
props?: Props
23+
props?: Record<string, any>
2424
slots?: {
2525
default?: Slot
2626
[key: string]: Slot
@@ -37,10 +37,7 @@ interface MountingOptions<Props> {
3737
stubs?: Record<string, any>
3838
}
3939

40-
export function mount<P>(
41-
originalComponent: any,
42-
options?: MountingOptions<P>
43-
): VueWrapper {
40+
export function mount(originalComponent: any, options?: MountingOptions) {
4441
const component = { ...originalComponent }
4542

4643
// Reset the document.body
@@ -69,16 +66,27 @@ export function mount<P>(
6966
component.mixins = [...(component.mixins || []), dataMixin]
7067
}
7168

69+
// we define props as reactive so that way when we update them with `setProps`
70+
// Vue's reactivity system will cause a rerender.
71+
const props = reactive({ ...options?.props, ref: 'VTU_COMPONENT' })
72+
7273
// create the wrapper component
73-
const Parent = (props?: VNodeProps) =>
74-
defineComponent({
75-
render() {
76-
return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots)
77-
}
78-
})
74+
const Parent = defineComponent({
75+
render() {
76+
return h(component, props, slots)
77+
}
78+
})
79+
80+
const setProps = (newProps: Record<string, unknown>) => {
81+
for (const [k, v] of Object.entries(newProps)) {
82+
props[k] = v
83+
}
84+
85+
return app.$nextTick()
86+
}
7987

8088
// create the vm
81-
const vm = createApp(Parent(options && options.props))
89+
const vm = createApp(Parent)
8290

8391
// global mocks mixin
8492
if (options?.global?.mocks) {
@@ -128,5 +136,5 @@ export function mount<P>(
128136
// mount the app!
129137
const app = vm.mount(el)
130138

131-
return createWrapper(app, events)
139+
return createWrapper(app, events, setProps)
132140
}

src/vue-wrapper.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComponentPublicInstance } from 'vue'
1+
import { ComponentPublicInstance, nextTick } from 'vue'
22
import { ShapeFlags } from '@vue/shared'
33

44
import { DOMWrapper } from './dom-wrapper'
@@ -10,9 +10,15 @@ export class VueWrapper implements WrapperAPI {
1010
private componentVM: ComponentPublicInstance
1111
private __emitted: Record<string, unknown[]> = {}
1212
private __vm: ComponentPublicInstance
13+
private __setProps: (props: Record<string, any>) => void
1314

14-
constructor(vm: ComponentPublicInstance, events: Record<string, unknown[]>) {
15+
constructor(
16+
vm: ComponentPublicInstance,
17+
events: Record<string, unknown[]>,
18+
setProps: (props: Record<string, any>) => void
19+
) {
1520
this.__vm = vm
21+
this.__setProps = setProps
1622
this.componentVM = this.vm.$refs['VTU_COMPONENT'] as ComponentPublicInstance
1723
this.__emitted = events
1824
}
@@ -78,6 +84,11 @@ export class VueWrapper implements WrapperAPI {
7884
return Array.from(results).map((x) => new DOMWrapper(x))
7985
}
8086

87+
setProps(props: Record<string, any>) {
88+
this.__setProps(props)
89+
return nextTick()
90+
}
91+
8192
trigger(eventString: string) {
8293
const rootElementWrapper = new DOMWrapper(this.element)
8394
return rootElementWrapper.trigger(eventString)
@@ -86,7 +97,8 @@ export class VueWrapper implements WrapperAPI {
8697

8798
export function createWrapper(
8899
vm: ComponentPublicInstance,
89-
events: Record<string, unknown[]>
100+
events: Record<string, unknown[]>,
101+
setProps: (props: Record<string, any>) => void
90102
): VueWrapper {
91-
return new VueWrapper(vm, events)
103+
return new VueWrapper(vm, events, setProps)
92104
}

tests/setProps.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { defineComponent, h, computed } from 'vue'
2+
3+
import { mount } from '../src'
4+
5+
describe('setProps', () => {
6+
it('updates a primitive prop', async () => {
7+
const Foo = {
8+
props: ['foo'],
9+
template: '<div>{{ foo }}</div>'
10+
}
11+
const wrapper = mount(Foo, {
12+
props: {
13+
foo: 'foo'
14+
}
15+
})
16+
expect(wrapper.html()).toContain('foo')
17+
18+
await wrapper.setProps({ foo: 'qux' })
19+
expect(wrapper.html()).toContain('qux')
20+
})
21+
22+
it('updates a function prop', async () => {
23+
const Foo = {
24+
props: ['obj'],
25+
template: `
26+
<div>
27+
<div v-if="obj.foo()">foo</div>
28+
</div>
29+
`
30+
}
31+
const wrapper = mount(Foo, {
32+
props: {
33+
obj: {
34+
foo: () => true
35+
}
36+
}
37+
})
38+
expect(wrapper.html()).toContain('foo')
39+
40+
await wrapper.setProps({ obj: { foo: () => false } })
41+
expect(wrapper.html()).not.toContain('foo')
42+
})
43+
44+
it('sets component props, and updates DOM when props were not initially passed', async () => {
45+
const Foo = {
46+
props: ['foo'],
47+
template: `<div>{{ foo }}</div>`
48+
}
49+
const wrapper = mount(Foo)
50+
expect(wrapper.html()).not.toContain('foo')
51+
52+
await wrapper.setProps({ foo: 'foo' })
53+
54+
expect(wrapper.html()).toContain('foo')
55+
})
56+
57+
it('triggers a watcher', async () => {
58+
const Foo = {
59+
props: ['foo'],
60+
data() {
61+
return {
62+
bar: 'original-bar'
63+
}
64+
},
65+
watch: {
66+
foo(val: string) {
67+
this.bar = val
68+
}
69+
},
70+
template: `<div>{{ bar }}</div>`
71+
}
72+
const wrapper = mount(Foo)
73+
expect(wrapper.html()).toContain('original-bar')
74+
75+
await wrapper.setProps({ foo: 'updated-bar' })
76+
77+
expect(wrapper.html()).toContain('updated-bar')
78+
})
79+
80+
it('works with composition API', async () => {
81+
const Foo = defineComponent({
82+
props: {
83+
foo: { type: String }
84+
},
85+
setup(props) {
86+
const foobar = computed(() => `${props.foo}-bar`)
87+
return () =>
88+
h('div', `Foo is: ${props.foo}. Foobar is: ${foobar.value}`)
89+
}
90+
})
91+
const wrapper = mount(Foo, {
92+
props: {
93+
foo: 'foo'
94+
}
95+
})
96+
expect(wrapper.html()).toContain('Foo is: foo. Foobar is: foo-bar')
97+
98+
await wrapper.setProps({ foo: 'qux' })
99+
100+
expect(wrapper.html()).toContain('Foo is: qux. Foobar is: qux-bar')
101+
})
102+
103+
it('non-existent props are rendered as attributes', async () => {
104+
const Foo = {
105+
props: ['foo'],
106+
template: '<div>{{ foo }}</div>'
107+
}
108+
const wrapper = mount(Foo, {
109+
props: {
110+
foo: 'foo'
111+
}
112+
})
113+
expect(wrapper.html()).toContain('foo')
114+
115+
const nonExistentProp = { bar: 'qux' }
116+
await wrapper.setProps(nonExistentProp)
117+
118+
expect(wrapper.attributes()).toEqual(nonExistentProp)
119+
expect(wrapper.html()).toBe('<div bar="qux">foo</div>')
120+
})
121+
})

0 commit comments

Comments
 (0)