@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.
Files changed (121) hide show
  1. package/README.md +4 -5
  2. package/dist/act.d.ts +2 -0
  3. package/dist/act.d.ts.map +1 -1
  4. package/dist/act.js.map +1 -1
  5. package/dist/bound-queries.d.ts +2 -3
  6. package/dist/bound-queries.d.ts.map +1 -1
  7. package/dist/bound-queries.js.map +1 -1
  8. package/dist/build-queries.d.ts.map +1 -1
  9. package/dist/build-queries.js +18 -7
  10. package/dist/build-queries.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +3 -3
  14. package/dist/config.js.map +1 -1
  15. package/dist/errors.js +2 -2
  16. package/dist/errors.js.map +1 -1
  17. package/dist/index.d.ts +7 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +4 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchers.d.ts +125 -21
  22. package/dist/matchers.d.ts.map +1 -1
  23. package/dist/matchers.js +258 -21
  24. package/dist/matchers.js.map +1 -1
  25. package/dist/normalize.d.ts +10 -0
  26. package/dist/normalize.d.ts.map +1 -0
  27. package/dist/normalize.js +20 -0
  28. package/dist/normalize.js.map +1 -0
  29. package/dist/pretty-widget.d.ts +2 -1
  30. package/dist/pretty-widget.d.ts.map +1 -1
  31. package/dist/pretty-widget.js +3 -3
  32. package/dist/pretty-widget.js.map +1 -1
  33. package/dist/queries.d.ts +4 -21
  34. package/dist/queries.d.ts.map +1 -1
  35. package/dist/queries.js +46 -59
  36. package/dist/queries.js.map +1 -1
  37. package/dist/render.js +3 -3
  38. package/dist/render.js.map +1 -1
  39. package/dist/suggestions.d.ts +3 -0
  40. package/dist/suggestions.d.ts.map +1 -1
  41. package/dist/suggestions.js.map +1 -1
  42. package/dist/traversal.d.ts +5 -0
  43. package/dist/traversal.d.ts.map +1 -1
  44. package/dist/traversal.js +1 -0
  45. package/dist/traversal.js.map +1 -1
  46. package/dist/types.d.ts +69 -5
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js.map +1 -1
  49. package/dist/user-event/adjustment.d.ts +13 -0
  50. package/dist/user-event/adjustment.d.ts.map +1 -1
  51. package/dist/user-event/adjustment.js +11 -0
  52. package/dist/user-event/adjustment.js.map +1 -1
  53. package/dist/user-event/click.d.ts +7 -0
  54. package/dist/user-event/click.d.ts.map +1 -1
  55. package/dist/user-event/click.js +7 -0
  56. package/dist/user-event/click.js.map +1 -1
  57. package/dist/user-event/controller.d.ts +31 -1
  58. package/dist/user-event/controller.d.ts.map +1 -1
  59. package/dist/user-event/controller.js +36 -1
  60. package/dist/user-event/controller.js.map +1 -1
  61. package/dist/user-event/gesture.d.ts +51 -0
  62. package/dist/user-event/gesture.d.ts.map +1 -1
  63. package/dist/user-event/gesture.js +47 -4
  64. package/dist/user-event/gesture.js.map +1 -1
  65. package/dist/user-event/index.d.ts +27 -0
  66. package/dist/user-event/index.d.ts.map +1 -1
  67. package/dist/user-event/index.js.map +1 -1
  68. package/dist/user-event/keyboard.d.ts +4 -2
  69. package/dist/user-event/keyboard.d.ts.map +1 -1
  70. package/dist/user-event/keyboard.js +13 -12
  71. package/dist/user-event/keyboard.js.map +1 -1
  72. package/dist/user-event/pointer.js +5 -5
  73. package/dist/user-event/pointer.js.map +1 -1
  74. package/dist/user-event/selection.d.ts +15 -0
  75. package/dist/user-event/selection.d.ts.map +1 -1
  76. package/dist/user-event/selection.js +15 -0
  77. package/dist/user-event/selection.js.map +1 -1
  78. package/dist/user-event/state.d.ts +1 -1
  79. package/dist/user-event/state.d.ts.map +1 -1
  80. package/dist/user-event/state.js +1 -1
  81. package/dist/user-event/state.js.map +1 -1
  82. package/dist/user-event/text.d.ts +30 -3
  83. package/dist/user-event/text.d.ts.map +1 -1
  84. package/dist/user-event/text.js +28 -1
  85. package/dist/user-event/text.js.map +1 -1
  86. package/dist/wait-for.d.ts +2 -0
  87. package/dist/wait-for.d.ts.map +1 -1
  88. package/dist/wait-for.js.map +1 -1
  89. package/dist/widget-accessible-properties.d.ts +28 -2
  90. package/dist/widget-accessible-properties.d.ts.map +1 -1
  91. package/dist/widget-accessible-properties.js +129 -10
  92. package/dist/widget-accessible-properties.js.map +1 -1
  93. package/dist/within.js.map +1 -1
  94. package/package.json +7 -7
  95. package/src/act.ts +2 -0
  96. package/src/bound-queries.ts +2 -3
  97. package/src/build-queries.ts +20 -8
  98. package/src/config.ts +4 -3
  99. package/src/errors.ts +2 -2
  100. package/src/index.ts +11 -2
  101. package/src/matchers.ts +500 -53
  102. package/src/normalize.ts +28 -0
  103. package/src/pretty-widget.ts +5 -4
  104. package/src/queries.ts +79 -105
  105. package/src/render.tsx +3 -3
  106. package/src/suggestions.ts +3 -0
  107. package/src/traversal.ts +5 -0
  108. package/src/types.ts +88 -4
  109. package/src/user-event/adjustment.ts +13 -0
  110. package/src/user-event/click.ts +7 -0
  111. package/src/user-event/controller.ts +50 -1
  112. package/src/user-event/gesture.ts +57 -6
  113. package/src/user-event/index.ts +27 -0
  114. package/src/user-event/keyboard.ts +18 -16
  115. package/src/user-event/pointer.ts +5 -5
  116. package/src/user-event/selection.ts +15 -0
  117. package/src/user-event/state.ts +2 -2
  118. package/src/user-event/text.ts +31 -4
  119. package/src/wait-for.ts +2 -0
  120. package/src/widget-accessible-properties.ts +188 -10
  121. 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
