@gtkx/testing 0.18.0 → 0.18.2
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/dist/bind-queries.d.ts +1 -0
- package/dist/bind-queries.d.ts.map +1 -0
- package/dist/bind-queries.js +1 -0
- package/dist/bind-queries.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -0
- package/dist/error-builder.d.ts +1 -0
- package/dist/error-builder.d.ts.map +1 -0
- package/dist/error-builder.js +1 -0
- package/dist/error-builder.js.map +1 -0
- package/dist/fire-event.d.ts +1 -0
- package/dist/fire-event.d.ts.map +1 -0
- package/dist/fire-event.js +1 -0
- package/dist/fire-event.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/pretty-widget.d.ts +1 -0
- package/dist/pretty-widget.d.ts.map +1 -0
- package/dist/pretty-widget.js +1 -0
- package/dist/pretty-widget.js.map +1 -0
- package/dist/queries.d.ts +1 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +1 -0
- package/dist/queries.js.map +1 -0
- package/dist/render-hook.d.ts +1 -0
- package/dist/render-hook.d.ts.map +1 -0
- package/dist/render-hook.js +1 -0
- package/dist/render-hook.js.map +1 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1 -0
- package/dist/render.js.map +1 -0
- package/dist/role-helpers.d.ts +1 -0
- package/dist/role-helpers.d.ts.map +1 -0
- package/dist/role-helpers.js +1 -0
- package/dist/role-helpers.js.map +1 -0
- package/dist/screen.d.ts +1 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +1 -0
- package/dist/screen.js.map +1 -0
- package/dist/screenshot.d.ts +1 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +1 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +1 -0
- package/dist/timing.js.map +1 -0
- package/dist/traversal.d.ts +8 -0
- package/dist/traversal.d.ts.map +1 -0
- package/dist/traversal.js +1 -0
- package/dist/traversal.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/user-event.d.ts +1 -0
- package/dist/user-event.d.ts.map +1 -0
- package/dist/user-event.js +1 -0
- package/dist/user-event.js.map +1 -0
- package/dist/wait-for.d.ts +1 -0
- package/dist/wait-for.d.ts.map +1 -0
- package/dist/wait-for.js +1 -0
- package/dist/wait-for.js.map +1 -0
- package/dist/widget-text.d.ts +1 -0
- package/dist/widget-text.d.ts.map +1 -0
- package/dist/widget-text.js +1 -0
- package/dist/widget-text.js.map +1 -0
- package/dist/widget.d.ts +1 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widget.js +1 -0
- package/dist/widget.js.map +1 -0
- package/dist/within.d.ts +1 -0
- package/dist/within.d.ts.map +1 -0
- package/dist/within.js +1 -0
- package/dist/within.js.map +1 -0
- package/package.json +7 -5
- package/src/bind-queries.ts +52 -0
- package/src/config.ts +89 -0
- package/src/error-builder.ts +102 -0
- package/src/fire-event.ts +43 -0
- package/src/index.ts +51 -0
- package/src/pretty-widget.ts +205 -0
- package/src/queries.ts +511 -0
- package/src/render-hook.tsx +71 -0
- package/src/render.tsx +192 -0
- package/src/role-helpers.ts +126 -0
- package/src/screen.ts +125 -0
- package/src/screenshot.ts +105 -0
- package/src/timing.ts +17 -0
- package/src/traversal.ts +48 -0
- package/src/types.ts +210 -0
- package/src/user-event.ts +492 -0
- package/src/wait-for.ts +115 -0
- package/src/widget-text.ts +206 -0
- package/src/widget.ts +15 -0
- package/src/within.ts +31 -0
package/src/queries.ts
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { buildMultipleFoundError, buildNotFoundError } from "./error-builder.js";
|
|
3
|
+
import { type Container, findAll, traverse } from "./traversal.js";
|
|
4
|
+
import type { ByRoleOptions, TextMatch, TextMatchOptions } from "./types.js";
|
|
5
|
+
import { waitFor } from "./wait-for.js";
|
|
6
|
+
import {
|
|
7
|
+
getWidgetCheckedState,
|
|
8
|
+
getWidgetExpandedState,
|
|
9
|
+
getWidgetPressedState,
|
|
10
|
+
getWidgetSelectedState,
|
|
11
|
+
getWidgetTestId,
|
|
12
|
+
getWidgetText,
|
|
13
|
+
} from "./widget-text.js";
|
|
14
|
+
|
|
15
|
+
const buildNormalizer = (options?: TextMatchOptions): ((text: string) => string) => {
|
|
16
|
+
if (options?.normalizer) {
|
|
17
|
+
return options.normalizer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const trim = options?.trim ?? true;
|
|
21
|
+
const collapseWhitespace = options?.collapseWhitespace ?? true;
|
|
22
|
+
|
|
23
|
+
return (text: string): string => {
|
|
24
|
+
let result = text;
|
|
25
|
+
if (trim) {
|
|
26
|
+
result = result.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (collapseWhitespace) {
|
|
30
|
+
result = result.replace(/\s+/g, " ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const normalizeText = (text: string, options?: TextMatchOptions): string => {
|
|
38
|
+
const normalizer = buildNormalizer(options);
|
|
39
|
+
return normalizer(text);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const matchText = (
|
|
43
|
+
actual: string | null,
|
|
44
|
+
expected: TextMatch,
|
|
45
|
+
widget: Gtk.Widget,
|
|
46
|
+
options?: TextMatchOptions,
|
|
47
|
+
): boolean => {
|
|
48
|
+
if (actual === null) return false;
|
|
49
|
+
|
|
50
|
+
const normalizedActual = normalizeText(actual, options);
|
|
51
|
+
|
|
52
|
+
if (typeof expected === "function") {
|
|
53
|
+
return expected(normalizedActual, widget);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (expected instanceof RegExp) {
|
|
57
|
+
expected.lastIndex = 0;
|
|
58
|
+
return expected.test(normalizedActual);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalizedExpected = normalizeText(expected, options);
|
|
62
|
+
const exact = options?.exact ?? true;
|
|
63
|
+
return exact
|
|
64
|
+
? normalizedActual === normalizedExpected
|
|
65
|
+
: normalizedActual.toLowerCase().includes(normalizedExpected.toLowerCase());
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const matchByRoleOptions = (widget: Gtk.Widget, options?: ByRoleOptions): boolean => {
|
|
69
|
+
if (!options) return true;
|
|
70
|
+
|
|
71
|
+
if (options.name !== undefined) {
|
|
72
|
+
const text = getWidgetText(widget);
|
|
73
|
+
if (!matchText(text, options.name, widget, options)) return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.checked !== undefined) {
|
|
77
|
+
const checked = getWidgetCheckedState(widget);
|
|
78
|
+
if (checked !== options.checked) return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options.pressed !== undefined) {
|
|
82
|
+
const pressed = getWidgetPressedState(widget);
|
|
83
|
+
if (pressed !== options.pressed) return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.expanded !== undefined) {
|
|
87
|
+
const expanded = getWidgetExpandedState(widget);
|
|
88
|
+
if (expanded !== options.expanded) return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.selected !== undefined) {
|
|
92
|
+
const selected = getWidgetSelectedState(widget);
|
|
93
|
+
if (selected !== options.selected) return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Finds all elements matching a role without throwing.
|
|
101
|
+
*
|
|
102
|
+
* @param container - The container to search within
|
|
103
|
+
* @param role - The GTK accessible role to match
|
|
104
|
+
* @param options - Query options including name and state filters
|
|
105
|
+
* @returns Array of matching widgets (empty if none found)
|
|
106
|
+
*/
|
|
107
|
+
export const queryAllByRole = (
|
|
108
|
+
container: Container,
|
|
109
|
+
role: Gtk.AccessibleRole,
|
|
110
|
+
options?: ByRoleOptions,
|
|
111
|
+
): Gtk.Widget[] => {
|
|
112
|
+
return findAll(container, (node) => {
|
|
113
|
+
if (node.getAccessibleRole() !== role) return false;
|
|
114
|
+
return matchByRoleOptions(node, options);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Finds a single element matching a role without throwing.
|
|
120
|
+
*
|
|
121
|
+
* @param container - The container to search within
|
|
122
|
+
* @param role - The GTK accessible role to match
|
|
123
|
+
* @param options - Query options including name and state filters
|
|
124
|
+
* @returns The matching widget or null if not found
|
|
125
|
+
* @throws Error if multiple elements match
|
|
126
|
+
*/
|
|
127
|
+
export const queryByRole = (
|
|
128
|
+
container: Container,
|
|
129
|
+
role: Gtk.AccessibleRole,
|
|
130
|
+
options?: ByRoleOptions,
|
|
131
|
+
): Gtk.Widget | null => {
|
|
132
|
+
const matches = queryAllByRole(container, role, options);
|
|
133
|
+
|
|
134
|
+
if (matches.length > 1) {
|
|
135
|
+
throw buildMultipleFoundError(container, "role", { role, options }, matches.length);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return matches[0] ?? null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Finds all elements that are labelled by a GtkLabel whose text matches.
|
|
143
|
+
*
|
|
144
|
+
* Uses GtkLabel's mnemonic widget association to find form elements
|
|
145
|
+
* by their label text. Only returns widgets that are properly labelled
|
|
146
|
+
* via GtkLabel's mnemonic-widget property.
|
|
147
|
+
*
|
|
148
|
+
* @param container - The container to search within
|
|
149
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
150
|
+
* @param options - Query options including normalization
|
|
151
|
+
* @returns Array of labelled widgets (empty if none found)
|
|
152
|
+
*/
|
|
153
|
+
export const queryAllByLabelText = (
|
|
154
|
+
container: Container,
|
|
155
|
+
text: TextMatch,
|
|
156
|
+
options?: TextMatchOptions,
|
|
157
|
+
): Gtk.Widget[] => {
|
|
158
|
+
const results: Gtk.Widget[] = [];
|
|
159
|
+
|
|
160
|
+
for (const node of traverse(container)) {
|
|
161
|
+
if (!(node instanceof Gtk.Label)) continue;
|
|
162
|
+
|
|
163
|
+
const labelText = node.getLabel();
|
|
164
|
+
if (!labelText) continue;
|
|
165
|
+
if (!matchText(labelText, text, node, options)) continue;
|
|
166
|
+
|
|
167
|
+
const target = node.getMnemonicWidget();
|
|
168
|
+
if (target) {
|
|
169
|
+
results.push(target);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Finds a single element matching label text without throwing.
|
|
178
|
+
*
|
|
179
|
+
* @param container - The container to search within
|
|
180
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
181
|
+
* @param options - Query options including normalization
|
|
182
|
+
* @returns The matching widget or null if not found
|
|
183
|
+
* @throws Error if multiple elements match
|
|
184
|
+
*/
|
|
185
|
+
export const queryByLabelText = (
|
|
186
|
+
container: Container,
|
|
187
|
+
text: TextMatch,
|
|
188
|
+
options?: TextMatchOptions,
|
|
189
|
+
): Gtk.Widget | null => {
|
|
190
|
+
const matches = queryAllByLabelText(container, text, options);
|
|
191
|
+
|
|
192
|
+
if (matches.length > 1) {
|
|
193
|
+
throw buildMultipleFoundError(container, "labelText", { text, options }, matches.length);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return matches[0] ?? null;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Finds all elements matching text content without throwing.
|
|
201
|
+
*
|
|
202
|
+
* @param container - The container to search within
|
|
203
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
204
|
+
* @param options - Query options including normalization
|
|
205
|
+
* @returns Array of matching widgets (empty if none found)
|
|
206
|
+
*/
|
|
207
|
+
export const queryAllByText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget[] => {
|
|
208
|
+
return findAll(container, (node) => {
|
|
209
|
+
const widgetText = getWidgetText(node);
|
|
210
|
+
return matchText(widgetText, text, node, options);
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Finds a single element matching text content without throwing.
|
|
216
|
+
*
|
|
217
|
+
* @param container - The container to search within
|
|
218
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
219
|
+
* @param options - Query options including normalization
|
|
220
|
+
* @returns The matching widget or null if not found
|
|
221
|
+
* @throws Error if multiple elements match
|
|
222
|
+
*/
|
|
223
|
+
export const queryByText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget | null => {
|
|
224
|
+
const matches = queryAllByText(container, text, options);
|
|
225
|
+
|
|
226
|
+
if (matches.length > 1) {
|
|
227
|
+
throw buildMultipleFoundError(container, "text", { text, options }, matches.length);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return matches[0] ?? null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Finds all elements matching a test ID without throwing.
|
|
235
|
+
*
|
|
236
|
+
* @param container - The container to search within
|
|
237
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
238
|
+
* @param options - Query options including normalization
|
|
239
|
+
* @returns Array of matching widgets (empty if none found)
|
|
240
|
+
*/
|
|
241
|
+
export const queryAllByTestId = (container: Container, testId: TextMatch, options?: TextMatchOptions): Gtk.Widget[] => {
|
|
242
|
+
return findAll(container, (node) => {
|
|
243
|
+
const widgetTestId = getWidgetTestId(node);
|
|
244
|
+
return matchText(widgetTestId, testId, node, options);
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Finds a single element matching a test ID without throwing.
|
|
250
|
+
*
|
|
251
|
+
* @param container - The container to search within
|
|
252
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
253
|
+
* @param options - Query options including normalization
|
|
254
|
+
* @returns The matching widget or null if not found
|
|
255
|
+
* @throws Error if multiple elements match
|
|
256
|
+
*/
|
|
257
|
+
export const queryByTestId = (
|
|
258
|
+
container: Container,
|
|
259
|
+
testId: TextMatch,
|
|
260
|
+
options?: TextMatchOptions,
|
|
261
|
+
): Gtk.Widget | null => {
|
|
262
|
+
const matches = queryAllByTestId(container, testId, options);
|
|
263
|
+
|
|
264
|
+
if (matches.length > 1) {
|
|
265
|
+
throw buildMultipleFoundError(container, "testId", { testId, options }, matches.length);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return matches[0] ?? null;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const getAllByRole = (container: Container, role: Gtk.AccessibleRole, options?: ByRoleOptions): Gtk.Widget[] => {
|
|
272
|
+
const matches = queryAllByRole(container, role, options);
|
|
273
|
+
|
|
274
|
+
if (matches.length === 0) {
|
|
275
|
+
throw buildNotFoundError(container, "role", { role, options });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return matches;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const getByRole = (container: Container, role: Gtk.AccessibleRole, options?: ByRoleOptions): Gtk.Widget => {
|
|
282
|
+
const matches = getAllByRole(container, role, options);
|
|
283
|
+
|
|
284
|
+
if (matches.length > 1) {
|
|
285
|
+
throw buildMultipleFoundError(container, "role", { role, options }, matches.length);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return matches[0] as Gtk.Widget;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const getAllByLabelText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget[] => {
|
|
292
|
+
const matches = queryAllByLabelText(container, text, options);
|
|
293
|
+
|
|
294
|
+
if (matches.length === 0) {
|
|
295
|
+
throw buildNotFoundError(container, "labelText", { text, options });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return matches;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const getByLabelText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget => {
|
|
302
|
+
const matches = getAllByLabelText(container, text, options);
|
|
303
|
+
|
|
304
|
+
if (matches.length > 1) {
|
|
305
|
+
throw buildMultipleFoundError(container, "labelText", { text, options }, matches.length);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return matches[0] as Gtk.Widget;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const getAllByText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget[] => {
|
|
312
|
+
const matches = queryAllByText(container, text, options);
|
|
313
|
+
|
|
314
|
+
if (matches.length === 0) {
|
|
315
|
+
throw buildNotFoundError(container, "text", { text, options });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return matches;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const getByText = (container: Container, text: TextMatch, options?: TextMatchOptions): Gtk.Widget => {
|
|
322
|
+
const matches = getAllByText(container, text, options);
|
|
323
|
+
|
|
324
|
+
if (matches.length > 1) {
|
|
325
|
+
throw buildMultipleFoundError(container, "text", { text, options }, matches.length);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return matches[0] as Gtk.Widget;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const getAllByTestId = (container: Container, testId: TextMatch, options?: TextMatchOptions): Gtk.Widget[] => {
|
|
332
|
+
const matches = queryAllByTestId(container, testId, options);
|
|
333
|
+
|
|
334
|
+
if (matches.length === 0) {
|
|
335
|
+
throw buildNotFoundError(container, "testId", { testId, options });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return matches;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const getByTestId = (container: Container, testId: TextMatch, options?: TextMatchOptions): Gtk.Widget => {
|
|
342
|
+
const matches = getAllByTestId(container, testId, options);
|
|
343
|
+
|
|
344
|
+
if (matches.length > 1) {
|
|
345
|
+
throw buildMultipleFoundError(container, "testId", { testId, options }, matches.length);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return matches[0] as Gtk.Widget;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Finds a single element by accessible role.
|
|
353
|
+
*
|
|
354
|
+
* Waits for the element to appear, throwing if not found within timeout.
|
|
355
|
+
*
|
|
356
|
+
* @param container - The container to search within
|
|
357
|
+
* @param role - The GTK accessible role to match
|
|
358
|
+
* @param options - Query options including name, state filters, and timeout
|
|
359
|
+
* @returns Promise resolving to the matching widget
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```tsx
|
|
363
|
+
* const button = await findByRole(container, Gtk.AccessibleRole.BUTTON, { name: "Submit" });
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
export const findByRole = async (
|
|
367
|
+
container: Container,
|
|
368
|
+
role: Gtk.AccessibleRole,
|
|
369
|
+
options?: ByRoleOptions,
|
|
370
|
+
): Promise<Gtk.Widget> =>
|
|
371
|
+
waitFor(() => getByRole(container, role, options), {
|
|
372
|
+
timeout: options?.timeout,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Finds all elements matching an accessible role.
|
|
377
|
+
*
|
|
378
|
+
* @param container - The container to search within
|
|
379
|
+
* @param role - The GTK accessible role to match
|
|
380
|
+
* @param options - Query options including name, state filters, and timeout
|
|
381
|
+
* @returns Promise resolving to array of matching widgets
|
|
382
|
+
*/
|
|
383
|
+
export const findAllByRole = async (
|
|
384
|
+
container: Container,
|
|
385
|
+
role: Gtk.AccessibleRole,
|
|
386
|
+
options?: ByRoleOptions,
|
|
387
|
+
): Promise<Gtk.Widget[]> =>
|
|
388
|
+
waitFor(() => getAllByRole(container, role, options), {
|
|
389
|
+
timeout: options?.timeout,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Finds a single element that is labelled by a GtkLabel whose text matches.
|
|
394
|
+
*
|
|
395
|
+
* Waits for the element to appear, throwing if not found within timeout.
|
|
396
|
+
* Uses GtkLabel's mnemonic widget association to find form elements.
|
|
397
|
+
*
|
|
398
|
+
* @param container - The container to search within
|
|
399
|
+
* @param text - Label text to match (string, RegExp, or custom matcher)
|
|
400
|
+
* @param options - Query options including normalization and timeout
|
|
401
|
+
* @returns Promise resolving to the labelled widget
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```tsx
|
|
405
|
+
* const input = await findByLabelText(container, "Username");
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
export const findByLabelText = async (
|
|
409
|
+
container: Container,
|
|
410
|
+
text: TextMatch,
|
|
411
|
+
options?: TextMatchOptions,
|
|
412
|
+
): Promise<Gtk.Widget> =>
|
|
413
|
+
waitFor(() => getByLabelText(container, text, options), {
|
|
414
|
+
timeout: options?.timeout,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Finds all elements matching label or text content.
|
|
419
|
+
*
|
|
420
|
+
* @param container - The container to search within
|
|
421
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
422
|
+
* @param options - Query options including normalization and timeout
|
|
423
|
+
* @returns Promise resolving to array of matching widgets
|
|
424
|
+
*/
|
|
425
|
+
export const findAllByLabelText = async (
|
|
426
|
+
container: Container,
|
|
427
|
+
text: TextMatch,
|
|
428
|
+
options?: TextMatchOptions,
|
|
429
|
+
): Promise<Gtk.Widget[]> =>
|
|
430
|
+
waitFor(() => getAllByLabelText(container, text, options), {
|
|
431
|
+
timeout: options?.timeout,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Finds a single element by visible text content.
|
|
436
|
+
*
|
|
437
|
+
* Similar to {@link findByLabelText} but focuses on directly visible text.
|
|
438
|
+
*
|
|
439
|
+
* @param container - The container to search within
|
|
440
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
441
|
+
* @param options - Query options including normalization and timeout
|
|
442
|
+
* @returns Promise resolving to the matching widget
|
|
443
|
+
*/
|
|
444
|
+
export const findByText = async (
|
|
445
|
+
container: Container,
|
|
446
|
+
text: TextMatch,
|
|
447
|
+
options?: TextMatchOptions,
|
|
448
|
+
): Promise<Gtk.Widget> =>
|
|
449
|
+
waitFor(() => getByText(container, text, options), {
|
|
450
|
+
timeout: options?.timeout,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Finds all elements matching visible text content.
|
|
455
|
+
*
|
|
456
|
+
* @param container - The container to search within
|
|
457
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
458
|
+
* @param options - Query options including normalization and timeout
|
|
459
|
+
* @returns Promise resolving to array of matching widgets
|
|
460
|
+
*/
|
|
461
|
+
export const findAllByText = async (
|
|
462
|
+
container: Container,
|
|
463
|
+
text: TextMatch,
|
|
464
|
+
options?: TextMatchOptions,
|
|
465
|
+
): Promise<Gtk.Widget[]> =>
|
|
466
|
+
waitFor(() => getAllByText(container, text, options), {
|
|
467
|
+
timeout: options?.timeout,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Finds a single element by test ID (widget name).
|
|
472
|
+
*
|
|
473
|
+
* Uses the widget's `name` property as a test identifier.
|
|
474
|
+
* Set via the `name` prop on GTKX components.
|
|
475
|
+
*
|
|
476
|
+
* @param container - The container to search within
|
|
477
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
478
|
+
* @param options - Query options including normalization and timeout
|
|
479
|
+
* @returns Promise resolving to the matching widget
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```tsx
|
|
483
|
+
* // In component: <GtkButton name="submit-btn" />
|
|
484
|
+
* const button = await findByTestId(container, "submit-btn");
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
export const findByTestId = async (
|
|
488
|
+
container: Container,
|
|
489
|
+
testId: TextMatch,
|
|
490
|
+
options?: TextMatchOptions,
|
|
491
|
+
): Promise<Gtk.Widget> =>
|
|
492
|
+
waitFor(() => getByTestId(container, testId, options), {
|
|
493
|
+
timeout: options?.timeout,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Finds all elements matching a test ID pattern.
|
|
498
|
+
*
|
|
499
|
+
* @param container - The container to search within
|
|
500
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
501
|
+
* @param options - Query options including normalization and timeout
|
|
502
|
+
* @returns Promise resolving to array of matching widgets
|
|
503
|
+
*/
|
|
504
|
+
export const findAllByTestId = async (
|
|
505
|
+
container: Container,
|
|
506
|
+
testId: TextMatch,
|
|
507
|
+
options?: TextMatchOptions,
|
|
508
|
+
): Promise<Gtk.Widget[]> =>
|
|
509
|
+
waitFor(() => getAllByTestId(container, testId, options), {
|
|
510
|
+
timeout: options?.timeout,
|
|
511
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { render } from "./render.js";
|
|
3
|
+
import type { RenderHookOptions, RenderHookResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders a React hook for testing.
|
|
7
|
+
*
|
|
8
|
+
* Creates a test component that executes the hook and provides utilities
|
|
9
|
+
* for accessing the result, re-rendering with new props, and cleanup.
|
|
10
|
+
*
|
|
11
|
+
* @param callback - Function that calls the hook and returns its result
|
|
12
|
+
* @param options - Render options including initialProps and wrapper
|
|
13
|
+
* @returns A promise resolving to the hook result and utilities
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { renderHook } from "@gtkx/testing";
|
|
18
|
+
* import { useState } from "react";
|
|
19
|
+
*
|
|
20
|
+
* test("useState hook", async () => {
|
|
21
|
+
* const { result } = await renderHook(() => useState(0));
|
|
22
|
+
* expect(result.current[0]).toBe(0);
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { renderHook } from "@gtkx/testing";
|
|
29
|
+
*
|
|
30
|
+
* test("hook with props", async () => {
|
|
31
|
+
* const { result, rerender } = await renderHook(
|
|
32
|
+
* ({ multiplier }) => useMultiplier(multiplier),
|
|
33
|
+
* { initialProps: { multiplier: 2 } }
|
|
34
|
+
* );
|
|
35
|
+
*
|
|
36
|
+
* expect(result.current).toBe(2);
|
|
37
|
+
*
|
|
38
|
+
* await rerender({ multiplier: 3 });
|
|
39
|
+
* expect(result.current).toBe(3);
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const renderHook = async <Result, Props>(
|
|
44
|
+
callback: (props: Props) => Result,
|
|
45
|
+
options?: RenderHookOptions<Props>,
|
|
46
|
+
): Promise<RenderHookResult<Result, Props>> => {
|
|
47
|
+
const resultRef = { current: undefined as Result };
|
|
48
|
+
let currentProps = options?.initialProps as Props;
|
|
49
|
+
|
|
50
|
+
const TestComponent = ({ props }: { props: Props }): null => {
|
|
51
|
+
const result = callback(props);
|
|
52
|
+
const ref = useRef(resultRef);
|
|
53
|
+
ref.current.current = result;
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const renderResult = await render(<TestComponent props={currentProps} />, {
|
|
58
|
+
wrapper: options?.wrapper ?? true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
result: resultRef,
|
|
63
|
+
rerender: async (newProps?: Props) => {
|
|
64
|
+
if (newProps !== undefined) {
|
|
65
|
+
currentProps = newProps;
|
|
66
|
+
}
|
|
67
|
+
await renderResult.rerender(<TestComponent props={currentProps} />);
|
|
68
|
+
},
|
|
69
|
+
unmount: renderResult.unmount,
|
|
70
|
+
};
|
|
71
|
+
};
|