@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.
- package/README.md +55 -67
- package/dist/fire-event.d.ts +14 -8
- package/dist/fire-event.js +14 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/queries.d.ts +59 -24
- package/dist/queries.js +71 -38
- package/dist/render.d.ts +39 -10
- package/dist/render.js +63 -27
- package/dist/screen.d.ts +28 -3
- package/dist/screen.js +29 -4
- package/dist/timing.d.ts +16 -0
- package/dist/timing.js +16 -0
- package/dist/types.d.ts +55 -48
- package/dist/user-event.d.ts +79 -5
- package/dist/user-event.js +121 -20
- package/dist/wait-for.d.ts +30 -8
- package/dist/wait-for.js +31 -10
- package/dist/within.d.ts +18 -7
- package/dist/within.js +18 -7
- package/package.json +8 -4
package/dist/user-event.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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 (
|
|
93
|
-
throw new Error("Cannot select multiple options
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
element.setSelected(value);
|
|
122
|
+
if (element instanceof Gtk.DropDown) {
|
|
123
|
+
element.setSelected(valueArray[0]);
|
|
102
124
|
}
|
|
103
125
|
else {
|
|
104
|
-
element.setActive(
|
|
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
|
-
*
|
|
139
|
-
*
|
|
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
|
};
|
package/dist/wait-for.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @
|
|
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("
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
10
|
+
* @returns Object with query methods bound to the container
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```tsx
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
9
|
+
* @returns Object with query methods bound to the container
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```tsx
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
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.
|
|
36
|
-
"@gtkx/
|
|
37
|
-
"@gtkx/
|
|
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 .",
|