From ebd7701d899fa00068188226e080952112e93c3b Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Wed, 27 May 2020 11:32:47 +0100 Subject: [PATCH 1/9] fix: allow focus to change between type events --- src/__tests__/type.js | 62 ++++++++++++++++++++- src/index.js | 122 +++++++++++++++++++++++++++--------------- 2 files changed, 141 insertions(+), 43 deletions(-) diff --git a/src/__tests__/type.js b/src/__tests__/type.js index 153ef0a4..160e9797 100644 --- a/src/__tests__/type.js +++ b/src/__tests__/type.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, {Fragment} from 'react' import {render, screen} from '@testing-library/react' import userEvent from '../../src' @@ -256,3 +256,63 @@ test.each(['input', 'textarea'])( expect(onKeyUp).not.toHaveBeenCalled() }, ) + +test('should fire events on the currently focussed element', async () => { + const changeFocusLimit = 7 + const onKeyDown = jest.fn(event => { + if (event.target.value.length === changeFocusLimit) { + screen.getByTestId('input2').focus() + } + }) + + render( + + + + , + ) + + const text = 'Hello, world!' + + const input1 = screen.getByTestId('input1') + const input2 = screen.getByTestId('input2') + + await userEvent.type(input1, text) + + expect(input1).toHaveValue(text.slice(0, changeFocusLimit)) + expect(input2).toHaveValue(text.slice(changeFocusLimit)) + expect(input2).toHaveFocus() +}) + +test('should enter text up to maxLength of the current element if provided', async () => { + const changeFocusLimit = 7 + const input2MaxLength = 2 + + const onKeyDown = jest.fn(event => { + if (event.target.value.length === changeFocusLimit) { + screen.getByTestId('input2').focus() + } + }) + + render( + + + + , + ) + + const text = 'Hello, world!' + const input2ExpectedValue = text.slice( + changeFocusLimit, + changeFocusLimit + input2MaxLength, + ) + + const input1 = screen.getByTestId('input') + const input2 = screen.getByTestId('input2') + + await userEvent.type(input1, text) + + expect(input1).toHaveValue(text.slice(0, changeFocusLimit)) + expect(input2).toHaveValue(input2ExpectedValue) + expect(input2).toHaveFocus() +}) diff --git a/src/index.js b/src/index.js index 390a3f77..0a3938c5 100644 --- a/src/index.js +++ b/src/index.js @@ -5,16 +5,21 @@ function wait(time) { } function isMousePressEvent(event) { - return event === 'mousedown' || event === 'mouseup' || event === 'click' || event === 'dblclick'; + return ( + event === 'mousedown' || + event === 'mouseup' || + event === 'click' || + event === 'dblclick' + ) } function invert(map) { - const res = {}; + const res = {} for (const key of Object.keys(map)) { - res[map[key]] = key; + res[map[key]] = key } - return res; + return res } // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons @@ -22,48 +27,51 @@ const BUTTONS_TO_NAMES = { 0: 'none', 1: 'primary', 2: 'secondary', - 4: 'auxiliary' -}; -const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES); + 4: 'auxiliary', +} +const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES) // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button const BUTTON_TO_NAMES = { 0: 'primary', 1: 'auxiliary', - 2: 'secondary' -}; + 2: 'secondary', +} -const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES); +const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES) function convertMouseButtons(event, init, property, mapping) { if (!isMousePressEvent(event)) { - return 0; + return 0 } if (init[property] != null) { - return init[property]; + return init[property] } if (init.buttons != null) { - return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0; + return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0 } if (init.button != null) { - return mapping[BUTTON_TO_NAMES[init.button]] || 0; + return mapping[BUTTON_TO_NAMES[init.button]] || 0 } - return property != 'button' && isMousePressEvent(event) ? 1 : 0; + return property != 'button' && isMousePressEvent(event) ? 1 : 0 } function getMouseEventOptions(event, init, clickCount = 0) { - init = init || {}; + init = init || {} return { ...init, // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail - detail: event === 'mousedown' || event === 'mouseup' ? 1 + clickCount : clickCount, + detail: + event === 'mousedown' || event === 'mouseup' + ? 1 + clickCount + : clickCount, buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS), button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON), - }; + } } function clickLabel(label, init) { @@ -93,7 +101,10 @@ function clickBooleanElement(element, init) { function clickElement(element, previousElement, init) { fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init)) fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init)) - const continueDefaultHandling = fireEvent.mouseDown(element, getMouseEventOptions('mousedown', init)) + const continueDefaultHandling = fireEvent.mouseDown( + element, + getMouseEventOptions('mousedown', init), + ) const shouldFocus = element.ownerDocument.activeElement !== element if (continueDefaultHandling) { if (previousElement) previousElement.blur() @@ -108,7 +119,10 @@ function clickElement(element, previousElement, init) { function dblClickElement(element, previousElement, init) { fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init)) fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init)) - const continueDefaultHandling = fireEvent.mouseDown(element, getMouseEventOptions('mousedown', init)) + const continueDefaultHandling = fireEvent.mouseDown( + element, + getMouseEventOptions('mousedown', init), + ) const shouldFocus = element.ownerDocument.activeElement !== element if (continueDefaultHandling) { if (previousElement) previousElement.blur() @@ -220,8 +234,14 @@ function getPreviouslyFocusedElement(element) { function click(element, init) { const previouslyFocusedElement = getPreviouslyFocusedElement(element) if (previouslyFocusedElement) { - fireEvent.mouseMove(previouslyFocusedElement, getMouseEventOptions('mousemove', init)) - fireEvent.mouseLeave(previouslyFocusedElement, getMouseEventOptions('mouseleave', init)) + fireEvent.mouseMove( + previouslyFocusedElement, + getMouseEventOptions('mousemove', init), + ) + fireEvent.mouseLeave( + previouslyFocusedElement, + getMouseEventOptions('mouseleave', init), + ) } switch (element.tagName) { @@ -242,8 +262,14 @@ function click(element, init) { function dblClick(element, init) { const previouslyFocusedElement = getPreviouslyFocusedElement(element) if (previouslyFocusedElement) { - fireEvent.mouseMove(previouslyFocusedElement, getMouseEventOptions('mousemove', init)) - fireEvent.mouseLeave(previouslyFocusedElement, getMouseEventOptions('mouseleave', init)) + fireEvent.mouseMove( + previouslyFocusedElement, + getMouseEventOptions('mousemove', init), + ) + fireEvent.mouseLeave( + previouslyFocusedElement, + getMouseEventOptions('mouseleave', init), + ) } switch (element.tagName) { @@ -261,16 +287,22 @@ function dblClick(element, init) { function selectOptions(element, values, init) { const previouslyFocusedElement = getPreviouslyFocusedElement(element) if (previouslyFocusedElement) { - fireEvent.mouseMove(previouslyFocusedElement, getMouseEventOptions('mousemove', init)) - fireEvent.mouseLeave(previouslyFocusedElement, getMouseEventOptions('mouseleave', init)) + fireEvent.mouseMove( + previouslyFocusedElement, + getMouseEventOptions('mousemove', init), + ) + fireEvent.mouseLeave( + previouslyFocusedElement, + getMouseEventOptions('mouseleave', init), + ) } clickElement(element, previouslyFocusedElement, init) const valArray = Array.isArray(values) ? values : [values] - const selectedOptions = Array.from( - element.querySelectorAll('option'), - ).filter(opt => valArray.includes(opt.value) || valArray.includes(opt)) + const selectedOptions = Array.from(element.querySelectorAll('option')).filter( + opt => valArray.includes(opt.value) || valArray.includes(opt), + ) if (selectedOptions.length > 0) { if (element.multiple) { @@ -290,21 +322,27 @@ function clear(element) { async function type(element, text, {allAtOnce = false, delay} = {}) { if (element.disabled) return - const previousText = element.value - const computedText = - element.maxLength > 0 - ? text.slice(0, Math.max(element.maxLength - previousText.length, 0)) - : text + element.focus() if (allAtOnce) { if (!element.readOnly) { + const previousText = element.value + + const computedText = + element.maxLength > 0 + ? text.slice(0, Math.max(element.maxLength - previousText.length, 0)) + : text + fireEvent.input(element, { target: {value: previousText + computedText}, }) } } else { - let actuallyTyped = previousText + // The focussed element could change between each event, so get the currently active element each time + const currentElement = () => document.activeElement + const actuallyTyped = () => document.activeElement.value + 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 @@ -313,28 +351,28 @@ async function type(element, text, {allAtOnce = false, delay} = {}) { // eslint-disable-next-line no-await-in-loop if (delay > 0) await wait(delay) - const downEvent = fireEvent.keyDown(element, { + const downEvent = fireEvent.keyDown(currentElement(), { key, keyCode, which: keyCode, }) if (downEvent) { - const pressEvent = fireEvent.keyPress(element, { + const pressEvent = fireEvent.keyPress(currentElement(), { key, keyCode, charCode: keyCode, }) const isTextPastThreshold = - (actuallyTyped + key).length > (previousText + computedText).length + (actuallyTyped() + key).length > + (currentElement().maxLength || text.length) if (pressEvent && !isTextPastThreshold) { - actuallyTyped += key if (!element.readOnly) { - fireEvent.input(element, { + fireEvent.input(currentElement(), { target: { - value: actuallyTyped, + value: actuallyTyped() + key, }, bubbles: true, cancelable: true, @@ -343,7 +381,7 @@ async function type(element, text, {allAtOnce = false, delay} = {}) { } } - fireEvent.keyUp(element, { + fireEvent.keyUp(currentElement(), { key, keyCode, which: keyCode, From 82b00f0f942225b0966c0b590eea263f26fb97e2 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Wed, 27 May 2020 14:06:23 +0100 Subject: [PATCH 2/9] update fragement convention --- src/__tests__/type.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/type.js b/src/__tests__/type.js index 160e9797..0013001f 100644 --- a/src/__tests__/type.js +++ b/src/__tests__/type.js @@ -295,10 +295,10 @@ test('should enter text up to maxLength of the current element if provided', asy }) render( - + <> - , + , ) const text = 'Hello, world!' From 08bed74853b168fd700687b082789c0b6283e4f4 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Wed, 3 Jun 2020 17:51:34 +0100 Subject: [PATCH 3/9] Update document.activeElement to use element.ownerDocument Co-authored-by: Kent C. Dodds --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index f971632e..f01bc49e 100644 --- a/src/index.js +++ b/src/index.js @@ -352,8 +352,8 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { } } else { // The focussed element could change between each event, so get the currently active element each time - const currentElement = () => document.activeElement - const actuallyTyped = () => document.activeElement.value + const currentElement = () => element.ownerDocument.activeElement + const actuallyTyped = () => element.ownerDocument.activeElement.value for (let index = 0; index < text.length; index++) { const char = text[index] From dd708bf8ef9c2936554f307887c6dbea4ea9c2bd Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Wed, 3 Jun 2020 17:57:36 +0100 Subject: [PATCH 4/9] Check element isn't disabled before firing events --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index f01bc49e..abc0ae24 100644 --- a/src/index.js +++ b/src/index.js @@ -363,6 +363,8 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { // eslint-disable-next-line no-await-in-loop if (delay > 0) await wait(delay) + if(currentElement.disabled) return + const downEvent = fireEvent.keyDown(currentElement(), { key, keyCode, From 9414e21b2d9debb89ef0497e06e89b9d289353de Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Thu, 4 Jun 2020 08:28:47 +0100 Subject: [PATCH 5/9] Add compute text function --- src/index.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index abc0ae24..5430f308 100644 --- a/src/index.js +++ b/src/index.js @@ -337,24 +337,27 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { element.focus() + // The focussed element could change between each event, so get the currently active element each time + const currentElement = () => element.ownerDocument.activeElement + const actuallyTyped = () => element.ownerDocument.activeElement.value + + const computeText = () => + currentElement().maxLength > 0 + ? text.slice( + 0, + Math.max(currentElement().maxLength - actuallyTyped().length, 0), + ) + : text + if (allAtOnce) { if (!element.readOnly) { const previousText = element.value - const computedText = - element.maxLength > 0 - ? text.slice(0, Math.max(element.maxLength - previousText.length, 0)) - : text - fireEvent.input(element, { - target: {value: previousText + computedText}, + target: {value: previousText + computeText()}, }) } } else { - // The focussed element could change between each event, so get the currently active element each time - const currentElement = () => element.ownerDocument.activeElement - const actuallyTyped = () => element.ownerDocument.activeElement.value - 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 @@ -363,7 +366,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { // eslint-disable-next-line no-await-in-loop if (delay > 0) await wait(delay) - if(currentElement.disabled) return + if (currentElement.disabled) return const downEvent = fireEvent.keyDown(currentElement(), { key, @@ -380,7 +383,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { const isTextPastThreshold = (actuallyTyped() + key).length > - (currentElement().maxLength || text.length) + (actuallyTyped() + computeText()).length if (pressEvent && !isTextPastThreshold) { if (!element.readOnly) { From 87fbb2a421331548469aee3e7d5f0a17c798c9d2 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Thu, 4 Jun 2020 08:43:33 +0100 Subject: [PATCH 6/9] Rename actuallyTyped to currentValue --- src/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 5430f308..d779a952 100644 --- a/src/index.js +++ b/src/index.js @@ -339,13 +339,13 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { // The focussed element could change between each event, so get the currently active element each time const currentElement = () => element.ownerDocument.activeElement - const actuallyTyped = () => element.ownerDocument.activeElement.value + const currentValue = () => element.ownerDocument.activeElement.value const computeText = () => currentElement().maxLength > 0 ? text.slice( 0, - Math.max(currentElement().maxLength - actuallyTyped().length, 0), + Math.max(currentElement().maxLength - currentValue().length, 0), ) : text @@ -382,14 +382,14 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { }) const isTextPastThreshold = - (actuallyTyped() + key).length > - (actuallyTyped() + computeText()).length + (currentValue() + key).length > + (currentValue() + computeText()).length if (pressEvent && !isTextPastThreshold) { if (!element.readOnly) { fireEvent.input(currentElement(), { target: { - value: actuallyTyped() + key, + value: currentValue() + key, }, bubbles: true, cancelable: true, From 3ebc8980e6da2eb110abca2cab140555111de5a6 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Thu, 4 Jun 2020 08:44:05 +0100 Subject: [PATCH 7/9] Simplify isTextPastThreshold check --- src/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index d779a952..b72d306c 100644 --- a/src/index.js +++ b/src/index.js @@ -381,9 +381,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { charCode: keyCode, }) - const isTextPastThreshold = - (currentValue() + key).length > - (currentValue() + computeText()).length + const isTextPastThreshold = !computeText().length if (pressEvent && !isTextPastThreshold) { if (!element.readOnly) { From 85fa272b3d22f49a6cbdd9cbc7047b1b6cfc0370 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Thu, 4 Jun 2020 16:06:38 +0100 Subject: [PATCH 8/9] Fix function call Co-authored-by: Kent C. Dodds --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index b72d306c..b1a8a1bf 100644 --- a/src/index.js +++ b/src/index.js @@ -366,7 +366,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { // eslint-disable-next-line no-await-in-loop if (delay > 0) await wait(delay) - if (currentElement.disabled) return + if (currentElement().disabled) return const downEvent = fireEvent.keyDown(currentElement(), { key, From 34ddb49f11b3c95c95e777f3da54c35c1ce19362 Mon Sep 17 00:00:00 2001 From: Kayleigh Ridd Date: Thu, 4 Jun 2020 16:06:53 +0100 Subject: [PATCH 9/9] Correct typo Co-authored-by: Kent C. Dodds --- src/__tests__/type.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/type.js b/src/__tests__/type.js index 3f4edd6a..26652f36 100644 --- a/src/__tests__/type.js +++ b/src/__tests__/type.js @@ -257,7 +257,7 @@ test.each(['input', 'textarea'])( }, ) -test('should fire events on the currently focussed element', async () => { +test('should fire events on the currently focused element', async () => { const changeFocusLimit = 7 const onKeyDown = jest.fn(event => { if (event.target.value.length === changeFocusLimit) {