From 63347eb84a9174a306f1c5dda2e76562ce55c3d6 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 8 Apr 2018 03:48:12 -0700 Subject: [PATCH 1/8] feat(waitForElements): add implementation, tests, docs Add `mutationobserver-shim` to `devDependencies` to provide `MutationObserver` in jest tests where `jsdom` has no built-in support for it: https://github.com/jsdom/jsdom/issues/639 --- README.md | 45 ++++ package.json | 3 +- .../__snapshots__/waitForElements.js.snap | 51 +++++ src/__tests__/waitForElements.js | 204 ++++++++++++++++++ src/index.js | 1 + src/waitForElements.js | 43 ++++ 6 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/__snapshots__/waitForElements.js.snap create mode 100644 src/__tests__/waitForElements.js create mode 100644 src/waitForElements.js diff --git a/README.md b/README.md index 50cf689a..6b535037 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ when a real user uses it. * [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement) * [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement) * [`wait`](#wait) + * [`waitForElements`](#waitforelements) * [Custom Jest Matchers](#custom-jest-matchers) * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) @@ -305,6 +306,50 @@ The default `interval` is `50ms`. However it will run your callback immediately on the next tick of the event loop (in a `setTimeout`) before starting the intervals. +### `waitForElements` + +Defined as: + +```typescript +function waitForElements( + callback?: () => T | null | undefined, + options?: { + container?: HTMLElement + timeout?: number + mutationObserverOptions?: MutationObserverInit + }, +): Promise +``` + +When in need to wait for DOM elements to appear, disappear, or change you can use `waitForElements`. +The `waitForElements` function is a small wrapper +around the +[`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). +Here's a simple example: + +```javascript +// ... +// wait until the callback does not throw an error and returns a truthy value. In this case, that means +// it'll wait until we can get a form control with a label that matches "username" +// the difference from `wait` is that it reacts to DOM changes in the container +// and returns the value returned by the callback +const element = await waitForElements(() => getByLabelText(container, 'username')) +element.value = 'chucknorris' +// ... +``` + +Using `MutationObserver` is more efficient than polling the DOM at regular intervals with `wait`. + +The default `callback` is a no-op function (used like `await waitForElements()`). This can +be helpful if you only need to wait for the next DOM change (see `mutationObserverOptions` to learn which changes are detected). + +The default `timeout` is `4500ms` which will keep you under +[Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout). + +The default `mutationObserverOptions` is `{subtree: true, childList: true}` which will detect +additions and removals of child elements (including text nodes) in the `container` and any of its descendants. +It won't detect attribute changes unless you add `attributes: true` to the options. + ## Custom Jest Matchers There are two simple API which extend the `expect` API of jest for making assertions easier. diff --git a/package.json b/package.json index 436757f6..b4d25d3d 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ }, "devDependencies": { "jest-in-case": "^1.0.2", - "kcd-scripts": "^0.36.1" + "kcd-scripts": "^0.36.1", + "mutationobserver-shim": "^0.3.2" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", diff --git a/src/__tests__/__snapshots__/waitForElements.js.snap b/src/__tests__/__snapshots__/waitForElements.js.snap new file mode 100644 index 00000000..831bc96f --- /dev/null +++ b/src/__tests__/__snapshots__/waitForElements.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`it throws if timeout is exceeded 1`] = ` +Array [ + [Error: Timed out in waitForElements.], +] +`; + +exports[`it throws if timeout is exceeded 2`] = ` +
+`; + +exports[`it throws the same error that the callback has thrown if timeout is exceeded 1`] = ` +Array [ + [Error: Unable to find an element by: [data-testid="test"]], +] +`; + +exports[`it throws the same error that the callback has thrown if timeout is exceeded 2`] = ` +
+`; + +exports[`it waits for the callback to return a value and only reacts to DOM mutations 1`] = ` +
+
+
+
+
+
+
+
+
+`; diff --git a/src/__tests__/waitForElements.js b/src/__tests__/waitForElements.js new file mode 100644 index 00000000..66ec5bd6 --- /dev/null +++ b/src/__tests__/waitForElements.js @@ -0,0 +1,204 @@ +import 'mutationobserver-shim' +import {waitForElements} from '../' +// adds special assertions like toBeInTheDOM +import '../extend-expect' +import {render} from './helpers/test-utils' + +async function skipSomeTime(delayMs) { + await new Promise(resolve => setTimeout(resolve, delayMs)) +} + +async function skipSomeTimeForMutationObserver(delayMs = 50) { + // Using `setTimeout` >30ms instead of `wait` here because `mutationobserver-shim` uses `setTimeout` ~30ms. + await skipSomeTime(delayMs, 50) +} + +test('it waits for the callback to return a value and only reacts to DOM mutations', async () => { + const {container, getByTestId} = render( + `
`, + ) + + let nextElIndex = 0 + const makeMutationFn = () => () => { + container.appendChild( + render( + `
`, + ).container.firstChild, + ) + } + + const testEl = render( + `
`, + ).container.firstChild + testEl.parentNode.removeChild(testEl) + + const mutationsAndCallbacks = [ + [ + makeMutationFn(), + () => { + throw new Error('First exception.') + }, + ], + [ + makeMutationFn(), + () => { + throw new Error('Second exception.') + }, + ], + [makeMutationFn(), () => null], + [makeMutationFn(), () => undefined], + [makeMutationFn(), () => getByTestId('the-element-we-are-looking-for')], + [ + () => container.appendChild(testEl), + () => getByTestId('the-element-we-are-looking-for'), + ], + ] + + const callback = jest + .fn(() => { + throw new Error('This should be replaced with mockImplementation.') + }) + .mockName('callback') + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElements(callback, {container}).then( + successHandler, + errorHandler, + ) + + // No synchronous calls expected. + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + await skipSomeTimeForMutationObserver() + + // No calls without DOM mutations expected. + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + // Perform mutations one by one, waiting for each to trigger `MutationObserver`. + for (const [mutationImpl, callbackImpl] of mutationsAndCallbacks) { + callback.mockImplementation(callbackImpl) + mutationImpl() + await skipSomeTimeForMutationObserver() // eslint-disable-line no-await-in-loop + } + + expect(callback).toHaveBeenCalledTimes(mutationsAndCallbacks.length) + expect(successHandler).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledWith(testEl) + expect(errorHandler).toHaveBeenCalledTimes(0) + expect(container).toMatchSnapshot() + expect(testEl.parentNode).toBe(container) + + await promise +}) + +test('it waits for the attributes mutation if configured', async () => { + const {container} = render(``) + + const callback = jest + .fn(() => container.getAttribute('data-test-attribute')) + .mockName('callback') + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElements(callback, { + container, + mutationObserverOptions: {attributes: true}, + }).then(successHandler, errorHandler) + + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + await skipSomeTimeForMutationObserver() + + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'PASSED') + await skipSomeTimeForMutationObserver() + + expect(callback).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledWith('PASSED') + expect(errorHandler).toHaveBeenCalledTimes(0) + + await promise +}) + +test('it throws if timeout is exceeded', async () => { + const {container} = render(``) + + const callback = jest.fn(() => null).mockName('callback') + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElements(callback, { + container, + timeout: 300, + mutationObserverOptions: {attributes: true}, + }).then(successHandler, errorHandler) + + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'something changed once') + await skipSomeTimeForMutationObserver(200) + + expect(callback).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'something changed twice') + await skipSomeTimeForMutationObserver(150) + + expect(callback).toHaveBeenCalledTimes(2) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(1) + expect(errorHandler.mock.calls[0]).toMatchSnapshot() + expect(container).toMatchSnapshot() + + await promise +}) + +test('it throws the same error that the callback has thrown if timeout is exceeded', async () => { + const {container, getByTestId} = render(``) + + const callback = jest.fn(() => getByTestId('test')).mockName('callback') + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElements(callback, { + container, + timeout: 300, + mutationObserverOptions: {attributes: true}, + }).then(successHandler, errorHandler) + + expect(callback).toHaveBeenCalledTimes(0) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'something changed once') + await skipSomeTimeForMutationObserver(200) + + expect(callback).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'something changed twice') + await skipSomeTimeForMutationObserver(150) + + expect(callback).toHaveBeenCalledTimes(2) + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(1) + expect(errorHandler.mock.calls[0]).toMatchSnapshot() + expect(container).toMatchSnapshot() + + await promise +}) diff --git a/src/index.js b/src/index.js index e491fd3b..bb75a52f 100644 --- a/src/index.js +++ b/src/index.js @@ -6,4 +6,5 @@ export {queries} export * from './queries' export * from './wait' +export * from './waitForElements' export * from './matches' diff --git a/src/waitForElements.js b/src/waitForElements.js new file mode 100644 index 00000000..e5e2f064 --- /dev/null +++ b/src/waitForElements.js @@ -0,0 +1,43 @@ +function waitForElements( + callback = () => {}, + { + container = document, + timeout = 4500, + mutationObserverOptions = {subtree: true, childList: true}, + } = {}, +) { + return new Promise((resolve, reject) => { + // Disabling eslint prefer-const below: either prefer-const or no-use-before-define triggers. + let lastError, observer, timer // eslint-disable-line prefer-const + function onDone(error, result) { + clearTimeout(timer) + observer.disconnect() + if (error) { + reject(error) + } else { + resolve(result) + } + } + function onMutation() { + try { + const result = callback() + if (result) { + onDone(null, result) + } + // If `callback` returns falsy value, wait for the next mutation or timeout. + } catch (error) { + // Save the callback error to reject the promise with it. + lastError = error + // If `callback` throws an error, wait for the next mutation or timeout. + } + } + function onTimeout() { + onDone(lastError || new Error('Timed out in waitForElements.'), null) + } + timer = setTimeout(onTimeout, timeout) + observer = new MutationObserver(onMutation) + observer.observe(container, mutationObserverOptions) + }) +} + +export {waitForElements} From 5c4061834cf96bd8b878bf1a0ccc355da7d4c42e Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 8 Apr 2018 03:50:14 -0700 Subject: [PATCH 2/8] docs(contributors): update sompylasar --- .all-contributorsrc | 4 +++- README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c55c9d1d..96277113 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -115,7 +115,9 @@ "profile": "https://sompylasar.github.io", "contributions": [ "bug", - "ideas" + "ideas", + "code", + "doc" ] }, { diff --git a/README.md b/README.md index 6b535037..9a9e6aa5 100644 --- a/README.md +++ b/README.md @@ -644,7 +644,7 @@ Thanks goes to these people ([emoji key][emojis]): | [
Kent C. Dodds](https://kentcdodds.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Documentation") [πŸš‡](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=kentcdodds "Tests") | [
Ryan Castner](http://audiolion.github.io)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=audiolion "Documentation") | [
Daniel Sandiego](https://www.dnlsandiego.com)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=dnlsandiego "Code") | [
PaweΕ‚ MikoΕ‚ajczyk](https://github.com/Miklet)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=Miklet "Code") | [
Alejandro ÑÑñez Ortiz](http://co.linkedin.com/in/alejandronanez/)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=alejandronanez "Documentation") | [
Matt Parrish](https://github.com/pbomb)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Apbomb "Bug reports") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=pbomb "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[πŸ“¦](#platform-wKovacs64 "Packaging/porting to new platform") | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [
Ernesto GarcΓ­a](http://gnapse.github.io)
[πŸ’¬](#question-gnapse "Answering Questions") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") | +| [
Anto Aravinth](https://github.com/antoaravinth)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=antoaravinth "Documentation") | [
Jonah Moses](https://github.com/JonahMoses)
[πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=JonahMoses "Documentation") | [
Łukasz Gandecki](http://team.thebrain.pro)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Tests") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=lgandecki "Documentation") | [
Ivan Babak](https://sompylasar.github.io)
[πŸ›](https://github.com/kentcdodds/dom-testing-library/issues?q=author%3Asompylasar "Bug reports") [πŸ€”](#ideas-sompylasar "Ideas, Planning, & Feedback") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=sompylasar "Documentation") | [
Jesse Day](https://github.com/jday3)
[πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=jday3 "Code") | [
Ernesto GarcΓ­a](http://gnapse.github.io)
[πŸ’¬](#question-gnapse "Answering Questions") [πŸ’»](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Code") [πŸ“–](https://github.com/kentcdodds/dom-testing-library/commits?author=gnapse "Documentation") | From 1f46848a95735fd8c13a167b72f5b7109decf02d Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 8 Apr 2018 15:50:35 -0700 Subject: [PATCH 3/8] CR changes - rename `waitForElements` to `waitForElement` - move `mutationobserver-shim` to dependencies, import from `waitForElement` to provide the polyfill to the users of `dom-testing-library` - fix `kcd-scripts` version to match `master` branch - add synchronous `callback` call to detect an element if it's already present before any DOM mutation happens - add/change tests about synchronous `callback` call - tweak variable names in docs examples - add docs about the default `container` option value - add docs example about querying multiple elements - add docs about the `mutationobserver-shim` polyfill - add docs link and anchor to `mutationObserverOptions` - add docs link to MDN from the second mention of `MutationObserver` --- README.md | 36 +++++--- package.json | 6 +- ...lements.js.snap => waitForElement.js.snap} | 12 ++- .../{waitForElements.js => waitForElement.js} | 87 ++++++++++++++----- src/index.js | 2 +- src/{waitForElements.js => waitForElement.js} | 9 +- 6 files changed, 111 insertions(+), 41 deletions(-) rename src/__tests__/__snapshots__/{waitForElements.js.snap => waitForElement.js.snap} (81%) rename src/__tests__/{waitForElements.js => waitForElement.js} (72%) rename src/{waitForElements.js => waitForElement.js} (91%) diff --git a/README.md b/README.md index 9a9e6aa5..99e157e6 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ when a real user uses it. * [`getByText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbytextcontainer-htmlelement-text-textmatch-htmlelement) * [`getByAltText(container: HTMLElement, text: TextMatch): HTMLElement`](#getbyalttextcontainer-htmlelement-text-textmatch-htmlelement) * [`wait`](#wait) - * [`waitForElements`](#waitforelements) + * [`waitForElement`](#waitforelement) * [Custom Jest Matchers](#custom-jest-matchers) * [`toBeInTheDOM`](#tobeinthedom) * [`toHaveTextContent`](#tohavetextcontent) @@ -306,12 +306,12 @@ The default `interval` is `50ms`. However it will run your callback immediately on the next tick of the event loop (in a `setTimeout`) before starting the intervals. -### `waitForElements` +### `waitForElement` Defined as: ```typescript -function waitForElements( +function waitForElement( callback?: () => T | null | undefined, options?: { container?: HTMLElement @@ -321,8 +321,8 @@ function waitForElements( ): Promise ``` -When in need to wait for DOM elements to appear, disappear, or change you can use `waitForElements`. -The `waitForElements` function is a small wrapper +When in need to wait for DOM elements to appear, disappear, or change you can use `waitForElement`. +The `waitForElement` function is a small wrapper around the [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). Here's a simple example: @@ -331,22 +331,34 @@ Here's a simple example: // ... // wait until the callback does not throw an error and returns a truthy value. In this case, that means // it'll wait until we can get a form control with a label that matches "username" -// the difference from `wait` is that it reacts to DOM changes in the container +// the difference from `wait` is that rather than running your callback on +// an interval, it's run as soon as there are DOM changes in the container // and returns the value returned by the callback -const element = await waitForElements(() => getByLabelText(container, 'username')) -element.value = 'chucknorris' +const usernameElement = await waitForElement(() => getByLabelText(container, 'username')) +usernameElement.value = 'chucknorris' // ... ``` -Using `MutationObserver` is more efficient than polling the DOM at regular intervals with `wait`. +You can also wait for multiple elements at once: -The default `callback` is a no-op function (used like `await waitForElements()`). This can -be helpful if you only need to wait for the next DOM change (see `mutationObserverOptions` to learn which changes are detected). +```javascript +const [usernameElement, passwordElement] = waitForElement(() => [ + getByLabelText(container, 'username'), + getByLabelText(container, 'password'), +]) +``` + +Using [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is more efficient than polling the DOM at regular intervals with `wait`. This library sets up a [`'mutationobserver-shim'`](https://github.com/megawac/MutationObserver.js) on the global `window` object for cross-platform compatibility with older browsers and the [`jsdom`](https://github.com/jsdom/jsdom/issues/639) that is usually used in Node-based tests. + +The default `callback` is a no-op function (used like `await waitForElement()`). This can +be helpful if you only need to wait for the next DOM change (see [`mutationObserverOptions`](#mutationobserveroptions) to learn which changes are detected). + +The default `container` is the global `document`. Make sure the elements you wait for will be attached to it, or set a different `container`. The default `timeout` is `4500ms` which will keep you under [Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout). -The default `mutationObserverOptions` is `{subtree: true, childList: true}` which will detect +The default `mutationObserverOptions` is `{subtree: true, childList: true}` which will detect additions and removals of child elements (including text nodes) in the `container` and any of its descendants. It won't detect attribute changes unless you add `attributes: true` to the options. diff --git a/package.json b/package.json index b4d25d3d..dd8a24ee 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,12 @@ ], "dependencies": { "jest-matcher-utils": "^22.4.3", - "wait-for-expect": "^0.4.0" + "wait-for-expect": "^0.4.0", + "mutationobserver-shim": "^0.3.2" }, "devDependencies": { "jest-in-case": "^1.0.2", - "kcd-scripts": "^0.36.1", - "mutationobserver-shim": "^0.3.2" + "kcd-scripts": "^0.37.0" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js", diff --git a/src/__tests__/__snapshots__/waitForElements.js.snap b/src/__tests__/__snapshots__/waitForElement.js.snap similarity index 81% rename from src/__tests__/__snapshots__/waitForElements.js.snap rename to src/__tests__/__snapshots__/waitForElement.js.snap index 831bc96f..bb02523e 100644 --- a/src/__tests__/__snapshots__/waitForElements.js.snap +++ b/src/__tests__/__snapshots__/waitForElement.js.snap @@ -1,8 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`it returns immediately if the callback returns the value before any mutations 1`] = ` +
+
+
+`; + exports[`it throws if timeout is exceeded 1`] = ` Array [ - [Error: Timed out in waitForElements.], + [Error: Timed out in waitForElement.], ] `; diff --git a/src/__tests__/waitForElements.js b/src/__tests__/waitForElement.js similarity index 72% rename from src/__tests__/waitForElements.js rename to src/__tests__/waitForElement.js index 66ec5bd6..d3684a04 100644 --- a/src/__tests__/waitForElements.js +++ b/src/__tests__/waitForElement.js @@ -1,5 +1,4 @@ -import 'mutationobserver-shim' -import {waitForElements} from '../' +import {waitForElement, wait} from '../' // adds special assertions like toBeInTheDOM import '../extend-expect' import {render} from './helpers/test-utils' @@ -56,26 +55,31 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio const callback = jest .fn(() => { - throw new Error('This should be replaced with mockImplementation.') + throw new Error('No more calls are expected.') }) .mockName('callback') + .mockImplementation(() => { + throw new Error( + 'First callback call is synchronous, not returning any elements.', + ) + }) const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - const promise = waitForElements(callback, {container}).then( + const promise = waitForElement(callback, {container}).then( successHandler, errorHandler, ) - // No synchronous calls expected. - expect(callback).toHaveBeenCalledTimes(0) + // One synchronous `callback` call is expected. + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) await skipSomeTimeForMutationObserver() - // No calls without DOM mutations expected. - expect(callback).toHaveBeenCalledTimes(0) + // No more expected calls without DOM mutations. + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) @@ -86,7 +90,7 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio await skipSomeTimeForMutationObserver() // eslint-disable-line no-await-in-loop } - expect(callback).toHaveBeenCalledTimes(mutationsAndCallbacks.length) + expect(callback).toHaveBeenCalledTimes(1 + mutationsAndCallbacks.length) expect(successHandler).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledWith(testEl) expect(errorHandler).toHaveBeenCalledTimes(0) @@ -105,25 +109,25 @@ test('it waits for the attributes mutation if configured', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - const promise = waitForElements(callback, { + const promise = waitForElement(callback, { container, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) - expect(callback).toHaveBeenCalledTimes(0) + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) await skipSomeTimeForMutationObserver() - expect(callback).toHaveBeenCalledTimes(0) + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'PASSED') await skipSomeTimeForMutationObserver() - expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(2) expect(successHandler).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledWith('PASSED') expect(errorHandler).toHaveBeenCalledTimes(0) @@ -138,27 +142,27 @@ test('it throws if timeout is exceeded', async () => { const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - const promise = waitForElements(callback, { + const promise = waitForElement(callback, { container, timeout: 300, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) - expect(callback).toHaveBeenCalledTimes(0) + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed once') await skipSomeTimeForMutationObserver(200) - expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(2) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed twice') await skipSomeTimeForMutationObserver(150) - expect(callback).toHaveBeenCalledTimes(2) + expect(callback).toHaveBeenCalledTimes(3) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(1) expect(errorHandler.mock.calls[0]).toMatchSnapshot() @@ -174,27 +178,27 @@ test('it throws the same error that the callback has thrown if timeout is exceed const successHandler = jest.fn().mockName('successHandler') const errorHandler = jest.fn().mockName('errorHandler') - const promise = waitForElements(callback, { + const promise = waitForElement(callback, { container, timeout: 300, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) - expect(callback).toHaveBeenCalledTimes(0) + expect(callback).toHaveBeenCalledTimes(1) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed once') await skipSomeTimeForMutationObserver(200) - expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(2) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed twice') await skipSomeTimeForMutationObserver(150) - expect(callback).toHaveBeenCalledTimes(2) + expect(callback).toHaveBeenCalledTimes(3) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(1) expect(errorHandler.mock.calls[0]).toMatchSnapshot() @@ -202,3 +206,44 @@ test('it throws the same error that the callback has thrown if timeout is exceed await promise }) + +test('it returns immediately if the callback returns the value before any mutations', async () => { + const {container, getByTestId} = render( + `
`, + ) + + const callback = jest + .fn(() => getByTestId('initial-element')) + .mockName('callback') + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElement(callback, { + container, + timeout: 300, + mutationObserverOptions: {attributes: true}, + }).then(successHandler, errorHandler) + + // One synchronous `callback` call is expected. + expect(callback).toHaveBeenCalledTimes(1) + + // The promise callbacks are expected to be called asyncronously. + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + await wait() + expect(successHandler).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledWith(container.firstChild) + expect(errorHandler).toHaveBeenCalledTimes(0) + + container.setAttribute('data-test-attribute', 'something changed once') + await skipSomeTimeForMutationObserver(200) + + // No more calls are expected. + expect(callback).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledTimes(1) + expect(errorHandler).toHaveBeenCalledTimes(0) + + expect(container).toMatchSnapshot() + + await promise +}) diff --git a/src/index.js b/src/index.js index bb75a52f..af438504 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,5 @@ export {queries} export * from './queries' export * from './wait' -export * from './waitForElements' +export * from './waitForElement' export * from './matches' diff --git a/src/waitForElements.js b/src/waitForElement.js similarity index 91% rename from src/waitForElements.js rename to src/waitForElement.js index e5e2f064..e58bbf3f 100644 --- a/src/waitForElements.js +++ b/src/waitForElement.js @@ -1,4 +1,6 @@ -function waitForElements( +import 'mutationobserver-shim' + +function waitForElement( callback = () => {}, { container = document, @@ -32,12 +34,13 @@ function waitForElements( } } function onTimeout() { - onDone(lastError || new Error('Timed out in waitForElements.'), null) + onDone(lastError || new Error('Timed out in waitForElement.'), null) } timer = setTimeout(onTimeout, timeout) observer = new MutationObserver(onMutation) observer.observe(container, mutationObserverOptions) + onMutation() }) } -export {waitForElements} +export {waitForElement} From 88bab8733cbbf393404fc3fd37062163348cd2c1 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 8 Apr 2018 18:08:32 -0700 Subject: [PATCH 4/8] fix(waitForElement): ensure it works with default callback Should wait for the next DOM change, as advertised in the docs. The default value is `undefined` so that the `options` object can be used while still keeping the default callback: ``` waitForElement(undefined, {attributes: true}) ``` --- .../__snapshots__/waitForElement.js.snap | 40 ++++++++++++++----- src/__tests__/waitForElement.js | 29 ++++++++++++++ src/waitForElement.js | 10 ++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/__tests__/__snapshots__/waitForElement.js.snap b/src/__tests__/__snapshots__/waitForElement.js.snap index bb02523e..9ca53ee7 100644 --- a/src/__tests__/__snapshots__/waitForElement.js.snap +++ b/src/__tests__/__snapshots__/waitForElement.js.snap @@ -1,6 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`it returns immediately if the callback returns the value before any mutations 1`] = ` +exports[ + `it returns immediately if the callback returns the value before any mutations 1` +] = `
@@ -8,33 +10,39 @@ exports[`it returns immediately if the callback returns the value before any mut data-testid="initial-element" />
-`; +` exports[`it throws if timeout is exceeded 1`] = ` Array [ [Error: Timed out in waitForElement.], ] -`; +` exports[`it throws if timeout is exceeded 2`] = `
-`; +` -exports[`it throws the same error that the callback has thrown if timeout is exceeded 1`] = ` +exports[ + `it throws the same error that the callback has thrown if timeout is exceeded 1` +] = ` Array [ [Error: Unable to find an element by: [data-testid="test"]], ] -`; +` -exports[`it throws the same error that the callback has thrown if timeout is exceeded 2`] = ` +exports[ + `it throws the same error that the callback has thrown if timeout is exceeded 2` +] = `
-`; +` -exports[`it waits for the callback to return a value and only reacts to DOM mutations 1`] = ` +exports[ + `it waits for the callback to return a value and only reacts to DOM mutations 1` +] = `
-`; +` + +exports[`it waits for the next DOM mutation with default callback 1`] = ` + +
+ +` + +exports[`it waits for the next DOM mutation with default callback 2`] = ` + +
+ +` diff --git a/src/__tests__/waitForElement.js b/src/__tests__/waitForElement.js index d3684a04..bac0eabd 100644 --- a/src/__tests__/waitForElement.js +++ b/src/__tests__/waitForElement.js @@ -100,6 +100,35 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio await promise }) +test('it waits for the next DOM mutation with default callback', async () => { + const successHandler = jest.fn().mockName('successHandler') + const errorHandler = jest.fn().mockName('errorHandler') + + const promise = waitForElement().then(successHandler, errorHandler) + + // Promise callbacks are always asynchronous. + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + await skipSomeTimeForMutationObserver() + + // No more expected calls without DOM mutations. + expect(successHandler).toHaveBeenCalledTimes(0) + expect(errorHandler).toHaveBeenCalledTimes(0) + + document.body.appendChild(document.createElement('div')) + expect(document.body).toMatchSnapshot() + + await skipSomeTimeForMutationObserver() + + expect(successHandler).toHaveBeenCalledTimes(1) + expect(successHandler).toHaveBeenCalledWith(undefined) + expect(errorHandler).toHaveBeenCalledTimes(0) + expect(document.body).toMatchSnapshot() + + await promise +}) + test('it waits for the attributes mutation if configured', async () => { const {container} = render(``) diff --git a/src/waitForElement.js b/src/waitForElement.js index e58bbf3f..a392bda8 100644 --- a/src/waitForElement.js +++ b/src/waitForElement.js @@ -1,7 +1,7 @@ import 'mutationobserver-shim' function waitForElement( - callback = () => {}, + callback = undefined, { container = document, timeout = 4500, @@ -21,6 +21,10 @@ function waitForElement( } } function onMutation() { + if (callback === undefined) { + onDone(null, undefined) + return + } try { const result = callback() if (result) { @@ -39,7 +43,9 @@ function waitForElement( timer = setTimeout(onTimeout, timeout) observer = new MutationObserver(onMutation) observer.observe(container, mutationObserverOptions) - onMutation() + if (callback !== undefined) { + onMutation() + } }) } From b7158cf1b497043dff526ad0b31de31d566a508c Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 8 Apr 2018 18:14:26 -0700 Subject: [PATCH 5/8] CR: tweak docs examples for wait and waitForElement - use `container` in the examples as this is a more popular use case than the default value of global `document` - use full sentences with capital first letter and period in the example comments --- README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 99e157e6..192e6fe9 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,8 @@ Here's a simple example: ```javascript // ... -// wait until the callback does not throw an error. In this case, that means -// it'll wait until we can get a form control with a label that matches "username" +// Wait until the callback does not throw an error. In this case, that means +// it'll wait until we can get a form control with a label that matches "username". await wait(() => getByLabelText(container, 'username')) getByLabelText(container, 'username').value = 'chucknorris' // ... @@ -329,12 +329,15 @@ Here's a simple example: ```javascript // ... -// wait until the callback does not throw an error and returns a truthy value. In this case, that means -// it'll wait until we can get a form control with a label that matches "username" -// the difference from `wait` is that rather than running your callback on +// Wait until the callback does not throw an error and returns a truthy value. In this case, that means +// it'll wait until we can get a form control with a label that matches "username". +// The difference from `wait` is that rather than running your callback on // an interval, it's run as soon as there are DOM changes in the container -// and returns the value returned by the callback -const usernameElement = await waitForElement(() => getByLabelText(container, 'username')) +// and returns the value returned by the callback. +const usernameElement = await waitForElement( + () => getByLabelText(container, 'username'), + {container}, +) usernameElement.value = 'chucknorris' // ... ``` @@ -342,10 +345,13 @@ usernameElement.value = 'chucknorris' You can also wait for multiple elements at once: ```javascript -const [usernameElement, passwordElement] = waitForElement(() => [ - getByLabelText(container, 'username'), - getByLabelText(container, 'password'), -]) +const [usernameElement, passwordElement] = waitForElement( + () => [ + getByLabelText(container, 'username'), + getByLabelText(container, 'password'), + ], + {container}, +) ``` Using [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is more efficient than polling the DOM at regular intervals with `wait`. This library sets up a [`'mutationobserver-shim'`](https://github.com/megawac/MutationObserver.js) on the global `window` object for cross-platform compatibility with older browsers and the [`jsdom`](https://github.com/jsdom/jsdom/issues/639) that is usually used in Node-based tests. From 1252d0824d454b27ff8ed92390af681984069668 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Mon, 9 Apr 2018 11:55:51 -0700 Subject: [PATCH 6/8] CR: rename files to kebab-case --- .../{waitForElement.js.snap => wait-for-element.js.snap} | 0 src/__tests__/{waitForElement.js => wait-for-element.js} | 0 src/index.js | 2 +- src/{waitForElement.js => wait-for-element.js} | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename src/__tests__/__snapshots__/{waitForElement.js.snap => wait-for-element.js.snap} (100%) rename src/__tests__/{waitForElement.js => wait-for-element.js} (100%) rename src/{waitForElement.js => wait-for-element.js} (100%) diff --git a/src/__tests__/__snapshots__/waitForElement.js.snap b/src/__tests__/__snapshots__/wait-for-element.js.snap similarity index 100% rename from src/__tests__/__snapshots__/waitForElement.js.snap rename to src/__tests__/__snapshots__/wait-for-element.js.snap diff --git a/src/__tests__/waitForElement.js b/src/__tests__/wait-for-element.js similarity index 100% rename from src/__tests__/waitForElement.js rename to src/__tests__/wait-for-element.js diff --git a/src/index.js b/src/index.js index af438504..2a80220e 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,5 @@ export {queries} export * from './queries' export * from './wait' -export * from './waitForElement' +export * from './wait-for-element' export * from './matches' diff --git a/src/waitForElement.js b/src/wait-for-element.js similarity index 100% rename from src/waitForElement.js rename to src/wait-for-element.js From 414a3dca4f75abb64033d7e614255b66e42bf694 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Mon, 9 Apr 2018 12:01:57 -0700 Subject: [PATCH 7/8] CR: await promise -> return promise @kentcdodds: > Rather than `await promise`, I'd prefer `return promise`. Maybe I'm being irrational here, but it feels better to me. @sompylasar: > I'm changing this, but if this line was the only one with `await` expression, then `eslint` would say `async` function must have an `await`. We are lucky that there are more `await`s in all the tests. > > P.S. I don't agree with this rule because `async` functions have their use for the error handling; `async` function is just the one that is wrapped in a `return new Promise(...)`. --- src/__tests__/wait-for-element.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__tests__/wait-for-element.js b/src/__tests__/wait-for-element.js index bac0eabd..4a020e0a 100644 --- a/src/__tests__/wait-for-element.js +++ b/src/__tests__/wait-for-element.js @@ -97,7 +97,7 @@ test('it waits for the callback to return a value and only reacts to DOM mutatio expect(container).toMatchSnapshot() expect(testEl.parentNode).toBe(container) - await promise + return promise }) test('it waits for the next DOM mutation with default callback', async () => { @@ -126,7 +126,7 @@ test('it waits for the next DOM mutation with default callback', async () => { expect(errorHandler).toHaveBeenCalledTimes(0) expect(document.body).toMatchSnapshot() - await promise + return promise }) test('it waits for the attributes mutation if configured', async () => { @@ -161,7 +161,7 @@ test('it waits for the attributes mutation if configured', async () => { expect(successHandler).toHaveBeenCalledWith('PASSED') expect(errorHandler).toHaveBeenCalledTimes(0) - await promise + return promise }) test('it throws if timeout is exceeded', async () => { @@ -197,7 +197,7 @@ test('it throws if timeout is exceeded', async () => { expect(errorHandler.mock.calls[0]).toMatchSnapshot() expect(container).toMatchSnapshot() - await promise + return promise }) test('it throws the same error that the callback has thrown if timeout is exceeded', async () => { @@ -233,7 +233,7 @@ test('it throws the same error that the callback has thrown if timeout is exceed expect(errorHandler.mock.calls[0]).toMatchSnapshot() expect(container).toMatchSnapshot() - await promise + return promise }) test('it returns immediately if the callback returns the value before any mutations', async () => { @@ -274,5 +274,5 @@ test('it returns immediately if the callback returns the value before any mutati expect(container).toMatchSnapshot() - await promise + return promise }) From edfb24144f1bc1a34dfa4d3e6e91ff1b45a5d81d Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Mon, 9 Apr 2018 12:03:25 -0700 Subject: [PATCH 8/8] CR: shorter timeouts and wait times for quicker tests --- src/__tests__/wait-for-element.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/__tests__/wait-for-element.js b/src/__tests__/wait-for-element.js index 4a020e0a..90a6ab90 100644 --- a/src/__tests__/wait-for-element.js +++ b/src/__tests__/wait-for-element.js @@ -173,7 +173,7 @@ test('it throws if timeout is exceeded', async () => { const promise = waitForElement(callback, { container, - timeout: 300, + timeout: 70, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) @@ -182,14 +182,14 @@ test('it throws if timeout is exceeded', async () => { expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed once') - await skipSomeTimeForMutationObserver(200) + await skipSomeTimeForMutationObserver(50) expect(callback).toHaveBeenCalledTimes(2) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed twice') - await skipSomeTimeForMutationObserver(150) + await skipSomeTimeForMutationObserver(50) expect(callback).toHaveBeenCalledTimes(3) expect(successHandler).toHaveBeenCalledTimes(0) @@ -209,7 +209,7 @@ test('it throws the same error that the callback has thrown if timeout is exceed const promise = waitForElement(callback, { container, - timeout: 300, + timeout: 70, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) @@ -218,14 +218,14 @@ test('it throws the same error that the callback has thrown if timeout is exceed expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed once') - await skipSomeTimeForMutationObserver(200) + await skipSomeTimeForMutationObserver(50) expect(callback).toHaveBeenCalledTimes(2) expect(successHandler).toHaveBeenCalledTimes(0) expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed twice') - await skipSomeTimeForMutationObserver(150) + await skipSomeTimeForMutationObserver(50) expect(callback).toHaveBeenCalledTimes(3) expect(successHandler).toHaveBeenCalledTimes(0) @@ -249,7 +249,7 @@ test('it returns immediately if the callback returns the value before any mutati const promise = waitForElement(callback, { container, - timeout: 300, + timeout: 70, mutationObserverOptions: {attributes: true}, }).then(successHandler, errorHandler) @@ -265,7 +265,7 @@ test('it returns immediately if the callback returns the value before any mutati expect(errorHandler).toHaveBeenCalledTimes(0) container.setAttribute('data-test-attribute', 'something changed once') - await skipSomeTimeForMutationObserver(200) + await skipSomeTimeForMutationObserver(50) // No more calls are expected. expect(callback).toHaveBeenCalledTimes(1)