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