@gtkx/testing 0.9.3 → 0.10.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.
@@ -59,7 +59,7 @@ const tab = async (element, options) => {
59
59
  };
60
60
  const type = async (element, text) => {
61
61
  if (!isEditable(element)) {
62
- throw new Error("Cannot type into element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
62
+ throw new Error("Cannot type into element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
63
63
  }
64
64
  const editable = getNativeObject(element.id, Gtk.Editable);
65
65
  if (!editable)
@@ -70,7 +70,7 @@ const type = async (element, text) => {
70
70
  };
71
71
  const clear = async (element) => {
72
72
  if (!isEditable(element)) {
73
- throw new Error("Cannot clear element: element is not editable (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
73
+ throw new Error("Cannot clear element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
74
74
  }
75
75
  getNativeObject(element.id, Gtk.Editable)?.setText("");
76
76
  await tick();
@@ -82,34 +82,53 @@ const isSelectable = (widget) => {
82
82
  return false;
83
83
  return SELECTABLE_ROLES.has(accessible.getAccessibleRole());
84
84
  };
85
+ const selectListViewItems = (selectionModel, positions, exclusive) => {
86
+ if (positions.length === 0) {
87
+ selectionModel.unselectRange(0, selectionModel.getNItems());
88
+ return;
89
+ }
90
+ if (exclusive && positions.length === 1) {
91
+ selectionModel.selectItem(positions[0], true);
92
+ return;
93
+ }
94
+ const nItems = selectionModel.getNItems();
95
+ const selected = new Gtk.Bitset();
96
+ const mask = Gtk.Bitset.newRange(0, nItems);
97
+ for (const pos of positions) {
98
+ selected.add(pos);
99
+ }
100
+ selectionModel.setSelection(selected, mask);
101
+ };
102
+ const isListView = (widget) => {
103
+ return widget instanceof Gtk.ListView || widget instanceof Gtk.GridView || widget instanceof Gtk.ColumnView;
104
+ };
85
105
  const selectOptions = async (element, values) => {
106
+ const valueArray = Array.isArray(values) ? values : [values];
107
+ if (isListView(element)) {
108
+ const selectionModel = element.getModel();
109
+ const isMultiSelection = selectionModel instanceof Gtk.MultiSelection;
110
+ selectListViewItems(selectionModel, valueArray, !isMultiSelection);
111
+ await tick();
112
+ return;
113
+ }
86
114
  if (!isSelectable(element)) {
87
- throw new Error("Cannot select options: element is not a selectable widget (COMBO_BOX or LIST)");
115
+ throw new Error("Cannot select options: expected selectable widget (COMBO_BOX or LIST)");
88
116
  }
89
117
  const role = getNativeObject(element.id, Gtk.Accessible)?.getAccessibleRole();
90
- const valueArray = Array.isArray(values) ? values : [values];
91
118
  if (role === Gtk.AccessibleRole.COMBO_BOX) {
92
- if (valueArray.length > 1) {
93
- throw new Error("Cannot select multiple options on a ComboBox/DropDown");
94
- }
95
- const value = valueArray[0];
96
- if (typeof value !== "number") {
97
- throw new Error("ComboBox/DropDown selection requires a numeric index");
119
+ if (Array.isArray(values) && values.length > 1) {
120
+ throw new Error("Cannot select multiple options: ComboBox only supports single selection");
98
121
  }
99
- const isDropDown = element.constructor.name === "DropDown";
100
- if (isDropDown) {
101
- element.setSelected(value);
122
+ if (element instanceof Gtk.DropDown) {
123
+ element.setSelected(valueArray[0]);
102
124
  }
103
125
  else {
104
- element.setActive(value);
126
+ element.setActive(valueArray[0]);
105
127
  }
106
128
  }
107
129
  else if (role === Gtk.AccessibleRole.LIST) {
108
130
  const listBox = element;
109
131
  for (const value of valueArray) {
110
- if (typeof value !== "number") {
111
- throw new Error("ListBox selection requires numeric indices");
112
- }
113
132
  const row = listBox.getRowAtIndex(value);
114
133
  if (row) {
115
134
  listBox.selectRow(row);
@@ -120,12 +139,20 @@ const selectOptions = async (element, values) => {
120
139
  await tick();
121
140
  };
122
141
  const deselectOptions = async (element, values) => {
142
+ const valueArray = Array.isArray(values) ? values : [values];
143
+ if (isListView(element)) {
144
+ const selectionModel = element.getModel();
145
+ for (const pos of valueArray) {
146
+ selectionModel.unselectItem(pos);
147
+ }
148
+ await tick();
149
+ return;
150
+ }
123
151
  const role = getNativeObject(element.id, Gtk.Accessible)?.getAccessibleRole();
124
152
  if (role !== Gtk.AccessibleRole.LIST) {
125
153
  throw new Error("Cannot deselect options: only ListBox supports deselection");
126
154
  }
127
155
  const listBox = element;
128
- const valueArray = Array.isArray(values) ? values : [values];
129
156
  for (const value of valueArray) {
130
157
  const row = listBox.getRowAtIndex(value);
131
158
  if (row) {
@@ -135,17 +162,91 @@ const deselectOptions = async (element, values) => {
135
162
  await tick();
136
163
  };
137
164
  /**
138
- * Simulates user interactions with GTK widgets. Provides methods that mimic
139
- * real user behavior like clicking, typing, and clearing input fields.
165
+ * User interaction utilities for testing.
166
+ *
167
+ * Simulates user actions like clicking, typing, and selecting.
168
+ * All methods are async and wait for GTK event processing.
169
+ *
170
+ * @example
171
+ * ```tsx
172
+ * import { render, screen, userEvent } from "@gtkx/testing";
173
+ *
174
+ * test("form submission", async () => {
175
+ * await render(<LoginForm />);
176
+ *
177
+ * const input = await screen.findByRole(Gtk.AccessibleRole.TEXT_BOX);
178
+ * await userEvent.type(input, "username");
179
+ *
180
+ * const button = await screen.findByRole(Gtk.AccessibleRole.BUTTON);
181
+ * await userEvent.click(button);
182
+ * });
183
+ * ```
140
184
  */
141
185
  export const userEvent = {
186
+ /**
187
+ * Clicks or toggles a widget.
188
+ *
189
+ * For toggleable widgets (checkboxes, switches, toggle buttons),
190
+ * toggles the active state. For buttons, emits clicked signal.
191
+ */
142
192
  click,
193
+ /**
194
+ * Double-clicks a widget.
195
+ *
196
+ * Emits two consecutive clicked signals.
197
+ */
143
198
  dblClick,
199
+ /**
200
+ * Triple-clicks a widget.
201
+ *
202
+ * Emits three consecutive clicked signals. Useful for text selection.
203
+ */
144
204
  tripleClick,
205
+ /**
206
+ * Activates a widget.
207
+ *
208
+ * Calls the widget's activate method.
209
+ */
145
210
  activate,
211
+ /**
212
+ * Simulates Tab key navigation.
213
+ *
214
+ * @param element - Starting element
215
+ * @param options - Use `shift: true` for backwards navigation
216
+ */
146
217
  tab,
218
+ /**
219
+ * Types text into an editable widget.
220
+ *
221
+ * Appends text to the current content. Works with Entry, SearchEntry,
222
+ * and SpinButton widgets.
223
+ *
224
+ * @param element - The editable widget
225
+ * @param text - Text to type
226
+ */
147
227
  type,
228
+ /**
229
+ * Clears an editable widget's content.
230
+ *
231
+ * Sets the text to empty string.
232
+ */
148
233
  clear,
234
+ /**
235
+ * Selects options in a dropdown or list.
236
+ *
237
+ * Works with DropDown, ComboBox, ListBox, ListView, GridView, and ColumnView.
238
+ *
239
+ * @param element - The selectable widget
240
+ * @param values - Index or array of indices to select
241
+ */
149
242
  selectOptions,
243
+ /**
244
+ * Deselects options in a list.
245
+ *
246
+ * Works with ListBox and multi-selection list views.
247
+ *
248
+ * @param element - The selectable widget
249
+ * @param values - Index or array of indices to deselect
250
+ */
150
251
  deselectOptions,
151
252
  };
@@ -1,20 +1,42 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { WaitForOptions } from "./types.js";
3
3
  /**
4
- * Waits for a callback to succeed without throwing an error.
5
- * @param callback - Function to execute repeatedly until it succeeds
6
- * @param options - Wait options (timeout, interval, onTimeout)
4
+ * Waits for a callback to succeed.
5
+ *
6
+ * Repeatedly calls the callback until it returns without throwing,
7
+ * or until the timeout is reached.
8
+ *
9
+ * @param callback - Function to execute repeatedly
10
+ * @param options - Timeout and interval configuration
7
11
  * @returns Promise resolving to the callback's return value
8
- * @throws If the callback keeps failing after the timeout period
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { waitFor } from "@gtkx/testing";
16
+ *
17
+ * await waitFor(() => {
18
+ * expect(counter.value).toBe(5);
19
+ * }, { timeout: 2000 });
20
+ * ```
9
21
  */
10
22
  export declare const waitFor: <T>(callback: () => T, options?: WaitForOptions) => Promise<T>;
11
23
  type ElementOrCallback = Gtk.Widget | (() => Gtk.Widget | null);
12
24
  /**
13
25
  * Waits for an element to be removed from the widget tree.
14
- * @param elementOrCallback - The element to watch or a callback returning the element
15
- * @param options - Wait options (timeout, interval, onTimeout)
16
- * @returns Promise resolving when the element is removed
17
- * @throws If the element is already removed when called, or if timeout is reached
26
+ *
27
+ * Polls until the element no longer has a parent or no longer exists.
28
+ *
29
+ * @param elementOrCallback - Element or function returning element to watch
30
+ * @param options - Timeout and interval configuration
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * import { waitForElementToBeRemoved } from "@gtkx/testing";
35
+ *
36
+ * const loader = await screen.findByRole(Gtk.AccessibleRole.PROGRESS_BAR);
37
+ * await waitForElementToBeRemoved(loader);
38
+ * // Loader is now gone
39
+ * ```
18
40
  */
19
41
  export declare const waitForElementToBeRemoved: (elementOrCallback: ElementOrCallback, options?: WaitForOptions) => Promise<void>;
20
42
  export {};
package/dist/wait-for.js CHANGED
@@ -1,11 +1,23 @@
1
1
  const DEFAULT_TIMEOUT = 1000;
2
2
  const DEFAULT_INTERVAL = 50;
3
3
  /**
4
- * Waits for a callback to succeed without throwing an error.
5
- * @param callback - Function to execute repeatedly until it succeeds
6
- * @param options - Wait options (timeout, interval, onTimeout)
4
+ * Waits for a callback to succeed.
5
+ *
6
+ * Repeatedly calls the callback until it returns without throwing,
7
+ * or until the timeout is reached.
8
+ *
9
+ * @param callback - Function to execute repeatedly
10
+ * @param options - Timeout and interval configuration
7
11
  * @returns Promise resolving to the callback's return value
8
- * @throws If the callback keeps failing after the timeout period
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { waitFor } from "@gtkx/testing";
16
+ *
17
+ * await waitFor(() => {
18
+ * expect(counter.value).toBe(5);
19
+ * }, { timeout: 2000 });
20
+ * ```
9
21
  */
10
22
  export const waitFor = async (callback, options) => {
11
23
  const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
@@ -45,17 +57,26 @@ const isElementRemoved = (element) => {
45
57
  };
46
58
  /**
47
59
  * Waits for an element to be removed from the widget tree.
48
- * @param elementOrCallback - The element to watch or a callback returning the element
49
- * @param options - Wait options (timeout, interval, onTimeout)
50
- * @returns Promise resolving when the element is removed
51
- * @throws If the element is already removed when called, or if timeout is reached
60
+ *
61
+ * Polls until the element no longer has a parent or no longer exists.
62
+ *
63
+ * @param elementOrCallback - Element or function returning element to watch
64
+ * @param options - Timeout and interval configuration
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * import { waitForElementToBeRemoved } from "@gtkx/testing";
69
+ *
70
+ * const loader = await screen.findByRole(Gtk.AccessibleRole.PROGRESS_BAR);
71
+ * await waitForElementToBeRemoved(loader);
72
+ * // Loader is now gone
73
+ * ```
52
74
  */
53
75
  export const waitForElementToBeRemoved = async (elementOrCallback, options) => {
54
76
  const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
55
77
  const initialElement = getElement(elementOrCallback);
56
78
  if (initialElement === null) {
57
- throw new Error("The element(s) given to waitForElementToBeRemoved are already removed. " +
58
- "waitForElementToBeRemoved requires that the element is present before waiting for removal.");
79
+ throw new Error("Elements already removed: waitForElementToBeRemoved requires elements to be present initially");
59
80
  }
60
81
  const startTime = Date.now();
61
82
  while (Date.now() - startTime < timeout) {
package/dist/within.d.ts CHANGED
@@ -1,18 +1,29 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type { BoundQueries } from "./types.js";
3
3
  /**
4
- * Scopes queries to a specific container element.
5
- * Returns an object with all query methods bound to the given container,
6
- * allowing you to search only within a subtree of the widget hierarchy.
4
+ * Creates scoped query methods for a container widget.
5
+ *
6
+ * Use this to query within a specific section of your UI rather than
7
+ * the entire application.
7
8
  *
8
9
  * @param container - The widget to scope queries to
9
- * @returns An object containing all query methods bound to the container
10
+ * @returns Object with query methods bound to the container
10
11
  *
11
12
  * @example
12
13
  * ```tsx
13
- * const dialog = await screen.findByRole(AccessibleRole.DIALOG);
14
- * const { findByText } = within(dialog);
15
- * const button = await findByText("Confirm");
14
+ * import { render, within } from "@gtkx/testing";
15
+ *
16
+ * test("scoped queries", async () => {
17
+ * await render(<MyPage />);
18
+ *
19
+ * const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
20
+ * const sidebarQueries = within(sidebar);
21
+ *
22
+ * // Only searches within the sidebar
23
+ * const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
24
+ * });
16
25
  * ```
26
+ *
27
+ * @see {@link screen} for global queries
17
28
  */
18
29
  export declare const within: (container: Gtk.Widget) => BoundQueries;
package/dist/within.js CHANGED
@@ -1,18 +1,29 @@
1
1
  import * as queries from "./queries.js";
2
2
  /**
3
- * Scopes queries to a specific container element.
4
- * Returns an object with all query methods bound to the given container,
5
- * allowing you to search only within a subtree of the widget hierarchy.
3
+ * Creates scoped query methods for a container widget.
4
+ *
5
+ * Use this to query within a specific section of your UI rather than
6
+ * the entire application.
6
7
  *
7
8
  * @param container - The widget to scope queries to
8
- * @returns An object containing all query methods bound to the container
9
+ * @returns Object with query methods bound to the container
9
10
  *
10
11
  * @example
11
12
  * ```tsx
12
- * const dialog = await screen.findByRole(AccessibleRole.DIALOG);
13
- * const { findByText } = within(dialog);
14
- * const button = await findByText("Confirm");
13
+ * import { render, within } from "@gtkx/testing";
14
+ *
15
+ * test("scoped queries", async () => {
16
+ * await render(<MyPage />);
17
+ *
18
+ * const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
19
+ * const sidebarQueries = within(sidebar);
20
+ *
21
+ * // Only searches within the sidebar
22
+ * const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
23
+ * });
15
24
  * ```
25
+ *
26
+ * @see {@link screen} for global queries
16
27
  */
17
28
  export const within = (container) => ({
18
29
  findByRole: (role, options) => queries.findByRole(container, role, options),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/testing",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "Testing utilities for GTKX applications",
5
5
  "keywords": [
6
6
  "gtk",
@@ -32,9 +32,13 @@
32
32
  "dist"
33
33
  ],
34
34
  "dependencies": {
35
- "@gtkx/ffi": "0.9.3",
36
- "@gtkx/react": "0.9.3",
37
- "@gtkx/native": "0.9.3"
35
+ "@gtkx/ffi": "0.10.0",
36
+ "@gtkx/native": "0.10.0",
37
+ "@gtkx/react": "0.10.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/react-reconciler": "^0.32.3",
41
+ "react-reconciler": "^0.33.0"
38
42
  },
39
43
  "scripts": {
40
44
  "build": "tsc -b && cp ../../README.md .",