From 4f5ae12d703ff700ac4cb89ffd2bfa0823412e54 Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Tue, 28 Apr 2020 16:35:20 +0200 Subject: [PATCH 1/3] feat: split attrs and props mounting options This allows a slightly better type checking, even if `defineComponent({ props: { a: String }})` sadly has `$props` typed as `{ a: string } & VNodeProps` and `VNodeProps` allows anything. I'm not sure we can do much on VTU side for now. --- README.md | 2 +- src/mount.ts | 33 ++++++++++++--------------- src/vue-shims.d.ts | 5 ----- test-dts/mount.d-test.ts | 31 ++++++++++++++++--------- tests/mountingOptions/props.spec.ts | 35 ++++++++++++++++------------- 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 68a52bdbf..efbd8c5fa 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ component | ✅ | (new!) nested in [`global`](https://vuejs.github.io/vue-test-u directives | ✅ | (new!) nested in [`global`](https://vuejs.github.io/vue-test-utils-next-docs/api/#global) stubs | ✅ attachToDocument |✅| renamed `attachTo`. See [here](https://github.com/vuejs/vue-test-utils/pull/1492) -attrs | ⚰️ | use `props` instead, it assigns both attributes and props. +attrs | ✅ scopedSlots | ⚰️ | scopedSlots are merged with slots in Vue 3 context | ⚰️ | different from Vue 2, does not make sense anymore. localVue | ⚰️ | may not make sense anymore since we do not mutate the global Vue instance in Vue 3. diff --git a/src/mount.ts b/src/mount.ts index c93cdb6a3..086653152 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -32,6 +32,7 @@ type Slot = VNode | string | { render: Function } interface MountingOptions { data?: () => Record props?: Props + attrs?: Record slots?: { default?: Slot [key: string]: Slot @@ -47,37 +48,27 @@ type ExtractComponent = T extends { new (): infer PublicInstance } : any // Component declared with defineComponent -export function mount< - TestedComponent extends ComponentPublicInstance, - PublicProps extends TestedComponent['$props'] ->( +export function mount( originalComponent: { new (): TestedComponent } & Component, - options?: MountingOptions + options?: MountingOptions ): VueWrapper // Component declared with { props: { ... } } -export function mount< - TestedComponent extends ComponentOptionsWithObjectProps, - PublicProps extends ExtractPropTypes ->( +export function mount( originalComponent: TestedComponent, - options?: MountingOptions + options?: MountingOptions> ): VueWrapper> // Component declared with { props: [] } -export function mount< - TestedComponent extends ComponentOptionsWithArrayProps, - PublicProps extends Record ->( +export function mount( originalComponent: TestedComponent, - options?: MountingOptions + options?: MountingOptions> ): VueWrapper> // Component declared with no props export function mount< TestedComponent extends ComponentOptionsWithoutProps, - ComponentT extends ComponentOptionsWithoutProps & {}, - PublicProps extends Record + ComponentT extends ComponentOptionsWithoutProps & {} >( originalComponent: ComponentT extends { new (): any } ? never : ComponentT, - options?: MountingOptions + options?: MountingOptions ): VueWrapper> export function mount( originalComponent: any, @@ -121,7 +112,11 @@ export function mount( // 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: MOUNT_COMPONENT_REF }) + const props = reactive({ + ...options?.attrs, + ...options?.props, + ref: MOUNT_COMPONENT_REF + }) // create the wrapper component const Parent = defineComponent({ diff --git a/src/vue-shims.d.ts b/src/vue-shims.d.ts index f33825234..e875bbea9 100644 --- a/src/vue-shims.d.ts +++ b/src/vue-shims.d.ts @@ -3,8 +3,3 @@ declare module '*.vue' { import Vue from 'vue' export default any } - -declare module 'vue' { - import Vue from 'vue/dist/vue' - export = Vue -} diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index e5a2daea6..9155d09ac 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -7,21 +7,25 @@ const AppWithDefine = defineComponent({ a: { type: String, required: true - } + }, + b: Number }, template: '' }) // accept props let wrapper = mount(AppWithDefine, { - props: { a: 'Hello' } + props: { a: 'Hello', b: 2 } }) // vm is properly typed expectType(wrapper.vm.a) // can receive extra props +// ideally, it should not +// but the props have type { a: string } & VNodeProps +// which allows any property mount(AppWithDefine, { - props: { a: 'Hello', b: 2 } + props: { a: 'Hello', c: 2 } }) // wrong prop type should not compile @@ -48,10 +52,12 @@ wrapper = mount(AppWithProps, { // vm is properly typed expectType(wrapper.vm.a) -// can receive extra props -mount(AppWithProps, { - props: { a: 'Hello', b: 2 } -}) +// can't receive extra props +expectError( + mount(AppWithProps, { + props: { a: 'Hello', b: 2 } + }) +) // wrong prop type should not compile expectError( @@ -73,6 +79,7 @@ wrapper = mount(AppWithArrayProps, { expectType(wrapper.vm.a) // can receive extra props +// as they are declared as `string[]` mount(AppWithArrayProps, { props: { a: 'Hello', b: 2 } }) @@ -81,7 +88,9 @@ const AppWithoutProps = { template: '' } -// can receive extra props -wrapper = mount(AppWithoutProps, { - props: { b: 'Hello' } -}) +// can't receive extra props +expectError( + (wrapper = mount(AppWithoutProps, { + props: { b: 'Hello' } + })) +) diff --git a/tests/mountingOptions/props.spec.ts b/tests/mountingOptions/props.spec.ts index e743aedd7..66890f28c 100644 --- a/tests/mountingOptions/props.spec.ts +++ b/tests/mountingOptions/props.spec.ts @@ -1,22 +1,23 @@ import { defineComponent, h } from 'vue' -import WithProps from '../components/WithProps.vue' import { mount } from '../../src' describe('mountingOptions.props', () => { - test('passes props', () => { - const Component = defineComponent({ - props: { - message: { - type: String, - required: true - } + const Component = defineComponent({ + props: { + message: { + type: String, + required: true }, - - render() { - return h('div', {}, `Message is ${this.message}`) + otherMessage: { + type: String } - }) + }, + render() { + return h('div', {}, `Message is ${this.message}`) + } + }) + test('passes props', () => { const wrapper = mount(Component, { props: { message: 'Hello' @@ -26,12 +27,14 @@ describe('mountingOptions.props', () => { }) test('assigns extra attributes on components', () => { - const wrapper = mount(WithProps, { + const wrapper = mount(Component, { props: { + message: 'Hello World' + }, + attrs: { class: 'HelloFromTheOtherSide', id: 'hello', - disabled: true, - msg: 'Hello World' + disabled: true } }) @@ -42,7 +45,7 @@ describe('mountingOptions.props', () => { }) expect(wrapper.props()).toEqual({ - msg: 'Hello World' + message: 'Hello World' }) }) From d271a124761bdc38308329a334fae1e9d2513dfc Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Fri, 1 May 2020 15:16:56 +0200 Subject: [PATCH 2/3] test: split moutingProps.attrs tests --- tests/mountingOptions/attrs.spec.ts | 57 +++++++++++++++++++++++++++++ tests/mountingOptions/props.spec.ts | 31 +++++----------- 2 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 tests/mountingOptions/attrs.spec.ts diff --git a/tests/mountingOptions/attrs.spec.ts b/tests/mountingOptions/attrs.spec.ts new file mode 100644 index 000000000..4fd57d7ef --- /dev/null +++ b/tests/mountingOptions/attrs.spec.ts @@ -0,0 +1,57 @@ +import { defineComponent, h } from 'vue' +import { mount } from '../../src' + +describe('mountingOptions.attrs', () => { + const Component = defineComponent({ + props: { + message: { + type: String, + required: true + }, + otherMessage: { + type: String + } + }, + + render() { + return h('div', {}, `Message is ${this.message}`) + } + }) + + test('assigns extra attributes on components', () => { + const wrapper = mount(Component, { + props: { + message: 'Hello World' + }, + attrs: { + class: 'HelloFromTheOtherSide', + id: 'hello', + disabled: true + } + }) + + expect(wrapper.attributes()).toEqual({ + class: 'HelloFromTheOtherSide', + disabled: 'true', + id: 'hello' + }) + + expect(wrapper.props()).toEqual({ + message: 'Hello World' + }) + }) + + test('assigns event listeners', async () => { + const Component = { + template: '' + } + const onCustomEvent = jest.fn() + const wrapper = mount(Component, { attrs: { onCustomEvent } }) + const button = wrapper.find('button') + await button.trigger('click') + await button.trigger('click') + await button.trigger('click') + + expect(onCustomEvent).toHaveBeenCalledTimes(3) + }) +}) diff --git a/tests/mountingOptions/props.spec.ts b/tests/mountingOptions/props.spec.ts index 66890f28c..a5289f5c2 100644 --- a/tests/mountingOptions/props.spec.ts +++ b/tests/mountingOptions/props.spec.ts @@ -26,40 +26,27 @@ describe('mountingOptions.props', () => { expect(wrapper.text()).toBe('Message is Hello') }) - test('assigns extra attributes on components', () => { + test('assigns extra properties as attributes on components', () => { + // the recommended way is to use `attrs` though + // and ideally it should not even compile, but props is too loosely typed + // for components defined with `defineComponent` const wrapper = mount(Component, { props: { - message: 'Hello World' - }, - attrs: { + message: 'Hello World', class: 'HelloFromTheOtherSide', id: 'hello', disabled: true } }) + expect(wrapper.props()).toEqual({ + message: 'Hello World' + }) + expect(wrapper.attributes()).toEqual({ class: 'HelloFromTheOtherSide', disabled: 'true', id: 'hello' }) - - expect(wrapper.props()).toEqual({ - message: 'Hello World' - }) - }) - - test('assigns event listeners', async () => { - const Component = { - template: '' - } - const onCustomEvent = jest.fn() - const wrapper = mount(Component, { props: { onCustomEvent } }) - const button = wrapper.find('button') - await button.trigger('click') - await button.trigger('click') - await button.trigger('click') - - expect(onCustomEvent).toHaveBeenCalledTimes(3) }) }) From ff1a72cdc992df170fbb78b6f78e71bfbc002ffa Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Sat, 2 May 2020 11:59:26 +0200 Subject: [PATCH 3/3] test: extra mouting props can be given to props if cast --- test-dts/mount.d-test.ts | 5 +++++ tests/mountingOptions/attrs.spec.ts | 27 +++++++++++++++------------ tests/mountingOptions/props.spec.ts | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index 9155d09ac..eda62aace 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -94,3 +94,8 @@ expectError( props: { b: 'Hello' } })) ) + +// except if explicitly cast +mount(AppWithoutProps, { + props: { b: 'Hello' } as never +}) diff --git a/tests/mountingOptions/attrs.spec.ts b/tests/mountingOptions/attrs.spec.ts index 4fd57d7ef..56d6691ed 100644 --- a/tests/mountingOptions/attrs.spec.ts +++ b/tests/mountingOptions/attrs.spec.ts @@ -41,17 +41,20 @@ describe('mountingOptions.attrs', () => { }) }) - test('assigns event listeners', async () => { - const Component = { - template: '' - } - const onCustomEvent = jest.fn() - const wrapper = mount(Component, { attrs: { onCustomEvent } }) - const button = wrapper.find('button') - await button.trigger('click') - await button.trigger('click') - await button.trigger('click') - - expect(onCustomEvent).toHaveBeenCalledTimes(3) + test('is overridden by a prop with the same name', () => { + const wrapper = mount(Component, { + props: { + message: 'Hello World' + }, + attrs: { + message: 'HelloFromTheOtherSide' + } + }) + + expect(wrapper.props()).toEqual({ + message: 'Hello World' + }) + + expect(wrapper.attributes()).toEqual({}) }) }) diff --git a/tests/mountingOptions/props.spec.ts b/tests/mountingOptions/props.spec.ts index a5289f5c2..4b5b34fce 100644 --- a/tests/mountingOptions/props.spec.ts +++ b/tests/mountingOptions/props.spec.ts @@ -49,4 +49,19 @@ describe('mountingOptions.props', () => { id: 'hello' }) }) + + test('assigns event listeners', async () => { + const Component = { + template: '' + } + const onCustomEvent = jest.fn() + // Note that, as the component does not have any props declared, we need to cast the mounting props + const wrapper = mount(Component, { props: { onCustomEvent } as never }) + const button = wrapper.find('button') + await button.trigger('click') + await button.trigger('click') + await button.trigger('click') + + expect(onCustomEvent).toHaveBeenCalledTimes(3) + }) })