@uniflowed/react-testing 0.0.0-alpha.2 → 0.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/internal/dom.js +197 -20
- package/internal/events.js +203 -46
- package/internal/queries.js +86 -27
- package/internal/render.js +138 -12
- package/internal/screen.js +187 -47
- package/package.json +3 -3
package/internal/screen.js
CHANGED
|
@@ -3,11 +3,29 @@
|
|
|
3
3
|
// The six forms of every query, generated once.
|
|
4
4
|
//
|
|
5
5
|
// Writing `getByText`, `queryByText`, `findByText`, `getAllByText`,
|
|
6
|
-
// `queryAllByText` and `findAllByText` by hand, for
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// `queryAllByText` and `findAllByText` by hand, for six queries, is thirty-six
|
|
7
|
+
// functions that differ in two decisions: whether finding nothing is an error,
|
|
8
|
+
// and whether to wait. So the two decisions are written once and the thirty-six
|
|
9
|
+
// are derived — which also means a new query is one entry rather than six
|
|
10
|
+
// functions.
|
|
11
|
+
//
|
|
12
|
+
// # The names are written down, and the behaviour is not
|
|
13
|
+
//
|
|
14
|
+
// `Queries` used to be `{ readonly [string]: (matcher: mixed, options?: mixed)
|
|
15
|
+
// => any }`, which is a way of writing "this object has whatever you ask it
|
|
16
|
+
// for, and it is whatever you like". That is a hole in the published type of a
|
|
17
|
+
// package whose entire purpose is testing *typed* components:
|
|
18
|
+
// `screen.getByRole("button").valeu` was not a mistake anybody's checker would
|
|
19
|
+
// find, `screen.getByTest("save")` was not a misspelling, and
|
|
20
|
+
// `await screen.getByText("Save")` — the missing `find`, which is the single
|
|
21
|
+
// most common mistake this library invites — was fine.
|
|
22
|
+
//
|
|
23
|
+
// So the thirty-six names are written out below. Flow has no template literal
|
|
24
|
+
// types, so `getBy${Name}` is not something a type can compute; the names have
|
|
25
|
+
// to be listed for the type to exist at all. What is *not* repeated is any
|
|
26
|
+
// behaviour: `forms` is still the one place the six decisions are made, and the
|
|
27
|
+
// listing below is a naming, six lines per query, which is the part a reader
|
|
28
|
+
// wants to be able to check against the runtime by eye.
|
|
11
29
|
|
|
12
30
|
import {
|
|
13
31
|
allByDisplayValue,
|
|
@@ -18,24 +36,92 @@ import {
|
|
|
18
36
|
allByText,
|
|
19
37
|
queryFailure,
|
|
20
38
|
} from "./queries.js";
|
|
21
|
-
import type { Matcher, MatcherOptions } from "./queries.js";
|
|
22
|
-
import {
|
|
39
|
+
import type { Matcher, MatcherOptions, RoleOptions } from "./queries.js";
|
|
40
|
+
import { bodyOf } from "./dom.js";
|
|
23
41
|
import { waitFor } from "./render.js";
|
|
24
42
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* One query's six forms, over whatever that query matches on.
|
|
45
|
+
*
|
|
46
|
+
* Generic in the target because `ByRole` does not take a `Matcher` — it takes
|
|
47
|
+
* a role, which is a string and only a string, and a regular expression over
|
|
48
|
+
* role names is a query that would silently match nothing. Generic in the
|
|
49
|
+
* options because `ByRole` is also the only query with more than `exact` to
|
|
50
|
+
* say.
|
|
51
|
+
*/
|
|
52
|
+
type Forms<TTarget, TOptions> = {|
|
|
53
|
+
readonly get: (target: TTarget, options?: TOptions) => Element,
|
|
54
|
+
readonly getAll: (target: TTarget, options?: TOptions) => Array<Element>,
|
|
55
|
+
readonly query: (target: TTarget, options?: TOptions) => Element | null,
|
|
56
|
+
readonly queryAll: (target: TTarget, options?: TOptions) => Array<Element>,
|
|
57
|
+
readonly find: (target: TTarget, options?: TOptions) => Promise<Element>,
|
|
58
|
+
readonly findAll: (target: TTarget, options?: TOptions) => Promise<Array<Element>>,
|
|
59
|
+
|};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The queries available on `screen` and on `within(element)`.
|
|
63
|
+
*
|
|
64
|
+
* Read down one column and the four questions of the module comment are the
|
|
65
|
+
* four return types: `getBy…` is an `Element` because it throws rather than
|
|
66
|
+
* hand back nothing, `queryBy…` is `Element | null` because its whole purpose
|
|
67
|
+
* is asking about absence, and the `findBy…` pair are promises because they
|
|
68
|
+
* wait.
|
|
69
|
+
*/
|
|
70
|
+
export type Queries = {|
|
|
71
|
+
readonly getByText: (matcher: Matcher, options?: MatcherOptions) => Element,
|
|
72
|
+
readonly getAllByText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
73
|
+
readonly queryByText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
|
|
74
|
+
readonly queryAllByText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
75
|
+
readonly findByText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
|
|
76
|
+
readonly findAllByText: (matcher: Matcher, options?: MatcherOptions) => Promise<Array<Element>>,
|
|
77
|
+
|
|
78
|
+
readonly getByRole: (role: string, options?: RoleOptions) => Element,
|
|
79
|
+
readonly getAllByRole: (role: string, options?: RoleOptions) => Array<Element>,
|
|
80
|
+
readonly queryByRole: (role: string, options?: RoleOptions) => Element | null,
|
|
81
|
+
readonly queryAllByRole: (role: string, options?: RoleOptions) => Array<Element>,
|
|
82
|
+
readonly findByRole: (role: string, options?: RoleOptions) => Promise<Element>,
|
|
83
|
+
readonly findAllByRole: (role: string, options?: RoleOptions) => Promise<Array<Element>>,
|
|
84
|
+
|
|
85
|
+
readonly getByLabelText: (matcher: Matcher, options?: MatcherOptions) => Element,
|
|
86
|
+
readonly getAllByLabelText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
87
|
+
readonly queryByLabelText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
|
|
88
|
+
readonly queryAllByLabelText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
89
|
+
readonly findByLabelText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
|
|
90
|
+
readonly findAllByLabelText: (
|
|
91
|
+
matcher: Matcher,
|
|
92
|
+
options?: MatcherOptions,
|
|
93
|
+
) => Promise<Array<Element>>,
|
|
94
|
+
|
|
95
|
+
readonly getByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Element,
|
|
96
|
+
readonly getAllByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
97
|
+
readonly queryByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
|
|
98
|
+
readonly queryAllByPlaceholderText: (
|
|
99
|
+
matcher: Matcher,
|
|
100
|
+
options?: MatcherOptions,
|
|
101
|
+
) => Array<Element>,
|
|
102
|
+
readonly findByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
|
|
103
|
+
readonly findAllByPlaceholderText: (
|
|
104
|
+
matcher: Matcher,
|
|
105
|
+
options?: MatcherOptions,
|
|
106
|
+
) => Promise<Array<Element>>,
|
|
107
|
+
|
|
108
|
+
readonly getByTestId: (matcher: Matcher, options?: MatcherOptions) => Element,
|
|
109
|
+
readonly getAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
110
|
+
readonly queryByTestId: (matcher: Matcher, options?: MatcherOptions) => Element | null,
|
|
111
|
+
readonly queryAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
112
|
+
readonly findByTestId: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
|
|
113
|
+
readonly findAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Promise<Array<Element>>,
|
|
114
|
+
|
|
115
|
+
readonly getByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Element,
|
|
116
|
+
readonly getAllByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
117
|
+
readonly queryByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Element | null,
|
|
118
|
+
readonly queryAllByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
|
|
119
|
+
readonly findByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
|
|
120
|
+
readonly findAllByDisplayValue: (
|
|
121
|
+
matcher: Matcher,
|
|
122
|
+
options?: MatcherOptions,
|
|
123
|
+
) => Promise<Array<Element>>,
|
|
124
|
+
|};
|
|
39
125
|
|
|
40
126
|
/**
|
|
41
127
|
* The six forms of one finder, bound to a root.
|
|
@@ -43,58 +129,112 @@ const FINDERS = {
|
|
|
43
129
|
* `getBy` fails when there is not exactly one, and says how many it saw and
|
|
44
130
|
* what the markup looked like, because "found 3 elements" and "found nothing"
|
|
45
131
|
* are different bugs and a test that reports neither wastes the reader's time.
|
|
132
|
+
*
|
|
133
|
+
* `TTarget` is bounded by `Matcher` rather than left free because
|
|
134
|
+
* `queryFailure` has to describe what was asked for, and it describes the
|
|
135
|
+
* three things a matcher can be. A role is a string, so the bound holds and
|
|
136
|
+
* the failure message is the same one it always was.
|
|
46
137
|
*/
|
|
47
|
-
function forms
|
|
48
|
-
|
|
138
|
+
function forms<TTarget extends Matcher, TOptions>(
|
|
139
|
+
name: string,
|
|
140
|
+
find: (root: Element, target: TTarget, options?: TOptions) => Array<Element>,
|
|
141
|
+
root: () => Element,
|
|
142
|
+
): Forms<TTarget, TOptions> {
|
|
143
|
+
const all = (target: TTarget, options?: TOptions) => find(root(), target, options);
|
|
49
144
|
|
|
50
145
|
return {
|
|
51
|
-
|
|
52
|
-
const found = all(
|
|
146
|
+
getAll: (target, options) => {
|
|
147
|
+
const found = all(target, options);
|
|
53
148
|
if (found.length === 0) {
|
|
54
|
-
throw queryFailure(`getAllBy${name}`,
|
|
149
|
+
throw queryFailure(`getAllBy${name}`, target, root(), 0);
|
|
55
150
|
}
|
|
56
151
|
return found;
|
|
57
152
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const found = all(
|
|
153
|
+
queryAll: all,
|
|
154
|
+
get: (target, options) => {
|
|
155
|
+
const found = all(target, options);
|
|
61
156
|
if (found.length !== 1) {
|
|
62
|
-
throw queryFailure(`getBy${name}`,
|
|
157
|
+
throw queryFailure(`getBy${name}`, target, root(), found.length);
|
|
63
158
|
}
|
|
64
159
|
return found[0];
|
|
65
160
|
},
|
|
66
|
-
|
|
67
|
-
const found = all(
|
|
161
|
+
query: (target, options) => {
|
|
162
|
+
const found = all(target, options);
|
|
68
163
|
if (found.length > 1) {
|
|
69
|
-
throw queryFailure(`queryBy${name}`,
|
|
164
|
+
throw queryFailure(`queryBy${name}`, target, root(), found.length);
|
|
70
165
|
}
|
|
71
166
|
return found[0] ?? null;
|
|
72
167
|
},
|
|
73
|
-
|
|
168
|
+
find: (target, options) =>
|
|
74
169
|
waitFor(() => {
|
|
75
|
-
const found = all(
|
|
170
|
+
const found = all(target, options);
|
|
76
171
|
if (found.length !== 1) {
|
|
77
|
-
throw queryFailure(`findBy${name}`,
|
|
172
|
+
throw queryFailure(`findBy${name}`, target, root(), found.length);
|
|
78
173
|
}
|
|
79
174
|
return found[0];
|
|
80
175
|
}),
|
|
81
|
-
|
|
176
|
+
findAll: (target, options) =>
|
|
82
177
|
waitFor(() => {
|
|
83
|
-
const found = all(
|
|
178
|
+
const found = all(target, options);
|
|
84
179
|
if (found.length === 0) {
|
|
85
|
-
throw queryFailure(`findAllBy${name}`,
|
|
180
|
+
throw queryFailure(`findAllBy${name}`, target, root(), 0);
|
|
86
181
|
}
|
|
87
182
|
return found;
|
|
88
183
|
}),
|
|
89
184
|
};
|
|
90
185
|
}
|
|
91
186
|
|
|
92
|
-
function queriesFor(root: () =>
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
187
|
+
function queriesFor(root: () => Element): Queries {
|
|
188
|
+
const text = forms("Text", allByText, root);
|
|
189
|
+
const role = forms("Role", allByRole, root);
|
|
190
|
+
const labelText = forms("LabelText", allByLabelText, root);
|
|
191
|
+
const placeholderText = forms("PlaceholderText", allByPlaceholderText, root);
|
|
192
|
+
const testId = forms("TestId", allByTestId, root);
|
|
193
|
+
const displayValue = forms("DisplayValue", allByDisplayValue, root);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
getByText: text.get,
|
|
197
|
+
getAllByText: text.getAll,
|
|
198
|
+
queryByText: text.query,
|
|
199
|
+
queryAllByText: text.queryAll,
|
|
200
|
+
findByText: text.find,
|
|
201
|
+
findAllByText: text.findAll,
|
|
202
|
+
|
|
203
|
+
getByRole: role.get,
|
|
204
|
+
getAllByRole: role.getAll,
|
|
205
|
+
queryByRole: role.query,
|
|
206
|
+
queryAllByRole: role.queryAll,
|
|
207
|
+
findByRole: role.find,
|
|
208
|
+
findAllByRole: role.findAll,
|
|
209
|
+
|
|
210
|
+
getByLabelText: labelText.get,
|
|
211
|
+
getAllByLabelText: labelText.getAll,
|
|
212
|
+
queryByLabelText: labelText.query,
|
|
213
|
+
queryAllByLabelText: labelText.queryAll,
|
|
214
|
+
findByLabelText: labelText.find,
|
|
215
|
+
findAllByLabelText: labelText.findAll,
|
|
216
|
+
|
|
217
|
+
getByPlaceholderText: placeholderText.get,
|
|
218
|
+
getAllByPlaceholderText: placeholderText.getAll,
|
|
219
|
+
queryByPlaceholderText: placeholderText.query,
|
|
220
|
+
queryAllByPlaceholderText: placeholderText.queryAll,
|
|
221
|
+
findByPlaceholderText: placeholderText.find,
|
|
222
|
+
findAllByPlaceholderText: placeholderText.findAll,
|
|
223
|
+
|
|
224
|
+
getByTestId: testId.get,
|
|
225
|
+
getAllByTestId: testId.getAll,
|
|
226
|
+
queryByTestId: testId.query,
|
|
227
|
+
queryAllByTestId: testId.queryAll,
|
|
228
|
+
findByTestId: testId.find,
|
|
229
|
+
findAllByTestId: testId.findAll,
|
|
230
|
+
|
|
231
|
+
getByDisplayValue: displayValue.get,
|
|
232
|
+
getAllByDisplayValue: displayValue.getAll,
|
|
233
|
+
queryByDisplayValue: displayValue.query,
|
|
234
|
+
queryAllByDisplayValue: displayValue.queryAll,
|
|
235
|
+
findByDisplayValue: displayValue.find,
|
|
236
|
+
findAllByDisplayValue: displayValue.findAll,
|
|
237
|
+
};
|
|
98
238
|
}
|
|
99
239
|
|
|
100
240
|
/**
|
|
@@ -105,9 +245,9 @@ function queriesFor(root: () => ParentNode): Queries {
|
|
|
105
245
|
* not see them would be unable to assert on the components most likely to have
|
|
106
246
|
* a bug.
|
|
107
247
|
*/
|
|
108
|
-
export const screen: Queries = queriesFor(() =>
|
|
248
|
+
export const screen: Queries = queriesFor(() => bodyOf());
|
|
109
249
|
|
|
110
250
|
/** The same queries, restricted to one element's subtree. */
|
|
111
|
-
export function within(element:
|
|
251
|
+
export function within(element: Element): Queries {
|
|
112
252
|
return queriesFor(() => element);
|
|
113
253
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/react-testing",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.5",
|
|
4
4
|
"description": "React Testing Library over a real DOM, part of the Unified Toolchain for Flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"internal"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@uniflowed/core": "0.0.0-alpha.
|
|
22
|
-
"@uniflowed/react": "0.0.0-alpha.
|
|
21
|
+
"@uniflowed/core": "0.0.0-alpha.5",
|
|
22
|
+
"@uniflowed/react": "0.0.0-alpha.5",
|
|
23
23
|
"happy-dom": "^20.13.2"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|