@gtkx/testing 1.0.0-rc.3 → 1.0.0

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 (215) hide show
  1. package/README.md +8 -9
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/act.d.ts +2 -0
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js.map +1 -1
  9. package/dist/build-queries.d.ts +27 -10
  10. package/dist/build-queries.d.ts.map +1 -1
  11. package/dist/build-queries.js +39 -30
  12. package/dist/build-queries.js.map +1 -1
  13. package/dist/cleanup-registry.d.ts +1 -1
  14. package/dist/cleanup-registry.d.ts.map +1 -1
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +3 -3
  19. package/dist/config.js.map +1 -1
  20. package/dist/errors.d.ts +10 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +12 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/fire-event.d.ts +35 -6
  25. package/dist/fire-event.d.ts.map +1 -1
  26. package/dist/fire-event.js +23 -7
  27. package/dist/fire-event.js.map +1 -1
  28. package/dist/frame-sync.d.ts +2 -1
  29. package/dist/frame-sync.d.ts.map +1 -1
  30. package/dist/frame-sync.js +15 -5
  31. package/dist/frame-sync.js.map +1 -1
  32. package/dist/harness-window.d.ts +4 -0
  33. package/dist/harness-window.d.ts.map +1 -0
  34. package/dist/harness-window.js +6 -0
  35. package/dist/harness-window.js.map +1 -0
  36. package/dist/index.d.ts +14 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +9 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal.d.ts +4 -0
  41. package/dist/internal.d.ts.map +1 -0
  42. package/dist/internal.js +4 -0
  43. package/dist/internal.js.map +1 -0
  44. package/dist/matchers.d.ts +125 -39
  45. package/dist/matchers.d.ts.map +1 -1
  46. package/dist/matchers.js +156 -18
  47. package/dist/matchers.js.map +1 -1
  48. package/dist/pretty-format.d.ts +2 -0
  49. package/dist/pretty-format.d.ts.map +1 -0
  50. package/dist/pretty-format.js +2 -0
  51. package/dist/pretty-format.js.map +1 -0
  52. package/dist/pretty-widget.d.ts +7 -3
  53. package/dist/pretty-widget.d.ts.map +1 -1
  54. package/dist/pretty-widget.js +74 -57
  55. package/dist/pretty-widget.js.map +1 -1
  56. package/dist/production.d.ts +8 -0
  57. package/dist/production.d.ts.map +1 -0
  58. package/dist/production.js +34 -0
  59. package/dist/production.js.map +1 -0
  60. package/dist/queries.d.ts +99 -56
  61. package/dist/queries.d.ts.map +1 -1
  62. package/dist/queries.js +123 -83
  63. package/dist/queries.js.map +1 -1
  64. package/dist/query-helpers.d.ts +44 -0
  65. package/dist/query-helpers.d.ts.map +1 -0
  66. package/dist/query-helpers.js +57 -0
  67. package/dist/query-helpers.js.map +1 -0
  68. package/dist/render-children.d.ts +9 -0
  69. package/dist/render-children.d.ts.map +1 -0
  70. package/dist/render-children.js +11 -0
  71. package/dist/render-children.js.map +1 -0
  72. package/dist/render.d.ts.map +1 -1
  73. package/dist/render.js +13 -11
  74. package/dist/render.js.map +1 -1
  75. package/dist/role-helpers.d.ts +12 -3
  76. package/dist/role-helpers.d.ts.map +1 -1
  77. package/dist/role-helpers.js +13 -4
  78. package/dist/role-helpers.js.map +1 -1
  79. package/dist/role-naming.d.ts +5 -0
  80. package/dist/role-naming.d.ts.map +1 -0
  81. package/dist/role-naming.js +96 -0
  82. package/dist/role-naming.js.map +1 -0
  83. package/dist/screenshot.d.ts +12 -20
  84. package/dist/screenshot.d.ts.map +1 -1
  85. package/dist/screenshot.js +183 -91
  86. package/dist/screenshot.js.map +1 -1
  87. package/dist/suggestions.d.ts +3 -0
  88. package/dist/suggestions.d.ts.map +1 -1
  89. package/dist/suggestions.js.map +1 -1
  90. package/dist/traversal.d.ts +9 -1
  91. package/dist/traversal.d.ts.map +1 -1
  92. package/dist/traversal.js +26 -6
  93. package/dist/traversal.js.map +1 -1
  94. package/dist/types.d.ts +67 -17
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/dist/user-event/adjustment.d.ts +16 -0
  98. package/dist/user-event/adjustment.d.ts.map +1 -1
  99. package/dist/user-event/adjustment.js +34 -2
  100. package/dist/user-event/adjustment.js.map +1 -1
  101. package/dist/user-event/click.d.ts +45 -3
  102. package/dist/user-event/click.d.ts.map +1 -1
  103. package/dist/user-event/click.js +177 -39
  104. package/dist/user-event/click.js.map +1 -1
  105. package/dist/user-event/column-header-click.d.ts +4 -0
  106. package/dist/user-event/column-header-click.d.ts.map +1 -0
  107. package/dist/user-event/column-header-click.js +65 -0
  108. package/dist/user-event/column-header-click.js.map +1 -0
  109. package/dist/user-event/controller.d.ts +29 -0
  110. package/dist/user-event/controller.d.ts.map +1 -1
  111. package/dist/user-event/controller.js +28 -0
  112. package/dist/user-event/controller.js.map +1 -1
  113. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  114. package/dist/user-event/event-wrapper.js +2 -1
  115. package/dist/user-event/event-wrapper.js.map +1 -1
  116. package/dist/user-event/gesture.d.ts +51 -0
  117. package/dist/user-event/gesture.d.ts.map +1 -1
  118. package/dist/user-event/gesture.js +47 -4
  119. package/dist/user-event/gesture.js.map +1 -1
  120. package/dist/user-event/index.d.ts +70 -2
  121. package/dist/user-event/index.d.ts.map +1 -1
  122. package/dist/user-event/index.js +43 -26
  123. package/dist/user-event/index.js.map +1 -1
  124. package/dist/user-event/indexed-children.d.ts +10 -0
  125. package/dist/user-event/indexed-children.d.ts.map +1 -0
  126. package/dist/user-event/indexed-children.js +50 -0
  127. package/dist/user-event/indexed-children.js.map +1 -0
  128. package/dist/user-event/keyboard.d.ts +4 -2
  129. package/dist/user-event/keyboard.d.ts.map +1 -1
  130. package/dist/user-event/keyboard.js +46 -16
  131. package/dist/user-event/keyboard.js.map +1 -1
  132. package/dist/user-event/list-row-click.d.ts +4 -0
  133. package/dist/user-event/list-row-click.d.ts.map +1 -0
  134. package/dist/user-event/list-row-click.js +39 -0
  135. package/dist/user-event/list-row-click.js.map +1 -0
  136. package/dist/user-event/native-click.d.ts +6 -0
  137. package/dist/user-event/native-click.d.ts.map +1 -0
  138. package/dist/user-event/native-click.js +32 -0
  139. package/dist/user-event/native-click.js.map +1 -0
  140. package/dist/user-event/pointer.d.ts +1 -1
  141. package/dist/user-event/pointer.d.ts.map +1 -1
  142. package/dist/user-event/pointer.js +37 -13
  143. package/dist/user-event/pointer.js.map +1 -1
  144. package/dist/user-event/selection.d.ts +18 -0
  145. package/dist/user-event/selection.d.ts.map +1 -1
  146. package/dist/user-event/selection.js +56 -30
  147. package/dist/user-event/selection.js.map +1 -1
  148. package/dist/user-event/state.d.ts +1 -1
  149. package/dist/user-event/state.d.ts.map +1 -1
  150. package/dist/user-event/state.js +1 -1
  151. package/dist/user-event/state.js.map +1 -1
  152. package/dist/user-event/text.d.ts +34 -3
  153. package/dist/user-event/text.d.ts.map +1 -1
  154. package/dist/user-event/text.js +94 -17
  155. package/dist/user-event/text.js.map +1 -1
  156. package/dist/wait-for.d.ts +2 -0
  157. package/dist/wait-for.d.ts.map +1 -1
  158. package/dist/wait-for.js.map +1 -1
  159. package/dist/widget-accessible-properties.d.ts +14 -10
  160. package/dist/widget-accessible-properties.d.ts.map +1 -1
  161. package/dist/widget-accessible-properties.js +135 -173
  162. package/dist/widget-accessible-properties.js.map +1 -1
  163. package/dist/widget-getters.d.ts +8 -0
  164. package/dist/widget-getters.d.ts.map +1 -0
  165. package/dist/widget-getters.js +19 -0
  166. package/dist/widget-getters.js.map +1 -0
  167. package/dist/within.d.ts +3 -2
  168. package/dist/within.d.ts.map +1 -1
  169. package/dist/within.js +3 -2
  170. package/dist/within.js.map +1 -1
  171. package/package.json +13 -8
  172. package/src/accessible-native.ts +333 -0
  173. package/src/act.ts +2 -0
  174. package/src/build-queries.ts +66 -48
  175. package/src/cleanup-registry.ts +1 -1
  176. package/src/config.ts +4 -3
  177. package/src/errors.ts +14 -3
  178. package/src/fire-event.ts +61 -8
  179. package/src/frame-sync.ts +21 -5
  180. package/src/harness-window.ts +9 -0
  181. package/src/index.ts +18 -11
  182. package/src/internal.ts +11 -0
  183. package/src/matchers.ts +466 -64
  184. package/src/pretty-format.ts +1 -0
  185. package/src/pretty-widget.ts +100 -72
  186. package/src/production.ts +52 -0
  187. package/src/queries.ts +128 -89
  188. package/src/query-helpers.ts +97 -0
  189. package/src/render-children.ts +25 -0
  190. package/src/render.tsx +18 -13
  191. package/src/role-helpers.ts +14 -4
  192. package/src/role-naming.ts +99 -0
  193. package/src/screenshot.ts +272 -103
  194. package/src/suggestions.ts +3 -0
  195. package/src/traversal.ts +49 -6
  196. package/src/types.ts +69 -19
  197. package/src/user-event/adjustment.ts +42 -2
  198. package/src/user-event/click.ts +248 -46
  199. package/src/user-event/column-header-click.ts +90 -0
  200. package/src/user-event/controller.ts +29 -0
  201. package/src/user-event/event-wrapper.ts +3 -1
  202. package/src/user-event/gesture.ts +57 -6
  203. package/src/user-event/index.ts +119 -29
  204. package/src/user-event/indexed-children.ts +75 -0
  205. package/src/user-event/keyboard.ts +70 -20
  206. package/src/user-event/list-row-click.ts +57 -0
  207. package/src/user-event/native-click.ts +46 -0
  208. package/src/user-event/pointer.ts +52 -13
  209. package/src/user-event/selection.ts +74 -33
  210. package/src/user-event/state.ts +2 -2
  211. package/src/user-event/text.ts +121 -23
  212. package/src/wait-for.ts +2 -0
  213. package/src/widget-accessible-properties.ts +183 -214
  214. package/src/widget-getters.ts +32 -0
  215. package/src/within.ts +3 -2
