@uniflowed/react-testing 0.0.0-alpha.10

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.
@@ -0,0 +1,298 @@
1
+ // @flow
2
+ //
3
+ // Mounting a component into a real document.
4
+ //
5
+ // Everything that changes the tree goes through React's `act`, which is what
6
+ // makes a test able to assert immediately afterwards: `act` runs the render,
7
+ // the effects, and the microtasks React queued, then returns. Without it a
8
+ // test asserts against the DOM as it was before React got to it, and the fix
9
+ // people reach for is a sleep.
10
+
11
+ import { createRequire } from "node:module";
12
+
13
+ // A type-only import: `React.Node` is the only thing this module needs from
14
+ // React itself, and importing the namespace as a value left an unused binding
15
+ // in the bundle.
16
+ import type * as React from "@uniflowed/react";
17
+ import { act } from "@uniflowed/react";
18
+
19
+ import { bodyOf, installActEnvironment, installDom, setActEnvironment } from "./dom.js";
20
+
21
+ /** What `render` hands back. */
22
+ export type RenderResult = {|
23
+ /** The element the tree was mounted into. */
24
+ readonly container: Element,
25
+ /** The document body, which is where a portal ends up. */
26
+ readonly baseElement: Element,
27
+ /** Render different elements into the same container. */
28
+ readonly rerender: (ui: React.Node) => void,
29
+ /** Take the tree down and remove the container. */
30
+ readonly unmount: () => void,
31
+ /** The container's markup, for a failure message. */
32
+ readonly asFragment: () => string,
33
+ |};
34
+
35
+ type Mounted = {|
36
+ container: Element,
37
+ root: ReactRoot,
38
+ |};
39
+
40
+ const mounted: Array<Mounted> = [];
41
+
42
+ /**
43
+ * Render `ui` into a fresh container in the document body.
44
+ *
45
+ * Anything still mounted from an earlier `render` is taken down first. `screen`
46
+ * queries the whole document, so a tree left over from the previous test would
47
+ * make "there is exactly one Save button" false for reasons that have nothing
48
+ * to do with the test being read.
49
+ */
50
+ export function render(ui: React.Node, options?: {| readonly container?: Element |}): RenderResult {
51
+ installDom();
52
+ cleanup();
53
+
54
+ const { createRoot } = requireClient();
55
+ const container = options?.container ?? createContainer();
56
+ const root = createRoot(container);
57
+ mounted.push({ container, root });
58
+
59
+ act(() => {
60
+ root.render(ui);
61
+ });
62
+
63
+ return {
64
+ container,
65
+ baseElement: bodyOf(),
66
+ rerender: (next: React.Node) => {
67
+ act(() => {
68
+ root.render(next);
69
+ });
70
+ },
71
+ unmount: () => {
72
+ act(() => {
73
+ root.unmount();
74
+ });
75
+ container.remove();
76
+ const index = mounted.findIndex((entry) => entry.container === container);
77
+ if (index >= 0) {
78
+ mounted.splice(index, 1);
79
+ }
80
+ },
81
+ asFragment: () => container.innerHTML,
82
+ };
83
+ }
84
+
85
+ /** Unmount everything this module has mounted. */
86
+ export function cleanup(): void {
87
+ while (mounted.length > 0) {
88
+ const entry = mounted.pop();
89
+ if (entry == null) {
90
+ break;
91
+ }
92
+ act(() => {
93
+ entry.root.unmount();
94
+ });
95
+ entry.container.remove();
96
+ }
97
+ }
98
+
99
+ function createContainer(): Element {
100
+ const container = globalThis.document.createElement("div");
101
+ bodyOf().appendChild(container);
102
+ return container;
103
+ }
104
+
105
+ /**
106
+ * `react-dom/client`, loaded only once a test renders.
107
+ *
108
+ * It reads `document` while it is being evaluated, so it cannot be a static
109
+ * import of this module: ESM evaluates every import before the module body,
110
+ * and the DOM has to exist first. `render` is synchronous — a test asserts on
111
+ * the line after it — so this cannot be `await import(…)` either.
112
+ *
113
+ * That leaves a synchronous require, through `node:module`. All three hosts
114
+ * uf supports provide it, and React ships a CommonJS build for exactly this
115
+ * kind of caller.
116
+ */
117
+ let client: ReactDomClient | null = null;
118
+ function requireClient(): ReactDomClient {
119
+ if (client == null) {
120
+ const load = createRequire(import.meta.url);
121
+ client = load("react-dom/client");
122
+ }
123
+ return client;
124
+ }
125
+
126
+ /**
127
+ * As much of `react-dom/client` as this module uses.
128
+ *
129
+ * The annotation is the trust boundary and it is deliberately one line wide: a
130
+ * synchronous `require` of a CommonJS build returns `any` whatever anyone
131
+ * writes, so the choice is not between `any` and certainty, it is between
132
+ * saying what is expected of the module and saying nothing. `createRoot` and
133
+ * the two methods below are the whole of what is expected, and a React that
134
+ * stopped providing them would fail here rather than at
135
+ * `root.render is not a function` inside an unrelated test.
136
+ */
137
+ type ReactDomClient = {|
138
+ readonly createRoot: (container: Element) => ReactRoot,
139
+ |};
140
+
141
+ /** A React root, as much of one as this module touches. */
142
+ type ReactRoot = {| render(node: React.Node): void, unmount(): void |};
143
+
144
+ /**
145
+ * Run `body`, letting React flush everything it queues.
146
+ *
147
+ * Exported because a test that changes state outside an event — a timer
148
+ * firing, a promise settling — has to tell React that the change happened,
149
+ * and this is how.
150
+ */
151
+ export function actively<T>(body: () => T): T {
152
+ // A test that acts without having rendered — a timer firing in a hook test
153
+ // — reaches `act` without going through `render`, and `act` still has to
154
+ // know it is being called by a test.
155
+ installActEnvironment();
156
+
157
+ // A box holding the body's result, rather than a `let result: T`.
158
+ //
159
+ // The result is produced inside a callback, and Flow cannot see that `act`
160
+ // called it: an annotated `let` written only there is
161
+ // `possibly uninitialized variable` on the way out, and a `T | void` would
162
+ // be wrong for the caller who wrote `act(() => {})` and whose `T` *is*
163
+ // `void`. A box distinguishes "not produced" from "produced `undefined`",
164
+ // and the check below states, at runtime, the invariant the checker cannot
165
+ // prove: `act` calls its scope, synchronously, always.
166
+ const produced: { current: {| value: T |} | null } = { current: null };
167
+ const scope: mixed = act(() => {
168
+ produced.current = { value: body() };
169
+ // Handed back so React keeps the scope open until an async body settles.
170
+ // Without this the scope closed on the first tick and every update the
171
+ // body was still waiting for landed outside it, which React reports as
172
+ // "an update was not wrapped in act(...)".
173
+ return produced.current.value;
174
+ });
175
+
176
+ const held = produced.current;
177
+ if (held == null) {
178
+ throw new Error("act(...) did not run its scope, so there is no result to return");
179
+ }
180
+ const result = held.value;
181
+
182
+ if (isThenable(result) && isThenable(scope)) {
183
+ // `Promise.resolve`, not `scope.then(…)`: `act` hands back a bare thenable
184
+ // — an object with a `then` and nothing else — whose `then` returns
185
+ // `undefined` rather than a promise. Chaining off it directly produced an
186
+ // `undefined` that `await` resolved immediately, so the caller carried on
187
+ // while the scope was still open: the body's timers had not fired, and
188
+ // every later `act` nested inside the scope that was never closed and
189
+ // flushed nothing. `render` after one of those returned an empty
190
+ // container.
191
+ //
192
+ // # The one cast in this file, and why it is still here
193
+ //
194
+ // On this branch `T` *is* a promise — `isThenable(result)` is the runtime
195
+ // proof — so a promise that resolves to what `result` resolves to, once
196
+ // the scope has closed, is a `T`, and the signature above is true. Flow
197
+ // cannot follow the last step. Refining `result` says something about the
198
+ // value; the return type is about `T`, and there is no way to write "T is
199
+ // a promise here" in Flow:
200
+ //
201
+ // * a type guard (`value is Promise<mixed>`) refines the value and
202
+ // leaves `T` alone, so the helper it enables returns `Promise<mixed>`
203
+ // and `Promise<unknown> is incompatible with T` in its place;
204
+ // * a conditional return type (`T extends Promise<infer U> ? Promise<U>
205
+ // : T`) — which this checker does support — is unevaluated while `T`
206
+ // is generic, so the body cannot be checked against it either;
207
+ // * overloading, which is how Flow's own `react` library definition
208
+ // describes `act`, is available to a library definition and not to an
209
+ // implementation.
210
+ //
211
+ // The remaining honest answers all change behaviour: returning `Promise<T>`
212
+ // for every call would hand `act(() => {})` a floating promise, and
213
+ // returning the scope itself would depend on React's thenable passing the
214
+ // callback's value through, which is the assumption the comment above
215
+ // records going wrong. So the cast stays, suppressed by name rather than
216
+ // renamed to `$FlowFixMe`: the directive says which rule it is escaping and
217
+ // this comment says why, which a rename says nothing about.
218
+ // uf-lint-disable-next-line flow/unclear-type
219
+ return Promise.resolve(scope).then(() => result) as any;
220
+ }
221
+ return result;
222
+ }
223
+
224
+ /** Whether `value` is something to await. */
225
+ function isThenable(value: mixed): boolean {
226
+ if (value == null || typeof value !== "object") {
227
+ return false;
228
+ }
229
+ const object: { readonly [string]: mixed } = value;
230
+ return typeof object.then === "function";
231
+ }
232
+
233
+ /**
234
+ * Wait until `body` stops throwing, or give up.
235
+ *
236
+ * Polling rather than observing mutations, because what a test waits for is
237
+ * usually not a DOM change at all — it is a promise resolving, a fetch
238
+ * settling, a timer firing — and a mutation observer sees none of those.
239
+ */
240
+ export async function waitFor<T>(
241
+ body: () => T | Promise<T>,
242
+ options?: {| readonly timeout?: number, readonly interval?: number |},
243
+ ): Promise<T> {
244
+ installActEnvironment();
245
+ const timeout = options?.timeout ?? 1000;
246
+ const interval = options?.interval ?? 20;
247
+
248
+ // React is told this is not an act environment for as long as the wait
249
+ // lasts, and told again afterwards.
250
+ //
251
+ // The update a test waits for arrives between two polls, and React reports
252
+ // it as "an update to X inside a test was not wrapped in act(...)" —
253
+ // correctly, since nothing was there to flush it. The fix cannot be to put
254
+ // the polling loop inside an `act` scope: `act` holds updates back until
255
+ // the scope closes, so the loop would poll a tree that cannot change and
256
+ // every `waitFor` would run to its timeout.
257
+ //
258
+ // So the scope is stood down instead. The warning exists to catch an update
259
+ // a test did not know it was causing; a test that wrote `waitFor` knows.
260
+ // Counted rather than saved and restored, because waits nest: every
261
+ // `findBy…` is a `waitFor`, and a test may put one inside another. The
262
+ // outermost wait stands the environment down and the outermost restores it.
263
+ waits += 1;
264
+ if (waits === 1) {
265
+ setActEnvironment(false);
266
+ }
267
+ try {
268
+ return await poll(body, timeout, interval);
269
+ } finally {
270
+ waits -= 1;
271
+ if (waits === 0) {
272
+ setActEnvironment(true);
273
+ }
274
+ }
275
+ }
276
+
277
+ /** How many waits are in progress. */
278
+ let waits = 0;
279
+
280
+ /** Call `body` until it stops throwing, or give up after `timeout`. */
281
+ async function poll<T>(body: () => T | Promise<T>, timeout: number, interval: number): Promise<T> {
282
+ const deadline = Date.now() + timeout;
283
+ let lastError: mixed = null;
284
+
285
+ while (true) {
286
+ try {
287
+ return await body();
288
+ } catch (error) {
289
+ lastError = error;
290
+ }
291
+ if (Date.now() >= deadline) {
292
+ throw lastError instanceof Error
293
+ ? lastError
294
+ : new Error(`waitFor timed out after ${timeout}ms: ${String(lastError)}`);
295
+ }
296
+ await new Promise((resolve) => setTimeout(resolve, interval));
297
+ }
298
+ }
@@ -0,0 +1,294 @@
1
+ // @flow
2
+ //
3
+ // The six forms of every query, generated once.
4
+ //
5
+ // Writing `getByText`, `queryByText`, `findByText`, `getAllByText`,
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.
29
+
30
+ import {
31
+ MATCHER_OPTION_KEYS,
32
+ ROLE_OPTION_KEYS,
33
+ allByDisplayValue,
34
+ allByLabelText,
35
+ allByPlaceholderText,
36
+ allByRole,
37
+ allByTestId,
38
+ allByText,
39
+ atCallSite,
40
+ queryFailure,
41
+ rejectUnknownOptions,
42
+ } from "./queries.js";
43
+ import type { Matcher, MatcherOptions, RoleOptions } from "./queries.js";
44
+ import { bodyOf } from "./dom.js";
45
+ import { waitFor } from "./render.js";
46
+
47
+ /**
48
+ * One query's six forms, over whatever that query matches on.
49
+ *
50
+ * Generic in the target because `ByRole` does not take a `Matcher` — it takes
51
+ * a role, which is a string and only a string, and a regular expression over
52
+ * role names is a query that would silently match nothing. Generic in the
53
+ * options because `ByRole` is also the only query with more than `exact` to
54
+ * say.
55
+ */
56
+ type Forms<TTarget, TOptions> = {|
57
+ readonly get: (target: TTarget, options?: TOptions) => Element,
58
+ readonly getAll: (target: TTarget, options?: TOptions) => Array<Element>,
59
+ readonly query: (target: TTarget, options?: TOptions) => Element | null,
60
+ readonly queryAll: (target: TTarget, options?: TOptions) => Array<Element>,
61
+ readonly find: (target: TTarget, options?: TOptions) => Promise<Element>,
62
+ readonly findAll: (target: TTarget, options?: TOptions) => Promise<Array<Element>>,
63
+ |};
64
+
65
+ /**
66
+ * The queries available on `screen` and on `within(element)`.
67
+ *
68
+ * Read down one column and the four questions of the module comment are the
69
+ * four return types: `getBy…` is an `Element` because it throws rather than
70
+ * hand back nothing, `queryBy…` is `Element | null` because its whole purpose
71
+ * is asking about absence, and the `findBy…` pair are promises because they
72
+ * wait.
73
+ */
74
+ export type Queries = {|
75
+ readonly getByText: (matcher: Matcher, options?: MatcherOptions) => Element,
76
+ readonly getAllByText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
77
+ readonly queryByText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
78
+ readonly queryAllByText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
79
+ readonly findByText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
80
+ readonly findAllByText: (matcher: Matcher, options?: MatcherOptions) => Promise<Array<Element>>,
81
+
82
+ readonly getByRole: (role: string, options?: RoleOptions) => Element,
83
+ readonly getAllByRole: (role: string, options?: RoleOptions) => Array<Element>,
84
+ readonly queryByRole: (role: string, options?: RoleOptions) => Element | null,
85
+ readonly queryAllByRole: (role: string, options?: RoleOptions) => Array<Element>,
86
+ readonly findByRole: (role: string, options?: RoleOptions) => Promise<Element>,
87
+ readonly findAllByRole: (role: string, options?: RoleOptions) => Promise<Array<Element>>,
88
+
89
+ readonly getByLabelText: (matcher: Matcher, options?: MatcherOptions) => Element,
90
+ readonly getAllByLabelText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
91
+ readonly queryByLabelText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
92
+ readonly queryAllByLabelText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
93
+ readonly findByLabelText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
94
+ readonly findAllByLabelText: (
95
+ matcher: Matcher,
96
+ options?: MatcherOptions,
97
+ ) => Promise<Array<Element>>,
98
+
99
+ readonly getByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Element,
100
+ readonly getAllByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
101
+ readonly queryByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Element | null,
102
+ readonly queryAllByPlaceholderText: (
103
+ matcher: Matcher,
104
+ options?: MatcherOptions,
105
+ ) => Array<Element>,
106
+ readonly findByPlaceholderText: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
107
+ readonly findAllByPlaceholderText: (
108
+ matcher: Matcher,
109
+ options?: MatcherOptions,
110
+ ) => Promise<Array<Element>>,
111
+
112
+ readonly getByTestId: (matcher: Matcher, options?: MatcherOptions) => Element,
113
+ readonly getAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
114
+ readonly queryByTestId: (matcher: Matcher, options?: MatcherOptions) => Element | null,
115
+ readonly queryAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
116
+ readonly findByTestId: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
117
+ readonly findAllByTestId: (matcher: Matcher, options?: MatcherOptions) => Promise<Array<Element>>,
118
+
119
+ readonly getByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Element,
120
+ readonly getAllByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
121
+ readonly queryByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Element | null,
122
+ readonly queryAllByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Array<Element>,
123
+ readonly findByDisplayValue: (matcher: Matcher, options?: MatcherOptions) => Promise<Element>,
124
+ readonly findAllByDisplayValue: (
125
+ matcher: Matcher,
126
+ options?: MatcherOptions,
127
+ ) => Promise<Array<Element>>,
128
+ |};
129
+
130
+ /**
131
+ * The six forms of one finder, bound to a root.
132
+ *
133
+ * `getBy` fails when there is not exactly one, and says how many it saw and
134
+ * what the markup looked like, because "found 3 elements" and "found nothing"
135
+ * are different bugs and a test that reports neither wastes the reader's time.
136
+ *
137
+ * `TTarget` is bounded by `Matcher` rather than left free because
138
+ * `queryFailure` has to describe what was asked for, and it describes the
139
+ * three things a matcher can be. A role is a string, so the bound holds and
140
+ * the failure message is the same one it always was.
141
+ *
142
+ * `known` is the keys the query's options may have, and each of the six checks
143
+ * before it looks at anything. Six lines rather than one inside `all`, because
144
+ * the message has to name the function the reader typed — `getByRole`, not
145
+ * "a role query" — and because the two waiting forms have to raise now rather
146
+ * than a second from now: an option a query does not take is a mistake in the
147
+ * test, not a condition that is about to come true.
148
+ */
149
+ function forms<TTarget extends Matcher, TOptions>(
150
+ name: string,
151
+ find: (root: Element, target: TTarget, options?: TOptions) => Array<Element>,
152
+ root: () => Element,
153
+ known: $ReadOnlyArray<string>,
154
+ ): Forms<TTarget, TOptions> {
155
+ const all = (target: TTarget, options?: TOptions) => find(root(), target, options);
156
+ const check = (form: string, options?: TOptions) => {
157
+ rejectUnknownOptions(`${form}By${name}`, options, known);
158
+ };
159
+
160
+ return {
161
+ getAll: (target, options) => {
162
+ check("getAll", options);
163
+ const found = all(target, options);
164
+ if (found.length === 0) {
165
+ throw queryFailure(`getAllBy${name}`, target, root(), 0);
166
+ }
167
+ return found;
168
+ },
169
+ queryAll: (target, options) => {
170
+ check("queryAll", options);
171
+ return all(target, options);
172
+ },
173
+ get: (target, options) => {
174
+ check("get", options);
175
+ const found = all(target, options);
176
+ if (found.length !== 1) {
177
+ throw queryFailure(`getBy${name}`, target, root(), found.length);
178
+ }
179
+ return found[0];
180
+ },
181
+ query: (target, options) => {
182
+ check("query", options);
183
+ const found = all(target, options);
184
+ if (found.length > 1) {
185
+ throw queryFailure(`queryBy${name}`, target, root(), found.length);
186
+ }
187
+ return found[0] ?? null;
188
+ },
189
+ // The two waiting forms build an error at the call and hand it to the
190
+ // failure on the way out. A wait keeps the *last* attempt's failure, and
191
+ // the last attempt runs from a timer: by then the stack under it is the
192
+ // poll loop and nothing else, so the failure has no line of the test left
193
+ // in it to report. The synchronous four need none of this — they throw
194
+ // while the caller is still on the stack.
195
+ find: async (target, options) => {
196
+ check("find", options);
197
+ const asked = new Error("asked here");
198
+ try {
199
+ return await waitFor(() => {
200
+ const found = all(target, options);
201
+ if (found.length !== 1) {
202
+ throw queryFailure(`findBy${name}`, target, root(), found.length);
203
+ }
204
+ return found[0];
205
+ });
206
+ } catch (error) {
207
+ throw atCallSite(error, asked);
208
+ }
209
+ },
210
+ findAll: async (target, options) => {
211
+ check("findAll", options);
212
+ const asked = new Error("asked here");
213
+ try {
214
+ return await waitFor(() => {
215
+ const found = all(target, options);
216
+ if (found.length === 0) {
217
+ throw queryFailure(`findAllBy${name}`, target, root(), 0);
218
+ }
219
+ return found;
220
+ });
221
+ } catch (error) {
222
+ throw atCallSite(error, asked);
223
+ }
224
+ },
225
+ };
226
+ }
227
+
228
+ function queriesFor(root: () => Element): Queries {
229
+ const text = forms("Text", allByText, root, MATCHER_OPTION_KEYS);
230
+ const role = forms("Role", allByRole, root, ROLE_OPTION_KEYS);
231
+ const labelText = forms("LabelText", allByLabelText, root, MATCHER_OPTION_KEYS);
232
+ const placeholderText = forms("PlaceholderText", allByPlaceholderText, root, MATCHER_OPTION_KEYS);
233
+ const testId = forms("TestId", allByTestId, root, MATCHER_OPTION_KEYS);
234
+ const displayValue = forms("DisplayValue", allByDisplayValue, root, MATCHER_OPTION_KEYS);
235
+
236
+ return {
237
+ getByText: text.get,
238
+ getAllByText: text.getAll,
239
+ queryByText: text.query,
240
+ queryAllByText: text.queryAll,
241
+ findByText: text.find,
242
+ findAllByText: text.findAll,
243
+
244
+ getByRole: role.get,
245
+ getAllByRole: role.getAll,
246
+ queryByRole: role.query,
247
+ queryAllByRole: role.queryAll,
248
+ findByRole: role.find,
249
+ findAllByRole: role.findAll,
250
+
251
+ getByLabelText: labelText.get,
252
+ getAllByLabelText: labelText.getAll,
253
+ queryByLabelText: labelText.query,
254
+ queryAllByLabelText: labelText.queryAll,
255
+ findByLabelText: labelText.find,
256
+ findAllByLabelText: labelText.findAll,
257
+
258
+ getByPlaceholderText: placeholderText.get,
259
+ getAllByPlaceholderText: placeholderText.getAll,
260
+ queryByPlaceholderText: placeholderText.query,
261
+ queryAllByPlaceholderText: placeholderText.queryAll,
262
+ findByPlaceholderText: placeholderText.find,
263
+ findAllByPlaceholderText: placeholderText.findAll,
264
+
265
+ getByTestId: testId.get,
266
+ getAllByTestId: testId.getAll,
267
+ queryByTestId: testId.query,
268
+ queryAllByTestId: testId.queryAll,
269
+ findByTestId: testId.find,
270
+ findAllByTestId: testId.findAll,
271
+
272
+ getByDisplayValue: displayValue.get,
273
+ getAllByDisplayValue: displayValue.getAll,
274
+ queryByDisplayValue: displayValue.query,
275
+ queryAllByDisplayValue: displayValue.queryAll,
276
+ findByDisplayValue: displayValue.find,
277
+ findAllByDisplayValue: displayValue.findAll,
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Queries over the whole document.
283
+ *
284
+ * The document rather than the rendered container, because a dialog, a tooltip
285
+ * and a toast are rendered into a portal outside it — and a test that could
286
+ * not see them would be unable to assert on the components most likely to have
287
+ * a bug.
288
+ */
289
+ export const screen: Queries = queriesFor(() => bodyOf());
290
+
291
+ /** The same queries, restricted to one element's subtree. */
292
+ export function within(element: Element): Queries {
293
+ return queriesFor(() => element);
294
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@uniflowed/react-testing",
3
+ "version": "0.0.0-alpha.10",
4
+ "description": "React Testing Library over a real DOM, part of the Unified Toolchain for Flow.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
+ "directory": "packages/react-testing"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "internal"
19
+ ],
20
+ "dependencies": {
21
+ "@uniflowed/core": "0.0.0-alpha.10",
22
+ "@uniflowed/react": "0.0.0-alpha.10",
23
+ "happy-dom": "^20.13.2"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=19",
27
+ "react-dom": ">=19"
28
+ }
29
+ }