diff --git a/__tests__/react/type.js b/__tests__/react/type.js index ca804e9b0..4576d5b0b 100644 --- a/__tests__/react/type.js +++ b/__tests__/react/type.js @@ -208,4 +208,353 @@ describe("userEvent.type", () => { expect(onKeyUp).not.toHaveBeenCalled(); } ); + + describe("special characters", () => { + afterEach(jest.clearAllMocks); + + const onChange = jest.fn().mockImplementation(e => e.persist()); + const onKeyDown = jest.fn().mockImplementation(e => e.persist()); + const onKeyPress = jest.fn().mockImplementation(e => e.persist()); + const onKeyUp = jest.fn().mockImplementation(e => e.persist()); + + it.each(["a{bc", "a{bc}", "a{backspacee}c"])( + "properly parses %s", + async text => { + const { getByTestId } = render( + React.createElement("input", { + "data-testid": "input" + }) + ); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + expect(inputEl).toHaveProperty("value", text); + } + ); + + describe("{enter}", () => { + describe("input", () => { + it("should record key up/down/press events from {enter}", async () => { + const { getByTestId } = render( + React.createElement("input", { + "data-testid": "input", + onChange, + onKeyDown, + onKeyPress, + onKeyUp + }) + ); + + const text = "abc{enter}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "abc"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(onChange).toHaveBeenCalledTimes(3); + expect(onKeyPress).toHaveBeenCalledTimes(4); + expect(onKeyDown).toHaveBeenCalledTimes(4); + expect(onKeyUp).toHaveBeenCalledTimes(4); + }); + }); + + describe("textarea", () => { + it("should be able to type newlines with {enter}", async () => { + const { getByTestId } = render( + React.createElement("textarea", { + "data-testid": "input", + onChange, + onKeyDown, + onKeyPress, + onKeyUp + }) + ); + + const text = "a{enter}{enter}b{enter}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a\n\nb\n"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(onChange).toHaveBeenCalledTimes(5); + expect(onKeyPress).toHaveBeenCalledTimes(5); + expect(onKeyDown).toHaveBeenCalledTimes(5); + expect(onKeyUp).toHaveBeenCalledTimes(5); + }); + }); + }); + + describe("{esc}", () => { + describe("input", () => { + it("should record key up/down/press events from {esc}", async () => { + const { getByTestId } = render( + React.createElement("input", { + "data-testid": "input", + onChange, + onKeyDown, + onKeyPress, + onKeyUp + }) + ); + + const text = "a{esc}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onKeyPress).toHaveBeenCalledTimes(1); + expect(onKeyDown).toHaveBeenCalledTimes(2); + expect(onKeyUp).toHaveBeenCalledTimes(2); + }); + }); + + describe("textarea", () => { + it("should be able to type newlines with {esc}", async () => { + const { getByTestId } = render( + React.createElement("textarea", { + "data-testid": "input", + onChange, + onKeyDown, + onKeyPress, + onKeyUp + }) + ); + + const text = "a{esc}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onKeyPress).toHaveBeenCalledTimes(1); + expect(onKeyDown).toHaveBeenCalledTimes(2); + expect(onKeyUp).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe("{backspace}", () => { + describe.each(["input", "textarea"])("%s", elementType => { + it.each([ + [ + "ab{backspace}c", + "ac", + { keyDown: 4, keyUp: 4, keyPress: 3, change: 4 } + ], + [ + "a{backspace}{backspace}bc", + "bc", + { keyDown: 5, keyUp: 5, keyPress: 3, change: 4 } + ], + [ + "a{{backspace}}", + "a}", + { keyDown: 4, keyUp: 4, keyPress: 3, change: 4 } + ] + ])( + "input `%s` should output `%s` and have the correct number of fired events", + async ( + typeText, + expectedText, + { + keyDown: numKeyDownEvents, + keyUp: numKeyUpEvents, + keyPress: numKeyPressEvents, + change: numOnChangeEvents + } + ) => { + const { getByTestId } = render( + React.createElement(elementType, { + "data-testid": "input", + onChange, + onKeyDown, + onKeyPress, + onKeyUp + }) + ); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, typeText); + + expect(inputEl).toHaveProperty("value", expectedText); + expect(onChange).toHaveBeenCalledTimes(numOnChangeEvents); + expect(onKeyDown).toHaveBeenCalledTimes(numKeyDownEvents); + expect(onKeyUp).toHaveBeenCalledTimes(numKeyUpEvents); + expect(onKeyPress).toHaveBeenCalledTimes(numKeyPressEvents); + } + ); + }); + }); + + describe("modifiers", () => { + describe.each([ + ["shift", "Shift", "shiftKey"], + ["ctrl", "Control", "ctrlKey"], + ["alt", "Alt", "altKey"], + ["meta", "OS", "metaKey"] + ])("%s", (modifierText, modifierKey, modifierProperty) => { + describe.each(["input", "textarea"])("%s", elementType => { + it("only adds modifier to following keystroke", async () => { + const handler = jest.fn().mockImplementation(e => e.persist()); + + const { getByTestId } = render( + React.createElement(elementType, { + "data-testid": "input", + onKeyDown: handler, + onKeyPress: handler, + onKeyUp: handler + }) + ); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, `{${modifierText}}ab`); + + expect(inputEl).toHaveProperty("value", "ab"); + + expect(handler).toHaveBeenCalledWithEventAtIndex(0, { + type: "keydown", + key: modifierKey, + [modifierProperty]: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(1, { + type: "keydown", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(2, { + type: "keypress", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(3, { + type: "keyup", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(4, { + type: "keydown", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(5, { + type: "keypress", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(6, { + type: "keyup", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(7, { + type: "keyup", + key: modifierKey, + [modifierProperty]: false + }); + }); + }); + }); + + it("can handle multiple held modifiers", async () => { + const handler = jest.fn().mockImplementation(e => e.persist()); + + const { getByTestId } = render( + React.createElement("input", { + "data-testid": "input", + onKeyDown: handler, + onKeyPress: handler, + onKeyUp: handler + }) + ); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, "{ctrl}{shift}ab"); + + expect(inputEl).toHaveProperty("value", "ab"); + + expect(handler).toHaveBeenCalledTimes(10); + + expect(handler).toHaveBeenCalledWithEventAtIndex(0, { + type: "keydown", + key: "Control", + ctrlKey: false, + shiftKey: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(1, { + type: "keydown", + key: "Shift", + ctrlKey: true, + shiftKey: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(2, { + type: "keydown", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(3, { + type: "keypress", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(4, { + type: "keyup", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(5, { + type: "keydown", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(6, { + type: "keypress", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(7, { + type: "keyup", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(8, { + type: "keyup", + key: "Control", + ctrlKey: false, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(9, { + type: "keyup", + key: "Shift", + ctrlKey: false, + shiftKey: false + }); + }); + }); + }); }); diff --git a/__tests__/vue/type.js b/__tests__/vue/type.js index 4ce3fe7f5..ba5fe2a3c 100644 --- a/__tests__/vue/type.js +++ b/__tests__/vue/type.js @@ -189,4 +189,347 @@ describe("userEvent.type", () => { expect(keyup).not.toHaveBeenCalled(); } ); + + describe("special characters", () => { + it.each(["a{bc", "a{bc}", "a{backspacee}c"])( + "properly parses %s", + async text => { + const { getByTestId } = renderComponent("input"); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + expect(inputEl).toHaveProperty("value", text); + } + ); + + describe("{enter}", () => { + describe("input", () => { + it("should record key up/down/press events from {enter}", async () => { + const input = jest.fn(); + const keydown = jest.fn(); + const keypress = jest.fn(); + const keyup = jest.fn(); + + const { getByTestId } = renderComponent("input", { + input, + keydown, + keypress, + keyup + }); + + const text = "abc{enter}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "abc"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(input).toHaveBeenCalledTimes(3); + expect(keypress).toHaveBeenCalledTimes(4); + expect(keydown).toHaveBeenCalledTimes(4); + expect(keyup).toHaveBeenCalledTimes(4); + }); + }); + + describe("textarea", () => { + it("should be able to type newlines with {esc}", async () => { + const input = jest.fn(); + const keydown = jest.fn(); + const keypress = jest.fn(); + const keyup = jest.fn(); + + const { getByTestId } = renderComponent("textarea", { + input, + keydown, + keypress, + keyup + }); + + const text = "a{enter}{enter}b{enter}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a\n\nb\n"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(input).toHaveBeenCalledTimes(5); + expect(keypress).toHaveBeenCalledTimes(5); + expect(keydown).toHaveBeenCalledTimes(5); + expect(keyup).toHaveBeenCalledTimes(5); + }); + }); + }); + + describe("{esc}", () => { + describe("input", () => { + it("should record key up/down/press events from {esc}", async () => { + const input = jest.fn(); + const keydown = jest.fn(); + const keypress = jest.fn(); + const keyup = jest.fn(); + + const { getByTestId } = renderComponent("input", { + input, + keydown, + keypress, + keyup + }); + + const text = "a{esc}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(input).toHaveBeenCalledTimes(1); + expect(keypress).toHaveBeenCalledTimes(1); + expect(keydown).toHaveBeenCalledTimes(2); + expect(keyup).toHaveBeenCalledTimes(2); + }); + }); + + describe("textarea", () => { + it("should be able to type newlines with {esc}", async () => { + const input = jest.fn(); + const keydown = jest.fn(); + const keypress = jest.fn(); + const keyup = jest.fn(); + + const { getByTestId } = renderComponent("textarea", { + input, + keydown, + keypress, + keyup + }); + + const text = "a{esc}"; + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, text); + + const expectedText = "a"; + + expect(inputEl).toHaveProperty("value", expectedText); + expect(input).toHaveBeenCalledTimes(1); + expect(keypress).toHaveBeenCalledTimes(1); + expect(keydown).toHaveBeenCalledTimes(2); + expect(keyup).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe("{backspace}", () => { + describe.each(["input", "textarea"])("%s", elementType => { + it.each([ + [ + "ab{backspace}c", + "ac", + { keyDown: 4, keyUp: 4, keyPress: 3, input: 4 } + ], + [ + "a{backspace}{backspace}bc", + "bc", + { keyDown: 5, keyUp: 5, keyPress: 3, input: 4 } + ], + [ + "a{{backspace}}", + "a}", + { keyDown: 4, keyUp: 4, keyPress: 3, input: 4 } + ] + ])( + "input `%s` should output `%s` and have the correct number of fired events", + async ( + typeText, + expectedText, + { + keyDown: numKeyDownEvents, + keyUp: numKeyUpEvents, + keyPress: numKeyPressEvents, + input: numInputEvents + } + ) => { + const input = jest.fn(); + const keydown = jest.fn(); + const keypress = jest.fn(); + const keyup = jest.fn(); + + const { getByTestId } = renderComponent(elementType, { + input, + keydown, + keypress, + keyup + }); + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, typeText); + + expect(inputEl).toHaveProperty("value", expectedText); + expect(input).toHaveBeenCalledTimes(numInputEvents); + expect(keydown).toHaveBeenCalledTimes(numKeyDownEvents); + expect(keyup).toHaveBeenCalledTimes(numKeyUpEvents); + expect(keypress).toHaveBeenCalledTimes(numKeyPressEvents); + } + ); + }); + }); + + describe("modifiers", () => { + describe.each([ + ["shift", "Shift", "shiftKey"], + ["ctrl", "Control", "ctrlKey"], + ["alt", "Alt", "altKey"], + ["meta", "OS", "metaKey"] + ])("%s", (modifierText, modifierKey, modifierProperty) => { + describe.each(["input", "textarea"])("%s", elementType => { + it("only adds modifier to following keystroke", async () => { + const handler = jest.fn(); + + const { getByTestId } = renderComponent(elementType, { + keydown: handler, + keypress: handler, + keyup: handler + }); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, `{${modifierText}}ab`); + + expect(inputEl).toHaveProperty("value", "ab"); + + expect(handler).toHaveBeenCalledTimes(8); + + expect(handler).toHaveBeenCalledWithEventAtIndex(0, { + type: "keydown", + key: modifierKey, + [modifierProperty]: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(1, { + type: "keydown", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(2, { + type: "keypress", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(3, { + type: "keyup", + key: "a", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(4, { + type: "keydown", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(5, { + type: "keypress", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(6, { + type: "keyup", + key: "b", + [modifierProperty]: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(7, { + type: "keyup", + key: modifierKey, + [modifierProperty]: false + }); + }); + }); + }); + + it("can handle multiple held modifiers", async () => { + const handler = jest.fn(); + + const { getByTestId } = renderComponent("input", { + keydown: handler, + keypress: handler, + keyup: handler + }); + + const inputEl = getByTestId("input"); + + await userEvent.type(inputEl, "{ctrl}{shift}ab"); + + expect(inputEl).toHaveProperty("value", "ab"); + + expect(handler).toHaveBeenCalledTimes(10); + + expect(handler).toHaveBeenCalledWithEventAtIndex(0, { + type: "keydown", + key: "Control", + ctrlKey: false, + shiftKey: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(1, { + type: "keydown", + key: "Shift", + ctrlKey: true, + shiftKey: false + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(2, { + type: "keydown", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(3, { + type: "keypress", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(4, { + type: "keyup", + key: "a", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(5, { + type: "keydown", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(6, { + type: "keypress", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(7, { + type: "keyup", + key: "b", + ctrlKey: true, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(8, { + type: "keyup", + key: "Control", + ctrlKey: false, + shiftKey: true + }); + expect(handler).toHaveBeenCalledWithEventAtIndex(9, { + type: "keyup", + key: "Shift", + ctrlKey: false, + shiftKey: false + }); + }); + }); + }); }); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..9c8024dc6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + setupFilesAfterEnv: ["/src/setupTests.js"] +}; diff --git a/src/index.js b/src/index.js index 075e3edb8..eef77fbc4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,5 @@ import { fireEvent } from "@testing-library/dom"; - -function wait(time) { - return new Promise(function(resolve) { - setTimeout(() => resolve(), time); - }); -} +import type from "./type"; function findTagInParents(element, tagName) { if (element.parentNode == null) return undefined; @@ -100,11 +95,6 @@ function selectOption(select, option) { fireEvent.change(select); } -function fireChangeEvent(event) { - fireEvent.change(event.target); - event.target.removeEventListener("blur", fireChangeEvent); -} - const userEvent = { click(element) { const focusedElement = element.ownerDocument.activeElement; @@ -175,71 +165,6 @@ const userEvent = { } }, - async type(element, text, userOpts = {}) { - if (element.disabled) return; - const defaultOpts = { - allAtOnce: false, - delay: 0 - }; - const opts = Object.assign(defaultOpts, userOpts); - - const computedText = text.slice(0, element.maxLength || text.length); - - const previousText = element.value; - - if (opts.allAtOnce) { - if (element.readOnly) return; - fireEvent.input(element, { - target: { value: previousText + computedText } - }); - } else { - let actuallyTyped = previousText; - for (let index = 0; index < text.length; index++) { - const char = text[index]; - const key = char; // TODO: check if this also valid for characters with diacritic markers e.g. úé etc - const keyCode = char.charCodeAt(0); - - if (opts.delay > 0) await wait(opts.delay); - - const downEvent = fireEvent.keyDown(element, { - key: key, - keyCode: keyCode, - which: keyCode - }); - - if (downEvent) { - const pressEvent = fireEvent.keyPress(element, { - key: key, - keyCode, - charCode: keyCode - }); - - const isTextPastThreshold = - (actuallyTyped + key).length > (previousText + computedText).length; - - if (pressEvent && !isTextPastThreshold) { - actuallyTyped += key; - if (!element.readOnly) - fireEvent.input(element, { - target: { - value: actuallyTyped - }, - bubbles: true, - cancelable: true - }); - } - } - - fireEvent.keyUp(element, { - key: key, - keyCode: keyCode, - which: keyCode - }); - } - } - element.addEventListener("blur", fireChangeEvent); - }, - tab({ shift = false, focusTrap = document } = {}) { const focusableElements = focusTrap.querySelectorAll( "input, button, select, textarea, a[href], [tabindex]" @@ -273,7 +198,9 @@ const userEvent = { } else { next.focus(); } - } + }, + + type }; export default userEvent; diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 000000000..cfd8593fc --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,36 @@ +import diff from "jest-diff"; + +const pick = (obj, keys) => + keys.reduce((acc, key) => ({ ...acc, [key]: obj[key] }), {}); + +expect.extend({ + toHaveBeenCalledWithEventAtIndex(received, callIndex, matchEvent) { + const event = received.mock.calls[callIndex][0]; + const keys = Object.keys(matchEvent); + + for (const key of keys) { + if (event[key] !== matchEvent[key]) { + const diffString = diff(matchEvent, pick(event, keys), { + expand: this.expand + }); + + return { + actual: received, + message: () => + `Expected event at call index ${callIndex} to be called with matching event properties.\n\n` + + `Difference:\n\n${diffString}`, + pass: false + }; + } + } + + return { + actual: received, + message: () => + `Expected event at call index ${callIndex} not to have any matching properties\n\n` + + `Expected: not ${this.utils.printExpected(matchEvent)}\n` + + `Received: ${this.utils.printReceived(pick(event, keys))}`, + pass: true + }; + } +}); diff --git a/src/type.js b/src/type.js new file mode 100644 index 000000000..eb690a156 --- /dev/null +++ b/src/type.js @@ -0,0 +1,184 @@ +import { fireEvent } from "@testing-library/dom"; + +const wait = time => { + return new Promise(resolve => { + setTimeout(() => resolve(), time); + }); +}; + +const fireChangeEvent = event => { + fireEvent.change(event.target); + event.target.removeEventListener("blur", fireChangeEvent); +}; + +const specialKeyMap = { + ["{enter}"]: { + key: "Enter", + code: 13, + typed: "\n" + }, + ["{esc}"]: { + key: "Escape", + code: 27, + skipPressEvent: true + }, + ["{backspace}"]: { + key: "Backspace", + code: 8, + inputType: "deleteWordBackward", + skipPressEvent: true + }, + ["{shift}"]: { + key: "Shift", + code: 16, + modifier: "shiftKey" + }, + ["{ctrl}"]: { + key: "Control", + code: 17, + modifier: "ctrlKey" + }, + ["{alt}"]: { + key: "Alt", + code: 18, + modifier: "altKey" + }, + ["{meta}"]: { + key: "OS", + code: 91, + modifier: "metaKey" + } +}; + +const parseIntoKeys = text => + text + .split(/({[^{}]+?})/) + .map(part => { + if (specialKeyMap[part]) { + return { + typed: "", + skipPressEvent: false, + ...specialKeyMap[part] + }; + } + + return Array.from(part).map(char => { + const code = char.charCodeAt(0); + + return { key: char, code, typed: char, skipPressEvent: false }; + }); + }) + .reduce((acc, next) => acc.concat(next), []); + +const commitKeyPress = ( + inputString = "", + { element, key: { typed = "", inputType } } +) => { + if (inputType === "deleteWordBackward") { + return inputString.slice(0, -1); + } + + if (typed === "\n" && element.tagName === "INPUT") { + return inputString; + } + + return inputString + typed; +}; + +const getModifiersFromKeys = keys => + keys.reduce((acc, next) => ({ ...acc, [next.modifier]: true }), {}); + +const makeKeyEvent = ({ key, heldKeys = [] }) => ({ + charCode: key.code, + key: key.key, + keyCode: key.code, + which: key.code, + ...getModifiersFromKeys(heldKeys) +}); + +const releaseHeldKeys = ({ element, heldKeys }) => { + heldKeys.forEach((heldKey, i) => { + fireEvent.keyUp( + element, + makeKeyEvent({ key: heldKey, heldKeys: heldKeys.slice(i + 1) }) + ); + }); +}; + +const fireTypeEvents = async ({ element, text, opts, computedText }) => { + const previousText = element.value; + let actuallyTyped = previousText; + let heldKeys = []; + const keys = parseIntoKeys(text); + + for (const key of keys) { + if (opts.delay > 0) await wait(opts.delay); + + const downEvent = fireEvent.keyDown( + element, + makeKeyEvent({ key, heldKeys }) + ); + + if (key.modifier) { + heldKeys.push(key); + continue; + } + + if (downEvent) { + const pressEvent = key.skipPressEvent + ? true + : fireEvent.keyPress(element, makeKeyEvent({ key, heldKeys })); + + const isTextPastThreshold = + (actuallyTyped + key.typed).length > + (previousText + computedText).length; + + if (pressEvent && !isTextPastThreshold) { + const lastTyped = actuallyTyped; + actuallyTyped = commitKeyPress(actuallyTyped, { + element, + key + }); + + if (!element.readOnly && lastTyped !== actuallyTyped) { + fireEvent.input(element, { + target: { + value: actuallyTyped + }, + inputType: key.inputType, + bubbles: true, + cancelable: true + }); + } + } + } + + fireEvent.keyUp(element, makeKeyEvent({ key, heldKeys })); + } + + releaseHeldKeys({ element, heldKeys }); +}; + +const type = async (element, text, userOpts = {}) => { + if (element.disabled) return; + const defaultOpts = { + allAtOnce: false, + delay: 0 + }; + const opts = Object.assign(defaultOpts, userOpts); + + const computedText = text.slice(0, element.maxLength || text.length); + + if (opts.allAtOnce) { + if (element.readOnly) return; + fireEvent.input(element, { + target: { value: element.value + computedText } + }); + } else { + fireTypeEvents({ element, text, opts, computedText }); + } + + element.addEventListener("blur", fireChangeEvent); +}; + +export default type;