diff --git a/README.md b/README.md
index 9f70c191..e82446b3 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ change the state of the checkbox.
+
- [Installation](#installation)
- [API](#api)
- [`click(element)`](#clickelement)
@@ -158,8 +159,8 @@ import userEvent from '@testing-library/user-event'
test('type', async () => {
render()
- await userEvent.type(screen.getByRole('textbox'), 'Hello, World!')
- expect(screen.getByRole('textbox')).toHaveAttribute('value', 'Hello, World!')
+ await userEvent.type(screen.getByRole('textbox'), 'Hello,{enter}World!')
+ expect(screen.getByRole('textbox')).toHaveValue('Hello,\nWorld!')
})
```
@@ -170,6 +171,32 @@ one character at the time. `false` is the default value.
are typed. By default it's 0. You can use this option if your component has a
different behavior for fast or slow users.
+#### Special characters
+
+The following special character strings are supported:
+
+| Text string | Key | Modifier | Notes |
+| ------------- | --------- | ---------- | ---------------------------------------------------------------------------------- |
+| `{enter}` | Enter | N/A | Will insert a newline character (`` only). |
+| `{esc}` | Escape | N/A | |
+| `{backspace}` | Backspace | N/A | Will delete the previous character (or the characters within the `selectedRange`). |
+| `{shift}` | Shift | `shiftKey` | Does **not** capitalize following characters. |
+| `{ctrl}` | Control | `ctrlKey` | |
+| `{alt}` | Alt | `altKey` | |
+| `{meta}` | OS | `metaKey` | |
+
+> **A note about modifiers:** Modifier keys (`{shift}`, `{ctrl}`, `{alt}`,
+> `{meta}`) will activate their corresponding event modifiers for the duration
+> of type command or until they are closed (via `{/shift}`, `{/ctrl}`, etc.).
+
+
+
+> We take the same
+> [stance as Cypress](https://docs.cypress.io/api/commands/type.html#Modifiers)
+> in that we do not simulate the behavior that happens with modifier key
+> combinations as different operating systems function differently in this
+> regard.
+
### `upload(element, file, [{ clickInit, changeInit }])`
Uploads file to an ``. For uploading multiple files use `` with
@@ -411,6 +438,7 @@ Thanks goes to these people ([emoji key][emojis]):
+
This project follows the [all-contributors][all-contributors] specification.
diff --git a/package-lock.json b/package-lock.json
index 778c91b1..78fe09d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1957,15 +1957,6 @@
"picomatch": "^2.2.2"
}
},
- "@samverschueren/stream-to-observable": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
- "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
- "dev": true,
- "requires": {
- "any-observable": "^0.3.0"
- }
- },
"@sinonjs/commons": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz",
@@ -2014,14 +2005,13 @@
}
},
"@testing-library/react": {
- "version": "10.0.5",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.0.5.tgz",
- "integrity": "sha512-ExpCW9rJcs4Fd7oxWBpGim2H8aLa1u+wh3mS0vOR8iQFObiIC2wlK6x8Ty8F3relX6WfDAeONEDCt7i0nLBdEw==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.2.0.tgz",
+ "integrity": "sha512-TYQZ4vz0lGCGRgFqQivrtUGQhAlRSxHlYB0sDFJ6h2BZ0IrgRMF3EDQixn5UJk8oMsZJuE1HNnOA0yP4Ci2kyA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.10.2",
- "@testing-library/dom": "^7.8.0",
- "@types/testing-library__react": "^10.0.1"
+ "@testing-library/dom": "^7.9.0"
}
},
"@textlint/ast-node-types": {
@@ -2046,9 +2036,9 @@
}
},
"@types/babel__core": {
- "version": "7.1.7",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz",
- "integrity": "sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==",
+ "version": "7.1.8",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.8.tgz",
+ "integrity": "sha512-KXBiQG2OXvaPWFPDS1rD8yV9vO0OuWIqAEqLsbfX0oU2REN5KuoMnZ1gClWcBhO5I3n6oTVAmrMufOvRqdmFTQ==",
"dev": true,
"requires": {
"@babel/parser": "^7.1.0",
@@ -2078,9 +2068,9 @@
}
},
"@types/babel__traverse": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.11.tgz",
- "integrity": "sha512-ddHK5icION5U6q11+tV2f9Mo6CZVuT8GJKld2q9LqHSZbvLbH34Kcu2yFGckZut453+eQU6btIA3RihmnRgI+Q==",
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz",
+ "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==",
"dev": true,
"requires": {
"@babel/types": "^7.3.0"
@@ -2149,9 +2139,9 @@
"dev": true
},
"@types/node": {
- "version": "14.0.8",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.8.tgz",
- "integrity": "sha512-GogwPm4hw2XCLlej7jn2wF+O3G6HflG6bUtSX/xHmSmlDZkw9M2t5IihljSP8TARpHGdd/ugZOsX9LkDi/K6OQ==",
+ "version": "14.0.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.10.tgz",
+ "integrity": "sha512-Bz23oN/5bi0rniKT24ExLf4cK0JdvN3dH/3k0whYkdN4eI4vS2ZW/2ENNn2uxHCzWcbdHIa/GRuWQytfzCjRYw==",
"dev": true
},
"@types/normalize-package-data": {
@@ -2172,31 +2162,6 @@
"integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==",
"dev": true
},
- "@types/prop-types": {
- "version": "15.7.3",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
- "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
- "dev": true
- },
- "@types/react": {
- "version": "16.9.35",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.35.tgz",
- "integrity": "sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==",
- "dev": true,
- "requires": {
- "@types/prop-types": "*",
- "csstype": "^2.2.0"
- }
- },
- "@types/react-dom": {
- "version": "16.9.8",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz",
- "integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==",
- "dev": true,
- "requires": {
- "@types/react": "*"
- }
- },
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@@ -2212,15 +2177,6 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
"dev": true
},
- "@types/testing-library__dom": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-7.0.2.tgz",
- "integrity": "sha512-8yu1gSwUEAwzg2OlPNbGq+ixhmSviGurBu1+ivxRKq1eRcwdjkmlwtPvr9VhuxTq2fNHBWN2po6Iem3Xt5A6rg==",
- "dev": true,
- "requires": {
- "pretty-format": "^25.1.0"
- }
- },
"@types/testing-library__jest-dom": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.1.tgz",
@@ -2230,17 +2186,6 @@
"@types/jest": "*"
}
},
- "@types/testing-library__react": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-10.0.1.tgz",
- "integrity": "sha512-RbDwmActAckbujLZeVO/daSfdL1pnjVqas25UueOkAY5r7vriavWf0Zqg7ghXMHa8ycD/kLkv8QOj31LmSYwww==",
- "dev": true,
- "requires": {
- "@types/react-dom": "*",
- "@types/testing-library__dom": "*",
- "pretty-format": "^25.1.0"
- }
- },
"@types/yargs": {
"version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
@@ -2639,12 +2584,6 @@
"color-convert": "^2.0.1"
}
},
- "any-observable": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
- "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
- "dev": true
- },
"anymatch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
@@ -3566,9 +3505,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001066",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz",
- "integrity": "sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw==",
+ "version": "1.0.30001077",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001077.tgz",
+ "integrity": "sha512-AEzsGvjBJL0lby/87W96PyEvwN0GsYvk5LHsglLg9tW37K4BqvAvoSCdWIE13OZQ8afupqZ73+oL/1LkedN8hA==",
"dev": true
},
"capture-exit": {
@@ -3795,9 +3734,9 @@
}
},
"clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
+ "version": "0.1.19",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz",
+ "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=",
"dev": true
},
"co": {
@@ -4232,12 +4171,6 @@
}
}
},
- "csstype": {
- "version": "2.6.10",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz",
- "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==",
- "dev": true
- },
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@@ -4327,15 +4260,6 @@
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
- "defaults": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
- "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
- "dev": true,
- "requires": {
- "clone": "^1.0.2"
- }
- },
"deferred-leveldown": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz",
@@ -4488,9 +4412,9 @@
"dev": true
},
"entities": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
- "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
+ "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
"dev": true
}
}
@@ -4572,15 +4496,9 @@
}
},
"electron-to-chromium": {
- "version": "1.3.455",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.455.tgz",
- "integrity": "sha512-4lwnxp+ArqOX9hiLwLpwhfqvwzUHFuDgLz4NTiU3lhygUzWtocIJ/5Vix+mWVNE2HQ9aI1k2ncGe5H/0OktMvA==",
- "dev": true
- },
- "elegant-spinner": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-2.0.0.tgz",
- "integrity": "sha512-5YRYHhvhYzV/FC4AiMdeSIg3jAYGq9xFvbhZMpPlJoBsfYgrw2DSCYeXfat6tYBu45PWiyRr3+flaCPPmviPaA==",
+ "version": "1.3.459",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.459.tgz",
+ "integrity": "sha512-aN3Z89qEYIwVjzGi9SrcTjjopRZ3STUA6xTufS0fxZy8xOO2iqVw8rYKdT32CHgOKHOYj5KGmz3n6xUKE4QJiQ==",
"dev": true
},
"elliptic": {
@@ -4708,9 +4626,9 @@
"dev": true
},
"escodegen": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
- "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.2.tgz",
+ "integrity": "sha512-InuOIiKk8wwuOFg6x9BQXbzjrQhtyXh46K9bqVTPzSo2FnyMBaYGBMC6PhQy7yxxil9vIedFBweQBMK74/7o8A==",
"dev": true,
"requires": {
"esprima": "^4.0.1",
@@ -5139,9 +5057,9 @@
"dev": true
},
"eslint-plugin-testing-library": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.1.4.tgz",
- "integrity": "sha512-EaopQ4/Q3yEgqkoTzPMBU/a+yCn07jqIgEi+pFjnMndXs7ts8bGDg4ZlQpqlNZiUGdadanHk1+/2tp6/afyxEA==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.2.0.tgz",
+ "integrity": "sha512-ScM0vdpnRt2Piziilfr93z0J0IxyUc6fz/vnqwLhra26/xXB62pAmLBwOjn7VORGcaV/2chKNPPdwhBiQNM6Lw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "^2.29.0"
@@ -5154,9 +5072,9 @@
"dev": true
},
"eslint-scope": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
- "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
+ "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
@@ -5173,9 +5091,9 @@
}
},
"eslint-visitor-keys": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
- "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz",
+ "integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==",
"dev": true
},
"espree": {
@@ -6533,9 +6451,9 @@
}
},
"interpret": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.3.0.tgz",
- "integrity": "sha512-RDVhhDkycLoSQtE9o0vpK/vOccVDsCbWVzRxArGYnlQLcihPl2loFbPyiH7CM0m2/ijOJU3+PZbnBPaB6NJ1MA==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true
},
"invariant": {
@@ -6618,9 +6536,9 @@
"dev": true
},
"is-callable": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
+ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
"dev": true
},
"is-ci": {
@@ -6806,12 +6724,12 @@
}
},
"is-regex": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
- "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
+ "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
"dev": true,
"requires": {
- "has": "^1.0.3"
+ "has-symbols": "^1.0.1"
}
},
"is-regexp": {
@@ -8914,12 +8832,6 @@
"xtend": "~2.0.4"
},
"dependencies": {
- "clone": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz",
- "integrity": "sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=",
- "dev": true
- },
"level-fix-range": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/level-fix-range/-/level-fix-range-2.0.0.tgz",
@@ -9043,9 +8955,9 @@
"dev": true
},
"lint-staged": {
- "version": "10.2.7",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.7.tgz",
- "integrity": "sha512-srod2bTpF8riaLz+Bgr6v0mI/nSntE8M9jbh4WwAhoosx0G7RKEUIG7mI5Nu5SMbTF9o8GROPgK0Lhf5cDnUUw==",
+ "version": "10.2.9",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.9.tgz",
+ "integrity": "sha512-ziRAuXEqvJLSXg43ezBpHxRW8FOJCXISaXU//BWrxRrp5cBdRkIx7g5IsB3OI45xYGE0S6cOacfekSjDyDKF2g==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
@@ -9054,8 +8966,9 @@
"cosmiconfig": "^6.0.0",
"debug": "^4.1.1",
"dedent": "^0.7.0",
+ "enquirer": "^2.3.5",
"execa": "^4.0.1",
- "listr2": "^2.0.2",
+ "listr2": "^2.1.0",
"log-symbols": "^4.0.0",
"micromatch": "^4.0.2",
"normalize-path": "^3.0.0",
@@ -9188,25 +9101,19 @@
}
},
"listr2": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.0.4.tgz",
- "integrity": "sha512-oJaAcplPsa72rKW0eg4P4LbEJjhH+UO2I8uqR/I2wzHrVg16ohSfUy0SlcHS21zfYXxtsUpL8YXGHjyfWMR0cg==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-2.1.0.tgz",
+ "integrity": "sha512-pWrbMLO+6jxGbgAasTLUzfRYdBaQvv6sNTWDfIX8ENBNmTwt/eafZ/LlJ66/dNaDnEhzCpWricLH4U9cjSAHYg==",
"dev": true,
"requires": {
- "@samverschueren/stream-to-observable": "^0.3.0",
"chalk": "^4.0.0",
- "cli-cursor": "^3.1.0",
"cli-truncate": "^2.1.0",
- "elegant-spinner": "^2.0.0",
- "enquirer": "^2.3.5",
"figures": "^3.2.0",
"indent-string": "^4.0.0",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
- "pad": "^3.2.0",
"rxjs": "^6.5.5",
- "through": "^2.3.8",
- "uuid": "^7.0.2"
+ "through": "^2.3.8"
},
"dependencies": {
"chalk": {
@@ -9824,9 +9731,9 @@
}
},
"node-releases": {
- "version": "1.1.57",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.57.tgz",
- "integrity": "sha512-ZQmnWS7adi61A9JsllJ2gdj2PauElcjnOwTp2O011iGzoakTxUsDGSe+6vD7wXbKdqhSFymC0OSx35aAMhrSdw==",
+ "version": "1.1.58",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz",
+ "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==",
"dev": true
},
"normalize-package-data": {
@@ -10091,15 +9998,6 @@
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true
},
- "pad": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/pad/-/pad-3.2.0.tgz",
- "integrity": "sha512-2u0TrjcGbOjBTJpyewEl4hBO3OeX5wWue7eIFPzQTg6wFSvoaHcBTTUY5m+n0hd04gmTCPuY0kCpVIVuw5etwg==",
- "dev": true,
- "requires": {
- "wcwidth": "^1.0.1"
- }
- },
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -10222,9 +10120,9 @@
"dev": true
},
"pbkdf2": {
- "version": "3.0.17",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
- "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
+ "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
"dev": true,
"requires": {
"create-hash": "^1.1.2",
@@ -10980,9 +10878,9 @@
}
},
"rollup": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.12.0.tgz",
- "integrity": "sha512-vKwc/xFkZGM9DRai3Eztpr/4g0yYDgNKVq8tLXhq/aSLbR+/EVL6rTjEW9bgWgeYEIKoN66/5w2Bjv1gzyHR/w==",
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.13.1.tgz",
+ "integrity": "sha512-EiICynxIO1DTFmFn+/98gfaqCToK2nbjPjHJLuNvpcwc+P035VrXmJxi3JsOhqkdty+0cOEhJ26ceGTY3UPMPQ==",
"dev": true,
"requires": {
"fsevents": "~2.1.2"
@@ -11143,17 +11041,6 @@
"jest-worker": "^26.0.0",
"serialize-javascript": "^3.0.0",
"terser": "^4.7.0"
- },
- "dependencies": {
- "serialize-javascript": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
- "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
- "dev": true,
- "requires": {
- "randombytes": "^2.1.0"
- }
- }
}
},
"rollup-pluginutils": {
@@ -11290,10 +11177,13 @@
"dev": true
},
"serialize-javascript": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
- "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
- "dev": true
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
+ "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
},
"set-blocking": {
"version": "2.0.0",
@@ -12007,16 +11897,16 @@
}
},
"terser-webpack-plugin": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
- "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz",
+ "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==",
"dev": true,
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
- "serialize-javascript": "^2.1.2",
+ "serialize-javascript": "^3.1.0",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
@@ -12508,7 +12398,8 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"v8-compile-cache": {
"version": "2.1.1",
@@ -12755,15 +12646,6 @@
"chokidar": "^2.1.8"
}
},
- "wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
- "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
- "dev": true,
- "requires": {
- "defaults": "^1.0.3"
- }
- },
"webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
diff --git a/package.json b/package.json
index 5b895106..eaf33416 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"lint": "kcd-scripts lint",
"setup": "npm install && npm run validate -s",
"test": "kcd-scripts test",
+ "test:debug": "kcd-scripts --inspect-brk test --runInBand",
"test:update": "npm test -- --updateSnapshot --coverage",
"validate": "kcd-scripts validate"
},
@@ -42,7 +43,8 @@
"devDependencies": {
"@testing-library/dom": "^7.9.0",
"@testing-library/jest-dom": "^5.9.0",
- "@testing-library/react": "^10.0.5",
+ "@testing-library/react": "^10.2.0",
+ "is-ci": "^2.0.0",
"kcd-scripts": "^6.2.0",
"react": "^16.13.1",
"react-dom": "^16.13.1"
diff --git a/src/__tests__/click.js b/src/__tests__/click.js
index 810a25d7..5630c230 100644
--- a/src/__tests__/click.js
+++ b/src/__tests__/click.js
@@ -1,95 +1,64 @@
import React from 'react'
import {render, screen} from '@testing-library/react'
-import '@testing-library/jest-dom/extend-expect'
import userEvent from '..'
+import {setup} from './helpers/utils'
+
+test('click in input', () => {
+ const {element, getEventCalls} = setup('input')
+ userEvent.click(element)
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ mouseover: Left (0)
+ mousemove: Left (0)
+ mousedown: Left (0)
+ focus
+ mouseup: Left (0)
+ click: Left (0)
+ `)
+})
-test.each(['input', 'textarea'])(
- 'should fire the correct events for <%s>',
- type => {
- const events = []
- const eventsHandler = jest.fn(evt => events.push(evt.type))
- render(
- React.createElement(type, {
- 'data-testid': 'element',
- onMouseOver: eventsHandler,
- onMouseMove: eventsHandler,
- onMouseDown: eventsHandler,
- onFocus: eventsHandler,
- onMouseUp: eventsHandler,
- onClick: eventsHandler,
- }),
- )
-
- userEvent.click(screen.getByTestId('element'))
-
- expect(events).toEqual([
- 'mouseover',
- 'mousemove',
- 'mousedown',
- 'focus',
- 'mouseup',
- 'click',
- ])
- },
-)
+test('click in textarea', () => {
+ const {element, getEventCalls} = setup('textarea')
+ userEvent.click(element)
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ mouseover: Left (0)
+ mousemove: Left (0)
+ mousedown: Left (0)
+ focus
+ mouseup: Left (0)
+ click: Left (0)
+ `)
+})
it('should fire the correct events for ', () => {
- const events = []
- const eventsHandler = jest.fn(evt => events.push(evt.type))
- render(
- ,
- )
-
- userEvent.click(screen.getByTestId('element'))
-
- expect(events).toEqual([
- 'mouseover',
- 'mousemove',
- 'mousedown',
- 'focus',
- 'mouseup',
- 'click',
- 'change',
- ])
-
- expect(screen.getByTestId('element')).toHaveProperty('checked', true)
+ const {element, getEventCalls} = setup('input', {type: 'checkbox'})
+ expect(element).not.toBeChecked()
+ userEvent.click(element)
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ mouseover: Left (0)
+ mousemove: Left (0)
+ mousedown: Left (0)
+ focus
+ mouseup: Left (0)
+ click: unchecked -> checked
+ input: checked
+ change
+ `)
})
it('should fire the correct events for ', () => {
- const events = []
- const eventsHandler = jest.fn(evt => events.push(evt.type))
- render(
- ,
- )
-
- userEvent.click(screen.getByTestId('element'))
-
- expect(events).toEqual([])
-
- expect(screen.getByTestId('element')).toHaveProperty('checked', false)
+ const {element, getEventCalls} = setup('input', {
+ type: 'checkbox',
+ disabled: true,
+ })
+ userEvent.click(element)
+ expect(element).toBeDisabled()
+ // no event calls is expected here:
+ expect(getEventCalls()).toMatchInlineSnapshot(``)
+ expect(element).toBeDisabled()
})
+// TODO: Update all these tests to use the setup util...
+
it('should fire the correct events for ', () => {
const events = []
const eventsHandler = jest.fn(evt => events.push(evt.type))
@@ -434,12 +403,14 @@ test.each(['input', 'textarea'])(
it('should fire mouse events with the correct properties', () => {
const events = []
- const eventsHandler = jest.fn(evt => events.push({
- type: evt.type,
- button: evt.button,
- buttons: evt.buttons,
- detail: evt.detail
- }))
+ const eventsHandler = jest.fn(evt =>
+ events.push({
+ type: evt.type,
+ button: evt.button,
+ buttons: evt.buttons,
+ detail: evt.detail,
+ }),
+ )
render(
{
type: 'mouseover',
button: 0,
buttons: 0,
- detail: 0
+ detail: 0,
},
{
type: 'mousemove',
button: 0,
buttons: 0,
- detail: 0
+ detail: 0,
},
{
type: 'mousedown',
button: 0,
buttons: 1,
- detail: 1
+ detail: 1,
},
{
type: 'mouseup',
button: 0,
buttons: 1,
- detail: 1
+ detail: 1,
},
{
type: 'click',
button: 0,
buttons: 1,
- detail: 1
+ detail: 1,
},
])
})
it('should fire mouse events with custom button property', () => {
const events = []
- const eventsHandler = jest.fn(evt => events.push({
- type: evt.type,
- button: evt.button,
- buttons: evt.buttons,
- detail: evt.detail,
- altKey: evt.altKey
- }))
+ const eventsHandler = jest.fn(evt =>
+ events.push({
+ type: evt.type,
+ button: evt.button,
+ buttons: evt.buttons,
+ detail: evt.detail,
+ altKey: evt.altKey,
+ }),
+ )
render(
{
userEvent.click(screen.getByTestId('div'), {
button: 1,
- altKey: true
+ altKey: true,
})
expect(events).toEqual([
@@ -519,47 +492,49 @@ it('should fire mouse events with custom button property', () => {
button: 0,
buttons: 0,
detail: 0,
- altKey: true
+ altKey: true,
},
{
type: 'mousemove',
button: 0,
buttons: 0,
detail: 0,
- altKey: true
+ altKey: true,
},
{
type: 'mousedown',
button: 1,
buttons: 4,
detail: 1,
- altKey: true
+ altKey: true,
},
{
type: 'mouseup',
button: 1,
buttons: 4,
detail: 1,
- altKey: true
+ altKey: true,
},
{
type: 'click',
button: 1,
buttons: 4,
detail: 1,
- altKey: true
+ altKey: true,
},
])
})
it('should fire mouse events with custom buttons property', () => {
const events = []
- const eventsHandler = jest.fn(evt => events.push({
- type: evt.type,
- button: evt.button,
- buttons: evt.buttons,
- detail: evt.detail
- }))
+ const eventsHandler = jest.fn(evt =>
+ events.push({
+ type: evt.type,
+ button: evt.button,
+ buttons: evt.buttons,
+ detail: evt.detail,
+ }),
+ )
render(
{
)
userEvent.click(screen.getByTestId('div'), {
- buttons: 4
+ buttons: 4,
})
expect(events).toEqual([
@@ -581,31 +556,31 @@ it('should fire mouse events with custom buttons property', () => {
type: 'mouseover',
button: 0,
buttons: 0,
- detail: 0
+ detail: 0,
},
{
type: 'mousemove',
button: 0,
buttons: 0,
- detail: 0
+ detail: 0,
},
{
type: 'mousedown',
button: 1,
buttons: 4,
- detail: 1
+ detail: 1,
},
{
type: 'mouseup',
button: 1,
buttons: 4,
- detail: 1
+ detail: 1,
},
{
type: 'click',
button: 1,
buttons: 4,
- detail: 1
+ detail: 1,
},
])
})
diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js
new file mode 100644
index 00000000..cf834752
--- /dev/null
+++ b/src/__tests__/helpers/utils.js
@@ -0,0 +1,185 @@
+import React from 'react'
+import {render} from '@testing-library/react'
+
+// this is pretty helpful:
+// https://jsbin.com/nimelileyo/edit?js,output
+
+// all of the stuff below is complex magic that makes the simpler tests work
+// sorrynotsorry...
+
+const unstringSnapshotSerializer = {
+ test: val => typeof val === 'string',
+ print: val => val,
+}
+
+expect.addSnapshotSerializer(unstringSnapshotSerializer)
+
+let eventListeners = []
+
+function getTestData(element) {
+ return {
+ value: element.value,
+ selectionStart: element.selectionStart,
+ selectionEnd: element.selectionEnd,
+ checked: element.checked,
+ }
+}
+
+function addEventListener(el, type, listener, options) {
+ const hijackedListener = e => {
+ e.testData = {previous: e.target.previousTestData}
+ const retVal = listener(e)
+ const next = getTestData(e.target)
+ e.testData.next = next
+ e.target.previousTestData = next
+ return retVal
+ }
+ eventListeners.push({el, type, listener: hijackedListener})
+ el.addEventListener(type, hijackedListener, options)
+}
+
+function setup(elementType, props, ...children) {
+ const {
+ container: {firstChild: element},
+ } = render(React.createElement(elementType, props, ...children))
+ element.previousTestData = getTestData(element)
+
+ const getEventCalls = addListeners(element)
+ return {element, getEventCalls}
+}
+
+function addListeners(element) {
+ const generalListener = jest.fn().mockName('eventListener')
+ const listeners = [
+ 'keydown',
+ 'keyup',
+ 'keypress',
+ 'input',
+ 'change',
+ 'blur',
+ 'focus',
+ 'click',
+ 'mouseover',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave',
+ 'mouseup',
+ 'mousedown',
+ ]
+
+ for (const name of listeners) {
+ addEventListener(element, name, generalListener)
+ }
+ function getEventCalls() {
+ return generalListener.mock.calls
+ .map(([event]) => {
+ const window = event.target.ownerDocument.defaultView
+ const modifiers = ['altKey', 'shiftKey', 'metaKey', 'ctrlKey']
+ .filter(key => event[key])
+ .map(k => `{${k.replace('Key', '')}}`)
+ .join('')
+
+ if (
+ event.type === 'click' &&
+ event.hasOwnProperty('testData') &&
+ (element.type === 'checkbox' || element.type === 'radio')
+ ) {
+ return getCheckboxOrRadioClickedLine(event)
+ }
+
+ if (event.type === 'input' && event.hasOwnProperty('testData')) {
+ return getInputLine(element, event)
+ }
+
+ if (event instanceof window.KeyboardEvent) {
+ return getKeyboardEventLine(event, modifiers)
+ }
+
+ if (event instanceof window.MouseEvent) {
+ return getMouseEventLine(event, modifiers)
+ }
+
+ return [event.type, modifiers].join(' ').trim()
+ })
+ .join('\n')
+ }
+ return getEventCalls
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+const mouseButtonMap = {
+ 0: 'Left',
+ 1: 'Middle',
+ 2: 'Right',
+ 3: 'Browser Back',
+ 4: 'Browser Forward',
+}
+function getMouseEventLine(event, modifiers) {
+ return [
+ `${event.type}:`,
+ mouseButtonMap[event.button],
+ `(${event.button})`,
+ modifiers,
+ ]
+ .join(' ')
+ .trim()
+}
+
+function getKeyboardEventLine(event, modifiers) {
+ return [
+ `${event.type}:`,
+ event.key,
+ typeof event.keyCode === 'undefined' ? null : `(${event.keyCode})`,
+ modifiers,
+ ]
+ .join(' ')
+ .trim()
+}
+
+function getCheckboxOrRadioClickedLine(event) {
+ const {previous, next} = event.testData
+
+ return `${event.type}: ${previous.checked ? '' : 'un'}checked -> ${
+ next.checked ? '' : 'un'
+ }checked`
+}
+
+function getInputLine(element, event) {
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
+ const {previous, next} = event.testData
+
+ if (element.type === 'checkbox' || element.type === 'radio') {
+ return `${event.type}: ${next.checked ? '' : 'un'}checked`
+ } else {
+ const prevVal = [
+ previous.value.slice(0, previous.selectionStart),
+ ...(previous.selectionStart === previous.selectionEnd
+ ? ['{CURSOR}']
+ : [
+ '{SELECTION}',
+ previous.value.slice(
+ previous.selectionStart,
+ previous.selectionEnd,
+ ),
+ '{/SELECTION}',
+ ]),
+ previous.value.slice(previous.selectionEnd),
+ ].join('')
+ return `${event.type}: "${prevVal}" -> "${next.value}"`
+ }
+ } else {
+ throw new Error(
+ `fired ${event.type} event on a ${element.tagName} which probably doesn't make sense. Fix that, or handle it in the setup function`,
+ )
+ }
+}
+
+// eslint-disable-next-line jest/prefer-hooks-on-top
+afterEach(() => {
+ for (const {el, type, listener} of eventListeners) {
+ el.removeEventListener(type, listener)
+ }
+ eventListeners = []
+})
+
+export {setup, addEventListener}
diff --git a/src/__tests__/selectoptions.js b/src/__tests__/selectoptions.js
index 899b3a50..98c6813b 100644
--- a/src/__tests__/selectoptions.js
+++ b/src/__tests__/selectoptions.js
@@ -302,3 +302,19 @@ test('sets the selected prop on the selected OPTION using OPTGROUPS', () => {
expect(screen.getByTestId('val2').selected).toBe(false)
expect(screen.getByTestId('val3').selected).toBe(true)
})
+
+test('does not select anything if no matching options are given', () => {
+ const {
+ container: {firstChild: select},
+ } = render(
+ ,
+ )
+
+ userEvent.selectOptions(select, 'Matches nothing')
+ expect(select.selectedIndex).toBe(0)
+})
diff --git a/src/__tests__/type-modifiers.js b/src/__tests__/type-modifiers.js
new file mode 100644
index 00000000..47704470
--- /dev/null
+++ b/src/__tests__/type-modifiers.js
@@ -0,0 +1,232 @@
+import userEvent from '..'
+import {setup, addEventListener} from './helpers/utils'
+
+// Note, use the setup function at the bottom of the file...
+// but don't hurt yourself trying to read it 😅
+
+// keep in mind that we do not handle modifier interactions. This is primarily
+// because modifiers behave differently on different operating systems.
+// For example: {alt}{backspace}{/alt} will remove everything from the current
+// cursor position to the beginning of the word on Mac, but you need to use
+// {ctrl}{backspace}{/ctrl} to do that on Windows. And that doesn't appear to
+// be consistent within an OS either 🙃
+// So we're not going to even try.
+
+// This also means that '{shift}a' will fire an input event with the shiftKey,
+// but will not capitalize "a".
+
+test('{esc} triggers typing the escape character', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{esc}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Escape (27)
+ keyup: Escape (27)
+ `)
+})
+
+test('{backspace} triggers typing the backspace character and deletes the character behind the cursor', async () => {
+ const {element: input, getEventCalls} = setup('input')
+ input.value = 'yo'
+ input.setSelectionRange(1, 1)
+
+ await userEvent.type(input, '{backspace}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Backspace (8)
+ input: "y{CURSOR}o" -> "o"
+ keyup: Backspace (8)
+ `)
+})
+
+test('{backspace} on a readOnly input', async () => {
+ const {element: input, getEventCalls} = setup('input')
+ input.readOnly = true
+ input.value = 'yo'
+ input.setSelectionRange(1, 1)
+
+ await userEvent.type(input, '{backspace}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Backspace (8)
+ keyup: Backspace (8)
+ `)
+})
+
+test('{backspace} deletes the selected range', async () => {
+ const {element: input, getEventCalls} = setup('input')
+ input.value = 'Hi there'
+ input.setSelectionRange(1, 5)
+
+ await userEvent.type(input, '{backspace}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Backspace (8)
+ input: "H{SELECTION}i th{/SELECTION}ere" -> "Here"
+ keyup: Backspace (8)
+ `)
+})
+
+test('{alt}a{/alt}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{alt}a{/alt}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Alt (18) {alt}
+ keydown: a (97) {alt}
+ keypress: a (97) {alt}
+ input: "{CURSOR}" -> "a"
+ keyup: a (97) {alt}
+ keyup: Alt (18)
+ `)
+})
+
+test('{meta}a{/meta}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{meta}a{/meta}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Meta (93) {meta}
+ keydown: a (97) {meta}
+ keypress: a (97) {meta}
+ input: "{CURSOR}" -> "a"
+ keyup: a (97) {meta}
+ keyup: Meta (93)
+ `)
+})
+
+test('{ctrl}a{/ctrl}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{ctrl}a{/ctrl}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Control (17) {ctrl}
+ keydown: a (97) {ctrl}
+ keypress: a (97) {ctrl}
+ input: "{CURSOR}" -> "a"
+ keyup: a (97) {ctrl}
+ keyup: Control (17)
+ `)
+})
+
+test('{shift}a{/shift}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{shift}a{/shift}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Shift (16) {shift}
+ keydown: a (97) {shift}
+ keypress: a (97) {shift}
+ input: "{CURSOR}" -> "a"
+ keyup: a (97) {shift}
+ keyup: Shift (16)
+ `)
+})
+
+test('a{enter}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, 'a{enter}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: a (97)
+ keypress: a (97)
+ input: "{CURSOR}" -> "a"
+ keyup: a (97)
+ keydown: Enter (13)
+ keypress: Enter (13)
+ keyup: Enter (13)
+ `)
+})
+
+test('{enter} with preventDefault keydown', async () => {
+ const {element: input, getEventCalls} = setup('input')
+ addEventListener(input, 'keydown', e => e.preventDefault())
+
+ await userEvent.type(input, '{enter}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Enter (13)
+ keyup: Enter (13)
+ `)
+})
+
+test('{enter} on a button', async () => {
+ const {element: button, getEventCalls} = setup('button')
+
+ await userEvent.type(button, '{enter}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Enter (13)
+ keypress: Enter (13)
+ click: Left (0)
+ keyup: Enter (13)
+ `)
+})
+
+test('{enter} on a textarea', async () => {
+ const {element: textarea, getEventCalls} = setup('textarea')
+
+ await userEvent.type(textarea, '{enter}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Enter (13)
+ keypress: Enter (13)
+ input: "{CURSOR}" -> "
+ "
+ keyup: Enter (13)
+ `)
+})
+
+test('{meta}{enter}{/meta} on a button', async () => {
+ const {element: button, getEventCalls} = setup('button')
+
+ await userEvent.type(button, '{meta}{enter}{/meta}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Meta (93) {meta}
+ keydown: Enter (13) {meta}
+ keypress: Enter (13) {meta}
+ click: Left (0) {meta}
+ keyup: Enter (13) {meta}
+ keyup: Meta (93)
+ `)
+})
+
+test('{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}', async () => {
+ const {element: input, getEventCalls} = setup('input')
+
+ await userEvent.type(input, '{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}')
+
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: Meta (93) {meta}
+ keydown: Alt (18) {alt}{meta}
+ keydown: Control (17) {alt}{meta}{ctrl}
+ keydown: a (97) {alt}{meta}{ctrl}
+ keypress: a (97) {alt}{meta}{ctrl}
+ input: "{CURSOR}" -> "a"
+ keyup: a (97) {alt}{meta}{ctrl}
+ keyup: Control (17) {alt}{meta}
+ keyup: Alt (18) {meta}
+ keyup: Meta (93)
+ `)
+})
diff --git a/src/__tests__/type.js b/src/__tests__/type.js
index 26652f36..8dc95614 100644
--- a/src/__tests__/type.js
+++ b/src/__tests__/type.js
@@ -1,39 +1,58 @@
import React, {Fragment} from 'react'
import {render, screen} from '@testing-library/react'
-import userEvent from '../../src'
-
-test.each(['input', 'textarea'])('should type text in <%s>', async type => {
- const onChange = jest.fn()
- render(
- React.createElement(type, {
- 'data-testid': 'input',
- onChange,
- }),
- )
- const text = 'Hello, world!'
- await userEvent.type(screen.getByTestId('input'), text)
- expect(onChange).toHaveBeenCalledTimes(text.length)
- expect(screen.getByTestId('input')).toHaveProperty('value', text)
+import userEvent from '..'
+import {setup} from './helpers/utils'
+
+it('types text in input', async () => {
+ const {element, getEventCalls} = setup('input')
+ await userEvent.type(element, 'Sup')
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: S (83)
+ keypress: S (83)
+ input: "{CURSOR}" -> "S"
+ keyup: S (83)
+ keydown: u (117)
+ keypress: u (117)
+ input: "S{CURSOR}" -> "Su"
+ keyup: u (117)
+ keydown: p (112)
+ keypress: p (112)
+ input: "Su{CURSOR}" -> "Sup"
+ keyup: p (112)
+ `)
})
-test('should append text one by one', async () => {
- const onChange = jest.fn()
- render()
- await userEvent.type(screen.getByTestId('input'), 'hello')
- await userEvent.type(screen.getByTestId('input'), ' world')
- expect(onChange).toHaveBeenCalledTimes('hello world'.length)
- expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world')
+it('types text in textarea', async () => {
+ const {element, getEventCalls} = setup('textarea')
+ await userEvent.type(element, 'Sup')
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: S (83)
+ keypress: S (83)
+ input: "{CURSOR}" -> "S"
+ keyup: S (83)
+ keydown: u (117)
+ keypress: u (117)
+ input: "S{CURSOR}" -> "Su"
+ keyup: u (117)
+ keydown: p (112)
+ keypress: p (112)
+ input: "Su{CURSOR}" -> "Sup"
+ keyup: p (112)
+ `)
})
test('should append text all at once', async () => {
- const onChange = jest.fn()
- render()
- await userEvent.type(screen.getByTestId('input'), 'hello', {allAtOnce: true})
- await userEvent.type(screen.getByTestId('input'), ' world', {allAtOnce: true})
- expect(onChange).toHaveBeenCalledTimes(2)
- expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world')
+ const {element, getEventCalls} = setup('input')
+ await userEvent.type(element, 'Sup', {allAtOnce: true})
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ input: "{CURSOR}" -> "Sup"
+ `)
})
+// TODO: Let's migrate these tests to use the setup util
test('should not type when event.preventDefault() is called', async () => {
const onChange = jest.fn()
const onKeydown = jest
@@ -316,3 +335,64 @@ test('should enter text up to maxLength of the current element if provided', asy
expect(input2).toHaveValue(input2ExpectedValue)
expect(input2).toHaveFocus()
})
+
+test('should replace selected text one by one', async () => {
+ const onChange = jest.fn()
+ const {
+ container: {firstChild: input},
+ } = render()
+ const selectionStart = 'hello world'.search('world')
+ const selectionEnd = selectionStart + 'world'.length
+ input.setSelectionRange(selectionStart, selectionEnd)
+ await userEvent.type(input, 'friend')
+ expect(onChange).toHaveBeenCalledTimes('friend'.length)
+ expect(input).toHaveValue('hello friend')
+})
+
+test('should replace selected text one by one up to maxLength if provided', async () => {
+ const maxLength = 10
+ const onChange = jest.fn()
+ const {
+ container: {firstChild: input},
+ } = render(
+ ,
+ )
+ const selectionStart = 'hello world'.search('world')
+ const selectionEnd = selectionStart + 'world'.length
+ input.setSelectionRange(selectionStart, selectionEnd)
+ const resultIfUnlimited = 'hello friend'
+ const slicedText = resultIfUnlimited.slice(0, maxLength)
+ await userEvent.type(input, 'friend')
+ const truncatedCharCount = resultIfUnlimited.length - slicedText.length
+ expect(onChange).toHaveBeenCalledTimes('friend'.length - truncatedCharCount)
+ expect(input).toHaveValue(slicedText)
+})
+
+test('should replace selected text all at once', async () => {
+ const onChange = jest.fn()
+ const {
+ container: {firstChild: input},
+ } = render()
+ const selectionStart = 'hello world'.search('world')
+ const selectionEnd = selectionStart + 'world'.length
+ input.setSelectionRange(selectionStart, selectionEnd)
+ await userEvent.type(input, 'friend', {allAtOnce: true})
+ expect(onChange).toHaveBeenCalledTimes(1)
+ expect(input).toHaveValue('hello friend')
+})
+
+test('does not continue firing events when disabled during typing', async () => {
+ function TestComp() {
+ const [disabled, setDisabled] = React.useState(false)
+ return setDisabled(true)} />
+ }
+ const {
+ container: {firstChild: input},
+ } = render()
+ await userEvent.type(input, 'hi there')
+ expect(input).toHaveValue('h')
+})
diff --git a/src/index.js b/src/index.js
index b1a8a1bf..febda901 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,11 +1,5 @@
-import {
- getConfig as getDOMTestingLibraryConfig,
- fireEvent,
-} from '@testing-library/dom'
-
-function wait(time) {
- return new Promise(resolve => setTimeout(() => resolve(), time))
-}
+import {fireEvent} from '@testing-library/dom'
+import {type} from './type'
function isMousePressEvent(event) {
return (
@@ -323,88 +317,6 @@ function clear(element) {
backspace(element)
}
-// this needs to be wrapped in the asyncWrapper for React's act and angular's change detection
-async function type(...args) {
- let result
- await getDOMTestingLibraryConfig().asyncWrapper(async () => {
- result = await typeImpl(...args)
- })
- return result
-}
-
-async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
- if (element.disabled) return
-
- element.focus()
-
- // The focussed element could change between each event, so get the currently active element each time
- const currentElement = () => element.ownerDocument.activeElement
- const currentValue = () => element.ownerDocument.activeElement.value
-
- const computeText = () =>
- currentElement().maxLength > 0
- ? text.slice(
- 0,
- Math.max(currentElement().maxLength - currentValue().length, 0),
- )
- : text
-
- if (allAtOnce) {
- if (!element.readOnly) {
- const previousText = element.value
-
- fireEvent.input(element, {
- target: {value: previousText + computeText()},
- })
- }
- } else {
- for (let index = 0; index < text.length; index++) {
- const char = text[index]
- const key = char // TODO: check if this also valid for characters with diacritic markers e.g. úé etc
- const keyCode = char.charCodeAt(0)
-
- // eslint-disable-next-line no-await-in-loop
- if (delay > 0) await wait(delay)
-
- if (currentElement().disabled) return
-
- const downEvent = fireEvent.keyDown(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- })
-
- if (downEvent) {
- const pressEvent = fireEvent.keyPress(currentElement(), {
- key,
- keyCode,
- charCode: keyCode,
- })
-
- const isTextPastThreshold = !computeText().length
-
- if (pressEvent && !isTextPastThreshold) {
- if (!element.readOnly) {
- fireEvent.input(currentElement(), {
- target: {
- value: currentValue() + key,
- },
- bubbles: true,
- cancelable: true,
- })
- }
- }
- }
-
- fireEvent.keyUp(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- })
- }
- }
-}
-
function upload(element, fileOrFiles, {clickInit, changeInit} = {}) {
if (element.disabled) return
const focusedElement = element.ownerDocument.activeElement
diff --git a/src/tick.js b/src/tick.js
new file mode 100644
index 00000000..126e39bc
--- /dev/null
+++ b/src/tick.js
@@ -0,0 +1,54 @@
+/* istanbul ignore file */
+// the part of this file that we need tested is definitely being run
+// and the part that is not cannot easily have useful tests written
+// anyway. So we're just going to ignore coverage for this file
+// we're using this to ensure events are not fired synchronously one
+// after the other because that's not how things work when a user
+// fires them...
+/**
+ * copied from React's enqueueTask.js
+ */
+
+let didWarnAboutMessageChannel = false
+let enqueueTask
+try {
+ // read require off the module object to get around the bundlers.
+ // we don't want them to detect a require and bundle a Node polyfill.
+ const requireString = `require${Math.random()}`.slice(0, 7)
+ const nodeRequire = module && module[requireString]
+ // assuming we're in node, let's try to get node's
+ // version of setImmediate, bypassing fake timers if any.
+ enqueueTask = nodeRequire.call(module, 'timers').setImmediate
+} catch (_err) {
+ // we're in a browser
+ // we can't use regular timers because they may still be faked
+ // so we try MessageChannel+postMessage instead
+ enqueueTask = callback => {
+ const supportsMessageChannel = typeof MessageChannel === 'function'
+ if (supportsMessageChannel) {
+ const channel = new MessageChannel()
+ channel.port1.onmessage = callback
+ channel.port2.postMessage(undefined)
+ } else if (didWarnAboutMessageChannel === false) {
+ didWarnAboutMessageChannel = true
+
+ // eslint-disable-next-line no-console
+ console.error(
+ 'This browser does not have a MessageChannel implementation, ' +
+ 'so enqueuing tasks via await act(async () => ...) will fail. ' +
+ 'Please file an issue at https://github.com/testing-library/user-event/issues ' +
+ 'if you encounter this warning.',
+ )
+ }
+ }
+}
+
+function tick() {
+ return {
+ then(resolve) {
+ enqueueTask(resolve)
+ },
+ }
+}
+
+export {tick}
diff --git a/src/type.js b/src/type.js
new file mode 100644
index 00000000..de87434f
--- /dev/null
+++ b/src/type.js
@@ -0,0 +1,313 @@
+import {
+ getConfig as getDOMTestingLibraryConfig,
+ fireEvent,
+} from '@testing-library/dom'
+import {tick} from './tick'
+
+function wait(time) {
+ return new Promise(resolve => setTimeout(() => resolve(), time))
+}
+
+// this needs to be wrapped in the asyncWrapper for React's act and angular's change detection
+async function type(...args) {
+ let result
+ await getDOMTestingLibraryConfig().asyncWrapper(async () => {
+ result = await typeImpl(...args)
+ })
+ return result
+}
+
+async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
+ if (element.disabled) return
+
+ element.focus()
+
+ // The focused element could change between each event, so get the currently active element each time
+ const currentElement = () => element.ownerDocument.activeElement
+ const currentValue = () => element.ownerDocument.activeElement.value
+
+ if (allAtOnce) {
+ if (!element.readOnly) {
+ fireEvent.input(element, {
+ target: {value: calculateNewValue(text)},
+ })
+ }
+ } else {
+ const eventCallbackMap = {
+ ...modifier({
+ name: 'shift',
+ key: 'Shift',
+ keyCode: 16,
+ modifierProperty: 'shiftKey',
+ }),
+ ...modifier({
+ name: 'ctrl',
+ key: 'Control',
+ keyCode: 17,
+ modifierProperty: 'ctrlKey',
+ }),
+ ...modifier({
+ name: 'alt',
+ key: 'Alt',
+ keyCode: 18,
+ modifierProperty: 'altKey',
+ }),
+ ...modifier({
+ name: 'meta',
+ key: 'Meta',
+ keyCode: 93,
+ modifierProperty: 'metaKey',
+ }),
+ '{enter}': async ({eventOverrides}) => {
+ const key = 'Enter'
+ const keyCode = 13
+
+ const keyDownDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (keyDownDefaultNotPrevented) {
+ await tick()
+
+ fireEvent.keyPress(currentElement(), {
+ key,
+ keyCode,
+ charCode: keyCode,
+ ...eventOverrides,
+ })
+ }
+
+ if (currentElement().tagName === 'BUTTON') {
+ await tick()
+ fireEvent.click(currentElement(), {
+ ...eventOverrides,
+ })
+ }
+
+ if (currentElement().tagName === 'TEXTAREA') {
+ await tick()
+ fireEvent.input(currentElement(), {
+ target: {value: calculateNewValue('\n')},
+ inputType: 'insertLineBreak',
+ ...eventOverrides,
+ })
+ }
+
+ await tick()
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ '{esc}': async ({eventOverrides}) => {
+ const key = 'Escape'
+ const keyCode = 27
+
+ fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ await tick()
+
+ // NOTE: Browsers do not fire a keypress on meta key presses
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ '{backspace}': async ({eventOverrides}) => {
+ const key = 'Backspace'
+ const keyCode = 8
+
+ fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (!currentElement().readOnly) {
+ await tick()
+
+ const {selectionStart} = currentElement()
+
+ fireEvent.input(currentElement(), {
+ target: {value: calculateNewValue('')},
+ inputType: 'deleteContentBackward',
+ ...eventOverrides,
+ })
+
+ element.setSelectionRange?.(selectionStart, selectionStart)
+ }
+
+ await tick()
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ }
+ const eventCallbacks = []
+ let remainingString = text
+ while (remainingString) {
+ const eventKey = Object.keys(eventCallbackMap).find(key =>
+ remainingString.startsWith(key),
+ )
+ if (eventKey) {
+ eventCallbacks.push(eventCallbackMap[eventKey])
+ remainingString = remainingString.slice(eventKey.length)
+ } else {
+ const character = remainingString[0]
+ eventCallbacks.push((...args) => typeCharacter(character, ...args))
+ remainingString = remainingString.slice(1)
+ }
+ }
+ const eventOverrides = {}
+ for (const callback of eventCallbacks) {
+ if (delay > 0) await wait(delay)
+ if (!currentElement().disabled) {
+ const returnValue = await callback({eventOverrides})
+ Object.assign(eventOverrides, returnValue?.eventOverrides)
+ }
+ }
+ }
+
+ function calculateNewValue(newEntry) {
+ const {selectionStart, selectionEnd} = currentElement()
+ // can't use .maxLength property because of a jsdom bug:
+ // https://github.com/jsdom/jsdom/issues/2927
+ const maxLength = Number(currentElement().getAttribute('maxlength') ?? -1)
+ const value = currentValue()
+ let newValue
+ if (selectionStart === selectionEnd) {
+ if (selectionStart === 0) {
+ // at the beginning of the input
+ newValue = newEntry + value
+ } else if (selectionStart === value.length) {
+ // at the end of the input
+ newValue = value + newEntry
+ } else {
+ // in the middle of the input
+ newValue =
+ value.slice(0, selectionStart - 1) +
+ newEntry +
+ value.slice(selectionEnd)
+ }
+ } else {
+ // we have something selected
+ newValue =
+ value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd)
+ }
+
+ if (maxLength < 0) {
+ return newValue
+ } else {
+ return newValue.slice(0, maxLength)
+ }
+ }
+
+ async function typeCharacter(char, {eventOverrides}) {
+ const key = char // TODO: check if this also valid for characters with diacritic markers e.g. úé etc
+ const keyCode = char.charCodeAt(0)
+
+ const keyDownDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (keyDownDefaultNotPrevented) {
+ await tick()
+
+ const keyPressDefaultNotPrevented = fireEvent.keyPress(currentElement(), {
+ key,
+ keyCode,
+ charCode: keyCode,
+ ...eventOverrides,
+ })
+
+ const newValue = calculateNewValue(key)
+
+ if (keyPressDefaultNotPrevented && newValue !== currentValue()) {
+ if (!currentElement().readOnly) {
+ await tick()
+
+ const {selectionStart} = currentElement()
+
+ fireEvent.input(currentElement(), {
+ target: {
+ value: newValue,
+ },
+ })
+
+ element.setSelectionRange?.(selectionStart + 1, selectionStart + 1)
+ }
+ }
+ }
+
+ await tick()
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ }
+
+ function modifier({name, key, keyCode, modifierProperty}) {
+ return {
+ [`{${name}}`]: ({eventOverrides}) => {
+ const newEventOverrides = {[modifierProperty]: true}
+
+ fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ ...newEventOverrides,
+ })
+
+ return {eventOverrides: newEventOverrides}
+ },
+ [`{/${name}}`]: ({eventOverrides}) => {
+ const newEventOverrides = {[modifierProperty]: false}
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ ...newEventOverrides,
+ })
+
+ return {eventOverrides: newEventOverrides}
+ },
+ }
+ }
+}
+
+export {type}
+
+/*
+eslint
+ no-await-in-loop: "off",
+ no-loop-func: "off",
+ max-lines-per-function: "off",
+*/
diff --git a/tests/setup-env.js b/tests/setup-env.js
index 9c33c513..839137dc 100644
--- a/tests/setup-env.js
+++ b/tests/setup-env.js
@@ -1,4 +1,5 @@
import '@testing-library/jest-dom/extend-expect'
+import isCI from 'is-ci'
// prevent console calls from making it out into the wild
beforeEach(() => {
@@ -8,28 +9,28 @@ beforeEach(() => {
jest.spyOn(console, 'info')
})
+// but we only assert in CI because it's annoying locally during development
afterEach(() => {
- if (console.error.mock.calls.length) {
+ if (isCI && console.error.mock.calls.length) {
throw new Error(`console.error should not be called in tests`)
}
console.error.mockRestore()
- if (console.log.mock.calls.length) {
+ if (isCI && console.log.mock.calls.length) {
throw new Error(`console.log should not be called in tests`)
}
console.log.mockRestore()
- if (console.warn.mock.calls.length) {
+ if (isCI && console.warn.mock.calls.length) {
throw new Error(`console.warn should not be called in tests`)
}
console.warn.mockRestore()
- if (console.info.mock.calls.length) {
+ if (isCI && console.info.mock.calls.length) {
throw new Error(`console.info should not be called in tests`)
}
console.info.mockRestore()
})
-
/*
eslint
no-console: "off",