From 6c6332ff6b2f100796f0ad57166fba2ae56d95e5 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 17:09:26 -0400 Subject: [PATCH 1/4] added fireEvent util --- .all-contributorsrc | 11 ++ README.md | 4 +- package.json | 4 +- src/__tests__/events.js | 287 +++++++++++++++++++++++++++ src/__tests__/helpers/react.js | 13 ++ src/events.js | 352 +++++++++++++++++++++++++++++++++ src/index.js | 1 + 7 files changed, 669 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/events.js create mode 100644 src/__tests__/helpers/react.js create mode 100644 src/events.js diff --git a/.all-contributorsrc b/.all-contributorsrc index 96277113..ac1795bc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -139,6 +139,17 @@ "code", "doc" ] + }, + { + "login": "jomaxx", + "name": "Josef Maxx Blake", + "avatar_url": "https://avatars2.githubusercontent.com/u/2747424?v=4", + "profile": "http://jomaxx.com", + "contributions": [ + "code", + "doc", + "test" + ] } ] } diff --git a/README.md b/README.md index 192e6fe9..8e1ac6f7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![downloads][downloads-badge]][npmtrends] [![MIT License][license-badge]][license] -[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] @@ -662,7 +662,7 @@ Thanks goes to these people ([emoji key][emojis]): | [
Kent C. Dodds](https://kentcdodds.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Documentation") [πŸš‡](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Tests") | [
Ryan Castner](http://audiolion.github.io)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=dnlsandiego "Code") | [
PaweΕ‚ MikoΕ‚ajczyk](https://github.com/Miklet)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=Miklet "Code") | [
Alejandro ÑÑñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Apbomb "Bug reports") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[πŸ“¦](#platform-wKovacs64 "Packaging/porting to new platform") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Documentation") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [
Ernesto GarcΓ­a](http://gnapse.github.io)
[πŸ’¬](#question-gnapse "Answering Questions") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") | +| [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Documentation") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [
Ernesto GarcΓ­a](http://gnapse.github.io)
[πŸ’¬](#question-gnapse "Answering Questions") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") | [
Josef Maxx Blake](http://jomaxx.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=jomaxx "Tests") | diff --git a/package.json b/package.json index a374bbae..8b02c2d7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ }, "devDependencies": { "jest-in-case": "^1.0.2", - "kcd-scripts": "^0.37.0" + "kcd-scripts": "^0.37.0", + "react": "^16.3.1", + "react-dom": "^16.3.1" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", diff --git a/src/__tests__/events.js b/src/__tests__/events.js new file mode 100644 index 00000000..cc1819f9 --- /dev/null +++ b/src/__tests__/events.js @@ -0,0 +1,287 @@ +import React from 'react' +import {mount, unmount} from './helpers/react' +import {fireEvent} from '../' + +describe('Clipboard Events', () => { + ;['copy', 'paste'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Composition Events', () => { + ;['compositionEnd', 'compositionStart', 'compositionUpdate'].forEach( + eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }, + ) +}) + +describe('Keyboard Events', () => { + ;['keyDown', 'keyPress', 'keyUp'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Focus Events', () => { + ;['focus', 'blur'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Form Events', () => { + ;['change', 'input', 'invalid'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + ;['submit'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('form') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Mouse Events', () => { + ;[ + 'click', + 'contextMenu', + 'dblClick', + 'drag', + 'dragEnd', + 'dragEnter', + 'dragExit', + 'dragLeave', + 'dragOver', + 'dragStart', + 'drop', + 'mouseDown', + 'mouseEnter', + 'mouseLeave', + 'mouseMove', + 'mouseOut', + 'mouseOver', + 'mouseUp', + ].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('button') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Selection Events', () => { + ;['select'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('input') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Touch Events', () => { + ;['touchCancel', 'touchEnd', 'touchMove', 'touchStart'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('button') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('UI Events', () => { + ;['scroll'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('div') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Wheel Events', () => { + ;['wheel'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('div') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Media Events', () => { + ;[ + 'abort', + 'canPlay', + 'canPlayThrough', + 'durationChange', + 'emptied', + 'encrypted', + 'ended', + 'error', + 'loadedData', + 'loadedMetadata', + 'loadStart', + 'pause', + 'play', + 'playing', + 'progress', + 'rateChange', + 'seeked', + 'seeking', + 'stalled', + 'suspend', + 'timeUpdate', + 'volumeChange', + 'waiting', + ].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('video') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Image Events', () => { + ;['load', 'error'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('img') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Animation Events', () => { + ;['animationStart', 'animationEnd', 'animationIteration'].forEach( + eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('div') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }, + ) +}) + +describe('Transition Events', () => { + ;['transitionEnd'].forEach(eventName => { + it(`fires ${eventName}`, () => { + const node = document.createElement('div') + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) +}) + +describe('Alias Events', () => { + it(`fires doubleClick`, () => { + const node = document.createElement('button') + const spy = jest.fn() + node.addEventListener('dblclick', spy) + fireEvent.doubleClick(node) + expect(spy).toHaveBeenCalledTimes(1) + }) +}) + +describe('React Events', () => { + afterEach(unmount) + + // todo figure out why this test is broken + it.skip(`triggers onChange`, () => { + let node + const spy = jest.fn() + + mount( + React.createElement('input', { + onChange: spy, + ref: el => (node = el), + }), + ) + + fireEvent.change(node) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it(`triggers onMouseEnter`, () => { + let node + const spy = jest.fn() + + mount( + React.createElement('div', { + onMouseEnter: spy, + ref: el => (node = el), + }), + ) + + fireEvent.mouseEnter(node) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it(`triggers onMouseLeave`, () => { + let node + const spy = jest.fn() + + mount( + React.createElement('div', { + onMouseLeave: spy, + ref: el => (node = el), + }), + ) + + fireEvent.mouseLeave(node) + expect(spy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/helpers/react.js b/src/__tests__/helpers/react.js new file mode 100644 index 00000000..aa82630f --- /dev/null +++ b/src/__tests__/helpers/react.js @@ -0,0 +1,13 @@ +import ReactDOM from 'react-dom' + +const node = document.body.appendChild(document.createElement('div')) + +function mount(element) { + ReactDOM.render(element, node) +} + +function unmount() { + ReactDOM.unmountComponentAtNode(node) +} + +export {mount, unmount} diff --git a/src/events.js b/src/events.js new file mode 100644 index 00000000..78560682 --- /dev/null +++ b/src/events.js @@ -0,0 +1,352 @@ +// fallback to Event +const AnimationEvent = global.AnimationEvent || Event +const ClipboardEvent = global.ClipboardEvent || Event +const CompositionEvent = global.CompositionEvent || Event +const DragEvent = global.DragEvent || Event +const FocusEvent = global.FocusEvent || Event +const InputEvent = global.InputEvent || Event +const KeyboardEvent = global.KeyboardEvent || Event +const MouseEvent = global.MouseEvent || Event +const ProgressEvent = global.ProgressEvent || Event +const TouchEvent = global.TouchEvent || Event +const TransitionEvent = global.TransitionEvent || Event +const UIEvent = global.UIEvent || Event +const WheelEvent = global.WheelEvent || Event + +/** + * Include convenience methods for all events supported in React. + * https://reactjs.org/docs/events.html#supported-events + */ +const eventMap = { + // https://reactjs.org/docs/events.html#clipboard-events + copy: { + EventType: CompositionEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + cut: { + EventType: ClipboardEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + paste: { + EventType: ClipboardEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#composition-events + compositionEnd: { + EventType: CompositionEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + compositionStart: { + EventType: CompositionEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + compositionUpdate: { + EventType: CompositionEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#keyboard-events + keyDown: { + EventType: KeyboardEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + keyPress: { + EventType: KeyboardEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + keyUp: { + EventType: KeyboardEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#focus-events + focus: { + EventType: FocusEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + blur: { + EventType: FocusEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#form-events + change: { + EventType: InputEvent, + defaultInit: {bubbles: true, cancelable: true}, + before(node) { + // input event will trigger onChange for React + fireEvent.input(node) + }, + }, + input: { + EventType: InputEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + invalid: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: true}, + }, + submit: { + EventType: Event, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#mouse-events + click: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true, button: 0}, + }, + contextMenu: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + dblClick: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + drag: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + dragEnd: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + dragEnter: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + dragExit: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + dragLeave: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + dragOver: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + dragStart: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + drop: { + EventType: DragEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + mouseDown: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + mouseEnter: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + after(node) { + // mouseover event will trigger onMouseEnter for React + fireEvent.mouseOver(node) + }, + }, + mouseLeave: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + after(node) { + // mouseout event will trigger onMouseLeave for React + fireEvent.mouseOut(node) + }, + }, + mouseMove: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + mouseOut: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + mouseOver: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + mouseUp: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#selection-events + select: { + EventType: Event, + defaultInit: {bubbles: true, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#touch-events + touchCancel: { + EventType: TouchEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + touchEnd: { + EventType: TouchEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + touchMove: { + EventType: TouchEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + touchStart: { + EventType: TouchEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#ui-events + scroll: { + EventType: UIEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#wheel-events + wheel: { + EventType: WheelEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // https://reactjs.org/docs/events.html#media-events + abort: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + canPlay: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + canPlayThrough: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + durationChange: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + emptied: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + encrypted: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + ended: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + // error: { + // EventType: Event, + // defaultInit: {bubbles: false, cancelable: false}, + // }, + loadedData: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + loadedMetadata: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + loadStart: { + EventType: ProgressEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + pause: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + play: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + playing: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + progress: { + EventType: ProgressEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + rateChange: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + seeked: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + seeking: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + stalled: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + suspend: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + timeUpdate: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + volumeChange: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + waiting: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#image-events + load: { + EventType: UIEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + error: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#animation-events + animationStart: { + EventType: AnimationEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + animationEnd: { + EventType: AnimationEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + animationIteration: { + EventType: AnimationEvent, + defaultInit: {bubbles: true, cancelable: false}, + }, + // https://reactjs.org/docs/events.html#transition-events + transitionEnd: { + EventType: TransitionEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, +} + +const eventAliasMap = { + doubleClick: 'dblClick', +} + +function fireEvent(element, event) { + return element.dispatchEvent(event) +} + +Object.entries(eventMap).forEach( + ([key, {EventType, defaultInit, before, after}]) => { + const eventName = key.toLowerCase() + + fireEvent[key] = (node, init) => { + const eventInit = Object.assign({}, defaultInit, init) + const event = new EventType(eventName, eventInit) + if (before) before(node, event) + const ret = fireEvent(node, event) + if (after) after(node, event) + return ret + } + }, +) + +Object.entries(eventAliasMap).forEach(([aliasKey, key]) => { + fireEvent[aliasKey] = (...args) => fireEvent[key](...args) +}) + +export {fireEvent} diff --git a/src/index.js b/src/index.js index 2a80220e..df1a0fd4 100644 --- a/src/index.js +++ b/src/index.js @@ -8,3 +8,4 @@ export * from './queries' export * from './wait' export * from './wait-for-element' export * from './matches' +export * from './events' From 810e5a089b42b4c32d954979ab9eb11e95520dc0 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 22:19:49 -0400 Subject: [PATCH 2/4] remove react logic --- README.md | 25 +++ package.json | 8 +- src/__tests__/events.js | 400 +++++++++++---------------------- src/__tests__/helpers/react.js | 13 -- src/events.js | 96 ++++---- 5 files changed, 199 insertions(+), 343 deletions(-) delete mode 100644 src/__tests__/helpers/react.js diff --git a/README.md b/README.md index 8e1ac6f7..6dd7019e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ when a real user uses it. * [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement) * [`wait`](#wait) * [`waitForElement`](#waitforelement) + * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event) * [Custom Jest Matchers](#custom-jest-matchers) * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) @@ -368,6 +369,30 @@ The default `timeout` is `4500ms` which will keep you under additions and removals of child elements (including text nodes) in the `container` and any of its descendants. It won't detect attribute changes unless you add `attributes: true` to the options. +### `fireEvent(node: HTMLElement, event: Event)` + +Fire DOM events. + +```javascript +// +fireEvent( + getElementByText('Submit'), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }), +) +``` + +#### `fireEvent[eventName](node: HTMLElement, eventInit)` + +Convenience methods for firing DOM events. Look [here](./src/events.js) for full list. + +```javascript +// +fireEvent.click(getElementByText('Submit')) +``` + ## Custom Jest Matchers There are two simple API which extend the `expect` API of jest for making assertions easier. diff --git a/package.json b/package.json index 8b02c2d7..1ba1cfee 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,12 @@ ], "dependencies": { "jest-matcher-utils": "^22.4.3", - "wait-for-expect": "^0.4.0", - "mutationobserver-shim": "^0.3.2" + "mutationobserver-shim": "^0.3.2", + "wait-for-expect": "^0.4.0" }, "devDependencies": { "jest-in-case": "^1.0.2", - "kcd-scripts": "^0.37.0", - "react": "^16.3.1", - "react-dom": "^16.3.1" + "kcd-scripts": "^0.37.0" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", diff --git a/src/__tests__/events.js b/src/__tests__/events.js index cc1819f9..b57e2f21 100644 --- a/src/__tests__/events.js +++ b/src/__tests__/events.js @@ -1,287 +1,151 @@ -import React from 'react' -import {mount, unmount} from './helpers/react' import {fireEvent} from '../' -describe('Clipboard Events', () => { - ;['copy', 'paste'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Composition Events', () => { - ;['compositionEnd', 'compositionStart', 'compositionUpdate'].forEach( - eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }, - ) -}) - -describe('Keyboard Events', () => { - ;['keyDown', 'keyPress', 'keyUp'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Focus Events', () => { - ;['focus', 'blur'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Form Events', () => { - ;['change', 'input', 'invalid'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) - ;['submit'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('form') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Mouse Events', () => { - ;[ - 'click', - 'contextMenu', - 'dblClick', - 'drag', - 'dragEnd', - 'dragEnter', - 'dragExit', - 'dragLeave', - 'dragOver', - 'dragStart', - 'drop', - 'mouseDown', - 'mouseEnter', - 'mouseLeave', - 'mouseMove', - 'mouseOut', - 'mouseOver', - 'mouseUp', - ].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('button') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Selection Events', () => { - ;['select'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('input') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Touch Events', () => { - ;['touchCancel', 'touchEnd', 'touchMove', 'touchStart'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('button') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('UI Events', () => { - ;['scroll'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('div') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Wheel Events', () => { - ;['wheel'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('div') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Media Events', () => { - ;[ - 'abort', - 'canPlay', - 'canPlayThrough', - 'durationChange', - 'emptied', - 'encrypted', - 'ended', - 'error', - 'loadedData', - 'loadedMetadata', - 'loadStart', - 'pause', - 'play', - 'playing', - 'progress', - 'rateChange', - 'seeked', - 'seeking', - 'stalled', - 'suspend', - 'timeUpdate', - 'volumeChange', - 'waiting', - ].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('video') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Image Events', () => { - ;['load', 'error'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('img') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) - }) - }) -}) - -describe('Animation Events', () => { - ;['animationStart', 'animationEnd', 'animationIteration'].forEach( - eventName => { +const eventTypes = [ + { + type: 'Clipboard', + events: ['copy', 'paste'], + elementType: 'input', + }, + { + type: 'Composition', + events: ['compositionEnd', 'compositionStart', 'compositionUpdate'], + elementType: 'input', + }, + { + type: 'Keyboard', + events: ['keyDown', 'keyPress', 'keyUp'], + elementType: 'input', + }, + { + type: 'Focus', + events: ['focus', 'blur'], + elementType: 'input', + }, + { + type: 'Form', + events: ['focus', 'blur'], + elementType: 'input', + }, + { + type: 'Focus', + events: ['change', 'input', 'invalid'], + elementType: 'input', + }, + { + type: 'Focus', + events: ['submit'], + elementType: 'form', + }, + { + type: 'Mouse', + events: [ + 'click', + 'contextMenu', + 'dblClick', + 'drag', + 'dragEnd', + 'dragEnter', + 'dragExit', + 'dragLeave', + 'dragOver', + 'dragStart', + 'drop', + 'mouseDown', + 'mouseEnter', + 'mouseLeave', + 'mouseMove', + 'mouseOut', + 'mouseOver', + 'mouseUp', + ], + elementType: 'button', + }, + { + type: 'Selection', + events: ['select'], + elementType: 'input', + }, + { + type: 'Touch', + events: ['touchCancel', 'touchEnd', 'touchMove', 'touchStart'], + elementType: 'button', + }, + { + type: 'UI', + events: ['scroll'], + elementType: 'div', + }, + { + type: 'Wheel', + events: ['wheel'], + elementType: 'div', + }, + { + type: 'Media', + events: [ + 'abort', + 'canPlay', + 'canPlayThrough', + 'durationChange', + 'emptied', + 'encrypted', + 'ended', + 'error', + 'loadedData', + 'loadedMetadata', + 'loadStart', + 'pause', + 'play', + 'playing', + 'progress', + 'rateChange', + 'seeked', + 'seeking', + 'stalled', + 'suspend', + 'timeUpdate', + 'volumeChange', + 'waiting', + ], + elementType: 'video', + }, + { + type: 'Image', + events: ['load', 'error'], + elementType: 'img', + }, + { + type: 'Animation', + events: ['animationStart', 'animationEnd', 'animationIteration'], + elementType: 'div', + }, + { + type: 'Transition', + events: ['transitionEnd'], + elementType: 'div', + }, +] + +eventTypes.forEach(({type, events, elementType}) => { + describe(`${type} Events`, () => { + events.forEach(eventName => { it(`fires ${eventName}`, () => { - const node = document.createElement('div') + const node = document.createElement(elementType) const spy = jest.fn() node.addEventListener(eventName.toLowerCase(), spy) fireEvent[eventName](node) expect(spy).toHaveBeenCalledTimes(1) }) - }, - ) -}) - -describe('Transition Events', () => { - ;['transitionEnd'].forEach(eventName => { - it(`fires ${eventName}`, () => { - const node = document.createElement('div') - const spy = jest.fn() - node.addEventListener(eventName.toLowerCase(), spy) - fireEvent[eventName](node) - expect(spy).toHaveBeenCalledTimes(1) }) }) }) -describe('Alias Events', () => { +describe(`Aliased Events`, () => { it(`fires doubleClick`, () => { - const node = document.createElement('button') + const node = document.createElement('div') const spy = jest.fn() node.addEventListener('dblclick', spy) fireEvent.doubleClick(node) expect(spy).toHaveBeenCalledTimes(1) }) }) - -describe('React Events', () => { - afterEach(unmount) - - // todo figure out why this test is broken - it.skip(`triggers onChange`, () => { - let node - const spy = jest.fn() - - mount( - React.createElement('input', { - onChange: spy, - ref: el => (node = el), - }), - ) - - fireEvent.change(node) - expect(spy).toHaveBeenCalledTimes(1) - }) - - it(`triggers onMouseEnter`, () => { - let node - const spy = jest.fn() - - mount( - React.createElement('div', { - onMouseEnter: spy, - ref: el => (node = el), - }), - ) - - fireEvent.mouseEnter(node) - expect(spy).toHaveBeenCalledTimes(1) - }) - - it(`triggers onMouseLeave`, () => { - let node - const spy = jest.fn() - - mount( - React.createElement('div', { - onMouseLeave: spy, - ref: el => (node = el), - }), - ) - - fireEvent.mouseLeave(node) - expect(spy).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/__tests__/helpers/react.js b/src/__tests__/helpers/react.js deleted file mode 100644 index aa82630f..00000000 --- a/src/__tests__/helpers/react.js +++ /dev/null @@ -1,13 +0,0 @@ -import ReactDOM from 'react-dom' - -const node = document.body.appendChild(document.createElement('div')) - -function mount(element) { - ReactDOM.render(element, node) -} - -function unmount() { - ReactDOM.unmountComponentAtNode(node) -} - -export {mount, unmount} diff --git a/src/events.js b/src/events.js index 78560682..7f369965 100644 --- a/src/events.js +++ b/src/events.js @@ -1,24 +1,23 @@ -// fallback to Event -const AnimationEvent = global.AnimationEvent || Event -const ClipboardEvent = global.ClipboardEvent || Event -const CompositionEvent = global.CompositionEvent || Event -const DragEvent = global.DragEvent || Event -const FocusEvent = global.FocusEvent || Event -const InputEvent = global.InputEvent || Event -const KeyboardEvent = global.KeyboardEvent || Event -const MouseEvent = global.MouseEvent || Event -const ProgressEvent = global.ProgressEvent || Event -const TouchEvent = global.TouchEvent || Event -const TransitionEvent = global.TransitionEvent || Event -const UIEvent = global.UIEvent || Event -const WheelEvent = global.WheelEvent || Event +const { + AnimationEvent, + ClipboardEvent, + CompositionEvent, + DragEvent, + Event, + FocusEvent, + InputEvent, + KeyboardEvent, + MouseEvent, + ProgressEvent, + TouchEvent, + TransitionEvent, + UIEvent, + WheelEvent, +} = + typeof window === 'undefined' ? global : window -/** - * Include convenience methods for all events supported in React. - * https://reactjs.org/docs/events.html#supported-events - */ const eventMap = { - // https://reactjs.org/docs/events.html#clipboard-events + // Clipboard Events copy: { EventType: CompositionEvent, defaultInit: {bubbles: true, cancelable: true}, @@ -31,7 +30,7 @@ const eventMap = { EventType: ClipboardEvent, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#composition-events + // Composition Events compositionEnd: { EventType: CompositionEvent, defaultInit: {bubbles: true, cancelable: true}, @@ -44,7 +43,7 @@ const eventMap = { EventType: CompositionEvent, defaultInit: {bubbles: true, cancelable: false}, }, - // https://reactjs.org/docs/events.html#keyboard-events + // Keyboard Events keyDown: { EventType: KeyboardEvent, defaultInit: {bubbles: true, cancelable: true}, @@ -57,7 +56,7 @@ const eventMap = { EventType: KeyboardEvent, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#focus-events + // Focus Events focus: { EventType: FocusEvent, defaultInit: {bubbles: false, cancelable: false}, @@ -66,14 +65,10 @@ const eventMap = { EventType: FocusEvent, defaultInit: {bubbles: false, cancelable: false}, }, - // https://reactjs.org/docs/events.html#form-events + // Form Events change: { EventType: InputEvent, defaultInit: {bubbles: true, cancelable: true}, - before(node) { - // input event will trigger onChange for React - fireEvent.input(node) - }, }, input: { EventType: InputEvent, @@ -87,7 +82,7 @@ const eventMap = { EventType: Event, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#mouse-events + // Mouse Events click: { EventType: MouseEvent, defaultInit: {bubbles: true, cancelable: true, button: 0}, @@ -139,18 +134,10 @@ const eventMap = { mouseEnter: { EventType: MouseEvent, defaultInit: {bubbles: true, cancelable: true}, - after(node) { - // mouseover event will trigger onMouseEnter for React - fireEvent.mouseOver(node) - }, }, mouseLeave: { EventType: MouseEvent, defaultInit: {bubbles: true, cancelable: true}, - after(node) { - // mouseout event will trigger onMouseLeave for React - fireEvent.mouseOut(node) - }, }, mouseMove: { EventType: MouseEvent, @@ -168,12 +155,12 @@ const eventMap = { EventType: MouseEvent, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#selection-events + // Selection Events select: { EventType: Event, defaultInit: {bubbles: true, cancelable: false}, }, - // https://reactjs.org/docs/events.html#touch-events + // Touch Events touchCancel: { EventType: TouchEvent, defaultInit: {bubbles: true, cancelable: false}, @@ -190,17 +177,17 @@ const eventMap = { EventType: TouchEvent, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#ui-events + // UI Events scroll: { EventType: UIEvent, defaultInit: {bubbles: false, cancelable: false}, }, - // https://reactjs.org/docs/events.html#wheel-events + // Wheel Events wheel: { EventType: WheelEvent, defaultInit: {bubbles: true, cancelable: true}, }, - // https://reactjs.org/docs/events.html#media-events + // Media Events abort: { EventType: Event, defaultInit: {bubbles: false, cancelable: false}, @@ -293,7 +280,7 @@ const eventMap = { EventType: Event, defaultInit: {bubbles: false, cancelable: false}, }, - // https://reactjs.org/docs/events.html#image-events + // Image Events load: { EventType: UIEvent, defaultInit: {bubbles: false, cancelable: false}, @@ -302,7 +289,7 @@ const eventMap = { EventType: Event, defaultInit: {bubbles: false, cancelable: false}, }, - // https://reactjs.org/docs/events.html#animation-events + // Animation Events animationStart: { EventType: AnimationEvent, defaultInit: {bubbles: true, cancelable: false}, @@ -315,7 +302,7 @@ const eventMap = { EventType: AnimationEvent, defaultInit: {bubbles: true, cancelable: false}, }, - // https://reactjs.org/docs/events.html#transition-events + // Transition Events transitionEnd: { EventType: TransitionEvent, defaultInit: {bubbles: true, cancelable: true}, @@ -330,20 +317,15 @@ function fireEvent(element, event) { return element.dispatchEvent(event) } -Object.entries(eventMap).forEach( - ([key, {EventType, defaultInit, before, after}]) => { - const eventName = key.toLowerCase() +Object.entries(eventMap).forEach(([key, {EventType = Event, defaultInit}]) => { + const eventName = key.toLowerCase() - fireEvent[key] = (node, init) => { - const eventInit = Object.assign({}, defaultInit, init) - const event = new EventType(eventName, eventInit) - if (before) before(node, event) - const ret = fireEvent(node, event) - if (after) after(node, event) - return ret - } - }, -) + fireEvent[key] = (node, init) => { + const eventInit = Object.assign({}, defaultInit, init) + const event = new EventType(eventName, eventInit) + return fireEvent(node, event) + } +}) Object.entries(eventAliasMap).forEach(([aliasKey, key]) => { fireEvent[aliasKey] = (...args) => fireEvent[key](...args) From 356e510f49eb845c6e97c4f0c3b7ff698e417529 Mon Sep 17 00:00:00 2001 From: Josef Blake Date: Mon, 9 Apr 2018 22:28:05 -0400 Subject: [PATCH 3/4] :100: coverage --- src/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.js b/src/events.js index 7f369965..ad5a8e33 100644 --- a/src/events.js +++ b/src/events.js @@ -14,7 +14,7 @@ const { UIEvent, WheelEvent, } = - typeof window === 'undefined' ? global : window + typeof window === 'undefined' ? /* istanbul ignore next */ global : window const eventMap = { // Clipboard Events From 3c0bd5f63b2bd4505649653ad1c44f24f8ad8a77 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 9 Apr 2018 21:30:44 -0600 Subject: [PATCH 4/4] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6dd7019e..69809c09 100644 --- a/README.md +++ b/README.md @@ -384,13 +384,17 @@ fireEvent( ) ``` -#### `fireEvent[eventName](node: HTMLElement, eventInit)` +#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)` -Convenience methods for firing DOM events. Look [here](./src/events.js) for full list. +Convenience methods for firing DOM events. Check out +[src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js) +for a full list as well as default `eventProperties`. ```javascript // -fireEvent.click(getElementByText('Submit')) +const rightClick = {button: 2} +fireEvent.click(getElementByText('Submit'), rightClick) +// default `button` property for click events is set to `0` which is a left click. ``` ## Custom Jest Matchers