@gtkx/testing 0.3.5 → 0.4.0
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/index.d.ts +1 -1
- package/dist/queries.d.ts +13 -13
- package/dist/queries.js +116 -49
- package/dist/render.js +8 -4
- package/dist/screen.d.ts +7 -7
- package/dist/types.d.ts +35 -9
- package/dist/user-event.js +26 -10
- package/dist/widget.js +8 -4
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { fireEvent } from "./fire-event.js";
|
|
|
2
2
|
export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, findByLabelText, findByRole, findByTestId, findByText, } from "./queries.js";
|
|
3
3
|
export { cleanup, render, teardown } from "./render.js";
|
|
4
4
|
export { screen } from "./screen.js";
|
|
5
|
-
export type { BoundQueries, ByRoleOptions, RenderOptions, RenderResult, TextMatchOptions, WaitForOptions, } from "./types.js";
|
|
5
|
+
export type { BoundQueries, ByRoleOptions, NormalizerOptions, RenderOptions, RenderResult, TextMatch, TextMatchFunction, TextMatchOptions, WaitForOptions, } from "./types.js";
|
|
6
6
|
export type { TabOptions } from "./user-event.js";
|
|
7
7
|
export { userEvent } from "./user-event.js";
|
|
8
8
|
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
|
-
import type { ByRoleOptions, TextMatchOptions } from "./types.js";
|
|
3
|
+
import type { ByRoleOptions, TextMatch, TextMatchOptions } from "./types.js";
|
|
4
4
|
type Container = Gtk.Application | Gtk.Widget;
|
|
5
5
|
/**
|
|
6
6
|
* Waits for and finds a single widget matching the specified accessible role.
|
|
@@ -21,49 +21,49 @@ export declare const findAllByRole: (container: Container, role: AccessibleRole,
|
|
|
21
21
|
/**
|
|
22
22
|
* Waits for and finds a single widget matching the specified label text.
|
|
23
23
|
* @param container - The container to search within
|
|
24
|
-
* @param text - The text or
|
|
24
|
+
* @param text - The text, pattern, or matcher function
|
|
25
25
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
26
26
|
* @returns Promise resolving to the matching widget
|
|
27
27
|
*/
|
|
28
|
-
export declare const findByLabelText: (container: Container, text:
|
|
28
|
+
export declare const findByLabelText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
29
29
|
/**
|
|
30
30
|
* Waits for and finds all widgets matching the specified label text.
|
|
31
31
|
* @param container - The container to search within
|
|
32
|
-
* @param text - The text or
|
|
32
|
+
* @param text - The text, pattern, or matcher function
|
|
33
33
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
34
34
|
* @returns Promise resolving to array of matching widgets
|
|
35
35
|
*/
|
|
36
|
-
export declare const findAllByLabelText: (container: Container, text:
|
|
36
|
+
export declare const findAllByLabelText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
37
37
|
/**
|
|
38
38
|
* Waits for and finds a single widget matching the specified text content.
|
|
39
39
|
* @param container - The container to search within
|
|
40
|
-
* @param text - The text or
|
|
40
|
+
* @param text - The text, pattern, or matcher function
|
|
41
41
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
42
42
|
* @returns Promise resolving to the matching widget
|
|
43
43
|
*/
|
|
44
|
-
export declare const findByText: (container: Container, text:
|
|
44
|
+
export declare const findByText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
45
45
|
/**
|
|
46
46
|
* Waits for and finds all widgets matching the specified text content.
|
|
47
47
|
* @param container - The container to search within
|
|
48
|
-
* @param text - The text or
|
|
48
|
+
* @param text - The text, pattern, or matcher function
|
|
49
49
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
50
50
|
* @returns Promise resolving to array of matching widgets
|
|
51
51
|
*/
|
|
52
|
-
export declare const findAllByText: (container: Container, text:
|
|
52
|
+
export declare const findAllByText: (container: Container, text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
53
53
|
/**
|
|
54
54
|
* Waits for and finds a single widget matching the specified test ID.
|
|
55
55
|
* @param container - The container to search within
|
|
56
|
-
* @param testId - The test ID or
|
|
56
|
+
* @param testId - The test ID, pattern, or matcher function
|
|
57
57
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
58
58
|
* @returns Promise resolving to the matching widget
|
|
59
59
|
*/
|
|
60
|
-
export declare const findByTestId: (container: Container, testId:
|
|
60
|
+
export declare const findByTestId: (container: Container, testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
61
61
|
/**
|
|
62
62
|
* Waits for and finds all widgets matching the specified test ID.
|
|
63
63
|
* @param container - The container to search within
|
|
64
|
-
* @param testId - The test ID or
|
|
64
|
+
* @param testId - The test ID, pattern, or matcher function
|
|
65
65
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
66
66
|
* @returns Promise resolving to array of matching widgets
|
|
67
67
|
*/
|
|
68
|
-
export declare const findAllByTestId: (container: Container, testId:
|
|
68
|
+
export declare const findAllByTestId: (container: Container, testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
69
69
|
export {};
|
package/dist/queries.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
import { getInterface
|
|
1
|
+
import { getInterface } from "@gtkx/ffi";
|
|
2
2
|
import { Accessible, AccessibleRole, Button, CheckButton, Editable, Expander, Frame, Label, MenuButton, StackPage, Switch, ToggleButton, Window, } from "@gtkx/ffi/gtk";
|
|
3
3
|
import { findAll } from "./traversal.js";
|
|
4
4
|
import { waitFor } from "./wait-for.js";
|
|
5
|
-
const
|
|
5
|
+
const buildNormalizer = (options) => {
|
|
6
|
+
if (options?.normalizer) {
|
|
7
|
+
return options.normalizer;
|
|
8
|
+
}
|
|
9
|
+
const trim = options?.trim ?? true;
|
|
10
|
+
const collapseWhitespace = options?.collapseWhitespace ?? true;
|
|
11
|
+
return (text) => {
|
|
12
|
+
let result = text;
|
|
13
|
+
if (trim) {
|
|
14
|
+
result = result.trim();
|
|
15
|
+
}
|
|
16
|
+
if (collapseWhitespace) {
|
|
17
|
+
result = result.replace(/\s+/g, " ");
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
6
22
|
const normalizeText = (text, options) => {
|
|
7
|
-
const normalizer = options
|
|
23
|
+
const normalizer = buildNormalizer(options);
|
|
8
24
|
return normalizer(text);
|
|
9
25
|
};
|
|
10
|
-
const matchText = (actual, expected, options) => {
|
|
26
|
+
const matchText = (actual, expected, widget, options) => {
|
|
11
27
|
if (actual === null)
|
|
12
28
|
return false;
|
|
13
29
|
const normalizedActual = normalizeText(actual, options);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
if (typeof expected === "function") {
|
|
31
|
+
return expected(normalizedActual, widget);
|
|
32
|
+
}
|
|
33
|
+
if (expected instanceof RegExp) {
|
|
34
|
+
expected.lastIndex = 0;
|
|
35
|
+
return expected.test(normalizedActual);
|
|
18
36
|
}
|
|
19
|
-
|
|
37
|
+
const normalizedExpected = normalizeText(expected, options);
|
|
38
|
+
const exact = options?.exact ?? true;
|
|
39
|
+
return exact ? normalizedActual === normalizedExpected : normalizedActual.includes(normalizedExpected);
|
|
20
40
|
};
|
|
21
41
|
const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
22
42
|
AccessibleRole.BUTTON,
|
|
@@ -31,44 +51,68 @@ const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
|
31
51
|
]);
|
|
32
52
|
const isInternalLabel = (widget) => {
|
|
33
53
|
const accessible = getInterface(widget, Accessible);
|
|
34
|
-
if (accessible.getAccessibleRole() !== AccessibleRole.LABEL)
|
|
54
|
+
if (!accessible || accessible.getAccessibleRole() !== AccessibleRole.LABEL)
|
|
35
55
|
return false;
|
|
36
56
|
const parent = widget.getParent();
|
|
37
57
|
if (!parent)
|
|
38
58
|
return false;
|
|
39
|
-
const
|
|
40
|
-
|
|
59
|
+
const parentAccessible = getInterface(parent, Accessible);
|
|
60
|
+
if (!parentAccessible)
|
|
61
|
+
return false;
|
|
62
|
+
return ROLES_WITH_INTERNAL_LABELS.has(parentAccessible.getAccessibleRole());
|
|
63
|
+
};
|
|
64
|
+
const collectChildLabels = (widget) => {
|
|
65
|
+
const labels = [];
|
|
66
|
+
let child = widget.getFirstChild();
|
|
67
|
+
while (child) {
|
|
68
|
+
const childAccessible = getInterface(child, Accessible);
|
|
69
|
+
if (childAccessible?.getAccessibleRole() === AccessibleRole.LABEL) {
|
|
70
|
+
const labelText = getInterface(child, Label)?.getLabel();
|
|
71
|
+
if (labelText)
|
|
72
|
+
labels.push(labelText);
|
|
73
|
+
}
|
|
74
|
+
labels.push(...collectChildLabels(child));
|
|
75
|
+
child = child.getNextSibling();
|
|
76
|
+
}
|
|
77
|
+
return labels;
|
|
41
78
|
};
|
|
42
79
|
const getWidgetText = (widget) => {
|
|
43
80
|
if (isInternalLabel(widget))
|
|
44
81
|
return null;
|
|
45
|
-
const role = getInterface(widget, Accessible)
|
|
82
|
+
const role = getInterface(widget, Accessible)?.getAccessibleRole();
|
|
83
|
+
if (role === undefined)
|
|
84
|
+
return null;
|
|
46
85
|
switch (role) {
|
|
47
86
|
case AccessibleRole.BUTTON:
|
|
48
87
|
case AccessibleRole.LINK:
|
|
49
|
-
case AccessibleRole.TAB:
|
|
50
|
-
|
|
88
|
+
case AccessibleRole.TAB: {
|
|
89
|
+
const directLabel = getInterface(widget, Button)?.getLabel() ??
|
|
90
|
+
getInterface(widget, MenuButton)?.getLabel() ??
|
|
91
|
+
getInterface(widget, Expander)?.getLabel();
|
|
92
|
+
if (directLabel)
|
|
93
|
+
return directLabel;
|
|
94
|
+
const childLabels = collectChildLabels(widget);
|
|
95
|
+
return childLabels.length > 0 ? childLabels.join(" ") : null;
|
|
96
|
+
}
|
|
51
97
|
case AccessibleRole.TOGGLE_BUTTON:
|
|
52
|
-
return getInterface(widget, ToggleButton)
|
|
98
|
+
return getInterface(widget, ToggleButton)?.getLabel() ?? null;
|
|
53
99
|
case AccessibleRole.CHECKBOX:
|
|
54
100
|
case AccessibleRole.RADIO:
|
|
55
|
-
return getInterface(widget, CheckButton)
|
|
101
|
+
return getInterface(widget, CheckButton)?.getLabel() ?? null;
|
|
56
102
|
case AccessibleRole.LABEL:
|
|
57
|
-
return getInterface(widget, Label)
|
|
103
|
+
return getInterface(widget, Label)?.getLabel() ?? null;
|
|
58
104
|
case AccessibleRole.TEXT_BOX:
|
|
59
105
|
case AccessibleRole.SEARCH_BOX:
|
|
60
106
|
case AccessibleRole.SPIN_BUTTON:
|
|
61
|
-
return getInterface(widget, Editable)
|
|
107
|
+
return getInterface(widget, Editable)?.getText() ?? null;
|
|
62
108
|
case AccessibleRole.GROUP:
|
|
63
|
-
return
|
|
109
|
+
return getInterface(widget, Frame)?.getLabel() ?? null;
|
|
64
110
|
case AccessibleRole.WINDOW:
|
|
65
111
|
case AccessibleRole.DIALOG:
|
|
66
112
|
case AccessibleRole.ALERT_DIALOG:
|
|
67
|
-
return getInterface(widget, Window)
|
|
113
|
+
return getInterface(widget, Window)?.getTitle() ?? null;
|
|
68
114
|
case AccessibleRole.TAB_PANEL:
|
|
69
|
-
return
|
|
70
|
-
case AccessibleRole.SWITCH:
|
|
71
|
-
return null;
|
|
115
|
+
return getInterface(widget, StackPage)?.getTitle() ?? null;
|
|
72
116
|
default:
|
|
73
117
|
return null;
|
|
74
118
|
}
|
|
@@ -77,26 +121,32 @@ const getWidgetTestId = (widget) => {
|
|
|
77
121
|
return widget.getName();
|
|
78
122
|
};
|
|
79
123
|
const getWidgetCheckedState = (widget) => {
|
|
80
|
-
const
|
|
124
|
+
const accessible = getInterface(widget, Accessible);
|
|
125
|
+
if (!accessible)
|
|
126
|
+
return undefined;
|
|
127
|
+
const role = accessible.getAccessibleRole();
|
|
81
128
|
switch (role) {
|
|
82
129
|
case AccessibleRole.CHECKBOX:
|
|
83
130
|
case AccessibleRole.RADIO:
|
|
84
|
-
return getInterface(widget, CheckButton)
|
|
131
|
+
return getInterface(widget, CheckButton)?.getActive();
|
|
85
132
|
case AccessibleRole.TOGGLE_BUTTON:
|
|
86
|
-
return getInterface(widget, ToggleButton)
|
|
133
|
+
return getInterface(widget, ToggleButton)?.getActive();
|
|
87
134
|
case AccessibleRole.SWITCH:
|
|
88
|
-
return getInterface(widget, Switch)
|
|
135
|
+
return getInterface(widget, Switch)?.getActive();
|
|
89
136
|
default:
|
|
90
137
|
return undefined;
|
|
91
138
|
}
|
|
92
139
|
};
|
|
93
140
|
const getWidgetExpandedState = (widget) => {
|
|
94
|
-
const
|
|
141
|
+
const accessible = getInterface(widget, Accessible);
|
|
142
|
+
if (!accessible)
|
|
143
|
+
return undefined;
|
|
144
|
+
const role = accessible.getAccessibleRole();
|
|
95
145
|
if (role === AccessibleRole.BUTTON) {
|
|
96
146
|
const parent = widget.getParent();
|
|
97
147
|
if (!parent)
|
|
98
148
|
return undefined;
|
|
99
|
-
return getInterface(parent, Expander)
|
|
149
|
+
return getInterface(parent, Expander)?.getExpanded();
|
|
100
150
|
}
|
|
101
151
|
return undefined;
|
|
102
152
|
};
|
|
@@ -105,7 +155,7 @@ const matchByRoleOptions = (widget, options) => {
|
|
|
105
155
|
return true;
|
|
106
156
|
if (options.name !== undefined) {
|
|
107
157
|
const text = getWidgetText(widget);
|
|
108
|
-
if (!matchText(text, options.name, options))
|
|
158
|
+
if (!matchText(text, options.name, widget, options))
|
|
109
159
|
return false;
|
|
110
160
|
}
|
|
111
161
|
if (options.checked !== undefined) {
|
|
@@ -139,7 +189,8 @@ const formatByRoleError = (role, options) => {
|
|
|
139
189
|
};
|
|
140
190
|
const getAllByRole = (container, role, options) => {
|
|
141
191
|
const matches = findAll(container, (node) => {
|
|
142
|
-
|
|
192
|
+
const accessible = getInterface(node, Accessible);
|
|
193
|
+
if (!accessible || accessible.getAccessibleRole() !== role)
|
|
143
194
|
return false;
|
|
144
195
|
return matchByRoleOptions(node, options);
|
|
145
196
|
});
|
|
@@ -161,7 +212,7 @@ const getByRole = (container, role, options) => {
|
|
|
161
212
|
const getAllByLabelText = (container, text, options) => {
|
|
162
213
|
const matches = findAll(container, (node) => {
|
|
163
214
|
const widgetText = getWidgetText(node);
|
|
164
|
-
return matchText(widgetText, text, options);
|
|
215
|
+
return matchText(widgetText, text, node, options);
|
|
165
216
|
});
|
|
166
217
|
if (matches.length === 0) {
|
|
167
218
|
throw new Error(`Unable to find any elements with label text "${text}"`);
|
|
@@ -181,7 +232,7 @@ const getByLabelText = (container, text, options) => {
|
|
|
181
232
|
const getAllByText = (container, text, options) => {
|
|
182
233
|
const matches = findAll(container, (node) => {
|
|
183
234
|
const widgetText = getWidgetText(node);
|
|
184
|
-
return matchText(widgetText, text, options);
|
|
235
|
+
return matchText(widgetText, text, node, options);
|
|
185
236
|
});
|
|
186
237
|
if (matches.length === 0) {
|
|
187
238
|
throw new Error(`Unable to find any elements with text "${text}"`);
|
|
@@ -201,7 +252,7 @@ const getByText = (container, text, options) => {
|
|
|
201
252
|
const getAllByTestId = (container, testId, options) => {
|
|
202
253
|
const matches = findAll(container, (node) => {
|
|
203
254
|
const widgetTestId = getWidgetTestId(node);
|
|
204
|
-
return matchText(widgetTestId, testId, options);
|
|
255
|
+
return matchText(widgetTestId, testId, node, options);
|
|
205
256
|
});
|
|
206
257
|
if (matches.length === 0) {
|
|
207
258
|
throw new Error(`Unable to find any elements with test id "${testId}"`);
|
|
@@ -225,7 +276,9 @@ const getByTestId = (container, testId, options) => {
|
|
|
225
276
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
226
277
|
* @returns Promise resolving to the matching widget
|
|
227
278
|
*/
|
|
228
|
-
export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options), {
|
|
279
|
+
export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options), {
|
|
280
|
+
timeout: options?.timeout,
|
|
281
|
+
});
|
|
229
282
|
/**
|
|
230
283
|
* Waits for and finds all widgets matching the specified accessible role.
|
|
231
284
|
* @param container - The container to search within
|
|
@@ -233,52 +286,66 @@ export const findByRole = async (container, role, options) => waitFor(() => getB
|
|
|
233
286
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
234
287
|
* @returns Promise resolving to array of matching widgets
|
|
235
288
|
*/
|
|
236
|
-
export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options), {
|
|
289
|
+
export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options), {
|
|
290
|
+
timeout: options?.timeout,
|
|
291
|
+
});
|
|
237
292
|
/**
|
|
238
293
|
* Waits for and finds a single widget matching the specified label text.
|
|
239
294
|
* @param container - The container to search within
|
|
240
|
-
* @param text - The text or
|
|
295
|
+
* @param text - The text, pattern, or matcher function
|
|
241
296
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
242
297
|
* @returns Promise resolving to the matching widget
|
|
243
298
|
*/
|
|
244
|
-
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options), {
|
|
299
|
+
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options), {
|
|
300
|
+
timeout: options?.timeout,
|
|
301
|
+
});
|
|
245
302
|
/**
|
|
246
303
|
* Waits for and finds all widgets matching the specified label text.
|
|
247
304
|
* @param container - The container to search within
|
|
248
|
-
* @param text - The text or
|
|
305
|
+
* @param text - The text, pattern, or matcher function
|
|
249
306
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
250
307
|
* @returns Promise resolving to array of matching widgets
|
|
251
308
|
*/
|
|
252
|
-
export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options), {
|
|
309
|
+
export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options), {
|
|
310
|
+
timeout: options?.timeout,
|
|
311
|
+
});
|
|
253
312
|
/**
|
|
254
313
|
* Waits for and finds a single widget matching the specified text content.
|
|
255
314
|
* @param container - The container to search within
|
|
256
|
-
* @param text - The text or
|
|
315
|
+
* @param text - The text, pattern, or matcher function
|
|
257
316
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
258
317
|
* @returns Promise resolving to the matching widget
|
|
259
318
|
*/
|
|
260
|
-
export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options), {
|
|
319
|
+
export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options), {
|
|
320
|
+
timeout: options?.timeout,
|
|
321
|
+
});
|
|
261
322
|
/**
|
|
262
323
|
* Waits for and finds all widgets matching the specified text content.
|
|
263
324
|
* @param container - The container to search within
|
|
264
|
-
* @param text - The text or
|
|
325
|
+
* @param text - The text, pattern, or matcher function
|
|
265
326
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
266
327
|
* @returns Promise resolving to array of matching widgets
|
|
267
328
|
*/
|
|
268
|
-
export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options), {
|
|
329
|
+
export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options), {
|
|
330
|
+
timeout: options?.timeout,
|
|
331
|
+
});
|
|
269
332
|
/**
|
|
270
333
|
* Waits for and finds a single widget matching the specified test ID.
|
|
271
334
|
* @param container - The container to search within
|
|
272
|
-
* @param testId - The test ID or
|
|
335
|
+
* @param testId - The test ID, pattern, or matcher function
|
|
273
336
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
274
337
|
* @returns Promise resolving to the matching widget
|
|
275
338
|
*/
|
|
276
|
-
export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options), {
|
|
339
|
+
export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options), {
|
|
340
|
+
timeout: options?.timeout,
|
|
341
|
+
});
|
|
277
342
|
/**
|
|
278
343
|
* Waits for and finds all widgets matching the specified test ID.
|
|
279
344
|
* @param container - The container to search within
|
|
280
|
-
* @param testId - The test ID or
|
|
345
|
+
* @param testId - The test ID, pattern, or matcher function
|
|
281
346
|
* @param options - Text matching options (exact, normalizer, timeout)
|
|
282
347
|
* @returns Promise resolving to array of matching widgets
|
|
283
348
|
*/
|
|
284
|
-
export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options), {
|
|
349
|
+
export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options), {
|
|
350
|
+
timeout: options?.timeout,
|
|
351
|
+
});
|
package/dist/render.js
CHANGED
|
@@ -11,15 +11,19 @@ let container = null;
|
|
|
11
11
|
const getWidgetLabel = (widget) => {
|
|
12
12
|
if (!hasLabel(widget))
|
|
13
13
|
return null;
|
|
14
|
-
const
|
|
14
|
+
const accessible = getInterface(widget, Gtk.Accessible);
|
|
15
|
+
if (!accessible)
|
|
16
|
+
return null;
|
|
17
|
+
const role = accessible.getAccessibleRole();
|
|
15
18
|
if (role === Gtk.AccessibleRole.LABEL) {
|
|
16
|
-
return getInterface(widget, Gtk.Label)
|
|
19
|
+
return getInterface(widget, Gtk.Label)?.getLabel() ?? null;
|
|
17
20
|
}
|
|
18
|
-
return getInterface(widget, Gtk.Button)
|
|
21
|
+
return getInterface(widget, Gtk.Button)?.getLabel() ?? null;
|
|
19
22
|
};
|
|
20
23
|
const printWidgetTree = (root, indent = 0) => {
|
|
21
24
|
const prefix = " ".repeat(indent);
|
|
22
|
-
const
|
|
25
|
+
const accessibleRole = getInterface(root, Gtk.Accessible)?.getAccessibleRole();
|
|
26
|
+
const role = accessibleRole !== undefined ? (Gtk.AccessibleRole[accessibleRole] ?? "UNKNOWN") : "UNKNOWN";
|
|
23
27
|
const labelText = getWidgetLabel(root);
|
|
24
28
|
const label = labelText ? ` label="${labelText}"` : "";
|
|
25
29
|
let result = `${prefix}<${root.constructor.name} role=${role}${label}>\n`;
|
package/dist/screen.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import type { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
|
-
import type { ByRoleOptions, TextMatchOptions } from "./types.js";
|
|
3
|
+
import type { ByRoleOptions, TextMatch, TextMatchOptions } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Sets the root application for screen queries. Called internally by render().
|
|
6
6
|
* @param root - The GTK application to use as query root, or null to clear
|
|
@@ -13,12 +13,12 @@ export declare const setScreenRoot: (root: Gtk.Application | null) => void;
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const screen: {
|
|
15
15
|
findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
16
|
-
findByLabelText: (text:
|
|
17
|
-
findByText: (text:
|
|
18
|
-
findByTestId: (testId:
|
|
16
|
+
findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
17
|
+
findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
18
|
+
findByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
19
19
|
findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
20
|
-
findAllByLabelText: (text:
|
|
21
|
-
findAllByText: (text:
|
|
22
|
-
findAllByTestId: (testId:
|
|
20
|
+
findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
21
|
+
findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
22
|
+
findAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
23
23
|
debug: () => void;
|
|
24
24
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import type { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
3
|
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* A function that receives the text content and widget, returning true for a match.
|
|
6
|
+
* Matches React Testing Library's function matcher signature.
|
|
7
|
+
*/
|
|
8
|
+
export type TextMatchFunction = (content: string, widget: Gtk.Widget) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Flexible text matching: string for exact/partial, RegExp for patterns, or function for custom logic.
|
|
11
|
+
* Matches React Testing Library's TextMatch type.
|
|
12
|
+
*/
|
|
13
|
+
export type TextMatch = string | RegExp | TextMatchFunction;
|
|
14
|
+
/**
|
|
15
|
+
* Options for normalizing text before comparison.
|
|
16
|
+
*/
|
|
17
|
+
export interface NormalizerOptions {
|
|
18
|
+
/** Whether to trim whitespace from text. Defaults to true. */
|
|
19
|
+
trim?: boolean;
|
|
20
|
+
/** Whether to collapse multiple whitespace into single spaces. Defaults to true. */
|
|
21
|
+
collapseWhitespace?: boolean;
|
|
22
|
+
}
|
|
4
23
|
/**
|
|
5
24
|
* Options for text matching in queries.
|
|
6
25
|
*/
|
|
7
26
|
export interface TextMatchOptions {
|
|
8
27
|
/** Whether to match the entire string exactly. Defaults to true. */
|
|
9
28
|
exact?: boolean;
|
|
10
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Custom function to normalize text before comparison.
|
|
31
|
+
* Cannot be used with trim/collapseWhitespace options.
|
|
32
|
+
*/
|
|
11
33
|
normalizer?: (text: string) => string;
|
|
34
|
+
/** Whether to trim whitespace from text. Defaults to true. */
|
|
35
|
+
trim?: boolean;
|
|
36
|
+
/** Whether to collapse multiple whitespace into single spaces. Defaults to true. */
|
|
37
|
+
collapseWhitespace?: boolean;
|
|
12
38
|
/** Maximum time in milliseconds to wait for a match. */
|
|
13
39
|
timeout?: number;
|
|
14
40
|
}
|
|
@@ -16,8 +42,8 @@ export interface TextMatchOptions {
|
|
|
16
42
|
* Options for querying elements by their accessible role.
|
|
17
43
|
*/
|
|
18
44
|
export interface ByRoleOptions extends TextMatchOptions {
|
|
19
|
-
/** Filter by the element's accessible name. */
|
|
20
|
-
name?:
|
|
45
|
+
/** Filter by the element's accessible name. Supports string, RegExp, or function matcher. */
|
|
46
|
+
name?: TextMatch;
|
|
21
47
|
/** Filter checkboxes/switches by checked state. */
|
|
22
48
|
checked?: boolean;
|
|
23
49
|
/** Filter toggle buttons by pressed state. */
|
|
@@ -62,19 +88,19 @@ export interface BoundQueries {
|
|
|
62
88
|
/** Find a single element by its accessible role. */
|
|
63
89
|
findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
64
90
|
/** Find a single element by its associated label text. */
|
|
65
|
-
findByLabelText: (text:
|
|
91
|
+
findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
66
92
|
/** Find a single element by its text content. */
|
|
67
|
-
findByText: (text:
|
|
93
|
+
findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
68
94
|
/** Find a single element by its test ID. */
|
|
69
|
-
findByTestId: (testId:
|
|
95
|
+
findByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
70
96
|
/** Find all elements matching an accessible role. */
|
|
71
97
|
findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
72
98
|
/** Find all elements with matching label text. */
|
|
73
|
-
findAllByLabelText: (text:
|
|
99
|
+
findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
74
100
|
/** Find all elements with matching text content. */
|
|
75
|
-
findAllByText: (text:
|
|
101
|
+
findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
76
102
|
/** Find all elements with matching test ID. */
|
|
77
|
-
findAllByTestId: (testId:
|
|
103
|
+
findAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
78
104
|
}
|
|
79
105
|
/**
|
|
80
106
|
* The result returned by the render function. Includes query methods
|
package/dist/user-event.js
CHANGED
|
@@ -3,18 +3,29 @@ import { Accessible, AccessibleRole, DirectionType, Editable, Widget, } from "@g
|
|
|
3
3
|
import { fireEvent } from "./fire-event.js";
|
|
4
4
|
import { tick } from "./timing.js";
|
|
5
5
|
import { isEditable } from "./widget.js";
|
|
6
|
-
const TOGGLEABLE_ROLES = new Set([
|
|
6
|
+
const TOGGLEABLE_ROLES = new Set([
|
|
7
|
+
AccessibleRole.CHECKBOX,
|
|
8
|
+
AccessibleRole.RADIO,
|
|
9
|
+
AccessibleRole.TOGGLE_BUTTON,
|
|
10
|
+
AccessibleRole.SWITCH,
|
|
11
|
+
]);
|
|
7
12
|
const isToggleable = (widget) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
13
|
+
const accessible = getInterface(widget, Accessible);
|
|
14
|
+
if (!accessible)
|
|
15
|
+
return false;
|
|
16
|
+
return TOGGLEABLE_ROLES.has(accessible.getAccessibleRole());
|
|
10
17
|
};
|
|
11
18
|
const click = async (element) => {
|
|
12
19
|
if (isToggleable(element)) {
|
|
13
|
-
const role = getInterface(element, Accessible)
|
|
20
|
+
const role = getInterface(element, Accessible)?.getAccessibleRole();
|
|
14
21
|
if (role === AccessibleRole.CHECKBOX || role === AccessibleRole.RADIO) {
|
|
15
22
|
const checkButton = element;
|
|
16
23
|
checkButton.setActive(!checkButton.getActive());
|
|
17
24
|
}
|
|
25
|
+
else if (role === AccessibleRole.SWITCH) {
|
|
26
|
+
const switchWidget = element;
|
|
27
|
+
switchWidget.setActive(!switchWidget.getActive());
|
|
28
|
+
}
|
|
18
29
|
else {
|
|
19
30
|
const toggleButton = element;
|
|
20
31
|
toggleButton.setActive(!toggleButton.getActive());
|
|
@@ -42,7 +53,7 @@ const tab = async (element, options) => {
|
|
|
42
53
|
const direction = options?.shift ? DirectionType.TAB_BACKWARD : DirectionType.TAB_FORWARD;
|
|
43
54
|
const root = element.getRoot();
|
|
44
55
|
if (root) {
|
|
45
|
-
getInterface(root, Widget)
|
|
56
|
+
getInterface(root, Widget)?.childFocus(direction);
|
|
46
57
|
}
|
|
47
58
|
await tick();
|
|
48
59
|
};
|
|
@@ -51,6 +62,8 @@ const type = async (element, text) => {
|
|
|
51
62
|
throw new Error("Cannot type into element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
52
63
|
}
|
|
53
64
|
const editable = getInterface(element, Editable);
|
|
65
|
+
if (!editable)
|
|
66
|
+
return;
|
|
54
67
|
const currentText = editable.getText();
|
|
55
68
|
editable.setText(currentText + text);
|
|
56
69
|
await tick();
|
|
@@ -59,19 +72,21 @@ const clear = async (element) => {
|
|
|
59
72
|
if (!isEditable(element)) {
|
|
60
73
|
throw new Error("Cannot clear element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
61
74
|
}
|
|
62
|
-
getInterface(element, Editable)
|
|
75
|
+
getInterface(element, Editable)?.setText("");
|
|
63
76
|
await tick();
|
|
64
77
|
};
|
|
65
78
|
const SELECTABLE_ROLES = new Set([AccessibleRole.COMBO_BOX, AccessibleRole.LIST]);
|
|
66
79
|
const isSelectable = (widget) => {
|
|
67
|
-
const
|
|
68
|
-
|
|
80
|
+
const accessible = getInterface(widget, Accessible);
|
|
81
|
+
if (!accessible)
|
|
82
|
+
return false;
|
|
83
|
+
return SELECTABLE_ROLES.has(accessible.getAccessibleRole());
|
|
69
84
|
};
|
|
70
85
|
const selectOptions = async (element, values) => {
|
|
71
86
|
if (!isSelectable(element)) {
|
|
72
87
|
throw new Error("Cannot select options: element is not a selectable widget (COMBO_BOX or LIST)");
|
|
73
88
|
}
|
|
74
|
-
const role = getInterface(element, Accessible)
|
|
89
|
+
const role = getInterface(element, Accessible)?.getAccessibleRole();
|
|
75
90
|
const valueArray = Array.isArray(values) ? values : [values];
|
|
76
91
|
if (role === AccessibleRole.COMBO_BOX) {
|
|
77
92
|
if (valueArray.length > 1) {
|
|
@@ -98,13 +113,14 @@ const selectOptions = async (element, values) => {
|
|
|
98
113
|
const row = listBox.getRowAtIndex(value);
|
|
99
114
|
if (row) {
|
|
100
115
|
listBox.selectRow(row);
|
|
116
|
+
row.activate();
|
|
101
117
|
}
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
await tick();
|
|
105
121
|
};
|
|
106
122
|
const deselectOptions = async (element, values) => {
|
|
107
|
-
const role = getInterface(element, Accessible)
|
|
123
|
+
const role = getInterface(element, Accessible)?.getAccessibleRole();
|
|
108
124
|
if (role !== AccessibleRole.LIST) {
|
|
109
125
|
throw new Error("Cannot deselect options: only ListBox supports deselection");
|
|
110
126
|
}
|
package/dist/widget.js
CHANGED
|
@@ -5,8 +5,10 @@ const EDITABLE_ROLES = new Set([AccessibleRole.TEXT_BOX, AccessibleRole.SEARCH_B
|
|
|
5
5
|
* Checks if a widget has an editable accessible role (text box, search box, or spin button).
|
|
6
6
|
*/
|
|
7
7
|
export const isEditable = (widget) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const accessible = getInterface(widget, Accessible);
|
|
9
|
+
if (!accessible)
|
|
10
|
+
return false;
|
|
11
|
+
return EDITABLE_ROLES.has(accessible.getAccessibleRole());
|
|
10
12
|
};
|
|
11
13
|
const LABEL_ROLES = new Set([
|
|
12
14
|
AccessibleRole.BUTTON,
|
|
@@ -22,6 +24,8 @@ const LABEL_ROLES = new Set([
|
|
|
22
24
|
* Checks if a widget has an accessible role that supports labels.
|
|
23
25
|
*/
|
|
24
26
|
export const hasLabel = (widget) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
+
const accessible = getInterface(widget, Accessible);
|
|
28
|
+
if (!accessible)
|
|
29
|
+
return false;
|
|
30
|
+
return LABEL_ROLES.has(accessible.getAccessibleRole());
|
|
27
31
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Testing utilities for GTKX applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@gtkx/
|
|
36
|
-
"@gtkx/
|
|
37
|
-
"@gtkx/
|
|
35
|
+
"@gtkx/ffi": "0.4.0",
|
|
36
|
+
"@gtkx/native": "0.4.0",
|
|
37
|
+
"@gtkx/react": "0.4.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b && cp ../../README.md .",
|