From 34adf55de62807f4f1f80d2272de7e7efef91525 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Apr 2020 18:58:03 +0200 Subject: [PATCH 1/6] feat: add modifiers to trigger function --- package.json | 1 + src/create-dom-event.ts | 96 +++++++++++++++++++++++ src/dom-wrapper.ts | 10 +-- src/types.ts | 2 +- src/vue-wrapper.ts | 4 +- tests/trigger.spec.ts | 165 +++++++++++++++++++++++++++++++++------- yarn.lock | 5 ++ 7 files changed, 247 insertions(+), 36 deletions(-) create mode 100644 src/create-dom-event.ts diff --git a/package.json b/package.json index e72668204..39204980d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@vue/compiler-sfc": "^3.0.0-alpha.13", "babel-jest": "^25.2.3", "babel-preset-jest": "^25.2.1", + "dom-event-types": "^1.0.0", "flush-promises": "^1.0.2", "husky": "^4.2.3", "jest": "^25.1.0", diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts new file mode 100644 index 000000000..8c8119d96 --- /dev/null +++ b/src/create-dom-event.ts @@ -0,0 +1,96 @@ +import eventTypes from 'dom-event-types' + +const keyCodesByKeyName = { + backspace: 8, + tab: 9, + enter: 13, + esc: 27, + space: 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + insert: 45, + delete: 46 +} + +function getEventProperties(eventParams) { + const { modifier, meta, options } = eventParams + const keyCode = keyCodesByKeyName[modifier] || options.key || options.keyCode || options.code + + return { + ...options, // What the user passed in as the second argument to #trigger + bubbles: meta.bubbles, + meta: meta.cancelable, + // Any derived options should go here + key: keyCode, + keyCode, + code: keyCode + } +} + +function createEvent(eventParams) { + const { eventType, meta } = eventParams + const metaEventInterface = window[meta.eventInterface] + + const SupportedEventInterface = + typeof metaEventInterface === 'function' + ? metaEventInterface + : window.Event + + const eventProperties = getEventProperties(eventParams) + + const event = new SupportedEventInterface( + eventType, + // event properties can only be added when the event is instantiated + // custom properties must be added after the event has been instantiated + eventProperties + ) + + return event +} + +function createEventForOldBrowsers(eventParams) { + const { eventType, modifier, meta } = eventParams + const { bubbles, cancelable } = meta + + const event = document.createEvent('Event') + event.initEvent(eventType, bubbles, cancelable) + event['keyCode'] = keyCodesByKeyName[modifier] + return event +} + +export default function createDOMEvent(eventString: String, options: Object = {}) { + const [eventType, modifier] = eventString.split('.') + const meta = eventTypes[eventType] + || { eventInterface: 'Event', cancelable: true, bubbles: true } + + const eventParams = { eventType, modifier, meta, options } + + const event = + typeof window.Event === 'function' + ? createEvent(eventParams) + : createEventForOldBrowsers(eventParams) // Fallback for IE10,11 - https://stackoverflow.com/questions/26596123 + + const eventPrototype = Object.getPrototypeOf(event) + + Object.keys(options).forEach(key => { + const propertyDescriptor = Object.getOwnPropertyDescriptor( + eventPrototype, + key + ) + + const canSetProperty = !( + propertyDescriptor && propertyDescriptor.set === undefined + ) + if (canSetProperty) { + event[key] = options[key] + } + }) + + return event +} diff --git a/src/dom-wrapper.ts b/src/dom-wrapper.ts index c7b6d22fb..ae12dffad 100644 --- a/src/dom-wrapper.ts +++ b/src/dom-wrapper.ts @@ -3,6 +3,8 @@ import { nextTick } from 'vue' import { WrapperAPI } from './types' import { ErrorWrapper } from './error-wrapper' +import createDOMEvent from './create-dom-event' + export class DOMWrapper implements WrapperAPI { element: ElementType @@ -134,12 +136,10 @@ export class DOMWrapper implements WrapperAPI { return new DOMWrapper(parentElement).trigger('change') } - async trigger(eventString: string) { - const evt = document.createEvent('Event') - evt.initEvent(eventString) - + async trigger(eventString: string, options: Object = {}) { if (this.element) { - this.element.dispatchEvent(evt) + const event = createDOMEvent(eventString, options) + this.element.dispatchEvent(event) } return nextTick diff --git a/src/types.ts b/src/types.ts index d1e8f2cff..638e6f33a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,5 +10,5 @@ export interface WrapperAPI { findAll(selector: string): DOMWrapper[] html: () => string text: () => string - trigger: (eventString: string) => Promise<(fn?: () => void) => Promise> + trigger: (eventString: string, options?: Object) => Promise<(fn?: () => void) => Promise> } diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 0e857cca7..4fd7fb13f 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -105,9 +105,9 @@ export class VueWrapper return nextTick() } - trigger(eventString: string) { + trigger(eventString: string, options: Object = {}) { const rootElementWrapper = new DOMWrapper(this.element) - return rootElementWrapper.trigger(eventString) + return rootElementWrapper.trigger(eventString, options) } } diff --git a/tests/trigger.spec.ts b/tests/trigger.spec.ts index e2d148cb3..9a7422010 100644 --- a/tests/trigger.spec.ts +++ b/tests/trigger.spec.ts @@ -3,44 +3,153 @@ import { defineComponent, h, ref } from 'vue' import { mount } from '../src' describe('trigger', () => { - it('works on the root element', async () => { - const Component = defineComponent({ - setup() { - return { - count: ref(0) - } - }, - render() { - return h('div', { onClick: () => this.count++ }, `Count: ${this.count}`) - } + describe('on click', () => { + it('works on the root element', async () => { + const Component = defineComponent({ + setup() { + return { + count: ref(0) + } + }, + + render() { + return h('div', { onClick: () => this.count++ }, `Count: ${this.count}`) + } + }) + + const wrapper = mount(Component) + await wrapper.trigger('click') + + expect(wrapper.text()).toBe('Count: 1') + }) + + it('works on a nested element', async () => { + const Component = defineComponent({ + setup() { + return { + count: ref(0) + } + }, + + render() { + return h('div', {}, [ + h('p', {}, `Count: ${this.count}`), + h('button', { onClick: () => this.count++ }) + ]) + } + }) + + const wrapper = mount(Component) + await wrapper.find('button').trigger('click') + + expect(wrapper.find('p').text()).toBe('Count: 1') }) - const wrapper = mount(Component) - await wrapper.trigger('click') + it('causes DOM to update after a click handler method that changes components data is called', async () => { + const Component = defineComponent({ + setup() { + return { + isActive: ref(false) + } + }, - expect(wrapper.text()).toBe('Count: 1') + render() { + return h('div', { + onClick: () => this.isActive = !this.isActive, + class: { active: this.isActive } + }) + } + }) + const wrapper = mount(Component, {}) + + expect(wrapper.classes()).not.toContain('active') + await wrapper.trigger('click') + expect(wrapper.classes()).toContain('active') + }) }) - it('works on a nested element', async () => { - const Component = defineComponent({ - setup() { - return { - count: ref(0) - } - }, + describe('on keydown', () => { + it('causes keydown handler to fire when "keydown" is triggered', async () => { + const keydownHandler = jest.fn() + const Component = { + template: '', + methods: { + keydownHandler + }, + } + const wrapper = mount(Component, {}) + await wrapper.trigger('keydown') + + expect(keydownHandler).toHaveBeenCalledTimes(1) + }) + + it('causes keydown handler to fire when "keydown.enter" is triggered', async () => { + const keydownHandler = jest.fn() + const Component = { + template: '', + methods: { + keydownHandler + }, + } + const wrapper = mount(Component, {}) + await wrapper.trigger('keydown', { key: 'Enter' }) + + expect(keydownHandler).toHaveBeenCalledTimes(1) + }) - render() { - return h('div', {}, [ - h('p', {}, `Count: ${this.count}`), - h('button', { onClick: () => this.count++ }) - ]) + it('causes keydown handler to fire with the appropiate keyCode when wrapper.trigger("keydown", { keyCode: 65 }) is fired', async () => { + const keydownHandler = jest.fn() + const Component = { + template: '', + methods: { + keydownHandler + }, } + const wrapper = mount(Component, {}) + await wrapper.trigger('keydown', { keyCode: 65 }) + + expect(keydownHandler).toHaveBeenCalledTimes(1) + expect(keydownHandler.mock.calls[0][0]['keyCode']).toBe(65) }) - const wrapper = mount(Component) - await wrapper.find('button').trigger('click') + it('causes keydown handler to fire converting keyName in an apropiate keyCode when wrapper.trigger("keydown.${keyName}") is fired', async () => { + let keydownHandler = jest.fn() + + const keyCodesByKeyName = { + backspace: 8, + tab: 9, + enter: 13, + esc: 27, + space: 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40, + insert: 45, + delete: 46 + } - expect(wrapper.find('p').text()).toBe('Count: 1') + const Component = { + template: '', + methods: { + keydownHandler + }, + } + const wrapper = mount(Component, {}) + + for (const keyName in keyCodesByKeyName) { + const keyCode = keyCodesByKeyName[keyName] + wrapper.trigger(`keydown.${keyName}`) + + const calls = keydownHandler.mock.calls + expect(calls[calls.length-1][0].keyCode).toEqual(keyCode) + } + }) }) }) + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 46f1283bb..5b90dc665 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2232,6 +2232,11 @@ dir-glob@^2.2.2: dependencies: path-type "^3.0.0" +dom-event-types@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae" + integrity sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" From af32e9b3aae0ede402018616e61ca09ff97a77b0 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Apr 2020 19:31:32 +0200 Subject: [PATCH 2/6] fix:lint --- src/create-dom-event.ts | 28 +++++++++++++++++---------- src/types.ts | 5 ++++- tests/trigger.spec.ts | 42 +++++++++++++++++++++-------------------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts index 8c8119d96..d67e45c07 100644 --- a/src/create-dom-event.ts +++ b/src/create-dom-event.ts @@ -20,7 +20,11 @@ const keyCodesByKeyName = { function getEventProperties(eventParams) { const { modifier, meta, options } = eventParams - const keyCode = keyCodesByKeyName[modifier] || options.key || options.keyCode || options.code + const keyCode = + keyCodesByKeyName[modifier] || + options.key || + options.keyCode || + options.code return { ...options, // What the user passed in as the second argument to #trigger @@ -38,9 +42,7 @@ function createEvent(eventParams) { const metaEventInterface = window[meta.eventInterface] const SupportedEventInterface = - typeof metaEventInterface === 'function' - ? metaEventInterface - : window.Event + typeof metaEventInterface === 'function' ? metaEventInterface : window.Event const eventProperties = getEventProperties(eventParams) @@ -64,21 +66,27 @@ function createEventForOldBrowsers(eventParams) { return event } -export default function createDOMEvent(eventString: String, options: Object = {}) { +export default function createDOMEvent( + eventString: String, + options: Object = {} +) { const [eventType, modifier] = eventString.split('.') - const meta = eventTypes[eventType] - || { eventInterface: 'Event', cancelable: true, bubbles: true } + const meta = eventTypes[eventType] || { + eventInterface: 'Event', + cancelable: true, + bubbles: true + } const eventParams = { eventType, modifier, meta, options } - const event = + const event = typeof window.Event === 'function' ? createEvent(eventParams) : createEventForOldBrowsers(eventParams) // Fallback for IE10,11 - https://stackoverflow.com/questions/26596123 const eventPrototype = Object.getPrototypeOf(event) - Object.keys(options).forEach(key => { + Object.keys(options).forEach((key) => { const propertyDescriptor = Object.getOwnPropertyDescriptor( eventPrototype, key @@ -88,7 +96,7 @@ export default function createDOMEvent(eventString: String, options: Object = {} propertyDescriptor && propertyDescriptor.set === undefined ) if (canSetProperty) { - event[key] = options[key] + event[key] = options[key] } }) diff --git a/src/types.ts b/src/types.ts index 638e6f33a..49a7e5b3b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,5 +10,8 @@ export interface WrapperAPI { findAll(selector: string): DOMWrapper[] html: () => string text: () => string - trigger: (eventString: string, options?: Object) => Promise<(fn?: () => void) => Promise> + trigger: ( + eventString: string, + options?: Object + ) => Promise<(fn?: () => void) => Promise> } diff --git a/tests/trigger.spec.ts b/tests/trigger.spec.ts index 9a7422010..0d639635d 100644 --- a/tests/trigger.spec.ts +++ b/tests/trigger.spec.ts @@ -3,7 +3,6 @@ import { defineComponent, h, ref } from 'vue' import { mount } from '../src' describe('trigger', () => { - describe('on click', () => { it('works on the root element', async () => { const Component = defineComponent({ @@ -12,18 +11,22 @@ describe('trigger', () => { count: ref(0) } }, - + render() { - return h('div', { onClick: () => this.count++ }, `Count: ${this.count}`) + return h( + 'div', + { onClick: () => this.count++ }, + `Count: ${this.count}` + ) } }) - + const wrapper = mount(Component) await wrapper.trigger('click') - + expect(wrapper.text()).toBe('Count: 1') }) - + it('works on a nested element', async () => { const Component = defineComponent({ setup() { @@ -31,7 +34,7 @@ describe('trigger', () => { count: ref(0) } }, - + render() { return h('div', {}, [ h('p', {}, `Count: ${this.count}`), @@ -39,10 +42,10 @@ describe('trigger', () => { ]) } }) - + const wrapper = mount(Component) await wrapper.find('button').trigger('click') - + expect(wrapper.find('p').text()).toBe('Count: 1') }) @@ -55,14 +58,14 @@ describe('trigger', () => { }, render() { - return h('div', { - onClick: () => this.isActive = !this.isActive, - class: { active: this.isActive } + return h('div', { + onClick: () => (this.isActive = !this.isActive), + class: { active: this.isActive } }) } }) const wrapper = mount(Component, {}) - + expect(wrapper.classes()).not.toContain('active') await wrapper.trigger('click') expect(wrapper.classes()).toContain('active') @@ -76,21 +79,21 @@ describe('trigger', () => { template: '', methods: { keydownHandler - }, + } } const wrapper = mount(Component, {}) await wrapper.trigger('keydown') expect(keydownHandler).toHaveBeenCalledTimes(1) }) - + it('causes keydown handler to fire when "keydown.enter" is triggered', async () => { const keydownHandler = jest.fn() const Component = { template: '', methods: { keydownHandler - }, + } } const wrapper = mount(Component, {}) await wrapper.trigger('keydown', { key: 'Enter' }) @@ -104,7 +107,7 @@ describe('trigger', () => { template: '', methods: { keydownHandler - }, + } } const wrapper = mount(Component, {}) await wrapper.trigger('keydown', { keyCode: 65 }) @@ -138,7 +141,7 @@ describe('trigger', () => { template: '', methods: { keydownHandler - }, + } } const wrapper = mount(Component, {}) @@ -147,9 +150,8 @@ describe('trigger', () => { wrapper.trigger(`keydown.${keyName}`) const calls = keydownHandler.mock.calls - expect(calls[calls.length-1][0].keyCode).toEqual(keyCode) + expect(calls[calls.length - 1][0].keyCode).toEqual(keyCode) } }) }) }) - \ No newline at end of file From 6ac131414074caaba075865e2c436ba19bf62b92 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Apr 2020 20:19:36 +0200 Subject: [PATCH 3/6] fix:build --- rollup.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rollup.config.js b/rollup.config.js index d3a65ae96..b8642ad05 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -26,7 +26,8 @@ function createEntry(options) { 'lodash/camelCase', 'lodash/upperFirst', 'lodash/kebabCase', - 'lodash/flow' + 'lodash/flow', + 'dom-event-types' ], plugins: [resolve()], output: { From 18ca4227dd36c616e99b26af7b4fe4dea423098e Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Apr 2020 23:33:34 +0200 Subject: [PATCH 4/6] feat: prevent trigger on disabled items --- src/create-dom-event.ts | 8 +- src/dom-wrapper.ts | 30 +++++++- tests/trigger.spec.ts | 157 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 172 insertions(+), 23 deletions(-) diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts index d67e45c07..4bd8f4622 100644 --- a/src/create-dom-event.ts +++ b/src/create-dom-event.ts @@ -20,18 +20,13 @@ const keyCodesByKeyName = { function getEventProperties(eventParams) { const { modifier, meta, options } = eventParams - const keyCode = - keyCodesByKeyName[modifier] || - options.key || - options.keyCode || - options.code + const keyCode = keyCodesByKeyName[modifier] || options.keyCode || options.code return { ...options, // What the user passed in as the second argument to #trigger bubbles: meta.bubbles, meta: meta.cancelable, // Any derived options should go here - key: keyCode, keyCode, code: keyCode } @@ -95,6 +90,7 @@ export default function createDOMEvent( const canSetProperty = !( propertyDescriptor && propertyDescriptor.set === undefined ) + if (canSetProperty) { event[key] = options[key] } diff --git a/src/dom-wrapper.ts b/src/dom-wrapper.ts index ae12dffad..1f95602f3 100644 --- a/src/dom-wrapper.ts +++ b/src/dom-wrapper.ts @@ -137,7 +137,35 @@ export class DOMWrapper implements WrapperAPI { } async trigger(eventString: string, options: Object = {}) { - if (this.element) { + if (options['target']) { + throw Error( + `[vue-test-utils]: you cannot set the target value of an event. See the notes section ` + + `of the docs for more details—` + + `https://vue-test-utils.vuejs.org/api/wrapper/trigger.html` + ) + } + + const isDisabled = () => { + const validTagsToBeDisabled = [ + 'BUTTON', + 'COMMAND', + 'FIELDSET', + 'KEYGEN', + 'OPTGROUP', + 'OPTION', + 'SELECT', + 'TEXTAREA', + 'INPUT' + ] + const hasDisabledAttribute = this.attributes().disabled !== undefined + const elementCanBeDisabled = validTagsToBeDisabled.includes( + this.element.tagName + ) + + return hasDisabledAttribute && elementCanBeDisabled + } + + if (this.element && !isDisabled()) { const event = createDOMEvent(eventString, options) this.element.dispatchEvent(event) } diff --git a/tests/trigger.spec.ts b/tests/trigger.spec.ts index 0d639635d..73baca0dc 100644 --- a/tests/trigger.spec.ts +++ b/tests/trigger.spec.ts @@ -77,43 +77,65 @@ describe('trigger', () => { const keydownHandler = jest.fn() const Component = { template: '', - methods: { - keydownHandler - } + methods: { keydownHandler } } const wrapper = mount(Component, {}) - await wrapper.trigger('keydown') + // is not fired when a diferent event is triggered + await wrapper.trigger('click') + expect(keydownHandler).not.toHaveBeenCalled() + + // is called when 'keydown' is triggered + await wrapper.trigger('keydown') expect(keydownHandler).toHaveBeenCalledTimes(1) + + // is called when 'keydown' is triggered with a modificator + await wrapper.trigger('keydown.enter') + expect(keydownHandler).toHaveBeenCalledTimes(2) + + // is called when 'keydown' is triggered with an option + await wrapper.trigger('keydown', { key: 'K' }) + expect(keydownHandler).toHaveBeenCalledTimes(3) }) it('causes keydown handler to fire when "keydown.enter" is triggered', async () => { const keydownHandler = jest.fn() const Component = { template: '', - methods: { - keydownHandler - } + methods: { keydownHandler } } const wrapper = mount(Component, {}) - await wrapper.trigger('keydown', { key: 'Enter' }) + // is not called when key is not 'enter' + await wrapper.trigger('keydown', { key: 'Backspace' }) + expect(keydownHandler).not.toHaveBeenCalled() + + // is not called when key is uppercase 'ENTER' + await wrapper.trigger('keydown', { key: 'ENTER' }) + expect(keydownHandler).not.toHaveBeenCalled() + + // is called when key is lowercase 'enter' + await wrapper.trigger('keydown', { key: 'enter' }) expect(keydownHandler).toHaveBeenCalledTimes(1) + expect(keydownHandler.mock.calls[0][0].key).toBe('enter') + + // is called when key is titlecase 'Enter' + await wrapper.trigger('keydown', { key: 'Enter' }) + expect(keydownHandler).toHaveBeenCalledTimes(2) + expect(keydownHandler.mock.calls[1][0].key).toBe('Enter') }) it('causes keydown handler to fire with the appropiate keyCode when wrapper.trigger("keydown", { keyCode: 65 }) is fired', async () => { const keydownHandler = jest.fn() const Component = { template: '', - methods: { - keydownHandler - } + methods: { keydownHandler } } const wrapper = mount(Component, {}) await wrapper.trigger('keydown', { keyCode: 65 }) expect(keydownHandler).toHaveBeenCalledTimes(1) - expect(keydownHandler.mock.calls[0][0]['keyCode']).toBe(65) + expect(keydownHandler.mock.calls[0][0].keyCode).toBe(65) }) it('causes keydown handler to fire converting keyName in an apropiate keyCode when wrapper.trigger("keydown.${keyName}") is fired', async () => { @@ -139,9 +161,7 @@ describe('trigger', () => { const Component = { template: '', - methods: { - keydownHandler - } + methods: { keydownHandler } } const wrapper = mount(Component, {}) @@ -150,8 +170,113 @@ describe('trigger', () => { wrapper.trigger(`keydown.${keyName}`) const calls = keydownHandler.mock.calls - expect(calls[calls.length - 1][0].keyCode).toEqual(keyCode) + const currentCall = calls[calls.length - 1][0] + + // expect(currentCall.key).toBe('') + expect(currentCall.keyCode).toBe(keyCode) + // expect(currentCall.code).toBe(keyCode.toString()) } }) }) + + describe('on disabled elements', () => { + it('does not fires when trigger is called on a valid disabled element', async () => { + const validElementsToBeDisabled = [ + 'button', + 'fieldset', + 'optgroup', + 'option', + 'select', + 'textarea', + 'input' + ] + + for (let element of validElementsToBeDisabled) { + const clickHandler = jest.fn() + const Component = { + template: `<${element} disabled @click="clickHandler" />`, + methods: { clickHandler } + } + const wrapper = mount(Component, {}) + await wrapper.trigger('click') + + expect(clickHandler).not.toHaveBeenCalled() + } + }) + + it('is fired when trigger is called on a element set as disabled but who is invalid to be disabled', async () => { + const invalidElementsToBeDisabled = ['div', 'span', 'a'] + + for (let element of invalidElementsToBeDisabled) { + const clickHandler = jest.fn() + const Component = { + template: `<${element} disabled @click="clickHandler" />`, + methods: { clickHandler } + } + const wrapper = mount(Component, {}) + await wrapper.trigger('click') + + expect(clickHandler).toHaveBeenCalledTimes(1) + } + }) + }) + + describe('event modifiers', () => { + const eventModifiers = [ + 'stop', + 'prevent', + 'capture', + 'self', + 'once', + 'passive' + ] + for (let modifier of eventModifiers) { + it(`handles .${modifier}`, async () => { + const keydownHandler = jest.fn() + const Component = { + template: ``, + methods: { keydownHandler } + } + const wrapper = mount(Component, {}) + + await wrapper.trigger('keydown') + + expect(keydownHandler).toHaveBeenCalledTimes(1) + }) + } + }) + + describe('custom data', () => { + it('adds custom data to events', () => { + const updateHandler = jest.fn() + const Component = { + template: '
', + methods: { updateHandler } + } + const wrapper = mount(Component, {}) + + wrapper.trigger('update', { customData: 123 }) + expect(updateHandler).toHaveBeenCalledTimes(1) + expect(updateHandler.mock.calls[0][0].customData).toBe(123) + }) + }) + + describe('errors', () => { + it('throws error if options contains a target value', () => { + const expectedErrorMessage = + '[vue-test-utils]: you cannot set the target value of an event. See the notes section of the docs for more details—https://vue-test-utils.vuejs.org/api/wrapper/trigger.html' + + const clickHandler = jest.fn() + const Component = { + template: '
', + methods: { clickHandler } + } + const wrapper = mount(Component, {}) + + const fn = wrapper.trigger('click', { target: 'something' }) + expect(fn).rejects.toThrowError(expectedErrorMessage) + + expect(clickHandler).not.toHaveBeenCalled() + }) + }) }) From ee85de107653b1e544e1431ae580aec31767e4ec Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Apr 2020 16:29:48 +0200 Subject: [PATCH 5/6] feat: drop IE support --- src/create-dom-event.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts index 4bd8f4622..17a02938c 100644 --- a/src/create-dom-event.ts +++ b/src/create-dom-event.ts @@ -51,16 +51,6 @@ function createEvent(eventParams) { return event } -function createEventForOldBrowsers(eventParams) { - const { eventType, modifier, meta } = eventParams - const { bubbles, cancelable } = meta - - const event = document.createEvent('Event') - event.initEvent(eventType, bubbles, cancelable) - event['keyCode'] = keyCodesByKeyName[modifier] - return event -} - export default function createDOMEvent( eventString: String, options: Object = {} @@ -74,10 +64,7 @@ export default function createDOMEvent( const eventParams = { eventType, modifier, meta, options } - const event = - typeof window.Event === 'function' - ? createEvent(eventParams) - : createEventForOldBrowsers(eventParams) // Fallback for IE10,11 - https://stackoverflow.com/questions/26596123 + const event = createEvent(eventParams) const eventPrototype = Object.getPrototypeOf(event) From c01e2354eb83b661de842582968fc10ac955a380 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Apr 2020 17:24:23 +0200 Subject: [PATCH 6/6] refactor: add some types --- src/create-dom-event.ts | 63 ++++++++++++++++++++++++----------------- src/dom-wrapper.ts | 6 ++-- src/vue-wrapper.ts | 3 +- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/create-dom-event.ts b/src/create-dom-event.ts index 17a02938c..3f6e7ddd3 100644 --- a/src/create-dom-event.ts +++ b/src/create-dom-event.ts @@ -1,5 +1,19 @@ import eventTypes from 'dom-event-types' +interface TriggerOptions { + code?: String + key?: String + keyCode?: Number + [custom: string]: any +} + +interface EventParams { + eventType: string + modifier: string + meta: any + options?: TriggerOptions +} + const keyCodesByKeyName = { backspace: 8, tab: 9, @@ -18,9 +32,11 @@ const keyCodesByKeyName = { delete: 46 } -function getEventProperties(eventParams) { +function getEventProperties(eventParams: EventParams) { const { modifier, meta, options } = eventParams - const keyCode = keyCodesByKeyName[modifier] || options.keyCode || options.code + const keyCode = + keyCodesByKeyName[modifier] || + (options && (options.keyCode || options.code)) return { ...options, // What the user passed in as the second argument to #trigger @@ -32,7 +48,7 @@ function getEventProperties(eventParams) { } } -function createEvent(eventParams) { +function createEvent(eventParams: EventParams) { const { eventType, meta } = eventParams const metaEventInterface = window[meta.eventInterface] @@ -51,10 +67,7 @@ function createEvent(eventParams) { return event } -export default function createDOMEvent( - eventString: String, - options: Object = {} -) { +function createDOMEvent(eventString: String, options?: TriggerOptions) { const [eventType, modifier] = eventString.split('.') const meta = eventTypes[eventType] || { eventInterface: 'Event', @@ -62,26 +75,24 @@ export default function createDOMEvent( bubbles: true } - const eventParams = { eventType, modifier, meta, options } - - const event = createEvent(eventParams) - + const eventParams: EventParams = { eventType, modifier, meta, options } + const event: Event = createEvent(eventParams) const eventPrototype = Object.getPrototypeOf(event) - Object.keys(options).forEach((key) => { - const propertyDescriptor = Object.getOwnPropertyDescriptor( - eventPrototype, - key - ) - - const canSetProperty = !( - propertyDescriptor && propertyDescriptor.set === undefined - ) - - if (canSetProperty) { - event[key] = options[key] - } - }) - + options && + Object.keys(options).forEach((key) => { + const propertyDescriptor = Object.getOwnPropertyDescriptor( + eventPrototype, + key + ) + const canSetProperty = !( + propertyDescriptor && propertyDescriptor.set === undefined + ) + if (canSetProperty) { + event[key] = options[key] + } + }) return event } + +export { TriggerOptions, createDOMEvent } diff --git a/src/dom-wrapper.ts b/src/dom-wrapper.ts index 1f95602f3..fcdad352d 100644 --- a/src/dom-wrapper.ts +++ b/src/dom-wrapper.ts @@ -3,7 +3,7 @@ import { nextTick } from 'vue' import { WrapperAPI } from './types' import { ErrorWrapper } from './error-wrapper' -import createDOMEvent from './create-dom-event' +import { TriggerOptions, createDOMEvent } from './create-dom-event' export class DOMWrapper implements WrapperAPI { element: ElementType @@ -136,8 +136,8 @@ export class DOMWrapper implements WrapperAPI { return new DOMWrapper(parentElement).trigger('change') } - async trigger(eventString: string, options: Object = {}) { - if (options['target']) { + async trigger(eventString: string, options?: TriggerOptions) { + if (options && options['target']) { throw Error( `[vue-test-utils]: you cannot set the target value of an event. See the notes section ` + `of the docs for more details—` + diff --git a/src/vue-wrapper.ts b/src/vue-wrapper.ts index 4fd7fb13f..44e3a0390 100644 --- a/src/vue-wrapper.ts +++ b/src/vue-wrapper.ts @@ -5,6 +5,7 @@ import { DOMWrapper } from './dom-wrapper' import { WrapperAPI } from './types' import { ErrorWrapper } from './error-wrapper' import { MOUNT_ELEMENT_ID } from './constants' +import { TriggerOptions } from './create-dom-event' export class VueWrapper implements WrapperAPI { @@ -105,7 +106,7 @@ export class VueWrapper return nextTick() } - trigger(eventString: string, options: Object = {}) { + trigger(eventString: string, options?: TriggerOptions) { const rootElementWrapper = new DOMWrapper(this.element) return rootElementWrapper.trigger(eventString, options) }