@gtkx/testing 0.3.5 → 0.4.0

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