@gtkx/testing 0.20.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 +122 -55
- 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 -32
- 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 +60 -18
- package/src/matchers.ts +200 -0
- package/src/pretty-widget.ts +99 -138
- package/src/queries.ts +321 -425
- 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 -80
- 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
package/src/types.ts
CHANGED
|
@@ -1,210 +1,149 @@
|
|
|
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
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Text matching pattern.
|
|
16
|
-
*
|
|
17
|
-
* Can be a string for exact/substring matching, a RegExp for pattern matching,
|
|
18
|
-
* or a custom function for advanced matching logic.
|
|
19
|
-
*/
|
|
20
|
-
export type TextMatch = string | RegExp | TextMatchFunction;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Options for text normalization before matching.
|
|
24
|
-
*/
|
|
7
|
+
/** A custom matcher predicate: given a candidate widget's normalized text content and the widget itself, returns whether it matches. */
|
|
8
|
+
export type MatcherFunction = (content: string, widget: Gtk.Widget) => boolean;
|
|
9
|
+
|
|
10
|
+
/** A value used to match widget text: a string or number, a regular expression, or a custom {@link MatcherFunction}. */
|
|
11
|
+
export type Matcher = string | number | RegExp | MatcherFunction;
|
|
12
|
+
|
|
13
|
+
/** Normalizes a widget's text before it is compared against a matcher. */
|
|
14
|
+
export type NormalizerFn = (text: string) => string;
|
|
15
|
+
|
|
16
|
+
/** Options controlling the default text normalizer. */
|
|
25
17
|
export type NormalizerOptions = {
|
|
26
|
-
/**
|
|
27
|
-
trim?: boolean;
|
|
28
|
-
/**
|
|
29
|
-
collapseWhitespace?: boolean;
|
|
18
|
+
/** Trim leading and trailing whitespace. */
|
|
19
|
+
trim?: boolean | undefined;
|
|
20
|
+
/** Collapse runs of whitespace into a single space. */
|
|
21
|
+
collapseWhitespace?: boolean | undefined;
|
|
30
22
|
};
|
|
31
23
|
|
|
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;
|
|
24
|
+
/** Options controlling how asynchronous queries and waits poll for a condition. */
|
|
25
|
+
export type WaitForOptions = {
|
|
26
|
+
/** Maximum time in milliseconds to keep retrying before failing. */
|
|
27
|
+
timeout?: number | undefined;
|
|
28
|
+
/** Delay in milliseconds between retries. */
|
|
29
|
+
interval?: number | undefined;
|
|
30
|
+
/** Transforms the error thrown when the timeout elapses. */
|
|
31
|
+
onTimeout?: ((error: Error) => Error) | undefined;
|
|
32
|
+
/** Error whose stack trace attributes the timeout failure to the calling code. */
|
|
33
|
+
stackTraceError?: Error | undefined;
|
|
46
34
|
};
|
|
47
35
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
|
|
36
|
+
/** Options controlling text matching and, for asynchronous queries, polling behavior. */
|
|
37
|
+
export type MatcherOptions = {
|
|
38
|
+
/** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
|
|
39
|
+
exact?: boolean | undefined;
|
|
40
|
+
/** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
|
|
41
|
+
normalizer?: NormalizerFn | undefined;
|
|
42
|
+
/** Forwarded to the default normalizer when no custom `normalizer` is given. */
|
|
43
|
+
trim?: boolean | undefined;
|
|
44
|
+
/** Forwarded to the default normalizer when no custom `normalizer` is given. */
|
|
45
|
+
collapseWhitespace?: boolean | undefined;
|
|
46
|
+
/** Whether to include a suggested better query in error messages. */
|
|
47
|
+
suggest?: boolean | undefined;
|
|
48
|
+
} & WaitForOptions;
|
|
49
|
+
|
|
50
|
+
/** Constraints on a widget's numeric range value used by role queries. */
|
|
51
|
+
export type ByRoleValue = {
|
|
52
|
+
/** The current value. */
|
|
53
|
+
now?: number | undefined;
|
|
54
|
+
min?: number | undefined;
|
|
55
|
+
max?: number | undefined;
|
|
56
|
+
/** Matcher for the value's textual representation. */
|
|
57
|
+
text?: Matcher | undefined;
|
|
66
58
|
};
|
|
67
59
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
/** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
|
|
61
|
+
export type ByRoleOptions = MatcherOptions & {
|
|
62
|
+
name?: Matcher | undefined;
|
|
63
|
+
checked?: boolean | undefined;
|
|
64
|
+
pressed?: boolean | undefined;
|
|
65
|
+
selected?: boolean | undefined;
|
|
66
|
+
expanded?: boolean | undefined;
|
|
67
|
+
/** Heading or hierarchy level. */
|
|
68
|
+
level?: number | undefined;
|
|
69
|
+
busy?: boolean | undefined;
|
|
70
|
+
description?: Matcher | undefined;
|
|
71
|
+
value?: ByRoleValue | undefined;
|
|
72
|
+
/** When true, include widgets excluded from the accessibility tree. */
|
|
73
|
+
hidden?: boolean | undefined;
|
|
78
74
|
};
|
|
79
75
|
|
|
80
|
-
/**
|
|
81
|
-
* A wrapper component that exposes its root GTK widget via `ref`.
|
|
82
|
-
* Accept `ref` as a prop and pass it through to the root intrinsic element.
|
|
83
|
-
*/
|
|
76
|
+
/** A React component that wraps rendered content, receiving it as its children. */
|
|
84
77
|
export type WrapperComponent = ComponentType<{
|
|
85
78
|
children: ReactNode;
|
|
86
|
-
ref?: Ref<Gtk.Widget>;
|
|
87
79
|
}>;
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
export type RenderOptions = {
|
|
93
|
-
/**
|
|
94
|
-
* Wrapper component or boolean.
|
|
95
|
-
* - `true` (default): Wrap in GtkApplicationWindow
|
|
96
|
-
* - `false`: No wrapper
|
|
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
|
-
};
|
|
81
|
+
type Query = (container: Container, ...args: never[]) => unknown;
|
|
82
|
+
|
|
83
|
+
export type QueryMap = Record<string, Query>;
|
|
107
84
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
queryAllByTestId: (testId: 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 test ID (waits and throws if not found) */
|
|
138
|
-
findByTestId: (testId: 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 test ID (waits and throws if none found) */
|
|
146
|
-
findAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
85
|
+
type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
|
|
86
|
+
? (...args: A) => R
|
|
87
|
+
: never;
|
|
88
|
+
|
|
89
|
+
export type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
|
|
90
|
+
|
|
91
|
+
/** Options for {@link render}: the container and base element to mount into, an optional wrapper, React behavior toggles, error callbacks, and custom queries to bind. */
|
|
92
|
+
export type RenderOptions<Q extends QueryMap = Record<never, never>> = {
|
|
93
|
+
container?: Gtk.Widget | RootElement | undefined;
|
|
94
|
+
/** Root of the subtree that bound queries search. */
|
|
95
|
+
baseElement?: Container | undefined;
|
|
96
|
+
wrapper?: WrapperComponent | undefined;
|
|
97
|
+
/** Render inside React StrictMode. */
|
|
98
|
+
reactStrictMode?: boolean | undefined;
|
|
99
|
+
/** Enable widget animations during the test. */
|
|
100
|
+
animations?: boolean | undefined;
|
|
101
|
+
/** Called for errors caught by React error boundaries. */
|
|
102
|
+
onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
103
|
+
/** Called for errors React recovered from automatically. */
|
|
104
|
+
onRecoverableError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
105
|
+
/** Custom queries to bind to the rendered result. */
|
|
106
|
+
queries?: Q | undefined;
|
|
147
107
|
};
|
|
148
108
|
|
|
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;
|
|
109
|
+
export type DebugUtilities = {
|
|
110
|
+
debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
|
|
111
|
+
logRoles: () => void;
|
|
112
|
+
screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
165
113
|
};
|
|
166
114
|
|
|
167
|
-
/**
|
|
168
|
-
* Result returned by {@link screenshot} and screen.screenshot.
|
|
169
|
-
*/
|
|
115
|
+
/** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
|
|
170
116
|
export type ScreenshotResult = {
|
|
171
|
-
/** Base64-encoded PNG image data */
|
|
172
117
|
data: string;
|
|
173
|
-
/** MIME type of the image (always "image/png") */
|
|
174
118
|
mimeType: string;
|
|
175
|
-
/** Width of the captured image in pixels */
|
|
176
119
|
width: number;
|
|
177
|
-
/** Height of the captured image in pixels */
|
|
178
120
|
height: number;
|
|
179
121
|
};
|
|
180
122
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
183
|
-
*/
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Initial props passed to the hook callback.
|
|
187
|
-
*/
|
|
188
|
-
initialProps?: Props;
|
|
189
|
-
/**
|
|
190
|
-
* Wrapper component or boolean.
|
|
191
|
-
* - `true` (default): Wrap in GtkApplicationWindow
|
|
192
|
-
* - `false`: No wrapper
|
|
193
|
-
* - Component: Custom wrapper that passes `ref` to its root element
|
|
194
|
-
*/
|
|
195
|
-
wrapper?: boolean | WrapperComponent;
|
|
123
|
+
/** Options for capturing a screenshot: the poll timeout and interval, plus a rendering scale factor. */
|
|
124
|
+
export type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
|
|
125
|
+
/** Device scale factor applied when rendering. */
|
|
126
|
+
scale?: number;
|
|
196
127
|
};
|
|
197
128
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
129
|
+
/** Selects the window to screenshot by index, or by title (exact string or regular expression); undefined targets the default window. */
|
|
130
|
+
export type WindowSelector = number | string | RegExp | undefined;
|
|
131
|
+
|
|
132
|
+
/** Options for {@link renderHook}: an optional wrapper and the initial props (required unless the props type permits undefined). */
|
|
133
|
+
export type RenderHookOptions<Props> = {
|
|
134
|
+
wrapper?: WrapperComponent;
|
|
135
|
+
} & (undefined extends Props
|
|
136
|
+
? {
|
|
137
|
+
initialProps?: Props;
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
initialProps: Props;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/** The result of {@link renderHook}: the latest hook return value plus functions to rerender with new props and to unmount. */
|
|
203
144
|
export type RenderHookResult<Result, Props> = {
|
|
204
|
-
/**
|
|
145
|
+
/** Holds the most recent value returned by the hook under `current`. */
|
|
205
146
|
result: { current: Result };
|
|
206
|
-
/** Re-render the hook with optional new props */
|
|
207
147
|
rerender: (newProps?: Props) => Promise<void>;
|
|
208
|
-
/** Unmount the component containing the hook */
|
|
209
148
|
unmount: () => Promise<void>;
|
|
210
149
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
3
|
+
|
|
4
|
+
export type ScrollDelta = {
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const slide = (widget: Gtk.Widget, value: number): Promise<void> =>
|
|
10
|
+
wrapEvent(widget, () => {
|
|
11
|
+
if (!(widget instanceof Gtk.Range)) {
|
|
12
|
+
throw new Error(`userEvent.slide requires a Gtk.Range (e.g. Gtk.Scale), got ${widget.constructor.name}`);
|
|
13
|
+
}
|
|
14
|
+
widget.emit("change-value", Gtk.ScrollType.JUMP, value);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type ScrollAdjustments = {
|
|
18
|
+
horizontal: Gtk.Adjustment | null;
|
|
19
|
+
vertical: Gtk.Adjustment | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null => {
|
|
23
|
+
for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
|
|
24
|
+
if (current instanceof Gtk.ScrolledWindow) {
|
|
25
|
+
return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
|
|
26
|
+
}
|
|
27
|
+
if (current instanceof Gtk.Scrollable) {
|
|
28
|
+
return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
|
|
35
|
+
if (!adjustment || delta === 0) return;
|
|
36
|
+
adjustment.setValue(adjustment.getValue() + delta);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
|
|
40
|
+
wrapEvent(widget, () => {
|
|
41
|
+
const adjustments = resolveScrollAdjustments(widget);
|
|
42
|
+
if (!adjustments) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"userEvent.scroll: no Gtk.ScrolledWindow or Gtk.Scrollable found on the widget or its ancestors",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
|
|
48
|
+
applyScrollDelta(adjustments.vertical, delta.y ?? 0);
|
|
49
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { getOrCreateControllers, queryController } from "./controller.js";
|
|
3
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
4
|
+
|
|
5
|
+
const pressPointOf = (widget: Gtk.Widget): { x: number; y: number } => {
|
|
6
|
+
const width = widget.getWidth();
|
|
7
|
+
const height = widget.getHeight();
|
|
8
|
+
if (width > 0 && height > 0) return { x: width / 2, y: height / 2 };
|
|
9
|
+
return { x: 0, y: 0 };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const emitPress = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
|
|
13
|
+
const { x, y } = pressPointOf(widget);
|
|
14
|
+
for (const controller of controllers) {
|
|
15
|
+
controller.emit("pressed", nPress, x, y);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const emitRelease = (widget: Gtk.Widget, controllers: Gtk.GestureClick[], nPress: number): void => {
|
|
20
|
+
const { x, y } = pressPointOf(widget);
|
|
21
|
+
for (const controller of controllers) {
|
|
22
|
+
controller.emit("released", nPress, x, y);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const emitClickSequence = (widget: Gtk.Widget, target: Gtk.Widget, nPress: number): Promise<void> =>
|
|
27
|
+
wrapEvent(widget, () => {
|
|
28
|
+
const controllers = getOrCreateControllers(target, Gtk.GestureClick);
|
|
29
|
+
|
|
30
|
+
for (let i = 1; i <= nPress; i++) {
|
|
31
|
+
emitPress(target, controllers, i);
|
|
32
|
+
emitRelease(target, controllers, i);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const findClickableAncestor = (widget: Gtk.Widget): Gtk.Widget | null => {
|
|
37
|
+
let current = widget.getParent();
|
|
38
|
+
while (current) {
|
|
39
|
+
if (current instanceof Gtk.Button || queryController(current, Gtk.GestureClick) !== null) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
current = current.getParent();
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const click = async (widget: Gtk.Widget): Promise<void> => {
|
|
48
|
+
if (widget instanceof Gtk.Button) {
|
|
49
|
+
await emitClickSequence(widget, widget, 1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (widget instanceof Gtk.Switch) {
|
|
53
|
+
await wrapEvent(widget, () => {
|
|
54
|
+
widget.setActive(!widget.getActive());
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (widget.getAccessibleRole() !== Gtk.AccessibleRole.LABEL) {
|
|
59
|
+
let activated = false;
|
|
60
|
+
await wrapEvent(widget, () => {
|
|
61
|
+
activated = widget.activate();
|
|
62
|
+
});
|
|
63
|
+
if (activated) return;
|
|
64
|
+
}
|
|
65
|
+
const target = findClickableAncestor(widget);
|
|
66
|
+
if (target) await emitClickSequence(widget, target, 1);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const dblClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 2);
|
|
70
|
+
export const tripleClick = (widget: Gtk.Widget): Promise<void> => emitClickSequence(widget, widget, 3);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
|
|
3
|
+
export type ControllerConstructor<T extends Gtk.EventController> = new () => T;
|
|
4
|
+
|
|
5
|
+
export const queryAllControllers = <T extends Gtk.EventController>(
|
|
6
|
+
widget: Gtk.Widget,
|
|
7
|
+
controllerType: ControllerConstructor<T>,
|
|
8
|
+
): T[] => {
|
|
9
|
+
const controllers = widget.observeControllers();
|
|
10
|
+
const nItems = controllers.getNItems();
|
|
11
|
+
const matches: T[] = [];
|
|
12
|
+
for (let i = 0; i < nItems; i++) {
|
|
13
|
+
const controller = controllers.getItem(i);
|
|
14
|
+
if (controller instanceof controllerType) matches.push(controller);
|
|
15
|
+
}
|
|
16
|
+
return matches;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const queryController = <T extends Gtk.EventController>(
|
|
20
|
+
widget: Gtk.Widget,
|
|
21
|
+
controllerType: ControllerConstructor<T>,
|
|
22
|
+
): T | null => queryAllControllers(widget, controllerType)[0] ?? null;
|
|
23
|
+
|
|
24
|
+
export const getAllControllers = <T extends Gtk.EventController>(
|
|
25
|
+
widget: Gtk.Widget,
|
|
26
|
+
controllerType: ControllerConstructor<T>,
|
|
27
|
+
): T[] => {
|
|
28
|
+
const controllers = queryAllControllers(widget, controllerType);
|
|
29
|
+
if (controllers.length === 0) {
|
|
30
|
+
throw new Error(`No ${controllerType.name} controller is attached to the widget`);
|
|
31
|
+
}
|
|
32
|
+
return controllers;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getOrCreateControllers = <T extends Gtk.EventController>(
|
|
36
|
+
widget: Gtk.Widget,
|
|
37
|
+
controllerType: ControllerConstructor<T>,
|
|
38
|
+
): T[] => {
|
|
39
|
+
const existing = queryAllControllers(widget, controllerType);
|
|
40
|
+
if (existing.length > 0) return existing;
|
|
41
|
+
const controller = new controllerType();
|
|
42
|
+
widget.addController(controller);
|
|
43
|
+
return [controller];
|
|
44
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { type ControllerConstructor, getAllControllers, getOrCreateControllers } from "./controller.js";
|
|
3
|
+
import { wrapEvent } from "./event-wrapper.js";
|
|
4
|
+
|
|
5
|
+
type ControllerEmit<T extends Gtk.EventController> = (controller: T) => void | PromiseLike<void>;
|
|
6
|
+
|
|
7
|
+
const emitOnAll = async <T extends Gtk.EventController>(controllers: T[], emit: ControllerEmit<T>): Promise<void> => {
|
|
8
|
+
for (const controller of controllers) {
|
|
9
|
+
await emit(controller);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const dispatchOnOrCreateControllers = <T extends Gtk.EventController>(
|
|
14
|
+
widget: Gtk.Widget,
|
|
15
|
+
controllerType: ControllerConstructor<T>,
|
|
16
|
+
emit: ControllerEmit<T>,
|
|
17
|
+
): Promise<void> => wrapEvent(widget, () => emitOnAll(getOrCreateControllers(widget, controllerType), emit));
|
|
18
|
+
|
|
19
|
+
export const dispatchOnControllers = <T extends Gtk.EventController>(
|
|
20
|
+
widget: Gtk.Widget,
|
|
21
|
+
controllerType: ControllerConstructor<T>,
|
|
22
|
+
emit: ControllerEmit<T>,
|
|
23
|
+
): Promise<void> => wrapEvent(widget, () => emitOnAll(getAllControllers(widget, controllerType), emit));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { runInAct } from "../act.js";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { formatRole } from "../role-helpers.js";
|
|
5
|
+
|
|
6
|
+
const NOT_SENSITIVE = "it is not sensitive (the widget or one of its ancestors is disabled)";
|
|
7
|
+
const WINDOW_NOT_ALLOCATED = "its window has not been allocated a size";
|
|
8
|
+
const NOT_MAPPED = "it is not mapped (it is not shown on screen, e.g. it is hidden or on a non-visible page)";
|
|
9
|
+
const WINDOW_NOT_ACTIVE = "its window never became active";
|
|
10
|
+
|
|
11
|
+
const actionableHop = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 1));
|
|
12
|
+
|
|
13
|
+
const displayDeliversActivation = (window: Gtk.Window): boolean => window.getDisplay().getDefaultSeat() !== null;
|
|
14
|
+
|
|
15
|
+
const findActionabilityFailure = (widget: Gtk.Widget): string | null => {
|
|
16
|
+
if (!widget.isSensitive()) return NOT_SENSITIVE;
|
|
17
|
+
const root = widget.getRoot();
|
|
18
|
+
if (!(root instanceof Gtk.Window) || !root.getVisible()) return null;
|
|
19
|
+
if (root.getAllocatedWidth() === 0) return WINDOW_NOT_ALLOCATED;
|
|
20
|
+
if (!widget.getMapped()) return NOT_MAPPED;
|
|
21
|
+
if (displayDeliversActivation(root) && !root.isActive()) return WINDOW_NOT_ACTIVE;
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const describeWidget = (widget: Gtk.Widget): string => {
|
|
26
|
+
const tag = widget.constructor.name;
|
|
27
|
+
const name = widget.getName();
|
|
28
|
+
const nameAttribute = name && !name.endsWith(tag) ? ` name="${name}"` : "";
|
|
29
|
+
return `<${tag}${nameAttribute} role="${formatRole(widget.getAccessibleRole())}">`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const waitForActionable = async (widget: Gtk.Widget): Promise<void> => {
|
|
33
|
+
const timeout = getConfig().actionabilityTimeout;
|
|
34
|
+
const deadline = Date.now() + timeout;
|
|
35
|
+
let failure = findActionabilityFailure(widget);
|
|
36
|
+
while (failure !== null && Date.now() < deadline) {
|
|
37
|
+
await actionableHop();
|
|
38
|
+
failure = findActionabilityFailure(widget);
|
|
39
|
+
}
|
|
40
|
+
if (failure !== null) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Cannot dispatch user event: ${describeWidget(widget)} did not become actionable within ${timeout}ms because ${failure}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const wrapEvent = (widget: Gtk.Widget, body: () => void | PromiseLike<void>): Promise<void> =>
|
|
48
|
+
Promise.resolve()
|
|
49
|
+
.then(() => waitForActionable(widget))
|
|
50
|
+
.then(() =>
|
|
51
|
+
runInAct(async () => {
|
|
52
|
+
await body();
|
|
53
|
+
}),
|
|
54
|
+
);
|