@gtkx/testing 0.21.0 → 1.0.0-rc.1
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 +48 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +32 -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 +103 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +4 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +11 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +18 -51
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -40
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +7 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +24 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +119 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +7 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +9 -29
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +3 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +30 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +55 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +103 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +21 -25
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +69 -115
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +130 -177
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +212 -325
- 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 +15 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +14 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +8 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +11 -41
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +129 -118
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +16 -51
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +39 -58
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +6 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +21 -109
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +20 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +89 -48
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +22 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +56 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +10 -8
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +28 -13
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +103 -172
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +8 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +33 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +7 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +65 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +7 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +28 -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 +10 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +3 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +48 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +31 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +105 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +44 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +38 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +9 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +173 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +6 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +27 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +4 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +106 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +6 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +2 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +17 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +107 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +13 -33
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +62 -73
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +33 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +215 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +9 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +15 -25
- package/dist/within.js.map +1 -1
- package/package.json +21 -12
- package/src/act.ts +68 -0
- package/src/bound-queries.ts +36 -0
- package/src/build-queries.ts +185 -0
- package/src/cleanup-registry.ts +14 -0
- package/src/config.ts +28 -59
- package/src/editable.ts +30 -0
- package/src/errors.ts +130 -0
- package/src/fire-event.ts +11 -38
- package/src/frame-sync.ts +34 -0
- package/src/index.ts +56 -14
- package/src/matchers.ts +200 -0
- package/src/pretty-widget.ts +99 -138
- package/src/queries.ts +320 -420
- package/src/register-test-hooks.ts +16 -0
- package/src/render-hook.tsx +31 -44
- package/src/render.tsx +161 -149
- package/src/role-helpers.ts +47 -67
- package/src/screen.ts +21 -114
- package/src/screenshot.ts +104 -55
- package/src/suggestions.ts +87 -0
- package/src/traversal.ts +32 -18
- package/src/types.ts +115 -176
- package/src/user-event/adjustment.ts +49 -0
- package/src/user-event/click.ts +70 -0
- package/src/user-event/controller.ts +44 -0
- package/src/user-event/dispatch.ts +23 -0
- package/src/user-event/event-wrapper.ts +54 -0
- package/src/user-event/gesture.ts +154 -0
- package/src/user-event/index.ts +74 -0
- package/src/user-event/keyboard.ts +204 -0
- package/src/user-event/pointer.ts +34 -0
- package/src/user-event/selection.ts +122 -0
- package/src/user-event/state.ts +6 -0
- package/src/user-event/text.ts +137 -0
- package/src/wait-for.ts +79 -81
- package/src/widget-accessible-properties.ts +244 -0
- package/src/within.ts +26 -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,185 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { runWithExpensiveErrorDiagnosticsDisabled, suggestionError } from "./errors.js";
|
|
4
|
+
import { getSuggestedQuery, type Method, type Variant } from "./suggestions.js";
|
|
5
|
+
import type { Container } from "./traversal.js";
|
|
6
|
+
import type { MatcherOptions, WaitForOptions } from "./types.js";
|
|
7
|
+
import { waitFor } from "./wait-for.js";
|
|
8
|
+
|
|
9
|
+
export type QueryAllBy<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
|
|
10
|
+
|
|
11
|
+
type MultipleErrorBuilder<Args extends unknown[]> = (
|
|
12
|
+
container: Container,
|
|
13
|
+
matches: Gtk.Widget[],
|
|
14
|
+
...args: Args
|
|
15
|
+
) => Error;
|
|
16
|
+
type MissingErrorBuilder<Args extends unknown[]> = (container: Container, ...args: Args) => Error;
|
|
17
|
+
|
|
18
|
+
export type BuiltQueries<Args extends unknown[]> = {
|
|
19
|
+
queryBy: (container: Container, ...args: Args) => Gtk.Widget | null;
|
|
20
|
+
getAllBy: QueryAllBy<Args>;
|
|
21
|
+
getBy: (container: Container, ...args: Args) => Gtk.Widget;
|
|
22
|
+
findAllBy: (container: Container, ...args: Args) => Promise<Gtk.Widget[]>;
|
|
23
|
+
findBy: (container: Container, ...args: Args) => Promise<Gtk.Widget>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const extractWaitForOptions = (args: unknown[]): WaitForOptions => {
|
|
27
|
+
const last = args[args.length - 1];
|
|
28
|
+
if (last && typeof last === "object" && !(last instanceof RegExp)) {
|
|
29
|
+
const { timeout, interval, onTimeout } = last as WaitForOptions;
|
|
30
|
+
return { timeout, interval, onTimeout };
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const extractSuggestOption = (args: unknown[]): boolean | undefined => {
|
|
36
|
+
const last = args[args.length - 1];
|
|
37
|
+
if (last && typeof last === "object" && !(last instanceof RegExp)) {
|
|
38
|
+
return (last as MatcherOptions).suggest;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const maybeThrowSuggestion = (options: {
|
|
44
|
+
container: Container;
|
|
45
|
+
match: Gtk.Widget;
|
|
46
|
+
queryName: Method;
|
|
47
|
+
variant: Variant;
|
|
48
|
+
suggest: boolean | undefined;
|
|
49
|
+
}): void => {
|
|
50
|
+
const { container, match, queryName, variant, suggest } = options;
|
|
51
|
+
const shouldSuggest = suggest ?? getConfig().throwSuggestions;
|
|
52
|
+
if (!shouldSuggest) return;
|
|
53
|
+
|
|
54
|
+
const suggestion = getSuggestedQuery(match, variant);
|
|
55
|
+
if (suggestion && suggestion.queryName !== queryName) {
|
|
56
|
+
throw suggestionError(suggestion.toString(), container);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type FindQuery<Args extends unknown[]> = (container: Container, ...args: Args) => unknown;
|
|
61
|
+
|
|
62
|
+
const reRunForDiagnostics = <Args extends unknown[]>(
|
|
63
|
+
query: FindQuery<Args>,
|
|
64
|
+
container: Container,
|
|
65
|
+
args: Args,
|
|
66
|
+
fallback: Error,
|
|
67
|
+
): Error => {
|
|
68
|
+
try {
|
|
69
|
+
query(container, ...args);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof Error) return error;
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findOptions = <Args extends unknown[]>(
|
|
77
|
+
query: FindQuery<Args>,
|
|
78
|
+
container: Container,
|
|
79
|
+
args: Args,
|
|
80
|
+
): WaitForOptions => {
|
|
81
|
+
const userOptions = extractWaitForOptions(args);
|
|
82
|
+
const onTimeout =
|
|
83
|
+
userOptions.onTimeout ?? ((fallback: Error) => reRunForDiagnostics(query, container, args, fallback));
|
|
84
|
+
return {
|
|
85
|
+
stackTraceError: new Error("STACK_TRACE_MESSAGE"),
|
|
86
|
+
onTimeout,
|
|
87
|
+
timeout: userOptions.timeout,
|
|
88
|
+
interval: userOptions.interval,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type SingleQuery<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget | null;
|
|
93
|
+
|
|
94
|
+
const singleFrom =
|
|
95
|
+
<Args extends unknown[]>(
|
|
96
|
+
allQuery: QueryAllBy<Args>,
|
|
97
|
+
getMultipleError: MultipleErrorBuilder<Args>,
|
|
98
|
+
): SingleQuery<Args> =>
|
|
99
|
+
(container, ...args) => {
|
|
100
|
+
const matches = allQuery(container, ...args);
|
|
101
|
+
if (matches.length > 1) throw getMultipleError(container, matches, ...args);
|
|
102
|
+
return matches[0] ?? null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const allOrThrow =
|
|
106
|
+
<Args extends unknown[]>(
|
|
107
|
+
allQuery: QueryAllBy<Args>,
|
|
108
|
+
getMissingError: MissingErrorBuilder<Args>,
|
|
109
|
+
): QueryAllBy<Args> =>
|
|
110
|
+
(container, ...args) => {
|
|
111
|
+
const matches = allQuery(container, ...args);
|
|
112
|
+
if (matches.length === 0) throw getMissingError(container, ...args);
|
|
113
|
+
return matches;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const wrapSingleWithSuggestion =
|
|
117
|
+
<Args extends unknown[]>(query: SingleQuery<Args>, queryName: Method, variant: Variant): SingleQuery<Args> =>
|
|
118
|
+
(container, ...args) => {
|
|
119
|
+
const match = query(container, ...args);
|
|
120
|
+
if (match) {
|
|
121
|
+
maybeThrowSuggestion({ container, match, queryName, variant, suggest: extractSuggestOption(args) });
|
|
122
|
+
}
|
|
123
|
+
return match;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const wrapAllWithSuggestion =
|
|
127
|
+
<Args extends unknown[]>(query: QueryAllBy<Args>, queryName: Method, variant: Variant): QueryAllBy<Args> =>
|
|
128
|
+
(container, ...args) => {
|
|
129
|
+
const matches = query(container, ...args);
|
|
130
|
+
const [first] = matches;
|
|
131
|
+
if (first !== undefined) {
|
|
132
|
+
maybeThrowSuggestion({ container, match: first, queryName, variant, suggest: extractSuggestOption(args) });
|
|
133
|
+
}
|
|
134
|
+
return matches;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const requireSingle =
|
|
138
|
+
<Args extends unknown[]>(query: SingleQuery<Args>, getMissingError: MissingErrorBuilder<Args>) =>
|
|
139
|
+
(container: Container, ...args: Args): Gtk.Widget => {
|
|
140
|
+
const match = query(container, ...args);
|
|
141
|
+
if (match === null) throw getMissingError(container, ...args);
|
|
142
|
+
return match;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const buildQueries = <Args extends unknown[]>(
|
|
146
|
+
queryName: Method,
|
|
147
|
+
queryAllBy: QueryAllBy<Args>,
|
|
148
|
+
getMultipleError: MultipleErrorBuilder<Args>,
|
|
149
|
+
getMissingError: MissingErrorBuilder<Args>,
|
|
150
|
+
): BuiltQueries<Args> => {
|
|
151
|
+
const getAllBy = allOrThrow(queryAllBy, getMissingError);
|
|
152
|
+
const getBy = requireSingle(singleFrom(getAllBy, getMultipleError), getMissingError);
|
|
153
|
+
|
|
154
|
+
const queryByWithSuggestion = wrapSingleWithSuggestion(
|
|
155
|
+
singleFrom(queryAllBy, getMultipleError),
|
|
156
|
+
queryName,
|
|
157
|
+
"query",
|
|
158
|
+
);
|
|
159
|
+
const getAllByWithSuggestion = wrapAllWithSuggestion(getAllBy, queryName, "getAll");
|
|
160
|
+
const getByWithSuggestion: (container: Container, ...args: Args) => Gtk.Widget = (container, ...args) => {
|
|
161
|
+
const match = getBy(container, ...args);
|
|
162
|
+
maybeThrowSuggestion({ container, match, queryName, variant: "get", suggest: extractSuggestOption(args) });
|
|
163
|
+
return match;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const findAllBy = (container: Container, ...args: Args): Promise<Gtk.Widget[]> =>
|
|
167
|
+
waitFor(
|
|
168
|
+
() => runWithExpensiveErrorDiagnosticsDisabled(() => getAllByWithSuggestion(container, ...args)),
|
|
169
|
+
findOptions(getAllByWithSuggestion, container, args),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const findBy = (container: Container, ...args: Args): Promise<Gtk.Widget> =>
|
|
173
|
+
waitFor(
|
|
174
|
+
() => runWithExpensiveErrorDiagnosticsDisabled(() => getByWithSuggestion(container, ...args)),
|
|
175
|
+
findOptions(getByWithSuggestion, container, args),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
queryBy: queryByWithSuggestion,
|
|
180
|
+
getAllBy: getAllByWithSuggestion,
|
|
181
|
+
getBy: getByWithSuggestion,
|
|
182
|
+
findAllBy,
|
|
183
|
+
findBy,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CleanupFunction = () => void | Promise<void>;
|
|
2
|
+
|
|
3
|
+
const cleanupQueue = new Set<CleanupFunction>();
|
|
4
|
+
|
|
5
|
+
export const addToCleanupQueue = (fn: CleanupFunction): void => {
|
|
6
|
+
cleanupQueue.add(fn);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const runCleanup = async (): Promise<void> => {
|
|
10
|
+
for (const fn of cleanupQueue) {
|
|
11
|
+
await fn();
|
|
12
|
+
}
|
|
13
|
+
cleanupQueue.clear();
|
|
14
|
+
};
|
package/src/config.ts
CHANGED
|
@@ -1,89 +1,58 @@
|
|
|
1
1
|
import type { Container } from "./traversal.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Global configuration for the testing harness, controlling query behavior,
|
|
5
|
+
* error formatting, and the timeouts used by async utilities and actionability
|
|
6
|
+
* checks.
|
|
5
7
|
*/
|
|
6
8
|
export type Config = {
|
|
7
|
-
|
|
8
|
-
* Whether to show role suggestions in error messages when elements are not found.
|
|
9
|
-
* @default true
|
|
10
|
-
*/
|
|
11
|
-
showSuggestions: boolean;
|
|
9
|
+
throwSuggestions: boolean;
|
|
12
10
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
* Allows customizing how errors are constructed.
|
|
16
|
-
*/
|
|
17
|
-
getElementError: (message: string, container: Container) => Error;
|
|
11
|
+
/** Builds the error thrown when a query fails, given a message and optional container. */
|
|
12
|
+
getElementError: (message: string, container?: Container) => Error;
|
|
18
13
|
|
|
19
|
-
/**
|
|
20
|
-
* Default timeout in milliseconds for async utilities (waitFor, findBy* queries).
|
|
21
|
-
* @default 1000
|
|
22
|
-
*/
|
|
14
|
+
/** Default timeout in milliseconds for async utilities such as waitFor. */
|
|
23
15
|
asyncUtilTimeout: number;
|
|
16
|
+
|
|
17
|
+
/** Timeout in milliseconds for waiting on a widget to become actionable. */
|
|
18
|
+
actionabilityTimeout: number;
|
|
24
19
|
};
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Function form of a configuration update: receives the current config and
|
|
23
|
+
* returns the fields to override.
|
|
24
|
+
*/
|
|
25
|
+
export type ConfigFn = (existingConfig: Config) => Partial<Config>;
|
|
26
|
+
|
|
27
|
+
const defaultGetElementError = (message: string, _container?: Container): Error => {
|
|
28
|
+
const error = new Error(message);
|
|
29
|
+
error.name = "GtkxElementError";
|
|
30
|
+
return error;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
const defaultConfig: Config = {
|
|
31
|
-
|
|
34
|
+
throwSuggestions: false,
|
|
32
35
|
getElementError: defaultGetElementError,
|
|
33
36
|
asyncUtilTimeout: 1000,
|
|
37
|
+
actionabilityTimeout: 500,
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
let currentConfig: Config = { ...defaultConfig };
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
|
-
* Returns the current testing
|
|
40
|
-
*
|
|
41
|
-
* @returns The current configuration object
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```tsx
|
|
45
|
-
* import { getConfig } from "@gtkx/testing";
|
|
46
|
-
*
|
|
47
|
-
* const config = getConfig();
|
|
48
|
-
* console.log(config.showSuggestions);
|
|
49
|
-
* ```
|
|
43
|
+
* Returns the current global testing configuration.
|
|
50
44
|
*/
|
|
51
|
-
export const getConfig = ():
|
|
45
|
+
export const getConfig = (): Config => {
|
|
52
46
|
return currentConfig;
|
|
53
47
|
};
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Accepts either a partial configuration object or a function that receives
|
|
59
|
-
* the current configuration and returns updates.
|
|
50
|
+
* Merges the given overrides into the global testing configuration.
|
|
60
51
|
*
|
|
61
|
-
* @param newConfig
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```tsx
|
|
65
|
-
* import { configure } from "@gtkx/testing";
|
|
66
|
-
*
|
|
67
|
-
* // Disable role suggestions
|
|
68
|
-
* configure({ showSuggestions: false });
|
|
69
|
-
*
|
|
70
|
-
* // Use updater function
|
|
71
|
-
* configure((current) => ({
|
|
72
|
-
* showSuggestions: !current.showSuggestions,
|
|
73
|
-
* }));
|
|
74
|
-
* ```
|
|
52
|
+
* @param newConfig Either a partial config object or a function that receives
|
|
53
|
+
* the current config and returns the fields to override.
|
|
75
54
|
*/
|
|
76
|
-
export const configure = (newConfig: Partial<Config> |
|
|
55
|
+
export const configure = (newConfig: Partial<Config> | ConfigFn): void => {
|
|
77
56
|
const updates = typeof newConfig === "function" ? newConfig(currentConfig) : newConfig;
|
|
78
57
|
currentConfig = { ...currentConfig, ...updates };
|
|
79
58
|
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resets configuration to defaults.
|
|
83
|
-
* Primarily used for testing.
|
|
84
|
-
*
|
|
85
|
-
* @internal
|
|
86
|
-
*/
|
|
87
|
-
export const resetConfig = (): void => {
|
|
88
|
-
currentConfig = { ...defaultConfig };
|
|
89
|
-
};
|
package/src/editable.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
|
|
3
|
+
export type EditableTarget = Gtk.Editable | Gtk.TextView;
|
|
4
|
+
|
|
5
|
+
export const EDITABLE_ROLES: Set<Gtk.AccessibleRole> = new Set<Gtk.AccessibleRole>([
|
|
6
|
+
Gtk.AccessibleRole.TEXT_BOX,
|
|
7
|
+
Gtk.AccessibleRole.SEARCH_BOX,
|
|
8
|
+
Gtk.AccessibleRole.SPIN_BUTTON,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const isEditable = (widget: unknown): widget is EditableTarget =>
|
|
12
|
+
widget instanceof Gtk.Editable || widget instanceof Gtk.TextView;
|
|
13
|
+
|
|
14
|
+
export const getEditableDelegate = (widget: Gtk.Widget): Gtk.Widget | null => {
|
|
15
|
+
if (!(widget instanceof Gtk.Editable)) return null;
|
|
16
|
+
return widget.getDelegate();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const readTextViewBufferText = (widget: Gtk.TextView): string => {
|
|
20
|
+
const buffer = widget.getBuffer();
|
|
21
|
+
const [start, end] = buffer.getBounds();
|
|
22
|
+
return buffer.getText(start, end, true);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const readEditableText = (widget: EditableTarget): string => {
|
|
26
|
+
if (widget instanceof Gtk.TextView) {
|
|
27
|
+
return readTextViewBufferText(widget);
|
|
28
|
+
}
|
|
29
|
+
return widget.getText();
|
|
30
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { prettyWidget } from "./pretty-widget.js";
|
|
4
|
+
import { formatRole, prettyRoles } from "./role-helpers.js";
|
|
5
|
+
import type { Container } from "./traversal.js";
|
|
6
|
+
import type { ByRoleOptions, ByRoleValue, Matcher } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const formatTextMatcher = (text: Matcher): string => {
|
|
9
|
+
if (typeof text === "function") {
|
|
10
|
+
return "custom function";
|
|
11
|
+
}
|
|
12
|
+
if (text instanceof RegExp) {
|
|
13
|
+
return text.toString();
|
|
14
|
+
}
|
|
15
|
+
return `'${text}'`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const formatByRoleValue = (value: ByRoleValue): string => {
|
|
19
|
+
const parts: string[] = [];
|
|
20
|
+
if (value.now !== undefined) parts.push(`now=${value.now}`);
|
|
21
|
+
if (value.min !== undefined) parts.push(`min=${value.min}`);
|
|
22
|
+
if (value.max !== undefined) parts.push(`max=${value.max}`);
|
|
23
|
+
if (value.text !== undefined) parts.push(`text ${formatTextMatcher(value.text)}`);
|
|
24
|
+
return parts.join(", ");
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const formatByRoleDescription = (role: Gtk.AccessibleRole, options?: ByRoleOptions): string => {
|
|
28
|
+
const parts = [`role '${formatRole(role).toUpperCase()}'`];
|
|
29
|
+
if (options?.name) parts.push(`name ${formatTextMatcher(options.name)}`);
|
|
30
|
+
if (options?.checked !== undefined) parts.push(`checked=${options.checked}`);
|
|
31
|
+
if (options?.pressed !== undefined) parts.push(`pressed=${options.pressed}`);
|
|
32
|
+
if (options?.selected !== undefined) parts.push(`selected=${options.selected}`);
|
|
33
|
+
if (options?.expanded !== undefined) parts.push(`expanded=${options.expanded}`);
|
|
34
|
+
if (options?.level !== undefined) parts.push(`level=${options.level}`);
|
|
35
|
+
if (options?.busy !== undefined) parts.push(`busy=${options.busy}`);
|
|
36
|
+
if (options?.description) parts.push(`description ${formatTextMatcher(options.description)}`);
|
|
37
|
+
if (options?.value) parts.push(`value ${formatByRoleValue(options.value)}`);
|
|
38
|
+
if (options?.hidden !== undefined) parts.push(`hidden=${options.hidden}`);
|
|
39
|
+
return parts.join(" and ");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type QueryDescriptor =
|
|
43
|
+
| { queryType: "role"; role: Gtk.AccessibleRole; options?: ByRoleOptions | undefined }
|
|
44
|
+
| { queryType: "text"; text: Matcher }
|
|
45
|
+
| { queryType: "labelText"; text: Matcher }
|
|
46
|
+
| { queryType: "name"; name: Matcher }
|
|
47
|
+
| { queryType: "placeholderText"; text: Matcher }
|
|
48
|
+
| { queryType: "displayValue"; value: Matcher };
|
|
49
|
+
|
|
50
|
+
const formatQueryDescription = (descriptor: QueryDescriptor): string => {
|
|
51
|
+
switch (descriptor.queryType) {
|
|
52
|
+
case "role":
|
|
53
|
+
return formatByRoleDescription(descriptor.role, descriptor.options);
|
|
54
|
+
case "text":
|
|
55
|
+
return `text ${formatTextMatcher(descriptor.text)}`;
|
|
56
|
+
case "labelText":
|
|
57
|
+
return `label text ${formatTextMatcher(descriptor.text)}`;
|
|
58
|
+
case "name":
|
|
59
|
+
return `name ${formatTextMatcher(descriptor.name)}`;
|
|
60
|
+
case "placeholderText":
|
|
61
|
+
return `placeholder text ${formatTextMatcher(descriptor.text)}`;
|
|
62
|
+
case "displayValue":
|
|
63
|
+
return `display value ${formatTextMatcher(descriptor.value)}`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let expensiveErrorDiagnosticsDisabled = false;
|
|
68
|
+
|
|
69
|
+
export const runWithExpensiveErrorDiagnosticsDisabled = <T>(callback: () => T): T => {
|
|
70
|
+
const previous = expensiveErrorDiagnosticsDisabled;
|
|
71
|
+
expensiveErrorDiagnosticsDisabled = true;
|
|
72
|
+
try {
|
|
73
|
+
return callback();
|
|
74
|
+
} finally {
|
|
75
|
+
expensiveErrorDiagnosticsDisabled = previous;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const buildElementError = (container: Container, headLines: string[]): Error => {
|
|
80
|
+
const config = getConfig();
|
|
81
|
+
const lines = expensiveErrorDiagnosticsDisabled
|
|
82
|
+
? headLines
|
|
83
|
+
: [...headLines, "", prettyWidget(container, { highlight: false })];
|
|
84
|
+
return config.getElementError(lines.join("\n"), container);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const notFoundError = (container: Container, descriptor: QueryDescriptor): Error => {
|
|
88
|
+
const description = formatQueryDescription(descriptor);
|
|
89
|
+
const headLines = [`Unable to find an element with ${description}`];
|
|
90
|
+
|
|
91
|
+
if (!expensiveErrorDiagnosticsDisabled && descriptor.queryType === "role") {
|
|
92
|
+
headLines.push("", "Here are the accessible roles:", "", prettyRoles(container));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return buildElementError(container, headLines);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const allByVariantHint = (descriptor: QueryDescriptor): string => {
|
|
99
|
+
const variant = descriptor.queryType === "role" ? "getAllByRole" : "getAllBy*";
|
|
100
|
+
return (
|
|
101
|
+
"(If this is intentional, use the *AllBy* variant of the query, " +
|
|
102
|
+
`e.g. queryAllBy*/getAllBy*/findAllBy*, such as ${variant}.)`
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const multipleFoundError = (container: Container, descriptor: QueryDescriptor, matches: Gtk.Widget[]): Error => {
|
|
107
|
+
const description = formatQueryDescription(descriptor);
|
|
108
|
+
const headLines = [
|
|
109
|
+
`Found ${matches.length} elements with ${description}, but expected only one`,
|
|
110
|
+
"",
|
|
111
|
+
allByVariantHint(descriptor),
|
|
112
|
+
];
|
|
113
|
+
if (!expensiveErrorDiagnosticsDisabled) {
|
|
114
|
+
const renderedMatches = matches.map((widget) => prettyWidget(widget, { highlight: false }));
|
|
115
|
+
headLines.push("", "Here are the matching elements:", "", ...renderedMatches);
|
|
116
|
+
}
|
|
117
|
+
return buildElementError(container, headLines);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const suggestionError = (suggestion: string, container: Container): Error => {
|
|
121
|
+
const config = getConfig();
|
|
122
|
+
const message = `A better query is available, try this:\n${suggestion}\n`;
|
|
123
|
+
return config.getElementError(message, container);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const timeoutError = (timeout: number, lastError: Error | null): Error => {
|
|
127
|
+
const baseMessage = `Timed out after ${timeout}ms`;
|
|
128
|
+
const message = lastError ? `${baseMessage}.\n\n${lastError.message}` : baseMessage;
|
|
129
|
+
return getConfig().getElementError(message);
|
|
130
|
+
};
|
package/src/fire-event.ts
CHANGED
|
@@ -1,43 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { tick } from "./timing.js";
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import { runInAct } from "./act.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Emits a
|
|
5
|
+
* Emits a GObject signal on a widget or object inside React's act environment,
|
|
6
|
+
* so any resulting state updates are flushed before the promise resolves.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param element - The widget or event controller to emit the signal on
|
|
12
|
-
* @param signalName - GTK signal name (e.g., "clicked", "activate", "drag-begin")
|
|
13
|
-
* @param args - Additional signal arguments as GValues
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* import { fireEvent } from "@gtkx/testing";
|
|
18
|
-
* import { Value } from "@gtkx/ffi/gobject";
|
|
19
|
-
*
|
|
20
|
-
* // Emit signal on widget
|
|
21
|
-
* await fireEvent(widget, "clicked");
|
|
22
|
-
*
|
|
23
|
-
* // Emit signal on gesture controller
|
|
24
|
-
* const gesture = widget.observeControllers().getObject(0) as Gtk.GestureDrag;
|
|
25
|
-
* await fireEvent(gesture, "drag-begin", Value.newFromDouble(100), Value.newFromDouble(100));
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @see {@link userEvent} for high-level user interactions
|
|
8
|
+
* @param element The GObject instance to emit the signal on.
|
|
9
|
+
* @param signalName Name of the signal to emit.
|
|
10
|
+
* @param args Arguments passed to the signal handlers.
|
|
29
11
|
*/
|
|
30
|
-
export const fireEvent = async (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
): Promise<void> => {
|
|
35
|
-
const gtype = typeFromName((element.constructor as typeof GObject).glibTypeName);
|
|
36
|
-
const signalId = signalLookup(signalName, gtype);
|
|
37
|
-
|
|
38
|
-
const instanceValue = Value.newFromObject(element as GObject);
|
|
39
|
-
|
|
40
|
-
signalEmitv([instanceValue, ...args], signalId, 0);
|
|
41
|
-
|
|
42
|
-
await tick();
|
|
12
|
+
export const fireEvent = async (element: GObject.Object, signalName: string, ...args: unknown[]): Promise<void> => {
|
|
13
|
+
await runInAct(() => {
|
|
14
|
+
element.emit(signalName, ...args);
|
|
15
|
+
});
|
|
43
16
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
|
|
3
|
+
const CLOCK_STALL_FALLBACK_MS = 500;
|
|
4
|
+
|
|
5
|
+
export const scheduleAfterLayout = (widget: Gtk.Widget | null, callback: () => void): void => {
|
|
6
|
+
let done = false;
|
|
7
|
+
let removeTick: (() => void) | null = null;
|
|
8
|
+
let fallback: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
+
|
|
10
|
+
const finish = (): void => {
|
|
11
|
+
if (done) return;
|
|
12
|
+
done = true;
|
|
13
|
+
if (fallback !== null) clearTimeout(fallback);
|
|
14
|
+
removeTick?.();
|
|
15
|
+
removeTick = null;
|
|
16
|
+
fallback = null;
|
|
17
|
+
callback();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (widget === null || widget.getFrameClock() === null || widget.getWidth() > 0) {
|
|
21
|
+
queueMicrotask(finish);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fallback = setTimeout(finish, CLOCK_STALL_FALLBACK_MS);
|
|
26
|
+
|
|
27
|
+
const tickId = widget.addTickCallback(() => {
|
|
28
|
+
if (widget.getWidth() === 0) return true;
|
|
29
|
+
finish();
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
removeTick = () => widget.removeTickCallback(tickId);
|
|
34
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,51 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
import "./register-test-hooks.js";
|
|
2
|
+
|
|
3
|
+
export { act } from "./act.js";
|
|
4
|
+
export type { BoundQueries, RenderResult, Screen } from "./bound-queries.js";
|
|
5
|
+
export type { Config, ConfigFn } from "./config.js";
|
|
2
6
|
export { configure, getConfig } from "./config.js";
|
|
3
7
|
export { fireEvent } from "./fire-event.js";
|
|
8
|
+
export type { TextExpectation } from "./matchers.js";
|
|
9
|
+
export { matchers, registerMatchers } from "./matchers.js";
|
|
4
10
|
export type { PrettyWidgetOptions } from "./pretty-widget.js";
|
|
5
|
-
export { prettyWidget } from "./pretty-widget.js";
|
|
11
|
+
export { logWidget, prettyWidget } from "./pretty-widget.js";
|
|
6
12
|
export {
|
|
13
|
+
findAllByDisplayValue,
|
|
7
14
|
findAllByLabelText,
|
|
8
15
|
findAllByName,
|
|
16
|
+
findAllByPlaceholderText,
|
|
9
17
|
findAllByRole,
|
|
10
18
|
findAllByText,
|
|
19
|
+
findByDisplayValue,
|
|
11
20
|
findByLabelText,
|
|
12
21
|
findByName,
|
|
22
|
+
findByPlaceholderText,
|
|
13
23
|
findByRole,
|
|
14
24
|
findByText,
|
|
25
|
+
getAllByDisplayValue,
|
|
26
|
+
getAllByLabelText,
|
|
27
|
+
getAllByName,
|
|
28
|
+
getAllByPlaceholderText,
|
|
29
|
+
getAllByRole,
|
|
30
|
+
getAllByText,
|
|
31
|
+
getByDisplayValue,
|
|
32
|
+
getByLabelText,
|
|
33
|
+
getByName,
|
|
34
|
+
getByPlaceholderText,
|
|
35
|
+
getByRole,
|
|
36
|
+
getByText,
|
|
37
|
+
getDefaultNormalizer,
|
|
38
|
+
queryAllByDisplayValue,
|
|
15
39
|
queryAllByLabelText,
|
|
16
40
|
queryAllByName,
|
|
41
|
+
queryAllByPlaceholderText,
|
|
17
42
|
queryAllByRole,
|
|
18
43
|
queryAllByText,
|
|
44
|
+
queryByDisplayValue,
|
|
19
45
|
queryByLabelText,
|
|
20
46
|
queryByName,
|
|
47
|
+
queryByPlaceholderText,
|
|
21
48
|
queryByRole,
|
|
22
49
|
queryByText,
|
|
23
50
|
} from "./queries.js";
|
|
24
51
|
export { cleanup, render } from "./render.js";
|
|
25
52
|
export { renderHook } from "./render-hook.js";
|
|
26
|
-
export
|
|
27
|
-
export { getRoles, logRoles, prettyRoles } from "./role-helpers.js";
|
|
53
|
+
export { formatRole, getRoles, logRoles, prettyRoles } from "./role-helpers.js";
|
|
28
54
|
export { screen } from "./screen.js";
|
|
29
|
-
export
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
55
|
+
export { captureAndSaveScreenshot, logScreenshotPath, screenshot } from "./screenshot.js";
|
|
56
|
+
export type { Method, Suggestion, Variant } from "./suggestions.js";
|
|
57
|
+
export { getSuggestedQuery } from "./suggestions.js";
|
|
32
58
|
export type { Container } from "./traversal.js";
|
|
33
59
|
export type {
|
|
34
|
-
BoundQueries,
|
|
35
60
|
ByRoleOptions,
|
|
61
|
+
ByRoleValue,
|
|
62
|
+
Matcher,
|
|
63
|
+
MatcherFunction,
|
|
64
|
+
MatcherOptions,
|
|
65
|
+
NormalizerFn,
|
|
36
66
|
NormalizerOptions,
|
|
37
67
|
RenderHookOptions,
|
|
38
68
|
RenderHookResult,
|
|
39
69
|
RenderOptions,
|
|
40
|
-
|
|
70
|
+
ScreenshotOptions,
|
|
41
71
|
ScreenshotResult,
|
|
42
|
-
TextMatch,
|
|
43
|
-
TextMatchFunction,
|
|
44
|
-
TextMatchOptions,
|
|
45
72
|
WaitForOptions,
|
|
73
|
+
WindowSelector,
|
|
46
74
|
WrapperComponent,
|
|
47
75
|
} from "./types.js";
|
|
48
|
-
export type {
|
|
49
|
-
|
|
76
|
+
export type {
|
|
77
|
+
DragOffset,
|
|
78
|
+
DragOptions,
|
|
79
|
+
DropContent,
|
|
80
|
+
DropOptions,
|
|
81
|
+
PointerInput,
|
|
82
|
+
TabOptions,
|
|
83
|
+
TypeOptions,
|
|
84
|
+
UserEvent,
|
|
85
|
+
} from "./user-event/index.js";
|
|
86
|
+
export { userEvent } from "./user-event/index.js";
|
|
50
87
|
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
|
88
|
+
export {
|
|
89
|
+
getWidgetErrorMessage,
|
|
90
|
+
getWidgetInvalidState,
|
|
91
|
+
getWidgetNodeText,
|
|
92
|
+
} from "./widget-accessible-properties.js";
|
|
51
93
|
export { within } from "./within.js";
|