@gtkx/testing 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.setup()` - Create reusable instance
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
- ### List Example
183
+ ### Todo App
190
184
 
191
- Comprehensive showcase of ListView, GridView, and ColumnView with sorting:
185
+ A todo app demonstrating `@gtkx/testing` with realistic component tests:
192
186
 
193
187
  ```bash
194
- turbo start --filter=list-example
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 { UserEventInstance, UserEventOptions } from "./user-event.js";
6
+ export type { TabOptions } from "./user-event.js";
7
7
  export { userEvent } from "./user-event.js";
8
8
  export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
9
9
  export { within } from "./within.js";
package/dist/queries.d.ts CHANGED
@@ -22,7 +22,7 @@ export declare const findAllByRole: (container: Container, role: AccessibleRole,
22
22
  * Waits for and finds a single widget matching the specified label text.
23
23
  * @param container - The container to search within
24
24
  * @param text - The text or pattern to match
25
- * @param options - Text matching options (exact, normalizer)
25
+ * @param options - Text matching options (exact, normalizer, timeout)
26
26
  * @returns Promise resolving to the matching widget
27
27
  */
28
28
  export declare const findByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
@@ -30,7 +30,7 @@ export declare const findByLabelText: (container: Container, text: string | RegE
30
30
  * Waits for and finds all widgets matching the specified label text.
31
31
  * @param container - The container to search within
32
32
  * @param text - The text or pattern to match
33
- * @param options - Text matching options (exact, normalizer)
33
+ * @param options - Text matching options (exact, normalizer, timeout)
34
34
  * @returns Promise resolving to array of matching widgets
35
35
  */
36
36
  export declare const findAllByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
@@ -38,7 +38,7 @@ export declare const findAllByLabelText: (container: Container, text: string | R
38
38
  * Waits for and finds a single widget matching the specified text content.
39
39
  * @param container - The container to search within
40
40
  * @param text - The text or pattern to match
41
- * @param options - Text matching options (exact, normalizer)
41
+ * @param options - Text matching options (exact, normalizer, timeout)
42
42
  * @returns Promise resolving to the matching widget
43
43
  */
44
44
  export declare const findByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
@@ -46,7 +46,7 @@ export declare const findByText: (container: Container, text: string | RegExp, o
46
46
  * Waits for and finds all widgets matching the specified text content.
47
47
  * @param container - The container to search within
48
48
  * @param text - The text or pattern to match
49
- * @param options - Text matching options (exact, normalizer)
49
+ * @param options - Text matching options (exact, normalizer, timeout)
50
50
  * @returns Promise resolving to array of matching widgets
51
51
  */
52
52
  export declare const findAllByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
@@ -54,7 +54,7 @@ export declare const findAllByText: (container: Container, text: string | RegExp
54
54
  * Waits for and finds a single widget matching the specified test ID.
55
55
  * @param container - The container to search within
56
56
  * @param testId - The test ID or pattern to match
57
- * @param options - Text matching options (exact, normalizer)
57
+ * @param options - Text matching options (exact, normalizer, timeout)
58
58
  * @returns Promise resolving to the matching widget
59
59
  */
60
60
  export declare const findByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
@@ -62,7 +62,7 @@ export declare const findByTestId: (container: Container, testId: string | RegEx
62
62
  * Waits for and finds all widgets matching the specified test ID.
63
63
  * @param container - The container to search within
64
64
  * @param testId - The test ID or pattern to match
65
- * @param options - Text matching options (exact, normalizer)
65
+ * @param options - Text matching options (exact, normalizer, timeout)
66
66
  * @returns Promise resolving to array of matching widgets
67
67
  */
68
68
  export declare const findAllByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
package/dist/queries.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getObject } from "@gtkx/ffi";
2
- import { AccessibleRole, Button, CheckButton, Expander, Label, ToggleButton } from "@gtkx/ffi/gtk";
2
+ import { AccessibleRole, Button, CheckButton, Editable, Expander, Frame, Label, StackPage, Switch, ToggleButton, Window, } from "@gtkx/ffi/gtk";
3
3
  import { findAll } from "./traversal.js";
4
4
  import { waitFor } from "./wait-for.js";
5
+ import { asAccessible } from "./widget.js";
5
6
  const DEFAULT_NORMALIZER = (text) => text.trim().replace(/\s+/g, " ");
6
7
  const normalizeText = (text, options) => {
7
8
  const normalizer = options?.normalizer ?? DEFAULT_NORMALIZER;
@@ -18,12 +19,25 @@ const matchText = (actual, expected, options) => {
18
19
  }
19
20
  return expected.test(normalizedActual);
20
21
  };
21
- const asAccessible = (widget) => widget;
22
22
  const asButton = (widget) => getObject(widget.ptr, Button);
23
23
  const asLabel = (widget) => getObject(widget.ptr, Label);
24
24
  const asCheckButton = (widget) => getObject(widget.ptr, CheckButton);
25
25
  const asToggleButton = (widget) => getObject(widget.ptr, ToggleButton);
26
26
  const asExpander = (widget) => getObject(widget.ptr, Expander);
27
+ const asFrame = (widget) => getObject(widget.ptr, Frame);
28
+ const asWindow = (widget) => getObject(widget.ptr, Window);
29
+ const asStackPage = (widget) => getObject(widget.ptr, StackPage);
30
+ const ROLES_WITH_INTERNAL_LABELS = new Set([
31
+ AccessibleRole.BUTTON,
32
+ AccessibleRole.TOGGLE_BUTTON,
33
+ AccessibleRole.CHECKBOX,
34
+ AccessibleRole.RADIO,
35
+ AccessibleRole.MENU_ITEM,
36
+ AccessibleRole.MENU_ITEM_CHECKBOX,
37
+ AccessibleRole.MENU_ITEM_RADIO,
38
+ AccessibleRole.TAB,
39
+ AccessibleRole.LINK,
40
+ ]);
27
41
  const isInternalLabel = (widget) => {
28
42
  const accessible = asAccessible(widget);
29
43
  if (accessible.getAccessibleRole() !== AccessibleRole.LABEL)
@@ -32,13 +46,7 @@ const isInternalLabel = (widget) => {
32
46
  if (!parent)
33
47
  return false;
34
48
  const parentRole = asAccessible(parent).getAccessibleRole();
35
- return (parentRole === 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);
49
+ return ROLES_WITH_INTERNAL_LABELS.has(parentRole);
42
50
  };
43
51
  const getWidgetText = (widget) => {
44
52
  if (isInternalLabel(widget))
@@ -46,19 +54,40 @@ const getWidgetText = (widget) => {
46
54
  const role = asAccessible(widget).getAccessibleRole();
47
55
  switch (role) {
48
56
  case AccessibleRole.BUTTON:
57
+ case AccessibleRole.LINK:
58
+ case AccessibleRole.TAB:
59
+ return asButton(widget).getLabel();
49
60
  case AccessibleRole.TOGGLE_BUTTON:
61
+ return asToggleButton(widget).getLabel();
50
62
  case AccessibleRole.CHECKBOX:
51
63
  case AccessibleRole.RADIO:
52
- case AccessibleRole.MENU_ITEM:
53
- case AccessibleRole.MENU_ITEM_CHECKBOX:
54
- case AccessibleRole.MENU_ITEM_RADIO:
55
- return asButton(widget).getLabel();
64
+ return asCheckButton(widget).getLabel();
56
65
  case AccessibleRole.LABEL:
57
66
  return asLabel(widget).getLabel();
58
67
  case AccessibleRole.TEXT_BOX:
59
68
  case AccessibleRole.SEARCH_BOX:
60
69
  case AccessibleRole.SPIN_BUTTON:
61
- return widget.getText();
70
+ return getObject(widget.ptr, Editable).getText();
71
+ case AccessibleRole.GROUP:
72
+ try {
73
+ return asFrame(widget).getLabel();
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ case AccessibleRole.WINDOW:
79
+ case AccessibleRole.DIALOG:
80
+ case AccessibleRole.ALERT_DIALOG:
81
+ return asWindow(widget).getTitle();
82
+ case AccessibleRole.TAB_PANEL:
83
+ try {
84
+ return asStackPage(widget).getTitle();
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ case AccessibleRole.SWITCH:
90
+ return null;
62
91
  default:
63
92
  return null;
64
93
  }
@@ -66,15 +95,20 @@ const getWidgetText = (widget) => {
66
95
  const getWidgetTestId = (widget) => {
67
96
  return widget.getName();
68
97
  };
98
+ const asSwitch = (widget) => getObject(widget.ptr, Switch);
69
99
  const getWidgetCheckedState = (widget) => {
70
100
  const role = asAccessible(widget).getAccessibleRole();
71
- if (role === AccessibleRole.CHECKBOX || role === AccessibleRole.RADIO) {
72
- return asCheckButton(widget).getActive();
73
- }
74
- if (role === AccessibleRole.TOGGLE_BUTTON) {
75
- return asToggleButton(widget).getActive();
101
+ switch (role) {
102
+ case AccessibleRole.CHECKBOX:
103
+ case AccessibleRole.RADIO:
104
+ return asCheckButton(widget).getActive();
105
+ case AccessibleRole.TOGGLE_BUTTON:
106
+ return asToggleButton(widget).getActive();
107
+ case AccessibleRole.SWITCH:
108
+ return asSwitch(widget).getActive();
109
+ default:
110
+ return undefined;
76
111
  }
77
- return undefined;
78
112
  };
79
113
  const getWidgetExpandedState = (widget) => {
80
114
  const role = asAccessible(widget).getAccessibleRole();
@@ -211,7 +245,7 @@ const getByTestId = (container, testId, options) => {
211
245
  * @param options - Additional filtering options (name, checked, expanded)
212
246
  * @returns Promise resolving to the matching widget
213
247
  */
214
- export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options));
248
+ export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options), { timeout: options?.timeout });
215
249
  /**
216
250
  * Waits for and finds all widgets matching the specified accessible role.
217
251
  * @param container - The container to search within
@@ -219,52 +253,52 @@ export const findByRole = async (container, role, options) => waitFor(() => getB
219
253
  * @param options - Additional filtering options (name, checked, expanded)
220
254
  * @returns Promise resolving to array of matching widgets
221
255
  */
222
- export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options));
256
+ export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options), { timeout: options?.timeout });
223
257
  /**
224
258
  * Waits for and finds a single widget matching the specified label text.
225
259
  * @param container - The container to search within
226
260
  * @param text - The text or pattern to match
227
- * @param options - Text matching options (exact, normalizer)
261
+ * @param options - Text matching options (exact, normalizer, timeout)
228
262
  * @returns Promise resolving to the matching widget
229
263
  */
230
- export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options));
264
+ export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options), { timeout: options?.timeout });
231
265
  /**
232
266
  * Waits for and finds all widgets matching the specified label text.
233
267
  * @param container - The container to search within
234
268
  * @param text - The text or pattern to match
235
- * @param options - Text matching options (exact, normalizer)
269
+ * @param options - Text matching options (exact, normalizer, timeout)
236
270
  * @returns Promise resolving to array of matching widgets
237
271
  */
238
- export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options));
272
+ export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options), { timeout: options?.timeout });
239
273
  /**
240
274
  * Waits for and finds a single widget matching the specified text content.
241
275
  * @param container - The container to search within
242
276
  * @param text - The text or pattern to match
243
- * @param options - Text matching options (exact, normalizer)
277
+ * @param options - Text matching options (exact, normalizer, timeout)
244
278
  * @returns Promise resolving to the matching widget
245
279
  */
