@gtkx/testing 1.0.0-rc.2 → 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/bound-queries.d.ts +2 -3
- package/dist/bound-queries.d.ts.map +1 -1
- package/dist/bound-queries.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/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +125 -21
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js +258 -21
- package/dist/matchers.js.map +1 -1
- package/dist/normalize.d.ts +10 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +20 -0
- package/dist/normalize.js.map +1 -0
- 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 +4 -21
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +46 -59
- package/dist/queries.js.map +1 -1
- package/dist/render.js +3 -3
- 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 +69 -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 +31 -1
- package/dist/user-event/controller.d.ts.map +1 -1
- package/dist/user-event/controller.js +36 -1
- 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 +28 -2
- package/dist/widget-accessible-properties.d.ts.map +1 -1
- package/dist/widget-accessible-properties.js +129 -10
- package/dist/widget-accessible-properties.js.map +1 -1
- package/dist/within.js.map +1 -1
- package/package.json +7 -7
- package/src/act.ts +2 -0
- package/src/bound-queries.ts +2 -3
- package/src/build-queries.ts +20 -8
- package/src/config.ts +4 -3
- package/src/errors.ts +2 -2
- package/src/index.ts +11 -2
- package/src/matchers.ts +500 -53
- package/src/normalize.ts +28 -0
- package/src/pretty-widget.ts +5 -4
- package/src/queries.ts +79 -105
- package/src/render.tsx +3 -3
- package/src/suggestions.ts +3 -0
- package/src/traversal.ts +5 -0
- package/src/types.ts +88 -4
- package/src/user-event/adjustment.ts +13 -0
- package/src/user-event/click.ts +7 -0
- package/src/user-event/controller.ts +50 -1
- 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 +188 -10
- package/src/within.ts +1 -1
package/src/types.ts
CHANGED
|
@@ -13,6 +13,11 @@ type MatcherFunction = (content: string, widget: Gtk.Widget) => boolean;
|
|
|
13
13
|
type Matcher = string | number | RegExp | MatcherFunction;
|
|
14
14
|
/** Normalizes a widget's text before it is compared against a matcher. */
|
|
15
15
|
type NormalizerFn = (text: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* A widget class usable as a query's `as` constraint, such as `Gtk.Button`. Abstract classes and
|
|
18
|
+
* generated GInterface pseudo-classes are accepted.
|
|
19
|
+
*/
|
|
20
|
+
type WidgetType<T extends Gtk.Widget = Gtk.Widget> = abstract new (...args: never[]) => T;
|
|
16
21
|
|
|
17
22
|
/** Options controlling the default text normalizer. */
|
|
18
23
|
type NormalizerOptions = {
|
|
@@ -35,7 +40,7 @@ type WaitForOptions = {
|
|
|
35
40
|
};
|
|
36
41
|
|
|
37
42
|
/** Options controlling text matching and, for asynchronous queries, polling behavior. */
|
|
38
|
-
type MatcherOptions = {
|
|
43
|
+
type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
|
|
39
44
|
/** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
|
|
40
45
|
exact?: boolean | undefined;
|
|
41
46
|
/** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
|
|
@@ -46,29 +51,41 @@ type MatcherOptions = {
|
|
|
46
51
|
collapseWhitespace?: boolean | undefined;
|
|
47
52
|
/** Whether to include a suggested better query in error messages. */
|
|
48
53
|
suggest?: boolean | undefined;
|
|
54
|
+
/** Restricts matches to instances of this widget class, and narrows the query's return type to it. */
|
|
55
|
+
as?: WidgetType<T> | undefined;
|
|
49
56
|
} & WaitForOptions;
|
|
50
57
|
|
|
51
58
|
/** Constraints on a widget's numeric range value used by role queries. */
|
|
52
59
|
type ByRoleValue = {
|
|
53
60
|
/** The current value. */
|
|
54
61
|
now?: number | undefined;
|
|
62
|
+
/** The lower bound of the value's range. */
|
|
55
63
|
min?: number | undefined;
|
|
64
|
+
/** The upper bound of the value's range. */
|
|
56
65
|
max?: number | undefined;
|
|
57
66
|
/** Matcher for the value's textual representation. */
|
|
58
67
|
text?: Matcher | undefined;
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
/** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
|
|
62
|
-
type ByRoleOptions = MatcherOptions & {
|
|
71
|
+
type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
|
|
72
|
+
/** Matcher for the widget's accessible name. */
|
|
63
73
|
name?: Matcher | undefined;
|
|
74
|
+
/** Required checked state; a mixed check button reads as neither, so it matches neither value. */
|
|
64
75
|
checked?: boolean | undefined;
|
|
76
|
+
/** Required active state of a toggle button; any other widget matches neither value. */
|
|
65
77
|
pressed?: boolean | undefined;
|
|
78
|
+
/** Required selected state of a row, list item, grid cell, option or tree item. */
|
|
66
79
|
selected?: boolean | undefined;
|
|
80
|
+
/** Required expanded state of an expander or tree expander. */
|
|
67
81
|
expanded?: boolean | undefined;
|
|
68
82
|
/** Heading or hierarchy level. */
|
|
69
83
|
level?: number | undefined;
|
|
84
|
+
/** Required busy state, which an unset state satisfies as false. */
|
|
70
85
|
busy?: boolean | undefined;
|
|
86
|
+
/** Matcher for the widget's own accessible description, ignoring its `described-by` targets. */
|
|
71
87
|
description?: Matcher | undefined;
|
|
88
|
+
/** Constraints on the widget's range value, each checked only when given. */
|
|
72
89
|
value?: ByRoleValue | undefined;
|
|
73
90
|
/** When true, include widgets excluded from the accessibility tree. */
|
|
74
91
|
hidden?: boolean | undefined;
|
|
@@ -79,28 +96,83 @@ type WrapperComponent = ComponentType<{
|
|
|
79
96
|
children: ReactNode;
|
|
80
97
|
}>;
|
|
81
98
|
|
|
99
|
+
/** A custom query, taking the scope to search as its first argument and its matcher arguments after it. */
|
|
82
100
|
type Query = (container: Container, ...args: never[]) => unknown;
|
|
101
|
+
/** Custom queries keyed by the name each is bound under. */
|
|
83
102
|
type QueryMap = Record<string, Query>;
|
|
84
103
|
|
|
104
|
+
/** A query with its container argument already applied. */
|
|
85
105
|
type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
|
|
86
106
|
? (...args: A) => R
|
|
87
107
|
: never;
|
|
88
108
|
|
|
109
|
+
/** Custom queries with their container argument already applied, keyed as they were passed in. */
|
|
89
110
|
type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
|
|
90
111
|
|
|
112
|
+
/** What each variant of a query family yields for the widget type it matched. */
|
|
113
|
+
type QueryFamilyReturns<T extends Gtk.Widget> = {
|
|
114
|
+
/** The single match, or null when nothing matched; throws when more than one matched. */
|
|
115
|
+
queryBy: T | null;
|
|
116
|
+
/** Every match, empty when nothing matched. */
|
|
117
|
+
queryAllBy: T[];
|
|
118
|
+
/** The single match; throws when nothing or more than one matched. */
|
|
119
|
+
getBy: T;
|
|
120
|
+
/** Every match; throws when nothing matched. */
|
|
121
|
+
getAllBy: T[];
|
|
122
|
+
/** The single match, retried until it appears or the timeout elapses. */
|
|
123
|
+
findBy: Promise<T>;
|
|
124
|
+
/** Every match, retried until at least one appears or the timeout elapses. */
|
|
125
|
+
findAllBy: Promise<T[]>;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** What a query family matches against, which fixes the arguments it takes. */
|
|
129
|
+
type QueryKind = "role" | "text" | "name" | "value";
|
|
130
|
+
|
|
131
|
+
/** The arguments a query of the given kind takes after the family's leading ones. */
|
|
132
|
+
type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
|
|
133
|
+
? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
|
|
134
|
+
: Kind extends "name"
|
|
135
|
+
? [name: Matcher, options?: MatcherOptions<T>]
|
|
136
|
+
: Kind extends "value"
|
|
137
|
+
? [value: Matcher, options?: MatcherOptions<T>]
|
|
138
|
+
: [text: Matcher, options?: MatcherOptions<T>];
|
|
139
|
+
|
|
140
|
+
/**
|
|
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.
|
|
143
|
+
*/
|
|
144
|
+
type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
|
|
145
|
+
[K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
|
|
146
|
+
...args: [...Head, ...QueryArgs<Kind, T>]
|
|
147
|
+
) => QueryFamilyReturns<T>[K];
|
|
148
|
+
};
|
|
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
|
+
*/
|
|
154
|
+
type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
|
|
155
|
+
QueryFamily<"LabelText", "text", Head> &
|
|
156
|
+
QueryFamily<"Text", "text", Head> &
|
|
157
|
+
QueryFamily<"Name", "name", Head> &
|
|
158
|
+
QueryFamily<"PlaceholderText", "text", Head> &
|
|
159
|
+
QueryFamily<"DisplayValue", "value", Head>;
|
|
160
|
+
|
|
91
161
|
/**
|
|
92
162
|
* Options for {@link render}: the container and base element to mount into, an optional wrapper,
|
|
93
163
|
* React behavior toggles, error callbacks, and custom queries to bind.
|
|
94
164
|
*/
|
|
95
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. */
|
|
96
167
|
container?: Gtk.Widget | RootElement | undefined;
|
|
97
168
|
/** Root of the subtree that bound queries search. */
|
|
98
169
|
baseElement?: Container | undefined;
|
|
170
|
+
/** Component wrapped around the rendered element, such as a context provider. */
|
|
99
171
|
wrapper?: WrapperComponent | undefined;
|
|
100
172
|
/** Render inside React StrictMode. */
|
|
101
|
-
|
|
173
|
+
isReactStrictMode?: boolean | undefined;
|
|
102
174
|
/** Enable widget animations during the test. */
|
|
103
|
-
|
|
175
|
+
areAnimationsEnabled?: boolean | undefined;
|
|
104
176
|
/** Called for errors caught by React error boundaries. */
|
|
105
177
|
onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
|
|
106
178
|
/** Called for errors React recovered from automatically. */
|
|
@@ -109,17 +181,25 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
|
|
|
109
181
|
queries?: Q | undefined;
|
|
110
182
|
};
|
|
111
183
|
|
|
184
|
+
/** Console helpers bundled with the bound queries on {@link screen} and on a render result. */
|
|
112
185
|
type DebugUtilities = {
|
|
186
|
+
/** Prints the widget tree of the given containers, or of the whole scope, to the console. */
|
|
113
187
|
debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
|
|
188
|
+
/** Prints the accessible roles found in the scope, with the widgets carrying each one. */
|
|
114
189
|
logRoles: () => void;
|
|
190
|
+
/** Captures a window, writes the image to a temporary file, and logs its path. */
|
|
115
191
|
screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
116
192
|
};
|
|
117
193
|
|
|
118
194
|
/** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
|
|
119
195
|
type ScreenshotResult = {
|
|
196
|
+
/** Base64-encoded image bytes. */
|
|
120
197
|
data: string;
|
|
198
|
+
/** MIME type of the encoded image, always `image/png`. */
|
|
121
199
|
mimeType: string;
|
|
200
|
+
/** Image width in pixels, after the scale factor is applied. */
|
|
122
201
|
width: number;
|
|
202
|
+
/** Image height in pixels, after the scale factor is applied. */
|
|
123
203
|
height: number;
|
|
124
204
|
};
|
|
125
205
|
|
|
@@ -156,7 +236,9 @@ type RenderHookOptions<Props> = {
|
|
|
156
236
|
type RenderHookResult<Result, Props> = {
|
|
157
237
|
/** Holds the most recent value returned by the hook under `current`. */
|
|
158
238
|
result: { current: Result };
|
|
239
|
+
/** Re-invokes the hook, keeping the previous props when none are given. */
|
|
159
240
|
rerender: (newProps?: Props) => Promise<void>;
|
|
241
|
+
/** Unmounts the component that calls the hook. */
|
|
160
242
|
unmount: () => Promise<void>;
|
|
161
243
|
};
|
|
162
244
|
|
|
@@ -169,9 +251,11 @@ export {
|
|
|
169
251
|
type MatcherOptions,
|
|
170
252
|
type ByRoleValue,
|
|
171
253
|
type ByRoleOptions,
|
|
254
|
+
type WidgetType,
|
|
172
255
|
type WrapperComponent,
|
|
173
256
|
type QueryMap,
|
|
174
257
|
type BoundCustomQueries,
|
|
258
|
+
type QueryFamilies,
|
|
175
259
|
type RenderOptions,
|
|
176
260
|
type DebugUtilities,
|
|
177
261
|
type ScreenshotResult,
|
|
@@ -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,26 @@ 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
|
+
*/
|
|
71
|
+
const getController = <T extends Gtk.EventController>(
|
|
72
|
+
widget: Gtk.Widget,
|
|
73
|
+
controllerType: ControllerConstructor<T>,
|
|
74
|
+
): T => {
|
|
75
|
+
const [controller] = getAllControllers(widget, controllerType);
|
|
76
|
+
|
|
77
|
+
if (controller === undefined) {
|
|
78
|
+
throw new Error(`No ${controllerType.name} controller is attached to the widget`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return controller;
|
|
82
|
+
};
|
|
83
|
+
|
|
42
84
|
const getOrCreateControllers = <T extends Gtk.EventController>(
|
|
43
85
|
widget: Gtk.Widget,
|
|
44
86
|
controllerType: ControllerConstructor<T>,
|
|
@@ -55,4 +97,11 @@ const getOrCreateControllers = <T extends Gtk.EventController>(
|
|
|
55
97
|
return [controller];
|
|
56
98
|
};
|
|
57
99
|
|
|
58
|
-
export {
|
|
100
|
+
export {
|
|
101
|
+
queryAllControllers,
|
|
102
|
+
queryController,
|
|
103
|
+
getAllControllers,
|
|
104
|
+
getController,
|
|
105
|
+
getOrCreateControllers,
|
|
106
|
+
type ControllerConstructor,
|
|
107
|
+
};
|
|
@@ -9,13 +9,17 @@ type DropContent = string | number | boolean | GObject.Value;
|
|
|
9
9
|
|
|
10
10
|
/** Options for a drop: the drop coordinates within the target. */
|
|
11
11
|
type DropOptions = {
|
|
12
|
+
/** Horizontal drop coordinate within the target, in pixels. */
|
|
12
13
|
x?: number;
|
|
14
|
+
/** Vertical drop coordinate within the target, in pixels. */
|
|
13
15
|
y?: number;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/** A drag offset relative to the drag start point. */
|
|
17
19
|
type DragOffset = {
|
|
20
|
+
/** Horizontal distance from the drag start point, in pixels. */
|
|
18
21
|
x: number;
|
|
22
|
+
/** Vertical distance from the drag start point, in pixels. */
|
|
19
23
|
y: number;
|
|
20
24
|
};
|
|
21
25
|
|
|
@@ -24,9 +28,13 @@ type DragOffset = {
|
|
|
24
28
|
* explicit intermediate offsets emitted before the final one.
|
|
25
29
|
*/
|
|
26
30
|
type DragOptions = {
|
|
31
|
+
/** Horizontal coordinate the drag begins at; defaults to 0. */
|
|
27
32
|
startX?: number;
|
|
33
|
+
/** Vertical coordinate the drag begins at; defaults to 0. */
|
|
28
34
|
startY?: number;
|
|
35
|
+
/** Number of evenly interpolated drag updates, the last of which is the requested offset; defaults to 2. */
|
|
29
36
|
steps?: number;
|
|
37
|
+
/** Explicit intermediate offsets emitted before the requested one, used instead of `steps`. */
|
|
30
38
|
offsets?: DragOffset[];
|
|
31
39
|
};
|
|
32
40
|
|
|
@@ -37,8 +45,8 @@ type DragInstancePatch = {
|
|
|
37
45
|
|
|
38
46
|
type SavedDragState = {
|
|
39
47
|
instance: DragInstancePatch;
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
hasOwnStartPoint: boolean;
|
|
49
|
+
hasOwnOffset: boolean;
|
|
42
50
|
previousStartPoint: DragInstancePatch["getStartPoint"];
|
|
43
51
|
previousOffset: DragInstancePatch["getOffset"];
|
|
44
52
|
};
|
|
@@ -65,32 +73,56 @@ const buildDropValue = (content: DropContent): GObject.Value => {
|
|
|
65
73
|
});
|
|
66
74
|
};
|
|
67
75
|
|
|
76
|
+
/** Emits `enter` at the widget's origin on its motion controllers, adding one when it has none. */
|
|
68
77
|
const hover = (widget: Gtk.Widget): Promise<void> =>
|
|
69
78
|
dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
|
|
70
79
|
controller.emit("enter", 0, 0);
|
|
71
80
|
});
|
|
72
81
|
|
|
82
|
+
/** Emits `leave` on the widget's motion controllers, adding one when it has none. */
|
|
73
83
|
const unhover = (widget: Gtk.Widget): Promise<void> =>
|
|
74
84
|
dispatchOnOrCreateControllers(widget, Gtk.EventControllerMotion, (controller) => {
|
|
75
85
|
controller.emit("leave");
|
|
76
86
|
});
|
|
77
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Emits `angle-changed` on the widget's rotate gestures.
|
|
90
|
+
*
|
|
91
|
+
* @param angle Current angle, in radians.
|
|
92
|
+
* @param deltaAngle Difference from the angle the gesture started at; defaults to `angle`.
|
|
93
|
+
* @throws When the widget has no Gtk.GestureRotate.
|
|
94
|
+
*/
|
|
78
95
|
const rotate = (widget: Gtk.Widget, angle: number, deltaAngle: number = angle): Promise<void> =>
|
|
79
96
|
dispatchOnControllers(widget, Gtk.GestureRotate, (controller) => {
|
|
80
97
|
controller.emit("angle-changed", angle, deltaAngle);
|
|
81
98
|
},
|
|
82
99
|
);
|
|
83
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Emits `scale-changed` with the given scale delta on the widget's zoom gestures.
|
|
103
|
+
*
|
|
104
|
+
* @throws When the widget has no Gtk.GestureZoom.
|
|
105
|
+
*/
|
|
84
106
|
const zoom = (widget: Gtk.Widget, scale: number): Promise<void> =>
|
|
85
107
|
dispatchOnControllers(widget, Gtk.GestureZoom, (controller) => {
|
|
86
108
|
controller.emit("scale-changed", scale);
|
|
87
109
|
});
|
|
88
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Emits `swipe` with the given per-axis velocity, in pixels per second, on the widget's swipe gestures.
|
|
113
|
+
*
|
|
114
|
+
* @throws When the widget has no Gtk.GestureSwipe.
|
|
115
|
+
*/
|
|
89
116
|
const swipe = (widget: Gtk.Widget, velocityX: number, velocityY: number): Promise<void> =>
|
|
90
117
|
dispatchOnControllers(widget, Gtk.GestureSwipe, (controller) => {
|
|
91
118
|
controller.emit("swipe", velocityX, velocityY);
|
|
92
119
|
});
|
|
93
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Emits `pressed` at the given point in widget coordinates on the widget's long-press gestures.
|
|
123
|
+
*
|
|
124
|
+
* @throws When the widget has no Gtk.GestureLongPress.
|
|
125
|
+
*/
|
|
94
126
|
const longPress = (widget: Gtk.Widget, x = 0, y = 0): Promise<void> =>
|
|
95
127
|
dispatchOnControllers(widget, Gtk.GestureLongPress, (controller) => {
|
|
96
128
|
controller.emit("pressed", x, y);
|
|
@@ -99,13 +131,13 @@ const longPress = (widget: Gtk.Widget, x = 0, y = 0): Promise<void> =>
|
|
|
99
131
|
const restoreDragState = (saved: SavedDragState): void => {
|
|
100
132
|
const { instance } = saved;
|
|
101
133
|
|
|
102
|
-
if (saved.
|
|
134
|
+
if (saved.hasOwnStartPoint) {
|
|
103
135
|
instance.getStartPoint = saved.previousStartPoint;
|
|
104
136
|
} else {
|
|
105
137
|
delete instance.getStartPoint;
|
|
106
138
|
}
|
|
107
139
|
|
|
108
|
-
if (saved.
|
|
140
|
+
if (saved.hasOwnOffset) {
|
|
109
141
|
instance.getOffset = saved.previousOffset;
|
|
110
142
|
} else {
|
|
111
143
|
delete instance.getOffset;
|
|
@@ -117,8 +149,8 @@ const patchDragState = (controller: Gtk.GestureDrag, start: DragOffset, offset:
|
|
|
117
149
|
|
|
118
150
|
const saved: SavedDragState = {
|
|
119
151
|
instance,
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
hasOwnStartPoint: Object.hasOwn(instance, "getStartPoint"),
|
|
153
|
+
hasOwnOffset: Object.hasOwn(instance, "getOffset"),
|
|
122
154
|
previousStartPoint: instance.getStartPoint,
|
|
123
155
|
previousOffset: instance.getOffset,
|
|
124
156
|
};
|
|
@@ -177,6 +209,14 @@ const resolveDragUpdates = (dx: number, dy: number, options: DragOptions): DragO
|
|
|
177
209
|
return updates;
|
|
178
210
|
};
|
|
179
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Runs a `drag-begin`, `drag-update`, `drag-end` sequence ending at the offset `dx`, `dy` on the
|
|
214
|
+
* widget's drag gestures, overriding each gesture's start point and offset for the duration so
|
|
215
|
+
* handlers read the simulated values back.
|
|
216
|
+
*
|
|
217
|
+
* @throws When the widget is a Gtk.Range, whose slider reads pointer coordinates from the display,
|
|
218
|
+
* or when it has no Gtk.GestureDrag.
|
|
219
|
+
*/
|
|
180
220
|
const drag = async (widget: Gtk.Widget, dx: number, dy: number, options: DragOptions = {}): Promise<void> => {
|
|
181
221
|
if (widget instanceof Gtk.Range) {
|
|
182
222
|
throw new TypeError(
|
|
@@ -202,11 +242,22 @@ const emitDrop = (target: Gtk.Widget, content: DropContent, options: DropOptions
|
|
|
202
242
|
}
|
|
203
243
|
};
|
|
204
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Emits `drop` with the given content on every drop target attached to the widget.
|
|
247
|
+
*
|
|
248
|
+
* @throws When the widget has no Gtk.DropTarget.
|
|
249
|
+
*/
|
|
205
250
|
const drop = (widget: Gtk.Widget, content: DropContent, options: DropOptions = {}): Promise<void> =>
|
|
206
251
|
wrapEvent(widget, () => {
|
|
207
252
|
emitDrop(widget, content, options);
|
|
208
253
|
});
|
|
209
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Emits `drop` with the given content on the target's drop targets, after checking that the source
|
|
257
|
+
* carries a drag source.
|
|
258
|
+
*
|
|
259
|
+
* @throws When the source has no Gtk.DragSource, or the target no Gtk.DropTarget.
|
|
260
|
+
*/
|
|
210
261
|
const dragAndDrop = async (
|
|
211
262
|
source: Gtk.Widget,
|
|
212
263
|
target: Gtk.Widget,
|
package/src/user-event/index.ts
CHANGED
|
@@ -13,29 +13,56 @@ import { clear, copy, cut, paste, type } from "./text.js";
|
|
|
13
13
|
* keyboard, pointer, gestures, selection, and scrolling.
|
|
14
14
|
*/
|
|
15
15
|
type UserEvent = {
|
|
16
|
+
/**
|
|
17
|
+
* Clicks a button, toggles a switch, and otherwise activates the widget, falling back to a click
|
|
18
|
+
* gesture on its nearest clickable ancestor when activation does nothing.
|
|
19
|
+
*/
|
|
16
20
|
click: typeof click;
|
|
21
|
+
/** Emits a two-press click gesture on the widget. */
|
|
17
22
|
dblClick: typeof dblClick;
|
|
23
|
+
/** Emits a three-press click gesture on the widget. */
|
|
18
24
|
tripleClick: typeof tripleClick;
|
|
25
|
+
/** Moves focus within the widget's root, forward by default and backward with `isShiftHeld`. */
|
|
19
26
|
tab: typeof tab;
|
|
27
|
+
/** Focuses an editable widget, applies any initial selection, and inserts the text at the cursor. */
|
|
20
28
|
type: typeof type;
|
|
29
|
+
/** Empties an editable widget's text. */
|
|
21
30
|
clear: typeof clear;
|
|
31
|
+
/** Writes an editable widget's current selection to the clipboard. */
|
|
22
32
|
copy: typeof copy;
|
|
33
|
+
/** Writes an editable widget's current selection to the clipboard and deletes it. */
|
|
23
34
|
cut: typeof cut;
|
|
35
|
+
/** Inserts the given text, or the clipboard's text, at an editable widget's cursor. */
|
|
24
36
|
paste: typeof paste;
|
|
37
|
+
/** Selects the items at the given positions in a list view, drop-down, combo box, or list box. */
|
|
25
38
|
selectOptions: typeof selectOptions;
|
|
39
|
+
/** Deselects the items at the given positions in a list view or list box. */
|
|
26
40
|
deselectOptions: typeof deselectOptions;
|
|
41
|
+
/** Emits a motion `enter` on the widget, adding a motion controller when it has none. */
|
|
27
42
|
hover: typeof hover;
|
|
43
|
+
/** Emits a motion `leave` on the widget, adding a motion controller when it has none. */
|
|
28
44
|
unhover: typeof unhover;
|
|
45
|
+
/** Emits `angle-changed` on the widget's rotate gestures. */
|
|
29
46
|
rotate: typeof rotate;
|
|
47
|
+
/** Emits `scale-changed` on the widget's zoom gestures. */
|
|
30
48
|
zoom: typeof zoom;
|
|
49
|
+
/** Emits `swipe` with the given velocity on the widget's swipe gestures. */
|
|
31
50
|
swipe: typeof swipe;
|
|
51
|
+
/** Emits `pressed` at the given point on the widget's long-press gestures. */
|
|
32
52
|
longPress: typeof longPress;
|
|
53
|
+
/** Runs a begin, update, and end sequence on the widget's drag gestures, ending at the given offset. */
|
|
33
54
|
drag: typeof drag;
|
|
55
|
+
/** Emits `drop` with the given content on the widget's drop targets. */
|
|
34
56
|
drop: typeof drop;
|
|
57
|
+
/** Requires a drag source on the source widget, then emits `drop` with the content on the target's drop targets. */
|
|
35
58
|
dragAndDrop: typeof dragAndDrop;
|
|
59
|
+
/** Emits a jump `change-value` on a Gtk.Range so it moves to the given value. */
|
|
36
60
|
slide: typeof slide;
|
|
61
|
+
/** Adds the delta to the adjustments of the widget itself, or of its nearest scrollable ancestor. */
|
|
37
62
|
scroll: typeof scroll;
|
|
63
|
+
/** Sends a key sequence, dispatching matching shortcuts and tracking held modifiers across calls. */
|
|
38
64
|
keyboard: (widget: Gtk.Widget, input: string) => Promise<void>;
|
|
65
|
+
/** Applies a pointer token, tracking whether the left button is held across calls. */
|
|
39
66
|
pointer: (widget: Gtk.Widget, input: PointerInput) => Promise<void>;
|
|
40
67
|
};
|
|
41
68
|
|
|
@@ -6,12 +6,13 @@ import { fireEvent } from "../fire-event.js";
|
|
|
6
6
|
import { getOrCreateControllers } from "./controller.js";
|
|
7
7
|
import { wrapEvent } from "./event-wrapper.js";
|
|
8
8
|
|
|
9
|
-
/** Options for
|
|
9
|
+
/** Options for `userEvent.tab`. */
|
|
10
10
|
type TabOptions = {
|
|
11
|
-
|
|
11
|
+
/** Move focus backward instead of forward. */
|
|
12
|
+
isShiftHeld?: boolean;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
type KeyAction = { keyval: number;
|
|
15
|
+
type KeyAction = { keyval: number; isPress: boolean };
|
|
15
16
|
type ParseStep = { actions: KeyAction[]; next: number };
|
|
16
17
|
|
|
17
18
|
const KEY_MAP: Record<string, number> = {
|
|
@@ -58,9 +59,10 @@ const MODIFIER_KEYVAL_TO_MASK: Record<number, number> = {
|
|
|
58
59
|
[Gdk.KEY_Meta_R]: Gdk.ModifierType.META_MASK,
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
/** Moves focus within the widget's root, forward by default and backward when `isShiftHeld` is set. */
|
|
61
63
|
const tab = (widget: Gtk.Widget, options?: TabOptions): Promise<void> =>
|
|
62
64
|
wrapEvent(widget, () => {
|
|
63
|
-
const direction = options?.
|
|
65
|
+
const direction = options?.isShiftHeld ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
|
|
64
66
|
const root = widget.getRoot();
|
|
65
67
|
|
|
66
68
|
if (root) {
|
|
@@ -68,7 +70,7 @@ const tab = (widget: Gtk.Widget, options?: TabOptions): Promise<void> =>
|
|
|
68
70
|
}
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
const parseKeyToken = (token: string): { keyval: number;
|
|
73
|
+
const parseKeyToken = (token: string): { keyval: number; isPress: boolean; isRelease: boolean } => {
|
|
72
74
|
let keyName = token;
|
|
73
75
|
let isPress = true;
|
|
74
76
|
let isRelease = true;
|
|
@@ -87,7 +89,7 @@ const parseKeyToken = (token: string): { keyval: number; press: boolean; release
|
|
|
87
89
|
throw new Error(`Unknown key: {${keyName}}`);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
return { keyval,
|
|
92
|
+
return { keyval, isPress, isRelease };
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const parseCharAt = (input: string, i: number): ParseStep => {
|
|
@@ -95,8 +97,8 @@ const parseCharAt = (input: string, i: number): ParseStep => {
|
|
|
95
97
|
|
|
96
98
|
return {
|
|
97
99
|
actions: [
|
|
98
|
-
{ keyval,
|
|
99
|
-
{ keyval,
|
|
100
|
+
{ keyval, isPress: true },
|
|
101
|
+
{ keyval, isPress: false },
|
|
100
102
|
],
|
|
101
103
|
next: i + 1,
|
|
102
104
|
};
|
|
@@ -109,15 +111,15 @@ const parseBraceAt = (input: string, i: number): ParseStep | null => {
|
|
|
109
111
|
return null;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
const { keyval,
|
|
114
|
+
const { keyval, isPress, isRelease } = parseKeyToken(input.slice(i + 1, endBrace));
|
|
113
115
|
const actions: KeyAction[] = [];
|
|
114
116
|
|
|
115
|
-
if (
|
|
116
|
-
actions.push({ keyval,
|
|
117
|
+
if (isPress) {
|
|
118
|
+
actions.push({ keyval, isPress: true });
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
if (
|
|
120
|
-
actions.push({ keyval,
|
|
121
|
+
if (isRelease) {
|
|
122
|
+
actions.push({ keyval, isPress: false });
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
return { actions, next: endBrace + 1 };
|
|
@@ -151,7 +153,7 @@ const updateModifierState = (state: UserEventState, action: KeyAction): void =>
|
|
|
151
153
|
return;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
if (action.
|
|
156
|
+
if (action.isPress) {
|
|
155
157
|
state.modifierState |= mask;
|
|
156
158
|
} else {
|
|
157
159
|
state.modifierState &= ~mask;
|
|
@@ -267,13 +269,13 @@ const applyKeyAction = async (
|
|
|
267
269
|
action: KeyAction,
|
|
268
270
|
): Promise<void> => {
|
|
269
271
|
updateModifierState(state, action);
|
|
270
|
-
const signalName = action.
|
|
272
|
+
const signalName = action.isPress ? "key-pressed" : "key-released";
|
|
271
273
|
|
|
272
274
|
for (const controller of controllers) {
|
|
273
275
|
controller.emit(signalName, action.keyval, 0, state.modifierState);
|
|
274
276
|
}
|
|
275
277
|
|
|
276
|
-
if (action.
|
|
278
|
+
if (action.isPress) {
|
|
277
279
|
await handleKeyPress(widget, action.keyval, state.modifierState);
|
|
278
280
|
}
|
|
279
281
|
};
|