@gtkx/testing 0.21.0 → 1.0.0-rc.2

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 (216) hide show
  1. package/README.md +137 -34
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +56 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +33 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +115 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +5 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +21 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +20 -52
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +17 -46
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +8 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +26 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +30 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +135 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +8 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +11 -30
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +4 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +37 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +17 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +59 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +117 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +24 -26
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +111 -117
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +190 -180
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +291 -320
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +16 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +15 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +9 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +13 -42
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +138 -121
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +19 -53
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +48 -63
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +7 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +24 -110
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +21 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +98 -50
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +27 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +61 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +12 -10
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +36 -15
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +115 -169
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +10 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +35 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +8 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +73 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +8 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +31 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +11 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +4 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +59 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +35 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +138 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +51 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +42 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +10 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +204 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +10 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +30 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +5 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +105 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +7 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +3 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +18 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +113 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +16 -36
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +74 -70
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +34 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +260 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +10 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +16 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +23 -12
  150. package/src/act.ts +77 -0
  151. package/src/bound-queries.ts +37 -0
  152. package/src/build-queries.ts +226 -0
  153. package/src/cleanup-registry.ts +27 -0
  154. package/src/config.ts +35 -62
  155. package/src/editable.ts +37 -0
  156. package/src/errors.ts +191 -0
  157. package/src/fire-event.ts +13 -38
  158. package/src/frame-sync.ts +51 -0
  159. package/src/index.ts +58 -15
  160. package/src/matchers.ts +251 -0
  161. package/src/pretty-widget.ts +163 -130
  162. package/src/queries.ts +485 -396
  163. package/src/register-test-hooks.ts +19 -0
  164. package/src/render-hook.tsx +38 -45
  165. package/src/render.tsx +192 -155
  166. package/src/role-helpers.ts +65 -72
  167. package/src/screen.ts +26 -114
  168. package/src/screenshot.ts +128 -56
  169. package/src/suggestions.ts +106 -0
  170. package/src/traversal.ts +56 -22
  171. package/src/types.ts +144 -172
  172. package/src/user-event/adjustment.ts +62 -0
  173. package/src/user-event/click.ts +99 -0
  174. package/src/user-event/controller.ts +58 -0
  175. package/src/user-event/dispatch.ts +25 -0
  176. package/src/user-event/event-wrapper.ts +80 -0
  177. package/src/user-event/gesture.ts +239 -0
  178. package/src/user-event/index.ts +81 -0
  179. package/src/user-event/keyboard.ts +290 -0
  180. package/src/user-event/pointer.ts +46 -0
  181. package/src/user-event/selection.ts +159 -0
  182. package/src/user-event/state.ts +8 -0
  183. package/src/user-event/text.ts +172 -0
  184. package/src/wait-for.ts +117 -78
  185. package/src/widget-accessible-properties.ts +388 -0
  186. package/src/within.ts +29 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