- reactStrictMode?: boolean | undefined;
173
+ isReactStrictMode?: boolean | undefined;
102
174
  /** Enable widget animations during the test. */
103
- animations?: boolean | undefined;
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);
@@ -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 { queryAllControllers, queryController, getAllControllers, getOrCreateControllers, type ControllerConstructor };
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
- ownsStartPoint: boolean;
41
- ownsOffset: boolean;
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.ownsStartPoint) {
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.ownsOffset) {
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
- ownsStartPoint: Object.hasOwn(instance, "getStartPoint"),
121
- ownsOffset: Object.hasOwn(instance, "getOffset"),
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,
@@ -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 {@link tab}: when `shift` is set, move focus backward instead of forward. */
9
+ /** Options for `userEvent.tab`. */
10
10
  type TabOptions = {
11
- shift?: boolean;
11
+ /** Move focus backward instead of forward. */
12
+ isShiftHeld?: boolean;
12
13
  };
13
14
 
14
- type KeyAction = { keyval: number; press: boolean };
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?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
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; press: boolean; release: boolean } => {
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, press: isPress, release: isRelease };
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, press: true },
99
- { keyval, press: false },
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, press, release } = parseKeyToken(input.slice(i + 1, endBrace));
114
+ const { keyval, isPress, isRelease } = parseKeyToken(input.slice(i + 1, endBrace));
113
115
  const actions: KeyAction[] = [];
114
116
 
115
- if (press) {
116
- actions.push({ keyval, press: true });
117
+ if (isPress) {
118
+ actions.push({ keyval, isPress: true });
117
119
  }
118
120
 
119
- if (release) {
120
- actions.push({ keyval, press: false });
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.press) {
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.press ? "key-pressed" : "key-released";
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.press) {
278
+ if (action.isPress) {
277
279
  await handleKeyPress(widget, action.keyval, state.modifierState);
278
280
  }
279
281
  };