diff --git a/__tests__/react/clear.js b/__tests__/react/clear.js new file mode 100644 index 00000000..d07bcb83 --- /dev/null +++ b/__tests__/react/clear.js @@ -0,0 +1,70 @@ +import React from "react"; +import { cleanup, render, wait, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; +import userEvent from "../../src"; + +afterEach(cleanup); + +describe("userEvent.clear", () => { + it.each(["input", "textarea"])("should clear text in <%s>", type => { + const onChange = jest.fn(); + const { getByTestId } = render( + React.createElement(type, { + "data-testid": "input", + onChange: onChange, + value: "Hello, world!" + }) + ); + + const input = getByTestId("input"); + userEvent.clear(input); + expect(input.value).toBe(""); + }); + + it.each(["input", "textarea"])( + "should not clear when <%s> is disabled", + type => { + const text = "Hello, world!"; + const onChange = jest.fn(); + const { getByTestId } = render( + React.createElement(type, { + "data-testid": "input", + onChange: onChange, + value: text, + disabled: true + }) + ); + + const input = getByTestId("input"); + userEvent.clear(input); + expect(input).toHaveProperty("value", text); + } + ); + + it.each(["input", "textarea"])( + "should not clear when <%s> is readOnly", + type => { + const onChange = jest.fn(); + const onKeyDown = jest.fn(); + const onKeyUp = jest.fn(); + + const text = "Hello, world!"; + const { getByTestId } = render( + React.createElement(type, { + "data-testid": "input", + onChange, + onKeyDown, + onKeyUp, + value: text, + readOnly: true + }) + ); + + const input = getByTestId("input"); + userEvent.clear(input); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onKeyUp).toHaveBeenCalledTimes(1); + expect(input).toHaveProperty("value", text); + } + ); +}); diff --git a/src/index.js b/src/index.js index 075e3edb..33ce5f8f 100644 --- a/src/index.js +++ b/src/index.js @@ -105,6 +105,56 @@ function fireChangeEvent(event) { event.target.removeEventListener("blur", fireChangeEvent); } +function getSelectionBounds(element) { + return { + start: element.selectionStart, + end: element.selectionEnd + }; +} + +function deleteLeftOfCursor(element) { + const { start, end } = getSelectionBounds(element); + if (start === end) { + if (start === 0) { + // there's nothing left of cursor + return; + } + element.setSelectionRange(start - 1, end); + } + + const value = element.value || ""; + const updatedValue = value.substring(0, start) + value.substring(end); + element.value = updatedValue; + + element.setSelectionRange(start, start); +} + +const Keys = { + Backspace: { keyCode: 8, code: "Backspace", key: "Backspace" } +}; + +function backspace(element) { + const eventOptions = { + key: Keys.Backspace.key, + keyCode: Keys.Backspace.keyCode, + which: Keys.Backspace.keyCode + }; + fireEvent.keyDown(element, eventOptions); + fireEvent.keyUp(element, eventOptions); + + if (!element.readOnly) { + fireEvent.input(element, { + inputType: "deleteContentBackward" + }); + deleteLeftOfCursor(element); + } +} + +function selectAll(element) { + userEvent.dblClick(element); // simulate events (will not actually select) + element.setSelectionRange(0, element.value.length); +} + const userEvent = { click(element) { const focusedElement = element.ownerDocument.activeElement; @@ -175,6 +225,14 @@ const userEvent = { } }, + clear(element) { + if (element.disabled) return; + + selectAll(element); + backspace(element); + element.addEventListener("blur", fireChangeEvent); + }, + async type(element, text, userOpts = {}) { if (element.disabled) return; const defaultOpts = {