diff --git a/.all-contributorsrc b/.all-contributorsrc
index 74a0edf5c..9a5fd3d65 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1020,7 +1020,8 @@
"doc",
"code",
"test",
- "review"
+ "review",
+ "infra"
]
},
{
@@ -1309,7 +1310,8 @@
"profile": "https://codepen.io/ariperkkio/",
"contributions": [
"bug",
- "code"
+ "code",
+ "doc"
]
},
{
@@ -1320,6 +1322,71 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "ph-fritsche",
+ "name": "Philipp Fritsche",
+ "avatar_url": "https://avatars.githubusercontent.com/u/39068198?v=4",
+ "profile": "https://github.com/ph-fritsche",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "renatoalencar",
+ "name": "Renato Alencar",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6964593?v=4",
+ "profile": "https://medium.com/@renatoalencar",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "SimenB",
+ "name": "Simen Bekkhus",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1404810?v=4",
+ "profile": "https://github.com/SimenB",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "gaearon",
+ "name": "Dan Abramov",
+ "avatar_url": "https://avatars.githubusercontent.com/u/810438?v=4",
+ "profile": "https://github.com/gaearon",
+ "contributions": [
+ "bug",
+ "review"
+ ]
+ },
+ {
+ "login": "MatanBobi",
+ "name": "Matan Borenkraout",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12711091?v=4",
+ "profile": "https://matan.io",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "simcha90",
+ "name": "simcha90",
+ "avatar_url": "https://avatars.githubusercontent.com/u/56388545?v=4",
+ "profile": "https://github.com/simcha90",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "amitmiran137",
+ "name": "Amit Miran",
+ "avatar_url": "https://avatars.githubusercontent.com/u/47772523?v=4",
+ "profile": "https://github.com/amitmiran137",
+ "contributions": [
+ "infra"
+ ]
}
],
"repoHost": "https://github.com"
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 33b5d0d84..816b629b7 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -31,6 +31,7 @@ supported version.
Relevant code or config
```js
+
```
What you did:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index c1802ea6a..a937c57d5 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -3,7 +3,7 @@ on:
push:
branches:
- '+([0-9])?(.{+([0-9]),x}).x'
- - 'master'
+ - 'main'
- 'next'
- 'next-major'
- 'beta'
@@ -16,19 +16,20 @@ jobs:
if: ${{ !contains(github.head_ref, 'all-contributors') }}
strategy:
matrix:
- node: [10.14.2, 12, 14, 15]
+ node: [10.14.2, 12, 14, 15, 16]
runs-on: ubuntu-latest
steps:
- name: π Cancel Previous Runs
- uses: styfle/cancel-workflow-action@0.6.0
- with:
- access_token: ${{ secrets.GITHUB_TOKEN }}
+ uses: styfle/cancel-workflow-action@0.9.0
- name: β¬οΈ Checkout repo
uses: actions/checkout@v2
+ with:
+ # required by codecov/codecov-action
+ fetch-depth: 0
- name: β Setup node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
@@ -37,30 +38,35 @@ jobs:
with:
useLockFile: false
+ # TODO: Can be removed if https://github.com/kentcdodds/kcd-scripts/pull/146 is released
+ - name: Verify format (`npm run format` committed?)
+ run: npm run format -- --check --no-write
+
- name: βΆοΈ Run validate script
run: npm run validate
- name: β¬οΈ Upload coverage report
uses: codecov/codecov-action@v1
+ with:
+ fail_ci_if_error: true
+ flags: node-${{ matrix.node }}
release:
needs: main
runs-on: ubuntu-latest
if:
${{ github.repository == 'testing-library/dom-testing-library' &&
- contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha',
+ contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha',
github.ref) && github.event_name == 'push' }}
steps:
- name: π Cancel Previous Runs
- uses: styfle/cancel-workflow-action@0.6.0
- with:
- access_token: ${{ secrets.GITHUB_TOKEN }}
+ uses: styfle/cancel-workflow-action@0.9.0
- name: β¬οΈ Checkout repo
uses: actions/checkout@v2
- name: β Setup node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v2
with:
node-version: 14
@@ -79,7 +85,7 @@ jobs:
branches: |
[
'+([0-9])?(.{+([0-9]),x}).x',
- 'master',
+ 'main',
'next',
'next-major',
{name: 'beta', prerelease: true},
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5a38dda93..5aa761468 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,20 +11,20 @@ series [How to Contribute to an Open Source Project on GitHub][egghead]
2. Run `npm run setup` to install dependencies and run validation
3. Create a branch for your PR with `git checkout -b pr/your-branch-name`
-> Tip: Keep your `master` branch pointing at the original repository and make
-> pull requests from branches on your fork. To do this, run:
+> Tip: Keep your `main` branch pointing at the original repository and make pull
+> requests from branches on your fork. To do this, run:
>
> ```
> git remote add upstream https://github.com/testing-library/dom-testing-library.git
> git fetch upstream
-> git branch --set-upstream-to=upstream/master master
+> git branch --set-upstream-to=upstream/main main
> ```
>
> This will add the original repository as a "remote" called "upstream," Then
-> fetch the git information from that remote, then set your local `master`
-> branch to use the upstream master branch whenever you run `git pull`. Then you
-> can make all of your pull request branches based on this `master` branch.
-> Whenever you want to update your version of `master`, do a regular `git pull`.
+> fetch the git information from that remote, then set your local `main` branch
+> to use the upstream main branch whenever you run `git pull`. Then you can make
+> all of your pull request branches based on this `main` branch. Whenever you
+> want to update your version of `main`, do a regular `git pull`.
## Committing and Pushing changes
@@ -32,18 +32,6 @@ Please make sure to run the tests before you commit your changes. You can run
`npm run test:update` which will update any snapshots that need updating. Make
sure to include those changes (if they exist) in your commit.
-### opt into git hooks
-
-There are git hooks set up with this project that are automatically installed
-when you install dependencies. They're really handy, but are turned off by
-default (so as to not hinder new contributors). You can opt into these by
-creating a file called `.opt-in` at the root of the project and putting this
-inside:
-
-```
-pre-commit
-```
-
## Help needed
Please checkout the [the open issues][issues]
diff --git a/README.md b/README.md
index 5301407f1..8a2494cfa 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
height="80"
width="80"
alt="octopus"
- src="https://raw.githubusercontent.com/testing-library/dom-testing-library/master/other/octopus.png"
+ src="https://raw.githubusercontent.com/testing-library/dom-testing-library/main/other/octopus.png"
/>
@@ -41,7 +41,7 @@ practices.
@@ -262,7 +262,7 @@ Thanks goes to these people ([emoji key][emojis]):
Ben Monro π» π€ β οΈ π |
Stephan Meijer π€ π» β οΈ |
JoΓ£o Forja π» β οΈ |
- Nick McCurdy π π» β οΈ π |
+ Nick McCurdy π π» β οΈ π π |
Caleb Meredith π» |
Marco Moretti π» β οΈ π |
@@ -300,10 +300,19 @@ Thanks goes to these people ([emoji key][emojis]):
Romain Trotard π» |
Thomas Marshall π» β οΈ |
johnjessewood π π» |
- Ari PerkkiΓΆ π π» |
+ Ari PerkkiΓΆ π π» π |
Nathan Force π» |
+ Philipp Fritsche π» |
+ Renato Alencar π» β οΈ |
+ Simen Bekkhus π |
+ Dan Abramov π π |
+ Matan Borenkraout π» |
+ simcha90 π» |
+
+
+ Amit Miran π |
@@ -332,11 +341,11 @@ Contributions of any kind welcome!
[downloads-badge]: https://img.shields.io/npm/dm/@testing-library/dom.svg?style=flat-square
[npmtrends]: http://www.npmtrends.com/@testing-library/dom
[license-badge]: https://img.shields.io/npm/l/@testing-library/dom.svg?style=flat-square
-[license]: https://github.com/testing-library/dom-testing-library/blob/master/LICENSE
+[license]: https://github.com/testing-library/dom-testing-library/blob/main/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
-[coc]: https://github.com/testing-library/dom-testing-library/blob/master/CODE_OF_CONDUCT.md
+[coc]: https://github.com/testing-library/dom-testing-library/blob/main/CODE_OF_CONDUCT.md
[github-watch-badge]: https://img.shields.io/github/watchers/testing-library/dom-testing-library.svg?style=social
[github-watch]: https://github.com/testing-library/dom-testing-library/watchers
[github-star-badge]: https://img.shields.io/github/stars/testing-library/dom-testing-library.svg?style=social
diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md
index 87d1f70b7..a2f1a4787 100644
--- a/other/MAINTAINING.md
+++ b/other/MAINTAINING.md
@@ -61,7 +61,7 @@ to release. See the next section on Releases for more about that.
## Release
-Our releases are automatic. They happen whenever code lands into `master`. A
+Our releases are automatic. They happen whenever code lands into `main`. A
GitHub Action gets kicked off and if it's successful, a tool called
[`semantic-release`](https://github.com/semantic-release/semantic-release) is
used to automatically publish a new release to npm as well as a changelog to
diff --git a/package.json b/package.json
index 8e7640b53..a679be250 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
},
"scripts": {
"build": "kcd-scripts build --no-ts-defs --ignore \"**/__tests__/**,**/__node_tests__/**,**/__mocks__/**\" && kcd-scripts build --no-ts-defs --bundle --no-clean",
+ "format": "kcd-scripts format",
"lint": "kcd-scripts lint",
"setup": "npm install && npm run validate -s",
"test": "kcd-scripts test",
@@ -35,7 +36,7 @@
},
"files": [
"dist",
- "types"
+ "types/*.d.ts"
],
"dependencies": {
"@babel/code-frame": "^7.10.4",
diff --git a/src/__tests__/helpers.js b/src/__tests__/helpers.js
index 1bc858374..eb86a1d10 100644
--- a/src/__tests__/helpers.js
+++ b/src/__tests__/helpers.js
@@ -5,10 +5,6 @@ import {
runWithRealTimers,
} from '../helpers'
-const globalObj = typeof window === 'undefined' ? global : window
-
-afterEach(() => jest.useRealTimers())
-
test('returns global document if exists', () => {
expect(getDocument()).toBe(document)
})
@@ -21,6 +17,11 @@ describe('window retrieval throws when given something other than a node', () =>
`"It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?"`,
)
})
+ test('Array as node', () => {
+ expect(() => getWindowFromNode([])).toThrowErrorMatchingInlineSnapshot(
+ `"It looks like you passed an Array instead of a DOM node. Did you do something like \`fireEvent.click(screen.getAllBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`?"`,
+ )
+ })
test('unknown as node', () => {
expect(() => getWindowFromNode({})).toThrowErrorMatchingInlineSnapshot(
`"Unable to find the \\"window\\" object for the given node. Please file an issue with the code that's causing you to see this error: https://github.com/testing-library/dom-testing-library/issues/new"`,
@@ -53,42 +54,66 @@ describe('query container validation throws when validation fails', () => {
})
})
-test('should always use realTimers before using callback when timers are faked with useFakeTimers', () => {
- const originalSetTimeout = globalObj.setTimeout
+describe('run with real timers', () => {
+ const realSetTimeout = global.setTimeout
+
+ afterEach(() => {
+ // restore timers replaced by jest.useFakeTimers()
+ jest.useRealTimers()
+ // restore setTimeout replaced by assignment
+ global.setTimeout = realSetTimeout
+ })
+
+ test('use real timers when timers are faked with jest.useFakeTimers(legacy)', () => {
+ // legacy timers use mocks and do not rely on a clock instance
+ jest.useFakeTimers('legacy')
+ runWithRealTimers(() => {
+ expect(global.setTimeout).toBe(realSetTimeout)
+ })
+ expect(global.setTimeout._isMockFunction).toBe(true)
+ expect(global.setTimeout.clock).toBeUndefined()
+ })
- // legacy timers use mocks and do not rely on a clock instance
- jest.useFakeTimers('legacy')
- runWithRealTimers(() => {
- expect(originalSetTimeout).toEqual(globalObj.setTimeout)
+ test('use real timers when timers are faked with jest.useFakeTimers(modern)', () => {
+ // modern timers use a clock instance instead of a mock
+ jest.useFakeTimers('modern')
+ runWithRealTimers(() => {
+ expect(global.setTimeout).toBe(realSetTimeout)
+ })
+ expect(global.setTimeout._isMockFunction).toBeUndefined()
+ expect(global.setTimeout.clock).toBeDefined()
})
- expect(globalObj.setTimeout._isMockFunction).toBe(true)
- expect(globalObj.setTimeout.clock).toBeUndefined()
- jest.useRealTimers()
+ test('do not use real timers when timers are not faked with jest.useFakeTimers', () => {
+ // useFakeTimers is not used, timers are faked in some other way
+ const fakedSetTimeout = callback => {
+ callback()
+ }
+ fakedSetTimeout.clock = jest.fn()
+ global.setTimeout = fakedSetTimeout
- // modern timers use a clock instance instead of a mock
- jest.useFakeTimers('modern')
- runWithRealTimers(() => {
- expect(originalSetTimeout).toEqual(globalObj.setTimeout)
+ runWithRealTimers(() => {
+ expect(global.setTimeout).toBe(fakedSetTimeout)
+ })
+ expect(global.setTimeout).toBe(fakedSetTimeout)
})
- expect(globalObj.setTimeout._isMockFunction).toBeUndefined()
- expect(globalObj.setTimeout.clock).toBeDefined()
-})
-test('should not use realTimers when timers are not faked with useFakeTimers', () => {
- const originalSetTimeout = globalObj.setTimeout
+ describe('run with setImmediate and clearImmediate deleted', () => {
+ const setImmediate = global.setImmediate
+ const clearImmediate = global.clearImmediate
- // useFakeTimers is not used, timers are faked in some other way
- const fakedSetTimeout = callback => {
- callback()
- }
- fakedSetTimeout.clock = jest.fn()
+ beforeEach(() => {
+ delete global.setImmediate
+ delete global.clearImmediate
+ })
- globalObj.setTimeout = fakedSetTimeout
+ afterEach(() => {
+ global.setImmediate = setImmediate
+ global.clearImmediate = clearImmediate
+ })
- runWithRealTimers(() => {
- expect(fakedSetTimeout).toEqual(globalObj.setTimeout)
+ test('safe check for setImmediate and clearImmediate', () => {
+ expect(() => runWithRealTimers(() => {})).not.toThrow()
+ })
})
-
- globalObj.setTimeout = originalSetTimeout
})
diff --git a/src/config.ts b/src/config.ts
index ae80f4833..a6b807c03 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -77,7 +77,7 @@ export function runWithExpensiveErrorDiagnosticsDisabled(
}
}
-export function configure(newConfig: Partial | ConfigFn) {
+export function configure(newConfig: ConfigFn | Partial) {
if (typeof newConfig === 'function') {
// Pass the existing config out to the provided function
// and accept a delta in return
diff --git a/src/event-map.js b/src/event-map.js
index 6b0260945..e8cb6a5d2 100644
--- a/src/event-map.js
+++ b/src/event-map.js
@@ -1,350 +1,350 @@
export const eventMap = {
- // Clipboard Events
- copy: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- cut: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- paste: {
- EventType: 'ClipboardEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Composition Events
- compositionEnd: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- compositionStart: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- compositionUpdate: {
- EventType: 'CompositionEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Keyboard Events
- keyDown: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- keyPress: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- keyUp: {
- EventType: 'KeyboardEvent',
- defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
- },
- // Focus Events
- focus: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- blur: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- focusIn: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- focusOut: {
- EventType: 'FocusEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- // Form Events
- change: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: false},
- },
- input: {
- EventType: 'InputEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- invalid: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: true},
- },
- submit: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: true},
- },
- reset: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: true},
- },
- // Mouse Events
- click: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true},
- },
- contextMenu: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dblClick: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- drag: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragEnd: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragEnter: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragExit: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragLeave: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- dragOver: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- dragStart: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- drop: {
- EventType: 'DragEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseDown: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseEnter: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- mouseLeave: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: false, cancelable: false, composed: true},
- },
- mouseMove: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseOut: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseOver: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- mouseUp: {
- EventType: 'MouseEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Selection Events
- select: {
- EventType: 'Event',
- defaultInit: {bubbles: true, cancelable: false},
- },
- // Touch Events
- touchCancel: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- touchEnd: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- touchMove: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- touchStart: {
- EventType: 'TouchEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // UI Events
- scroll: {
- EventType: 'UIEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Wheel Events
- wheel: {
- EventType: 'WheelEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- // Media Events
- abort: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- canPlay: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- canPlayThrough: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- durationChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- emptied: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- encrypted: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- ended: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadedData: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadedMetadata: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- loadStart: {
- EventType: 'ProgressEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- pause: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- play: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- playing: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- progress: {
- EventType: 'ProgressEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- rateChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- seeked: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- seeking: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- stalled: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- suspend: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- timeUpdate: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- volumeChange: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- waiting: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Image Events
- load: {
- EventType: 'UIEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- error: {
- EventType: 'Event',
- defaultInit: {bubbles: false, cancelable: false},
- },
- // Animation Events
- animationStart: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- animationEnd: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- animationIteration: {
- EventType: 'AnimationEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- // Transition Events
- transitionEnd: {
- EventType: 'TransitionEvent',
- defaultInit: {bubbles: true, cancelable: true},
- },
- // pointer events
- pointerOver: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerEnter: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- pointerDown: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerMove: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerUp: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerCancel: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- pointerOut: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: true, composed: true},
- },
- pointerLeave: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: false, cancelable: false},
- },
- gotPointerCapture: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- lostPointerCapture: {
- EventType: 'PointerEvent',
- defaultInit: {bubbles: true, cancelable: false, composed: true},
- },
- // history events
- popState: {
- EventType: 'PopStateEvent',
- defaultInit: {bubbles: true, cancelable: false},
- },
- }
-
- export const eventAliasMap = {
- doubleClick: 'dblClick',
- }
+ // Clipboard Events
+ copy: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ cut: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ paste: {
+ EventType: 'ClipboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Composition Events
+ compositionEnd: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ compositionStart: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ compositionUpdate: {
+ EventType: 'CompositionEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Keyboard Events
+ keyDown: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ keyPress: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ keyUp: {
+ EventType: 'KeyboardEvent',
+ defaultInit: {bubbles: true, cancelable: true, charCode: 0, composed: true},
+ },
+ // Focus Events
+ focus: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ blur: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ focusIn: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ focusOut: {
+ EventType: 'FocusEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ // Form Events
+ change: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ input: {
+ EventType: 'InputEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ invalid: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: true},
+ },
+ submit: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ reset: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ // Mouse Events
+ click: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, button: 0, composed: true},
+ },
+ contextMenu: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dblClick: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ drag: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragEnd: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragEnter: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragExit: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragLeave: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ dragOver: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ dragStart: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ drop: {
+ EventType: 'DragEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseDown: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseEnter: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ mouseLeave: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: false, cancelable: false, composed: true},
+ },
+ mouseMove: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseOut: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseOver: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ mouseUp: {
+ EventType: 'MouseEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Selection Events
+ select: {
+ EventType: 'Event',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ // Touch Events
+ touchCancel: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ touchEnd: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ touchMove: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ touchStart: {
+ EventType: 'TouchEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // UI Events
+ scroll: {
+ EventType: 'UIEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Wheel Events
+ wheel: {
+ EventType: 'WheelEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ // Media Events
+ abort: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ canPlay: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ canPlayThrough: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ durationChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ emptied: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ encrypted: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ ended: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadedData: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadedMetadata: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ loadStart: {
+ EventType: 'ProgressEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ pause: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ play: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ playing: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ progress: {
+ EventType: 'ProgressEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ rateChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ seeked: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ seeking: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ stalled: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ suspend: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ timeUpdate: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ volumeChange: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ waiting: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Image Events
+ load: {
+ EventType: 'UIEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ error: {
+ EventType: 'Event',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ // Animation Events
+ animationStart: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ animationEnd: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ animationIteration: {
+ EventType: 'AnimationEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+ // Transition Events
+ transitionEnd: {
+ EventType: 'TransitionEvent',
+ defaultInit: {bubbles: true, cancelable: true},
+ },
+ // pointer events
+ pointerOver: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ pointerEnter: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ pointerDown: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ pointerMove: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ pointerUp: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ pointerCancel: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ pointerOut: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: true, composed: true},
+ },
+ pointerLeave: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: false, cancelable: false},
+ },
+ gotPointerCapture: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ lostPointerCapture: {
+ EventType: 'PointerEvent',
+ defaultInit: {bubbles: true, cancelable: false, composed: true},
+ },
+ // history events
+ popState: {
+ EventType: 'PopStateEvent',
+ defaultInit: {bubbles: true, cancelable: false},
+ },
+}
+
+export const eventAliasMap = {
+ doubleClick: 'dblClick',
+}
diff --git a/src/get-node-text.js b/src/get-node-text.ts
similarity index 75%
rename from src/get-node-text.js
rename to src/get-node-text.ts
index 54b29b7ce..fe718e0dd 100644
--- a/src/get-node-text.js
+++ b/src/get-node-text.ts
@@ -1,8 +1,8 @@
import {TEXT_NODE} from './helpers'
-function getNodeText(node) {
+function getNodeText(node: HTMLElement): string {
if (node.matches('input[type=submit], input[type=button]')) {
- return node.value
+ return (node as HTMLInputElement).value
}
return Array.from(node.childNodes)
diff --git a/src/helpers.js b/src/helpers.js
index f686d46ba..4536ddd52 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -5,52 +5,61 @@ const TEXT_NODE = 3
// Currently this fn only supports jest timers, but it could support other test runners in the future.
function runWithRealTimers(callback) {
- const fakeTimersType = getJestFakeTimersType()
- if (fakeTimersType) {
- jest.useRealTimers()
- }
+ return hasJestTimers()
+ ? runWithJestRealTimers(callback).callbackReturnValue
+ : // istanbul ignore next
+ callback()
+}
- const callbackReturnValue = callback()
+function hasJestTimers() {
+ return (
+ typeof jest !== 'undefined' &&
+ jest !== null &&
+ typeof jest.useRealTimers === 'function'
+ )
+}
- if (fakeTimersType) {
- jest.useFakeTimers(fakeTimersType)
+function runWithJestRealTimers(callback) {
+ const timerAPI = {
+ clearInterval,
+ clearTimeout,
+ setInterval,
+ setTimeout,
}
- return callbackReturnValue
-}
-
-function getJestFakeTimersType() {
- // istanbul ignore if
- if (
- typeof jest === 'undefined' ||
- typeof globalObj.setTimeout === 'undefined'
- ) {
- return null
+ // For more on why we have the check here,
+ // checkout https://github.com/testing-library/dom-testing-library/issues/914
+ if (typeof setImmediate === 'function') {
+ timerAPI.setImmediate = setImmediate
+ }
+ if (typeof clearImmediate === 'function') {
+ timerAPI.clearImmediate = clearImmediate
}
- if (
- typeof globalObj.setTimeout._isMockFunction !== 'undefined' &&
- globalObj.setTimeout._isMockFunction
- ) {
- return 'legacy'
+ jest.useRealTimers()
+
+ const callbackReturnValue = callback()
+
+ const usedFakeTimers = Object.entries(timerAPI).some(
+ ([name, func]) => func !== globalObj[name],
+ )
+
+ if (usedFakeTimers) {
+ jest.useFakeTimers(timerAPI.setTimeout?.clock ? 'modern' : 'legacy')
}
- if (
- typeof globalObj.setTimeout.clock !== 'undefined' &&
- typeof jest.getRealSystemTime !== 'undefined'
- ) {
- try {
- // jest.getRealSystemTime is only supported for Jest's `modern` fake timers and otherwise throws
- jest.getRealSystemTime()
- return 'modern'
- } catch {
- // not using Jest's modern fake timers
- }
+ return {
+ callbackReturnValue,
+ usedFakeTimers,
}
- return null
}
-const jestFakeTimersAreEnabled = () => Boolean(getJestFakeTimersType())
+function jestFakeTimersAreEnabled() {
+ return hasJestTimers()
+ ? runWithJestRealTimers(() => {}).usedFakeTimers
+ : // istanbul ignore next
+ false
+}
// we only run our tests in node, and setImmediate is supported in node.
// istanbul ignore next
@@ -92,6 +101,10 @@ function getWindowFromNode(node) {
throw new Error(
`It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?`,
)
+ } else if (Array.isArray(node)) {
+ throw new Error(
+ `It looks like you passed an Array instead of a DOM node. Did you do something like \`fireEvent.click(screen.getAllBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`?`,
+ )
} else {
// The user passed something unusual to a calling function
throw new Error(
diff --git a/src/label-helpers.ts b/src/label-helpers.ts
index e8010a1b8..4e1a3fed8 100644
--- a/src/label-helpers.ts
+++ b/src/label-helpers.ts
@@ -11,7 +11,7 @@ const labelledNodeNames = [
]
function getTextContent(
- node: Node | Element | HTMLInputElement,
+ node: Element | HTMLInputElement | Node,
): string | null {
if (labelledNodeNames.includes(node.nodeName.toLowerCase())) {
return ''
@@ -58,12 +58,14 @@ function getLabels(
container: Element,
element: Element,
{selector = '*'} = {},
-) {
+): {content: string | null; formControl: HTMLElement | null}[] {
const ariaLabelledBy = element.getAttribute('aria-labelledby')
const labelsId = ariaLabelledBy ? ariaLabelledBy.split(' ') : []
return labelsId.length
? labelsId.map(labelId => {
- const labellingElement = container.querySelector(`[id="${labelId}"]`)
+ const labellingElement = container.querySelector(
+ `[id="${labelId}"]`,
+ )
return labellingElement
? {content: getLabelContent(labellingElement), formControl: null}
: {content: '', formControl: null}
@@ -73,7 +75,7 @@ function getLabels(
const formControlSelector =
'button, input, meter, output, progress, select, textarea'
const labelledFormControl = Array.from(
- label.querySelectorAll(formControlSelector),
+ label.querySelectorAll(formControlSelector),
).filter(formControlElement => formControlElement.matches(selector))[0]
return {content: textToMatch, formControl: labelledFormControl}
})
diff --git a/src/matches.ts b/src/matches.ts
index 5e27ad3e1..b649cb69d 100644
--- a/src/matches.ts
+++ b/src/matches.ts
@@ -5,8 +5,6 @@ import {
DefaultNormalizerOptions,
} from '../types'
-type Nullish = T | null | undefined
-
function assertNotNullOrUndefined(
matcher: T,
): asserts matcher is NonNullable {
@@ -19,9 +17,9 @@ function assertNotNullOrUndefined(
}
function fuzzyMatches(
- textToMatch: Nullish,
- node: Nullish,
- matcher: Nullish,
+ textToMatch: string | null,
+ node: Element | null,
+ matcher: Matcher | null,
normalizer: NormalizerFn,
) {
if (typeof textToMatch !== 'string') {
@@ -43,9 +41,9 @@ function fuzzyMatches(
}
function matches(
- textToMatch: Nullish,
- node: Nullish,
- matcher: Nullish,
+ textToMatch: string | null,
+ node: Element | null,
+ matcher: Matcher | null,
normalizer: NormalizerFn,
) {
if (typeof textToMatch !== 'string') {
diff --git a/src/queries/all-utils.js b/src/queries/all-utils.ts
similarity index 100%
rename from src/queries/all-utils.js
rename to src/queries/all-utils.ts
diff --git a/src/queries/alt-text.js b/src/queries/alt-text.ts
similarity index 76%
rename from src/queries/alt-text.js
rename to src/queries/alt-text.ts
index a31c5164e..6be732773 100644
--- a/src/queries/alt-text.js
+++ b/src/queries/alt-text.ts
@@ -1,23 +1,26 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
+import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {matches, fuzzyMatches, makeNormalizer, buildQueries} from './all-utils'
-function queryAllByAltText(
+const queryAllByAltText: AllByBoundAttribute = (
container,
alt,
{exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
+) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll('img,input,area')).filter(node =>
+ return Array.from(
+ container.querySelectorAll('img,input,area'),
+ ).filter(node =>
matcher(node.getAttribute('alt'), node, alt, matchNormalizer),
)
}
-const getMultipleError = (c, alt) =>
+const getMultipleError: GetErrorFunction = (c, alt) =>
`Found multiple elements with the alt text: ${alt}`
-const getMissingError = (c, alt) =>
+const getMissingError: GetErrorFunction = (c, alt) =>
`Unable to find an element with the alt text: ${alt}`
const queryAllByAltTextWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/queries/display-value.js b/src/queries/display-value.ts
similarity index 58%
rename from src/queries/display-value.js
rename to src/queries/display-value.ts
index 75ab083fe..bab8a136e 100644
--- a/src/queries/display-value.js
+++ b/src/queries/display-value.ts
@@ -1,5 +1,6 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
+import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {
getNodeText,
matches,
@@ -8,33 +9,38 @@ import {
buildQueries,
} from './all-utils'
-function queryAllByDisplayValue(
+const queryAllByDisplayValue: AllByBoundAttribute = (
container,
value,
{exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
+) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll(`input,textarea,select`)).filter(
- node => {
- if (node.tagName === 'SELECT') {
- const selectedOptions = Array.from(node.options).filter(
- option => option.selected,
- )
- return selectedOptions.some(optionNode =>
- matcher(getNodeText(optionNode), optionNode, value, matchNormalizer),
- )
- } else {
- return matcher(node.value, node, value, matchNormalizer)
- }
- },
- )
+ return Array.from(
+ container.querySelectorAll(`input,textarea,select`),
+ ).filter(node => {
+ if (node.tagName === 'SELECT') {
+ const selectedOptions = Array.from(
+ (node as HTMLSelectElement).options,
+ ).filter(option => option.selected)
+ return selectedOptions.some(optionNode =>
+ matcher(getNodeText(optionNode), optionNode, value, matchNormalizer),
+ )
+ } else {
+ return matcher(
+ (node as HTMLInputElement).value,
+ node,
+ value,
+ matchNormalizer,
+ )
+ }
+ })
}
-const getMultipleError = (c, value) =>
+const getMultipleError: GetErrorFunction = (c, value) =>
`Found multiple elements with the display value: ${value}.`
-const getMissingError = (c, value) =>
+const getMissingError: GetErrorFunction = (c, value) =>
`Unable to find an element with the display value: ${value}.`
const queryAllByDisplayValueWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/queries/index.js b/src/queries/index.ts
similarity index 100%
rename from src/queries/index.js
rename to src/queries/index.ts
diff --git a/src/queries/label-text.js b/src/queries/label-text.ts
similarity index 84%
rename from src/queries/label-text.js
rename to src/queries/label-text.ts
index b844fb9c1..0b59d2535 100644
--- a/src/queries/label-text.js
+++ b/src/queries/label-text.ts
@@ -1,6 +1,7 @@
import {getConfig} from '../config'
import {checkContainerType} from '../helpers'
import {getLabels, getRealLabels, getLabelContent} from '../label-helpers'
+import {AllByText, GetErrorFunction} from '../../types'
import {
fuzzyMatches,
matches,
@@ -12,19 +13,21 @@ import {
wrapSingleQueryWithSuggestion,
} from './all-utils'
-function queryAllLabels(container) {
- return Array.from(container.querySelectorAll('label,input'))
+function queryAllLabels(
+ container: HTMLElement,
+): {textToMatch: string | null; node: HTMLElement}[] {
+ return Array.from(container.querySelectorAll('label,input'))
.map(node => {
return {node, textToMatch: getLabelContent(node)}
})
.filter(({textToMatch}) => textToMatch !== null)
}
-function queryAllLabelsByText(
+const queryAllLabelsByText: AllByText = (
container,
text,
{exact = true, trim, collapseWhitespace, normalizer} = {},
-) {
+) => {
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
@@ -37,27 +40,32 @@ function queryAllLabelsByText(
.map(({node}) => node)
}
-function queryAllByLabelText(
+const queryAllByLabelText: AllByText = (
container,
text,
{selector = '*', exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
+) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- const matchingLabelledElements = Array.from(container.querySelectorAll('*'))
+ const matchingLabelledElements = Array.from(
+ container.querySelectorAll('*'),
+ )
.filter(element => {
return (
getRealLabels(element).length || element.hasAttribute('aria-labelledby')
)
})
- .reduce((labelledElements, labelledElement) => {
+ .reduce((labelledElements, labelledElement) => {
const labelList = getLabels(container, labelledElement, {selector})
labelList
.filter(label => Boolean(label.formControl))
.forEach(label => {
- if (matcher(label.content, label.formControl, text, matchNormalizer))
+ if (
+ matcher(label.content, label.formControl, text, matchNormalizer) &&
+ label.formControl
+ )
labelledElements.push(label.formControl)
})
const labelsValue = labelList
@@ -92,6 +100,9 @@ function queryAllByLabelText(
return labelledElements
}, [])
.concat(
+ // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
queryAllByAttribute('aria-label', container, text, {
exact,
normalizer: matchNormalizer,
@@ -110,7 +121,7 @@ function queryAllByLabelText(
// )
// however, we can give a more helpful error message than the generic one,
// so we're writing this one out by hand.
-const getAllByLabelText = (container, text, ...rest) => {
+const getAllByLabelText: AllByText = (container, text, ...rest) => {
const els = queryAllByLabelText(container, text, ...rest)
if (!els.length) {
const labels = queryAllLabelsByText(container, text, ...rest)
@@ -146,7 +157,10 @@ const getAllByLabelText = (container, text, ...rest) => {
return els
}
-function getTagNameOfElementAssociatedWithLabelViaFor(container, label) {
+function getTagNameOfElementAssociatedWithLabelViaFor(
+ container: Element,
+ label: Element,
+): string | null {
const htmlFor = label.getAttribute('for')
if (!htmlFor) {
return null
@@ -157,7 +171,7 @@ function getTagNameOfElementAssociatedWithLabelViaFor(container, label) {
}
// the reason mentioned above is the same reason we're not using buildQueries
-const getMultipleError = (c, text) =>
+const getMultipleError: GetErrorFunction = (c, text) =>
`Found multiple elements with the text of: ${text}`
const queryByLabelText = wrapSingleQueryWithSuggestion(
makeSingleQuery(queryAllByLabelText, getMultipleError),
diff --git a/src/queries/placeholder-text.js b/src/queries/placeholder-text.ts
similarity index 68%
rename from src/queries/placeholder-text.js
rename to src/queries/placeholder-text.ts
index bdea59458..9c07c1371 100644
--- a/src/queries/placeholder-text.js
+++ b/src/queries/placeholder-text.ts
@@ -1,14 +1,18 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
+import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {queryAllByAttribute, buildQueries} from './all-utils'
-function queryAllByPlaceholderText(...args) {
- checkContainerType(...args)
+const queryAllByPlaceholderText: AllByBoundAttribute = (...args) => {
+ checkContainerType(args[0])
+ // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
return queryAllByAttribute('placeholder', ...args)
}
-const getMultipleError = (c, text) =>
+const getMultipleError: GetErrorFunction = (c, text) =>
`Found multiple elements with the placeholder text of: ${text}`
-const getMissingError = (c, text) =>
+const getMissingError: GetErrorFunction = (c, text) =>
`Unable to find an element with the placeholder text of: ${text}`
const queryAllByPlaceholderTextWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/queries/test-id.js b/src/queries/test-id.ts
similarity index 67%
rename from src/queries/test-id.js
rename to src/queries/test-id.ts
index f2ef5a9df..6a9c9c812 100644
--- a/src/queries/test-id.js
+++ b/src/queries/test-id.ts
@@ -1,17 +1,21 @@
import {checkContainerType} from '../helpers'
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
+import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {queryAllByAttribute, getConfig, buildQueries} from './all-utils'
const getTestIdAttribute = () => getConfig().testIdAttribute
-function queryAllByTestId(...args) {
- checkContainerType(...args)
+const queryAllByTestId: AllByBoundAttribute = (...args) => {
+ checkContainerType(args[0])
+ // TODO: Remove ignore after `queryAllByAttribute` will be moved to TS
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
return queryAllByAttribute(getTestIdAttribute(), ...args)
}
-const getMultipleError = (c, id) =>
+const getMultipleError: GetErrorFunction = (c, id) =>
`Found multiple elements by: [${getTestIdAttribute()}="${id}"]`
-const getMissingError = (c, id) =>
+const getMissingError: GetErrorFunction = (c, id) =>
`Unable to find an element by: [${getTestIdAttribute()}="${id}"]`
const queryAllByTestIdWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/queries/text.js b/src/queries/text.ts
similarity index 68%
rename from src/queries/text.js
rename to src/queries/text.ts
index 903bba25c..17faa17c7 100644
--- a/src/queries/text.js
+++ b/src/queries/text.ts
@@ -1,6 +1,7 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
import {DEFAULT_IGNORE_TAGS} from '../config'
+import {AllByText, GetErrorFunction} from '../../types'
import {
fuzzyMatches,
matches,
@@ -9,7 +10,7 @@ import {
buildQueries,
} from './all-utils'
-function queryAllByText(
+const queryAllByText: AllByText = (
container,
text,
{
@@ -20,22 +21,28 @@ function queryAllByText(
ignore = DEFAULT_IGNORE_TAGS,
normalizer,
} = {},
-) {
+) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- let baseArray = []
+ let baseArray: HTMLElement[] = []
if (typeof container.matches === 'function' && container.matches(selector)) {
baseArray = [container]
}
- return [...baseArray, ...Array.from(container.querySelectorAll(selector))]
- .filter(node => !ignore || !node.matches(ignore))
- .filter(node => matcher(getNodeText(node), node, text, matchNormalizer))
+ return (
+ [
+ ...baseArray,
+ ...Array.from(container.querySelectorAll(selector)),
+ ]
+ // TODO: `matches` according lib.dom.d.ts can get only `string` but according our code it can handle also boolean :)
+ .filter(node => !ignore || !node.matches(ignore as string))
+ .filter(node => matcher(getNodeText(node), node, text, matchNormalizer))
+ )
}
-const getMultipleError = (c, text) =>
+const getMultipleError: GetErrorFunction = (c, text) =>
`Found multiple elements with the text: ${text}`
-const getMissingError = (c, text) =>
+const getMissingError: GetErrorFunction = (c, text) =>
`Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`
const queryAllByTextWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/queries/title.js b/src/queries/title.ts
similarity index 77%
rename from src/queries/title.js
rename to src/queries/title.ts
index b56e6e05d..82fdab6b9 100644
--- a/src/queries/title.js
+++ b/src/queries/title.ts
@@ -1,5 +1,6 @@
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
+import {AllByBoundAttribute, GetErrorFunction} from '../../types'
import {
fuzzyMatches,
matches,
@@ -8,19 +9,21 @@ import {
buildQueries,
} from './all-utils'
-const isSvgTitle = node =>
+const isSvgTitle = (node: HTMLElement) =>
node.tagName.toLowerCase() === 'title' &&
node.parentElement?.tagName.toLowerCase() === 'svg'
-function queryAllByTitle(
+const queryAllByTitle: AllByBoundAttribute = (
container,
text,
{exact = true, collapseWhitespace, trim, normalizer} = {},
-) {
+) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
- return Array.from(container.querySelectorAll('[title], svg > title')).filter(
+ return Array.from(
+ container.querySelectorAll('[title], svg > title'),
+ ).filter(
node =>
matcher(node.getAttribute('title'), node, text, matchNormalizer) ||
(isSvgTitle(node) &&
@@ -28,9 +31,9 @@ function queryAllByTitle(
)
}
-const getMultipleError = (c, title) =>
+const getMultipleError: GetErrorFunction = (c, title) =>
`Found multiple elements with the title: ${title}.`
-const getMissingError = (c, title) =>
+const getMissingError: GetErrorFunction = (c, title) =>
`Unable to find an element with the title: ${title}.`
const queryAllByTitleWithSuggestions = wrapAllByQueryWithSuggestion(
diff --git a/src/wait-for-element-to-be-removed.js b/src/wait-for-element-to-be-removed.js
index ff9adb9d1..2b5bcb9a9 100644
--- a/src/wait-for-element-to-be-removed.js
+++ b/src/wait-for-element-to-be-removed.js
@@ -20,7 +20,7 @@ async function waitForElementToBeRemoved(callback, options) {
const elements = Array.isArray(callback) ? callback : [callback]
const getRemainingElements = elements.map(element => {
let parent = element.parentElement
- if(parent === null) return () => null
+ if (parent === null) return () => null
while (parent.parentElement) parent = parent.parentElement
return () => (parent.contains(element) ? element : null)
})
diff --git a/src/wait-for.js b/src/wait-for.js
index 9eb448421..860b7a15c 100644
--- a/src/wait-for.js
+++ b/src/wait-for.js
@@ -52,8 +52,8 @@ function waitFor(
const overallTimeoutTimer = setTimeout(handleTimeout, timeout)
- const usingFakeTimers = jestFakeTimersAreEnabled()
- if (usingFakeTimers) {
+ const usingJestFakeTimers = jestFakeTimersAreEnabled()
+ if (usingJestFakeTimers) {
checkCallback()
// this is a dangerous rule to disable because it could lead to an
// infinite loop. However, eslint isn't smart enough to know that we're
@@ -107,7 +107,7 @@ function waitFor(
finished = true
clearTimeout(overallTimeoutTimer)
- if (!usingFakeTimers) {
+ if (!usingJestFakeTimers) {
clearInterval(intervalId)
observer.disconnect()
}
diff --git a/types/__tests__/type-tests.ts b/types/__tests__/type-tests.ts
index 4f3dc5a93..9b5c81e8c 100644
--- a/types/__tests__/type-tests.ts
+++ b/types/__tests__/type-tests.ts
@@ -53,7 +53,7 @@ export async function testQueryHelpers() {
content.split(/\s+/).some(id => id === automationId)
const queryAllByAutomationId = (
container: HTMLElement,
- automationId: string | string[],
+ automationId: string[] | string,
options?: MatcherOptions,
) =>
queryAllByAttribute(
diff --git a/types/config.d.ts b/types/config.d.ts
index f85362cff..38504748a 100644
--- a/types/config.d.ts
+++ b/types/config.d.ts
@@ -17,5 +17,5 @@ export interface ConfigFn {
(existingConfig: Config): Partial
}
-export function configure(configDelta: Partial | ConfigFn): void
+export function configure(configDelta: ConfigFn | Partial): void
export function getConfig(): Config
diff --git a/types/get-node-text.d.ts b/types/get-node-text.d.ts
index 5c5654b5c..3f01b6291 100644
--- a/types/get-node-text.d.ts
+++ b/types/get-node-text.d.ts
@@ -1 +1 @@
-export function getNodeText(node: HTMLElement): string;
+export function getNodeText(node: HTMLElement): string
diff --git a/types/get-queries-for-element.d.ts b/types/get-queries-for-element.d.ts
index b93adfe14..04a485396 100644
--- a/types/get-queries-for-element.d.ts
+++ b/types/get-queries-for-element.d.ts
@@ -1,29 +1,40 @@
-import * as queries from './queries';
+import * as queries from './queries'
export type BoundFunction = T extends (
- attribute: string,
- element: HTMLElement,
- text: infer P,
- options: infer Q,
+ attribute: string,
+ element: HTMLElement,
+ text: infer P,
+ options: infer Q,
) => infer R
- ? (text: P, options?: Q) => R
- : T extends (a1: any, text: infer P, options: infer Q, waitForElementOptions: infer W) => infer R
- ? (text: P, options?: Q, waitForElementOptions?: W) => R
- : T extends (a1: any, text: infer P, options: infer Q) => infer R
- ? (text: P, options?: Q) => R
- : never;
-export type BoundFunctions = { [P in keyof T]: BoundFunction };
+ ? (text: P, options?: Q) => R
+ : T extends (
+ a1: any,
+ text: infer P,
+ options: infer Q,
+ waitForElementOptions: infer W,
+ ) => infer R
+ ? (text: P, options?: Q, waitForElementOptions?: W) => R
+ : T extends (a1: any, text: infer P, options: infer Q) => infer R
+ ? (text: P, options?: Q) => R
+ : never
+export type BoundFunctions = {[P in keyof T]: BoundFunction}
export type Query = (
- container: HTMLElement,
- ...args: any[]
-) => Error | Promise | Promise | HTMLElement[] | HTMLElement | null;
+ container: HTMLElement,
+ ...args: any[]
+) =>
+ | Error
+ | HTMLElement
+ | HTMLElement[]
+ | Promise
+ | Promise
+ | null
export interface Queries {
- [T: string]: Query;
+ [T: string]: Query
}
export function getQueriesForElement(
- element: HTMLElement,
- queriesToBind?: T,
-): BoundFunctions;
+ element: HTMLElement,
+ queriesToBind?: T,
+): BoundFunctions
diff --git a/types/index.d.ts b/types/index.d.ts
index 5b199dcf2..38830ff44 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1,25 +1,25 @@
// TypeScript Version: 3.8
-import { getQueriesForElement } from './get-queries-for-element';
-import * as queries from './queries';
-import * as queryHelpers from './query-helpers';
+import {getQueriesForElement} from './get-queries-for-element'
+import * as queries from './queries'
+import * as queryHelpers from './query-helpers'
-declare const within: typeof getQueriesForElement;
-export { queries, queryHelpers, within };
+declare const within: typeof getQueriesForElement
+export {queries, queryHelpers, within}
-export * from './queries';
-export * from './query-helpers';
-export * from './screen';
-export * from './wait';
-export * from './wait-for';
-export * from './wait-for-dom-change';
-export * from './wait-for-element';
-export * from './wait-for-element-to-be-removed';
-export * from './matches';
-export * from './get-node-text';
-export * from './events';
-export * from './get-queries-for-element';
-export * from './pretty-dom';
-export * from './role-helpers';
-export * from './config';
-export * from './suggestions';
+export * from './queries'
+export * from './query-helpers'
+export * from './screen'
+export * from './wait'
+export * from './wait-for'
+export * from './wait-for-dom-change'
+export * from './wait-for-element'
+export * from './wait-for-element-to-be-removed'
+export * from './matches'
+export * from './get-node-text'
+export * from './events'
+export * from './get-queries-for-element'
+export * from './pretty-dom'
+export * from './role-helpers'
+export * from './config'
+export * from './suggestions'
diff --git a/types/matches.d.ts b/types/matches.d.ts
index 2d05d4ab1..13fa36920 100644
--- a/types/matches.d.ts
+++ b/types/matches.d.ts
@@ -1,12 +1,10 @@
import {ARIARole} from 'aria-query'
-type Nullish = T | null | undefined
-
export type MatcherFunction = (
content: string,
- element: Nullish,
+ element: Element | null,
) => boolean
-export type Matcher = MatcherFunction | RegExp | string | number
+export type Matcher = MatcherFunction | RegExp | number | string
// Get autocomplete for ARIARole union types, while still supporting another string
// Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-505826972
diff --git a/types/queries.d.ts b/types/queries.d.ts
index d81fdceff..42777ffd3 100644
--- a/types/queries.d.ts
+++ b/types/queries.d.ts
@@ -108,8 +108,8 @@ export interface ByRoleOptions extends MatcherOptions {
* Only considers elements with the specified accessible name.
*/
name?:
- | string
| RegExp
+ | string
| ((accessibleName: string, element: Element) => boolean)
}
diff --git a/types/query-helpers.d.ts b/types/query-helpers.d.ts
index 26c80f191..28db0a317 100644
--- a/types/query-helpers.d.ts
+++ b/types/query-helpers.d.ts
@@ -1,8 +1,11 @@
import {Matcher, MatcherOptions} from './matches'
import {waitForOptions} from './wait-for'
+export type GetErrorFunction = (c: Element | null, alt: string) => string
+
export interface SelectorMatcherOptions extends MatcherOptions {
selector?: string
+ ignore?: boolean | string
}
export type QueryByAttribute = (
diff --git a/types/screen.d.ts b/types/screen.d.ts
index 9c04f4fc8..d7c53186a 100644
--- a/types/screen.d.ts
+++ b/types/screen.d.ts
@@ -8,7 +8,7 @@ export type Screen = BoundFunctions & {
* of elements
*/
debug: (
- element?: Element | HTMLDocument | Array,
+ element?: Array | Element | HTMLDocument,
maxLength?: number,
options?: OptionsReceived,
) => void
diff --git a/types/suggestions.d.ts b/types/suggestions.d.ts
index c27436417..e332dfdb9 100644
--- a/types/suggestions.d.ts
+++ b/types/suggestions.d.ts
@@ -14,30 +14,30 @@ export interface Suggestion {
}
export type Variant =
+ | 'find'
+ | 'findAll'
| 'get'
| 'getAll'
| 'query'
| 'queryAll'
- | 'find'
- | 'findAll'
export type Method =
- | 'Role'
- | 'role'
+ | 'AltText'
+ | 'alttext'
+ | 'DisplayValue'
+ | 'displayvalue'
| 'LabelText'
| 'labeltext'
| 'PlaceholderText'
| 'placeholdertext'
+ | 'Role'
+ | 'role'
+ | 'TestId'
+ | 'testid'
| 'Text'
| 'text'
- | 'DisplayValue'
- | 'displayvalue'
- | 'AltText'
- | 'alttext'
| 'Title'
| 'title'
- | 'TestId'
- | 'testid'
export function getSuggestedQuery(
element: HTMLElement,
diff --git a/types/wait-for-dom-change.d.ts b/types/wait-for-dom-change.d.ts
index 5b69a6294..44a098754 100644
--- a/types/wait-for-dom-change.d.ts
+++ b/types/wait-for-dom-change.d.ts
@@ -1,7 +1,7 @@
-import { waitForOptions } from "./wait-for";
+import {waitForOptions} from './wait-for'
/**
* @deprecated `waitForDomChange` has been deprecated.
* Use `waitFor` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.
*/
-export function waitForDomChange(options?: waitForOptions): Promise;
+export function waitForDomChange(options?: waitForOptions): Promise
diff --git a/types/wait-for-element-to-be-removed.d.ts b/types/wait-for-element-to-be-removed.d.ts
index d0daae536..e570aac7c 100644
--- a/types/wait-for-element-to-be-removed.d.ts
+++ b/types/wait-for-element-to-be-removed.d.ts
@@ -1,6 +1,6 @@
-import { waitForOptions } from "./wait-for";
+import {waitForOptions} from './wait-for'
export function waitForElementToBeRemoved(
- callback: (() => T) | T,
- options?: waitForOptions,
-): Promise;
+ callback: T | (() => T),
+ options?: waitForOptions,
+): Promise
diff --git a/types/wait-for-element.d.ts b/types/wait-for-element.d.ts
index 051227cb5..29e7e72cc 100644
--- a/types/wait-for-element.d.ts
+++ b/types/wait-for-element.d.ts
@@ -1,8 +1,11 @@
-import { waitForOptions } from "./wait-for";
+import {waitForOptions} from './wait-for'
/**
* @deprecated `waitForElement` has been deprecated.
* Use a `find*` query (preferred: https://testing-library.com/docs/dom-testing-library/api-queries#findby)
* or use `waitFor` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor
*/
-export function waitForElement(callback: () => T, options?: waitForOptions): Promise;
+export function waitForElement(
+ callback: () => T,
+ options?: waitForOptions,
+): Promise
diff --git a/types/wait-for.d.ts b/types/wait-for.d.ts
index f5850daca..ab1941695 100644
--- a/types/wait-for.d.ts
+++ b/types/wait-for.d.ts
@@ -7,6 +7,6 @@ export interface waitForOptions {
}
export function waitFor(
- callback: () => T | Promise,
+ callback: () => Promise | T,
options?: waitForOptions,
): Promise
diff --git a/types/wait.d.ts b/types/wait.d.ts
index 473e35f03..88015e713 100644
--- a/types/wait.d.ts
+++ b/types/wait.d.ts
@@ -4,9 +4,9 @@
* Learn more: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.
*/
export function wait(
- callback?: () => void,
- options?: {
- timeout?: number;
- interval?: number;
- },
-): Promise;
+ callback?: () => void,
+ options?: {
+ timeout?: number
+ interval?: number
+ },
+): Promise