From bc9f13f6e9423a1140ea2220474c315ae9448ddd Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 10 Apr 2020 11:12:52 +1000 Subject: [PATCH 1/7] wip: setProps --- src/mount.ts | 33 ++++++++++++++++++++------------- tests/setProps.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 tests/setProps.spec.ts diff --git a/src/mount.ts b/src/mount.ts index 89f903646..e2faf5f99 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -9,7 +9,8 @@ import { Plugin, Directive, Component, - getCurrentInstance + getCurrentInstance, + reactive } from 'vue' import { VueWrapper, createWrapper } from './vue-wrapper' @@ -38,10 +39,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 @@ -70,16 +68,22 @@ export function mount

( component.mixins = [...(component.mixins || []), dataMixin] } + // @ts-ignore + const theProps = reactive({ ...options?.props }) + + function setProps(props: any) { + theProps.foo = props.foo + } + // 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, theProps, slots) + } + }) // create the vm - const vm = createApp(Parent(options && options.props)) + const vm = createApp(Parent) // global mocks mixin if (options?.global?.mocks) { @@ -129,5 +133,8 @@ export function mount

( // mount the app! const app = vm.mount(el) - return createWrapper(app, events) + return { + wrapper: createWrapper(app, events), + setProps + } } diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts new file mode 100644 index 000000000..79f4db2b7 --- /dev/null +++ b/tests/setProps.spec.ts @@ -0,0 +1,37 @@ +import { h, reactive } from 'vue' + +import { mount } from '../src' + +// const setProps = (wrapper: any, props: any) => { + +// } + +test.only('', async () => { + const Foo = { + props: ['foo'], + template: '

{{ foo }}
' + } + // const data = reactive({ + // foo: 'foo' + // }) + // const Wrapper = { + // components: { Foo }, + // data() { + // return data + // }, + // template: '
' + // } + + const { wrapper, setProps } = mount(Foo, { + props: { + foo: 'bar' + } + }) + + console.log(wrapper.html()) + setProps({ foo: 'qux' }) + // data.foo = 'bar' + await wrapper.vm.$nextTick() + + console.log(wrapper.html()) +}) From ca24da79f5eef6469672f95ba34afd3792a1733b Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 10 Apr 2020 12:02:39 +1000 Subject: [PATCH 2/7] wip: return nextTick from setProps --- src/mount.ts | 30 ++++++++++++++++------------ tests/setProps.spec.ts | 45 +++++++++++++----------------------------- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index e2faf5f99..b32cf7aa0 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -4,13 +4,12 @@ import { VNode, defineComponent, VNodeNormalizedChildren, - VNodeProps, ComponentOptions, Plugin, Directive, Component, - getCurrentInstance, - reactive + reactive, + nextTick } from 'vue' import { VueWrapper, createWrapper } from './vue-wrapper' @@ -20,9 +19,9 @@ 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 @@ -39,7 +38,7 @@ interface MountingOptions { stubs?: Record } -export function mount

(originalComponent: any, options?: MountingOptions

) { +export function mount(originalComponent: any, options?: MountingOptions) { const component = { ...originalComponent } // Reset the document.body @@ -68,20 +67,25 @@ export function mount

(originalComponent: any, options?: MountingOptions

) { component.mixins = [...(component.mixins || []), dataMixin] } - // @ts-ignore - const theProps = reactive({ ...options?.props }) - - function setProps(props: any) { - theProps.foo = props.foo - } + // 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 }) // create the wrapper component const Parent = defineComponent({ render() { - return h(component, theProps, slots) + 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) diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts index 79f4db2b7..046b37b99 100644 --- a/tests/setProps.spec.ts +++ b/tests/setProps.spec.ts @@ -1,37 +1,20 @@ -import { h, reactive } from 'vue' - import { mount } from '../src' -// const setProps = (wrapper: any, props: any) => { - -// } - -test.only('', async () => { - const Foo = { - props: ['foo'], - template: '

{{ foo }}
' - } - // const data = reactive({ - // foo: 'foo' - // }) - // const Wrapper = { - // components: { Foo }, - // data() { - // return data - // }, - // template: '
' - // } - - const { wrapper, setProps } = mount(Foo, { - props: { - foo: 'bar' +describe('setProps', () => { + it('updates a prop', async () => { + const Foo = { + props: ['foo'], + template: '
{{ foo }}
' } - }) - console.log(wrapper.html()) - setProps({ foo: 'qux' }) - // data.foo = 'bar' - await wrapper.vm.$nextTick() + const { wrapper, setProps } = mount(Foo, { + props: { + foo: 'foo' + } + }) + expect(wrapper.html()).toContain('foo') - console.log(wrapper.html()) + await setProps({ foo: 'qux' }) + expect(wrapper.html()).toContain('qux') + }) }) From 26e6a1e342d66686983e163e13e3363e7e993fe7 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 10 Apr 2020 12:32:03 +1000 Subject: [PATCH 3/7] tests: add tests for setProps and watcher --- tests/setProps.spec.ts | 61 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts index 046b37b99..540ef2036 100644 --- a/tests/setProps.spec.ts +++ b/tests/setProps.spec.ts @@ -1,12 +1,11 @@ import { mount } from '../src' describe('setProps', () => { - it('updates a prop', async () => { + it('updates a primitive prop', async () => { const Foo = { props: ['foo'], template: '
{{ foo }}
' } - const { wrapper, setProps } = mount(Foo, { props: { foo: 'foo' @@ -17,4 +16,62 @@ describe('setProps', () => { await setProps({ foo: 'qux' }) expect(wrapper.html()).toContain('qux') }) + + it('updates a function prop', async () => { + const Foo = { + props: ['obj'], + template: ` +
+
foo
+
+ ` + } + const { wrapper, setProps } = mount(Foo, { + props: { + obj: { + foo: () => true + } + } + }) + expect(wrapper.html()).toContain('foo') + + await 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, setProps } = mount(Foo) + expect(wrapper.html()).not.toContain('foo') + + await 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, setProps } = mount(Foo) + expect(wrapper.html()).toContain('original-bar') + + await setProps({ foo: 'updated-bar' }) + + expect(wrapper.html()).toContain('updated-bar') + }) }) From 1070598f1a6f2433ef1fa6d5ad25ea2197be5f0c Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Fri, 10 Apr 2020 12:41:21 +1000 Subject: [PATCH 4/7] tests: test setProps with composition API --- tests/setProps.spec.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts index 540ef2036..e1383e9ab 100644 --- a/tests/setProps.spec.ts +++ b/tests/setProps.spec.ts @@ -1,3 +1,5 @@ +import { defineComponent, h, computed } from 'vue' + import { mount } from '../src' describe('setProps', () => { @@ -74,4 +76,27 @@ describe('setProps', () => { 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, setProps } = mount(Foo, { + props: { + foo: 'foo' + } + }) + expect(wrapper.html()).toContain('Foo is: foo. Foobar is: foo-bar') + + await setProps({ foo: 'qux' }) + + expect(wrapper.html()).toContain('Foo is: qux. Foobar is: qux-bar') + }) }) From 59ba17f5024511c763b0cfb58623b044f2047e40 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Sat, 11 Apr 2020 11:29:59 +1000 Subject: [PATCH 5/7] feat: setProps --- src/mount.ts | 7 ++----- src/vue-wrapper.ts | 20 ++++++++++++++++---- tests/setProps.spec.ts | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index b32cf7aa0..bd0f19a4a 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -12,7 +12,7 @@ import { nextTick } 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' @@ -137,8 +137,5 @@ export function mount(originalComponent: any, options?: MountingOptions) { // mount the app! const app = vm.mount(el) - return { - wrapper: createWrapper(app, events), - setProps - } + 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 index e1383e9ab..b17ef85a3 100644 --- a/tests/setProps.spec.ts +++ b/tests/setProps.spec.ts @@ -8,14 +8,14 @@ describe('setProps', () => { props: ['foo'], template: '
{{ foo }}
' } - const { wrapper, setProps } = mount(Foo, { + const wrapper = mount(Foo, { props: { foo: 'foo' } }) expect(wrapper.html()).toContain('foo') - await setProps({ foo: 'qux' }) + await wrapper.setProps({ foo: 'qux' }) expect(wrapper.html()).toContain('qux') }) @@ -28,7 +28,7 @@ describe('setProps', () => { ` } - const { wrapper, setProps } = mount(Foo, { + const wrapper = mount(Foo, { props: { obj: { foo: () => true @@ -37,7 +37,7 @@ describe('setProps', () => { }) expect(wrapper.html()).toContain('foo') - await setProps({ obj: { foo: () => false } }) + await wrapper.setProps({ obj: { foo: () => false } }) expect(wrapper.html()).not.toContain('foo') }) @@ -46,10 +46,10 @@ describe('setProps', () => { props: ['foo'], template: `
{{ foo }}
` } - const { wrapper, setProps } = mount(Foo) + const wrapper = mount(Foo) expect(wrapper.html()).not.toContain('foo') - await setProps({ foo: 'foo' }) + await wrapper.setProps({ foo: 'foo' }) expect(wrapper.html()).toContain('foo') }) @@ -69,10 +69,10 @@ describe('setProps', () => { }, template: `
{{ bar }}
` } - const { wrapper, setProps } = mount(Foo) + const wrapper = mount(Foo) expect(wrapper.html()).toContain('original-bar') - await setProps({ foo: 'updated-bar' }) + await wrapper.setProps({ foo: 'updated-bar' }) expect(wrapper.html()).toContain('updated-bar') }) @@ -88,14 +88,14 @@ describe('setProps', () => { h('div', `Foo is: ${props.foo}. Foobar is: ${foobar.value}`) } }) - const { wrapper, setProps } = mount(Foo, { + const wrapper = mount(Foo, { props: { foo: 'foo' } }) expect(wrapper.html()).toContain('Foo is: foo. Foobar is: foo-bar') - await setProps({ foo: 'qux' }) + await wrapper.setProps({ foo: 'qux' }) expect(wrapper.html()).toContain('Foo is: qux. Foobar is: qux-bar') }) From 349ff16cf693e7c23043ef4b34db7fa45ea36d9f Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Sat, 11 Apr 2020 11:37:19 +1000 Subject: [PATCH 6/7] refactor: add VTU ref back --- src/mount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mount.ts b/src/mount.ts index bd0f19a4a..fafad35e3 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -69,7 +69,7 @@ export function mount(originalComponent: any, options?: MountingOptions) { // 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 }) + const props = reactive({ ...options?.props, ref: 'VTU_COMPONENT' }) // create the wrapper component const Parent = defineComponent({ From 4c21157b3ad6732ff168312a458c8878af382e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Fontcuberta?= Date: Sat, 11 Apr 2020 09:04:10 +0200 Subject: [PATCH 7/7] Add test for non-exitent props --- tests/setProps.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/setProps.spec.ts b/tests/setProps.spec.ts index b17ef85a3..c1ffb81fc 100644 --- a/tests/setProps.spec.ts +++ b/tests/setProps.spec.ts @@ -99,4 +99,23 @@ describe('setProps', () => { 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
') + }) })