246
- export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options));
280
+ export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options), { timeout: options?.timeout });
247
281
  /**
248
282
  * Waits for and finds all widgets matching the specified text content.
249
283
  * @param container - The container to search within
250
284
  * @param text - The text or pattern to match
251
- * @param options - Text matching options (exact, normalizer)
285
+ * @param options - Text matching options (exact, normalizer, timeout)
252
286
  * @returns Promise resolving to array of matching widgets
253
287
  */
254
- export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options));
288
+ export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options), { timeout: options?.timeout });
255
289
  /**
256
290
  * Waits for and finds a single widget matching the specified test ID.
257
291
  * @param container - The container to search within
258
292
  * @param testId - The test ID or pattern to match
259
- * @param options - Text matching options (exact, normalizer)
293
+ * @param options - Text matching options (exact, normalizer, timeout)
260
294
  * @returns Promise resolving to the matching widget
261
295
  */
262
- export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options));
296
+ export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options), { timeout: options?.timeout });
263
297
  /**
264
298
  * Waits for and finds all widgets matching the specified test ID.
265
299
  * @param container - The container to search within
266
300
  * @param testId - The test ID or pattern to match
267
- * @param options - Text matching options (exact, normalizer)
301
+ * @param options - Text matching options (exact, normalizer, timeout)
268
302
  * @returns Promise resolving to array of matching widgets
269
303
  */
