@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.
@@ -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 seven queries, is
7
- // forty-two functions that differ in two decisions: whether finding nothing is
8
- // an error, and whether to wait. So the two decisions are written once and the
9
- // forty-two are derived — which also means a new query is one entry rather
10
- // than six functions.
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 { documentOf } from "./dom.js";
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
- /** The queries available on `screen` and on `within(element)`. */
26
- export type Queries = {
27
- readonly [string]: (matcher: mixed, options?: mixed) => any,
28
- };
29
-
30
- /** Every query, as the one function each needs. */
31
- const FINDERS = {
32
- Text: allByText,
33
- Role: allByRole,
34
- LabelText: allByLabelText,
35
- PlaceholderText: allByPlaceholderText,
36
- TestId: allByTestId,
37
- DisplayValue: allByDisplayValue,
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(name: string, find: Function, root: () => ParentNode): { [string]: Function } {
48
- const all = (matcher: Matcher, options?: MatcherOptions) => find(root(), matcher, options);
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
- [`getAllBy${name}`]: (matcher: Matcher, options?: MatcherOptions) => {
52
- const found = all(matcher, options);
146
+ getAll: (target, options) => {
147
+ const found = all(target, options);
53
148
  if (found.length === 0) {
54
- throw queryFailure(`getAllBy${name}`, matcher, root(), 0);
149
+ throw queryFailure(`getAllBy${name}`, target, root(), 0);
55
150
  }
56
151
  return found;
57
152
  },
58
- [`queryAllBy${name}`]: all,
59
- [`getBy${name}`]: (matcher: Matcher, options?: MatcherOptions) => {
60
- const found = all(matcher, options);
153
+ queryAll: all,
154
+ get: (target, options) => {
155
+ const found = all(target, options);
61
156
  if (found.length !== 1) {
62
- throw queryFailure(`getBy${name}`, matcher, root(), found.length);
157
+ throw queryFailure(`getBy${name}`, target, root(), found.length);
63
158
  }
64
159
  return found[0];
65
160
  },
66
- [`queryBy${name}`]: (matcher: Matcher, options?: MatcherOptions) => {
67
- const found = all(matcher, options);
161
+ query: (target, options) => {
162
+ const found = all(target, options);
68
163
  if (found.length > 1) {
69
- throw queryFailure(`queryBy${name}`, matcher, root(), found.length);
164
+ throw queryFailure(`queryBy${name}`, target, root(), found.length);
70
165
  }
71
166
  return found[0] ?? null;
72
167
  },
73
- [`findBy${name}`]: (matcher: Matcher, options?: MatcherOptions) =>
168
+ find: (target, options) =>
74
169
  waitFor(() => {
75
- const found = all(matcher, options);
170
+ const found = all(target, options);
76
171
  if (found.length !== 1) {
77
- throw queryFailure(`findBy${name}`, matcher, root(), found.length);
172
+ throw queryFailure(`findBy${name}`, target, root(), found.length);
78
173
  }
79
174
  return found[0];
80
175
  }),
81
- [`findAllBy${name}`]: (matcher: Matcher, options?: MatcherOptions) =>
176
+ findAll: (target, options) =>
82
177
  waitFor(() => {
83
- const found = all(matcher, options);
178
+ const found = all(target, options);
84
179
  if (found.length === 0) {
85
- throw queryFailure(`findAllBy${name}`, matcher, root(), 0);
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: () => ParentNode): Queries {
93
- const queries = {};
94
- for (const name of Object.keys(FINDERS)) {
95
- Object.assign(queries, forms(name, (FINDERS as any)[name], root));
96
- }
97
- return queries as any;
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(() => documentOf().body);
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: ParentNode): Queries {
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.2",
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.2",
22
- "@uniflowed/react": "0.0.0-alpha.2",
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": {