@gtkx/testing 0.1.48 → 0.1.50
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 +90 -48
- package/dist/render.js +16 -10
- package/dist/timing.d.ts +1 -0
- package/dist/timing.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/user-event.d.ts +7 -23
- package/dist/user-event.js +133 -63
- 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;
|
|
@@ -23,43 +24,70 @@ const asLabel = (widget) => getObject(widget.ptr, Label);
|
|
|
23
24
|
const asCheckButton = (widget) => getObject(widget.ptr, CheckButton);
|
|
24
25
|
const asToggleButton = (widget) => getObject(widget.ptr, ToggleButton);
|
|
25
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
|
+
]);
|
|
26
41
|
const isInternalLabel = (widget) => {
|
|
27
|
-
const accessible = widget;
|
|
42
|
+
const accessible = asAccessible(widget);
|
|
28
43
|
if (accessible.getAccessibleRole() !== AccessibleRole.LABEL)
|
|
29
44
|
return false;
|
|
30
45
|
const parent = widget.getParent();
|
|
31
46
|
if (!parent)
|
|
32
47
|
return false;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
return (parentRole === AccessibleRole.BUTTON ||
|
|
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);
|
|
48
|
+
const parentRole = asAccessible(parent).getAccessibleRole();
|
|
49
|
+
return ROLES_WITH_INTERNAL_LABELS.has(parentRole);
|
|
42
50
|
};
|
|
43
51
|
const getWidgetText = (widget) => {
|
|
44
52
|
if (isInternalLabel(widget))
|
|
45
53
|
return null;
|
|
46
|
-
const
|
|
47
|
-
const role = accessible.getAccessibleRole();
|
|
54
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
48
55
|
switch (role) {
|
|
49
56
|
case AccessibleRole.BUTTON:
|
|
57
|
+
case AccessibleRole.LINK:
|
|
58
|
+
case AccessibleRole.TAB:
|
|
59
|
+
return asButton(widget).getLabel();
|
|
50
60
|
case AccessibleRole.TOGGLE_BUTTON:
|
|
61
|
+
return asToggleButton(widget).getLabel();
|
|
51
62
|
case AccessibleRole.CHECKBOX:
|
|
52
63
|
case AccessibleRole.RADIO:
|
|
53
|
-
|
|
54
|
-
case AccessibleRole.MENU_ITEM_CHECKBOX:
|
|
55
|
-
case AccessibleRole.MENU_ITEM_RADIO:
|
|
56
|
-
return asButton(widget).getLabel();
|
|
64
|
+
return asCheckButton(widget).getLabel();
|
|
57
65
|
case AccessibleRole.LABEL:
|
|
58
66
|
return asLabel(widget).getLabel();
|
|
59
67
|
case AccessibleRole.TEXT_BOX:
|
|
60
68
|
case AccessibleRole.SEARCH_BOX:
|
|
61
69
|
case AccessibleRole.SPIN_BUTTON:
|
|
62
|
-
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;
|
|
63
91
|
default:
|
|
64
92
|
return null;
|
|
65
93
|
}
|
|
@@ -67,20 +95,23 @@ const getWidgetText = (widget) => {
|
|
|
67
95
|
const getWidgetTestId = (widget) => {
|
|
68
96
|
return widget.getName();
|
|
69
97
|
};
|
|
98
|
+
const asSwitch = (widget) => getObject(widget.ptr, Switch);
|
|
70
99
|
const getWidgetCheckedState = (widget) => {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
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;
|
|
78
111
|
}
|
|
79
|
-
return undefined;
|
|
80
112
|
};
|
|
81
113
|
const getWidgetExpandedState = (widget) => {
|
|
82
|
-
const
|
|
83
|
-
const role = accessible.getAccessibleRole();
|
|
114
|
+
const role = asAccessible(widget).getAccessibleRole();
|
|
84
115
|
if (role === AccessibleRole.BUTTON) {
|
|
85
116
|
const parent = widget.getParent();
|
|
86
117
|
if (!parent)
|
|
@@ -128,8 +159,7 @@ const formatByRoleError = (role, options) => {
|
|
|
128
159
|
};
|
|
129
160
|
const getAllByRole = (container, role, options) => {
|
|
130
161
|
const matches = findAll(container, (node) => {
|
|
131
|
-
|
|
132
|
-
if (accessible.getAccessibleRole() !== role)
|
|
162
|
+
if (asAccessible(node).getAccessibleRole() !== role)
|
|
133
163
|
return false;
|
|
134
164
|
return matchByRoleOptions(node, options);
|
|
135
165
|
});
|
|
@@ -143,7 +173,10 @@ const getByRole = (container, role, options) => {
|
|
|
143
173
|
if (matches.length > 1) {
|
|
144
174
|
throw new Error(`Found ${matches.length} elements with ${formatByRoleError(role, options)}`);
|
|
145
175
|
}
|
|
146
|
-
|
|
176
|
+
const [first] = matches;
|
|
177
|
+
if (!first)
|
|
178
|
+
throw new Error(`Unable to find element with ${formatByRoleError(role, options)}`);
|
|
179
|
+
return first;
|
|
147
180
|
};
|
|
148
181
|
const getAllByLabelText = (container, text, options) => {
|
|
149
182
|
const matches = findAll(container, (node) => {
|
|
@@ -160,7 +193,10 @@ const getByLabelText = (container, text, options) => {
|
|
|
160
193
|
if (matches.length > 1) {
|
|
161
194
|
throw new Error(`Found ${matches.length} elements with label text "${text}"`);
|
|
162
195
|
}
|
|
163
|
-
|
|
196
|
+
const [first] = matches;
|
|
197
|
+
if (!first)
|
|
198
|
+
throw new Error(`Unable to find element with label text "${text}"`);
|
|
199
|
+
return first;
|
|
164
200
|
};
|
|
165
201
|
const getAllByText = (container, text, options) => {
|
|
166
202
|
const matches = findAll(container, (node) => {
|
|
@@ -177,7 +213,10 @@ const getByText = (container, text, options) => {
|
|
|
177
213
|
if (matches.length > 1) {
|
|
178
214
|
throw new Error(`Found ${matches.length} elements with text "${text}"`);
|
|
179
215
|
}
|
|
180
|
-
|
|
216
|
+
const [first] = matches;
|
|
217
|
+
if (!first)
|
|
218
|
+
throw new Error(`Unable to find element with text "${text}"`);
|
|
219
|
+
return first;
|
|
181
220
|
};
|
|
182
221
|
const getAllByTestId = (container, testId, options) => {
|
|
183
222
|
const matches = findAll(container, (node) => {
|
|
@@ -194,7 +233,10 @@ const getByTestId = (container, testId, options) => {
|
|
|
194
233
|
if (matches.length > 1) {
|
|
195
234
|
throw new Error(`Found ${matches.length} elements with test id "${testId}"`);
|
|
196
235
|
}
|
|
197
|
-
|
|
236
|
+
const [first] = matches;
|
|
237
|
+
if (!first)
|
|
238
|
+
throw new Error(`Unable to find element with test id "${testId}"`);
|
|
239
|
+
return first;
|
|
198
240
|
};
|
|
199
241
|
/**
|
|
200
242
|
* Waits for and finds a single widget matching the specified accessible role.
|
|
@@ -203,7 +245,7 @@ const getByTestId = (container, testId, options) => {
|
|
|
203
245
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
204
246
|
* @returns Promise resolving to the matching widget
|
|
205
247
|
*/
|
|
206
|
-
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 });
|
|
207
249
|
/**
|
|
208
250
|
* Waits for and finds all widgets matching the specified accessible role.
|
|
209
251
|
* @param container - The container to search within
|
|
@@ -211,52 +253,52 @@ export const findByRole = async (container, role, options) => waitFor(() => getB
|
|
|
211
253
|
* @param options - Additional filtering options (name, checked, expanded)
|
|
212
254
|
* @returns Promise resolving to array of matching widgets
|
|
213
255
|
*/
|
|
214
|
-
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 });
|
|
215
257
|
/**
|
|
216
258
|
* Waits for and finds a single widget matching the specified label text.
|
|
217
259
|
* @param container - The container to search within
|
|
218
260
|
* @param text - The text or pattern to match
|
|
219
|
-
* @param options - Text matching options (exact, normalizer)
|
|
261
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
220
262
|
* @returns Promise resolving to the matching widget
|
|
221
263
|
*/
|
|
222
|
-
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 });
|
|
223
265
|
/**
|
|
224
266
|
* Waits for and finds all widgets matching the specified label text.
|
|
225
267
|
* @param container - The container to search within
|
|
226
268
|
* @param text - The text or pattern to match
|
|
227
|
-
* @param options - Text matching options (exact, normalizer)
|
|
269
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
228
270
|
* @returns Promise resolving to array of matching widgets
|
|
229
271
|
*/
|
|
230
|
-
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 });
|
|
231
273
|
/**
|
|
232
274
|
* Waits for and finds a single widget matching the specified text content.
|
|
233
275
|
* @param container - The container to search within
|
|
234
276
|
* @param text - The text or pattern to match
|
|
235
|
-
* @param options - Text matching options (exact, normalizer)
|
|
277
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
236
278
|
* @returns Promise resolving to the matching widget
|
|
237
279
|
*/
|
|
238
|
-
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 });
|
|
239
281
|
/**
|
|
240
282
|
* Waits for and finds all widgets matching the specified text content.
|
|
241
283
|
* @param container - The container to search within
|
|
242
284
|
* @param text - The text or pattern to match
|
|
243
|
-
* @param options - Text matching options (exact, normalizer)
|
|
285
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
244
286
|
* @returns Promise resolving to array of matching widgets
|
|
245
287
|
*/
|
|
246
|
-
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 });
|
|
247
289
|
/**
|
|
248
290
|
* Waits for and finds a single widget matching the specified test ID.
|
|
249
291
|
* @param container - The container to search within
|
|
250
292
|
* @param testId - The test ID or pattern to match
|
|
251
|
-
* @param options - Text matching options (exact, normalizer)
|
|
293
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
252
294
|
* @returns Promise resolving to the matching widget
|
|
253
295
|
*/
|
|
254
|
-
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 });
|
|
255
297
|
/**
|
|
256
298
|
* Waits for and finds all widgets matching the specified test ID.
|
|
257
299
|
* @param container - The container to search within
|
|
258
300
|
* @param testId - The test ID or pattern to match
|
|
259
|
-
* @param options - Text matching options (exact, normalizer)
|
|
301
|
+
* @param options - Text matching options (exact, normalizer, timeout)
|
|
260
302
|
* @returns Promise resolving to array of matching widgets
|
|
261
303
|
*/
|
|
262
|
-
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,18 +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
|
+
import { tick } from "./timing.js";
|
|
8
|
+
import { asAccessible, hasLabel } from "./widget.js";
|
|
7
9
|
const ROOT_NODE_CONTAINER = Symbol.for("ROOT_NODE_CONTAINER");
|
|
8
10
|
const APP_ID = "com.gtkx.testing";
|
|
9
11
|
let container = null;
|
|
10
|
-
const
|
|
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
|
+
};
|
|
11
21
|
const printWidgetTree = (root, indent = 0) => {
|
|
12
|
-
const accessible = root;
|
|
13
22
|
const prefix = " ".repeat(indent);
|
|
14
|
-
const role = Gtk.AccessibleRole[
|
|
15
|
-
const
|
|
23
|
+
const role = Gtk.AccessibleRole[asAccessible(root).getAccessibleRole()] ?? "UNKNOWN";
|
|
24
|
+
const labelText = getWidgetLabel(root);
|
|
25
|
+
const label = labelText ? ` label="${labelText}"` : "";
|
|
16
26
|
let result = `${prefix}<${root.constructor.name} role=${role}${label}>\n`;
|
|
17
27
|
let child = root.getFirstChild();
|
|
18
28
|
while (child) {
|
|
@@ -21,7 +31,6 @@ const printWidgetTree = (root, indent = 0) => {
|
|
|
21
31
|
}
|
|
22
32
|
return result;
|
|
23
33
|
};
|
|
24
|
-
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
34
|
const update = async (instance, element, fiberRoot) => {
|
|
26
35
|
instance.updateContainer(element, fiberRoot, null, () => { });
|
|
27
36
|
await tick();
|
|
@@ -30,9 +39,7 @@ const ensureInitialized = () => {
|
|
|
30
39
|
const app = start(APP_ID);
|
|
31
40
|
if (!container) {
|
|
32
41
|
const instance = reconciler.getInstance();
|
|
33
|
-
container = instance.createContainer(
|
|
34
|
-
// biome-ignore lint/suspicious/noExplicitAny: testing only
|
|
35
|
-
ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => console.error("Test reconciler error:", error), () => { }, () => { }, () => { }, null);
|
|
42
|
+
container = instance.createContainer(ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => console.error("Test reconciler error:", error), () => { }, () => { }, () => { }, null);
|
|
36
43
|
}
|
|
37
44
|
return { app, container };
|
|
38
45
|
};
|
|
@@ -96,5 +103,4 @@ export const cleanup = async () => {
|
|
|
96
103
|
export const teardown = async () => {
|
|
97
104
|
await cleanup();
|
|
98
105
|
stop();
|
|
99
|
-
container = null;
|
|
100
106
|
};
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const tick: () => Promise<void>;
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
package/dist/types.d.ts
CHANGED
package/dist/user-event.d.ts
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export interface UserEventOptions {
|
|
6
|
-
/** Delay between events in milliseconds */
|
|
7
|
-
delay?: number;
|
|
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>;
|
|
2
|
+
export interface TabOptions {
|
|
3
|
+
/** If true, navigates backwards (Shift+Tab behavior) */
|
|
4
|
+
shift?: boolean;
|
|
23
5
|
}
|
|
24
6
|
/**
|
|
25
7
|
* Simulates user interactions with GTK widgets. Provides methods that mimic
|
|
26
8
|
* real user behavior like clicking, typing, and clearing input fields.
|
|
27
|
-
* Use userEvent.setup() to create an instance with custom options.
|
|
28
9
|
*/
|
|
29
10
|
export declare const userEvent: {
|
|
30
|
-
setup: (options?: UserEventOptions) => UserEventInstance;
|
|
31
11
|
click: (element: Gtk.Widget) => Promise<void>;
|
|
32
12
|
dblClick: (element: Gtk.Widget) => Promise<void>;
|
|
13
|
+
tripleClick: (element: Gtk.Widget) => Promise<void>;
|
|
33
14
|
activate: (element: Gtk.Widget) => Promise<void>;
|
|
15
|
+
tab: (element: Gtk.Widget, options?: TabOptions) => Promise<void>;
|
|
34
16
|
type: (element: Gtk.Widget, text: string) => Promise<void>;
|
|
35
17
|
clear: (element: Gtk.Widget) => Promise<void>;
|
|
18
|
+
selectOptions: (element: Gtk.Widget, values: string | string[] | number | number[]) => Promise<void>;
|
|
19
|
+
deselectOptions: (element: Gtk.Widget, values: number | number[]) => Promise<void>;
|
|
36
20
|
};
|
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
|
-
import {
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
element.
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
import { tick } from "./timing.js";
|
|
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.50",
|
|
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/ffi": "0.1.
|
|
37
|
-
"@gtkx/
|
|
35
|
+
"@gtkx/react": "0.1.50",
|
|
36
|
+
"@gtkx/ffi": "0.1.50",
|
|
37
|
+
"@gtkx/native": "0.1.50"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b && cp ../../README.md .",
|