@gtkx/testing 0.21.0 → 1.0.0-rc.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/README.md +137 -34
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +56 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +33 -0
- package/dist/bound-queries.d.ts.map +1 -0
- package/dist/bound-queries.js +2 -0
- package/dist/bound-queries.js.map +1 -0
- package/dist/build-queries.d.ts +16 -0
- package/dist/build-queries.d.ts.map +1 -0
- package/dist/build-queries.js +115 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +5 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +21 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +20 -52
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -46
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +8 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +26 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +8 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +11 -30
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +4 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +37 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +17 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +59 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +117 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +24 -26
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +111 -117
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +190 -180
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +291 -320
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.d.ts +2 -0
- package/dist/register-test-hooks.d.ts.map +1 -0
- package/dist/register-test-hooks.js +16 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +15 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +9 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +13 -42
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +138 -121
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +19 -53
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +48 -63
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +7 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +24 -110
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +21 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +98 -50
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +27 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +61 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +12 -10
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +36 -15
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +115 -169
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +10 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +35 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +8 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +73 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +8 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +31 -0
- package/dist/user-event/controller.js.map +1 -0
- package/dist/user-event/dispatch.d.ts +7 -0
- package/dist/user-event/dispatch.d.ts.map +1 -0
- package/dist/user-event/dispatch.js +11 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +4 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +59 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +35 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +138 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +51 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +42 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +10 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +204 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +10 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +30 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +5 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +105 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +7 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +3 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +18 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +113 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +16 -36
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +74 -70
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +34 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +260 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +10 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +16 -25
- package/dist/within.js.map +1 -1
- package/package.json +23 -12
- package/src/act.ts +77 -0
- package/src/bound-queries.ts +37 -0
- package/src/build-queries.ts +226 -0
- package/src/cleanup-registry.ts +27 -0
- package/src/config.ts +35 -62
- package/src/editable.ts +37 -0
- package/src/errors.ts +191 -0
- package/src/fire-event.ts +13 -38
- package/src/frame-sync.ts +51 -0
- package/src/index.ts +58 -15
- package/src/matchers.ts +251 -0
- package/src/pretty-widget.ts +163 -130
- package/src/queries.ts +485 -396
- package/src/register-test-hooks.ts +19 -0
- package/src/render-hook.tsx +38 -45
- package/src/render.tsx +192 -155
- package/src/role-helpers.ts +65 -72
- package/src/screen.ts +26 -114
- package/src/screenshot.ts +128 -56
- package/src/suggestions.ts +106 -0
- package/src/traversal.ts +56 -22
- package/src/types.ts +144 -172
- package/src/user-event/adjustment.ts +62 -0
- package/src/user-event/click.ts +99 -0
- package/src/user-event/controller.ts +58 -0
- package/src/user-event/dispatch.ts +25 -0
- package/src/user-event/event-wrapper.ts +80 -0
- package/src/user-event/gesture.ts +239 -0
- package/src/user-event/index.ts +81 -0
- package/src/user-event/keyboard.ts +290 -0
- package/src/user-event/pointer.ts +46 -0
- package/src/user-event/selection.ts +159 -0
- package/src/user-event/state.ts +8 -0
- package/src/user-event/text.ts +172 -0
- package/src/wait-for.ts +117 -78
- package/src/widget-accessible-properties.ts +388 -0
- package/src/within.ts +29 -27
- package/dist/bind-queries.d.ts +0 -14
- package/dist/bind-queries.d.ts.map +0 -1
- package/dist/bind-queries.js +0 -29
- package/dist/bind-queries.js.map +0 -1
- package/dist/error-builder.d.ts +0 -28
- package/dist/error-builder.d.ts.map +0 -1
- package/dist/error-builder.js +0 -81
- package/dist/error-builder.js.map +0 -1
- package/dist/timing.d.ts +0 -18
- package/dist/timing.d.ts.map +0 -1
- package/dist/timing.js +0 -18
- package/dist/timing.js.map +0 -1
- package/dist/user-event.d.ts +0 -138
- package/dist/user-event.d.ts.map +0 -1
- package/dist/user-event.js +0 -411
- package/dist/user-event.js.map +0 -1
- package/dist/widget-text.d.ts +0 -69
- package/dist/widget-text.d.ts.map +0 -1
- package/dist/widget-text.js +0 -177
- package/dist/widget-text.js.map +0 -1
- package/dist/widget.d.ts +0 -3
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -13
- package/dist/widget.js.map +0 -1
- package/src/bind-queries.ts +0 -52
- package/src/error-builder.ts +0 -102
- package/src/timing.ts +0 -17
- package/src/user-event.ts +0 -492
- package/src/widget-text.ts +0 -202
- package/src/widget.ts +0 -15
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { formatRole } from "./role-helpers.js";
|
|
3
|
+
import {
|
|
4
|
+
getWidgetAccessibleName,
|
|
5
|
+
getWidgetDisplayValue,
|
|
6
|
+
getWidgetLabelledByText,
|
|
7
|
+
getWidgetLabelText,
|
|
8
|
+
getWidgetName,
|
|
9
|
+
getWidgetOwnLabel,
|
|
10
|
+
getWidgetPlaceholderText,
|
|
11
|
+
} from "./widget-accessible-properties.js";
|
|
12
|
+
|
|
13
|
+
/** The query variant a suggestion targets. */
|
|
14
|
+
type Variant = "get" | "getAll" | "query" | "queryAll" | "find" | "findAll";
|
|
15
|
+
/** The query family a suggestion targets. */
|
|
16
|
+
type Method = "Role" | "LabelText" | "PlaceholderText" | "Text" | "DisplayValue" | "Name";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A suggested query for reaching a widget, including its family, variant, method name, and a
|
|
20
|
+
* `toString` that renders the full call.
|
|
21
|
+
*/
|
|
22
|
+
type Suggestion = {
|
|
23
|
+
queryName: Method;
|
|
24
|
+
/** The full query function name, such as `getByRole`. */
|
|
25
|
+
queryMethod: string;
|
|
26
|
+
variant: Variant;
|
|
27
|
+
toString: () => string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const makeSuggestion = (queryName: Method, variant: Variant, argsText: string): Suggestion => {
|
|
31
|
+
const queryMethod = `${variant}By${queryName}`;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
queryName,
|
|
35
|
+
queryMethod,
|
|
36
|
+
variant,
|
|
37
|
+
toString: () => `${queryMethod}(${argsText})`,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const roleSuggestion = (widget: Gtk.Widget, variant: Variant): Suggestion | undefined => {
|
|
42
|
+
const role = widget.getAccessibleRole();
|
|
43
|
+
|
|
44
|
+
if (role === Gtk.AccessibleRole.NONE || role === Gtk.AccessibleRole.GENERIC) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const roleText = `Gtk.AccessibleRole.${formatRole(role).toUpperCase()}`;
|
|
49
|
+
const name = getWidgetAccessibleName(widget);
|
|
50
|
+
|
|
51
|
+
if (name === null) {
|
|
52
|
+
return makeSuggestion("Role", variant, roleText);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return makeSuggestion("Role", variant, `${roleText}, { name: '${name}' }`);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const textSuggestion = (queryName: Method, variant: Variant, value: string | null): Suggestion | undefined => {
|
|
59
|
+
if (value === null) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return makeSuggestion(queryName, variant, `'${value}'`);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Computes the recommended query for reaching the given widget, preferring role, then label text,
|
|
68
|
+
* placeholder text, text, display value, and name.
|
|
69
|
+
* @param widget Widget to build a suggestion for.
|
|
70
|
+
* @param variant Query variant the suggestion should use.
|
|
71
|
+
* @param method Restrict the suggestion to a specific query family instead of choosing by priority.
|
|
72
|
+
* @returns The best available suggestion, or undefined when no query applies.
|
|
73
|
+
*/
|
|
74
|
+
const getSuggestedQuery = (
|
|
75
|
+
widget: Gtk.Widget,
|
|
76
|
+
variant: Variant = "get",
|
|
77
|
+
method?: Method,
|
|
78
|
+
): Suggestion | undefined => {
|
|
79
|
+
const builders: Record<Method, () => Suggestion | undefined> = {
|
|
80
|
+
Role: () => roleSuggestion(widget, variant),
|
|
81
|
+
LabelText: () =>
|
|
82
|
+
textSuggestion("LabelText", variant, getWidgetOwnLabel(widget) ?? getWidgetLabelledByText(widget)),
|
|
83
|
+
PlaceholderText: () => textSuggestion("PlaceholderText", variant, getWidgetPlaceholderText(widget)),
|
|
84
|
+
Text: () => textSuggestion("Text", variant, getWidgetLabelText(widget)),
|
|
85
|
+
DisplayValue: () => textSuggestion("DisplayValue", variant, getWidgetDisplayValue(widget)),
|
|
86
|
+
Name: () => textSuggestion("Name", variant, getWidgetName(widget)),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (method) {
|
|
90
|
+
return builders[method]();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const priority: Method[] = ["Role", "LabelText", "PlaceholderText", "Text", "DisplayValue", "Name"];
|
|
94
|
+
|
|
95
|
+
for (const candidate of priority) {
|
|
96
|
+
const suggestion = builders[candidate]();
|
|
97
|
+
|
|
98
|
+
if (suggestion) {
|
|
99
|
+
return suggestion;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export { getSuggestedQuery, type Variant, type Method, type Suggestion };
|
package/src/traversal.ts
CHANGED
|
@@ -1,49 +1,83 @@
|
|
|
1
|
-
import * as Gtk from "@gtkx/
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
|
|
3
|
+
type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* windows. When a `Gtk.Widget` is provided, queries are scoped to that
|
|
8
|
-
* widget's subtree.
|
|
5
|
+
* A scope that queries and traversal can run against: a widget, an event
|
|
6
|
+
* controller, a layout manager, a list item or header, an application, or the
|
|
7
|
+
* sentinel representing all current toplevel windows.
|
|
9
8
|
*/
|
|
10
|
-
|
|
9
|
+
type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const TOPLEVELS: unique symbol = Symbol("gtkx.toplevels");
|
|
12
|
+
|
|
13
|
+
const isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;
|
|
14
14
|
|
|
15
15
|
const traverseWidgetTree = function* (root: Gtk.Widget): Generator<Gtk.Widget> {
|
|
16
16
|
yield root;
|
|
17
|
-
|
|
18
17
|
let child = root.getFirstChild();
|
|
18
|
+
|
|
19
19
|
while (child) {
|
|
20
20
|
yield* traverseWidgetTree(child);
|
|
21
21
|
child = child.getNextSibling();
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const descendants = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
|
|
26
|
+
const tree = traverseWidgetTree(widget);
|
|
27
|
+
tree.next();
|
|
28
|
+
yield* tree;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolveRoot = (container: QueryContainer): Gtk.Widget | null => {
|
|
32
|
+
if (container instanceof Gtk.Widget) {
|
|
33
|
+
return container;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (container instanceof Gtk.EventController) {
|
|
37
|
+
return container.getWidget();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (container instanceof Gtk.LayoutManager) {
|
|
41
|
+
return container.getWidget();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader) {
|
|
45
|
+
return container.getChild();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const roots = function* (container: Container): Generator<Gtk.Widget> {
|
|
52
|
+
if (container === TOPLEVELS || isApplication(container)) {
|
|
53
|
+
yield* Gtk.Window.listToplevels();
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const root = resolveRoot(container);
|
|
59
|
+
|
|
60
|
+
if (root) {
|
|
61
|
+
yield root;
|
|
29
62
|
}
|
|
30
63
|
};
|
|
31
64
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
yield*
|
|
35
|
-
} else {
|
|
36
|
-
yield* traverseWidgetTree(container);
|
|
65
|
+
const traverse = function* (container: Container): Generator<Gtk.Widget> {
|
|
66
|
+
for (const root of roots(container)) {
|
|
67
|
+
yield* traverseWidgetTree(root);
|
|
37
68
|
}
|
|
38
69
|
};
|
|
39
70
|
|
|
40
|
-
|
|
71
|
+
const findAll = (container: Container, isMatch: (node: Gtk.Widget) => boolean): Gtk.Widget[] => {
|
|
41
72
|
const results: Gtk.Widget[] = [];
|
|
73
|
+
|
|
42
74
|
for (const node of traverse(container)) {
|
|
43
|
-
if (node
|
|
44
|
-
if (predicate(node)) {
|
|
75
|
+
if (isMatch(node)) {
|
|
45
76
|
results.push(node);
|
|
46
77
|
}
|
|
47
78
|
}
|
|
79
|
+
|
|
48
80
|
return results;
|
|
49
81
|
};
|
|
82
|
+
|
|
83
|
+
export { TOPLEVELS, descendants, roots, traverse, findAll, type Container };
|
package/src/types.ts
CHANGED
|
@@ -1,210 +1,182 @@
|
|
|
1
|
-
import type * as Gtk from "@gtkx/
|
|
2
|
-
import type {
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import type { RootElement } from "@gtkx/react";
|
|
3
|
+
import type { ComponentType, ErrorInfo, ReactNode } from "react";
|
|
4
|
+
import type { PrettyWidgetOptions } from "./pretty-widget.js";
|
|
3
5
|
import type { Container } from "./traversal.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param content - The normalized text content to match against
|
|
9
|
-
* @param widget - The widget being tested
|
|
10
|
-
* @returns `true` if the content matches
|
|
8
|
+
* A custom matcher predicate: given a candidate widget's normalized text content and the widget
|
|
9
|
+
* itself, returns whether it matches.
|
|
11
10
|
*/
|
|
12
|
-
|
|
11
|
+
type MatcherFunction = (content: string, widget: Gtk.Widget) => boolean;
|
|
12
|
+
/** A value used to match widget text: a string or number, a regular expression, or a custom {@link MatcherFunction}. */
|
|
13
|
+
type Matcher = string | number | RegExp | MatcherFunction;
|
|
14
|
+
/** Normalizes a widget's text before it is compared against a matcher. */
|
|
15
|
+
type NormalizerFn = (text: string) => string;
|
|
13
16
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export type TextMatch = string | RegExp | TextMatchFunction;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Options for text normalization before matching.
|
|
24
|
-
*/
|
|
25
|
-
export type NormalizerOptions = {
|
|
26
|
-
/** Remove leading/trailing whitespace (default: true) */
|
|
27
|
-
trim?: boolean;
|
|
28
|
-
/** Replace multiple whitespace characters with single space (default: true) */
|
|
29
|
-
collapseWhitespace?: boolean;
|
|
17
|
+
/** Options controlling the default text normalizer. */
|
|
18
|
+
type NormalizerOptions = {
|
|
19
|
+
/** Trim leading and trailing whitespace. */
|
|
20
|
+
trim?: boolean | undefined;
|
|
21
|
+
/** Collapse runs of whitespace into a single space. */
|
|
22
|
+
collapseWhitespace?: boolean | undefined;
|
|
30
23
|
};
|
|
31
24
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/** Replace multiple whitespace with single space (default: true) */
|
|
43
|
-
collapseWhitespace?: boolean;
|
|
44
|
-
/** Timeout in milliseconds for async queries (default: 1000) */
|
|
45
|
-
timeout?: number;
|
|
25
|
+
/** Options controlling how asynchronous queries and waits poll for a condition. */
|
|
26
|
+
type WaitForOptions = {
|
|
27
|
+
/** Maximum time in milliseconds to keep retrying before failing. */
|
|
28
|
+
timeout?: number | undefined;
|
|
29
|
+
/** Delay in milliseconds between retries. */
|
|
30
|
+
interval?: number | undefined;
|
|
31
|
+
/** Transforms the error thrown when the timeout elapses. */
|
|
32
|
+
onTimeout?: ((error: Error) => Error) | undefined;
|
|
33
|
+
/** Error whose stack trace attributes the timeout failure to the calling code. */
|
|
34
|
+
stackTraceError?: Error | undefined;
|
|
46
35
|
};
|
|
47
36
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
|
|
37
|
+
/** Options controlling text matching and, for asynchronous queries, polling behavior. */
|
|
38
|
+
type MatcherOptions = {
|
|
39
|
+
/** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
|
|
40
|
+
exact?: boolean | undefined;
|
|
41
|
+
/** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
|
|
42
|
+
normalizer?: NormalizerFn | undefined;
|
|
43
|
+
/** Forwarded to the default normalizer when no custom `normalizer` is given. */
|
|
44
|
+
trim?: boolean | undefined;
|
|
45
|
+
/** Forwarded to the default normalizer when no custom `normalizer` is given. */
|
|
46
|
+
collapseWhitespace?: boolean | undefined;
|
|
47
|
+
/** Whether to include a suggested better query in error messages. */
|
|
48
|
+
suggest?: boolean | undefined;
|
|
49
|
+
} & WaitForOptions;
|
|
50
|
+
|
|
51
|
+
/** Constraints on a widget's numeric range value used by role queries. */
|
|
52
|
+
type ByRoleValue = {
|
|
53
|
+
/** The current value. */
|
|
54
|
+
now?: number | undefined;
|
|
55
|
+
min?: number | undefined;
|
|
56
|
+
max?: number | undefined;
|
|
57
|
+
/** Matcher for the value's textual representation. */
|
|
58
|
+
text?: Matcher | undefined;
|
|
66
59
|
};
|
|
67
60
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
/** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
|
|
62
|
+
type ByRoleOptions = MatcherOptions & {
|
|
63
|
+
name?: Matcher | undefined;
|
|
64
|
+
checked?: boolean | undefined;
|
|
65
|
+
pressed?: boolean | undefined;
|
|
66
|
+
selected?: boolean | undefined;
|
|
67
|
+
expanded?: boolean | undefined;
|
|
68
|
+
/** Heading or hierarchy level. */
|
|
69
|
+
level?: number | undefined;
|
|
70
|
+
busy?: boolean | undefined;
|
|
71
|
+
description?: Matcher | undefined;
|
|
72
|
+
value?: ByRoleValue | undefined;
|
|
73
|
+
/** When true, include widgets excluded from the accessibility tree. */
|
|
74
|
+
hidden?: boolean | undefined;
|
|
78
75
|
};
|
|
79
76
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
* Accept `ref` as a prop and pass it through to the root intrinsic element.
|
|
83
|
-
*/
|
|
84
|
-
export type WrapperComponent = ComponentType<{
|
|
77
|
+
/** A React component that wraps rendered content, receiving it as its children. */
|
|
78
|
+
type WrapperComponent = ComponentType<{
|
|
85
79
|
children: ReactNode;
|
|
86
|
-
ref?: Ref<Gtk.Widget>;
|
|
87
80
|
}>;
|
|
88
81
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* - Component: Custom wrapper that passes `ref` to its root element
|
|
98
|
-
*/
|
|
99
|
-
wrapper?: boolean | WrapperComponent;
|
|
100
|
-
/**
|
|
101
|
-
* The element queries are bound to.
|
|
102
|
-
* Defaults to the GTK Application (searches all toplevel windows).
|
|
103
|
-
* Provide a specific widget or application to scope queries.
|
|
104
|
-
*/
|
|
105
|
-
baseElement?: Container;
|
|
106
|
-
};
|
|
82
|
+
type Query = (container: Container, ...args: never[]) => unknown;
|
|
83
|
+
type QueryMap = Record<string, Query>;
|
|
84
|
+
|
|
85
|
+
type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
|
|
86
|
+
? (...args: A) => R
|
|
87
|
+
: never;
|
|
88
|
+
|
|
89
|
+
type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
|
|
107
90
|
|
|
108
91
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @see {@link screen} for global queries
|
|
112
|
-
* @see {@link within} for scoped queries
|
|
92
|
+
* Options for {@link render}: the container and base element to mount into, an optional wrapper,
|
|
93
|
+
* React behavior toggles, error callbacks, and custom queries to bind.
|
|
113
94
|
*/
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
/** Query all elements by widget name (returns empty array if none found) */
|
|
130
|
-
queryAllByName: (name: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
|
|
131
|
-
/** Find single element by accessible role (waits and throws if not found) */
|
|
132
|
-
findByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
133
|
-
/** Find single element by label/text content (waits and throws if not found) */
|
|
134
|
-
findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
135
|
-
/** Find single element by visible text (waits and throws if not found) */
|
|
136
|
-
findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
137
|
-
/** Find single element by widget name (waits and throws if not found) */
|
|
138
|
-
findByName: (name: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
139
|
-
/** Find all elements by accessible role (waits and throws if none found) */
|
|
140
|
-
findAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
141
|
-
/** Find all elements by label/text content (waits and throws if none found) */
|
|
142
|
-
findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
143
|
-
/** Find all elements by visible text (waits and throws if none found) */
|
|
144
|
-
findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
145
|
-
/** Find all elements by widget name (waits and throws if none found) */
|
|
146
|
-
findAllByName: (name: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
95
|
+
type RenderOptions<Q extends QueryMap = Record<never, never>> = {
|
|
96
|
+
container?: Gtk.Widget | RootElement | undefined;
|
|
97
|
+
/** Root of the subtree that bound queries search. */
|
|
98
|
+
baseElement?: Container | undefined;
|
|
99
|
+
wrapper?: WrapperComponent | undefined;
|
|
100
|
+
/** Render inside React StrictMode. */
|
|
101
|
+
reactStrictMode?: boolean | undefined;
|
|
102
|
+
/** Enable widget animations during the test. */
|
|
103
|
+
animations?: boolean | undefined;
|
|
104
|
+
/** Called for errors caught by React error boundaries. */
|
|
105
|
+
onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
106
|
+
/** Called for errors React recovered from automatically. */
|
|
107
|
+
onRecoverableError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
108
|
+
/** Custom queries to bind to the rendered result. */
|
|
109
|
+
queries?: Q | undefined;
|
|
147
110
|
};
|
|
148
111
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*/
|
|
154
|
-
export type RenderResult = BoundQueries & {
|
|
155
|
-
/** The direct container widget wrapping the rendered content */
|
|
156
|
-
container: Gtk.Widget;
|
|
157
|
-
/** The element queries are bound to (defaults to the GTK Application) */
|
|
158
|
-
baseElement: Container;
|
|
159
|
-
/** Unmount the rendered component */
|
|
160
|
-
unmount: () => Promise<void>;
|
|
161
|
-
/** Re-render with a new element */
|
|
162
|
-
rerender: (element: ReactNode) => Promise<void>;
|
|
163
|
-
/** Print the widget tree to console for debugging */
|
|
164
|
-
debug: () => void;
|
|
112
|
+
type DebugUtilities = {
|
|
113
|
+
debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
|
|
114
|
+
logRoles: () => void;
|
|
115
|
+
screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
165
116
|
};
|
|
166
117
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
*/
|
|
170
|
-
export type ScreenshotResult = {
|
|
171
|
-
/** Base64-encoded PNG image data */
|
|
118
|
+
/** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
|
|
119
|
+
type ScreenshotResult = {
|
|
172
120
|
data: string;
|
|
173
|
-
/** MIME type of the image (always "image/png") */
|
|
174
121
|
mimeType: string;
|
|
175
|
-
/** Width of the captured image in pixels */
|
|
176
122
|
width: number;
|
|
177
|
-
/** Height of the captured image in pixels */
|
|
178
123
|
height: number;
|
|
179
124
|
};
|
|
180
125
|
|
|
126
|
+
/** Options for capturing a screenshot: the poll timeout and interval, plus a rendering scale factor. */
|
|
127
|
+
type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
|
|
128
|
+
/** Device scale factor applied when rendering. */
|
|
129
|
+
scale?: number;
|
|
130
|
+
};
|
|
131
|
+
|
|
181
132
|
/**
|
|
182
|
-
*
|
|
133
|
+
* Selects the window to screenshot by index, or by title (exact string or regular expression);
|
|
134
|
+
* undefined targets the default window.
|
|
183
135
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
136
|
+
type WindowSelector = number | string | RegExp | undefined;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Options for {@link renderHook}: an optional wrapper and the initial props (required unless the
|
|
140
|
+
* props type permits undefined).
|
|
141
|
+
*/
|
|
142
|
+
type RenderHookOptions<Props> = {
|
|
143
|
+
wrapper?: WrapperComponent;
|
|
144
|
+
} & (undefined extends Props
|
|
145
|
+
? {
|
|
146
|
+
initialProps?: Props;
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
initialProps: Props;
|
|
150
|
+
});
|
|
197
151
|
|
|
198
152
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* Provides access to the hook result and utilities for re-rendering and cleanup.
|
|
153
|
+
* The result of {@link renderHook}: the latest hook return value plus functions to rerender with new
|
|
154
|
+
* props and to unmount.
|
|
202
155
|
*/
|
|
203
|
-
|
|
204
|
-
/**
|
|
156
|
+
type RenderHookResult<Result, Props> = {
|
|
157
|
+
/** Holds the most recent value returned by the hook under `current`. */
|
|
205
158
|
result: { current: Result };
|
|
206
|
-
/** Re-render the hook with optional new props */
|
|
207
159
|
rerender: (newProps?: Props) => Promise<void>;
|
|
208
|
-
/** Unmount the component containing the hook */
|
|
209
160
|
unmount: () => Promise<void>;
|
|
210
161
|
};
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
type MatcherFunction,
|
|
165
|
+
type Matcher,
|
|
166
|
+
type NormalizerFn,
|
|
167
|
+
type NormalizerOptions,
|
|
168
|
+
type WaitForOptions,
|
|
169
|
+
type MatcherOptions,
|
|
170
|
+
type ByRoleValue,
|
|
171
|
+
type ByRoleOptions,
|
|
172
|
+
type WrapperComponent,
|
|
173
|
+
type QueryMap,
|
|
174
|
+
type BoundCustomQueries,
|
|
175
|
+
type RenderOptions,
|
|
176
|
+
type DebugUtilities,
|
|
177
|
+
type ScreenshotResult,
|
|
178
|
+
type ScreenshotOptions,
|
|
179
|
+
type WindowSelector,
|
|
180
|
+
type RenderHookOptions,
|
|
181
|
+
type RenderHookResult,
|
|
182
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
3
|
+
|
|
4
|
+
/** A scroll distance in pixels along each axis. */
|
|
5
|
+
type ScrollDelta = {
|
|
6
|
+
x?: number;
|
|
7
|
+
y?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ScrollAdjustments = {
|
|
11
|
+
horizontal: Gtk.Adjustment | null;
|
|
12
|
+
vertical: Gtk.Adjustment | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const slide = (widget: Gtk.Widget, value: number): Promise<void> =>
|
|
16
|
+
wrapEvent(widget, () => {
|
|
17
|
+
if (!(widget instanceof Gtk.Range)) {
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`userEvent.slide requires a Gtk.Range (e.g. Gtk.Scale), got ${widget.constructor.name}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
widget.emit("change-value", Gtk.ScrollType.JUMP, value);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null => {
|
|
27
|
+
for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
|
|
28
|
+
if (current instanceof Gtk.ScrolledWindow) {
|
|
29
|
+
return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (current instanceof Gtk.Scrollable) {
|
|
33
|
+
return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
|
|
41
|
+
if (!adjustment || delta === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
adjustment.setValue(adjustment.getValue() + delta);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
|
|
49
|
+
wrapEvent(widget, () => {
|
|
50
|
+
const adjustments = resolveScrollAdjustments(widget);
|
|
51
|
+
|
|
52
|
+
if (!adjustments) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"userEvent.scroll: no Gtk.ScrolledWindow or Gtk.Scrollable found on the widget or its ancestors",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
|
|
59
|
+
applyScrollDelta(adjustments.vertical, delta.y ?? 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { slide, scroll, type ScrollDelta };
|