@gtkx/testing 0.11.1 → 0.12.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/bind-queries.d.ts +13 -0
- package/dist/bind-queries.js +28 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +58 -0
- package/dist/error-builder.d.ts +27 -0
- package/dist/error-builder.js +80 -0
- package/dist/fire-event.js +2 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/pretty-widget.d.ts +36 -0
- package/dist/pretty-widget.js +150 -0
- package/dist/queries.d.ts +78 -3
- package/dist/queries.js +156 -177
- package/dist/render.js +4 -38
- package/dist/role-helpers.d.ts +66 -0
- package/dist/role-helpers.js +108 -0
- package/dist/screen.d.ts +39 -18
- package/dist/screen.js +78 -19
- package/dist/screenshot.d.ts +26 -0
- package/dist/screenshot.js +86 -0
- package/dist/traversal.d.ts +3 -2
- package/dist/traversal.js +2 -2
- package/dist/types.d.ts +37 -8
- package/dist/user-event.d.ts +39 -6
- package/dist/user-event.js +160 -13
- package/dist/widget-text.d.ts +29 -0
- package/dist/widget-text.js +146 -0
- package/dist/widget.d.ts +0 -1
- package/dist/widget.js +0 -13
- package/dist/within.js +2 -11
- package/package.json +5 -5
package/dist/queries.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
1
|
+
import { buildMultipleFoundError, buildNotFoundError, buildTimeoutError } from "./error-builder.js";
|
|
3
2
|
import { findAll } from "./traversal.js";
|
|
4
|
-
import {
|
|
3
|
+
import { getWidgetCheckedState, getWidgetExpandedState, getWidgetTestId, getWidgetText } from "./widget-text.js";
|
|
4
|
+
const DEFAULT_TIMEOUT = 1000;
|
|
5
|
+
const DEFAULT_INTERVAL = 50;
|
|
5
6
|
const buildNormalizer = (options) => {
|
|
6
7
|
if (options?.normalizer) {
|
|
7
8
|
return options.normalizer;
|
|
@@ -38,126 +39,6 @@ const matchText = (actual, expected, widget, options) => {
|
|
|
38
39
|
const exact = options?.exact ?? true;
|
|
39
40
|
return exact ? normalizedActual === normalizedExpected : normalizedActual.includes(normalizedExpected);
|
|
40
41
|
};
|
|
41
|
-
const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
42
|
-
Gtk.AccessibleRole.BUTTON,
|
|
43
|
-
Gtk.AccessibleRole.TOGGLE_BUTTON,
|
|
44
|
-
Gtk.AccessibleRole.CHECKBOX,
|
|
45
|
-
Gtk.AccessibleRole.RADIO,
|
|
46
|
-
Gtk.AccessibleRole.MENU_ITEM,
|
|
47
|
-
Gtk.AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
48
|
-
Gtk.AccessibleRole.MENU_ITEM_RADIO,
|
|
49
|
-
Gtk.AccessibleRole.TAB,
|
|
50
|
-
Gtk.AccessibleRole.LINK,
|
|
51
|
-
]);
|
|
52
|
-
const isInternalLabel = (widget) => {
|
|
53
|
-
if (widget.getAccessibleRole() !== Gtk.AccessibleRole.LABEL)
|
|
54
|
-
return false;
|
|
55
|
-
const parent = widget.getParent();
|
|
56
|
-
if (!parent)
|
|
57
|
-
return false;
|
|
58
|
-
if (parent.getAccessibleRole === undefined)
|
|
59
|
-
return false;
|
|
60
|
-
const parentRole = parent.getAccessibleRole();
|
|
61
|
-
if (!parentRole)
|
|
62
|
-
return false;
|
|
63
|
-
return ROLES_WITH_INTERNAL_LABELS.has(parentRole);
|
|
64
|
-
};
|
|
65
|
-
const getLabelText = (widget) => {
|
|
66
|
-
const asLabel = widget;
|
|
67
|
-
const asInscription = widget;
|
|
68
|
-
return asLabel.getLabel?.() ?? asInscription.getText?.() ?? null;
|
|
69
|
-
};
|
|
70
|
-
const collectChildLabels = (widget) => {
|
|
71
|
-
const labels = [];
|
|
72
|
-
let child = widget.getFirstChild();
|
|
73
|
-
while (child) {
|
|
74
|
-
if (child.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
|
|
75
|
-
const labelText = getLabelText(child);
|
|
76
|
-
if (labelText)
|
|
77
|
-
labels.push(labelText);
|
|
78
|
-
}
|
|
79
|
-
labels.push(...collectChildLabels(child));
|
|
80
|
-
child = child.getNextSibling();
|
|
81
|
-
}
|
|
82
|
-
return labels;
|
|
83
|
-
};
|
|
84
|
-
const getWidgetText = (widget) => {
|
|
85
|
-
if (isInternalLabel(widget))
|
|
86
|
-
return null;
|
|
87
|
-
const role = widget.getAccessibleRole();
|
|
88
|
-
if (role === undefined)
|
|
89
|
-
return null;
|
|
90
|
-
switch (role) {
|
|
91
|
-
case Gtk.AccessibleRole.BUTTON:
|
|
92
|
-
case Gtk.AccessibleRole.LINK:
|
|
93
|
-
case Gtk.AccessibleRole.TAB: {
|
|
94
|
-
const directLabel = widget.getLabel?.() ??
|
|
95
|
-
widget.getLabel?.() ??
|
|
96
|
-
widget.getLabel?.();
|
|
97
|
-
if (directLabel)
|
|
98
|
-
return directLabel;
|
|
99
|
-
const childLabels = collectChildLabels(widget);
|
|
100
|
-
return childLabels.length > 0 ? childLabels.join(" ") : null;
|
|
101
|
-
}
|
|
102
|
-
case Gtk.AccessibleRole.TOGGLE_BUTTON:
|
|
103
|
-
return widget.getLabel?.() ?? null;
|
|
104
|
-
case Gtk.AccessibleRole.CHECKBOX:
|
|
105
|
-
case Gtk.AccessibleRole.RADIO:
|
|
106
|
-
return widget.getLabel?.() ?? null;
|
|
107
|
-
case Gtk.AccessibleRole.LABEL:
|
|
108
|
-
return getLabelText(widget);
|
|
109
|
-
case Gtk.AccessibleRole.TEXT_BOX:
|
|
110
|
-
case Gtk.AccessibleRole.SEARCH_BOX:
|
|
111
|
-
case Gtk.AccessibleRole.SPIN_BUTTON:
|
|
112
|
-
return getNativeObject(widget.id, Gtk.Editable).getText() ?? null;
|
|
113
|
-
case Gtk.AccessibleRole.GROUP:
|
|
114
|
-
return widget.getLabel?.() ?? null;
|
|
115
|
-
case Gtk.AccessibleRole.WINDOW:
|
|
116
|
-
case Gtk.AccessibleRole.DIALOG:
|
|
117
|
-
case Gtk.AccessibleRole.ALERT_DIALOG:
|
|
118
|
-
return widget.getTitle() ?? null;
|
|
119
|
-
case Gtk.AccessibleRole.TAB_PANEL: {
|
|
120
|
-
const parent = widget.getParent();
|
|
121
|
-
if (parent) {
|
|
122
|
-
const stack = parent;
|
|
123
|
-
const page = stack.getPage?.(widget);
|
|
124
|
-
if (page) {
|
|
125
|
-
return page.getTitle() ?? null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
default:
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
const getWidgetTestId = (widget) => {
|
|
135
|
-
return widget.getName();
|
|
136
|
-
};
|
|
137
|
-
const getWidgetCheckedState = (widget) => {
|
|
138
|
-
const role = widget.getAccessibleRole();
|
|
139
|
-
switch (role) {
|
|
140
|
-
case Gtk.AccessibleRole.CHECKBOX:
|
|
141
|
-
case Gtk.AccessibleRole.RADIO:
|
|
142
|
-
return widget.getActive();
|
|
143
|
-
case Gtk.AccessibleRole.TOGGLE_BUTTON:
|
|
144
|
-
return widget.getActive();
|
|
145
|
-
case Gtk.AccessibleRole.SWITCH:
|
|
146
|
-
return widget.getActive();
|
|
147
|
-
default:
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
const getWidgetExpandedState = (widget) => {
|
|
152
|
-
const role = widget.getAccessibleRole();
|
|
153
|
-
if (role === Gtk.AccessibleRole.BUTTON) {
|
|
154
|
-
const parent = widget.getParent();
|
|
155
|
-
if (!parent)
|
|
156
|
-
return null;
|
|
157
|
-
return parent.getExpanded?.() ?? null;
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
};
|
|
161
42
|
const matchByRoleOptions = (widget, options) => {
|
|
162
43
|
if (!options)
|
|
163
44
|
return true;
|
|
@@ -178,103 +59,201 @@ const matchByRoleOptions = (widget, options) => {
|
|
|
178
59
|
}
|
|
179
60
|
return true;
|
|
180
61
|
};
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
62
|
+
const waitFor = async (callback, options) => {
|
|
63
|
+
const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
let lastError = null;
|
|
66
|
+
while (Date.now() - startTime < timeout) {
|
|
67
|
+
try {
|
|
68
|
+
return await callback();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
lastError = error;
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const timeoutError = buildTimeoutError(timeout, lastError);
|
|
76
|
+
if (onTimeout) {
|
|
77
|
+
throw onTimeout(timeoutError);
|
|
78
|
+
}
|
|
79
|
+
throw timeoutError;
|
|
197
80
|
};
|
|
198
|
-
|
|
199
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Finds all elements matching a role without throwing.
|
|
83
|
+
*
|
|
84
|
+
* @param container - The container to search within
|
|
85
|
+
* @param role - The GTK accessible role to match
|
|
86
|
+
* @param options - Query options including name and state filters
|
|
87
|
+
* @returns Array of matching widgets (empty if none found)
|
|
88
|
+
*/
|
|
89
|
+
export const queryAllByRole = (container, role, options) => {
|
|
90
|
+
return findAll(container, (node) => {
|
|
200
91
|
if (node.getAccessibleRole() !== role)
|
|
201
92
|
return false;
|
|
202
93
|
return matchByRoleOptions(node, options);
|
|
203
94
|
});
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Finds a single element matching a role without throwing.
|
|
98
|
+
*
|
|
99
|
+
* @param container - The container to search within
|
|
100
|
+
* @param role - The GTK accessible role to match
|
|
101
|
+
* @param options - Query options including name and state filters
|
|
102
|
+
* @returns The matching widget or null if not found
|
|
103
|
+
* @throws Error if multiple elements match
|
|
104
|
+
*/
|
|
105
|
+
export const queryByRole = (container, role, options) => {
|
|
106
|
+
const matches = queryAllByRole(container, role, options);
|
|
107
|
+
if (matches.length > 1) {
|
|
108
|
+
throw buildMultipleFoundError(container, "role", { role, options }, matches.length);
|
|
109
|
+
}
|
|
110
|
+
return matches[0] ?? null;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Finds all elements matching label text without throwing.
|
|
114
|
+
*
|
|
115
|
+
* @param container - The container to search within
|
|
116
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
117
|
+
* @param options - Query options including normalization
|
|
118
|
+
* @returns Array of matching widgets (empty if none found)
|
|
119
|
+
*/
|
|
120
|
+
export const queryAllByLabelText = (container, text, options) => {
|
|
121
|
+
return findAll(container, (node) => {
|
|
122
|
+
const widgetText = getWidgetText(node);
|
|
123
|
+
return matchText(widgetText, text, node, options);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Finds a single element matching label text without throwing.
|
|
128
|
+
*
|
|
129
|
+
* @param container - The container to search within
|
|
130
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
131
|
+
* @param options - Query options including normalization
|
|
132
|
+
* @returns The matching widget or null if not found
|
|
133
|
+
* @throws Error if multiple elements match
|
|
134
|
+
*/
|
|
135
|
+
export const queryByLabelText = (container, text, options) => {
|
|
136
|
+
const matches = queryAllByLabelText(container, text, options);
|
|
137
|
+
if (matches.length > 1) {
|
|
138
|
+
throw buildMultipleFoundError(container, "labelText", { text, options }, matches.length);
|
|
139
|
+
}
|
|
140
|
+
return matches[0] ?? null;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Finds all elements matching text content without throwing.
|
|
144
|
+
*
|
|
145
|
+
* @param container - The container to search within
|
|
146
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
147
|
+
* @param options - Query options including normalization
|
|
148
|
+
* @returns Array of matching widgets (empty if none found)
|
|
149
|
+
*/
|
|
150
|
+
export const queryAllByText = (container, text, options) => {
|
|
151
|
+
return findAll(container, (node) => {
|
|
152
|
+
const widgetText = getWidgetText(node);
|
|
153
|
+
return matchText(widgetText, text, node, options);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Finds a single element matching text content without throwing.
|
|
158
|
+
*
|
|
159
|
+
* @param container - The container to search within
|
|
160
|
+
* @param text - Text to match (string, RegExp, or custom matcher)
|
|
161
|
+
* @param options - Query options including normalization
|
|
162
|
+
* @returns The matching widget or null if not found
|
|
163
|
+
* @throws Error if multiple elements match
|
|
164
|
+
*/
|
|
165
|
+
export const queryByText = (container, text, options) => {
|
|
166
|
+
const matches = queryAllByText(container, text, options);
|
|
167
|
+
if (matches.length > 1) {
|
|
168
|
+
throw buildMultipleFoundError(container, "text", { text, options }, matches.length);
|
|
169
|
+
}
|
|
170
|
+
return matches[0] ?? null;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Finds all elements matching a test ID without throwing.
|
|
174
|
+
*
|
|
175
|
+
* @param container - The container to search within
|
|
176
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
177
|
+
* @param options - Query options including normalization
|
|
178
|
+
* @returns Array of matching widgets (empty if none found)
|
|
179
|
+
*/
|
|
180
|
+
export const queryAllByTestId = (container, testId, options) => {
|
|
181
|
+
return findAll(container, (node) => {
|
|
182
|
+
const widgetTestId = getWidgetTestId(node);
|
|
183
|
+
return matchText(widgetTestId, testId, node, options);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Finds a single element matching a test ID without throwing.
|
|
188
|
+
*
|
|
189
|
+
* @param container - The container to search within
|
|
190
|
+
* @param testId - Test ID to match (string, RegExp, or custom matcher)
|
|
191
|
+
* @param options - Query options including normalization
|
|
192
|
+
* @returns The matching widget or null if not found
|
|
193
|
+
* @throws Error if multiple elements match
|
|
194
|
+
*/
|
|
195
|
+
export const queryByTestId = (container, testId, options) => {
|
|
196
|
+
const matches = queryAllByTestId(container, testId, options);
|
|
197
|
+
if (matches.length > 1) {
|
|
198
|
+
throw buildMultipleFoundError(container, "testId", { testId, options }, matches.length);
|
|
199
|
+
}
|
|
200
|
+
return matches[0] ?? null;
|
|
201
|
+
};
|
|
202
|
+
const getAllByRole = (container, role, options) => {
|
|
203
|
+
const matches = queryAllByRole(container, role, options);
|
|
204
204
|
if (matches.length === 0) {
|
|
205
|
-
throw
|
|
205
|
+
throw buildNotFoundError(container, "role", { role, options });
|
|
206
206
|
}
|
|
207
207
|
return matches;
|
|
208
208
|
};
|
|
209
209
|
const getByRole = (container, role, options) => {
|
|
210
210
|
const matches = getAllByRole(container, role, options);
|
|
211
211
|
if (matches.length > 1) {
|
|
212
|
-
throw
|
|
212
|
+
throw buildMultipleFoundError(container, "role", { role, options }, matches.length);
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
if (!first)
|
|
216
|
-
throw new Error(`Unable to find element with ${formatByRoleError(role, options)}`);
|
|
217
|
-
return first;
|
|
214
|
+
return matches[0];
|
|
218
215
|
};
|
|
219
216
|
const getAllByLabelText = (container, text, options) => {
|
|
220
|
-
const matches =
|
|
221
|
-
const widgetText = getWidgetText(node);
|
|
222
|
-
return matchText(widgetText, text, node, options);
|
|
223
|
-
});
|
|
217
|
+
const matches = queryAllByLabelText(container, text, options);
|
|
224
218
|
if (matches.length === 0) {
|
|
225
|
-
throw
|
|
219
|
+
throw buildNotFoundError(container, "labelText", { text, options });
|
|
226
220
|
}
|
|
227
221
|
return matches;
|
|
228
222
|
};
|
|
229
223
|
const getByLabelText = (container, text, options) => {
|
|
230
224
|
const matches = getAllByLabelText(container, text, options);
|
|
231
225
|
if (matches.length > 1) {
|
|
232
|
-
throw
|
|
226
|
+
throw buildMultipleFoundError(container, "labelText", { text, options }, matches.length);
|
|
233
227
|
}
|
|
234
|
-
|
|
235
|
-
if (!first)
|
|
236
|
-
throw new Error(`Unable to find element with label text '${text}'`);
|
|
237
|
-
return first;
|
|
228
|
+
return matches[0];
|
|
238
229
|
};
|
|
239
230
|
const getAllByText = (container, text, options) => {
|
|
240
|
-
const matches =
|
|
241
|
-
const widgetText = getWidgetText(node);
|
|
242
|
-
return matchText(widgetText, text, node, options);
|
|
243
|
-
});
|
|
231
|
+
const matches = queryAllByText(container, text, options);
|
|
244
232
|
if (matches.length === 0) {
|
|
245
|
-
throw
|
|
233
|
+
throw buildNotFoundError(container, "text", { text, options });
|
|
246
234
|
}
|
|
247
235
|
return matches;
|
|
248
236
|
};
|
|
249
237
|
const getByText = (container, text, options) => {
|
|
250
238
|
const matches = getAllByText(container, text, options);
|
|
251
239
|
if (matches.length > 1) {
|
|
252
|
-
throw
|
|
240
|
+
throw buildMultipleFoundError(container, "text", { text, options }, matches.length);
|
|
253
241
|
}
|
|
254
|
-
|
|
255
|
-
if (!first)
|
|
256
|
-
throw new Error(`Unable to find element with text '${text}'`);
|
|
257
|
-
return first;
|
|
242
|
+
return matches[0];
|
|
258
243
|
};
|
|
259
244
|
const getAllByTestId = (container, testId, options) => {
|
|
260
|
-
const matches =
|
|
261
|
-
const widgetTestId = getWidgetTestId(node);
|
|
262
|
-
return matchText(widgetTestId, testId, node, options);
|
|
263
|
-
});
|
|
245
|
+
const matches = queryAllByTestId(container, testId, options);
|
|
264
246
|
if (matches.length === 0) {
|
|
265
|
-
throw
|
|
247
|
+
throw buildNotFoundError(container, "testId", { testId, options });
|
|
266
248
|
}
|
|
267
249
|
return matches;
|
|
268
250
|
};
|
|
269
251
|
const getByTestId = (container, testId, options) => {
|
|
270
252
|
const matches = getAllByTestId(container, testId, options);
|
|
271
253
|
if (matches.length > 1) {
|
|
272
|
-
throw
|
|
254
|
+
throw buildMultipleFoundError(container, "testId", { testId, options }, matches.length);
|
|
273
255
|
}
|
|
274
|
-
|
|
275
|
-
if (!first)
|
|
276
|
-
throw new Error(`Unable to find element with test id '${testId}'`);
|
|
277
|
-
return first;
|
|
256
|
+
return matches[0];
|
|
278
257
|
};
|
|
279
258
|
/**
|
|
280
259
|
* Finds a single element by accessible role.
|
package/dist/render.js
CHANGED
|
@@ -1,38 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { discardAllBatches, start } from "@gtkx/ffi";
|
|
3
3
|
import * as Gio from "@gtkx/ffi/gio";
|
|
4
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
5
4
|
import { ApplicationContext, GtkApplicationWindow, reconciler } from "@gtkx/react";
|
|
6
|
-
import
|
|
5
|
+
import { bindQueries } from "./bind-queries.js";
|
|
6
|
+
import { prettyWidget } from "./pretty-widget.js";
|
|
7
7
|
import { setScreenRoot } from "./screen.js";
|
|
8
8
|
import { tick } from "./timing.js";
|
|
9
|
-
import { hasLabel } from "./widget.js";
|
|
10
9
|
let application = null;
|
|
11
10
|
let container = null;
|
|
12
11
|
let lastRenderError = null;
|
|
13
|
-
const getWidgetLabel = (widget) => {
|
|
14
|
-
if (!hasLabel(widget))
|
|
15
|
-
return null;
|
|
16
|
-
const role = widget.getAccessibleRole();
|
|
17
|
-
if (role === Gtk.AccessibleRole.LABEL) {
|
|
18
|
-
return widget.getLabel?.() ?? null;
|
|
19
|
-
}
|
|
20
|
-
return widget.getLabel?.() ?? null;
|
|
21
|
-
};
|
|
22
|
-
const printWidgetTree = (root, indent = 0) => {
|
|
23
|
-
const prefix = " ".repeat(indent);
|
|
24
|
-
const role = root.getAccessibleRole();
|
|
25
|
-
const roleName = role !== undefined ? (Gtk.AccessibleRole[role] ?? "UNKNOWN") : "UNKNOWN";
|
|
26
|
-
const labelText = getWidgetLabel(root);
|
|
27
|
-
const label = labelText ? ` label="${labelText}"` : "";
|
|
28
|
-
let result = `${prefix}<${root.constructor.name} role=${roleName}${label}>\n`;
|
|
29
|
-
let child = root.getFirstChild();
|
|
30
|
-
while (child) {
|
|
31
|
-
result += printWidgetTree(child, indent + 1);
|
|
32
|
-
child = child.getNextSibling();
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
35
|
-
};
|
|
36
12
|
const update = async (instance, element, fiberRoot) => {
|
|
37
13
|
lastRenderError = null;
|
|
38
14
|
instance.updateContainer(element, fiberRoot, null, () => { });
|
|
@@ -97,14 +73,7 @@ export const render = async (element, options) => {
|
|
|
97
73
|
setScreenRoot(application);
|
|
98
74
|
return {
|
|
99
75
|
container: application,
|
|
100
|
-
|
|
101
|
-
findByLabelText: (text, opts) => queries.findByLabelText(application, text, opts),
|
|
102
|
-
findByText: (text, opts) => queries.findByText(application, text, opts),
|
|
103
|
-
findByTestId: (testId, opts) => queries.findByTestId(application, testId, opts),
|
|
104
|
-
findAllByRole: (role, opts) => queries.findAllByRole(application, role, opts),
|
|
105
|
-
findAllByLabelText: (text, opts) => queries.findAllByLabelText(application, text, opts),
|
|
106
|
-
findAllByText: (text, opts) => queries.findAllByText(application, text, opts),
|
|
107
|
-
findAllByTestId: (testId, opts) => queries.findAllByTestId(application, testId, opts),
|
|
76
|
+
...bindQueries(application),
|
|
108
77
|
unmount: () => update(instance, null, fiberRoot),
|
|
109
78
|
rerender: (newElement) => {
|
|
110
79
|
const wrapped = wrapElement(newElement, options?.wrapper);
|
|
@@ -112,10 +81,7 @@ export const render = async (element, options) => {
|
|
|
112
81
|
return update(instance, withCtx, fiberRoot);
|
|
113
82
|
},
|
|
114
83
|
debug: () => {
|
|
115
|
-
|
|
116
|
-
if (activeWindow) {
|
|
117
|
-
console.log(printWidgetTree(activeWindow));
|
|
118
|
-
}
|
|
84
|
+
console.log(prettyWidget(application));
|
|
119
85
|
},
|
|
120
86
|
};
|
|
121
87
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { type Container } from "./traversal.js";
|
|
3
|
+
/**
|
|
4
|
+
* Information about a widget and its accessible name.
|
|
5
|
+
*/
|
|
6
|
+
export type RoleInfo = {
|
|
7
|
+
widget: Gtk.Widget;
|
|
8
|
+
name: string | null;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Formats a GTK accessible role to a lowercase string.
|
|
12
|
+
*
|
|
13
|
+
* @param role - The GTK accessible role
|
|
14
|
+
* @returns Lowercase role name (e.g., "button", "checkbox")
|
|
15
|
+
*/
|
|
16
|
+
export declare const formatRole: (role: Gtk.AccessibleRole | undefined) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Collects all accessible roles and their widgets from a container.
|
|
19
|
+
*
|
|
20
|
+
* Returns a Map where keys are role names (lowercase) and values are
|
|
21
|
+
* arrays of widgets with that role, including their accessible names.
|
|
22
|
+
*
|
|
23
|
+
* @param container - The container to scan for roles
|
|
24
|
+
* @returns Map of role names to arrays of RoleInfo
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { getRoles } from "@gtkx/testing";
|
|
29
|
+
*
|
|
30
|
+
* const roles = getRoles(container);
|
|
31
|
+
* // Map {
|
|
32
|
+
* // "button" => [{ widget: ..., name: "Submit" }, { widget: ..., name: "Cancel" }],
|
|
33
|
+
* // "checkbox" => [{ widget: ..., name: "Remember me" }]
|
|
34
|
+
* // }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const getRoles: (container: Container) => Map<string, RoleInfo[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Formats roles into a readable string for error messages.
|
|
40
|
+
*
|
|
41
|
+
* @param container - The container to format roles for
|
|
42
|
+
* @returns Formatted string showing all roles and their accessible names
|
|
43
|
+
*/
|
|
44
|
+
export declare const prettyRoles: (container: Container) => string;
|
|
45
|
+
/**
|
|
46
|
+
* Logs all accessible roles in a container to the console.
|
|
47
|
+
*
|
|
48
|
+
* Useful for debugging test failures and discovering available roles.
|
|
49
|
+
*
|
|
50
|
+
* @param container - The container to log roles for
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { render, logRoles } from "@gtkx/testing";
|
|
55
|
+
*
|
|
56
|
+
* const { container } = await render(<MyComponent />);
|
|
57
|
+
* logRoles(container);
|
|
58
|
+
* // Console output:
|
|
59
|
+
* // button:
|
|
60
|
+
* // Name "Submit": <GtkButton role="button">Submit</GtkButton>
|
|
61
|
+
* // Name "Cancel": <GtkButton role="button">Cancel</GtkButton>
|
|
62
|
+
* // checkbox:
|
|
63
|
+
* // Name "Remember me": <GtkCheckButton role="checkbox">Remember me</GtkCheckButton>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const logRoles: (container: Container) => void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { traverse } from "./traversal.js";
|
|
3
|
+
import { getWidgetText } from "./widget-text.js";
|
|
4
|
+
/**
|
|
5
|
+
* Formats a GTK accessible role to a lowercase string.
|
|
6
|
+
*
|
|
7
|
+
* @param role - The GTK accessible role
|
|
8
|
+
* @returns Lowercase role name (e.g., "button", "checkbox")
|
|
9
|
+
*/
|
|
10
|
+
export const formatRole = (role) => {
|
|
11
|
+
if (role === undefined)
|
|
12
|
+
return "unknown";
|
|
13
|
+
const name = Gtk.AccessibleRole[role];
|
|
14
|
+
if (!name)
|
|
15
|
+
return String(role);
|
|
16
|
+
return name.toLowerCase();
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Collects all accessible roles and their widgets from a container.
|
|
20
|
+
*
|
|
21
|
+
* Returns a Map where keys are role names (lowercase) and values are
|
|
22
|
+
* arrays of widgets with that role, including their accessible names.
|
|
23
|
+
*
|
|
24
|
+
* @param container - The container to scan for roles
|
|
25
|
+
* @returns Map of role names to arrays of RoleInfo
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* import { getRoles } from "@gtkx/testing";
|
|
30
|
+
*
|
|
31
|
+
* const roles = getRoles(container);
|
|
32
|
+
* // Map {
|
|
33
|
+
* // "button" => [{ widget: ..., name: "Submit" }, { widget: ..., name: "Cancel" }],
|
|
34
|
+
* // "checkbox" => [{ widget: ..., name: "Remember me" }]
|
|
35
|
+
* // }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const getRoles = (container) => {
|
|
39
|
+
const roles = new Map();
|
|
40
|
+
for (const widget of traverse(container)) {
|
|
41
|
+
const role = widget.getAccessibleRole?.();
|
|
42
|
+
if (role === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const roleName = formatRole(role);
|
|
45
|
+
const name = getWidgetText(widget);
|
|
46
|
+
const info = { widget, name };
|
|
47
|
+
const existing = roles.get(roleName);
|
|
48
|
+
if (existing) {
|
|
49
|
+
existing.push(info);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
roles.set(roleName, [info]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return roles;
|
|
56
|
+
};
|
|
57
|
+
const formatWidgetPreview = (widget, name) => {
|
|
58
|
+
const tagName = widget.constructor.name;
|
|
59
|
+
const roleAttr = formatRole(widget.getAccessibleRole?.());
|
|
60
|
+
const nameDisplay = name ? `Name "${name}"` : 'Name ""';
|
|
61
|
+
return `${nameDisplay}: <${tagName} role="${roleAttr}">${name ?? ""}</${tagName}>`;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Formats roles into a readable string for error messages.
|
|
65
|
+
*
|
|
66
|
+
* @param container - The container to format roles for
|
|
67
|
+
* @returns Formatted string showing all roles and their accessible names
|
|
68
|
+
*/
|
|
69
|
+
export const prettyRoles = (container) => {
|
|
70
|
+
const roles = getRoles(container);
|
|
71
|
+
if (roles.size === 0) {
|
|
72
|
+
return "No accessible roles found in the widget tree.";
|
|
73
|
+
}
|
|
74
|
+
const lines = [];
|
|
75
|
+
const sortedRoles = [...roles.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
76
|
+
for (const [roleName, widgets] of sortedRoles) {
|
|
77
|
+
lines.push(`${roleName}:`);
|
|
78
|
+
for (const { widget, name } of widgets) {
|
|
79
|
+
lines.push(` ${formatWidgetPreview(widget, name)}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n").trimEnd();
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Logs all accessible roles in a container to the console.
|
|
87
|
+
*
|
|
88
|
+
* Useful for debugging test failures and discovering available roles.
|
|
89
|
+
*
|
|
90
|
+
* @param container - The container to log roles for
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { render, logRoles } from "@gtkx/testing";
|
|
95
|
+
*
|
|
96
|
+
* const { container } = await render(<MyComponent />);
|
|
97
|
+
* logRoles(container);
|
|
98
|
+
* // Console output:
|
|
99
|
+
* // button:
|
|
100
|
+
* // Name "Submit": <GtkButton role="button">Submit</GtkButton>
|
|
101
|
+
* // Name "Cancel": <GtkButton role="button">Cancel</GtkButton>
|
|
102
|
+
* // checkbox:
|
|
103
|
+
* // Name "Remember me": <GtkCheckButton role="checkbox">Remember me</GtkCheckButton>
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export const logRoles = (container) => {
|
|
107
|
+
console.log(prettyRoles(container));
|
|
108
|
+
};
|