package/src/traversal.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
2
 
3
+ /**
4
+ * A scope that resolves to a single root widget: the widget itself, the widget a controller or
5
+ * layout manager is attached to, or a list item's or header's child.
6
+ */
3
7
  type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
4
8
  /**
5
9
  * A scope that queries and traversal can run against: a widget, an event
@@ -8,26 +12,55 @@ type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk
8
12
  */
9
13
  type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
10
14
 
15
+ /** Container sentinel that widens a query to every toplevel window currently open. */
11
16
  const TOPLEVELS: unique symbol = Symbol("gtkx.toplevels");
12
17
 
13
18
  const isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;
19
+ const isAnyWidget = (): boolean => true;
20
+ const isOnScreen = (widget: Gtk.Widget): boolean => widget.getMapped();
21
+
22
+ const traverseWidgetTree = function* (
23
+ root: Gtk.Widget,
24
+ isIncluded: (widget: Gtk.Widget) => boolean,
25
+ ): Generator<Gtk.Widget> {
26
+ if (isIncluded(root)) {
27
+ yield root;
28
+ }
14
29
 
15
- const traverseWidgetTree = function* (root: Gtk.Widget): Generator<Gtk.Widget> {
16
- yield root;
17
30
  let child = root.getFirstChild();
18
31
 
19
32
  while (child) {
20
- yield* traverseWidgetTree(child);
33
+ yield* traverseWidgetTree(child, isIncluded);
34
+ child = child.getNextSibling();
35
+ }
36
+ };
37
+
38
+ const children = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
39
+ let child = widget.getFirstChild();
40
+
41
+ while (child) {
42
+ yield child;
21
43
  child = child.getNextSibling();
22
44
  }
23
45
  };
