diff --git a/package-lock.json b/package-lock.json index 29aac2e8..778c91b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1985,9 +1985,9 @@ } }, "@testing-library/dom": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.8.0.tgz", - "integrity": "sha512-Dfk8AqRF0h6CuWxTH0nX/kbxWfCkmQtJ+7CuHej/vhd71jX+dZz5JMpxc32WFwrkwKnRoFtPgMauS8A/j8GrUg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.9.0.tgz", + "integrity": "sha512-WYnJx9I94cYKib/Ber2BU3v1dUB+4n5wnJpvWJLTiwgERRTSElsivEtfX5S0LSljS122One6Bewhx2kgoZKXzA==", "dev": true, "requires": { "@babel/runtime": "^7.10.2", diff --git a/package.json b/package.json index 72176493..5b895106 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/runtime": "^7.10.2" }, "devDependencies": { - "@testing-library/dom": "^7.8.0", + "@testing-library/dom": "^7.9.0", "@testing-library/jest-dom": "^5.9.0", "@testing-library/react": "^10.0.5", "kcd-scripts": "^6.2.0", @@ -48,7 +48,7 @@ "react-dom": "^16.13.1" }, "peerDependencies": { - "@testing-library/dom": ">=5" + "@testing-library/dom": ">=7.9.0" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", @@ -56,7 +56,17 @@ "jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/tabindex-no-positive": "off", "no-return-assign": "off" - } + }, + "overrides": [ + { + "files": [ + "**/__tests__/**" + ], + "rules": { + "no-console": "off" + } + } + ] }, "eslintIgnore": [ "node_modules", diff --git a/src/__tests__/type.js b/src/__tests__/type.js index 153ef0a4..66dbf67b 100644 --- a/src/__tests__/type.js +++ b/src/__tests__/type.js @@ -2,7 +2,7 @@ import React from 'react' import {render, screen} from '@testing-library/react' import userEvent from '../../src' -test.each(['input', 'textarea'])('should type text in <%s>', type => { +test.each(['input', 'textarea'])('should type text in <%s>', async type => { const onChange = jest.fn() render( React.createElement(type, { @@ -11,30 +11,30 @@ test.each(['input', 'textarea'])('should type text in <%s>', type => { }), ) const text = 'Hello, world!' - userEvent.type(screen.getByTestId('input'), text) + await userEvent.type(screen.getByTestId('input'), text) expect(onChange).toHaveBeenCalledTimes(text.length) expect(screen.getByTestId('input')).toHaveProperty('value', text) }) -test('should append text one by one', () => { +test('should append text one by one', async () => { const onChange = jest.fn() render() - userEvent.type(screen.getByTestId('input'), 'hello') - userEvent.type(screen.getByTestId('input'), ' world') + await userEvent.type(screen.getByTestId('input'), 'hello') + await userEvent.type(screen.getByTestId('input'), ' world') expect(onChange).toHaveBeenCalledTimes('hello world'.length) expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world') }) -test('should append text all at once', () => { +test('should append text all at once', async () => { const onChange = jest.fn() render() - userEvent.type(screen.getByTestId('input'), 'hello', {allAtOnce: true}) - userEvent.type(screen.getByTestId('input'), ' world', {allAtOnce: true}) + await userEvent.type(screen.getByTestId('input'), 'hello', {allAtOnce: true}) + await userEvent.type(screen.getByTestId('input'), ' world', {allAtOnce: true}) expect(onChange).toHaveBeenCalledTimes(2) expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world') }) -test('should not type when event.preventDefault() is called', () => { +test('should not type when event.preventDefault() is called', async () => { const onChange = jest.fn() const onKeydown = jest .fn() @@ -43,7 +43,7 @@ test('should not type when event.preventDefault() is called', () => { , ) const text = 'Hello, world!' - userEvent.type(screen.getByTestId('input'), text) + await userEvent.type(screen.getByTestId('input'), text) expect(onKeydown).toHaveBeenCalledTimes(text.length) expect(onChange).toHaveBeenCalledTimes(0) expect(screen.getByTestId('input')).not.toHaveProperty('value', text) @@ -51,7 +51,7 @@ test('should not type when event.preventDefault() is called', () => { test.each(['input', 'textarea'])( 'should not type when <%s> is disabled', - type => { + async type => { const onChange = jest.fn() render( React.createElement(type, { @@ -61,7 +61,7 @@ test.each(['input', 'textarea'])( }), ) const text = 'Hello, world!' - userEvent.type(screen.getByTestId('input'), text) + await userEvent.type(screen.getByTestId('input'), text) expect(onChange).not.toHaveBeenCalled() expect(screen.getByTestId('input')).toHaveProperty('value', '') }, @@ -69,7 +69,7 @@ test.each(['input', 'textarea'])( test.each(['input', 'textarea'])( 'should not type when <%s> is readOnly', - type => { + async type => { const onChange = jest.fn() const onKeyDown = jest.fn() const onKeyPress = jest.fn() @@ -85,7 +85,7 @@ test.each(['input', 'textarea'])( }), ) const text = 'Hello, world!' - userEvent.type(screen.getByTestId('input'), text) + await userEvent.type(screen.getByTestId('input'), text) expect(onKeyDown).toHaveBeenCalledTimes(text.length) expect(onKeyPress).toHaveBeenCalledTimes(text.length) expect(onKeyUp).toHaveBeenCalledTimes(text.length) @@ -154,7 +154,7 @@ test.each(['input', 'textarea'])( test.each(['input', 'textarea'])( 'should enter text in <%s> up to maxLength if provided', - type => { + async type => { const onChange = jest.fn() const onKeyDown = jest.fn() const onKeyPress = jest.fn() @@ -177,7 +177,7 @@ test.each(['input', 'textarea'])( const inputEl = screen.getByTestId('input') - userEvent.type(inputEl, text) + await userEvent.type(inputEl, text) expect(inputEl).toHaveProperty('value', slicedText) expect(onChange).toHaveBeenCalledTimes(slicedText.length) @@ -205,7 +205,7 @@ test.each(['input', 'textarea'])( test.each(['input', 'textarea'])( 'should append text in <%s> up to maxLength if provided', - type => { + async type => { const onChange = jest.fn() const onKeyDown = jest.fn() const onKeyPress = jest.fn() @@ -230,8 +230,8 @@ test.each(['input', 'textarea'])( const inputEl = screen.getByTestId('input') - userEvent.type(inputEl, text1) - userEvent.type(inputEl, text2) + await userEvent.type(inputEl, text1) + await userEvent.type(inputEl, text2) expect(inputEl).toHaveProperty('value', slicedText) expect(onChange).toHaveBeenCalledTimes(slicedText.length) diff --git a/src/__tests__/wrapping-in-act-is-unnecessary.js b/src/__tests__/wrapping-in-act-is-unnecessary.js index 54903d4f..350c052e 100644 --- a/src/__tests__/wrapping-in-act-is-unnecessary.js +++ b/src/__tests__/wrapping-in-act-is-unnecessary.js @@ -22,3 +22,30 @@ test('act necessitating side effect', () => { expect(effectCallback).toHaveBeenCalledTimes(1) }) + +test('act necessitating async side effect', async () => { + function TestComponent() { + const [renderMessage, setRenderMessage] = React.useState(false) + function handleChange() { + Promise.resolve().then(() => { + setRenderMessage(true) + }) + } + return ( +
+ +
{renderMessage ? 'MESSAGE' : null}
+
+ ) + } + render() + + // https://github.com/testing-library/dom-testing-library/pull/602 + // before our fixes in DOM Testing Library, we had to wrap + // this next line in act for this test to pass. + await userEvent.type(screen.getByRole('textbox'), 'a') + + expect(await screen.findByText('MESSAGE')).toBeInTheDocument() + + expect(console.error).not.toHaveBeenCalled() +}) diff --git a/src/index.js b/src/index.js index 390a3f77..a0920dd4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,20 +1,28 @@ -import {fireEvent} from '@testing-library/dom' +import { + getConfig as getDOMTestingLibraryConfig, + fireEvent, +} from '@testing-library/dom' function wait(time) { return new Promise(resolve => setTimeout(() => resolve(), 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 +30,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 +104,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 +122,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 +237,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 +265,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 +290,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) { @@ -288,7 +323,16 @@ function clear(element) { backspace(element) } -async function type(element, text, {allAtOnce = false, delay} = {}) { +// this needs to be wrapped in the asyncWrapper for React's act and angular's change detection +async function type(...args) { + let result + await getDOMTestingLibraryConfig().asyncWrapper(async () => { + result = await typeImpl(...args) + }) + return result +} + +async function typeImpl(element, text, {allAtOnce = false, delay} = {}) { if (element.disabled) return const previousText = element.value