|
| 1 | +# Flexible React Testing Library |
| 2 | + |
| 3 | +A thin wrapper around [React Testing Library](https://github.com/testing-library/react-testing-library) |
| 4 | +which makes using custom queries easier. |
| 5 | + |
| 6 | +See [this PR](https://github.com/testing-library/dom-testing-library/issues/266) for the |
| 7 | +discussion behind this and for reasoning why this isn't in core `@testing-library/dom`. |
| 8 | + |
| 9 | +## Install dependency |
| 10 | + |
| 11 | +```bash |
| 12 | +npm install --save-dev flexible-testing-library-react |
| 13 | +``` |
| 14 | + |
| 15 | +This assumes you are using Jest for testing. |
| 16 | + |
| 17 | +## Usage |
| 18 | + |
| 19 | +This mostly follows the API of React Testing Library but with one important difference: |
| 20 | + |
| 21 | +```jsx |
| 22 | +// old |
| 23 | +import { screen, render } from '@testing-library/react'; |
| 24 | +render(<MyComponent />); |
| 25 | +screen.getByLabelText('foo').something(); |
| 26 | + |
| 27 | +// new |
| 28 | +import { screen, render, labelText } from 'flexible-testing-library-react'; |
| 29 | +render(<MyComponent />); |
| 30 | +screen.getBy(labelText('foo')).something(); |
| 31 | +``` |
| 32 | + |
| 33 | +Or the alternative (scoped) syntax: |
| 34 | + |
| 35 | +```jsx |
| 36 | +// old |
| 37 | +import { render } from '@testing-library/react'; |
| 38 | +const { getByLabelText } = render(<MyComponent />); |
| 39 | +getByLabelText('foo').something(); |
| 40 | + |
| 41 | +// new |
| 42 | +import { render, labelText } from 'flexible-testing-library-react'; |
| 43 | +const { getBy } = render(<MyComponent />); |
| 44 | +getBy(labelText('foo')).something(); |
| 45 | +``` |
| 46 | + |
| 47 | +Also parameters for `findBy` now live in a more logical place: |
| 48 | + |
| 49 | +```jsx |
| 50 | +// old |
| 51 | +import { screen, render } from '@testing-library/react'; |
| 52 | +render(<MyComponent />); |
| 53 | +screen.findByTitle('foo', {}, { timeout: 1000 }).something(); |
| 54 | + |
| 55 | +// new |
| 56 | +import { screen, render, title } from 'flexible-testing-library-react'; |
| 57 | +render(<MyComponent />); |
| 58 | +screen.findBy(title('foo'), { timeout: 1000 }).something(); |
| 59 | +// no need to pass the empty {} argument to title() any more! |
| 60 | +``` |
| 61 | + |
| 62 | +A new Jest matcher is also available: |
| 63 | + |
| 64 | +```jsx |
| 65 | +import { screen, render, labelText } from 'flexible-testing-library-react'; |
| 66 | +import 'flexible-testing-library-react/extend-expect'; |
| 67 | + |
| 68 | +render(<MyComponent />); |
| 69 | +expect(screen).toContainElementWith(labelText('foo')); |
| 70 | +expect(screen).not.toContainElementWith(labelText('nope')); |
| 71 | +``` |
| 72 | + |
| 73 | +(this matcher improves on `toBeInTheDocument`, which has |
| 74 | +[problems with negation](https://github.com/testing-library/jest-dom/issues/106)) |
| 75 | + |
| 76 | +## Reference |
| 77 | + |
| 78 | +### `getBy` |
| 79 | + |
| 80 | +```javascript |
| 81 | +getBy(title('hello')) |
| 82 | +``` |
| 83 | + |
| 84 | +Returns en element, or throws an exception if no elements were found (or multiple elements |
| 85 | +matched). |
| 86 | + |
| 87 | +### `getAllBy` |
| 88 | + |
| 89 | +```javascript |
| 90 | +getAllBy(title('hello')) |
| 91 | +``` |
| 92 | + |
| 93 | +Returns a list of elements, or throws an exception if no elements were found. |
| 94 | + |
| 95 | +### `queryBy` |
| 96 | + |
| 97 | +```javascript |
| 98 | +queryBy(title('hello')) |
| 99 | +``` |
| 100 | + |
| 101 | +Returns an element, or `null` if no elements were found, or throws an exception if multiple |
| 102 | +elements matched. |
| 103 | + |
| 104 | +### `queryAllBy` |
| 105 | + |
| 106 | +```javascript |
| 107 | +queryAllBy(title('hello')) |
| 108 | +``` |
| 109 | + |
| 110 | +Returns a list of elements (which could be empty). |
| 111 | + |
| 112 | +### `findBy` |
| 113 | + |
| 114 | +```javascript |
| 115 | +await findBy(title('hello')) |
| 116 | +await findBy(title('hello'), { timeout: 1000 }) |
| 117 | +``` |
| 118 | + |
| 119 | +Waits until at least one matching element exists and returns it, or throws if the |
| 120 | +timeout is reached before a matching element is found. Throws if multiple elements |
| 121 | +match. |
| 122 | + |
| 123 | +The second parameter is an optional options dictionary, which is passed directly to |
| 124 | +DOM Testing Library's [`waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor). |
| 125 | + |
| 126 | +### `findAllBy` |
| 127 | + |
| 128 | +```javascript |
| 129 | +await findAllBy(title('hello')) |
| 130 | +await findAllBy(title('hello'), { timeout: 1000 }) |
| 131 | +``` |
| 132 | + |
| 133 | +Waits until at least one matching element exists and returns a list of all matches, |
| 134 | +or throws if the timeout is reached before a matching element is found. |
| 135 | + |
| 136 | +The second parameter is an optional options dictionary, which is passed directly to |
| 137 | +DOM Testing Library's [`waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor). |
| 138 | + |
| 139 | +### Queries |
| 140 | + |
| 141 | +For a list of supported queries, see the |
| 142 | +[DOM Testing Library documentation](https://testing-library.com/docs/dom-testing-library/api-queries#queries); |
| 143 | +each query is available here. |
| 144 | + |
| 145 | +Examples (note that the options can be omitted but are shown here to demonstrate their usage): |
| 146 | + |
| 147 | +| Function | Example | Upstream Docs | |
| 148 | +|----------|---------|---------------| |
| 149 | +| `labelText` | `getAllBy(labelText('hello', { exact: false }))` | [ByLabelText](https://testing-library.com/docs/dom-testing-library/api-queries#bylabeltext) | |
| 150 | +| `placeholderText` | `getAllBy(placeholderText('hello', { exact: false }))` | [ByPlaceholderText](https://testing-library.com/docs/dom-testing-library/api-queries#byplaceholdertext) | |
| 151 | +| `text` | `getAllBy(text('hello', { exact: false }))` | [ByText](https://testing-library.com/docs/dom-testing-library/api-queries#bytext) | |
| 152 | +| `altText` | `getAllBy(altText('hello', { exact: false }))` | [ByAltText](https://testing-library.com/docs/dom-testing-library/api-queries#byalttext) | |
| 153 | +| `title` | `getAllBy(title('hello', { exact: false }))` | [ByTitle](https://testing-library.com/docs/dom-testing-library/api-queries#bytitle) | |
| 154 | +| `displayValue` | `getAllBy(displayValue('hello', { exact: false }))` | [ByDisplayValue](https://testing-library.com/docs/dom-testing-library/api-queries#bydisplayvalue) | |
| 155 | +| `role` | `getAllBy(role('tab', { selected: true }))` | [ByRole](https://testing-library.com/docs/dom-testing-library/api-queries#byrole) | |
| 156 | +| `testId` | `getAllBy(testId('hello', { exact: false }))` | [ByTestId](https://testing-library.com/docs/dom-testing-library/api-queries#bytestid) | |
| 157 | + |
| 158 | +As a convenience another query is available as a shorthand: |
| 159 | + |
| 160 | +| Function | Description | Example | |
| 161 | +|----------|-------------|---------| |
| 162 | +| `textFragment` | Same as `text` with `exact: false` in the options. | `getAllBy(textFragment('hello'))` | |
| 163 | + |
| 164 | +For other features, see the main [React Testing Library documentation](https://testing-library.com/docs/react-testing-library/intro). |
| 165 | + |
| 166 | +## Writing custom queries |
| 167 | + |
| 168 | +```javascript |
| 169 | +const positionInTable = (column, row) => ({ // parameters can be anything you like |
| 170 | + description: `in column ${column}, row ${row}`, |
| 171 | + queryAll: (container) => { |
| 172 | + // your query implementation here: |
| 173 | + const rowElement = container.querySelectorAll('tr')[row]; |
| 174 | + if (!rowElement) { |
| 175 | + return []; |
| 176 | + } |
| 177 | + const cellElement = rowElement.querySelectorAll('td')[column]; |
| 178 | + if (!cellElement) { |
| 179 | + return []; |
| 180 | + } |
| 181 | + return [cellElement]; // always return a list, even if there is only one element |
| 182 | + }, |
| 183 | +}); |
| 184 | +``` |
| 185 | + |
| 186 | +This can now be used easily with any of |
| 187 | +`getBy`, `getAllBy`, `findBy`, `findAllBy`, `queryBy`, `queryAllBy`: |
| 188 | + |
| 189 | +```jsx |
| 190 | +import { screen, render } from 'flexible-testing-library-react'; |
| 191 | +import { positionInTable } from './positionInTable'; |
| 192 | + |
| 193 | +render(<MyComponent />); |
| 194 | +screen.getBy(positionInTable(2, 3)).something(); |
| 195 | +``` |
| 196 | + |
| 197 | +And can be used with `toContainElementWith`: |
| 198 | + |
| 199 | +```jsx |
| 200 | +import { screen, render } from 'flexible-testing-library-react'; |
| 201 | +import 'flexible-testing-library-react/extend-expect'; |
| 202 | +import { positionInTable } from './positionInTable'; |
| 203 | + |
| 204 | +render(<MyComponent />); |
| 205 | +expect(screen).toContainElementWith(positionInTable(2, 3)); |
| 206 | +``` |
| 207 | + |
| 208 | +### Optional query configuration |
| 209 | + |
| 210 | +As well as a `description` and `queryAll`, you can provide some other (optional) |
| 211 | +configuration: |
| 212 | + |
| 213 | +- `multipleErrorDetail`: a string to include in error messages about finding too many |
| 214 | + matching elements. |
| 215 | +- `missingErrorDetail`: a string to include in error messages about not finding any |
| 216 | + element. |
| 217 | +- `getAll`: a version of `queryAll` which throws if no elements are found (can be used |
| 218 | + to provide more detailed error information). Note that you _must_ specify `queryAll`, |
| 219 | + even if you also provide `getAll`. |
| 220 | + |
| 221 | +### TypeScript |
| 222 | + |
| 223 | +The types for custom queries are: |
| 224 | + |
| 225 | +```typescript |
| 226 | +import type Query from 'flexible-testing-library-react'; |
| 227 | + |
| 228 | +const tableCell = (row: number, column: number): Query => ({ |
| 229 | + description: `in column ${column}, row ${row}`, |
| 230 | + queryAll: (container): NodeListOf<HTMLElement> | HTMLElement[] => { /* implementation here */ }, |
| 231 | +}); |
| 232 | +``` |
0 commit comments