24
46
 
25
47
  const descendants = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
26
- const tree = traverseWidgetTree(widget);
48
+ const tree = traverseWidgetTree(widget, isAnyWidget);
27
49
  tree.next();
28
50
  yield* tree;
29
51
  };
30
52
 
53
+ const relationCandidates = (widget: Gtk.Widget): Gtk.Accessible[] => {
54
+ const pool: Gtk.Accessible[] = [...widget.listMnemonicLabels(), ...descendants(widget)];
55
+ const root = widget.getRoot();
56
+
57
+ if (root instanceof Gtk.Widget) {
58
+ pool.push(...traverseWidgetTree(root, isAnyWidget));
59
+ }
60
+
61
+ return [...new Set(pool)];
62
+ };
63
+
31
64
  const resolveRoot = (container: QueryContainer): Gtk.Widget | null => {
32
65
  if (container instanceof Gtk.Widget) {
33
66
  return container;
@@ -64,7 +97,7 @@ const roots = function* (container: Container): Generator<Gtk.Widget> {
64
97
 
65
98
  const traverse = function* (container: Container): Generator<Gtk.Widget> {
66
99
  for (const root of roots(container)) {
67
- yield* traverseWidgetTree(root);
100
+ yield* traverseWidgetTree(root, isOnScreen);
68
101
  }
69
102
  };
70
103
 
@@ -80,4 +113,14 @@ const findAll = (container: Container, isMatch: (node: Gtk.Widget) => boolean):
80
113
  return results;
81
114
  };
82
115
 
83
- export { TOPLEVELS, descendants, roots, traverse, findAll, type Container };
116
+ export {
117
+ TOPLEVELS,
118
+ children,
119
+ descendants,
120
+ findAll,
121
+ isOnScreen,
122
+ relationCandidates,
123
+ roots,
124
+ traverse,
125
+ type Container,
126
+ };
package/src/types.ts CHANGED
@@ -55,11 +55,19 @@ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
55
55
  as?: WidgetType<T> | undefined;
56
56
  } & WaitForOptions;
57
57
 
58
- /** Constraints on a widget's numeric range value used by role queries. */
58
+ /**
59
+ * Constraints on a widget's numeric range value used by role queries. The numbers come from the
60
+ * accessibility tree and match within 0.001, the resolution GTK keeps them to.
61
+ */
59
62
  type ByRoleValue = {
60
63
  /** The current value. */
61
64
  now?: number | undefined;
65
+ /** The lower bound of the value's range. */
62
66
  min?: number | undefined;
67
+ /**
68
+ * The highest value the widget can reach, which on a paged widget such as a scrollbar is its
69
+ * adjustment's upper bound less one page.
70
+ */
63
71
  max?: number | undefined;
64
72
  /** Matcher for the value's textual representation. */
65
73
  text?: Matcher | undefined;
@@ -67,17 +75,25 @@ type ByRoleValue = {
67
75
 
68
76
  /** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
69
77
  type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
78
+ /** Matcher for the widget's accessible name. */
70
79
  name?: Matcher | undefined;
80
+ /** Required checked state; a mixed check button reads as neither, so it matches neither value. */
71
81
  checked?: boolean | undefined;
82
+ /** Required active state of a toggle button; any other widget matches neither value. */
72
83
  pressed?: boolean | undefined;
84
+ /** Required selected state of a row, list item, grid cell, option or tree item. */
73
85
  selected?: boolean | undefined;
86
+ /** Required expanded state of an expander or tree expander. */
74
87
  expanded?: boolean | undefined;
75
88
  /** Heading or hierarchy level. */
76
89
  level?: number | undefined;
90
+ /** Required busy state, which an unset state satisfies as false. */
77
91
  busy?: boolean | undefined;
92
+ /** Matcher for the widget's own accessible description, ignoring its `described-by` targets. */
78
93
  description?: Matcher | undefined;
94
+ /** Constraints on the widget's range value, each checked only when given. */
79
95
  value?: ByRoleValue | undefined;
80
- /** When true, include widgets excluded from the accessibility tree. */
96
+ /** When true, include widgets excluded from the accessibility tree. Widgets that are not mapped stay excluded. */
81
97
  hidden?: boolean | undefined;
82
98
  };
83
99
 
@@ -86,26 +102,39 @@ type WrapperComponent = ComponentType<{
86
102
  children: ReactNode;
87
103
  }>;
88
104
 
105
+ /** A custom query, taking the scope to search as its first argument and its matcher arguments after it. */
89
106
  type Query = (container: Container, ...args: never[]) => unknown;
107
+ /** Custom queries keyed by the name each is bound under. */
90
108
  type QueryMap = Record<string, Query>;
91
109
 
110
+ /** A query with its container argument already applied. */
92
111
  type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
93
112
  ? (...args: A) => R
94
113
  : never;
95
114
 
115
+ /** Custom queries with their container argument already applied, keyed as they were passed in. */
96
116
  type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
97
117
 
118
+ /** What each variant of a query family yields for the widget type it matched. */
98
119
  type QueryFamilyReturns<T extends Gtk.Widget> = {
120
+ /** The single match, or null when nothing matched; throws when more than one matched. */
99
121
  queryBy: T | null;
122
+ /** Every match, empty when nothing matched. */
100
123
  queryAllBy: T[];
124
+ /** The single match; throws when nothing or more than one matched. */
101
125
  getBy: T;
126
+ /** Every match; throws when nothing matched. */
102
127
  getAllBy: T[];
128
+ /** The single match, retried until it appears or the timeout elapses. */
103
129
  findBy: Promise<T>;
130
+ /** Every match, retried until at least one appears or the timeout elapses. */
104
131
  findAllBy: Promise<T[]>;
105
132
  };
106
133
 
134
+ /** What a query family matches against, which fixes the arguments it takes. */
107
135
  type QueryKind = "role" | "text" | "name" | "value";
108
136
 
137
+ /** The arguments a query of the given kind takes after the family's leading ones. */
109
138
  type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
110
139
  ? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
111
140
  : Kind extends "name"
@@ -115,9 +144,8 @@ type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "rol
115
144
  : [text: Matcher, options?: MatcherOptions<T>];
116
145
 
117
146
  /**
118
- * One query family (`queryBy`, `getBy`, `findBy` and their `All` variants) for a single suffix.
119
- * Each member takes an explicit widget type, as Testing Library's queries do, and also infers it
120
- * from an `as` option.
147
+ * One family's query variants, each named for its variant followed by `Suffix` and taking `Head`
148
+ * ahead of the family's own arguments.
121
149
  */
122
150
  type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
123
151
  [K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
@@ -125,6 +153,10 @@ type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unk
125
153
  ) => QueryFamilyReturns<T>[K];
126
154
  };
127
155
 
156
+ /**
157
+ * Every built-in query, spanning the Role, LabelText, Text, Name, PlaceholderText, and DisplayValue
158
+ * families, with `Head` prepended to each signature.
159
+ */
128
160
  type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
129
161
  QueryFamily<"LabelText", "text", Head> &
130
162
  QueryFamily<"Text", "text", Head> &
@@ -137,14 +169,16 @@ type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
137
169
  * React behavior toggles, error callbacks, and custom queries to bind.
138
170
  */
139
171
  type RenderOptions<Q extends QueryMap = Record<never, never>> = {
172
+ /** Widget or root element to mount into; an undecorated harness window is created when omitted. */
140
173
  container?: Gtk.Widget | RootElement | undefined;
141
174
  /** Root of the subtree that bound queries search. */
142
175
  baseElement?: Container | undefined;
176
+ /** Component wrapped around the rendered element, such as a context provider. */
143
177
  wrapper?: WrapperComponent | undefined;
144
178
  /** Render inside React StrictMode. */
145
- reactStrictMode?: boolean | undefined;
179
+ isReactStrictMode?: boolean | undefined;
146
180
  /** Enable widget animations during the test. */
147
- animations?: boolean | undefined;
181
+ areAnimationsEnabled?: boolean | undefined;
148
182
  /** Called for errors caught by React error boundaries. */
149
183
  onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
150
184
  /** Called for errors React recovered from automatically. */
@@ -153,40 +187,52 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
153
187
  queries?: Q | undefined;
154
188
  };
155
189
 
190
+ /** Console helpers bundled with the bound queries on {@link screen} and on a render result. */
156
191
  type DebugUtilities = {
192
+ /** Prints the widget tree of the given containers, or of the whole scope, to the console. */
157
193
  debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
194
+ /** Prints the accessible roles found in the scope, with the widgets carrying each one. */
158
195
  logRoles: () => void;
159
- screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
196
+ /**
197
+ * Captures what is on screen, which is the active toplevel window: GTK4 exposes no position for
198
+ * a toplevel, so windows cannot be composited into a single image.
199
+ */
200
+ screenshot: (options?: ScreenshotOptions) => Promise<ScreenshotResult>;
160
201
  };
161
202
 
162
203
  /** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
163
204
  type ScreenshotResult = {
205
+ /** Base64-encoded image bytes. */
164
206
  data: string;
207
+ /** MIME type of the encoded image, always `image/png`. */
165
208
  mimeType: string;
209
+ /** Image width in pixels, after the scale factor is applied. */
166
210
  width: number;
211
+ /** Image height in pixels, after the scale factor is applied. */
167
212
  height: number;
168
213
  };
