diff --git a/.all-contributorsrc b/.all-contributorsrc index 6950e596..a621a1e6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -4,7 +4,7 @@ "repoType": "github", "repoHost": "https://github.com", "files": [ - "README.md" + "CONTRIBUTORS.md" ], "imageSize": 100, "commit": true, @@ -50,7 +50,8 @@ "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", "profile": "https://michaeldeboey.be", "contributions": [ - "doc" + "doc", + "ideas" ] }, { @@ -423,7 +424,9 @@ "question", "code", "test", - "doc" + "doc", + "infra", + "ideas" ] }, { @@ -432,7 +435,8 @@ "avatar_url": "https://avatars1.githubusercontent.com/u/28659384?v=4", "profile": "http://timdeschryver.dev", "contributions": [ - "test" + "test", + "doc" ] }, { @@ -568,7 +572,11 @@ "profile": "https://github.com/ph-fritsche", "contributions": [ "code", - "test" + "test", + "bug", + "ideas", + "infra", + "maintenance" ] }, { @@ -957,6 +965,239 @@ "contributions": [ "code" ] + }, + { + "login": "patricklizon", + "name": "Patrick LizoΕ„", + "avatar_url": "https://avatars.githubusercontent.com/u/12571855?v=4", + "profile": "https://github.com/patricklizon", + "contributions": [ + "code" + ] + }, + { + "login": "malipramod", + "name": "Pramod Mali", + "avatar_url": "https://avatars.githubusercontent.com/u/13375870?v=4", + "profile": "https://pramodmali.tech/", + "contributions": [ + "ideas" + ] + }, + { + "login": "wolfe111", + "name": "wolfe111", + "avatar_url": "https://avatars.githubusercontent.com/u/15180314?v=4", + "profile": "https://github.com/wolfe111", + "contributions": [ + "bug" + ] + }, + { + "login": "tyler2grass", + "name": "Tyler Grass", + "avatar_url": "https://avatars.githubusercontent.com/u/88393125?v=4", + "profile": "https://github.com/tyler2grass", + "contributions": [ + "bug" + ] + }, + { + "login": "micscopau", + "name": "Michael Pauly", + "avatar_url": "https://avatars.githubusercontent.com/u/7364791?v=4", + "profile": "https://www.linkedin.com/in/michael-s-pauly/", + "contributions": [ + "bug" + ] + }, + { + "login": "rbrady-hs", + "name": "rbrady-hs", + "avatar_url": "https://avatars.githubusercontent.com/u/83345629?v=4", + "profile": "https://github.com/rbrady-hs", + "contributions": [ + "ideas" + ] + }, + { + "login": "Dm1Korneev", + "name": "Dmitriy Кorneev", + "avatar_url": "https://avatars.githubusercontent.com/u/7955306?v=4", + "profile": "https://github.com/Dm1Korneev", + "contributions": [ + "bug" + ] + }, + { + "login": "kumachan-mis", + "name": "Kumachan", + "avatar_url": "https://avatars.githubusercontent.com/u/29433058?v=4", + "profile": "https://github.com/kumachan-mis", + "contributions": [ + "bug" + ] + }, + { + "login": "themadtitanmathos", + "name": "Matthew Lloyd Williamson", + "avatar_url": "https://avatars.githubusercontent.com/u/54560914?v=4", + "profile": "https://github.com/themadtitanmathos", + "contributions": [ + "ideas" + ] + }, + { + "login": "bamthomas", + "name": "Bruno Thomas", + "avatar_url": "https://avatars.githubusercontent.com/u/551723?v=4", + "profile": "https://github.com/bamthomas", + "contributions": [ + "bug" + ] + }, + { + "login": "antfu", + "name": "Anthony Fu", + "avatar_url": "https://avatars.githubusercontent.com/u/11247099?v=4", + "profile": "https://antfu.me/", + "contributions": [ + "bug" + ] + }, + { + "login": "mohetti", + "name": "momokolo", + "avatar_url": "https://avatars.githubusercontent.com/u/73931283?v=4", + "profile": "https://github.com/mohetti", + "contributions": [ + "bug" + ] + }, + { + "login": "dannyharding10", + "name": "Danny", + "avatar_url": "https://avatars.githubusercontent.com/u/11875246?v=4", + "profile": "https://github.com/dannyharding10", + "contributions": [ + "bug" + ] + }, + { + "login": "lucaslcode", + "name": "Lucas Levin", + "avatar_url": "https://avatars.githubusercontent.com/u/32044095?v=4", + "profile": "https://lucas-levin.com/", + "contributions": [ + "bug" + ] + }, + { + "login": "MatanBobi", + "name": "Matan Borenkraout", + "avatar_url": "https://avatars.githubusercontent.com/u/12711091?v=4", + "profile": "https://matan.io/", + "contributions": [ + "doc" + ] + }, + { + "login": "kentcdodds", + "name": "Kent C. Dodds", + "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=4", + "profile": "https://kentcdodds.com/", + "contributions": [ + "code", + "infra", + "maintenance", + "review", + "test" + ] + }, + { + "login": "Dennis273", + "name": "Dennis273", + "avatar_url": "https://avatars.githubusercontent.com/u/19815164?v=4", + "profile": "https://github.com/Dennis273", + "contributions": [ + "bug", + "code", + "test" + ] + }, + { + "login": "piecyk", + "name": "Damian Pieczynski", + "avatar_url": "https://avatars.githubusercontent.com/u/82964?v=4", + "profile": "https://twitter.com/piecu", + "contributions": [ + "bug" + ] + }, + { + "login": "Gudahtt", + "name": "Mark Stacey", + "avatar_url": "https://avatars.githubusercontent.com/u/2459287?v=4", + "profile": "https://github.com/Gudahtt", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "lifeiscontent", + "name": "Aaron Reisman", + "avatar_url": "https://avatars.githubusercontent.com/u/180963?v=4", + "profile": "https://lifeiscontent.net/", + "contributions": [ + "ideas" + ] + }, + { + "login": "markwoon", + "name": "Mark Woon", + "avatar_url": "https://avatars.githubusercontent.com/u/215141?v=4", + "profile": "https://github.com/markwoon", + "contributions": [ + "bug" + ] + }, + { + "login": "joshunger", + "name": "Josh Unger", + "avatar_url": "https://avatars.githubusercontent.com/u/2301847?v=4", + "profile": "https://github.com/joshunger", + "contributions": [ + "bug" + ] + }, + { + "login": "robcaldecott", + "name": "Rob Caldecott", + "avatar_url": "https://avatars.githubusercontent.com/u/796702?v=4", + "profile": "https://github.com/robcaldecott", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "tbertrand7", + "name": "Tom Bertrand", + "avatar_url": "https://avatars.githubusercontent.com/u/14081248?v=4", + "profile": "https://github.com/tbertrand7", + "contributions": [ + "bug" + ] + }, + { + "login": "wKovacs64", + "name": "Justin Hall", + "avatar_url": "https://avatars.githubusercontent.com/u/1288694?v=4", + "profile": "https://justinrhall.dev", + "contributions": [ + "bug" + ] } ], "commitConvention": "none", diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index f0384a94..f7903d87 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,4 +1,4 @@ { "node": "14", - "sandboxes": ["vbcvs"] + "sandboxes": ["djcc7b", "vbcvs"] } diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..ae409af0 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/coverage +/dist +/node_modules diff --git a/.eslintrc.js b/.eslintrc.js index b9e85808..ff712056 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,14 +1,18 @@ module.exports = { extends: './node_modules/kcd-scripts/eslint.js', + parserOptions: { + project: ['./tsconfig.json', './tests/tsconfig.json'], + }, settings: { 'import/resolver': { - node: { - extensions: ['.js', '.ts'], - }, + typescript: {}, }, }, rules: { + 'no-await-in-loop': 0, 'testing-library/no-dom-import': 0, '@typescript-eslint/non-nullable-type-assertion-style': 0, + // ES2022 will be released in June 2022 + 'prefer-object-has-own': 0, }, } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index eaa68de9..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,54 +0,0 @@ - - -- `@testing-library/user-event` version: - - -- Testing Framework and version: - -- DOM Environment: - - - - -Relevant code or config - -```javascript - -``` - -What you did: - -What happened: - - - -Reproduction repository: - - - -Problem description: - -Suggested solution: diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..30e1f510 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,105 @@ +name: πŸ› Bug report +description: Create a report to help us improve +labels: + - needs assessment + - bug +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out this form. ❀ + + Bug reports are an important contribution to the library! + + + A clear and concise description of the bug helps to identify problems in + the implementation and validate proposed solutions. + - type: input + attributes: + label: Reproduction example + description: | + Please set up an online code example at https://codesandbox.io. + + You can use one of the following templates as a starting point: + Native DOM: https://codesandbox.io/s/djcc7b?file=/src/App.js + React: https://codesandbox.io/s/vbcvs?file=/src/App.js + validations: + required: true + - type: textarea + attributes: + label: Prerequisites + description: | + The minimal requirements to reproduce the described behavior. + placeholder: | + 1. Render `` element. + 2. Select the `b` per mouse. + 3. Press `x` on the keyboard. + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: | + A clear and concise description of what you expected to happen. + + Try to include a test asserting this behavior in the codesandbox example above. + + If the expected behavior is described e.g. at MDN, please include a link as reference. + placeholder: | + Input element value changes to `axc` and the cursor is at position 2. + validations: + required: true + - type: textarea + attributes: + label: Actual behavior + description: | + A clear and concise description of what did happen. + placeholder: | + Input element value changes to `Well, that was unexpected.`. + validations: + required: true + - type: input + attributes: + label: User-event version + description: | + Please make sure that you are using at least the latest stable version. + See https://github.com/testing-library/user-event/releases?q=prerelease%3Afalse + placeholder: 14.0.0 + validations: + required: true + - type: textarea + attributes: + label: Environment + description: > + If the bug is not reproducible per Codesandbox, please include at least + the following information about the environment. + + If any other third-party libraries are necessary for reproducing the + bug, please consider filing an issue with that library and/or open a + [Discussion](https://github.com/testing-library/user-event/discussions/new?category=q-a). + value: | + Testing Library framework: + + + JS framework: + + Test environment: + + + DOM implementation: + + - type: textarea + attributes: + label: Additional context + description: | + Any additional information on the topic you'd like to include. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..923f0fe2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: πŸ™‹ Discussions + url: https://github.com/testing-library/user-event/discussions/new?category=q-a + about: Please ask and answer questions in the Discussions. + - name: πŸ’¬ Discord + url: https://discord.gg/testing-library + about: Join our community at Discord. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..66f56993 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,50 @@ +name: πŸ€” Feature request +description: Suggest an idea for this project +labels: + - needs assessment + - enhancement +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out this form. ❀ + + A clear and concise description is a huge part of making a new feature + happen. + - type: textarea + attributes: + label: Problem description + description: | + The problem you are trying to solve. + What is it you are trying to achieve? + + Detail the minimal requirements to reproduce the problem. + + If you want to test e.g. certain features of your software, include a minimal component that implements the feature. + + Please set up an online code example at https://codesandbox.io. + + You can use one of the following templates as a starting point: + Native DOM: https://codesandbox.io/s/djcc7b?file=/src/App.js + React: https://codesandbox.io/s/vbcvs?file=/src/App.js + + If the problem is specific to some environment and/or is not reproducible per Codesandbox, + please provide the information necessary to reproduce it. + validations: + required: true + - type: textarea + attributes: + label: Suggested solution + description: | + Please describe what you want to happen. + + If you happen to have an idea of how this could be implemented, please include this information here. + + If aspects of the missing feature are described e.g. at MDN, please include links as reference. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: | + Any additional information on the topic. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b0c16d04..95f15dd2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,7 +5,6 @@ on: [ '+([0-9])?(.{+([0-9]),x}).x', 'main', - 'master', 'next', 'next-major', 'beta', @@ -33,6 +32,7 @@ jobs: uses: bahmutov/npm-install@v1 with: useLockFile: false + install-command: yarn --silent env: HUSKY_SKIP_INSTALL: true @@ -47,8 +47,8 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository == 'testing-library/user-event' && - contains('refs/heads/main,refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha', - github.ref) && github.event_name == 'push' }} + contains('refs/heads/main,refs/heads/beta,refs/heads/alpha', github.ref) + && github.event_name == 'push' }} steps: - name: ⬇️ Checkout repo uses: actions/checkout@v2 @@ -62,6 +62,7 @@ jobs: uses: bahmutov/npm-install@v1 with: useLockFile: false + install-command: yarn --silent env: HUSKY_SKIP_INSTALL: true @@ -69,19 +70,7 @@ jobs: run: npm run build - name: πŸš€ Release - uses: cycjimmy/semantic-release-action@v2 - with: - semantic_version: 17 - branches: | - [ - '+([0-9])?(.{+([0-9]),x}).x', - 'main', - 'master', - 'next', - 'next-major', - {name: 'beta', prerelease: true}, - {name: 'alpha', prerelease: true} - ] + uses: ph-fritsche/action-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..2411fb2a --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,179 @@ +## Contributors + +Thanks goes to these wonderful people ([emoji key][emojis]): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Giorgio Polvara

πŸ› πŸ’» πŸ“– πŸ€” πŸš‡ πŸ‘€ ⚠️

Weyert de Boer

πŸ’» ⚠️

Tim Whitbeck

πŸ› πŸ’»

MichaΓ«l De Boey

πŸ“– πŸ€”

Michael Lasky

πŸ’» πŸ“– πŸ€”

Ahmad Esmaeilzadeh

πŸ“–

Caleb Eby

πŸ’» πŸ› πŸ‘€

AdriΓ  Fontcuberta

πŸ› ⚠️ πŸ’»

Sky Wickenden

πŸ› πŸ’»

Bodnar Bogdan

πŸ› πŸ’»

Zach Perrault

πŸ“–

Ryan Stelly

πŸ“–

Ben Monro

πŸ’»

Christopher Martin

πŸ’»

Yuancheng Wu

πŸ‘€

MJ

πŸ“–

Jeff McRiffey

πŸ’» ⚠️

Jaga Santagostino

πŸ’» ⚠️

jordyvandomselaar

πŸ’» ⚠️

Ilya Lyamkin

πŸ’» ⚠️

Kenneth LujΓ‘n Rosas

πŸ’» ⚠️

Joe Morgan

πŸ’»

David Hirtle

πŸ’»

whiteUnicorn

πŸ’»

Matej Ε nuderl

πŸ‘€

Rodrigo Pombo

πŸ’»

Jake Verbaten

πŸ’»

Spencer Miskoviak

πŸ“–

Vadim Shvetsov

πŸ€” πŸ’» ⚠️

Greg Shtilman

πŸ’» ⚠️ πŸ›

Ricardo Busquet

πŸ› πŸ’» ⚠️

Doug Bacelar

πŸ’» ⚠️

Kayleigh Ridd

πŸ› πŸ’» ⚠️

Malcolm Kee

πŸ’» πŸ“– ⚠️

kelvinlzhang

πŸ›

Krzysztof

πŸ›

Pontus Lundin

πŸ’» ⚠️

Aleks Hudochenkov

πŸ›

Vijay Kumar Otti

πŸ›

Tom Picton

πŸ› πŸ’» ⚠️

Hung Viet Nguyen

πŸ›

Nick McCurdy

πŸ“† πŸ’¬ πŸ’» ⚠️ πŸ“– πŸš‡ πŸ€”

Tim Deschryver

⚠️ πŸ“–

Ben Dyer

πŸ’» ⚠️

Dan Kirkham

πŸ’»

Johannesklint

πŸ“–

Juan Carlos Medina

πŸ’» ⚠️

Dade Cook

πŸ’» ⚠️

Leandro Lourenci

πŸ’» ⚠️

Marco Moretti

πŸ’» ⚠️

ybentz

πŸ’» ⚠️

Nasdan

πŸ›

Javier MartΓ­nez

πŸ“–

JΓΆrg Bayreuther

πŸ’» ⚠️ πŸ“–

Lucas Bernalte

πŸ“–

Maxwell Newlands

πŸ’» ⚠️

ph-fritsche

πŸ’» ⚠️ πŸ› πŸ€” πŸš‡ 🚧

Rey Wright

πŸ› πŸ’»

Niklas Mischkulnig

πŸ’» ⚠️

Pascal Duez

πŸ’»

Malachi Willey

πŸ’» ⚠️

Clark Winters

πŸ“–

lazytype

πŸ’» ⚠️

LuΓ­s Takahashi

πŸ’» ⚠️

Jesu Castillo

πŸ’» ⚠️

Sarah Dayan

πŸ“–

Mirone

πŸ›

Amanda Pouget

πŸ“–

Sonic12040

πŸ’» ⚠️ πŸ“–

Gonzalo D'Elia

πŸ’» ⚠️ πŸ“–

Vasilii Kovalev

πŸ’» πŸ“–

Dale Seo

πŸ“–

Alex Boyce

πŸ’»

Ben Styles

πŸ’» ⚠️

Laura Beatris

πŸ’» ⚠️

Boris Serdiuk

πŸ›

bozdoz

πŸ“– πŸ› ⚠️

Jan Kattelans

πŸ’»

schoeneu

πŸ›

Martin Kapal

πŸ›

Stavros

πŸ›

geoffroymounier

πŸ›

Fergus McDonald

πŸ’»

Robin Ambachtsheer

πŸ›

Mohit

πŸ› πŸ’» ⚠️

Daniel Contreras

πŸ›

Eugene Ghanizadeh

πŸ’»

Victor Repkow

πŸ’»

Jonathan Felchlin

πŸ’»

sydneyjodon-wk

πŸ› πŸ’»

Charles Magic Woo

πŸ›

mkurcius

πŸ’»

Tim Fischbach

πŸ›

Brian Donovan

πŸ’»

Eric Wang

πŸ’»

Jesper Orb

πŸ’»

Johannes Fischer

πŸ’»

Andrew D.

πŸ’»

Patrick LizoΕ„

πŸ’»

Pramod Mali

πŸ€”

wolfe111

πŸ›

Tyler Grass

πŸ›

Michael Pauly

πŸ›

rbrady-hs

πŸ€”

Dmitriy Кorneev

πŸ›

Kumachan

πŸ›

Matthew Lloyd Williamson

πŸ€”

Bruno Thomas

πŸ›

Anthony Fu

πŸ›

momokolo

πŸ›

Danny

πŸ›

Lucas Levin

πŸ›

Matan Borenkraout

πŸ“–

Kent C. Dodds

πŸ’» πŸš‡ 🚧 πŸ‘€ ⚠️

Dennis273

πŸ› πŸ’» ⚠️

Damian Pieczynski

πŸ›

Mark Stacey

πŸ› πŸ’»

Aaron Reisman

πŸ€”

Mark Woon

πŸ›

Josh Unger

πŸ›

Rob Caldecott

πŸ› πŸ’»

Tom Bertrand

πŸ›

Justin Hall

πŸ›
+ + + + + + +This project follows the [all-contributors][all-contributors] specification. +Contributions of any kind welcome! + +[all-contributors]: https://github.com/all-contributors/all-contributors +[emojis]: https://github.com/all-contributors/all-contributors#emoji-key diff --git a/README.md b/README.md index 35c3e0fd..3dc9bebe 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@
-[**Read The Docs**](https://testing-library.com/docs/ecosystem-user-event) | -[Edit the docs](https://github.com/testing-library/testing-library-docs) +[**Read The Docs**](https://testing-library.com/docs/user-event/intro)
@@ -38,25 +37,6 @@ [![Tweet][twitter-badge]][twitter] -## Table of Contents - - - - -- [The problem](#the-problem) -- [The solution](#the-solution) -- [Installation](#installation) -- [Docs](#docs) -- [Known limitations](#known-limitations) -- [Issues](#issues) - - [πŸ› Bugs](#-bugs) - - [πŸ’‘ Feature Requests](#-feature-requests) - - [❓ Questions](#-questions) -- [Contributors](#contributors) -- [LICENSE](#license) - - - ## The problem From @@ -74,30 +54,6 @@ change the state of the checkbox. > [The more your tests resemble the way your software is used, the more > confidence they can give you.][guiding-principle] -## Installation - -With NPM: - -```sh -npm install --save-dev @testing-library/user-event @testing-library/dom -``` - -With Yarn: - -```sh -yarn add --dev @testing-library/user-event @testing-library/dom -``` - -## Docs - -[**Read The Docs**](https://testing-library.com/docs/ecosystem-user-event) | -[Edit the docs](https://github.com/testing-library/testing-library-docs) - -## Known limitations - -- No `` support. - [#423](https://github.com/testing-library/user-event/issues/423#issuecomment-669368863) - ## Issues Looking to contribute? Look for the [Good First Issue][good-first-issue] label. @@ -125,149 +81,10 @@ instead of filing an issue on GitHub. ## Contributors -Thanks goes to these people ([emoji key][emojis]): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Giorgio Polvara

πŸ› πŸ’» πŸ“– πŸ€” πŸš‡ πŸ‘€ ⚠️

Weyert de Boer

πŸ’» ⚠️

Tim Whitbeck

πŸ› πŸ’»

MichaΓ«l De Boey

πŸ“–

Michael Lasky

πŸ’» πŸ“– πŸ€”

Ahmad Esmaeilzadeh

πŸ“–

Caleb Eby

πŸ’» πŸ› πŸ‘€

AdriΓ  Fontcuberta

πŸ› ⚠️ πŸ’»

Sky Wickenden

πŸ› πŸ’»

Bodnar Bogdan

πŸ› πŸ’»

Zach Perrault

πŸ“–

Ryan Stelly

πŸ“–

Ben Monro

πŸ’»

Christopher Martin

πŸ’»

Yuancheng Wu

πŸ‘€

MJ

πŸ“–

Jeff McRiffey

πŸ’» ⚠️

Jaga Santagostino

πŸ’» ⚠️

jordyvandomselaar

πŸ’» ⚠️

Ilya Lyamkin

πŸ’» ⚠️

Kenneth LujΓ‘n Rosas

πŸ’» ⚠️

Joe Morgan

πŸ’»

David Hirtle

πŸ’»

whiteUnicorn

πŸ’»

Matej Ε nuderl

πŸ‘€

Rodrigo Pombo

πŸ’»

Jake Verbaten

πŸ’»

Spencer Miskoviak

πŸ“–

Vadim Shvetsov

πŸ€” πŸ’» ⚠️

Greg Shtilman

πŸ’» ⚠️ πŸ›

Ricardo Busquet

πŸ› πŸ’» ⚠️

Doug Bacelar

πŸ’» ⚠️

Kayleigh Ridd

πŸ› πŸ’» ⚠️

Malcolm Kee

πŸ’» πŸ“– ⚠️

kelvinlzhang

πŸ›

Krzysztof

πŸ›

Pontus Lundin

πŸ’» ⚠️

Aleks Hudochenkov

πŸ›

Vijay Kumar Otti

πŸ›

Tom Picton

πŸ› πŸ’» ⚠️

Hung Viet Nguyen

πŸ›

Nick McCurdy

πŸ“† πŸ’¬ πŸ’» ⚠️ πŸ“–

Tim Deschryver

⚠️

Ben Dyer

πŸ’» ⚠️

Dan Kirkham

πŸ’»

Johannesklint

πŸ“–

Juan Carlos Medina

πŸ’» ⚠️

Dade Cook

πŸ’» ⚠️

Leandro Lourenci

πŸ’» ⚠️

Marco Moretti

πŸ’» ⚠️

ybentz

πŸ’» ⚠️

Nasdan

πŸ›

Javier MartΓ­nez

πŸ“–

JΓΆrg Bayreuther

πŸ’» ⚠️ πŸ“–

Lucas Bernalte

πŸ“–

Maxwell Newlands

πŸ’» ⚠️

ph-fritsche

πŸ’» ⚠️

Rey Wright

πŸ› πŸ’»

Niklas Mischkulnig

πŸ’» ⚠️

Pascal Duez

πŸ’»

Malachi Willey

πŸ’» ⚠️

Clark Winters

πŸ“–

lazytype

πŸ’» ⚠️

LuΓ­s Takahashi

πŸ’» ⚠️

Jesu Castillo

πŸ’» ⚠️

Sarah Dayan

πŸ“–

Mirone

πŸ›

Amanda Pouget

πŸ“–

Sonic12040

πŸ’» ⚠️ πŸ“–

Gonzalo D'Elia

πŸ’» ⚠️ πŸ“–

Vasilii Kovalev

πŸ’» πŸ“–

Dale Seo

πŸ“–

Alex Boyce

πŸ’»

Ben Styles

πŸ’» ⚠️

Laura Beatris

πŸ’» ⚠️

Boris Serdiuk

πŸ›

bozdoz

πŸ“– πŸ› ⚠️

Jan Kattelans

πŸ’»

schoeneu

πŸ›

Martin Kapal

πŸ›

Stavros

πŸ›

geoffroymounier

πŸ›

Fergus McDonald

πŸ’»

Robin Ambachtsheer

πŸ›

Mohit

πŸ› πŸ’» ⚠️

Daniel Contreras

πŸ›

Eugene Ghanizadeh

πŸ’»

Victor Repkow

πŸ’»

Jonathan Felchlin

πŸ’»

sydneyjodon-wk

πŸ› πŸ’»

Charles Magic Woo

πŸ›

mkurcius

πŸ’»

Tim Fischbach

πŸ›

Brian Donovan

πŸ’»

Eric Wang

πŸ’»

Jesper Orb

πŸ’»

Johannes Fischer

πŸ’»

Andrew D.

πŸ’»
- - - - - - -This project follows the [all-contributors][all-contributors] specification. -Contributions of any kind welcome! +We most sincerely thank [the people who make this project +possible][contributors]. Contributions of any kind are welcome! πŸ’š -## LICENSE +## License [MIT](LICENSE) @@ -287,15 +104,13 @@ Contributions of any kind welcome! [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/user-event/blob/main/other/CODE_OF_CONDUCT.md +[coc]: https://github.com/testing-library/.github/blob/main/CODE_OF_CONDUCT.md [github-watch-badge]: https://img.shields.io/github/watchers/testing-library/user-event.svg?style=social [github-watch]: https://github.com/testing-library/user-event/watchers [github-star-badge]: https://img.shields.io/github/stars/testing-library/user-event.svg?style=social [github-star]: https://github.com/testing-library/user-event/stargazers [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20user-event%20by%20%40@TestingLib%20https%3A%2F%2Fgithub.com%2Ftesting-library%2Fuser-event%20%F0%9F%91%8D [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/testing-library/user-event.svg?style=social -[emojis]: https://github.com/all-contributors/all-contributors#emoji-key -[all-contributors]: https://github.com/all-contributors/all-contributors [all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/user-event?color=orange&style=flat-square [guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 [bugs]: https://github.com/testing-library/user-event/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug @@ -305,4 +120,5 @@ Contributions of any kind welcome! [discord-badge]: https://img.shields.io/discord/723559267868737556.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square [discord]: https://discord.gg/testing-library [stackoverflow]: https://stackoverflow.com/questions/tagged/user-event +[contributors]: https://github.com/testing-library/user-event/blob/main/CONTRIBUTORS.md diff --git a/build.js b/build.js new file mode 100644 index 00000000..426c71cb --- /dev/null +++ b/build.js @@ -0,0 +1,24 @@ +;(async () => { + const child = require('child_process') + const {build} = require('esbuild') + + await build({ + outfile: 'dist/index.mjs', + format: 'esm', + target: 'es6', + bundle: true, + external: ['@testing-library/dom'], + entryPoints: ['src/index.ts'], + }) + + await build({ + outfile: 'dist/index.cjs', + format: 'cjs', + target: 'node12', + bundle: true, + external: ['@testing-library/dom'], + entryPoints: ['src/index.ts'], + }) + + child.execSync('yarn tsc -p tsconfig.build.json') +})() diff --git a/jest.config.js b/jest.config.js index 67ee8aec..44cb3f12 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,27 @@ const config = require('kcd-scripts/jest') -module.exports = { - ...config, - testEnvironment: 'jest-environment-jsdom', +config.roots = [''] - // this repo is testing utils - testPathIgnorePatterns: config.testPathIgnorePatterns.filter(f => f !== '/__tests__/utils/'), +config.moduleNameMapper = { + '^#src$': '/src/index', + '^#src/(.*)$': '/src/$1', + '^#testHelpers$': '/tests/_helpers/index', } + +config.testEnvironment = 'jsdom' + +config.setupFilesAfterEnv = [ + '/tests/_setup-env.js', + '/tests/react/_env/setup-env.js', +] + +config.testMatch.push('/tests/**/*.+(js|jsx|ts|tsx)') + +// Ignore files/dirs starting with an underscore (setup, helper, ...) +// unless the file ends on `.test.{type}` so that we can add tests of our test utilities. +config.testPathIgnorePatterns.push('/_.*(?", "license": "MIT", "engines": { - "node": ">=10", + "node": ">=12", "npm": ">=6" }, "repository": { @@ -26,8 +25,17 @@ "files": [ "dist" ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "types": "./dist/types/index.d.ts", "scripts": { - "build": "kcd-scripts build", + "build": "node build.js", "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", @@ -36,51 +44,27 @@ "validate": "kcd-scripts validate", "typecheck": "kcd-scripts typecheck" }, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, "devDependencies": { - "@testing-library/dom": "^7.28.1", - "@testing-library/jest-dom": "^5.11.6", - "@testing-library/react": "^11.2.5", - "@types/estree": "0.0.45", + "@testing-library/dom": "^8.11.4", + "@testing-library/jest-dom": "^5.16.3", + "@testing-library/react": "^13.0.0", "@types/jest-in-case": "^1.0.3", - "@types/react": "^17.0.3", - "is-ci": "^2.0.0", + "@types/react": "^17.0.42", + "esbuild": "^0.14.27", + "eslint-import-resolver-typescript": "^2.7.0", + "is-ci": "^3.0.1", "jest-in-case": "^1.0.2", "jest-serializer-ansi": "^1.0.3", - "kcd-scripts": "^11.1.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "kcd-scripts": "^12.1.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react17": "npm:react@^17.0.2", + "reactDom17": "npm:react-dom@^17.0.2", + "reactIs17": "npm:react-is@^17.0.2", + "reactTesting17": "npm:@testing-library/react@^12.1.3", "typescript": "^4.1.2" }, "peerDependencies": { "@testing-library/dom": ">=7.21.4" - }, - "eslintConfig": { - "extends": "./node_modules/kcd-scripts/eslint.js", - "rules": { - "jsx-a11y/click-events-have-key-events": "off", - "jsx-a11y/tabindex-no-positive": "off", - "no-func-assign": "off", - "no-return-assign": "off", - "react/prop-types": "off", - "testing-library/no-dom-import": "off" - }, - "overrides": [ - { - "files": [ - "**/__tests__/**" - ], - "rules": { - "no-console": "off" - } - } - ] - }, - "eslintIgnore": [ - "node_modules", - "coverage", - "dist" - ] + } } diff --git a/src/.eslintrc b/src/.eslintrc deleted file mode 100644 index b7d78581..00000000 --- a/src/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rules": { - // everything in this directory is intentionally running in series, not parallel - // because user's cannot fire multiple events at the same time and we need - // all events fired in a predictable order. - "no-await-in-loop": "off" - } -} diff --git a/src/__mocks__/@testing-library/dom.js b/src/__mocks__/@testing-library/dom.js deleted file mode 100644 index 10fc4d71..00000000 --- a/src/__mocks__/@testing-library/dom.js +++ /dev/null @@ -1,50 +0,0 @@ -// this helps us track what the state is before and after an event is fired -// this is needed for determining the snapshot values -const actual = jest.requireActual('@testing-library/dom') - -function getTrackedElementValues(element) { - return { - value: element.value, - checked: element.checked, - selectionStart: element.selectionStart, - selectionEnd: element.selectionEnd, - - // unfortunately, changing a select option doesn't happen within fireEvent - // but rather imperatively via `options.selected = newValue` - // because of this we don't (currently) have a way to track before/after - // in a given fireEvent call. - } -} - -function wrapWithTestData(fn) { - return (element, init) => { - const before = getTrackedElementValues(element) - const testData = {before} - - // put it on the element so the event handler can grab it - element.testData = testData - const result = fn(element, init) - - const after = getTrackedElementValues(element) - Object.assign(testData, {after}) - - // elete the testData for the next event - delete element.testData - return result - } -} - -const mockFireEvent = wrapWithTestData(actual.fireEvent) - -for (const key of Object.keys(actual.fireEvent)) { - if (typeof actual.fireEvent[key] === 'function') { - mockFireEvent[key] = wrapWithTestData(actual.fireEvent[key], key) - } else { - mockFireEvent[key] = actual.fireEvent[key] - } -} - -module.exports = { - ...actual, - fireEvent: mockFireEvent, -} diff --git a/src/__tests__/clear.js b/src/__tests__/clear.js deleted file mode 100644 index 3cdb2c47..00000000 --- a/src/__tests__/clear.js +++ /dev/null @@ -1,94 +0,0 @@ -import userEvent from '../' -import {setup} from './helpers/utils' - -test('clears text', () => { - const {element, getEventSnapshot} = setup('') - userEvent.clear(element) - expect(element).toHaveValue('') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value=""] - - input[value="hello"] - pointerover - input[value="hello"] - pointerenter - input[value="hello"] - mouseover: Left (0) - input[value="hello"] - mouseenter: Left (0) - input[value="hello"] - pointermove - input[value="hello"] - mousemove: Left (0) - input[value="hello"] - pointerdown - input[value="hello"] - mousedown: Left (0) - input[value="hello"] - focus - input[value="hello"] - focusin - input[value="hello"] - pointerup - input[value="hello"] - mouseup: Left (0) - input[value="hello"] - click: Left (0) - input[value="hello"] - select - input[value="hello"] - keydown: Delete (46) - input[value=""] - input - input[value=""] - keyup: Delete (46) - `) -}) - -test('works with textarea', () => { - const {element} = setup('') - userEvent.clear(element) - expect(element).toHaveValue('') -}) - -test('does not clear text on disabled inputs', () => { - const {element, getEventSnapshot} = setup('') - userEvent.clear(element) - expect(element).toHaveValue('hello') - expect(getEventSnapshot()).toMatchInlineSnapshot( - `No events were fired on: input[value="hello"]`, - ) -}) - -test('does not clear text on readonly inputs', () => { - const {element, getEventSnapshot} = setup('') - userEvent.clear(element) - expect(element).toHaveValue('hello') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value="hello"] - - input[value="hello"] - pointerover - input[value="hello"] - pointerenter - input[value="hello"] - mouseover: Left (0) - input[value="hello"] - mouseenter: Left (0) - input[value="hello"] - pointermove - input[value="hello"] - mousemove: Left (0) - input[value="hello"] - pointerdown - input[value="hello"] - mousedown: Left (0) - input[value="hello"] - focus - input[value="hello"] - focusin - input[value="hello"] - pointerup - input[value="hello"] - mouseup: Left (0) - input[value="hello"] - click: Left (0) - input[value="hello"] - select - input[value="hello"] - keydown: Delete (46) - input[value="hello"] - keyup: Delete (46) - `) -}) - -test('clears even on inputs that cannot (programmatically) have a selection', () => { - const {element: email} = setup('') - userEvent.clear(email) - expect(email).toHaveValue('') - - const {element: password} = setup('') - userEvent.clear(password) - expect(password).toHaveValue('') - - const {element: number} = setup('') - userEvent.clear(number) - // jest-dom does funny stuff with toHaveValue on number inputs - // eslint-disable-next-line jest-dom/prefer-to-have-value - expect(number.value).toBe('') -}) - -test('non-inputs/textareas are currently unsupported', () => { - const {element} = setup('
') - - expect(() => userEvent.clear(element)).toThrowErrorMatchingInlineSnapshot( - `clear currently only supports input and textarea elements.`, - ) -}) diff --git a/src/__tests__/click.js b/src/__tests__/click.js deleted file mode 100644 index 21cd2b2d..00000000 --- a/src/__tests__/click.js +++ /dev/null @@ -1,486 +0,0 @@ -import userEvent from '../' -import {setup, addEventListener, addListeners} from './helpers/utils' - -test('click in button', () => { - const {element, getEventSnapshot} = setup('
`) - const input = element.children[0] - const button = element.children[1] - - addEventListener(button, 'click', () => input.focus()) - - expect(input).not.toHaveFocus() - - userEvent.click(button) - expect(input).toHaveFocus() - - userEvent.click(button) - expect(input).toHaveFocus() -}) - -test('gives focus to the form control when clicking the label', () => { - const {element} = setup(` -
- - -
- `) - const label = element.children[0] - const input = element.children[1] - - userEvent.click(label) - expect(input).toHaveFocus() -}) - -test('gives focus to the form control when clicking within a label', () => { - const {element} = setup(` -
- - -
- `) - const label = element.children[0] - const span = label.firstChild - const input = element.children[1] - - userEvent.click(span) - expect(input).toHaveFocus() -}) - -test('fires no events when clicking a label with a nested control that is disabled', () => { - const {element, getEventSnapshot} = setup(``) - userEvent.click(element) - expect(getEventSnapshot()).toMatchInlineSnapshot( - `No events were fired on: label`, - ) -}) - -test('does not crash if the label has no control', () => { - const {element} = setup(``) - userEvent.click(element) -}) - -test('clicking a label checks the checkbox', () => { - const {element} = setup(` -
- - -
- `) - const label = element.children[0] - const input = element.children[1] - - userEvent.click(label) - expect(input).toHaveFocus() - expect(input).toBeChecked() -}) - -test('clicking a label checks the radio', () => { - const {element} = setup(` -
- - -
- `) - const label = element.children[0] - const input = element.children[1] - - userEvent.click(label) - expect(input).toHaveFocus() - expect(input).toBeChecked() -}) - -test('submits a form when clicking on a `) - userEvent.click(element.children[0]) - expect(eventWasFired('submit')).toBe(true) -}) - -test('does not submit a form when clicking on a - - `) - userEvent.click(element.children[0]) - expect(getEventSnapshot()).not.toContain('submit') -}) - -test('does not fire blur on current element if is the same as previous', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(' - - `) - - document.body.focus() - userEvent.click(element.children[1]) - expect(element.children[1]).toHaveFocus() - - document.body.focus() - userEvent.click(element.children[0]) - expect(element).toHaveFocus() -}) - -test('right click fires `contextmenu` instead of `click', () => { - const {element, getEvents, clearEventCalls} = setup(` -
    -
  • 1
  • -
  • 2
  • -
  • 3
  • -
- ` - document.body.append(wrapper) - const listbox = wrapper.querySelector('[role="listbox"]') as HTMLUListElement - const options = Array.from( - wrapper.querySelectorAll('[role="option"]'), - ) - - // the user is responsible for handling aria-selected on listbox options - options.forEach(el => - el.addEventListener('click', e => { - const target = e.currentTarget as HTMLElement - target.setAttribute( - 'aria-selected', - JSON.stringify( - !JSON.parse(String(target.getAttribute('aria-selected'))), - ), - ) - }), - ) - - return { - ...addListeners(listbox), - listbox, - options, - } -} - -const eventLabelGetters = { - KeyboardEvent(event: KeyboardEvent) { - return [ - event.key, - typeof event.keyCode === 'undefined' ? null : `(${event.keyCode})`, - ] - .join(' ') - .trim() - }, - MouseEvent(event: MouseEvent) { - // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button - const mouseButtonMap: Record = { - 0: 'Left', - 1: 'Middle', - 2: 'Right', - 3: 'Browser Back', - 4: 'Browser Forward', - } - return `${mouseButtonMap[event.button]} (${event.button})` - }, -} as const - -let eventListeners: Array<{ - el: EventTarget - type: string - listener: EventListener -}> = [] - -// asside from the hijacked listener stuff here, it's also important to call -// this function rather than simply calling addEventListener yourself -// because it adds your listener to an eventListeners array which is cleaned -// up automatically which will help use avoid memory leaks. -function addEventListener( - el: EventTarget, - type: string, - listener: EventListener, - options?: AddEventListenerOptions, -) { - eventListeners.push({el, type, listener}) - el.addEventListener(type, listener, options) -} - -function getElementValue(element: Element) { - if (isElementType(element, 'select') && element.multiple) { - return JSON.stringify(Array.from(element.selectedOptions).map(o => o.value)) - } else if (element.getAttribute('role') === 'listbox') { - return JSON.stringify( - element.querySelector('[aria-selected="true"]')?.innerHTML, - ) - } else if (element.getAttribute('role') === 'option') { - return JSON.stringify(element.innerHTML) - } else if ( - isElementType(element, 'input', {type: 'checkbox'}) || - isElementType(element, 'input', {type: 'radio'}) || - isElementType(element, 'button') - ) { - // handled separately - return null - } - - return JSON.stringify((element as HTMLInputElement).value) -} - -function hasProperty( - obj: T, - prop: K, -): obj is T & {[k in K]: unknown} { - return prop in obj -} - -function getElementDisplayName(element: Element) { - const value = getElementValue(element) - const hasChecked = - isElementType(element, 'input', {type: 'checkbox'}) || - isElementType(element, 'input', {type: 'radio'}) - return [ - element.tagName.toLowerCase(), - element.id ? `#${element.id}` : null, - hasProperty(element, 'name') && element.name - ? `[name="${element.name}"]` - : null, - hasProperty(element, 'htmlFor') && element.htmlFor - ? `[for="${element.htmlFor}"]` - : null, - value ? `[value=${value}]` : null, - hasChecked ? `[checked=${element.checked}]` : null, - isElementType(element, 'option') ? `[selected=${element.selected}]` : null, - element.getAttribute('role') === 'option' - ? `[aria-selected=${element.getAttribute('aria-selected')}]` - : null, - ] - .filter(Boolean) - .join('') -} - -type CallData = { - event: Event - elementDisplayName: string - testData?: TestData -} - -type TestData = { - handled?: boolean - - // Where is this assigned? - before?: Element - after?: Element -} - -function isElement(target: EventTarget): target is Element { - return 'tagName' in target -} - -function isMouseEvent(event: Event): event is MouseEvent { - return event.constructor.name === 'MouseEvent' -} - -function addListeners( - element: Element & {testData?: TestData}, - { - eventHandlers = {}, - }: { - eventHandlers?: EventHandlers - } = {}, -) { - const eventHandlerCalls: {current: CallData[]} = {current: []} - const generalListener = jest - .fn((event: Event) => { - const target = event.target - const callData: CallData = { - event, - elementDisplayName: - target && isElement(target) ? getElementDisplayName(target) : '', - } - if (element.testData && !element.testData.handled) { - callData.testData = element.testData - // sometimes firing a single event (like click on a checkbox) will - // automatically fire more events (line input and change). - // and we don't want the test data applied to those, so we'll store - // this and not add the testData to our call if that was already handled - element.testData.handled = true - } - eventHandlerCalls.current.push(callData) - }) - .mockName('eventListener') - const listeners = Object.keys(eventMap) - - for (const name of listeners) { - addEventListener(element, name.toLowerCase(), (...args) => { - if (name in eventHandlers) { - generalListener(...args) - return eventHandlers[name](...args) - } - return generalListener(...args) - }) - } - // prevent default of submits in tests - if (isElementType(element, 'form')) { - addEventListener(element, 'submit', e => e.preventDefault()) - } - - function getEventSnapshot() { - const eventCalls = eventHandlerCalls.current - .map(({event, testData, elementDisplayName}) => { - const eventName = event.constructor.name - const eventLabel = - eventName in eventLabelGetters - ? eventLabelGetters[eventName as keyof typeof eventLabelGetters]( - event as KeyboardEvent & MouseEvent, - ) - : '' - const modifiers = ['altKey', 'shiftKey', 'metaKey', 'ctrlKey'] - .filter(key => event[key as keyof Event]) - .map(k => `{${k.replace('Key', '')}}`) - .join('') - - const firstLine = [ - `${elementDisplayName} - ${event.type}`, - [eventLabel, modifiers].filter(Boolean).join(' '), - ] - .filter(Boolean) - .join(': ') - - return [firstLine, testData?.before ? getChanges(testData) : null] - .filter(Boolean) - .join('\n') - }) - .join('\n') - .trim() - if (eventCalls.length) { - return { - snapshot: [ - `Events fired on: ${getElementDisplayName(element)}`, - eventCalls, - ].join('\n\n'), - } - } else { - return { - snapshot: `No events were fired on: ${getElementDisplayName(element)}`, - } - } - } - const clearEventCalls = () => { - generalListener.mockClear() - eventHandlerCalls.current = [] - } - const getEvents = (type?: string) => - generalListener.mock.calls - .map(([e]) => e) - .filter(e => !type || e.type === type) - const eventWasFired = (eventType: string) => getEvents(eventType).length > 0 - - function getClickEventsSnapshot() { - const lines = getEvents().map(e => - isMouseEvent(e) - ? `${e.type} - button=${e.button}; buttons=${e.buttons}; detail=${e.detail}` - : e.type, - ) - return {snapshot: lines.join('\n')} - } - - return { - getEventSnapshot, - getClickEventsSnapshot, - clearEventCalls, - getEvents, - eventWasFired, - } -} - -function getValueWithSelection(element?: Element) { - const {value, selectionStart, selectionEnd} = element as HTMLInputElement - - return [ - value.slice(0, selectionStart ?? undefined), - ...(selectionStart === selectionEnd - ? ['{CURSOR}'] - : [ - '{SELECTION}', - value.slice(selectionStart ?? 0, selectionEnd ?? undefined), - '{/SELECTION}', - ]), - value.slice(selectionEnd ?? undefined), - ].join('') -} - -const changeLabelGetter: Record string> = { - value: ({before, after}) => - [ - JSON.stringify(getValueWithSelection(before)), - JSON.stringify(getValueWithSelection(after)), - ].join(' -> '), - checked: ({before, after}) => - [ - (before as HTMLInputElement).checked ? 'checked' : 'unchecked', - (after as HTMLInputElement).checked ? 'checked' : 'unchecked', - ].join(' -> '), - - // unfortunately, changing a select option doesn't happen within fireEvent - // but rather imperatively via `options.selected = newValue` - // because of this we don't (currently) have a way to track before/after - // in a given fireEvent call. -} -changeLabelGetter.selectionStart = changeLabelGetter.value -changeLabelGetter.selectionEnd = changeLabelGetter.value - -const getDefaultLabel = ({ - key, - before, - after, -}: { - key: keyof T - before: T - after: T -}) => `${key}: ${JSON.stringify(before[key])} -> ${JSON.stringify(after[key])}` - -function getChanges({before, after}: TestData) { - const changes = new Set() - if (before && after) { - for (const key of Object.keys(before) as Array) { - if (after[key] !== before[key]) { - changes.add( - (key in changeLabelGetter ? changeLabelGetter[key] : getDefaultLabel)( - {key, before, after}, - ), - ) - } - } - } - - return Array.from(changes) - .filter(Boolean) - .map(line => ` ${line}`) - .join('\n') -} - -// eslint-disable-next-line jest/prefer-hooks-on-top -afterEach(() => { - for (const {el, type, listener} of eventListeners) { - el.removeEventListener(type, listener) - } - eventListeners = [] - document.body.innerHTML = '' -}) - -export {setup, setupSelect, setupListbox, addEventListener, addListeners} diff --git a/src/__tests__/hover.js b/src/__tests__/hover.js deleted file mode 100644 index 54cf0f12..00000000 --- a/src/__tests__/hover.js +++ /dev/null @@ -1,146 +0,0 @@ -import userEvent from '../' -import {addEventListener, setup} from './helpers/utils' - -test('hover', () => { - const {element, getEventSnapshot} = setup('') - const button = div.firstChild - - const calls = [] - function addListeners(el) { - for (const event of [ - 'mouseenter', - 'mouseover', - 'mouseleave', - 'mouseout', - 'pointerenter', - 'pointerover', - 'pointerleave', - 'pointerout', - ]) { - addEventListener(el, event, () => { - calls.push(`${el.tagName}: ${event}`) - }) - } - } - addListeners(div) - addListeners(button) - - userEvent.hover(button) - - expect(calls.join('\n')).toMatchInlineSnapshot(` -BUTTON: pointerover -DIV: pointerover -DIV: pointerenter -BUTTON: pointerenter -BUTTON: mouseover -DIV: mouseover -DIV: mouseenter -BUTTON: mouseenter -`) -}) - -test('fires non-bubbling events on parents for unhover', () => { - // Doesn't use getEventSnapshot() because: - // 1) We're asserting the events of both elements (not what bubbles to the outer div) - // 2) We're asserting the order of these events in a single list as they're - // interleaved across two elements. - const {element: div} = setup('
') - const button = div.firstChild - - const calls = [] - function addListeners(el) { - for (const event of [ - 'mouseenter', - 'mouseover', - 'mouseleave', - 'mouseout', - 'pointerenter', - 'pointerover', - 'pointerleave', - 'pointerout', - ]) { - addEventListener(el, event, () => { - calls.push(`${el.tagName}: ${event}`) - }) - } - } - addListeners(div) - addListeners(button) - - userEvent.unhover(button) - - expect(calls.join('\n')).toMatchInlineSnapshot(` -BUTTON: pointerout -DIV: pointerout -BUTTON: pointerleave -DIV: pointerleave -BUTTON: mouseout -DIV: mouseout -BUTTON: mouseleave -DIV: mouseleave -`) -}) - -test('throws when hovering element with pointer-events set to none', () => { - const {element} = setup(`
`) - expect(() => userEvent.hover(element)).toThrowError(/unable to hover/i) -}) - -test('does not throws when hover element with pointer-events set to none and skipPointerEventsCheck is set', () => { - const {element, getEventSnapshot} = setup( - `
`, - ) - userEvent.hover(element, undefined, {skipPointerEventsCheck: true}) - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: div - - div - pointerover - div - pointerenter - div - mouseover: Left (0) - div - mouseenter: Left (0) - div - pointermove - div - mousemove: Left (0) - `) -}) diff --git a/src/__tests__/keyboard/getNextKeyDef.ts b/src/__tests__/keyboard/getNextKeyDef.ts deleted file mode 100644 index 679be074..00000000 --- a/src/__tests__/keyboard/getNextKeyDef.ts +++ /dev/null @@ -1,126 +0,0 @@ -import cases from 'jest-in-case' -import {getNextKeyDef} from 'keyboard/getNextKeyDef' -import {defaultKeyMap} from 'keyboard/keyMap' -import {keyboardKey, keyboardOptions} from 'keyboard/types' - -const options: keyboardOptions = { - document, - keyboardMap: defaultKeyMap, - autoModify: false, - delay: 123, -} - -cases( - 'reference key per', - ({text, key, code}) => { - expect(getNextKeyDef(`${text}foo`, options)).toEqual( - expect.objectContaining({ - keyDef: expect.objectContaining({ - key, - code, - }) as keyboardKey, - consumedLength: text.length, - }), - ) - expect(getNextKeyDef(`${text}/foo`, options)).toEqual( - expect.objectContaining({ - keyDef: expect.objectContaining({ - key, - code, - }) as keyboardKey, - consumedLength: text.length, - }), - ) - }, - { - code: {text: '[ControlLeft]', key: 'Control', code: 'ControlLeft'}, - 'unimplemented code': {text: '[Foo]', key: 'Unknown', code: 'Foo'}, - key: {text: '{Control}', key: 'Control', code: 'ControlLeft'}, - 'unimplemented key': {text: '{Foo}', key: 'Foo', code: 'Unknown'}, - 'legacy modifier': {text: '{ctrl}', key: 'Control', code: 'ControlLeft'}, - 'printable character': {text: 'a', key: 'a', code: 'KeyA'}, - 'modifiers as printable characters': {text: '/', key: '/', code: 'Unknown'}, - '{ as printable': {text: '{{', key: '{', code: 'Unknown'}, - '[ as printable': {text: '[[', key: '[', code: 'Unknown'}, - }, -) - -cases( - 'modifiers', - ({text, modifiers}) => { - expect(getNextKeyDef(`${text}foo`, options)).toEqual( - expect.objectContaining(modifiers), - ) - }, - { - 'no releasePrevious': { - text: '{Control}', - modifiers: {releasePrevious: false}, - }, - 'releasePrevious per key': { - text: '{/Control}', - modifiers: {releasePrevious: true}, - }, - 'releasePrevious per code': { - text: '[/ControlLeft]', - modifiers: {releasePrevious: true}, - }, - 'default releaseSelf': { - text: '{Control}', - modifiers: {releaseSelf: true}, - }, - 'keep key pressed per key': { - text: '{Control>}', - modifiers: {releaseSelf: false}, - }, - 'keep key pressed per code': { - text: '[Control>]', - modifiers: {releaseSelf: false}, - }, - 'keep key pressed with repeatModifier': { - text: '{Control>2}', - modifiers: {releaseSelf: false}, - }, - 'release after repeatModifier': { - text: '{Control>2/}', - modifiers: {releaseSelf: true}, - }, - 'no releaseSelf on legacy modifier': { - text: '{ctrl}', - modifiers: {releaseSelf: false}, - }, - 'release legacy modifier': { - text: '{ctrl/}', - modifiers: {releaseSelf: true}, - }, - }, -) - -cases( - 'errors', - ({text, expectedError}) => { - expect(() => getNextKeyDef(`${text}`, options)).toThrow(expectedError) - }, - { - 'invalid descriptor': { - text: '{!}', - expectedError: 'but found "!" in "{!}"', - }, - 'missing descriptor': { - text: '', - expectedError: 'but found "" in ""', - }, - 'missing closing bracket': { - text: '{a)', - expectedError: 'but found ")" in "{a)"', - }, - 'invalid repeat modifier': { - text: '{a>e}', - expectedError: 'but found "e" in "{a>e}"', - }, - 'missing bracket after repeat modifier': { - text: '{a>3)', - expectedError: 'but found ")" in "{a>3)"', - }, - }, -) diff --git a/src/__tests__/keyboard/index.ts b/src/__tests__/keyboard/index.ts deleted file mode 100644 index 3eacd272..00000000 --- a/src/__tests__/keyboard/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -import userEvent from '../../index' -import {addListeners, setup} from '../helpers/utils' - -it('type without focus', () => { - const {element} = setup('') - const {getEventSnapshot} = addListeners(document.body) - - userEvent.keyboard('foo') - - expect(element).toHaveValue('') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: body - - body - keydown: f (102) - body - keypress: f (102) - body - keyup: f (102) - body - keydown: o (111) - body - keypress: o (111) - body - keyup: o (111) - body - keydown: o (111) - body - keypress: o (111) - body - keyup: o (111) - `) -}) - -it('type with focus', () => { - const {element} = setup('') - const {getEventSnapshot} = addListeners(document.body) - element.focus() - - userEvent.keyboard('foo') - - expect(element).toHaveValue('foo') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: body - - input[value=""] - focusin - input[value=""] - keydown: f (102) - input[value=""] - keypress: f (102) - input[value="f"] - input - input[value="f"] - keyup: f (102) - input[value="f"] - keydown: o (111) - input[value="f"] - keypress: o (111) - input[value="fo"] - input - input[value="fo"] - keyup: o (111) - input[value="fo"] - keydown: o (111) - input[value="fo"] - keypress: o (111) - input[value="foo"] - input - input[value="foo"] - keyup: o (111) - `) -}) - -it('type asynchronous', async () => { - const {element} = setup('') - const {getEventSnapshot} = addListeners(document.body) - element.focus() - - // eslint-disable-next-line testing-library/no-await-sync-events - await userEvent.keyboard('foo', {delay: 1}) - - expect(element).toHaveValue('foo') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: body - - input[value=""] - focusin - input[value=""] - keydown: f (102) - input[value=""] - keypress: f (102) - input[value="f"] - input - input[value="f"] - keyup: f (102) - input[value="f"] - keydown: o (111) - input[value="f"] - keypress: o (111) - input[value="fo"] - input - input[value="fo"] - keyup: o (111) - input[value="fo"] - keydown: o (111) - input[value="fo"] - keypress: o (111) - input[value="foo"] - input - input[value="foo"] - keyup: o (111) - `) -}) - -describe('error', () => { - afterEach(() => { - ;(console.error as jest.MockedFunction).mockClear() - }) - - it('error in sync', async () => { - const err = jest.spyOn(console, 'error') - err.mockImplementation(() => {}) - - userEvent.keyboard('{!') - - // the catch will be asynchronous - await Promise.resolve() - - expect(err).toHaveBeenCalledWith(expect.any(Error) as unknown) - expect(err.mock.calls[0][0]).toHaveProperty( - 'message', - expect.stringContaining('Expected key descriptor but found "!" in "{!"'), - ) - }) - - it('error in async', async () => { - const promise = userEvent.keyboard('{!', {delay: 1}) - - return expect(promise).rejects.toThrowError( - 'Expected key descriptor but found "!" in "{!"', - ) - }) -}) - -it('continue typing with state', () => { - const {element, getEventSnapshot, clearEventCalls} = setup('') - element.focus() - clearEventCalls() - - const state = userEvent.keyboard('[ShiftRight>]') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value=""] - - input[value=""] - keydown: Shift (16) {shift} - `) - clearEventCalls() - - userEvent.keyboard('F[/ShiftRight]', {keyboardState: state}) - - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value="F"] - - input[value=""] - keydown: F (70) {shift} - input[value=""] - keypress: F (70) {shift} - input[value="F"] - input - input[value="F"] - keyup: F (70) {shift} - input[value="F"] - keyup: Shift (16) - `) -}) diff --git a/src/__tests__/keyboard/keyboardImplementation.ts b/src/__tests__/keyboard/keyboardImplementation.ts deleted file mode 100644 index 1fb9fa59..00000000 --- a/src/__tests__/keyboard/keyboardImplementation.ts +++ /dev/null @@ -1,136 +0,0 @@ -import userEvent from '../../index' -import {setup} from '../helpers/utils' - -test('no character input if `altKey` or `ctrlKey` is pressed', () => { - const {element, eventWasFired} = setup(``) - element.focus() - - userEvent.keyboard('[ControlLeft>]g') - - expect(eventWasFired('keypress')).toBe(false) - expect(eventWasFired('input')).toBe(false) - - userEvent.keyboard('[AltLeft>]g') - - expect(eventWasFired('keypress')).toBe(false) - expect(eventWasFired('input')).toBe(false) -}) - -test('do not leak repeatKey in state', () => { - const {element} = setup(``) - ;(element as HTMLInputElement).focus() - - const keyboardState = userEvent.keyboard('{a>2}') - expect(keyboardState).toHaveProperty('repeatKey', undefined) -}) - -describe('pressing and releasing keys', () => { - it('fires event with releasing key twice', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(``) - - ;(element as HTMLInputElement).focus() - clearEventCalls() - - userEvent.keyboard('{ArrowLeft>}{ArrowLeft}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` -Events fired on: input[value=""] - -input[value=""] - keydown: ArrowLeft (37) -input[value=""] - select -input[value=""] - keyup: ArrowLeft (37) -input[value=""] - keydown: ArrowLeft (37) -input[value=""] - select -input[value=""] - keyup: ArrowLeft (37) -`) - }) - - it('fires event without releasing key', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(``) - - ;(element as HTMLInputElement).focus() - clearEventCalls() - - userEvent.keyboard('{a>}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` -Events fired on: input[value="a"] - -input[value=""] - keydown: a (97) -input[value=""] - keypress: a (97) -input[value="a"] - input -`) - }) - - it('fires event multiple times without releasing key', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(``) - ;(element as HTMLInputElement).focus() - clearEventCalls() - - userEvent.keyboard('{a>2}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` -Events fired on: input[value="aa"] - -input[value=""] - keydown: a (97) -input[value=""] - keypress: a (97) -input[value="a"] - input -input[value="a"] - keydown: a (97) -input[value="a"] - keypress: a (97) -input[value="aa"] - input -`) - }) - - it('fires event multiple times and releases key', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(``) - ;(element as HTMLInputElement).focus() - clearEventCalls() - - userEvent.keyboard('{a>2/}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` -Events fired on: input[value="aa"] - -input[value=""] - keydown: a (97) -input[value=""] - keypress: a (97) -input[value="a"] - input -input[value="a"] - keydown: a (97) -input[value="a"] - keypress: a (97) -input[value="aa"] - input -input[value="aa"] - keyup: a (97) -`) - }) - - it('fires event multiple times for multiple keys', () => { - const {element, getEventSnapshot, clearEventCalls} = setup(``) - ;(element as HTMLInputElement).focus() - clearEventCalls() - - userEvent.keyboard('{a>2}{b>2/}{c>2}{/a}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` -Events fired on: input[value="aabbcc"] - -input[value=""] - keydown: a (97) -input[value=""] - keypress: a (97) -input[value="a"] - input -input[value="a"] - keydown: a (97) -input[value="a"] - keypress: a (97) -input[value="aa"] - input -input[value="aa"] - keydown: b (98) -input[value="aa"] - keypress: b (98) -input[value="aab"] - input -input[value="aab"] - keydown: b (98) -input[value="aab"] - keypress: b (98) -input[value="aabb"] - input -input[value="aabb"] - keyup: b (98) -input[value="aabb"] - keydown: c (99) -input[value="aabb"] - keypress: c (99) -input[value="aabbc"] - input -input[value="aabbc"] - keydown: c (99) -input[value="aabbc"] - keypress: c (99) -input[value="aabbcc"] - input -input[value="aabbcc"] - keyup: a (97) -`) - }) -}) diff --git a/src/__tests__/keyboard/plugin/arrow.ts b/src/__tests__/keyboard/plugin/arrow.ts deleted file mode 100644 index 009744ab..00000000 --- a/src/__tests__/keyboard/plugin/arrow.ts +++ /dev/null @@ -1,45 +0,0 @@ -import userEvent from 'index' -import {setup} from '__tests__/helpers/utils' - -const setupInput = () => - setup(``).element - -test('collapse selection to the left', () => { - const el = setupInput() - el.setSelectionRange(2, 4) - - userEvent.type(el, '[ArrowLeft]') - - expect(el.selectionStart).toBe(2) - expect(el.selectionEnd).toBe(2) -}) - -test('collapse selection to the right', () => { - const el = setupInput() - el.setSelectionRange(2, 4) - - userEvent.type(el, '[ArrowRight]') - - expect(el.selectionStart).toBe(4) - expect(el.selectionEnd).toBe(4) -}) - -test('move cursor left', () => { - const el = setupInput() - el.setSelectionRange(2, 2) - - userEvent.type(el, '[ArrowLeft]') - - expect(el.selectionStart).toBe(1) - expect(el.selectionEnd).toBe(1) -}) - -test('move cursor right', () => { - const el = setupInput() - el.setSelectionRange(2, 2) - - userEvent.type(el, '[ArrowRight]') - - expect(el.selectionStart).toBe(3) - expect(el.selectionEnd).toBe(3) -}) diff --git a/src/__tests__/keyboard/plugin/character.ts b/src/__tests__/keyboard/plugin/character.ts deleted file mode 100644 index c8b6b1f5..00000000 --- a/src/__tests__/keyboard/plugin/character.ts +++ /dev/null @@ -1,44 +0,0 @@ -import userEvent from 'index' -import {setup} from '__tests__/helpers/utils' - -test('type [Enter] in textarea', () => { - const {element, getEvents} = setup(``) - - userEvent.type(element, 'oo[Enter]bar[ShiftLeft>][Enter]baz') - - expect(element).toHaveValue('foo\nbar\nbaz') - expect(getEvents('input')[2]).toHaveProperty('inputType', 'insertLineBreak') - expect(getEvents('input')[6]).toHaveProperty('inputType', 'insertLineBreak') -}) - -test('type [Enter] in contenteditable', () => { - const {element, getEvents} = setup(`
f
`) - element.focus() - - userEvent.keyboard('oo[Enter]bar[ShiftLeft>][Enter]baz') - - expect(element).toHaveTextContent('foo bar baz') - expect(element.firstChild).toHaveProperty('nodeValue', 'foo\nbar\nbaz') - expect(getEvents('input')[2]).toHaveProperty('inputType', 'insertParagraph') - expect(getEvents('input')[6]).toHaveProperty('inputType', 'insertLineBreak') -}) - -test.each([ - ['1e--5', 1e-5, undefined, 4], - ['1--e--5', null, '1--e5', 5], - ['.-1.-e--5', null, '.-1-e5', 6], - ['1.5e--5', 1.5e-5, undefined, 6], - ['1e5-', 1e5, undefined, 3], -])( - 'type invalid values into ', - (text, expectedValue, expectedCarryValue, expectedInputEvents) => { - const {element, getEvents} = setup(``) - element.focus() - - const state = userEvent.keyboard(text) - - expect(element).toHaveValue(expectedValue) - expect(state).toHaveProperty('carryValue', expectedCarryValue) - expect(getEvents('input')).toHaveLength(expectedInputEvents) - }, -) diff --git a/src/__tests__/keyboard/plugin/control.ts b/src/__tests__/keyboard/plugin/control.ts deleted file mode 100644 index 99699fa4..00000000 --- a/src/__tests__/keyboard/plugin/control.ts +++ /dev/null @@ -1,59 +0,0 @@ -import userEvent from 'index' -import {setup} from '__tests__/helpers/utils' - -test('press [Home] in textarea', () => { - const {element} = setup( - ``, - ) - element.setSelectionRange(2, 4) - - userEvent.type(element, '[Home]') - - expect(element).toHaveProperty('selectionStart', 0) - expect(element).toHaveProperty('selectionEnd', 0) -}) - -test('press [Home] in contenteditable', () => { - const {element} = setup(`
foo\nbar\baz
`) - document.getSelection()?.setPosition(element.firstChild, 2) - - userEvent.type(element, '[Home]') - - const selection = document.getSelection() - expect(selection).toHaveProperty('focusNode', element.firstChild) - expect(selection).toHaveProperty('focusOffset', 0) -}) - -test('press [End] in textarea', () => { - const {element} = setup( - ``, - ) - element.setSelectionRange(2, 4) - - userEvent.type(element, '[End]') - - expect(element).toHaveProperty('selectionStart', 10) - expect(element).toHaveProperty('selectionEnd', 10) -}) - -test('press [End] in contenteditable', () => { - const {element} = setup(`
foo\nbar\baz
`) - document.getSelection()?.setPosition(element.firstChild, 2) - - userEvent.type(element, '[End]') - - const selection = document.getSelection() - expect(selection).toHaveProperty('focusNode', element.firstChild) - expect(selection).toHaveProperty('focusOffset', 10) -}) - -test('use [Delete] on number input', () => { - const {element} = setup(``) - - userEvent.type( - element, - '1e-5[ArrowLeft][Delete]6[ArrowLeft][ArrowLeft][ArrowLeft][Delete][Delete]', - ) - - expect(element).toHaveValue(16) -}) diff --git a/src/__tests__/keyboard/plugin/functional.ts b/src/__tests__/keyboard/plugin/functional.ts deleted file mode 100644 index 68650e0b..00000000 --- a/src/__tests__/keyboard/plugin/functional.ts +++ /dev/null @@ -1,212 +0,0 @@ -import userEvent from 'index' -import {setup} from '__tests__/helpers/utils' - -test('produce extra events for the Control key when AltGraph is pressed', () => { - const {element, getEventSnapshot} = setup(``) - - userEvent.type(element as Element, '{AltGraph}') - - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value=""] - - input[value=""] - pointerover - input[value=""] - pointerenter - input[value=""] - mouseover: Left (0) - input[value=""] - mouseenter: Left (0) - input[value=""] - pointermove - input[value=""] - mousemove: Left (0) - input[value=""] - pointerdown - input[value=""] - mousedown: Left (0) - input[value=""] - focus - input[value=""] - focusin - input[value=""] - pointerup - input[value=""] - mouseup: Left (0) - input[value=""] - click: Left (0) - input[value=""] - keydown: Control (17) - input[value=""] - keydown: AltGraph (0) - input[value=""] - keyup: AltGraph (0) - input[value=""] - keyup: Control (17) - `) -}) - -test('backspace to valid value', () => { - const {element, getEventSnapshot} = setup(``) - - userEvent.type(element, '5e-[Backspace][Backspace]') - - expect(element).toHaveValue(5) - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value="5"] - - input[value=""] - pointerover - input[value=""] - pointerenter - input[value=""] - mouseover: Left (0) - input[value=""] - mouseenter: Left (0) - input[value=""] - pointermove - input[value=""] - mousemove: Left (0) - input[value=""] - pointerdown - input[value=""] - mousedown: Left (0) - input[value=""] - focus - input[value=""] - focusin - input[value=""] - pointerup - input[value=""] - mouseup: Left (0) - input[value=""] - click: Left (0) - input[value=""] - keydown: 5 (53) - input[value=""] - keypress: 5 (53) - input[value="5"] - input - input[value="5"] - keyup: 5 (53) - input[value="5"] - keydown: e (101) - input[value="5"] - keypress: e (101) - input[value=""] - input - input[value=""] - keyup: e (101) - input[value=""] - keydown: - (45) - input[value=""] - keypress: - (45) - input[value=""] - input - input[value=""] - keyup: - (45) - input[value=""] - keydown: Backspace (8) - input[value=""] - input - input[value=""] - keyup: Backspace (8) - input[value=""] - keydown: Backspace (8) - input[value="5"] - input - input[value="5"] - keyup: Backspace (8) - `) -}) - -test('trigger click event on [Enter] keydown on HTMLAnchorElement', () => { - const {element, getEventSnapshot, getEvents} = setup( - ``, - ) - element.focus() - - userEvent.keyboard('[Enter]') - - expect(getEvents('click')).toHaveLength(1) - expect(getEvents('click')[0]).toHaveProperty('detail', 0) - - // this snapshot should probably not contain a keypress event - // see https://github.com/testing-library/user-event/issues/589 - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: a - - a - focus - a - focusin - a - keydown: Enter (13) - a - keypress: Enter (13) - a - click: Left (0) - a - keyup: Enter (13) - `) -}) - -test('trigger click event on [Enter] keypress on HTMLButtonElement', () => { - const {element, getEventSnapshot, getEvents} = setup(`'} - ${submit === 'input' && ''} - `, - ) - - element.querySelector('input')?.focus() - - userEvent.keyboard('[Enter]') - - expect(getEvents('click')).toHaveLength(0) - expect(getEvents('submit')).toHaveLength(hasForm ? 1 : 0) - }, -) - -test('trigger click event on [Space] keyup on HTMLButtonElement', () => { - const {element, getEventSnapshot, getEvents} = setup(``, + ) + + await user.pointer({keys: '[MouseLeft]', target: element.children[0]}) + + expect(eventWasFired('submit')).toBe(true) + }) + + test('does not submit a form when clicking on a `, + ) + + await user.pointer({keys: '[MouseLeft]', target: element.children[0]}) + + expect(eventWasFired('submit')).toBe(false) + }) +}) + +test('secondary mouse button fires `contextmenu` instead of `click`', async () => { + const {element, getEvents, clearEventCalls, user} = setup(` + + `) + + await user.pointer({keys: '[MouseLeft>]', target: element.children[1]}) + expect(element.children[1]).toHaveFocus() + + await user.pointer({keys: '[TouchA]', target: element.children[0]}) + expect(element).toHaveFocus() +}) + +test('blur when outside of focusable context', async () => { + const { + elements: [focusable, notFocusable], + user, + } = setup(` +
+
+ `) + + expect(focusable).toHaveFocus() + await user.pointer({keys: '[MouseLeft>]', target: notFocusable}) + expect(document.body).toHaveFocus() +}) + +test('mousedown handlers can prevent moving focus', async () => { + const {element, user} = setup(``, {focus: false}) + element.addEventListener('mousedown', e => e.preventDefault()) + + await user.pointer({keys: '[MouseLeft>]', target: element}) + await user.pointer({keys: '[TouchA]', target: element}) + + expect(element).not.toHaveFocus() + expect(element).toHaveProperty('selectionStart', 0) +}) + +test('single mousedown moves cursor to the last text', async () => { + const {element, user} = setup( + `
foo bar baz
`, + ) + + await user.pointer({keys: '[MouseLeft>]', target: element}) + + expect(element).toHaveFocus() + expect(document.getSelection()).toHaveProperty( + 'focusNode', + element.firstChild, + ) + expect(document.getSelection()).toHaveProperty('focusOffset', 11) +}) + +test('double mousedown selects a word or a sequence of whitespace', async () => { + const {element, user} = setup( + ``, + ) + + await user.pointer({keys: '[MouseLeft][MouseLeft>]', target: element}) + + expect(element).toHaveProperty('selectionStart', 8) + expect(element).toHaveProperty('selectionEnd', 11) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft>]', + target: element, + offset: 0, + }) + + expect(element).toHaveProperty('selectionStart', 0) + expect(element).toHaveProperty('selectionEnd', 3) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft]', + target: element, + offset: 11, + }) + + expect(element).toHaveProperty('selectionStart', 8) + expect(element).toHaveProperty('selectionEnd', 11) + + element.value = 'foo bar ' + + await user.pointer({keys: '[MouseLeft][MouseLeft>]', target: element}) + + expect(element).toHaveProperty('selectionStart', 7) + expect(element).toHaveProperty('selectionEnd', 9) +}) + +test('triple mousedown selects whole line', async () => { + const {element, user} = setup( + ``, + ) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft][MouseLeft>]', + target: element, + }) + + expect(element).toHaveProperty('selectionStart', 0) + expect(element).toHaveProperty('selectionEnd', 11) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft][MouseLeft>]', + target: element, + offset: 0, + }) + + expect(element).toHaveProperty('selectionStart', 0) + expect(element).toHaveProperty('selectionEnd', 11) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft][MouseLeft>]', + target: element, + offset: 11, + }) + + expect(element).toHaveProperty('selectionStart', 0) + expect(element).toHaveProperty('selectionEnd', 11) +}) + +test('mousemove with pressed button extends selection', async () => { + const {element, user} = setup( + ``, + ) + + await user.pointer({ + keys: '[MouseLeft][MouseLeft>]', + target: element, + offset: 6, + }) + + expect(element).toHaveProperty('selectionStart', 4) + expect(element).toHaveProperty('selectionEnd', 7) + + await user.pointer({offset: 2}) + + expect(element).toHaveProperty('selectionStart', 2) + expect(element).toHaveProperty('selectionEnd', 7) + + await user.pointer({offset: 10}) + + expect(element).toHaveProperty('selectionStart', 4) + expect(element).toHaveProperty('selectionEnd', 10) + + await user.pointer({}) + + expect(element).toHaveProperty('selectionStart', 4) + expect(element).toHaveProperty('selectionEnd', 11) + + await user.pointer({offset: 5}) + + expect(element).toHaveProperty('selectionStart', 4) + expect(element).toHaveProperty('selectionEnd', 7) +}) + +test('selection is moved on non-input elements', async () => { + const {element, user} = setup( + `
foo bar baz
`, + ) + const span = element.querySelectorAll('span') + + await user.pointer({ + keys: '[MouseLeft][MouseLeft>]', + target: element, + offset: 6, + }) + + expect(document.getSelection()?.toString()).toBe('bar') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[1].previousSibling, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 1, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[1].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 3) + + await user.pointer({offset: 2}) + + expect(document.getSelection()?.toString()).toBe('o bar') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[0].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 2, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[1].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 3) + + await user.pointer({offset: 10}) + + expect(document.getSelection()?.toString()).toBe('bar ba') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[1].previousSibling, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 1, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[2].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 2) + + await user.pointer({}) + + expect(document.getSelection()?.toString()).toBe('bar baz') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[1].previousSibling, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 1, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[2].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 3) +}) + +test('`node` overrides the text offset approximation', async () => { + const {element, user} = setup( + `
foo bar
baz
`, + ) + const div = element.firstChild as HTMLDivElement + const span = element.querySelectorAll('span') + + await user.pointer({ + keys: '[MouseLeft>]', + target: element, + node: span[0].firstChild as Node, + offset: 1, + }) + await user.pointer({node: div, offset: 3}) + + expect(document.getSelection()?.toString()).toBe('oo bar') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[0].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 1, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + div, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 3) + + await user.pointer({ + keys: '[MouseLeft]', + target: element, + node: span[0].firstChild as Node, + }) + expect(document.getSelection()?.toString()).toBe('') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[0].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 3, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[0].firstChild, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 3) + + await user.pointer({ + keys: '[MouseLeft]', + target: element, + node: span[0] as Node, + }) + expect(document.getSelection()?.toString()).toBe('') + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startContainer', + span[0], + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'startOffset', + 1, + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty( + 'endContainer', + span[0], + ) + expect(document.getSelection()?.getRangeAt(0)).toHaveProperty('endOffset', 1) +}) + +describe('focus control when clicking label', () => { + test('click event on label moves focus to control', async () => { + const { + elements: [input, label], + user, + } = setup(`