From 7f4e58eb3f98ee9d41a7ec0cc085e312e4259604 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 15 Jun 2020 15:18:47 -0600 Subject: [PATCH 1/7] feat: make several API and implmenetation improvements --- README.md | 59 +- package.json | 5 +- src/.eslintrc | 8 + src/__mocks__/@testing-library/dom.js | 50 ++ src/__tests__/blur.js | 66 ++ src/__tests__/clear.js | 88 ++- src/__tests__/click.js | 405 +++++----- src/__tests__/dblclick.js | 435 +++++----- src/__tests__/deselect-options.js | 118 +++ src/__tests__/focus.js | 57 ++ .../{customElement.js => custom-element.js} | 0 src/__tests__/helpers/utils.js | 293 +++---- src/__tests__/hover.js | 41 +- src/__tests__/paste.js | 96 +++ src/__tests__/select-options.js | 135 ++++ src/__tests__/selectoptions.js | 79 -- src/__tests__/tab.js | 94 ++- src/__tests__/toggleselectoptions.js | 103 --- src/__tests__/type-modifiers.js | 748 ++++++++++++++---- src/__tests__/type.js | 624 ++++++++++----- src/__tests__/unhover.js | 40 +- src/__tests__/upload.js | 108 ++- src/blur.js | 14 + src/clear.js | 30 + src/click.js | 106 +++ src/focus.js | 14 + src/hover.js | 37 + src/index.js | 491 +----------- src/paste.js | 45 ++ src/select-options.js | 71 ++ src/tab.js | 118 +++ src/tick.js | 54 -- src/type.js | 586 +++++++------- src/upload.js | 51 ++ src/utils.js | 179 +++++ tests/setup-env.js | 3 + 36 files changed, 3444 insertions(+), 2007 deletions(-) create mode 100644 src/.eslintrc create mode 100644 src/__mocks__/@testing-library/dom.js create mode 100644 src/__tests__/blur.js create mode 100644 src/__tests__/deselect-options.js create mode 100644 src/__tests__/focus.js rename src/__tests__/helpers/{customElement.js => custom-element.js} (100%) create mode 100644 src/__tests__/paste.js create mode 100644 src/__tests__/select-options.js delete mode 100644 src/__tests__/selectoptions.js delete mode 100644 src/__tests__/toggleselectoptions.js create mode 100644 src/blur.js create mode 100644 src/clear.js create mode 100644 src/click.js create mode 100644 src/focus.js create mode 100644 src/hover.js create mode 100644 src/paste.js create mode 100644 src/select-options.js create mode 100644 src/tab.js delete mode 100644 src/tick.js create mode 100644 src/upload.js create mode 100644 src/utils.js diff --git a/README.md b/README.md index c87d47a0..e60ea19b 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,11 @@ change the state of the checkbox. - [`upload(element, file, [{ clickInit, changeInit }])`](#uploadelement-file--clickinit-changeinit-) - [`clear(element)`](#clearelement) - [`selectOptions(element, values)`](#selectoptionselement-values) - - [`toggleSelectOptions(element, values)`](#toggleselectoptionselement-values) + - [`deselectOptions(element, values)`](#deselectoptionselement-values) - [`tab({shift, focusTrap})`](#tabshift-focustrap) - - [`async hover(element)`](#async-hoverelement) - - [`async unhover(element)`](#async-unhoverelement) + - [`hover(element)`](#hoverelement) + - [`unhover(element)`](#unhoverelement) + - [`paste(element, text, eventInit, options)`](#pasteelement-text-eventinit-options) - [Issues](#issues) - [Contributors ✨](#contributors-) - [LICENSE](#license) @@ -164,9 +165,6 @@ test('type', async () => { }) ``` -If `options.allAtOnce` is `true`, `type` will write `text` at once rather than -one character at the time. `false` is the default value. - `options.delay` is the number of milliseconds that pass between two characters are typed. By default it's 0. You can use this option if your component has a different behavior for fast or slow users. @@ -308,16 +306,17 @@ userEvent.selectOptions(screen.getByTestId('select-multiple'), [ ]) ``` -### `toggleSelectOptions(element, values)` +### `deselectOptions(element, values)` -Toggle the specified option(s) of a `` +element. ```jsx import * as React from 'react' import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' -test('toggleSelectOptions', () => { +test('deselectOptions', () => { render( , ) - userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1', '3']) - - expect(screen.getByText('A').selected).toBe(true) - expect(screen.getByText('C').selected).toBe(true) - - userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1']) - - expect(screen.getByText('A').selected).toBe(false) + userEvent.selectOptions(screen.getByRole('listbox'), '2') + expect(screen.getByText('B').selected).toBe(true) + userEvent.deselectOptions(screen.getByRole('listbox'), '2') + expect(screen.getByText('B').selected).toBe(false) + // can do multiple at once as well: + // userEvent.deselectOptions(screen.getByRole('listbox'), ['1', '2']) }) ``` @@ -397,7 +394,7 @@ it('should cycle elements in document tab order', () => { }) ``` -### `async hover(element)` +### `hover(element)` Hovers over `element`. @@ -407,7 +404,7 @@ import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' import Tooltip from '../tooltip' -test('hover', async () => { +test('hover', () => { const messageText = 'Hello' render( @@ -415,19 +412,36 @@ test('hover', async () => { , ) - await userEvent.hover(screen.getByLabelText(/delete/i)) + userEvent.hover(screen.getByLabelText(/delete/i)) expect(screen.getByText(messageText)).toBeInTheDocument() - await userEvent.unhover(screen.getByLabelText(/delete/i)) + userEvent.unhover(screen.getByLabelText(/delete/i)) expect(screen.queryByText(messageText)).not.toBeInTheDocument() }) ``` -### `async unhover(element)` +### `unhover(element)` Unhovers out of `element`. > See [above](#async-hoverelement) for an example +### `paste(element, text, eventInit, options)` + +Allows you to simulate the user pasting some text into an input. + +```javascript +test('should paste text in input', () => { + render() + + const text = 'Hello, world!' + userEvent.paste(getByRole('textbox', {name: /paste your greeting/i}), text) + expect(element).toHaveValue(text) +}) +``` + +You can use the `eventInit` if what you're pasting should have `clipboardData` +(like `files`). + ## Issues **`user-event` is moving. Please create new issues in @@ -503,6 +517,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. diff --git a/package.json b/package.json index d72ec600..5af2bc11 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,10 @@ "@babel/runtime": "^7.10.2" }, "devDependencies": { - "@testing-library/dom": "^7.9.0", - "@testing-library/jest-dom": "^5.9.0", + "@testing-library/dom": "^7.16.0", + "@testing-library/jest-dom": "^5.10.1", "is-ci": "^2.0.0", + "jest-serializer-ansi": "^1.0.3", "kcd-scripts": "^6.2.3" }, "peerDependencies": { diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 00000000..b7d78581 --- /dev/null +++ b/src/.eslintrc @@ -0,0 +1,8 @@ +{ + "rules": { + // everything in this directory is intentionally running in series, not parallel + // because user's cannot fire multiple events at the same time and we need + // all events fired in a predictable order. + "no-await-in-loop": "off" + } +} diff --git a/src/__mocks__/@testing-library/dom.js b/src/__mocks__/@testing-library/dom.js new file mode 100644 index 00000000..10fc4d71 --- /dev/null +++ b/src/__mocks__/@testing-library/dom.js @@ -0,0 +1,50 @@ +// this helps us track what the state is before and after an event is fired +// this is needed for determining the snapshot values +const actual = jest.requireActual('@testing-library/dom') + +function getTrackedElementValues(element) { + return { + value: element.value, + checked: element.checked, + selectionStart: element.selectionStart, + selectionEnd: element.selectionEnd, + + // unfortunately, changing a select option doesn't happen within fireEvent + // but rather imperatively via `options.selected = newValue` + // because of this we don't (currently) have a way to track before/after + // in a given fireEvent call. + } +} + +function wrapWithTestData(fn) { + return (element, init) => { + const before = getTrackedElementValues(element) + const testData = {before} + + // put it on the element so the event handler can grab it + element.testData = testData + const result = fn(element, init) + + const after = getTrackedElementValues(element) + Object.assign(testData, {after}) + + // elete the testData for the next event + delete element.testData + return result + } +} + +const mockFireEvent = wrapWithTestData(actual.fireEvent) + +for (const key of Object.keys(actual.fireEvent)) { + if (typeof actual.fireEvent[key] === 'function') { + mockFireEvent[key] = wrapWithTestData(actual.fireEvent[key], key) + } else { + mockFireEvent[key] = actual.fireEvent[key] + } +} + +module.exports = { + ...actual, + fireEvent: mockFireEvent, +} diff --git a/src/__tests__/blur.js b/src/__tests__/blur.js new file mode 100644 index 00000000..c4035cac --- /dev/null +++ b/src/__tests__/blur.js @@ -0,0 +1,66 @@ +import {blur} from '../blur' +import {focus} from '../focus' +import {setup} from './helpers/utils' + +test('blur a button', () => { + const {element, getEventSnapshot, clearEventCalls} = setup(``) + const {element, eventWasFired} = setup(`
`) userEvent.click(element.children[0]) - expect(getEventCalls()).toContain('submit') + expect(eventWasFired('submit')).toBe(true) }) test('does not submit a form when clicking on a `) userEvent.click(element.children[0]) - expect(getEventCalls()).not.toContain('submit') + expect(getEventSnapshot()).not.toContain('submit') }) test('does not fire blur on current element if is the same as previous', () => { - const {element, getEventCalls, clearEventCalls} = setup('