diff --git a/src/mount.ts b/src/mount.ts index a6db298e3..97b84104f 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -4,23 +4,23 @@ import { VNode, defineComponent, VNodeNormalizedChildren, - VNodeProps, ComponentOptions, Plugin, Directive, - Component + Component, + reactive } from 'vue' -import { VueWrapper, createWrapper } from './vue-wrapper' +import { createWrapper } from './vue-wrapper' import { createEmitMixin } from './emitMixin' import { createDataMixin } from './dataMixin' import { MOUNT_ELEMENT_ID } from './constants' type Slot = VNode | string | { render: Function } -interface MountingOptions { +interface MountingOptions { data?: () => Record - props?: Props + props?: Record slots?: { default?: Slot [key: string]: Slot @@ -37,10 +37,7 @@ interface MountingOptions { stubs?: Record } -export function mount

( - originalComponent: any, - options?: MountingOptions

-): VueWrapper { +export function mount(originalComponent: any, options?: MountingOptions) { const component = { ...originalComponent } // Reset the document.body @@ -69,16 +66,27 @@ export function mount

( component.mixins = [...(component.mixins || []), dataMixin] } + // we define props as reactive so that way when we update them with `setProps` + // Vue's reactivity system will cause a rerender. + const props = reactive({ ...options?.props, ref: 'VTU_COMPONENT' }) + // create the wrapper component - const Parent = (props?: VNodeProps) => - defineComponent({ - render() { - return h(component, { ...props, ref: 'VTU_COMPONENT' }, slots) - } - }) + const Parent = defineComponent({ + render() { + return h(component, props, slots) + } + }) + + const setProps = (newProps: Record) => { + for (const [k, v] of Object.entries(newProps)) { + props[k] = v + } + + return app.$nextTick() + } // create the vm - const vm = createApp(Parent(options && options.props)) + const vm = createApp(Parent) // global mocks mixin if (options?.global?.mocks) { @@ -128,5 +136,5 @@ export function mount

( // mount the app! const app = vm.mount(el) - return createWrapper(app, events) + return createWrapper(app, events, setProps) } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 93f99ff03..11d6249b0 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -1,4 +1,4 @@ -import { ComponentPublicInstance } from 'vue' +import { ComponentPublicInstance, nextTick } from 'vue' import { ShapeFlags } from '@vue/shared' import { DOMWrapper } from './dom-wrapper' @@ -10,9 +10,15 @@ export class VueWrapper implements WrapperAPI { private componentVM: ComponentPublicInstance private __emitted: Record = {} private __vm: ComponentPublicInstance + private __setProps: (props: Record) => void - constructor(vm: ComponentPublicInstance, events: Record) { + constructor( + vm: ComponentPublicInstance, + events: Record, + setProps: (props: Record) => void + ) { this.__vm = vm + this.__setProps = setProps this.componentVM = this.vm.$refs['VTU_COMPONENT'] as ComponentPublicInstance this.__emitted = events } @@ -78,6 +84,11 @@ export class VueWrapper implements WrapperAPI { return Array.from(results).map((x) => new DOMWrapper(x)) } + setProps(props: Record) { + this.__setProps(props) + return nextTick() + } + trigger(eventString: string) { const rootElementWrapper = new DOMWrapper(this.element) return rootElementWrapper.trigger(eventString) @@ -86,7 +97,8 @@ export class VueWrapper implements WrapperAPI { export function createWrapper( vm: ComponentPublicInstance, - events: Record + events: Record, + setProps: (props: Record) => void ): VueWrapper { - return new VueWrapper(vm, events) + return new VueWrapper(vm, events, setProps) } diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts new file mode 100644 index 000000000..c1ffb81fc --- /dev/null +++ b/tests/setProps.spec.ts @@ -0,0 +1,121 @@ +import { defineComponent, h, computed } from 'vue' + +import { mount } from '../src' + +describe('setProps', () => { + it('updates a primitive prop', async () => { + const Foo = { + props: ['foo'], + template: '

{{ foo }}
' + } + const wrapper = mount(Foo, { + props: { + foo: 'foo' + } + }) + expect(wrapper.html()).toContain('foo') + + await wrapper.setProps({ foo: 'qux' }) + expect(wrapper.html()).toContain('qux') + }) + + it('updates a function prop', async () => { + const Foo = { + props: ['obj'], + template: ` +
+
foo
+
+ ` + } + const wrapper = mount(Foo, { + props: { + obj: { + foo: () => true + } + } + }) + expect(wrapper.html()).toContain('foo') + + await wrapper.setProps({ obj: { foo: () => false } }) + expect(wrapper.html()).not.toContain('foo') + }) + + it('sets component props, and updates DOM when props were not initially passed', async () => { + const Foo = { + props: ['foo'], + template: `
{{ foo }}
` + } + const wrapper = mount(Foo) + expect(wrapper.html()).not.toContain('foo') + + await wrapper.setProps({ foo: 'foo' }) + + expect(wrapper.html()).toContain('foo') + }) + + it('triggers a watcher', async () => { + const Foo = { + props: ['foo'], + data() { + return { + bar: 'original-bar' + } + }, + watch: { + foo(val: string) { + this.bar = val + } + }, + template: `
{{ bar }}
` + } + const wrapper = mount(Foo) + expect(wrapper.html()).toContain('original-bar') + + await wrapper.setProps({ foo: 'updated-bar' }) + + expect(wrapper.html()).toContain('updated-bar') + }) + + it('works with composition API', async () => { + const Foo = defineComponent({ + props: { + foo: { type: String } + }, + setup(props) { + const foobar = computed(() => `${props.foo}-bar`) + return () => + h('div', `Foo is: ${props.foo}. Foobar is: ${foobar.value}`) + } + }) + const wrapper = mount(Foo, { + props: { + foo: 'foo' + } + }) + expect(wrapper.html()).toContain('Foo is: foo. Foobar is: foo-bar') + + await wrapper.setProps({ foo: 'qux' }) + + expect(wrapper.html()).toContain('Foo is: qux. Foobar is: qux-bar') + }) + + it('non-existent props are rendered as attributes', async () => { + const Foo = { + props: ['foo'], + template: '
{{ foo }}
' + } + const wrapper = mount(Foo, { + props: { + foo: 'foo' + } + }) + expect(wrapper.html()).toContain('foo') + + const nonExistentProp = { bar: 'qux' } + await wrapper.setProps(nonExistentProp) + + expect(wrapper.attributes()).toEqual(nonExistentProp) + expect(wrapper.html()).toBe('
foo
') + }) +})