@gtkx/testing 1.0.0-rc.3 → 1.0.0-rc.4
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 +4 -5
- package/dist/act.d.ts +2 -0
- package/dist/act.d.ts.map +1 -1
- package/dist/act.js.map +1 -1
- package/dist/build-queries.d.ts.map +1 -1
- package/dist/build-queries.js +18 -7
- package/dist/build-queries.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/matchers.d.ts +44 -0
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js.map +1 -1
- package/dist/pretty-widget.d.ts +2 -1
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +3 -3
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +1 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js.map +1 -1
- package/dist/render.js +2 -2
- package/dist/render.js.map +1 -1
- package/dist/suggestions.d.ts +3 -0
- package/dist/suggestions.d.ts.map +1 -1
- package/dist/suggestions.js.map +1 -1
- package/dist/traversal.d.ts +5 -0
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +1 -0
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +43 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +13 -0
- package/dist/user-event/adjustment.d.ts.map +1 -1
- package/dist/user-event/adjustment.js +11 -0
- package/dist/user-event/adjustment.js.map +1 -1
- package/dist/user-event/click.d.ts +7 -0
- package/dist/user-event/click.d.ts.map +1 -1
- package/dist/user-event/click.js +7 -0
- package/dist/user-event/click.js.map +1 -1
- package/dist/user-event/controller.d.ts +29 -0
- package/dist/user-event/controller.d.ts.map +1 -1
- package/dist/user-event/controller.js +28 -0
- package/dist/user-event/controller.js.map +1 -1
- package/dist/user-event/gesture.d.ts +51 -0
- package/dist/user-event/gesture.d.ts.map +1 -1
- package/dist/user-event/gesture.js +47 -4
- package/dist/user-event/gesture.js.map +1 -1
- package/dist/user-event/index.d.ts +27 -0
- package/dist/user-event/index.d.ts.map +1 -1
- package/dist/user-event/index.js.map +1 -1
- package/dist/user-event/keyboard.d.ts +4 -2
- package/dist/user-event/keyboard.d.ts.map +1 -1
- package/dist/user-event/keyboard.js +13 -12
- package/dist/user-event/keyboard.js.map +1 -1
- package/dist/user-event/pointer.js +5 -5
- package/dist/user-event/pointer.js.map +1 -1
- package/dist/user-event/selection.d.ts +15 -0
- package/dist/user-event/selection.d.ts.map +1 -1
- package/dist/user-event/selection.js +15 -0
- package/dist/user-event/selection.js.map +1 -1
- package/dist/user-event/state.d.ts +1 -1
- package/dist/user-event/state.d.ts.map +1 -1
- package/dist/user-event/state.js +1 -1
- package/dist/user-event/state.js.map +1 -1
- package/dist/user-event/text.d.ts +30 -3
- package/dist/user-event/text.d.ts.map +1 -1
- package/dist/user-event/text.js +28 -1
- package/dist/user-event/text.js.map +1 -1
- package/dist/wait-for.d.ts +2 -0
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +17 -1
- package/dist/widget-accessible-properties.d.ts.map +1 -1
- package/dist/widget-accessible-properties.js +17 -0
- package/dist/widget-accessible-properties.js.map +1 -1
- package/package.json +7 -7
- package/src/act.ts +2 -0
- package/src/build-queries.ts +20 -8
- package/src/config.ts +4 -3
- package/src/errors.ts +2 -2
- package/src/matchers.ts +44 -0
- package/src/pretty-widget.ts +5 -4
- package/src/queries.ts +1 -0
- package/src/render.tsx +2 -2
- package/src/suggestions.ts +3 -0
- package/src/traversal.ts +5 -0
- package/src/types.ts +43 -5
- package/src/user-event/adjustment.ts +13 -0
- package/src/user-event/click.ts +7 -0
- package/src/user-event/controller.ts +29 -0
- package/src/user-event/gesture.ts +57 -6
- package/src/user-event/index.ts +27 -0
- package/src/user-event/keyboard.ts +18 -16
- package/src/user-event/pointer.ts +5 -5
- package/src/user-event/selection.ts +15 -0
- package/src/user-event/state.ts +2 -2
- package/src/user-event/text.ts +31 -4
- package/src/wait-for.ts +2 -0
- package/src/widget-accessible-properties.ts +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "Testing Library-style queries, user events, and screenshots for GTK4 widget trees rendered by GTKX",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -44,20 +44,20 @@
|
|
|
44
44
|
"node": ">=24"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@gtkx/
|
|
48
|
-
"@gtkx/
|
|
49
|
-
"@gtkx/utils": "1.0.0-rc.
|
|
47
|
+
"@gtkx/react": "1.0.0-rc.4",
|
|
48
|
+
"@gtkx/runtime": "1.0.0-rc.4",
|
|
49
|
+
"@gtkx/utils": "1.0.0-rc.4"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@types/react": "^19.2",
|
|
53
53
|
"react": "^19.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@types/react": "^19.2.
|
|
56
|
+
"@types/react": "^19.2.18",
|
|
57
57
|
"@vitest/expect": "^4.1.10",
|
|
58
58
|
"vitest": "^4.1.10",
|
|
59
|
-
"@gtkx/
|
|
60
|
-
"@gtkx/
|
|
59
|
+
"@gtkx/vitest": "1.0.0-rc.4",
|
|
60
|
+
"@gtkx/config": "1.0.0-rc.4"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"release": "tsx ../../scripts/release-package.ts"
|
package/src/act.ts
CHANGED
|
@@ -4,7 +4,9 @@ declare global {
|
|
|
4
4
|
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** Work run inside the act environment, either synchronous or asynchronous. */
|
|
7
8
|
type ActCallback<T> = () => T | PromiseLike<T>;
|
|
9
|
+
/** Runs a callback inside React's act environment and settles with its result once updates flush. */
|
|
8
10
|
type ActImplementation = <T>(callback: ActCallback<T>) => PromiseLike<T>;
|
|
9
11
|
|
|
10
12
|
const actImplementation: ActImplementation = reactAct;
|
package/src/build-queries.ts
CHANGED
|
@@ -39,7 +39,7 @@ const extractWaitForOptions = (args: unknown[]): WaitForOptions => {
|
|
|
39
39
|
return {};
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const extractShouldSuggest = (args: unknown[]): boolean | undefined => {
|
|
43
43
|
const last = args.at(-1);
|
|
44
44
|
|
|
45
45
|
if (last && typeof last === "object" && !(last instanceof RegExp)) {
|
|
@@ -54,12 +54,11 @@ const maybeThrowSuggestion = (options: {
|
|
|
54
54
|
match: Gtk.Widget;
|
|
55
55
|
queryName: Method;
|
|
56
56
|
variant: Variant;
|
|
57
|
-
|
|
57
|
+
shouldSuggest: boolean | undefined;
|
|
58
58
|
}): void => {
|
|
59
|
-
const { container, match, queryName, variant,
|
|
60
|
-
const shouldSuggest = suggest ?? getConfig().throwSuggestions;
|
|
59
|
+
const { container, match, queryName, variant, shouldSuggest } = options;
|
|
61
60
|
|
|
62
|
-
if (!shouldSuggest) {
|
|
61
|
+
if (!(shouldSuggest ?? getConfig().throwSuggestions)) {
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -141,7 +140,13 @@ const wrapSingleWithSuggestion =
|
|
|
141
140
|
const match = query(container, ...args);
|
|
142
141
|
|
|
143
142
|
if (match) {
|
|
144
|
-
maybeThrowSuggestion({
|
|
143
|
+
maybeThrowSuggestion({
|
|
144
|
+
container,
|
|
145
|
+
match,
|
|
146
|
+
queryName,
|
|
147
|
+
variant,
|
|
148
|
+
shouldSuggest: extractShouldSuggest(args),
|
|
149
|
+
});
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
return match;
|
|
@@ -159,7 +164,7 @@ const wrapAllWithSuggestion =
|
|
|
159
164
|
match: first,
|
|
160
165
|
queryName,
|
|
161
166
|
variant,
|
|
162
|
-
|
|
167
|
+
shouldSuggest: extractShouldSuggest(args),
|
|
163
168
|
});
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -197,7 +202,14 @@ const buildQueries = <Args extends unknown[]>(
|
|
|
197
202
|
|
|
198
203
|
const getByWithSuggestion: (container: Container, ...args: Args) => Gtk.Widget = (container, ...args) => {
|
|
199
204
|
const match = getBy(container, ...args);
|
|
200
|
-
|
|
205
|
+
|
|
206
|
+
maybeThrowSuggestion({
|
|
207
|
+
container,
|
|
208
|
+
match,
|
|
209
|
+
queryName,
|
|
210
|
+
variant: "get",
|
|
211
|
+
shouldSuggest: extractShouldSuggest(args),
|
|
212
|
+
});
|
|
201
213
|
|
|
202
214
|
return match;
|
|
203
215
|
};
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Container } from "./traversal.js";
|
|
|
6
6
|
* checks.
|
|
7
7
|
*/
|
|
8
8
|
type Config = {
|
|
9
|
+
/** Whether a successful query throws instead, naming a better query, when one is available. */
|
|
9
10
|
throwSuggestions: boolean;
|
|
10
11
|
|
|
11
12
|
/** Builds the error thrown when a query fails, given a message and optional container. */
|
|
@@ -34,7 +35,7 @@ const defaultConfig: Config = {
|
|
|
34
35
|
const currentConfig: Config = { ...defaultConfig };
|
|
35
36
|
|
|
36
37
|
function defaultGetElementError(message: string): Error {
|
|
37
|
-
return new
|
|
38
|
+
return new ElementError(message);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -55,8 +56,8 @@ const configure = (newConfig: Partial<Config> | ConfigFn): void => {
|
|
|
55
56
|
Object.assign(currentConfig, updates);
|
|
56
57
|
};
|
|
57
58
|
|
|
58
|
-
class
|
|
59
|
-
override name = "
|
|
59
|
+
class ElementError extends Error {
|
|
60
|
+
override name = "ElementError";
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export { getConfig, configure, type Config, type ConfigFn };
|
package/src/errors.ts
CHANGED
|
@@ -125,7 +125,7 @@ const buildElementError = (container: Container, headLines: string[]): Error =>
|
|
|
125
125
|
|
|
126
126
|
const lines = expensiveErrorDiagnostics.isDisabled
|
|
127
127
|
? headLines
|
|
128
|
-
: [...headLines, "", prettyWidget(container, {
|
|
128
|
+
: [...headLines, "", prettyWidget(container, { shouldHighlight: false })];
|
|
129
129
|
|
|
130
130
|
return config.getElementError(lines.join("\n"), container);
|
|
131
131
|
};
|
|
@@ -160,7 +160,7 @@ const multipleFoundError = (container: Container, descriptor: QueryDescriptor, m
|
|
|
160
160
|
];
|
|
161
161
|
|
|
162
162
|
if (!expensiveErrorDiagnostics.isDisabled) {
|
|
163
|
-
const renderedMatches = matches.map((widget) => prettyWidget(widget, {
|
|
163
|
+
const renderedMatches = matches.map((widget) => prettyWidget(widget, { shouldHighlight: false }));
|
|
164
164
|
headLines.push("", "Here are the matching elements:", "", ...renderedMatches);
|
|
165
165
|
}
|
|
166
166
|
|
package/src/matchers.ts
CHANGED
|
@@ -39,45 +39,89 @@ type TextContentOptions = {
|
|
|
39
39
|
|
|
40
40
|
/** The expected value for a style class: an exact class name or a regular expression. */
|
|
41
41
|
type ClassExpectation = string | RegExp;
|
|
42
|
+
/** The outcome of a matcher: whether it passed, and the failure text built on demand. */
|
|
42
43
|
type MatcherResult = { pass: boolean; message: () => string };
|
|
44
|
+
/** The matcher state bound as `this`, supplying the test runner's deep equality check. */
|
|
43
45
|
type MatcherContext = { equals: (actual: unknown, expected: unknown) => boolean };
|
|
46
|
+
/** Compares text read from the received widget; with no expected value, asserts the text is non-empty. */
|
|
44
47
|
type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
|
|
48
|
+
/** Asserts a single state of the received widget, taking no expected value. */
|
|
45
49
|
type StateMatcher = (received: unknown) => MatcherResult;
|
|
46
50
|
type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
|
|
47
51
|
type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
|
|
48
52
|
|
|
53
|
+
/** A text matcher that also takes normalization options for the text it reads. */
|
|
49
54
|
type TextContentMatcher = (
|
|
50
55
|
received: unknown,
|
|
51
56
|
expected?: TextExpectation,
|
|
52
57
|
options?: TextContentOptions,
|
|
53
58
|
) => MatcherResult;
|
|
54
59
|
|
|
60
|
+
/** Signatures of the widget assertion matchers, keyed by the name each is registered under. */
|
|
55
61
|
type MatcherImplementations = {
|
|
62
|
+
/** Asserts the text an editable widget or combo box shows; with no argument, that it is not empty. */
|
|
56
63
|
toHaveDisplayValue: TextMatcher;
|
|
64
|
+
/** Asserts the widget's own text, or its descendants' when it has none, contains or matches the expectation. */
|
|
57
65
|
toHaveTextContent: TextContentMatcher;
|
|
66
|
+
/** Asserts the widget's accessible name; with no argument, that it has one. */
|
|
58
67
|
toHaveAccessibleName: TextMatcher;
|
|
68
|
+
/**
|
|
69
|
+
* Asserts the joined accessible names of the widget's `described-by` targets, falling back to its
|
|
70
|
+
* `description` and then to its tooltip.
|
|
71
|
+
*/
|
|
59
72
|
toHaveAccessibleDescription: TextMatcher;
|
|
73
|
+
/** Asserts the joined accessible names of the widget's `error-message` targets, read only while it is invalid. */
|
|
60
74
|
toHaveAccessibleErrorMessage: TextMatcher;
|
|
75
|
+
/** Asserts the widget's placeholder text; with no argument, that it has some. */
|
|
61
76
|
toHavePlaceholderText: TextMatcher;
|
|
77
|
+
/** Asserts the text currently selected in an editable widget. */
|
|
62
78
|
toHaveSelection: TextMatcher;
|
|
79
|
+
/** Asserts the widget's checked state is checked rather than unchecked or mixed. */
|
|
63
80
|
toBeChecked: StateMatcher;
|
|
81
|
+
/** Asserts the widget's checked state is mixed. */
|
|
64
82
|
toBePartiallyChecked: StateMatcher;
|
|
83
|
+
/** Asserts a `Gtk.ToggleButton` is active, and throws for any other widget. */
|
|
65
84
|
toBePressed: StateMatcher;
|
|
85
|
+
/** Asserts a `Gtk.Expander` or the row behind a `Gtk.TreeExpander` is expanded, and throws for anything else. */
|
|
66
86
|
toBeExpanded: StateMatcher;
|
|
87
|
+
/**
|
|
88
|
+
* Asserts a `Gtk.ListBoxRow` or `Gtk.FlowBoxChild` is selected, or that a widget in a selectable role
|
|
89
|
+
* carries the selected state flag, and throws for anything else.
|
|
90
|
+
*/
|
|
67
91
|
toBeSelected: StateMatcher;
|
|
92
|
+
/** Asserts the widget is insensitive, or sits inside an insensitive ancestor. */
|
|
68
93
|
toBeDisabled: StateMatcher;
|
|
94
|
+
/** Asserts the widget and all of its ancestors are sensitive. */
|
|
69
95
|
toBeEnabled: StateMatcher;
|
|
96
|
+
/** Asserts the widget and its ancestors are all visible, and that none of them is fully transparent. */
|
|
70
97
|
toBeVisible: StateMatcher;
|
|
98
|
+
/** Asserts the widget's root is a window that is still in the toplevel list. */
|
|
71
99
|
toBeRooted: StateMatcher;
|
|
100
|
+
/** Asserts the widget has neither a child nor label text. */
|
|
72
101
|
toBeEmpty: StateMatcher;
|
|
102
|
+
/** Asserts the widget's accessible invalid state is set to anything other than false. */
|
|
73
103
|
toBeInvalid: StateMatcher;
|
|
104
|
+
/** Asserts the widget's accessible invalid state is unset or false. */
|
|
74
105
|
toBeValid: StateMatcher;
|
|
106
|
+
/** Asserts the widget's accessible required state. */
|
|
75
107
|
toBeRequired: StateMatcher;
|
|
108
|
+
/** Asserts the widget holds the platform focus state. */
|
|
76
109
|
toHaveFocus: StateMatcher;
|
|
110
|
+
/** Asserts a widget's numeric value, or its display value when a string is given. */
|
|
77
111
|
toHaveValue: (received: unknown, expected?: number | string) => MatcherResult;
|
|
112
|
+
/** Asserts the widget's accessible role. */
|
|
78
113
|
toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
|
|
114
|
+
/** Asserts the widget is, or is an ancestor of, the given widget. */
|
|
79
115
|
toContainElement: (received: unknown, descendant: Gtk.Widget | null) => MatcherResult;
|
|
116
|
+
/**
|
|
117
|
+
* Asserts the widget carries every given style class, each a name, a space-separated list, or a pattern.
|
|
118
|
+
* Pass `{ exact: true }` last to require exactly that set, which rules out patterns.
|
|
119
|
+
*/
|
|
80
120
|
toHaveClass: (received: unknown, ...args: unknown[]) => MatcherResult;
|
|
121
|
+
/**
|
|
122
|
+
* Asserts a GObject property, named as in GObject and looked up by its camel-cased accessor, equals the
|
|
123
|
+
* expected value. With no expected value, asserts the property is set. GObject values compare by identity.
|
|
124
|
+
*/
|
|
81
125
|
toHaveObjectProperty: (this: MatcherContext, received: unknown, ...args: unknown[]) => MatcherResult;
|
|
82
126
|
};
|
|
83
127
|
|
package/src/pretty-widget.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { formatRole } from "./role-helpers.js";
|
|
|
4
4
|
import { type Container, roots } from "./traversal.js";
|
|
5
5
|
import { getWidgetNodeText } from "./widget-accessible-properties.js";
|
|
6
6
|
|
|
7
|
+
/** Produces the value of the `id` attribute printed first on a widget's opening tag. */
|
|
7
8
|
type WidgetIdResolver = (widget: Gtk.Widget) => string;
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -14,7 +15,7 @@ type PrettyWidgetOptions = {
|
|
|
14
15
|
/** Truncates the output once it exceeds this many characters. */
|
|
15
16
|
maxLength?: number;
|
|
16
17
|
/** Whether to apply ANSI color highlighting; defaults to the terminal capabilities. */
|
|
17
|
-
|
|
18
|
+
shouldHighlight?: boolean;
|
|
18
19
|
/** Resolves an `id` attribute to show for each widget. */
|
|
19
20
|
getId?: WidgetIdResolver;
|
|
20
21
|
/** Stops descending past this depth, replacing deeper children with a summary line. */
|
|
@@ -76,7 +77,7 @@ const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [s
|
|
|
76
77
|
return [...idAttrs, ...otherAttrs];
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
+
const isHighlightSupported = (): boolean => {
|
|
80
81
|
if (typeof process === "undefined") {
|
|
81
82
|
return false;
|
|
82
83
|
}
|
|
@@ -206,8 +207,8 @@ const prettyWidget = (container: Container, options: PrettyWidgetOptions = {}):
|
|
|
206
207
|
return "";
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
const
|
|
210
|
-
const colors = createColors(
|
|
210
|
+
const shouldHighlight = options.shouldHighlight ?? isHighlightSupported();
|
|
211
|
+
const colors = createColors(shouldHighlight);
|
|
211
212
|
let output = "";
|
|
212
213
|
|
|
213
214
|
for (const root of roots(container)) {
|
package/src/queries.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
isWidgetChecked,
|
|
31
31
|
} from "./widget-accessible-properties.js";
|
|
32
32
|
|
|
33
|
+
/** The built-in queries in their unbound form, each taking the container to search as its first argument. */
|
|
33
34
|
type BuiltinQueries = QueryFamilies<[container: Container]>;
|
|
34
35
|
|
|
35
36
|
const roleQueries = nameQueryFamily(
|
package/src/render.tsx
CHANGED
|
@@ -165,7 +165,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
|
|
|
165
165
|
options?: RenderOptions<Q>,
|
|
166
166
|
): Promise<RenderResult<Q>> => {
|
|
167
167
|
installErrorHandler();
|
|
168
|
-
applyEnableAnimations(options?.
|
|
168
|
+
applyEnableAnimations(options?.areAnimationsEnabled === true);
|
|
169
169
|
const baseElement: Container = options?.baseElement ?? TOPLEVELS;
|
|
170
170
|
const Wrapper = options?.wrapper;
|
|
171
171
|
const resolved = resolveContainer(options?.container);
|
|
@@ -184,7 +184,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
|
|
|
184
184
|
const wrap = (node: ReactNode): ReactNode => {
|
|
185
185
|
const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
|
|
186
186
|
|
|
187
|
-
return options?.
|
|
187
|
+
return options?.isReactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
|
|
188
188
|
};
|
|
189
189
|
|
|
190
190
|
await update(wrap(element), root);
|
package/src/suggestions.ts
CHANGED
|
@@ -20,10 +20,13 @@ type Method = "Role" | "LabelText" | "PlaceholderText" | "Text" | "DisplayValue"
|
|
|
20
20
|
* `toString` that renders the full call.
|
|
21
21
|
*/
|
|
22
22
|
type Suggestion = {
|
|
23
|
+
/** The query family, such as `Role`. */
|
|
23
24
|
queryName: Method;
|
|
24
25
|
/** The full query function name, such as `getByRole`. */
|
|
25
26
|
queryMethod: string;
|
|
27
|
+
/** The variant the suggestion was asked for, which prefixes `Suggestion.queryMethod`. */
|
|
26
28
|
variant: Variant;
|
|
29
|
+
/** Renders the suggested call with its arguments, such as `getByRole(Gtk.AccessibleRole.BUTTON)`. */
|
|
27
30
|
toString: () => string;
|
|
28
31
|
};
|
|
29
32
|
|
package/src/traversal.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A scope that resolves to a single root widget: the widget itself, the widget a controller or
|
|
5
|
+
* layout manager is attached to, or a list item's or header's child.
|
|
6
|
+
*/
|
|
3
7
|
type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
|
|
4
8
|
/**
|
|
5
9
|
* A scope that queries and traversal can run against: a widget, an event
|
|
@@ -8,6 +12,7 @@ type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk
|
|
|
8
12
|
*/
|
|
9
13
|
type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
|
|
10
14
|
|
|
15
|
+
/** Container sentinel that widens a query to every toplevel window currently open. */
|
|
11
16
|
const TOPLEVELS: unique symbol = Symbol("gtkx.toplevels");
|
|
12
17
|
|
|
13
18
|
const isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;
|
package/src/types.ts
CHANGED
|
@@ -59,7 +59,9 @@ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
|
|
|
59
59
|
type ByRoleValue = {
|
|
60
60
|
/** The current value. */
|
|
61
61
|
now?: number | undefined;
|
|
62
|
+
/** The lower bound of the value's range. */
|
|
62
63
|
min?: number | undefined;
|
|
64
|
+
/** The upper bound of the value's range. */
|
|
63
65
|
max?: number | undefined;
|
|
64
66
|
/** Matcher for the value's textual representation. */
|
|
65
67
|
text?: Matcher | undefined;
|
|
@@ -67,15 +69,23 @@ type ByRoleValue = {
|
|
|
67
69
|
|
|
68
70
|
/** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
|
|
69
71
|
type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
|
|
72
|
+
/** Matcher for the widget's accessible name. */
|
|
70
73
|
name?: Matcher | undefined;
|
|
74
|
+
/** Required checked state; a mixed check button reads as neither, so it matches neither value. */
|
|
71
75
|
checked?: boolean | undefined;
|
|
76
|
+
/** Required active state of a toggle button; any other widget matches neither value. */
|
|
72
77
|
pressed?: boolean | undefined;
|
|
78
|
+
/** Required selected state of a row, list item, grid cell, option or tree item. */
|
|
73
79
|
selected?: boolean | undefined;
|
|
80
|
+
/** Required expanded state of an expander or tree expander. */
|
|
74
81
|
expanded?: boolean | undefined;
|
|
75
82
|
/** Heading or hierarchy level. */
|
|
76
83
|
level?: number | undefined;
|
|
84
|
+
/** Required busy state, which an unset state satisfies as false. */
|
|
77
85
|
busy?: boolean | undefined;
|
|
86
|
+
/** Matcher for the widget's own accessible description, ignoring its `described-by` targets. */
|
|
78
87
|
description?: Matcher | undefined;
|
|
88
|
+
/** Constraints on the widget's range value, each checked only when given. */
|
|
79
89
|
value?: ByRoleValue | undefined;
|
|
80
90
|
/** When true, include widgets excluded from the accessibility tree. */
|
|
81
91
|
hidden?: boolean | undefined;
|
|
@@ -86,26 +96,39 @@ type WrapperComponent = ComponentType<{
|
|
|
86
96
|
children: ReactNode;
|
|
87
97
|
}>;
|
|
88
98
|
|
|
99
|
+
/** A custom query, taking the scope to search as its first argument and its matcher arguments after it. */
|
|
89
100
|
type Query = (container: Container, ...args: never[]) => unknown;
|
|
101
|
+
/** Custom queries keyed by the name each is bound under. */
|
|
90
102
|
type QueryMap = Record<string, Query>;
|
|
91
103
|
|
|
104
|
+
/** A query with its container argument already applied. */
|
|
92
105
|
type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
|
|
93
106
|
? (...args: A) => R
|
|
94
107
|
: never;
|
|
95
108
|
|
|
109
|
+
/** Custom queries with their container argument already applied, keyed as they were passed in. */
|
|
96
110
|
type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
|
|
97
111
|
|
|
112
|
+
/** What each variant of a query family yields for the widget type it matched. */
|
|
98
113
|
type QueryFamilyReturns<T extends Gtk.Widget> = {
|
|
114
|
+
/** The single match, or null when nothing matched; throws when more than one matched. */
|
|
99
115
|
queryBy: T | null;
|
|
116
|
+
/** Every match, empty when nothing matched. */
|
|
100
117
|
queryAllBy: T[];
|
|
118
|
+
/** The single match; throws when nothing or more than one matched. */
|
|
101
119
|
getBy: T;
|
|
120
|
+
/** Every match; throws when nothing matched. */
|
|
102
121
|
getAllBy: T[];
|
|
122
|
+
/** The single match, retried until it appears or the timeout elapses. */
|
|
103
123
|
findBy: Promise<T>;
|
|
124
|
+
/** Every match, retried until at least one appears or the timeout elapses. */
|
|
104
125
|
findAllBy: Promise<T[]>;
|
|
105
126
|
};
|
|
106
127
|
|
|
128
|
+
/** What a query family matches against, which fixes the arguments it takes. */
|
|
107
129
|
type QueryKind = "role" | "text" | "name" | "value";
|
|
108
130
|
|
|
131
|
+
/** The arguments a query of the given kind takes after the family's leading ones. */
|
|
109
132
|
type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
|
|
110
133
|
? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
|
|
111
134
|
: Kind extends "name"
|
|
@@ -115,9 +138,8 @@ type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "rol
|
|
|
115
138
|
: [text: Matcher, options?: MatcherOptions<T>];
|
|
116
139
|
|
|
117
140
|
/**
|
|
118
|
-
* One query
|
|
119
|
-
*
|
|
120
|
-
* from an `as` option.
|
|
141
|
+
* One family's query variants, each named for its variant followed by `Suffix` and taking `Head`
|
|
142
|
+
* ahead of the family's own arguments.
|
|
121
143
|
*/
|
|
122
144
|
type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
|
|
123
145
|
[K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
|
|
@@ -125,6 +147,10 @@ type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unk
|
|
|
125
147
|
) => QueryFamilyReturns<T>[K];
|
|
126
148
|
};
|
|
127
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Every built-in query, spanning the Role, LabelText, Text, Name, PlaceholderText, and DisplayValue
|
|
152
|
+
* families, with `Head` prepended to each signature.
|
|
153
|
+
*/
|
|
128
154
|
type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
|
|
129
155
|
QueryFamily<"LabelText", "text", Head> &
|
|
130
156
|
QueryFamily<"Text", "text", Head> &
|
|
@@ -137,14 +163,16 @@ type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
|
|
|
137
163
|
* React behavior toggles, error callbacks, and custom queries to bind.
|
|
138
164
|
*/
|
|
139
165
|
type RenderOptions<Q extends QueryMap = Record<never, never>> = {
|
|
166
|
+
/** Widget or root element to mount into; an undecorated harness window is created when omitted. */
|
|
140
167
|
container?: Gtk.Widget | RootElement | undefined;
|
|
141
168
|
/** Root of the subtree that bound queries search. */
|
|
142
169
|
baseElement?: Container | undefined;
|
|
170
|
+
/** Component wrapped around the rendered element, such as a context provider. */
|
|
143
171
|
wrapper?: WrapperComponent | undefined;
|
|
144
172
|
/** Render inside React StrictMode. */
|
|
145
|
-
|
|
173
|
+
isReactStrictMode?: boolean | undefined;
|
|
146
174
|
/** Enable widget animations during the test. */
|
|
147
|
-
|
|
175
|
+
areAnimationsEnabled?: boolean | undefined;
|
|
148
176
|
/** Called for errors caught by React error boundaries. */
|
|
149
177
|
onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
150
178
|
/** Called for errors React recovered from automatically. */
|
|
@@ -153,17 +181,25 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
|
|
|
153
181
|
queries?: Q | undefined;
|
|
154
182
|
};
|
|
155
183
|
|
|
184
|
+
/** Console helpers bundled with the bound queries on {@link screen} and on a render result. */
|
|
156
185
|
type DebugUtilities = {
|
|
186
|
+
/** Prints the widget tree of the given containers, or of the whole scope, to the console. */
|
|
157
187
|
debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
|
|
188
|
+
/** Prints the accessible roles found in the scope, with the widgets carrying each one. */
|
|
158
189
|
logRoles: () => void;
|
|
190
|
+
/** Captures a window, writes the image to a temporary file, and logs its path. */
|
|
159
191
|
screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
160
192
|
};
|
|
161
193
|
|
|
162
194
|
/** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
|
|
163
195
|
type ScreenshotResult = {
|
|
196
|
+
/** Base64-encoded image bytes. */
|
|
164
197
|
data: string;
|
|
198
|
+
/** MIME type of the encoded image, always `image/png`. */
|
|
165
199
|
mimeType: string;
|
|
200
|
+
/** Image width in pixels, after the scale factor is applied. */
|
|
166
201
|
width: number;
|
|
202
|
+
/** Image height in pixels, after the scale factor is applied. */
|
|
167
203
|
height: number;
|
|
168
204
|
};
|
|
169
205
|
|
|
@@ -200,7 +236,9 @@ type RenderHookOptions<Props> = {
|
|
|
200
236
|
type RenderHookResult<Result, Props> = {
|
|
201
237
|
/** Holds the most recent value returned by the hook under `current`. */
|
|
202
238
|
result: { current: Result };
|
|
239
|
+
/** Re-invokes the hook, keeping the previous props when none are given. */
|
|
203
240
|
rerender: (newProps?: Props) => Promise<void>;
|
|
241
|
+
/** Unmounts the component that calls the hook. */
|
|
204
242
|
unmount: () => Promise<void>;
|
|
205
243
|
};
|
|
206
244
|
|
|
@@ -3,7 +3,9 @@ import { wrapEvent } from "./event-wrapper.js";
|
|
|
3
3
|
|
|
4
4
|
/** A scroll distance in pixels along each axis. */
|
|
5
5
|
type ScrollDelta = {
|
|
6
|
+
/** Distance added to the horizontal adjustment. */
|
|
6
7
|
x?: number;
|
|
8
|
+
/** Distance added to the vertical adjustment. */
|
|
7
9
|
y?: number;
|
|
8
10
|
};
|
|
9
11
|
|
|
@@ -12,6 +14,11 @@ type ScrollAdjustments = {
|
|
|
12
14
|
vertical: Gtk.Adjustment | null;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Emits a jump `change-value` so a Gtk.Range moves to the given value.
|
|
19
|
+
*
|
|
20
|
+
* @throws When the widget is not a Gtk.Range.
|
|
21
|
+
*/
|
|
15
22
|
const slide = (widget: Gtk.Widget, value: number): Promise<void> =>
|
|
16
23
|
wrapEvent(widget, () => {
|
|
17
24
|
if (!(widget instanceof Gtk.Range)) {
|
|
@@ -45,6 +52,12 @@ const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): voi
|
|
|
45
52
|
adjustment.setValue(adjustment.getValue() + delta);
|
|
46
53
|
};
|
|
47
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Adds the delta to the adjustments of the widget itself, or of its nearest Gtk.ScrolledWindow or
|
|
57
|
+
* Gtk.Scrollable ancestor.
|
|
58
|
+
*
|
|
59
|
+
* @throws When neither the widget nor any of its ancestors is scrollable.
|
|
60
|
+
*/
|
|
48
61
|
const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
|
|
49
62
|
wrapEvent(widget, () => {
|
|
50
63
|
const adjustments = resolveScrollAdjustments(widget);
|
package/src/user-event/click.ts
CHANGED
|
@@ -67,6 +67,11 @@ const tryActivate = async (widget: Gtk.Widget): Promise<boolean> => {
|
|
|
67
67
|
return isActivated;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Presses and releases a Gtk.Button, flips a Gtk.Switch, and otherwise activates the widget,
|
|
72
|
+
* falling back to a click gesture on the nearest Gtk.Button or click-gesture ancestor when
|
|
73
|
+
* activation does nothing. A widget with the label role is never activated.
|
|
74
|
+
*/
|
|
70
75
|
const click = async (widget: Gtk.Widget): Promise<void> => {
|
|
71
76
|
if (widget instanceof Gtk.Button) {
|
|
72
77
|
await emitClickSequence(widget, widget, 1);
|
|
@@ -93,7 +98,9 @@ const click = async (widget: Gtk.Widget): Promise<void> => {
|
|
|
93
98
|
}
|
|
94
99
|
};
|
|
95
100
|
|
|
101
|
+
/** Emits a two-press click gesture at the widget's center, adding a Gtk.GestureClick when it has none. */
|
|
96
102
|
const dblClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 2);
|
|
103
|
+
/** Emits a three-press click gesture at the widget's center, adding a Gtk.GestureClick when it has none. */
|
|
97
104
|
const tripleClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 3);
|
|
98
105
|
|
|
99
106
|
export { emitPress, emitRelease, click, dblClick, tripleClick };
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
2
|
|
|
3
|
+
/** An event controller class that can be constructed without arguments, such as `Gtk.GestureClick`. */
|
|
3
4
|
type ControllerConstructor<T extends Gtk.EventController> = new () => T;
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Returns every controller of the given type attached to a widget, in the order GTK4 reports them.
|
|
8
|
+
*
|
|
9
|
+
* @param widget The widget whose controllers are inspected.
|
|
10
|
+
* @param controllerType The controller class to match.
|
|
11
|
+
* @returns The matching controllers, or an empty array when the widget has none.
|
|
12
|
+
*/
|
|
5
13
|
const queryAllControllers = <T extends Gtk.EventController>(
|
|
6
14
|
widget: Gtk.Widget,
|
|
7
15
|
controllerType: ControllerConstructor<T>,
|
|
@@ -21,11 +29,25 @@ const queryAllControllers = <T extends Gtk.EventController>(
|
|
|
21
29
|
return matches;
|
|
22
30
|
};
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Returns the first controller of the given type attached to a widget.
|
|
34
|
+
*
|
|
35
|
+
* @param widget The widget whose controllers are inspected.
|
|
36
|
+
* @param controllerType The controller class to match.
|
|
37
|
+
* @returns The first matching controller, or null when the widget has none.
|
|
38
|
+
*/
|
|
24
39
|
const queryController = <T extends Gtk.EventController>(
|
|
25
40
|
widget: Gtk.Widget,
|
|
26
41
|
controllerType: ControllerConstructor<T>,
|
|
27
42
|
): T | null => queryAllControllers(widget, controllerType)[0] ?? null;
|
|
28
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns every controller of the given type attached to a widget.
|
|
46
|
+
*
|
|
47
|
+
* @param widget The widget whose controllers are inspected.
|
|
48
|
+
* @param controllerType The controller class to match.
|
|
49
|
+
* @throws When the widget has no controller of that type.
|
|
50
|
+
*/
|
|
29
51
|
const getAllControllers = <T extends Gtk.EventController>(
|
|
30
52
|
widget: Gtk.Widget,
|
|
31
53
|
controllerType: ControllerConstructor<T>,
|
|
@@ -39,6 +61,13 @@ const getAllControllers = <T extends Gtk.EventController>(
|
|
|
39
61
|
return controllers;
|
|
40
62
|
};
|
|
41
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Returns the first controller of the given type attached to a widget.
|
|
66
|
+
*
|
|
67
|
+
* @param widget The widget whose controllers are inspected.
|
|
68
|
+
* @param controllerType The controller class to match.
|
|
69
|
+
* @throws When the widget has no controller of that type.
|
|
70
|
+
*/
|
|
42
71
|
const getController = <T extends Gtk.EventController>(
|
|
43
72
|
widget: Gtk.Widget,
|
|
44
73
|
controllerType: ControllerConstructor<T>,
|