@@ -0,0 +1,106 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { formatRole } from "./role-helpers.js";
3
+ import {
4
+ getWidgetAccessibleName,
5
+ getWidgetDisplayValue,
6
+ getWidgetLabelledByText,
7
+ getWidgetLabelText,
8
+ getWidgetName,
9
+ getWidgetOwnLabel,
10
+ getWidgetPlaceholderText,
11
+ } from "./widget-accessible-properties.js";
12
+
13
+ /** The query variant a suggestion targets. */
14
+ type Variant = "get" | "getAll" | "query" | "queryAll" | "find" | "findAll";
15
+ /** The query family a suggestion targets. */
16
+ type Method = "Role" | "LabelText" | "PlaceholderText" | "Text" | "DisplayValue" | "Name";
17
+
18
+ /**
19
+ * A suggested query for reaching a widget, including its family, variant, method name, and a
20
+ * `toString` that renders the full call.
21
+ */
22
+ type Suggestion = {
23
+ queryName: Method;
24
+ /** The full query function name, such as `getByRole`. */
25
+ queryMethod: string;
26
+ variant: Variant;
27
+ toString: () => string;
28
+ };
29
+
30
+ const makeSuggestion = (queryName: Method, variant: Variant, argsText: string): Suggestion => {
31
+ const queryMethod = `${variant}By${queryName}`;
32
+
33
+ return {
34
+ queryName,
35
+ queryMethod,
36
+ variant,
37
+ toString: () => `${queryMethod}(${argsText})`,
38
+ };
39
+ };
40
+
41
+ const roleSuggestion = (widget: Gtk.Widget, variant: Variant): Suggestion | undefined => {
42
+ const role = widget.getAccessibleRole();
43
+
44
+ if (role === Gtk.AccessibleRole.NONE || role === Gtk.AccessibleRole.GENERIC) {
45
+ return undefined;
46
+ }
47
+
48
+ const roleText = `Gtk.AccessibleRole.${formatRole(role).toUpperCase()}`;
49
+ const name = getWidgetAccessibleName(widget);
50
+
51
+ if (name === null) {
52
+ return makeSuggestion("Role", variant, roleText);
53
+ }
54
+
55
+ return makeSuggestion("Role", variant, `${roleText}, { name: '${name}' }`);
56
+ };
57
+
58
+ const textSuggestion = (queryName: Method, variant: Variant, value: string | null): Suggestion | undefined => {
59
+ if (value === null) {
60
+ return undefined;
61
+ }
62
+
63
+ return makeSuggestion(queryName, variant, `'${value}'`);
64
+ };
65
+
66
+ /**
67
+ * Computes the recommended query for reaching the given widget, preferring role, then label text,
68
+ * placeholder text, text, display value, and name.
69
+ * @param widget Widget to build a suggestion for.
70
+ * @param variant Query variant the suggestion should use.
71
+ * @param method Restrict the suggestion to a specific query family instead of choosing by priority.
72
+ * @returns The best available suggestion, or undefined when no query applies.
73
+ */
74
+ const getSuggestedQuery = (
75
+ widget: Gtk.Widget,
76
+ variant: Variant = "get",
77
+ method?: Method,
78
+ ): Suggestion | undefined => {
79
+ const builders: Record<Method, () => Suggestion | undefined> = {
80
+ Role: () => roleSuggestion(widget, variant),
81
+ LabelText: () =>
82
+ textSuggestion("LabelText", variant, getWidgetOwnLabel(widget) ?? getWidgetLabelledByText(widget)),
83
+ PlaceholderText: () => textSuggestion("PlaceholderText", variant, getWidgetPlaceholderText(widget)),
84
+ Text: () => textSuggestion("Text", variant, getWidgetLabelText(widget)),
85
+ DisplayValue: () => textSuggestion("DisplayValue", variant, getWidgetDisplayValue(widget)),
86
+ Name: () => textSuggestion("Name", variant, getWidgetName(widget)),
87
+ };
88
+
89
+ if (method) {
90
+ return builders[method]();
91
+ }
92
+
93
+ const priority: Method[] = ["Role", "LabelText", "PlaceholderText", "Text", "DisplayValue", "Name"];
94
+
95
+ for (const candidate of priority) {
96
+ const suggestion = builders[candidate]();
97
+
98
+ if (suggestion) {
99
+ return suggestion;
100
+ }
101
+ }
102
+
103
+ return undefined;
104
+ };
105
+
106
+ export { getSuggestedQuery, type Variant, type Method, type Suggestion };
package/src/traversal.ts CHANGED
@@ -1,49 +1,83 @@
1
- import * as Gtk from "@gtkx/ffi/gtk";
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
2
 
3
+ type QueryContainer = Gtk.Widget | Gtk.EventController | Gtk.LayoutManager | Gtk.ListItem | Gtk.ListHeader;
3
4
  /**
4
- * Root element for scoping queries.
5
- *
6
- * When a `Gtk.Application` is provided, queries search across all toplevel
7
- * windows. When a `Gtk.Widget` is provided, queries are scoped to that
8
- * widget's subtree.
5
+ * A scope that queries and traversal can run against: a widget, an event
6
+ * controller, a layout manager, a list item or header, an application, or the
7
+ * sentinel representing all current toplevel windows.
9
8
  */
