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..69809c09 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] @@ -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,34 @@ 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, eventProperties: Object)` + +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 +// +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 There are two simple API which extend the `expect` API of jest for making assertions easier. @@ -662,7 +691,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..1ba1cfee 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ ], "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", diff --git a/src/__tests__/events.js b/src/__tests__/events.js new file mode 100644 index 00000000..b57e2f21 --- /dev/null +++ b/src/__tests__/events.js @@ -0,0 +1,151 @@ +import {fireEvent} from '../' + +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(elementType) + const spy = jest.fn() + node.addEventListener(eventName.toLowerCase(), spy) + fireEvent[eventName](node) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + }) +}) + +describe(`Aliased Events`, () => { + it(`fires doubleClick`, () => { + const node = document.createElement('div') + const spy = jest.fn() + node.addEventListener('dblclick', spy) + fireEvent.doubleClick(node) + expect(spy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/events.js b/src/events.js new file mode 100644 index 00000000..ad5a8e33 --- /dev/null +++ b/src/events.js @@ -0,0 +1,334 @@ +const { + AnimationEvent, + ClipboardEvent, + CompositionEvent, + DragEvent, + Event, + FocusEvent, + InputEvent, + KeyboardEvent, + MouseEvent, + ProgressEvent, + TouchEvent, + TransitionEvent, + UIEvent, + WheelEvent, +} = + typeof window === 'undefined' ? /* istanbul ignore next */ global : window + +const eventMap = { + // 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}, + }, + // 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}, + }, + // 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}, + }, + // Focus Events + focus: { + EventType: FocusEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + blur: { + EventType: FocusEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + // Form Events + change: { + EventType: InputEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + input: { + EventType: InputEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + invalid: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: true}, + }, + submit: { + EventType: Event, + defaultInit: {bubbles: true, cancelable: true}, + }, + // 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}, + }, + mouseLeave: { + EventType: MouseEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + 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}, + }, + // Selection Events + select: { + EventType: Event, + defaultInit: {bubbles: true, cancelable: false}, + }, + // 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}, + }, + // UI Events + scroll: { + EventType: UIEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + // Wheel Events + wheel: { + EventType: WheelEvent, + defaultInit: {bubbles: true, cancelable: true}, + }, + // 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}, + }, + // Image Events + load: { + EventType: UIEvent, + defaultInit: {bubbles: false, cancelable: false}, + }, + error: { + EventType: Event, + defaultInit: {bubbles: false, cancelable: false}, + }, + // 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}, + }, + // 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 = Event, defaultInit}]) => { + const eventName = key.toLowerCase() + + 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) +}) + +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'