@gtkx/testing 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bind-queries.d.ts +1 -0
- package/dist/bind-queries.d.ts.map +1 -0
- package/dist/bind-queries.js +1 -0
- package/dist/bind-queries.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -0
- package/dist/error-builder.d.ts +1 -0
- package/dist/error-builder.d.ts.map +1 -0
- package/dist/error-builder.js +1 -0
- package/dist/error-builder.js.map +1 -0
- package/dist/fire-event.d.ts +1 -0
- package/dist/fire-event.d.ts.map +1 -0
- package/dist/fire-event.js +1 -0
- package/dist/fire-event.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/pretty-widget.d.ts +1 -0
- package/dist/pretty-widget.d.ts.map +1 -0
- package/dist/pretty-widget.js +1 -0
- package/dist/pretty-widget.js.map +1 -0
- package/dist/queries.d.ts +1 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +1 -0
- package/dist/queries.js.map +1 -0
- package/dist/render-hook.d.ts +1 -0
- package/dist/render-hook.d.ts.map +1 -0
- package/dist/render-hook.js +1 -0
- package/dist/render-hook.js.map +1 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1 -0
- package/dist/render.js.map +1 -0
- package/dist/role-helpers.d.ts +1 -0
- package/dist/role-helpers.d.ts.map +1 -0
- package/dist/role-helpers.js +1 -0
- package/dist/role-helpers.js.map +1 -0
- package/dist/screen.d.ts +1 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +1 -0
- package/dist/screen.js.map +1 -0
- package/dist/screenshot.d.ts +1 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +1 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +1 -0
- package/dist/timing.js.map +1 -0
- package/dist/traversal.d.ts +8 -0
- package/dist/traversal.d.ts.map +1 -0
- package/dist/traversal.js +1 -0
- package/dist/traversal.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/user-event.d.ts +1 -0
- package/dist/user-event.d.ts.map +1 -0
- package/dist/user-event.js +1 -0
- package/dist/user-event.js.map +1 -0
- package/dist/wait-for.d.ts +1 -0
- package/dist/wait-for.d.ts.map +1 -0
- package/dist/wait-for.js +1 -0
- package/dist/wait-for.js.map +1 -0
- package/dist/widget-text.d.ts +1 -0
- package/dist/widget-text.d.ts.map +1 -0
- package/dist/widget-text.js +1 -0
- package/dist/widget-text.js.map +1 -0
- package/dist/widget.d.ts +1 -0
- package/dist/widget.d.ts.map +1 -0
- package/dist/widget.js +1 -0
- package/dist/widget.js.map +1 -0
- package/dist/within.d.ts +1 -0
- package/dist/within.d.ts.map +1 -0
- package/dist/within.js +1 -0
- package/dist/within.js.map +1 -0
- package/package.json +7 -5
- package/src/bind-queries.ts +52 -0
- package/src/config.ts +89 -0
- package/src/error-builder.ts +102 -0
- package/src/fire-event.ts +43 -0
- package/src/index.ts +51 -0
- package/src/pretty-widget.ts +205 -0
- package/src/queries.ts +511 -0
- package/src/render-hook.tsx +71 -0
- package/src/render.tsx +192 -0
- package/src/role-helpers.ts +126 -0
- package/src/screen.ts +125 -0
- package/src/screenshot.ts +105 -0
- package/src/timing.ts +17 -0
- package/src/traversal.ts +48 -0
- package/src/types.ts +210 -0
- package/src/user-event.ts +492 -0
- package/src/wait-for.ts +115 -0
- package/src/widget-text.ts +206 -0
- package/src/widget.ts +15 -0
- package/src/within.ts +31 -0
package/src/wait-for.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { buildTimeoutError } from "./error-builder.js";
|
|
4
|
+
import type { WaitForOptions } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_INTERVAL = 50;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Waits for a callback to succeed.
|
|
10
|
+
*
|
|
11
|
+
* Repeatedly calls the callback until it returns without throwing,
|
|
12
|
+
* or until the timeout is reached.
|
|
13
|
+
*
|
|
14
|
+
* @param callback - Function to execute repeatedly
|
|
15
|
+
* @param options - Timeout and interval configuration
|
|
16
|
+
* @returns Promise resolving to the callback's return value
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { waitFor } from "@gtkx/testing";
|
|
21
|
+
*
|
|
22
|
+
* await waitFor(() => {
|
|
23
|
+
* expect(counter.value).toBe(5);
|
|
24
|
+
* }, { timeout: 2000 });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const waitFor = async <T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T> => {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
let lastError: Error | null = null;
|
|
32
|
+
|
|
33
|
+
while (Date.now() - startTime < timeout) {
|
|
34
|
+
try {
|
|
35
|
+
return await callback();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
lastError = error as Error;
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const timeoutError = buildTimeoutError(timeout, lastError);
|
|
43
|
+
if (onTimeout) {
|
|
44
|
+
throw onTimeout(timeoutError);
|
|
45
|
+
}
|
|
46
|
+
throw timeoutError;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ElementOrCallback = Gtk.Widget | (() => Gtk.Widget | null);
|
|
50
|
+
|
|
51
|
+
const getElement = (elementOrCallback: ElementOrCallback): Gtk.Widget | null => {
|
|
52
|
+
if (typeof elementOrCallback === "function") {
|
|
53
|
+
return elementOrCallback();
|
|
54
|
+
}
|
|
55
|
+
return elementOrCallback;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isElementRemoved = (element: Gtk.Widget | null): boolean => {
|
|
59
|
+
if (element === null) return true;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const parent = element.getParent();
|
|
63
|
+
return parent === null;
|
|
64
|
+
} catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Waits for an element to be removed from the widget tree.
|
|
71
|
+
*
|
|
72
|
+
* Polls until the element no longer has a parent or no longer exists.
|
|
73
|
+
*
|
|
74
|
+
* @param elementOrCallback - Element or function returning element to watch
|
|
75
|
+
* @param options - Timeout and interval configuration
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* import { waitForElementToBeRemoved } from "@gtkx/testing";
|
|
80
|
+
*
|
|
81
|
+
* const loader = await screen.findByRole(Gtk.AccessibleRole.PROGRESS_BAR);
|
|
82
|
+
* await waitForElementToBeRemoved(loader);
|
|
83
|
+
* // Loader is now gone
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const waitForElementToBeRemoved = async (
|
|
87
|
+
elementOrCallback: ElementOrCallback,
|
|
88
|
+
options?: WaitForOptions,
|
|
89
|
+
): Promise<void> => {
|
|
90
|
+
const config = getConfig();
|
|
91
|
+
const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
92
|
+
|
|
93
|
+
const initialElement = getElement(elementOrCallback);
|
|
94
|
+
if (initialElement === null || isElementRemoved(initialElement)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"Element already removed: waitForElementToBeRemoved requires the element to be present initially",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
|
|
102
|
+
while (Date.now() - startTime < timeout) {
|
|
103
|
+
const element = getElement(elementOrCallback);
|
|
104
|
+
if (isElementRemoved(element)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const timeoutError = new Error(`Timed out after ${timeout}ms waiting for element to be removed.`);
|
|
111
|
+
if (onTimeout) {
|
|
112
|
+
throw onTimeout(timeoutError);
|
|
113
|
+
}
|
|
114
|
+
throw timeoutError;
|
|
115
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getNativeInterface } from "@gtkx/ffi";
|
|
2
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
3
|
+
|
|
4
|
+
const ROLES_WITH_INTERNAL_LABELS = new Set([
|
|
5
|
+
Gtk.AccessibleRole.BUTTON,
|
|
6
|
+
Gtk.AccessibleRole.TOGGLE_BUTTON,
|
|
7
|
+
Gtk.AccessibleRole.CHECKBOX,
|
|
8
|
+
Gtk.AccessibleRole.RADIO,
|
|
9
|
+
Gtk.AccessibleRole.LIST_ITEM,
|
|
10
|
+
Gtk.AccessibleRole.MENU_ITEM,
|
|
11
|
+
Gtk.AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
12
|
+
Gtk.AccessibleRole.MENU_ITEM_RADIO,
|
|
13
|
+
Gtk.AccessibleRole.ROW,
|
|
14
|
+
Gtk.AccessibleRole.TAB,
|
|
15
|
+
Gtk.AccessibleRole.LINK,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const isInternalLabel = (widget: Gtk.Widget): boolean => {
|
|
19
|
+
if (widget.getAccessibleRole() !== Gtk.AccessibleRole.LABEL) return false;
|
|
20
|
+
|
|
21
|
+
const parent = widget.getParent();
|
|
22
|
+
if (!parent) return false;
|
|
23
|
+
|
|
24
|
+
if (parent.getAccessibleRole === undefined) return false;
|
|
25
|
+
const parentRole = parent.getAccessibleRole();
|
|
26
|
+
if (!parentRole) return false;
|
|
27
|
+
|
|
28
|
+
if (ROLES_WITH_INTERNAL_LABELS.has(parentRole)) return true;
|
|
29
|
+
|
|
30
|
+
const labelText = getLabelText(widget);
|
|
31
|
+
if (!labelText) return false;
|
|
32
|
+
|
|
33
|
+
let ancestor: Gtk.Widget | null = parent;
|
|
34
|
+
while (ancestor) {
|
|
35
|
+
if (ancestor.getAccessibleRole === undefined) return false;
|
|
36
|
+
const role = ancestor.getAccessibleRole();
|
|
37
|
+
if (role && ROLES_WITH_INTERNAL_LABELS.has(role)) {
|
|
38
|
+
return getDefaultText(ancestor) === labelText;
|
|
39
|
+
}
|
|
40
|
+
ancestor = ancestor.getParent();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getLabelText = (widget: Gtk.Widget): string | null => {
|
|
47
|
+
const asLabel = widget as Gtk.Label;
|
|
48
|
+
const asInscription = widget as Gtk.Inscription;
|
|
49
|
+
return asLabel.getLabel?.() ?? asInscription.getText?.() ?? null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getDefaultText = (widget: Gtk.Widget): string | null => {
|
|
53
|
+
if ("getLabel" in widget && typeof widget.getLabel === "function") {
|
|
54
|
+
return (widget.getLabel() as string) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ("getText" in widget && typeof widget.getText === "function") {
|
|
58
|
+
return (widget.getText() as string) ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ("getTitle" in widget && typeof widget.getTitle === "function") {
|
|
62
|
+
return (widget.getTitle() as string) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return getNativeInterface(widget, Gtk.Editable)?.getText() ?? null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const collectChildLabels = (widget: Gtk.Widget): string[] => {
|
|
69
|
+
const labels: string[] = [];
|
|
70
|
+
let child = widget.getFirstChild();
|
|
71
|
+
|
|
72
|
+
while (child) {
|
|
73
|
+
if (child.getAccessibleRole() === Gtk.AccessibleRole.LABEL) {
|
|
74
|
+
const labelText = getLabelText(child);
|
|
75
|
+
if (labelText) labels.push(labelText);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
labels.push(...collectChildLabels(child));
|
|
79
|
+
child = child.getNextSibling();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return labels;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extracts the accessible text content from a widget based on its role.
|
|
87
|
+
*
|
|
88
|
+
* @param widget - The widget to extract text from
|
|
89
|
+
* @returns The accessible text or null if none found
|
|
90
|
+
*/
|
|
91
|
+
export const getWidgetText = (widget: Gtk.Widget): string | null => {
|
|
92
|
+
if (isInternalLabel(widget)) return null;
|
|
93
|
+
|
|
94
|
+
const role = widget.getAccessibleRole();
|
|
95
|
+
if (role === undefined) return null;
|
|
96
|
+
|
|
97
|
+
switch (role) {
|
|
98
|
+
case Gtk.AccessibleRole.BUTTON:
|
|
99
|
+
case Gtk.AccessibleRole.LINK:
|
|
100
|
+
case Gtk.AccessibleRole.TAB:
|
|
101
|
+
case Gtk.AccessibleRole.MENU_ITEM:
|
|
102
|
+
case Gtk.AccessibleRole.MENU_ITEM_CHECKBOX:
|
|
103
|
+
case Gtk.AccessibleRole.MENU_ITEM_RADIO: {
|
|
104
|
+
const directLabel = getDefaultText(widget);
|
|
105
|
+
if (directLabel) return directLabel;
|
|
106
|
+
|
|
107
|
+
const childLabels = collectChildLabels(widget);
|
|
108
|
+
return childLabels.length > 0 ? childLabels.join(" ") : null;
|
|
109
|
+
}
|
|
110
|
+
case Gtk.AccessibleRole.TAB_PANEL: {
|
|
111
|
+
const parent = widget.getParent();
|
|
112
|
+
if (parent) {
|
|
113
|
+
const stack = parent as Gtk.Stack;
|
|
114
|
+
const page = stack.getPage?.(widget);
|
|
115
|
+
if (page) {
|
|
116
|
+
return page.getTitle() ?? null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
return getDefaultText(widget);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gets the test ID from a widget (uses the widget's name property).
|
|
128
|
+
*
|
|
129
|
+
* @param widget - The widget to get the test ID from
|
|
130
|
+
* @returns The test ID or null if not set
|
|
131
|
+
*/
|
|
132
|
+
export const getWidgetTestId = (widget: Gtk.Widget): string | null => {
|
|
133
|
+
return widget.getName();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Gets the checked state from toggle-like widgets.
|
|
138
|
+
*
|
|
139
|
+
* @param widget - The widget to get the checked state from
|
|
140
|
+
* @returns The checked state or null if not applicable
|
|
141
|
+
*/
|
|
142
|
+
export const getWidgetCheckedState = (widget: Gtk.Widget): boolean | null => {
|
|
143
|
+
const role = widget.getAccessibleRole();
|
|
144
|
+
|
|
145
|
+
switch (role) {
|
|
146
|
+
case Gtk.AccessibleRole.CHECKBOX:
|
|
147
|
+
case Gtk.AccessibleRole.RADIO:
|
|
148
|
+
return (widget as Gtk.CheckButton).getActive();
|
|
149
|
+
case Gtk.AccessibleRole.TOGGLE_BUTTON:
|
|
150
|
+
return (widget as Gtk.ToggleButton).getActive();
|
|
151
|
+
case Gtk.AccessibleRole.SWITCH:
|
|
152
|
+
return (widget as Gtk.Switch).getActive();
|
|
153
|
+
default:
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets the pressed state from toggle button widgets.
|
|
160
|
+
*
|
|
161
|
+
* @param widget - The widget to get the pressed state from
|
|
162
|
+
* @returns The pressed state or null if not applicable
|
|
163
|
+
*/
|
|
164
|
+
export const getWidgetPressedState = (widget: Gtk.Widget): boolean | null => {
|
|
165
|
+
const role = widget.getAccessibleRole();
|
|
166
|
+
|
|
167
|
+
if (role === Gtk.AccessibleRole.TOGGLE_BUTTON) {
|
|
168
|
+
return (widget as Gtk.ToggleButton).getActive();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets the expanded state from expander widgets.
|
|
176
|
+
*
|
|
177
|
+
* @param widget - The widget to get the expanded state from
|
|
178
|
+
* @returns The expanded state or null if not applicable
|
|
179
|
+
*/
|
|
180
|
+
export const getWidgetExpandedState = (widget: Gtk.Widget): boolean | null => {
|
|
181
|
+
if (widget instanceof Gtk.Expander) {
|
|
182
|
+
return widget.getExpanded();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (widget instanceof Gtk.TreeExpander) {
|
|
186
|
+
return widget.getListRow()?.getExpanded() ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets the selected state from selectable widgets.
|
|
194
|
+
*
|
|
195
|
+
* @param widget - The widget to get the selected state from
|
|
196
|
+
* @returns The selected state or null if not applicable
|
|
197
|
+
*/
|
|
198
|
+
export const getWidgetSelectedState = (widget: Gtk.Widget): boolean | null => {
|
|
199
|
+
const role = widget.getAccessibleRole();
|
|
200
|
+
|
|
201
|
+
if (role === Gtk.AccessibleRole.ROW) {
|
|
202
|
+
return (widget as Gtk.ListBoxRow).isSelected();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
};
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
|
|
3
|
+
const EDITABLE_ROLES = new Set([
|
|
4
|
+
Gtk.AccessibleRole.TEXT_BOX,
|
|
5
|
+
Gtk.AccessibleRole.SEARCH_BOX,
|
|
6
|
+
Gtk.AccessibleRole.SPIN_BUTTON,
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const isEditable = (widget: unknown): widget is Gtk.Editable => {
|
|
10
|
+
if (!(widget instanceof Gtk.Widget)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return EDITABLE_ROLES.has(widget.getAccessibleRole());
|
|
15
|
+
};
|
package/src/within.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import { bindQueries } from "./bind-queries.js";
|
|
3
|
+
import type { BoundQueries } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates scoped query methods for a container widget.
|
|
7
|
+
*
|
|
8
|
+
* Use this to query within a specific section of your UI rather than
|
|
9
|
+
* the entire application.
|
|
10
|
+
*
|
|
11
|
+
* @param container - The widget to scope queries to
|
|
12
|
+
* @returns Object with query methods bound to the container
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { render, within } from "@gtkx/testing";
|
|
17
|
+
*
|
|
18
|
+
* test("scoped queries", async () => {
|
|
19
|
+
* await render(<MyPage />);
|
|
20
|
+
*
|
|
21
|
+
* const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
|
|
22
|
+
* const sidebarQueries = within(sidebar);
|
|
23
|
+
*
|
|
24
|
+
* // Only searches within the sidebar
|
|
25
|
+
* const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @see {@link screen} for global queries
|
|
30
|
+
*/
|
|
31
|
+
export const within = (container: Gtk.Widget): BoundQueries => bindQueries(container);
|