10
- export type Container = Gtk.Application | Gtk.Widget;
9
+ type Container = QueryContainer | Gtk.Application | typeof TOPLEVELS;
11
10
 
12
- export const isApplication = (container: Container): container is Gtk.Application =>
13
- "getWindows" in container && typeof container.getWindows === "function";
11
+ const TOPLEVELS: unique symbol = Symbol("gtkx.toplevels");
12
+
13
+ const isApplication = (container: Container): container is Gtk.Application => container instanceof Gtk.Application;
14
14
 
15
15
  const traverseWidgetTree = function* (root: Gtk.Widget): Generator<Gtk.Widget> {
16
16
  yield root;
17
-
18
17
  let child = root.getFirstChild();
18
+
19
19
  while (child) {
20
20
  yield* traverseWidgetTree(child);
21
21
  child = child.getNextSibling();
22
22
  }
23
23
  };
24
24
 
25
- const traverseWindows = function* (): Generator<Gtk.Widget> {
26
- const windows = Gtk.Window.listToplevels();
27
- for (const window of windows) {
28
- yield* traverseWidgetTree(window);
25
+ const descendants = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
26
+ const tree = traverseWidgetTree(widget);
27
+ tree.next();
28
+ yield* tree;
29
+ };
30
+
31
+ const resolveRoot = (container: QueryContainer): Gtk.Widget | null => {
32
+ if (container instanceof Gtk.Widget) {
33
+ return container;
34
+ }
35
+
36
+ if (container instanceof Gtk.EventController) {
37
+ return container.getWidget();
38
+ }
39
+
40
+ if (container instanceof Gtk.LayoutManager) {
41
+ return container.getWidget();
42
+ }
43
+
44
+ if (container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader) {
45
+ return container.getChild();
46
+ }
47
+
48
+ return null;
49
+ };
50
+
51
+ const roots = function* (container: Container): Generator<Gtk.Widget> {
52
+ if (container === TOPLEVELS || isApplication(container)) {
53
+ yield* Gtk.Window.listToplevels();
54
+
55
+ return;
56
+ }
57
+
58
+ const root = resolveRoot(container);
59
+
60
+ if (root) {
61
+ yield root;
29
62
  }
30
63
  };
31
64
 
32
- export const traverse = function* (container: Container): Generator<Gtk.Widget> {
33
- if (isApplication(container)) {
34
- yield* traverseWindows();
35
- } else {
36
- yield* traverseWidgetTree(container);
65
+ const traverse = function* (container: Container): Generator<Gtk.Widget> {
66
+ for (const root of roots(container)) {
67
+ yield* traverseWidgetTree(root);
37
68
  }
38
69
  };
39
70
 
40
- export const findAll = (container: Container, predicate: (node: Gtk.Widget) => boolean): Gtk.Widget[] => {
71
+ const findAll = (container: Container, isMatch: (node: Gtk.Widget) => boolean): Gtk.Widget[] => {
41
72
  const results: Gtk.Widget[] = [];
73
+
42
74
  for (const node of traverse(container)) {
43
- if (node.getAccessibleRole?.() === Gtk.AccessibleRole.LABEL) continue;
44
- if (predicate(node)) {
75
+ if (isMatch(node)) {
45
76
  results.push(node);
46
77
  }
47
78
  }
79
+
48
80
  return results;
49
81
  };
82
+
83
+ export { TOPLEVELS, descendants, roots, traverse, findAll, type Container };
package/src/types.ts CHANGED
@@ -1,210 +1,182 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
2
- import type { ComponentType, ReactNode, Ref } from "react";
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { RootElement } from "@gtkx/react";
3
+ import type { ComponentType, ErrorInfo, ReactNode } from "react";
4
+ import type { PrettyWidgetOptions } from "./pretty-widget.js";
3
5
  import type { Container } from "./traversal.js";
4
6
 
5
7
  /**
6
- * Custom function for matching text content.
7
- *
8
- * @param content - The normalized text content to match against
9
- * @param widget - The widget being tested
10
- * @returns `true` if the content matches
8
+ * A custom matcher predicate: given a candidate widget's normalized text content and the widget
9
+ * itself, returns whether it matches.
11
10
  */
12
- export type TextMatchFunction = (content: string, widget: Gtk.Widget) => boolean;
11
+ type MatcherFunction = (content: string, widget: Gtk.Widget) => boolean;
12
+ /** A value used to match widget text: a string or number, a regular expression, or a custom {@link MatcherFunction}. */
13
+ type Matcher = string | number | RegExp | MatcherFunction;
14
+ /** Normalizes a widget's text before it is compared against a matcher. */
15
+ type NormalizerFn = (text: string) => string;
13
16
 
14
- /**
15
- * Text matching pattern.
16
- *
17
- * Can be a string for exact/substring matching, a RegExp for pattern matching,
18
- * or a custom function for advanced matching logic.
19
- */
20
- export type TextMatch = string | RegExp | TextMatchFunction;
21
-
22
- /**
23
- * Options for text normalization before matching.
24
- */
25
- export type NormalizerOptions = {
26
- /** Remove leading/trailing whitespace (default: true) */
27
- trim?: boolean;
28
- /** Replace multiple whitespace characters with single space (default: true) */
29
- collapseWhitespace?: boolean;
17
+ /** Options controlling the default text normalizer. */
18
+ type NormalizerOptions = {
19
+ /** Trim leading and trailing whitespace. */
20
+ trim?: boolean | undefined;
21
+ /** Collapse runs of whitespace into a single space. */
22
+ collapseWhitespace?: boolean | undefined;
30
23
  };
31
24
 
32
- /**
33
- * Options for text-based queries.
34
- */
35
- export type TextMatchOptions = {
36
- /** Require exact match vs substring match (default: true) */
37
- exact?: boolean;
38
- /** Custom text normalizer function */
39
- normalizer?: (text: string) => string;
40
- /** Remove leading/trailing whitespace (default: true) */
41
- trim?: boolean;
42
- /** Replace multiple whitespace with single space (default: true) */
43
- collapseWhitespace?: boolean;
44
- /** Timeout in milliseconds for async queries (default: 1000) */
45
- timeout?: number;
25
+ /** Options controlling how asynchronous queries and waits poll for a condition. */
26
+ type WaitForOptions = {
27
+ /** Maximum time in milliseconds to keep retrying before failing. */
28
+ timeout?: number | undefined;
29
+ /** Delay in milliseconds between retries. */
30
+ interval?: number | undefined;
31
+ /** Transforms the error thrown when the timeout elapses. */
32
+ onTimeout?: ((error: Error) => Error) | undefined;
33
+ /** Error whose stack trace attributes the timeout failure to the calling code. */
34
+ stackTraceError?: Error | undefined;
46
35
  };
47
36
 
48
- /**
49
- * Options for role-based queries.
50
- *
51
- * Extends text matching options with accessible state filters.
52
- */
53
- export type ByRoleOptions = TextMatchOptions & {
54
- /** Filter by accessible name/label */
55
- name?: TextMatch;
56
- /** Filter by checked state (checkboxes, radios, toggles) */
57
- checked?: boolean;
58
- /** Filter by pressed state */
59
- pressed?: boolean;
60
- /** Filter by selected state */
61
- selected?: boolean;
62
- /** Filter by expanded state (expanders) */
63
- expanded?: boolean;
64
- /** Filter by heading level */
65
- level?: number;
37
+ /** Options controlling text matching and, for asynchronous queries, polling behavior. */
38
+ type MatcherOptions = {
39
+ /** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
40
+ exact?: boolean | undefined;
41
+ /** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
42
+ normalizer?: NormalizerFn | undefined;
43
+ /** Forwarded to the default normalizer when no custom `normalizer` is given. */
44
+ trim?: boolean | undefined;
45
+ /** Forwarded to the default normalizer when no custom `normalizer` is given. */
46
+ collapseWhitespace?: boolean | undefined;
47
+ /** Whether to include a suggested better query in error messages. */
48
+ suggest?: boolean | undefined;
49
+ } & WaitForOptions;
50
+
51
+ /** Constraints on a widget's numeric range value used by role queries. */
52
+ type ByRoleValue = {
53
+ /** The current value. */
54
+ now?: number | undefined;
55
+ min?: number | undefined;
56
+ max?: number | undefined;
57
+ /** Matcher for the value's textual representation. */
58
+ text?: Matcher | undefined;
66
59
  };
67
60
 
68
- /**
69
- * Options for {@link waitFor} and {@link waitForElementToBeRemoved}.
70
- */
71
- export type WaitForOptions = {
72
- /** Maximum time to wait in milliseconds (default: 1000) */
73
- timeout?: number;
74
- /** Polling interval in milliseconds (default: 50) */
75
- interval?: number;
76
- /** Custom error handler called on timeout */
77
- onTimeout?: (error: Error) => Error;
61
+ /** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
62
+ type ByRoleOptions = MatcherOptions & {
63
+ name?: Matcher | undefined;
64
+ checked?: boolean | undefined;
65
+ pressed?: boolean | undefined;
66
+ selected?: boolean | undefined;
67
+ expanded?: boolean | undefined;
68
+ /** Heading or hierarchy level. */
69
+ level?: number | undefined;
70
+ busy?: boolean | undefined;
71
+ description?: Matcher | undefined;
72
+ value?: ByRoleValue | undefined;
73
+ /** When true, include widgets excluded from the accessibility tree. */
74
+ hidden?: boolean | undefined;
78
75
  };
79
76
 
80
- /**
81
- * A wrapper component that exposes its root GTK widget via `ref`.
82
- * Accept `ref` as a prop and pass it through to the root intrinsic element.
83
- */
84
- export type WrapperComponent = ComponentType<{
77
+ /** A React component that wraps rendered content, receiving it as its children. */
78
+ type WrapperComponent = ComponentType<{
85
79
  children: ReactNode;
86
- ref?: Ref<Gtk.Widget>;
87
80
  }>;
88
81
 
89
- /**
90
- * Options for {@link render}.
91
- */
92
- export type RenderOptions = {
93
- /**
94
- * Wrapper component or boolean.
95
- * - `true` (default): Wrap in GtkApplicationWindow
96
- * - `false`: No wrapper
97
- * - Component: Custom wrapper that passes `ref` to its root element
98
- */
99
- wrapper?: boolean | WrapperComponent;
100
- /**
101
- * The element queries are bound to.
102
- * Defaults to the GTK Application (searches all toplevel windows).
103
- * Provide a specific widget or application to scope queries.
104
- */
105
- baseElement?: Container;
106
- };
82
+ type Query = (container: Container, ...args: never[]) => unknown;
83
+ type QueryMap = Record<string, Query>;
84
+
85
+ type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
86
+ ? (...args: A) => R
87
+ : never;
88
+
89
+ type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
107
90
 
108
91
  /**
109
- * Query methods bound to a container.
110
- *
111
- * @see {@link screen} for global queries
112
- * @see {@link within} for scoped queries
92
+ * Options for {@link render}: the container and base element to mount into, an optional wrapper,
93
+ * React behavior toggles, error callbacks, and custom queries to bind.
113
94
  */
114
- export type BoundQueries = {
115
- /** Query single element by accessible role (returns null if not found) */
116
- queryByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
117
- /** Query single element by label/text content (returns null if not found) */
118
- queryByLabelText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
119
- /** Query single element by visible text (returns null if not found) */
120
- queryByText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
121
- /** Query single element by widget name (returns null if not found) */
122
- queryByName: (name: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
123
- /** Query all elements by accessible role (returns empty array if none found) */
124
- queryAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
125
- /** Query all elements by label/text content (returns empty array if none found) */
126
- queryAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
127
- /** Query all elements by visible text (returns empty array if none found) */
128
- queryAllByText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
129
- /** Query all elements by widget name (returns empty array if none found) */
130
- queryAllByName: (name: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
131
- /** Find single element by accessible role (waits and throws if not found) */
132
- findByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
133
- /** Find single element by label/text content (waits and throws if not found) */
134
- findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
135
- /** Find single element by visible text (waits and throws if not found) */
136
- findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
137
- /** Find single element by widget name (waits and throws if not found) */
138
- findByName: (name: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
139
- /** Find all elements by accessible role (waits and throws if none found) */
140
- findAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
141
- /** Find all elements by label/text content (waits and throws if none found) */
142
- findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
143
- /** Find all elements by visible text (waits and throws if none found) */
144
- findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
145
- /** Find all elements by widget name (waits and throws if none found) */
146
- findAllByName: (name: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
95
+ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
96
+ container?: Gtk.Widget | RootElement | undefined;
97
+ /** Root of the subtree that bound queries search. */
98
+ baseElement?: Container | undefined;
99
+ wrapper?: WrapperComponent | undefined;
100
+ /** Render inside React StrictMode. */
101
+ reactStrictMode?: boolean | undefined;
102
+ /** Enable widget animations during the test. */
103
+ animations?: boolean | undefined;
104
+ /** Called for errors caught by React error boundaries. */
105
+ onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
106
+ /** Called for errors React recovered from automatically. */
107
+ onRecoverableError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
108
+ /** Custom queries to bind to the rendered result. */
109
+ queries?: Q | undefined;
147
110
  };
148
111
 
149
- /**
150
- * Result returned by {@link render}.
151
- *
152
- * Provides query methods and utilities for testing rendered components.
153
- */
154
- export type RenderResult = BoundQueries & {
155
- /** The direct container widget wrapping the rendered content */
156
- container: Gtk.Widget;
157
- /** The element queries are bound to (defaults to the GTK Application) */
158
- baseElement: Container;
159
- /** Unmount the rendered component */
160
- unmount: () => Promise<void>;
161
- /** Re-render with a new element */
162
- rerender: (element: ReactNode) => Promise<void>;
163
- /** Print the widget tree to console for debugging */
164
- debug: () => void;
112
+ type DebugUtilities = {
113
+ debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
114
+ logRoles: () => void;
115
+ screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
165
116
  };
166
117
 
167
- /**
168
- * Result returned by {@link screenshot} and screen.screenshot.
169
- */
170
- export type ScreenshotResult = {
171
- /** Base64-encoded PNG image data */
118
+ /** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
119
+ type ScreenshotResult = {
172
120
  data: string;
173
- /** MIME type of the image (always "image/png") */
174
121
  mimeType: string;
175
- /** Width of the captured image in pixels */
176
122
  width: number;
177
- /** Height of the captured image in pixels */
178
123
  height: number;
179
124
  };
180
125
 
126
+ /** Options for capturing a screenshot: the poll timeout and interval, plus a rendering scale factor. */
127
+ type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval"> & {
128
+ /** Device scale factor applied when rendering. */
129
+ scale?: number;
130
+ };
131
+
181
132
  /**
182
- * Options for {@link renderHook}.
133
+ * Selects the window to screenshot by index, or by title (exact string or regular expression);
134
+ * undefined targets the default window.
183
135
  */
184
- export type RenderHookOptions<Props> = {
185
- /**
186
- * Initial props passed to the hook callback.
187
- */
188
- initialProps?: Props;
189
- /**
190
- * Wrapper component or boolean.
191
- * - `true` (default): Wrap in GtkApplicationWindow
192
- * - `false`: No wrapper
193
- * - Component: Custom wrapper that passes `ref` to its root element
194
- */
195
- wrapper?: boolean | WrapperComponent;
196
- };
136
+ type WindowSelector = number | string | RegExp | undefined;
137
+
138
+ /**
139
+ * Options for {@link renderHook}: an optional wrapper and the initial props (required unless the
140
+ * props type permits undefined).
141
+ */
142
+ type RenderHookOptions<Props> = {
143
+ wrapper?: WrapperComponent;
144
+ } & (undefined extends Props
145
+ ? {
146
+ initialProps?: Props;
147
+ }
148
+ : {
149
+ initialProps: Props;
150
+ });
197
151
 
198
152
  /**
199
- * Result returned by {@link renderHook}.
200
- *
201
- * Provides access to the hook result and utilities for re-rendering and cleanup.
153
+ * The result of {@link renderHook}: the latest hook return value plus functions to rerender with new
154
+ * props and to unmount.
202
155
  */
203
- export type RenderHookResult<Result, Props> = {
204
- /** Object containing the current hook return value */
156
+ type RenderHookResult<Result, Props> = {
157
+ /** Holds the most recent value returned by the hook under `current`. */
205
158
  result: { current: Result };
206
- /** Re-render the hook with optional new props */
207
159
  rerender: (newProps?: Props) => Promise<void>;
208
- /** Unmount the component containing the hook */
209
160
  unmount: () => Promise<void>;
210
161
  };
162
+
163
+ export {
164
+ type MatcherFunction,
165
+ type Matcher,
166
+ type NormalizerFn,
167
+ type NormalizerOptions,
168
+ type WaitForOptions,
169
+ type MatcherOptions,
170
+ type ByRoleValue,
171
+ type ByRoleOptions,
172
+ type WrapperComponent,
173
+ type QueryMap,
174
+ type BoundCustomQueries,
175
+ type RenderOptions,
176
+ type DebugUtilities,
177
+ type ScreenshotResult,
178
+ type ScreenshotOptions,
179
+ type WindowSelector,
180
+ type RenderHookOptions,
181
+ type RenderHookResult,
182
+ };
@@ -0,0 +1,62 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { wrapEvent } from "./event-wrapper.js";
3
+
4
+ /** A scroll distance in pixels along each axis. */
5
+ type ScrollDelta = {
6
+ x?: number;
7
+ y?: number;
8
+ };
9
+
10
+ type ScrollAdjustments = {
11
+ horizontal: Gtk.Adjustment | null;
12
+ vertical: Gtk.Adjustment | null;
13
+ };
14
+
15
+ const slide = (widget: Gtk.Widget, value: number): Promise<void> =>
16
+ wrapEvent(widget, () => {
17
+ if (!(widget instanceof Gtk.Range)) {
18
+ throw new TypeError(
19
+ `userEvent.slide requires a Gtk.Range (e.g. Gtk.Scale), got ${widget.constructor.name}`,
20
+ );
21
+ }
22
+
23
+ widget.emit("change-value", Gtk.ScrollType.JUMP, value);
24
+ });
25
+
26
+ const resolveScrollAdjustments = (widget: Gtk.Widget): ScrollAdjustments | null => {
27
+ for (let current: Gtk.Widget | null = widget; current; current = current.getParent()) {
28
+ if (current instanceof Gtk.ScrolledWindow) {
29
+ return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
30
+ }
31
+
32
+ if (current instanceof Gtk.Scrollable) {
33
+ return { horizontal: current.getHadjustment(), vertical: current.getVadjustment() };
34
+ }
35
+ }
36
+
37
+ return null;
38
+ };
39
+
40
+ const applyScrollDelta = (adjustment: Gtk.Adjustment | null, delta: number): void => {
41
+ if (!adjustment || delta === 0) {
42
+ return;
43
+ }
44
+
45
+ adjustment.setValue(adjustment.getValue() + delta);
46
+ };
47
+
48
+ const scroll = (widget: Gtk.Widget, delta: ScrollDelta): Promise<void> =>
49
+ wrapEvent(widget, () => {
50
+ const adjustments = resolveScrollAdjustments(widget);
51
+
52
+ if (!adjustments) {
53
+ throw new Error(
54
+ "userEvent.scroll: no Gtk.ScrolledWindow or Gtk.Scrollable found on the widget or its ancestors",
55
+ );
56
+ }
57
+
58
+ applyScrollDelta(adjustments.horizontal, delta.x ?? 0);
59
+ applyScrollDelta(adjustments.vertical, delta.y ?? 0);
60
+ });
61
+
62
+ export { slide, scroll, type ScrollDelta };