169
214
 
170
- /** Options for capturing a screenshot: the poll timeout and interval, plus a rendering scale factor. */
215
+ /**
216
+ * Options for capturing a screenshot: the poll timeout and interval, a rendering scale factor, and
217
+ * a file to write the PNG to.
218
+ */
171
219
  type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
172
220
  /** Device scale factor applied when rendering. */
173
- scale?: number;
221
+ scale?: number | undefined;
222
+ /** File the PNG is written to, with its parent directories created as needed. */
223
+ path?: string | undefined;
174
224
  };
175
225
 
176
- /**
177
- * Selects the window to screenshot by index, or by title (exact string or regular expression);
178
- * undefined targets the default window.
179
- */
180
- type WindowSelector = number | string | RegExp | undefined;
181
-
182
226
  /**
183
227
  * Options for {@link renderHook}: an optional wrapper and the initial props (required unless the
184
228
  * props type permits undefined).
185
229
  */
186
230
  type RenderHookOptions<Props> = {
231
+ /** Component wrapped around the one calling the hook, such as a context provider. */
187
232
  wrapper?: WrapperComponent;
188
233
  } & (undefined extends Props
189
234
  ? {
235
+ /** Props the hook is invoked with until `rerender` is given new ones. */
190
236
  initialProps?: Props;
191
237
  }
192
238
  : {
@@ -198,9 +244,14 @@ type RenderHookOptions<Props> = {
198
244
  * props and to unmount.
199
245
  */
200
246
  type RenderHookResult<Result, Props> = {
201
- /** Holds the most recent value returned by the hook under `current`. */
202
- result: { current: Result };
247
+ /** Stable object the hook's return value is written to on every render. */
248
+ result: {
249
+ /** The hook's latest return value. */
250
+ current: Result;
251
+ };
252
+ /** Re-invokes the hook, keeping the previous props when none are given. */
203
253
  rerender: (newProps?: Props) => Promise<void>;
254
+ /** Unmounts the component that calls the hook. */
204
255
  unmount: () => Promise<void>;
205
256
  };
206
257
 
@@ -222,7 +273,6 @@ export {
222
273
  type DebugUtilities,
223
274
  type ScreenshotResult,
224
275
  type ScreenshotOptions,
225
- type WindowSelector,
226
276
  type RenderHookOptions,
227
277
  type RenderHookResult,
228
278
  };
@@ -1,9 +1,12 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
+ import { scheduleNextFrame } from "../frame-sync.js";
2
3
  import { wrapEvent } from "./event-wrapper.js";
3
4
 
4
5
  /** A scroll distance in pixels along each axis. */
5
6
  type ScrollDelta = {
7
+ /** Distance added to the horizontal adjustment. */
6
8
  x?: number;
9
+ /** Distance added to the vertical adjustment. */
7
10
  y?: number;
8
11
  };
9
12
 
@@ -12,6 +15,11 @@ type ScrollAdjustments = {
12
15
  vertical: Gtk.Adjustment | null;
13
16
  };
14
17
 
18
+ /**
19
+ * Emits a jump `change-value` so a Gtk.Range moves to the given value.
20
+ *
21
+ * @throws When the widget is not a Gtk.Range.
22
+ */
15
23
  const slide = (widget: Gtk.Widget, value: number): Promise<void> =>
16
24
  wrapEvent(widget, () => {
17
25
  if (!(widget instanceof Gtk.Range)) {
@@ -37,16 +45,47 @@ const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null
37
45
  return null;
38
46
  };
39
47
 
48
+ const stepToward = (adjustment: Gtk.Adjustment, target: number, step: number): number => {
49
+ const value = adjustment.getValue();
50
+ adjustment.setValue(value < target ? Math.min(value + step, target) : Math.max(value - step, target));
51
+
52
+ return adjustment.getValue();
53
+ };
54
+
55
+ const rampTo = (adjustment: Gtk.Adjustment, target: number, step: number): void => {
56
+ let value = adjustment.getValue();
57
+
58
+ while (value !== target) {
59
+ const moved = stepToward(adjustment, target, step);
60
+
61
+ if (moved === value) {
62
+ return;
63
+ }
64
+
65
+ value = moved;
66
+ }
67
+ };
68
+
40
69
  const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
41
70
  if (!adjustment || delta === 0) {
42
71
  return;
43
72
  }
44
73
 
45
- adjustment.setValue(adjustment.getValue() + delta);
74
+ const pageSize = adjustment.getPageSize();
75
+ rampTo(adjustment, adjustment.getValue() + delta, pageSize > 0 ? pageSize : Math.abs(delta));
46
76
  };
47
77
 
78
+ /**
79
+ * Adds the delta to the adjustments of the widget itself, or of its nearest Gtk.ScrolledWindow or
80
+ * Gtk.Scrollable ancestor.
81
+ *
82
+ * Each adjustment advances in viewport-sized steps rather than one jump, so virtualized views such
83
+ * as Gtk.ListView, Gtk.GridView and Gtk.ColumnView re-anchor onto the rows the new offset shows.
84
+ *
85
+ * @throws When neither the widget nor any of its ancestors is scrollable.
86
+ */
48
87
  const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
49
- wrapEvent(widget, () => {
88
+ wrapEvent(widget, async () => {
50
89
  const adjustments = resolveScrollAdjustments(widget);
51
90
 
52
91
  if (!adjustments) {
@@ -57,6 +96,7 @@ const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
57
96
 
58
97
  applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
59
98
  applyScrollDelta(adjustments.vertical, delta.y ?? 0);
99
+ await scheduleNextFrame(widget);
60
100
  });
61
101
 
62
102
  export { slide, scroll, type ScrollDelta };