@gtkx/testing 0.1.49 → 0.1.51
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/README.md +7 -12
- package/dist/index.d.ts +1 -1
- package/dist/queries.d.ts +6 -6
- package/dist/queries.js +68 -34
- package/dist/render.js +13 -5
- package/dist/types.d.ts +46 -0
- package/dist/user-event.d.ts +8 -21
- package/dist/user-event.js +132 -62
- package/dist/widget.d.ts +5 -9
- package/dist/widget.js +21 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -159,7 +159,9 @@ Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
|
159
159
|
- `userEvent.activate(element)` - Activate element (e.g., press Enter in input)
|
|
160
160
|
- `userEvent.type(element, text)` - Type text into input
|
|
161
161
|
- `userEvent.clear(element)` - Clear input text
|
|
162
|
-
- `userEvent.
|
|
162
|
+
- `userEvent.tab(element, options?)` - Simulate Tab navigation
|
|
163
|
+
- `userEvent.selectOptions(element, values)` - Select options in ComboBox/ListBox
|
|
164
|
+
- `userEvent.deselectOptions(element, values)` - Deselect options in ListBox
|
|
163
165
|
|
|
164
166
|
**Low-level Events**:
|
|
165
167
|
- `fireEvent(element, signalName, ...args)` - Emit any GTK signal with optional arguments
|
|
@@ -170,14 +172,6 @@ Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
|
170
172
|
|
|
171
173
|
## Examples
|
|
172
174
|
|
|
173
|
-
### Counter
|
|
174
|
-
|
|
175
|
-
A minimal counter app demonstrating state management:
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
turbo start --filter=counter-example
|
|
179
|
-
```
|
|
180
|
-
|
|
181
175
|
### GTK4 Demo
|
|
182
176
|
|
|
183
177
|
A comprehensive showcase of GTK4 widgets and features:
|
|
@@ -186,12 +180,13 @@ A comprehensive showcase of GTK4 widgets and features:
|
|
|
186
180
|
turbo start --filter=gtk4-demo
|
|
187
181
|
```
|
|
188
182
|
|
|
189
|
-
###
|
|
183
|
+
### Todo App
|
|
190
184
|
|
|
191
|
-
|
|
185
|
+
A todo app demonstrating `@gtkx/testing` with realistic component tests:
|
|
192
186
|
|
|
193
187
|
```bash
|
|
194
|
-
turbo start --filter=
|
|
188
|
+
turbo start --filter=todo
|
|
189
|
+
turbo test --filter=todo
|
|
195
190
|
```
|
|
196
191
|
|
|
197
192
|
## Packages
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, find
|
|
|
3
3
|
export { cleanup, render, teardown } from "./render.js";
|
|
4
4
|
export { screen } from "./screen.js";
|
|
5
5
|
export type { BoundQueries, ByRoleOptions, RenderOptions, RenderResult, TextMatchOptions, WaitForOptions, } from "./types.js";
|
|
6
|
-
export type {
|
|
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";
|
|
9
9
|
export { within } from "./within.js";
|
package/dist/queries.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare const findAllByRole: (container: Container, role: AccessibleRole,
|
|
|
22
22
|
* Waits for and finds a single widget matching the specified label text.
|
|
23
23
|
* @param container - The container to search within
|
|
24
24
|
* @param text - The text or pattern to match
|
|
25
|
-
* @param options - Text matching options (exact, normalizer)
|
|
25
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
26
26
|
* @returns Promise resolving to the matching widget
|
|
27
27
|
*/
|
|
28
28
|
export declare const findByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
@@ -30,7 +30,7 @@ export declare const findByLabelText: (container: Container, text: string | RegE
|
|
|
30
30
|
* Waits for and finds all widgets matching the specified label text.
|
|
31
31
|
* @param container - The container to search within
|
|
32
32
|
* @param text - The text or pattern to match
|
|
33
|
-
* @param options - Text matching options (exact, normalizer)
|
|
33
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
34
34
|
* @returns Promise resolving to array of matching widgets
|
|
35
35
|
*/
|
|
36
36
|
export declare const findAllByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
@@ -38,7 +38,7 @@ export declare const findAllByLabelText: (container: Container, text: string | R
|
|
|
38
38
|
* Waits for and finds a single widget matching the specified text content.
|
|
39
39
|
* @param container - The container to search within
|
|
40
40
|
* @param text - The text or pattern to match
|
|
41
|
-
* @param options - Text matching options (exact, normalizer)
|
|
41
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
42
42
|
* @returns Promise resolving to the matching widget
|
|
43
43
|
*/
|
|
44
44
|
export declare const findByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
@@ -46,7 +46,7 @@ export declare const findByText: (container: Container, text: string | RegExp, o
|
|
|
46
46
|
* Waits for and finds all widgets matching the specified text content.
|
|
47
47
|
* @param container - The container to search within
|
|
48
48
|
* @param text - The text or pattern to match
|
|
49
|
-
* @param options - Text matching options (exact, normalizer)
|
|
49
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
50
50
|
* @returns Promise resolving to array of matching widgets
|
|
51
51
|
*/
|
|
52
52
|
export declare const findAllByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
@@ -54,7 +54,7 @@ export declare const findAllByText: (container: Container, text: string | RegExp
|
|
|
54
54
|
* Waits for and finds a single widget matching the specified test ID.
|
|
55
55
|
* @param container - The container to search within
|
|
56
56
|
* @param testId - The test ID or pattern to match
|
|
57
|
-
* @param options - Text matching options (exact, normalizer)
|
|
57
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
58
58
|
* @returns Promise resolving to the matching widget
|
|
59
59
|
*/
|
|
60
60
|
export declare const findByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
@@ -62,7 +62,7 @@ export declare const findByTestId: (container: Container, testId: string | RegEx
|
|
|
62
62
|
* Waits for and finds all widgets matching the specified test ID.
|
|
63
63
|
* @param container - The container to search within
|
|
64
64
|
* @param testId - The test ID or pattern to match
|
|
65
|
-
* @param options - Text matching options (exact, normalizer)
|
|
65
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
66
66
|
* @returns Promise resolving to array of matching widgets
|
|
67
67
|
*/
|
|
68
68
|
export declare const findAllByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
package/dist/queries.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getObject } from "@gtkx/ffi";
|
|
2
|
-
import { AccessibleRole, Button, CheckButton, Expander, Label, ToggleButton } from "@gtkx/ffi/gtk";
|
|
2
|
+
import { AccessibleRole, Button, CheckButton, Editable, Expander, Frame, Label, StackPage, Switch, ToggleButton, Window, } from "@gtkx/ffi/gtk";
|
|
3
3
|
import { findAll } from "./traversal.js";
|
|
4
4
|
import { waitFor } from "./wait-for.js";
|
|
5
|
+
import { asAccessible } from "./widget.js";
|
|
5
6
|
const DEFAULT_NORMALIZER = (text) => text.trim().replace(/\s+/g, " ");
|
|
6
7
|
const normalizeText = (text, options) => {
|
|
7
8
|
const normalizer = options?.normalizer ?? DEFAULT_NORMALIZER;
|
|
@@ -18,12 +19,25 @@ const matchText = (actual, expected, options) => {
|
|
|
18
19
|
}
|
|
19
20
|
return expected.test(normalizedActual);
|
|
20
21
|
};
|
|
21
|
-
const asAccessible = (widget) => widget;
|
|
22
22
|
const asButton = (widget) => getObject(widget.ptr, Button);
|
|
23
23
|
const asLabel = (widget) => getObject(widget.ptr, Label);
|
|
24
24
|
const asCheckButton = (widget) => getObject(widget.ptr, CheckButton);
|
|
25
25
|
const asToggleButton = (widget) => getObject(widget.ptr, ToggleButton);
|
|
26
26
|
const asExpander = (widget) => getObject(widget.ptr, Expander);
|
|
27
|
+
const asFrame = (widget) => getObject(widget.ptr, Frame);
|
|
28
|
+
const asWindow = (widget) => getObject(widget.ptr, Window);
|
|
29
|
+
const asStackPage = (widget) => getObject(widget.ptr, StackPage);
|
|
30
|
+
const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
31
|
+
AccessibleRole.BUTTON,
|
|
32
|
+
AccessibleRole.TOGGLE_BUTTON,
|
|
33
|
+
AccessibleRole.CHECKBOX,
|
|
34
|
+
AccessibleRole.RADIO,
|
|
35
|
+
AccessibleRole.MENU_ITEM,
|
|
36
|
+
AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
37
|
+
AccessibleRole.MENU_ITEM_RADIO,
|
|
38
|
+
AccessibleRole.TAB,
|
|
39
|
+
AccessibleRole.LINK,
|
|
40
|
+
]);
|
|
27
41
|
const isInternalLabel = (widget) => {
|
|
28
42
|
const accessible = asAccessible(widget);
|
|
29
43
|
if (accessible.getAccessibleRole() !== AccessibleRole.LABEL)
|
|
@@ -32,13 +46,7 @@ const isInternalLabel = (widget) => {
|
|
|
32
46
|
if (!parent)
|
|
33
47
|
return false;
|
|
34
48
|
const parentRole = asAccessible(parent).getAccessibleRole();
|
|
35
|
-
return (parentRole
|
|
36
|
-
parentRole === AccessibleRole.TOGGLE_BUTTON ||
|
|
37
|
-
parentRole === AccessibleRole.CHECKBOX ||
|
|
38
|
-
parentRole === AccessibleRole.RADIO ||
|
|
39
|
-
parentRole === AccessibleRole.MENU_ITEM ||
|
|
40
|
-
parentRole === AccessibleRole.MENU_ITEM_CHECKBOX ||
|
|
41
|
-
parentRole === AccessibleRole.MENU_ITEM_RADIO);
|
|
49
|
+
return ROLES_WITH_INTERNAL_LABELS.has(parentRole);
|
|
42
50
|
};
|
|
43
51
|
const getWidgetText = (widget) => {
|
|
44
52
|
if (isInternalLabel(widget))
|
|
@@ -46,19 +54,40 @@ const getWidgetText = (widget) => {
|
|
|
46
54
|
const role = asAccessible(widget).getAccessibleRole();
|
|
47
55
|
switch (role) {
|
|
48
56
|
case AccessibleRole.BUTTON:
|
|
57
|
+
case AccessibleRole.LINK:
|
|
58
|
+
case AccessibleRole.TAB:
|
|
59
|
+
return asButton(widget).getLabel();
|
|
49
60
|
case AccessibleRole.TOGGLE_BUTTON:
|
|
61
|
+
return asToggleButton(widget).getLabel();
|
|
50
62
|
case AccessibleRole.CHECKBOX:
|
|
51
63
|
case AccessibleRole.RADIO:
|
|
52
|
-
|
|
53
|
-
case AccessibleRole.MENU_ITEM_CHECKBOX:
|
|
54
|
-
case AccessibleRole.MENU_ITEM_RADIO:
|
|
55
|
-
return asButton(widget).getLabel();
|
|
64
|
+
return asCheckButton(widget).getLabel();
|
|
56
65
|
case AccessibleRole.LABEL:
|
|
57
66
|
return asLabel(widget).getLabel();
|
|
58
67
|
case AccessibleRole.TEXT_BOX:
|
|
59
68
|
case AccessibleRole.SEARCH_BOX:
|
|
60
69
|
case AccessibleRole.SPIN_BUTTON:
|
|
61
|
-
return widget.getText();
|
|
70
|
+
return getObject(widget.ptr, Editable).getText();
|
|
71
|
+
case AccessibleRole.GROUP:
|
|
72
|
+
try {
|
|
73
|
+
return asFrame(widget).getLabel();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
case AccessibleRole.WINDOW:
|
|
79
|
+
case AccessibleRole.DIALOG:
|
|
80
|
+
case AccessibleRole.ALERT_DIALOG:
|
|
81
|
+
return asWindow(widget).getTitle();
|
|
82
|
+
case AccessibleRole.TAB_PANEL:
|
|
83
|
+
try {
|
|
84
|
+
return asStackPage(widget).getTitle();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
case AccessibleRole.SWITCH:
|
|
90
|
+
return null;
|
|
62
91
|
default:
|
|
63
92
|
return null;
|
|
64
93
|
}
|
|
@@ -66,15 +95,20 @@ const getWidgetText = (widget) => {
|
|
|
66
95
|
const getWidgetTestId = (widget) => {
|
|
67
96
|
return widget.getName();
|
|
68
97
|
};
|
|
98
|
+
const asSwitch = (widget) => getObject(widget.ptr, Switch);
|
|
69
99
|
const getWidgetCheckedState = (widget) => {
|
|
70
100
|
const role = asAccessible(widget).getAccessibleRole();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
switch (role) {
|
|
102
|
+
case AccessibleRole.CHECKBOX:
|
|
103
|
+
case AccessibleRole.RADIO:
|
|
104
|
+
return asCheckButton(widget).getActive();
|
|
105
|
+
case AccessibleRole.TOGGLE_BUTTON:
|
|
106
|
+
return asToggleButton(widget).getActive();
|
|
107
|
+
case AccessibleRole.SWITCH:
|
|
108
|
+
return asSwitch(widget).getActive();
|
|
109
|
+
default:
|
|
110
|
+
return undefined;
|
|
76
111
|
}
|
|
77
|
-
return undefined;
|
|
78
112
|
};
|
|
79
113
|
const getWidgetExpandedState = (widget) => {
|
|
80
114
|
const role = asAccessible(widget).getAccessibleRole();
|
|
@@ -211,7 +245,7 @@ const getByTestId = (container, testId, options) => {
|
|
|
211
245
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
212
246
|
* @returns Promise resolving to the matching widget
|
|
213
247
|
*/
|
|
214
|
-
export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options));
|
|
248
|
+
export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options), { timeout: options?.timeout });
|
|
215
249
|
/**
|
|
216
250
|
* Waits for and finds all widgets matching the specified accessible role.
|
|
217
251
|
* @param container - The container to search within
|
|
@@ -219,52 +253,52 @@ export const findByRole = async (container, role, options) => waitFor(() => getB
|
|
|
219
253
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
220
254
|
* @returns Promise resolving to array of matching widgets
|
|
221
255
|
*/
|
|
222
|
-
export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options));
|
|
256
|
+
export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options), { timeout: options?.timeout });
|
|
223
257
|
/**
|
|
224
258
|
* Waits for and finds a single widget matching the specified label text.
|
|
225
259
|
* @param container - The container to search within
|
|
226
260
|
* @param text - The text or pattern to match
|
|
227
|
-
* @param options - Text matching options (exact, normalizer)
|
|
261
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
228
262
|
* @returns Promise resolving to the matching widget
|
|
229
263
|
*/
|
|
230
|
-
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options));
|
|
264
|
+
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options), { timeout: options?.timeout });
|
|
231
265
|
/**
|
|
232
266
|
* Waits for and finds all widgets matching the specified label text.
|
|
233
267
|
* @param container - The container to search within
|
|
234
268
|
* @param text - The text or pattern to match
|
|
235
|
-
* @param options - Text matching options (exact, normalizer)
|
|
269
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
236
270
|
* @returns Promise resolving to array of matching widgets
|
|
237
271
|
*/
|
|
238
|
-
export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options));
|
|
272
|
+
export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options), { timeout: options?.timeout });
|
|
239
273
|
/**
|
|
240
274
|
* Waits for and finds a single widget matching the specified text content.
|
|
241
275
|
* @param container - The container to search within
|
|
242
276
|
* @param text - The text or pattern to match
|
|
243
|
-
* @param options - Text matching options (exact, normalizer)
|
|
277
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
244
278
|
* @returns Promise resolving to the matching widget
|
|
245
279
|
*/
|
|
246
|
-
export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options));
|
|
280
|
+
export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options), { timeout: options?.timeout });
|
|
247
281
|
/**
|
|
248
282
|
* Waits for and finds all widgets matching the specified text content.
|
|
249
283
|
* @param container - The container to search within
|
|
250
284
|
* @param text - The text or pattern to match
|
|
251
|
-
* @param options - Text matching options (exact, normalizer)
|
|
285
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
252
286
|
* @returns Promise resolving to array of matching widgets
|
|
253
287
|
*/
|
|
254
|
-
export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options));
|
|
288
|
+
export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options), { timeout: options?.timeout });
|
|
255
289
|
/**
|
|
256
290
|
* Waits for and finds a single widget matching the specified test ID.
|
|
257
291
|
* @param container - The container to search within
|
|
258
292
|
* @param testId - The test ID or pattern to match
|
|
259
|
-
* @param options - Text matching options (exact, normalizer)
|
|
293
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
260
294
|
* @returns Promise resolving to the matching widget
|
|
261
295
|
*/
|
|
262
|
-
export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options));
|
|
296
|
+
export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options), { timeout: options?.timeout });
|
|
263
297
|
/**
|
|
264
298
|
* Waits for and finds all widgets matching the specified test ID.
|
|
265
299
|
* @param container - The container to search within
|
|
266
300
|
* @param testId - The test ID or pattern to match
|
|
267
|
-
* @param options - Text matching options (exact, normalizer)
|
|
301
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
268
302
|
* @returns Promise resolving to array of matching widgets
|
|
269
303
|
*/
|
|
270
|
-
export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options));
|
|
304
|
+
export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options), { timeout: options?.timeout });
|
package/dist/render.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { getCurrentApp, start, stop } from "@gtkx/ffi";
|
|
2
|
+
import { getCurrentApp, getObject, start, stop } from "@gtkx/ffi";
|
|
3
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
4
|
import { ApplicationWindow, reconciler } from "@gtkx/react";
|
|
5
5
|
import * as queries from "./queries.js";
|
|
6
6
|
import { setScreenRoot } from "./screen.js";
|
|
7
7
|
import { tick } from "./timing.js";
|
|
8
|
+
import { asAccessible, hasLabel } from "./widget.js";
|
|
8
9
|
const ROOT_NODE_CONTAINER = Symbol.for("ROOT_NODE_CONTAINER");
|
|
9
10
|
const APP_ID = "com.gtkx.testing";
|
|
10
11
|
let container = null;
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
const getWidgetLabel = (widget) => {
|
|
13
|
+
if (!hasLabel(widget))
|
|
14
|
+
return null;
|
|
15
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
16
|
+
if (role === Gtk.AccessibleRole.LABEL) {
|
|
17
|
+
return getObject(widget.ptr, Gtk.Label).getLabel();
|
|
18
|
+
}
|
|
19
|
+
return getObject(widget.ptr, Gtk.Button).getLabel();
|
|
20
|
+
};
|
|
13
21
|
const printWidgetTree = (root, indent = 0) => {
|
|
14
22
|
const prefix = " ".repeat(indent);
|
|
15
23
|
const role = Gtk.AccessibleRole[asAccessible(root).getAccessibleRole()] ?? "UNKNOWN";
|
|
16
|
-
const
|
|
24
|
+
const labelText = getWidgetLabel(root);
|
|
25
|
+
const label = labelText ? ` label="${labelText}"` : "";
|
|
17
26
|
let result = `${prefix}<${root.constructor.name} role=${role}${label}>\n`;
|
|
18
27
|
let child = root.getFirstChild();
|
|
19
28
|
while (child) {
|
|
@@ -94,5 +103,4 @@ export const cleanup = async () => {
|
|
|
94
103
|
export const teardown = async () => {
|
|
95
104
|
await cleanup();
|
|
96
105
|
stop();
|
|
97
|
-
container = null;
|
|
98
106
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,41 +1,87 @@
|
|
|
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
|
+
* Options for text matching in queries.
|
|
6
|
+
*/
|
|
4
7
|
export interface TextMatchOptions {
|
|
8
|
+
/** Whether to match the entire string exactly. Defaults to true. */
|
|
5
9
|
exact?: boolean;
|
|
10
|
+
/** Custom function to normalize text before comparison. */
|
|
6
11
|
normalizer?: (text: string) => string;
|
|
12
|
+
/** Maximum time in milliseconds to wait for a match. */
|
|
13
|
+
timeout?: number;
|
|
7
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Options for querying elements by their accessible role.
|
|
17
|
+
*/
|
|
8
18
|
export interface ByRoleOptions extends TextMatchOptions {
|
|
19
|
+
/** Filter by the element's accessible name. */
|
|
9
20
|
name?: string | RegExp;
|
|
21
|
+
/** Filter checkboxes/switches by checked state. */
|
|
10
22
|
checked?: boolean;
|
|
23
|
+
/** Filter toggle buttons by pressed state. */
|
|
11
24
|
pressed?: boolean;
|
|
25
|
+
/** Filter selectable items by selected state. */
|
|
12
26
|
selected?: boolean;
|
|
27
|
+
/** Filter expandable elements by expanded state. */
|
|
13
28
|
expanded?: boolean;
|
|
29
|
+
/** Filter headings by their level (1-6). */
|
|
14
30
|
level?: number;
|
|
15
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for waitFor and related async utilities.
|
|
34
|
+
*/
|
|
16
35
|
export interface WaitForOptions {
|
|
36
|
+
/** Maximum time in milliseconds to wait. Defaults to 1000ms. */
|
|
17
37
|
timeout?: number;
|
|
38
|
+
/** Interval in milliseconds between condition checks. Defaults to 50ms. */
|
|
18
39
|
interval?: number;
|
|
40
|
+
/** Custom error handler called when timeout is reached. */
|
|
19
41
|
onTimeout?: (error: Error) => Error;
|
|
20
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Options for the render function.
|
|
45
|
+
*/
|
|
21
46
|
export interface RenderOptions {
|
|
47
|
+
/** A React component to wrap the rendered element. Useful for providing context. */
|
|
22
48
|
wrapper?: ComponentType<{
|
|
23
49
|
children: ReactNode;
|
|
24
50
|
}>;
|
|
25
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Query methods bound to a specific container. All queries return promises
|
|
54
|
+
* that resolve when a matching element is found or reject on timeout.
|
|
55
|
+
*/
|
|
26
56
|
export interface BoundQueries {
|
|
57
|
+
/** Find a single element by its accessible role. */
|
|
27
58
|
findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
59
|
+
/** Find a single element by its associated label text. */
|
|
28
60
|
findByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
61
|
+
/** Find a single element by its text content. */
|
|
29
62
|
findByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
63
|
+
/** Find a single element by its test ID. */
|
|
30
64
|
findByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
65
|
+
/** Find all elements matching an accessible role. */
|
|
31
66
|
findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
67
|
+
/** Find all elements with matching label text. */
|
|
32
68
|
findAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
69
|
+
/** Find all elements with matching text content. */
|
|
33
70
|
findAllByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
71
|
+
/** Find all elements with matching test ID. */
|
|
34
72
|
findAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
35
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The result returned by the render function. Includes query methods
|
|
76
|
+
* and utilities for interacting with the rendered component.
|
|
77
|
+
*/
|
|
36
78
|
export interface RenderResult extends BoundQueries {
|
|
79
|
+
/** The GTK Application instance containing the rendered component. */
|
|
37
80
|
container: Gtk.Application;
|
|
81
|
+
/** Unmount the rendered component and clean up resources. */
|
|
38
82
|
unmount: () => Promise<void>;
|
|
83
|
+
/** Re-render with a new element, preserving state where possible. */
|
|
39
84
|
rerender: (element: ReactNode) => Promise<void>;
|
|
85
|
+
/** Print the current widget tree to the console for debugging. */
|
|
40
86
|
debug: () => void;
|
|
41
87
|
}
|
package/dist/user-event.d.ts
CHANGED
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
/**
|
|
3
|
-
* Options for
|
|
3
|
+
* Options for the tab user event.
|
|
4
4
|
*/
|
|
5
|
-
export interface
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Instance returned by userEvent.setup() with bound options.
|
|
11
|
-
*/
|
|
12
|
-
export interface UserEventInstance {
|
|
13
|
-
/** Simulates a click on the element */
|
|
14
|
-
click: (element: Gtk.Widget) => Promise<void>;
|
|
15
|
-
/** Simulates a double-click on the element */
|
|
16
|
-
dblClick: (element: Gtk.Widget) => Promise<void>;
|
|
17
|
-
/** Activates the element (e.g., pressing Enter in an Entry) */
|
|
18
|
-
activate: (element: Gtk.Widget) => Promise<void>;
|
|
19
|
-
/** Types text into an input element */
|
|
20
|
-
type: (element: Gtk.Widget, text: string) => Promise<void>;
|
|
21
|
-
/** Clears the text content of an input element */
|
|
22
|
-
clear: (element: Gtk.Widget) => Promise<void>;
|
|
5
|
+
export interface TabOptions {
|
|
6
|
+
/** If true, navigates backwards (Shift+Tab behavior). */
|
|
7
|
+
shift?: boolean;
|
|
23
8
|
}
|
|
24
9
|
/**
|
|
25
10
|
* Simulates user interactions with GTK widgets. Provides methods that mimic
|
|
26
11
|
* real user behavior like clicking, typing, and clearing input fields.
|
|
27
|
-
* Use userEvent.setup() to create an instance with custom options.
|
|
28
12
|
*/
|
|
29
13
|
export declare const userEvent: {
|
|
30
|
-
setup: (options?: UserEventOptions) => UserEventInstance;
|
|
31
14
|
click: (element: Gtk.Widget) => Promise<void>;
|
|
32
15
|
dblClick: (element: Gtk.Widget) => Promise<void>;
|
|
16
|
+
tripleClick: (element: Gtk.Widget) => Promise<void>;
|
|
33
17
|
activate: (element: Gtk.Widget) => Promise<void>;
|
|
18
|
+
tab: (element: Gtk.Widget, options?: TabOptions) => Promise<void>;
|
|
34
19
|
type: (element: Gtk.Widget, text: string) => Promise<void>;
|
|
35
20
|
clear: (element: Gtk.Widget) => Promise<void>;
|
|
21
|
+
selectOptions: (element: Gtk.Widget, values: string | string[] | number | number[]) => Promise<void>;
|
|
22
|
+
deselectOptions: (element: Gtk.Widget, values: number | number[]) => Promise<void>;
|
|
36
23
|
};
|
package/dist/user-event.js
CHANGED
|
@@ -1,73 +1,143 @@
|
|
|
1
|
+
import { getObject } from "@gtkx/ffi";
|
|
2
|
+
import { AccessibleRole, CheckButton, ComboBox, DirectionType, DropDown, Editable, ListBox, ListBoxRow, ToggleButton, Widget, } from "@gtkx/ffi/gtk";
|
|
1
3
|
import { fireEvent } from "./fire-event.js";
|
|
2
4
|
import { tick } from "./timing.js";
|
|
3
|
-
import {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
element.
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
import { asAccessible, isEditable } from "./widget.js";
|
|
6
|
+
const TOGGLEABLE_ROLES = new Set([AccessibleRole.CHECKBOX, AccessibleRole.RADIO, AccessibleRole.TOGGLE_BUTTON]);
|
|
7
|
+
const isToggleable = (widget) => {
|
|
8
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
9
|
+
return TOGGLEABLE_ROLES.has(role);
|
|
10
|
+
};
|
|
11
|
+
const click = async (element) => {
|
|
12
|
+
if (isToggleable(element)) {
|
|
13
|
+
const role = asAccessible(element).getAccessibleRole();
|
|
14
|
+
if (role === AccessibleRole.CHECKBOX || role === AccessibleRole.RADIO) {
|
|
15
|
+
const checkButton = getObject(element.ptr, CheckButton);
|
|
16
|
+
checkButton.setActive(!checkButton.getActive());
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const toggleButton = getObject(element.ptr, ToggleButton);
|
|
20
|
+
toggleButton.setActive(!toggleButton.getActive());
|
|
21
|
+
}
|
|
22
|
+
// Note: setActive() automatically emits the "toggled" signal, so we don't need to emit it manually
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
fireEvent(element, "clicked");
|
|
26
|
+
}
|
|
27
|
+
await tick();
|
|
28
|
+
};
|
|
29
|
+
const dblClick = async (element) => {
|
|
30
|
+
fireEvent(element, "clicked");
|
|
31
|
+
await tick();
|
|
32
|
+
fireEvent(element, "clicked");
|
|
33
|
+
await tick();
|
|
34
|
+
};
|
|
35
|
+
const tripleClick = async (element) => {
|
|
36
|
+
fireEvent(element, "clicked");
|
|
37
|
+
await tick();
|
|
38
|
+
fireEvent(element, "clicked");
|
|
39
|
+
await tick();
|
|
40
|
+
fireEvent(element, "clicked");
|
|
41
|
+
await tick();
|
|
42
|
+
};
|
|
43
|
+
const activate = async (element) => {
|
|
44
|
+
element.activate();
|
|
45
|
+
await tick();
|
|
46
|
+
};
|
|
47
|
+
const tab = async (element, options) => {
|
|
48
|
+
const direction = options?.shift ? DirectionType.TAB_BACKWARD : DirectionType.TAB_FORWARD;
|
|
49
|
+
const root = element.getRoot();
|
|
50
|
+
if (root) {
|
|
51
|
+
const rootWidget = getObject(root.ptr, Widget);
|
|
52
|
+
rootWidget.childFocus(direction);
|
|
53
|
+
}
|
|
54
|
+
await tick();
|
|
55
|
+
};
|
|
56
|
+
const type = async (element, text) => {
|
|
57
|
+
if (!isEditable(element)) {
|
|
58
|
+
throw new Error("Cannot type into element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
59
|
+
}
|
|
60
|
+
const editable = getObject(element.ptr, Editable);
|
|
61
|
+
const currentText = editable.getText();
|
|
62
|
+
editable.setText(currentText + text);
|
|
63
|
+
await tick();
|
|
64
|
+
};
|
|
65
|
+
const clear = async (element) => {
|
|
66
|
+
if (!isEditable(element)) {
|
|
67
|
+
throw new Error("Cannot clear element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
68
|
+
}
|
|
69
|
+
const editable = getObject(element.ptr, Editable);
|
|
70
|
+
editable.setText("");
|
|
71
|
+
await tick();
|
|
72
|
+
};
|
|
73
|
+
const SELECTABLE_ROLES = new Set([AccessibleRole.COMBO_BOX, AccessibleRole.LIST]);
|
|
74
|
+
const isSelectable = (widget) => {
|
|
75
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
76
|
+
return SELECTABLE_ROLES.has(role);
|
|
77
|
+
};
|
|
78
|
+
const selectOptions = async (element, values) => {
|
|
79
|
+
if (!isSelectable(element)) {
|
|
80
|
+
throw new Error("Cannot select options: element is not a selectable widget (COMBO_BOX or LIST)");
|
|
81
|
+
}
|
|
82
|
+
const role = asAccessible(element).getAccessibleRole();
|
|
83
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
84
|
+
if (role === AccessibleRole.COMBO_BOX) {
|
|
85
|
+
if (valueArray.length > 1) {
|
|
86
|
+
throw new Error("Cannot select multiple options on a ComboBox/DropDown");
|
|
87
|
+
}
|
|
88
|
+
const value = valueArray[0];
|
|
89
|
+
if (typeof value !== "number") {
|
|
90
|
+
throw new Error("ComboBox/DropDown selection requires a numeric index");
|
|
91
|
+
}
|
|
92
|
+
const isDropDown = element.constructor.name === "DropDown";
|
|
93
|
+
if (isDropDown) {
|
|
94
|
+
getObject(element.ptr, DropDown).setSelected(value);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
getObject(element.ptr, ComboBox).setActive(value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (role === AccessibleRole.LIST) {
|
|
101
|
+
const listBox = getObject(element.ptr, ListBox);
|
|
102
|
+
for (const value of valueArray) {
|
|
103
|
+
if (typeof value !== "number") {
|
|
104
|
+
throw new Error("ListBox selection requires numeric indices");
|
|
23
105
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
clear: async (element) => {
|
|
29
|
-
if (!hasSetText(element)) {
|
|
30
|
-
throw new Error("Cannot clear element: no setText method available");
|
|
106
|
+
const row = listBox.getRowAtIndex(value);
|
|
107
|
+
if (row) {
|
|
108
|
+
listBox.selectRow(row);
|
|
31
109
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
await tick();
|
|
113
|
+
};
|
|
114
|
+
const deselectOptions = async (element, values) => {
|
|
115
|
+
const role = asAccessible(element).getAccessibleRole();
|
|
116
|
+
if (role !== AccessibleRole.LIST) {
|
|
117
|
+
throw new Error("Cannot deselect options: only ListBox supports deselection");
|
|
118
|
+
}
|
|
119
|
+
const listBox = getObject(element.ptr, ListBox);
|
|
120
|
+
const valueArray = Array.isArray(values) ? values : [values];
|
|
121
|
+
for (const value of valueArray) {
|
|
122
|
+
const row = listBox.getRowAtIndex(value);
|
|
123
|
+
if (row) {
|
|
124
|
+
listBox.unselectRow(getObject(row.ptr, ListBoxRow));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await tick();
|
|
36
128
|
};
|
|
37
129
|
/**
|
|
38
130
|
* Simulates user interactions with GTK widgets. Provides methods that mimic
|
|
39
131
|
* real user behavior like clicking, typing, and clearing input fields.
|
|
40
|
-
* Use userEvent.setup() to create an instance with custom options.
|
|
41
132
|
*/
|
|
42
133
|
export const userEvent = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await tick();
|
|
53
|
-
},
|
|
54
|
-
activate: async (element) => {
|
|
55
|
-
element.activate();
|
|
56
|
-
await tick();
|
|
57
|
-
},
|
|
58
|
-
type: async (element, text) => {
|
|
59
|
-
if (!hasSetText(element)) {
|
|
60
|
-
throw new Error("Cannot type into element: no setText method available");
|
|
61
|
-
}
|
|
62
|
-
const currentText = hasGetText(element) ? element.getText() : "";
|
|
63
|
-
element.setText(currentText + text);
|
|
64
|
-
await tick();
|
|
65
|
-
},
|
|
66
|
-
clear: async (element) => {
|
|
67
|
-
if (!hasSetText(element)) {
|
|
68
|
-
throw new Error("Cannot clear element: no setText method available");
|
|
69
|
-
}
|
|
70
|
-
element.setText("");
|
|
71
|
-
await tick();
|
|
72
|
-
},
|
|
134
|
+
click,
|
|
135
|
+
dblClick,
|
|
136
|
+
tripleClick,
|
|
137
|
+
activate,
|
|
138
|
+
tab,
|
|
139
|
+
type,
|
|
140
|
+
clear,
|
|
141
|
+
selectOptions,
|
|
142
|
+
deselectOptions,
|
|
73
143
|
};
|
package/dist/widget.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
export declare const hasSetText: (widget: unknown) => widget is WidgetWithSetText;
|
|
8
|
-
export declare const hasGetText: (widget: unknown) => widget is WidgetWithGetText;
|
|
9
|
-
export {};
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { type Accessible } from "@gtkx/ffi/gtk";
|
|
3
|
+
export declare const asAccessible: (widget: Gtk.Widget) => Accessible;
|
|
4
|
+
export declare const isEditable: (widget: Gtk.Widget) => boolean;
|
|
5
|
+
export declare const hasLabel: (widget: Gtk.Widget) => boolean;
|
package/dist/widget.js
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
export const
|
|
1
|
+
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
2
|
+
export const asAccessible = (widget) => widget;
|
|
3
|
+
const EDITABLE_ROLES = new Set([AccessibleRole.TEXT_BOX, AccessibleRole.SEARCH_BOX, AccessibleRole.SPIN_BUTTON]);
|
|
4
|
+
export const isEditable = (widget) => {
|
|
5
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
6
|
+
return EDITABLE_ROLES.has(role);
|
|
7
|
+
};
|
|
8
|
+
const LABEL_ROLES = new Set([
|
|
9
|
+
AccessibleRole.BUTTON,
|
|
10
|
+
AccessibleRole.TOGGLE_BUTTON,
|
|
11
|
+
AccessibleRole.CHECKBOX,
|
|
12
|
+
AccessibleRole.RADIO,
|
|
13
|
+
AccessibleRole.LABEL,
|
|
14
|
+
AccessibleRole.MENU_ITEM,
|
|
15
|
+
AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
16
|
+
AccessibleRole.MENU_ITEM_RADIO,
|
|
17
|
+
]);
|
|
18
|
+
export const hasLabel = (widget) => {
|
|
19
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
20
|
+
return LABEL_ROLES.has(role);
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
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/ffi": "0.1.
|
|
36
|
-
"@gtkx/
|
|
37
|
-
"@gtkx/
|
|
35
|
+
"@gtkx/ffi": "0.1.51",
|
|
36
|
+
"@gtkx/react": "0.1.51",
|
|
37
|
+
"@gtkx/native": "0.1.51"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b && cp ../../README.md .",
|