Skip to content

Commit 0e34fc6

Browse files
authored
feat: setData()/shallowMount with initialData for components using the Composition API / <script setup> (#2655)
```vue <script setup lang="ts"> import { ref } from 'vue' const count = ref(0) const inc = () => { count.value++ } </script> <template> <button @click="inc()">{{ count }}</button> </template> ``` Can now be tested like this: ```ts it('updates data on a component using <script setup>', async () => { const wrapper = shallowMount(ScriptSetup) await wrapper.setData({ count: 20 }) expect(wrapper.html()).toContain('20') }); ```
1 parent 8e9a119 commit 0e34fc6

File tree

3 files changed

+126
-23
lines changed

3 files changed

+126
-23
lines changed

src/createInstance.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
ComponentOptions,
1010
ConcreteComponent,
1111
DefineComponent,
12-
transformVNodeArgs
12+
transformVNodeArgs,
13+
proxyRefs
1314
} from 'vue'
1415

1516
import { MountingOptions, Slot } from './types'
@@ -20,6 +21,7 @@ import {
2021
isObject,
2122
isObjectComponent,
2223
isScriptSetup,
24+
mergeDeep,
2325
mergeGlobalProperties
2426
} from './utils'
2527
import { processSlot } from './utils/compileSlots'
@@ -153,11 +155,22 @@ export function createInstance(
153155
if (isObjectComponent(originalComponent)) {
154156
// component is guaranteed to be the same type as originalComponent
155157
const objectComponent = component as ComponentOptions
156-
const originalDataFn = originalComponent.data || (() => ({}))
157-
objectComponent.data = (vm) => ({
158-
...originalDataFn.call(vm, vm),
159-
...providedData
160-
})
158+
if (originalComponent.data) {
159+
const originalDataFn = originalComponent.data
160+
objectComponent.data = (vm) => ({
161+
...originalDataFn.call(vm, vm),
162+
...providedData
163+
})
164+
} else if (objectComponent.setup) {
165+
const originalSetupFn = objectComponent.setup
166+
objectComponent.setup = function (a, b) {
167+
const data = originalSetupFn(a, b)
168+
mergeDeep(proxyRefs(data), providedData)
169+
return data
170+
}
171+
} else {
172+
objectComponent.data = () => ({ ...providedData })
173+
}
161174
} else {
162175
throw new Error(
163176
'data() option is not supported on functional and class components'

src/vueWrapper.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { nextTick, App, ComponentPublicInstance, VNode } from 'vue'
1+
import { nextTick, App, ComponentPublicInstance, VNode, proxyRefs } from 'vue'
22

33
import { config } from './config'
44
import domEvents from './constants/dom-events'
@@ -247,7 +247,29 @@ export class VueWrapper<
247247
}
248248

249249
setData(data: Record<string, unknown>): Promise<void> {
250-
mergeDeep(this.componentVM.$data, data)
250+
/*
251+
Depending on how the component was defined, data can live in different places
252+
Vue sets some default placeholder in all the locations however, so we cannot just check
253+
if the data object exists or not.
254+
When using <script setup>, data lives in the setupState object, which is then marked with __isScriptSetup
255+
When using the setup() function, data lives in the setupState object, but is not marked with __isScriptSetup
256+
When using the object api, data lives in the data object, proxied through $data, HOWEVER
257+
the setupState object will also exist, and be frozen.
258+
*/
259+
// @ts-ignore
260+
if (this.componentVM.$.setupState.__isScriptSetup) {
261+
// data from <script setup>
262+
// @ts-ignore
263+
mergeDeep(this.componentVM.$.setupState, data)
264+
// @ts-ignore
265+
} else if (!Object.isFrozen(this.componentVM.$.setupState)) {
266+
// data from setup() function when using the object api
267+
// @ts-ignore
268+
mergeDeep(proxyRefs(this.componentVM.$.setupState), data)
269+
} else {
270+
// data when using data: {...} in the object api
271+
mergeDeep(this.componentVM.$data, data)
272+
}
251273
return nextTick()
252274
}
253275

tests/setData.spec.ts

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, expect, it, vi } from 'vitest'
22
import { defineComponent, ref } from 'vue'
33

4-
import { mount } from '../src'
4+
import { mount, shallowMount } from '../src'
5+
import ScriptSetup from './components/ScriptSetup.vue'
56

67
describe('setData', () => {
78
it('sets component data', async () => {
@@ -111,20 +112,6 @@ describe('setData', () => {
111112
)
112113
})
113114

114-
it('does not modify composition API setup data', async () => {
115-
const Component = defineComponent({
116-
template: `<div>Count is: {{ count }}</div>`,
117-
setup: () => ({ count: ref(1) })
118-
})
119-
const wrapper = mount(Component)
120-
121-
expect(wrapper.html()).toContain('Count is: 1')
122-
123-
expect(() => wrapper.setData({ count: 2 })).toThrowError(
124-
'Cannot add property count'
125-
)
126-
})
127-
128115
// https://github.com/vuejs/test-utils/issues/538
129116
it('updates data set via data mounting option using setData', async () => {
130117
const Comp = defineComponent({
@@ -266,4 +253,85 @@ describe('setData', () => {
266253
secondArray: [3, 4]
267254
})
268255
})
256+
257+
it('updates initial data on a component using <script setup>', async () => {
258+
const wrapper = shallowMount(ScriptSetup, {
259+
data() {
260+
return {
261+
count: 10
262+
}
263+
}
264+
})
265+
expect(wrapper.html()).toContain('10')
266+
})
267+
268+
it('updates nested data returned from setup()', async () => {
269+
const Component = {
270+
template: `<div>{{ nested.property }}</div>`,
271+
setup: () => ({
272+
nested: ref({
273+
property: 'initial value'
274+
})
275+
})
276+
}
277+
278+
const wrapper = mount(Component)
279+
280+
expect(wrapper.html()).toContain('initial value')
281+
282+
await wrapper.setData({ nested: { property: 'updated value' } })
283+
284+
expect(wrapper.html()).toContain('updated value')
285+
})
286+
287+
it('updates data on a component using <script setup>', async () => {
288+
const wrapper = shallowMount(ScriptSetup)
289+
await wrapper.setData({ count: 20 })
290+
expect(wrapper.html()).toContain('20')
291+
})
292+
293+
it('updates data on a component using setup()', async () => {
294+
const Component = {
295+
template: `<div><div v-if="show" id="show">Show</div></div>`,
296+
setup() {
297+
return {
298+
show: ref(false)
299+
}
300+
}
301+
}
302+
303+
const wrapper = mount(Component)
304+
305+
expect(wrapper.find('#show').exists()).toBe(false)
306+
307+
await wrapper.setData({ show: true })
308+
309+
expect(wrapper.find('#show').exists()).toBe(true)
310+
})
311+
312+
it('correctly sets initial array refs data when using <script setup>', async () => {
313+
const Component = {
314+
template: `<div><div v-for="item in items" :key="item">{{ item }}</div></div>`,
315+
setup() {
316+
const items = ref([])
317+
return { items }
318+
}
319+
}
320+
321+
const wrapper = shallowMount(Component, {
322+
data() {
323+
return {
324+
items: ['item1', 'item2']
325+
}
326+
}
327+
})
328+
329+
expect(wrapper.html()).toContain('item1')
330+
expect(wrapper.html()).toContain('item2')
331+
332+
await wrapper.setData({ items: ['item3', 'item4'] })
333+
334+
expect(wrapper.html()).toContain('item3')
335+
expect(wrapper.html()).toContain('item4')
336+
})
269337
})

0 commit comments

Comments
 (0)