270
- export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options));
304
+ export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options), { timeout: options?.timeout });
package/dist/render.js CHANGED
@@ -1,19 +1,28 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { getCurrentApp, start, stop } from "@gtkx/ffi";
2
+ import { getCurrentApp, getObject, start, stop } from "@gtkx/ffi";
3
3
  import * as Gtk from "@gtkx/ffi/gtk";
4
4
  import { ApplicationWindow, reconciler } from "@gtkx/react";
5
5
  import * as queries from "./queries.js";
6
6
  import { setScreenRoot } from "./screen.js";
7
7
  import { tick } from "./timing.js";
8
+ import { asAccessible, hasLabel } from "./widget.js";
8
9
  const ROOT_NODE_CONTAINER = Symbol.for("ROOT_NODE_CONTAINER");
9
10
  const APP_ID = "com.gtkx.testing";
10
11
  let container = null;
11
- const hasGetLabel = (widget) => typeof widget.getLabel === "function";
12
- const asAccessible = (widget) => widget;
12
+ const getWidgetLabel = (widget) => {
13
+ if (!hasLabel(widget))
14
+ return null;
15
+ const role = asAccessible(widget).getAccessibleRole();
16
+ if (role === Gtk.AccessibleRole.LABEL) {
17
+ return getObject(widget.ptr, Gtk.Label).getLabel();
18
+ }
19
+ return getObject(widget.ptr, Gtk.Button).getLabel();
20
+ };
13
21
  const printWidgetTree = (root, indent = 0) => {
14
22
  const prefix = " ".repeat(indent);
15
23
  const role = Gtk.AccessibleRole[asAccessible(root).getAccessibleRole()] ?? "UNKNOWN";
16
- const label = hasGetLabel(root) ? ` label="${root.getLabel()}"` : "";
24
+ const labelText = getWidgetLabel(root);
25
+ const label = labelText ? ` label="${labelText}"` : "";
17
26
  let result = `${prefix}<${root.constructor.name} role=${role}${label}>\n`;
18
27
  let child = root.getFirstChild();
19
28
  while (child) {
@@ -94,5 +103,4 @@ export const cleanup = async () => {
94
103
  export const teardown = async () => {
95
104
  await cleanup();
96
105
  stop();
97
- container = null;
98
106
  };
package/dist/types.d.ts CHANGED
@@ -1,41 +1,87 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { AccessibleRole } from "@gtkx/ffi/gtk";
3
3
  import type { ComponentType, ReactNode } from "react";
4
+ /**
5
+ * Options for text matching in queries.
6
+ */
4
7
  export interface TextMatchOptions {
8
+ /** Whether to match the entire string exactly. Defaults to true. */
5
9
  exact?: boolean;
10
+ /** Custom function to normalize text before comparison. */
6
11
  normalizer?: (text: string) => string;
12
+ /** Maximum time in milliseconds to wait for a match. */
13
+ timeout?: number;
7
14
  }
15
+ /**
16
+ * Options for querying elements by their accessible role.
17
+ */
8
18
  export interface ByRoleOptions extends TextMatchOptions {
19
+ /** Filter by the element's accessible name. */
9
20
  name?: string | RegExp;
21
+ /** Filter checkboxes/switches by checked state. */
10
22
  checked?: boolean;
23
+ /** Filter toggle buttons by pressed state. */
11
24
  pressed?: boolean;
25
+ /** Filter selectable items by selected state. */
12
26
  selected?: boolean;
27
+ /** Filter expandable elements by expanded state. */
13
28
  expanded?: boolean;
29
+ /** Filter headings by their level (1-6). */
14
30
  level?: number;
15
31
  }
32
+ /**
33
+ * Options for waitFor and related async utilities.
34
+ */
16
35
  export interface WaitForOptions {
36
+ /** Maximum time in milliseconds to wait. Defaults to 1000ms. */
17
37
  timeout?: number;
38
+ /** Interval in milliseconds between condition checks. Defaults to 50ms. */
18
39
  interval?: number;
40
+ /** Custom error handler called when timeout is reached. */
19
41
  onTimeout?: (error: Error) => Error;
20
42
  }
43
+ /**
44
+ * Options for the render function.
45
+ */
21
46
  export interface RenderOptions {
47
+ /** A React component to wrap the rendered element. Useful for providing context. */
22
48
  wrapper?: ComponentType<{
23
49
  children: ReactNode;
24
50
  }>;
25
51
  }
52
+ /**
53
+ * Query methods bound to a specific container. All queries return promises
54
+ * that resolve when a matching element is found or reject on timeout.
55
+ */
26
56
  export interface BoundQueries {
57
+ /** Find a single element by its accessible role. */
27
58
  findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
59
+ /** Find a single element by its associated label text. */
28
60
  findByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
61
+ /** Find a single element by its text content. */
29
62
  findByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
63
+ /** Find a single element by its test ID. */
30
64
  findByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
65
+ /** Find all elements matching an accessible role. */
31
66
  findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
67
+ /** Find all elements with matching label text. */
32
68
  findAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
69
+ /** Find all elements with matching text content. */
33
70
  findAllByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
71
+ /** Find all elements with matching test ID. */
34
72
  findAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
35
73
  }
74
+ /**
75
+ * The result returned by the render function. Includes query methods
76
+ * and utilities for interacting with the rendered component.
77
+ */
36
78
  export interface RenderResult extends BoundQueries {
79
+ /** The GTK Application instance containing the rendered component. */
37
80
  container: Gtk.Application;
81
+ /** Unmount the rendered component and clean up resources. */
38
82
  unmount: () => Promise<void>;
83
+ /** Re-render with a new element, preserving state where possible. */
39
84
  rerender: (element: ReactNode) => Promise<void>;
85
+ /** Print the current widget tree to the console for debugging. */
40
86
  debug: () => void;
41
87
  }
@@ -1,36 +1,23 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  /**
3
- * Options for configuring user event behavior.
3
+ * Options for the tab user event.
4
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>;
5
+ export interface TabOptions {
6
+ /** If true, navigates backwards (Shift+Tab behavior). */
7
+ shift?: boolean;
23
8
  }
24
9
  /**
25
10
  * Simulates user interactions with GTK widgets. Provides methods that mimic
26
11
  * real user behavior like clicking, typing, and clearing input fields.
27
- * Use userEvent.setup() to create an instance with custom options.
28
12
  */
29
13
  export declare const userEvent: {
30
- setup: (options?: UserEventOptions) => UserEventInstance;
31
14
  click: (element: Gtk.Widget) => Promise<void>;
32
15
  dblClick: (element: Gtk.Widget) => Promise<void>;
16
+ tripleClick: (element: Gtk.Widget) => Promise<void>;
33
17
  activate: (element: Gtk.Widget) => Promise<void>;
18
+ tab: (element: Gtk.Widget, options?: TabOptions) => Promise<void>;
34
19
  type: (element: Gtk.Widget, text: string) => Promise<void>;
35
20
  clear: (element: Gtk.Widget) => Promise<void>;
21
+ selectOptions: (element: Gtk.Widget, values: string | string[] | number | number[]) => Promise<void>;
22
+ deselectOptions: (element: Gtk.Widget, values: number | number[]) => Promise<void>;
36
23
  };
@@ -1,73 +1,143 @@
1
+ import { getObject } from "@gtkx/ffi";
2
+ import { AccessibleRole, CheckButton, ComboBox, DirectionType, DropDown, Editable, ListBox, ListBoxRow, ToggleButton, Widget, } from "@gtkx/ffi/gtk";
1
3
  import { fireEvent } from "./fire-event.js";
2
4
  import { tick } from "./timing.js";
3
- import { hasGetText, hasSetText } from "./widget.js";
4
- const createUserEventInstance = (_options) => {
5
- return {
6
- click: async (element) => {
7
- fireEvent(element, "clicked");
8
- await tick();
9
- },
10
- dblClick: async (element) => {
11
- fireEvent(element, "clicked");
12
- await tick();
13
- fireEvent(element, "clicked");
14
- await tick();
15
- },
16
- activate: async (element) => {
17
- element.activate();
18
- await tick();
19
- },
20
- type: async (element, text) => {
21
- if (!hasSetText(element)) {
22
- throw new Error("Cannot type into element: no setText method available");
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 currentText = hasGetText(element) ? element.getText() : "";
25
- element.setText(currentText + text);
26
- await tick();
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
- element.setText("");
33
- await tick();
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
- setup: (options) => createUserEventInstance(options),
44
- click: async (element) => {
45
- fireEvent(element, "clicked");
46
- await tick();
47
- },
48
- dblClick: async (element) => {
49
- fireEvent(element, "clicked");
50
- await tick();
51
- fireEvent(element, "clicked");
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 WidgetWithSetText = {
2
- setText: (text: string) => void;
3
- };
4
- type WidgetWithGetText = {
5
- getText: () => string;
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
- export const hasSetText = (widget) => typeof widget.setText === "function";
2
- export const hasGetText = (widget) => typeof widget.getText === "function";
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.49",
3
+ "version": "0.1.51",
4
4
  "description": "Testing utilities for GTKX applications",
5
5
  "keywords": [
6
6
  "gtk",
@@ -32,9 +32,9 @@
32
32
  "dist"
33
33
  ],
34
34
  "dependencies": {
35
- "@gtkx/ffi": "0.1.49",
36
- "@gtkx/native": "0.1.49",
37
- "@gtkx/react": "0.1.49"
35
+ "@gtkx/ffi": "0.1.51",
36
+ "@gtkx/react": "0.1.51",
37
+ "@gtkx/native": "0.1.51"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsc -b && cp ../../README.md .",