@gtkx/testing 1.0.0-rc.3 → 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 (103) 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/build-queries.d.ts.map +1 -1
  6. package/dist/build-queries.js +18 -7
  7. package/dist/build-queries.js.map +1 -1
  8. package/dist/config.d.ts +1 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +3 -3
  11. package/dist/config.js.map +1 -1
  12. package/dist/errors.js +2 -2
  13. package/dist/errors.js.map +1 -1
  14. package/dist/matchers.d.ts +44 -0
  15. package/dist/matchers.d.ts.map +1 -1
  16. package/dist/matchers.js.map +1 -1
  17. package/dist/pretty-widget.d.ts +2 -1
  18. package/dist/pretty-widget.d.ts.map +1 -1
  19. package/dist/pretty-widget.js +3 -3
  20. package/dist/pretty-widget.js.map +1 -1
  21. package/dist/queries.d.ts +1 -0
  22. package/dist/queries.d.ts.map +1 -1
  23. package/dist/queries.js.map +1 -1
  24. package/dist/render.js +2 -2
  25. package/dist/render.js.map +1 -1
  26. package/dist/suggestions.d.ts +3 -0
  27. package/dist/suggestions.d.ts.map +1 -1
  28. package/dist/suggestions.js.map +1 -1
  29. package/dist/traversal.d.ts +5 -0
  30. package/dist/traversal.d.ts.map +1 -1
  31. package/dist/traversal.js +1 -0
  32. package/dist/traversal.js.map +1 -1
  33. package/dist/types.d.ts +43 -5
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/user-event/adjustment.d.ts +13 -0
  37. package/dist/user-event/adjustment.d.ts.map +1 -1
  38. package/dist/user-event/adjustment.js +11 -0
  39. package/dist/user-event/adjustment.js.map +1 -1
  40. package/dist/user-event/click.d.ts +7 -0
  41. package/dist/user-event/click.d.ts.map +1 -1
  42. package/dist/user-event/click.js +7 -0
  43. package/dist/user-event/click.js.map +1 -1
  44. package/dist/user-event/controller.d.ts +29 -0
  45. package/dist/user-event/controller.d.ts.map +1 -1
  46. package/dist/user-event/controller.js +28 -0
  47. package/dist/user-event/controller.js.map +1 -1
  48. package/dist/user-event/gesture.d.ts +51 -0
  49. package/dist/user-event/gesture.d.ts.map +1 -1
  50. package/dist/user-event/gesture.js +47 -4
  51. package/dist/user-event/gesture.js.map +1 -1
  52. package/dist/user-event/index.d.ts +27 -0
  53. package/dist/user-event/index.d.ts.map +1 -1
  54. package/dist/user-event/index.js.map +1 -1
  55. package/dist/user-event/keyboard.d.ts +4 -2
  56. package/dist/user-event/keyboard.d.ts.map +1 -1
  57. package/dist/user-event/keyboard.js +13 -12
  58. package/dist/user-event/keyboard.js.map +1 -1
  59. package/dist/user-event/pointer.js +5 -5
  60. package/dist/user-event/pointer.js.map +1 -1
  61. package/dist/user-event/selection.d.ts +15 -0
  62. package/dist/user-event/selection.d.ts.map +1 -1
  63. package/dist/user-event/selection.js +15 -0
  64. package/dist/user-event/selection.js.map +1 -1
  65. package/dist/user-event/state.d.ts +1 -1
  66. package/dist/user-event/state.d.ts.map +1 -1
  67. package/dist/user-event/state.js +1 -1
  68. package/dist/user-event/state.js.map +1 -1
  69. package/dist/user-event/text.d.ts +30 -3
  70. package/dist/user-event/text.d.ts.map +1 -1
  71. package/dist/user-event/text.js +28 -1
  72. package/dist/user-event/text.js.map +1 -1
  73. package/dist/wait-for.d.ts +2 -0
  74. package/dist/wait-for.d.ts.map +1 -1
  75. package/dist/wait-for.js.map +1 -1
  76. package/dist/widget-accessible-properties.d.ts +17 -1
  77. package/dist/widget-accessible-properties.d.ts.map +1 -1
  78. package/dist/widget-accessible-properties.js +17 -0
  79. package/dist/widget-accessible-properties.js.map +1 -1
  80. package/package.json +7 -7
  81. package/src/act.ts +2 -0
  82. package/src/build-queries.ts +20 -8
  83. package/src/config.ts +4 -3
  84. package/src/errors.ts +2 -2
  85. package/src/matchers.ts +44 -0
  86. package/src/pretty-widget.ts +5 -4
  87. package/src/queries.ts +1 -0
  88. package/src/render.tsx +2 -2
  89. package/src/suggestions.ts +3 -0
  90. package/src/traversal.ts +5 -0
  91. package/src/types.ts +43 -5
  92. package/src/user-event/adjustment.ts +13 -0
  93. package/src/user-event/click.ts +7 -0
  94. package/src/user-event/controller.ts +29 -0
  95. package/src/user-event/gesture.ts +57 -6
  96. package/src/user-event/index.ts +27 -0
  97. package/src/user-event/keyboard.ts +18 -16
  98. package/src/user-event/pointer.ts +5 -5
  99. package/src/user-event/selection.ts +15 -0
  100. package/src/user-event/state.ts +2 -2
  101. package/src/user-event/text.ts +31 -4
  102. package/src/wait-for.ts +2 -0
  103. package/src/widget-accessible-properties.ts +17 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/testing",
3
- "version": "1.0.0-rc.3",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "Testing Library-style queries, user events, and screenshots for GTK4 widget trees rendered by GTKX",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -44,20 +44,20 @@
44
44
  "node": ">=24"
45
45
  },
46
46
  "dependencies": {
47
- "@gtkx/runtime": "1.0.0-rc.3",
48
- "@gtkx/react": "1.0.0-rc.3",
49
- "@gtkx/utils": "1.0.0-rc.3"
47
+ "@gtkx/react": "1.0.0-rc.4",
48
+ "@gtkx/runtime": "1.0.0-rc.4",
49
+ "@gtkx/utils": "1.0.0-rc.4"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@types/react": "^19.2",
53
53
  "react": "^19.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/react": "^19.2.17",
56
+ "@types/react": "^19.2.18",
57
57
  "@vitest/expect": "^4.1.10",
58
58
  "vitest": "^4.1.10",
59
- "@gtkx/config": "1.0.0-rc.3",
60
- "@gtkx/vitest": "1.0.0-rc.3"
59
+ "@gtkx/vitest": "1.0.0-rc.4",
60
+ "@gtkx/config": "1.0.0-rc.4"
61
61
  },
62
62
  "scripts": {
63
63
  "release": "tsx ../../scripts/release-package.ts"
package/src/act.ts CHANGED
@@ -4,7 +4,9 @@ declare global {
4
4
  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
5
5
  }
6
6
 
7
+ /** Work run inside the act environment, either synchronous or asynchronous. */
7
8
  type ActCallback<T> = () => T | PromiseLike<T>;
9
+ /** Runs a callback inside React's act environment and settles with its result once updates flush. */
8
10
  type ActImplementation = <T>(callback: ActCallback<T>) => PromiseLike<T>;
9
11
 
10
12
  const actImplementation: ActImplementation = reactAct;
@@ -39,7 +39,7 @@ const extractWaitForOptions = (args: unknown[]): WaitForOptions => {
39
39
  return {};
40
40
  };
41
41
 
42
- const extractSuggestOption = (args: unknown[]): boolean | undefined => {
42
+ const extractShouldSuggest = (args: unknown[]): boolean | undefined => {
43
43
  const last = args.at(-1);
44
44
 
45
45
  if (last && typeof last === "object" && !(last instanceof RegExp)) {
@@ -54,12 +54,11 @@ const maybeThrowSuggestion = (options: {
54
54
  match: Gtk.Widget;
55
55
  queryName: Method;
56
56
  variant: Variant;
57
- suggest: boolean | undefined;
57
+ shouldSuggest: boolean | undefined;
58
58
  }): void => {
59
- const { container, match, queryName, variant, suggest } = options;
60
- const shouldSuggest = suggest ?? getConfig().throwSuggestions;
59
+ const { container, match, queryName, variant, shouldSuggest } = options;
61
60
 
62
- if (!shouldSuggest) {
61
+ if (!(shouldSuggest ?? getConfig().throwSuggestions)) {
63
62
  return;
64
63
  }
65
64
 
@@ -141,7 +140,13 @@ const wrapSingleWithSuggestion =
141
140
  const match = query(container, ...args);
142
141
 
143
142
  if (match) {
144
- maybeThrowSuggestion({ container, match, queryName, variant, suggest: extractSuggestOption(args) });
143
+ maybeThrowSuggestion({
144
+ container,
145
+ match,
146
+ queryName,
147
+ variant,
148
+ shouldSuggest: extractShouldSuggest(args),
149
+ });
145
150
  }
146
151
 
147
152
  return match;
@@ -159,7 +164,7 @@ const wrapAllWithSuggestion =
159
164
  match: first,
160
165
  queryName,
161
166
  variant,
162
- suggest: extractSuggestOption(args),
167
+ shouldSuggest: extractShouldSuggest(args),
163
168
  });
164
169
  }
165
170
 
@@ -197,7 +202,14 @@ const buildQueries = <Args extends unknown[]>(
197
202
 
198
203
  const getByWithSuggestion: (container: Container, ...args: Args) => Gtk.Widget = (container, ...args) => {
199
204
  const match = getBy(container, ...args);
200
- maybeThrowSuggestion({ container, match, queryName, variant: "get", suggest: extractSuggestOption(args) });
205
+
206
+ maybeThrowSuggestion({
207
+ container,
208
+ match,
209
+ queryName,
210
+ variant: "get",
211
+ shouldSuggest: extractShouldSuggest(args),
212
+ });
201
213
 
202
214
  return match;
203
215
  };
package/src/config.ts CHANGED
@@ -6,6 +6,7 @@ import type { Container } from "./traversal.js";
6
6
  * checks.
7
7
  */
8
8
  type Config = {
9
+ /** Whether a successful query throws instead, naming a better query, when one is available. */
9
10
  throwSuggestions: boolean;
10
11
 
11
12
  /** Builds the error thrown when a query fails, given a message and optional container. */
@@ -34,7 +35,7 @@ const defaultConfig: Config = {
34
35
  const currentConfig: Config = { ...defaultConfig };
35
36
 
36
37
  function defaultGetElementError(message: string): Error {
37
- return new GtkxElementError(message);
38
+ return new ElementError(message);
38
39
  }
39
40
 
40
41
  /**
@@ -55,8 +56,8 @@ const configure = (newConfig: Partial<Config> | ConfigFn): void => {
55
56
  Object.assign(currentConfig, updates);
56
57
  };
57
58
 
58
- class GtkxElementError extends Error {
59
- override name = "GtkxElementError";
59
+ class ElementError extends Error {
60
+ override name = "ElementError";
60
61
  }
61
62
 
62
63
  export { getConfig, configure, type Config, type ConfigFn };
package/src/errors.ts CHANGED
@@ -125,7 +125,7 @@ const buildElementError = (container: Container, headLines: string[]): Error =>
125
125
 
126
126
  const lines = expensiveErrorDiagnostics.isDisabled
127
127
  ? headLines
128
- : [...headLines, "", prettyWidget(container, { highlight: false })];
128
+ : [...headLines, "", prettyWidget(container, { shouldHighlight: false })];
129
129
 
130
130
  return config.getElementError(lines.join("\n"), container);
131
131
  };
@@ -160,7 +160,7 @@ const multipleFoundError = (container: Container, descriptor: QueryDescriptor, m
160
160
  ];
161
161
 
162
162
  if (!expensiveErrorDiagnostics.isDisabled) {
163
- const renderedMatches = matches.map((widget) => prettyWidget(widget, { highlight: false }));
163
+ const renderedMatches = matches.map((widget) => prettyWidget(widget, { shouldHighlight: false }));
164
164
  headLines.push("", "Here are the matching elements:", "", ...renderedMatches);
165
165
  }
166
166
 
package/src/matchers.ts CHANGED
@@ -39,45 +39,89 @@ type TextContentOptions = {
39
39
 
40
40
  /** The expected value for a style class: an exact class name or a regular expression. */
41
41
  type ClassExpectation = string | RegExp;
42
+ /** The outcome of a matcher: whether it passed, and the failure text built on demand. */
42
43
  type MatcherResult = { pass: boolean; message: () => string };
44
+ /** The matcher state bound as `this`, supplying the test runner's deep equality check. */
43
45
  type MatcherContext = { equals: (actual: unknown, expected: unknown) => boolean };
46
+ /** Compares text read from the received widget; with no expected value, asserts the text is non-empty. */
44
47
  type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
48
+ /** Asserts a single state of the received widget, taking no expected value. */
45
49
  type StateMatcher = (received: unknown) => MatcherResult;
46
50
  type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
47
51
  type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
48
52
 
53
+ /** A text matcher that also takes normalization options for the text it reads. */
49
54
  type TextContentMatcher = (
50
55
  received: unknown,
51
56
  expected?: TextExpectation,
52
57
  options?: TextContentOptions,
53
58
  ) => MatcherResult;
54
59
 
60
+ /** Signatures of the widget assertion matchers, keyed by the name each is registered under. */
55
61
  type MatcherImplementations = {
62
+ /** Asserts the text an editable widget or combo box shows; with no argument, that it is not empty. */
56
63
  toHaveDisplayValue: TextMatcher;
64
+ /** Asserts the widget's own text, or its descendants' when it has none, contains or matches the expectation. */
57
65
  toHaveTextContent: TextContentMatcher;
66
+ /** Asserts the widget's accessible name; with no argument, that it has one. */
58
67
  toHaveAccessibleName: TextMatcher;
68
+ /**
69
+ * Asserts the joined accessible names of the widget's `described-by` targets, falling back to its
70
+ * `description` and then to its tooltip.
71
+ */
59
72
  toHaveAccessibleDescription: TextMatcher;
73
+ /** Asserts the joined accessible names of the widget's `error-message` targets, read only while it is invalid. */
60
74
  toHaveAccessibleErrorMessage: TextMatcher;
75
+ /** Asserts the widget's placeholder text; with no argument, that it has some. */
61
76
  toHavePlaceholderText: TextMatcher;
77
+ /** Asserts the text currently selected in an editable widget. */
62
78
  toHaveSelection: TextMatcher;
79
+ /** Asserts the widget's checked state is checked rather than unchecked or mixed. */
63
80
  toBeChecked: StateMatcher;
81
+ /** Asserts the widget's checked state is mixed. */
64
82
  toBePartiallyChecked: StateMatcher;
83
+ /** Asserts a `Gtk.ToggleButton` is active, and throws for any other widget. */
65
84
  toBePressed: StateMatcher;
85
+ /** Asserts a `Gtk.Expander` or the row behind a `Gtk.TreeExpander` is expanded, and throws for anything else. */
66
86
  toBeExpanded: StateMatcher;
87
+ /**
88
+ * Asserts a `Gtk.ListBoxRow` or `Gtk.FlowBoxChild` is selected, or that a widget in a selectable role
89
+ * carries the selected state flag, and throws for anything else.
90
+ */
67
91
  toBeSelected: StateMatcher;
92
+ /** Asserts the widget is insensitive, or sits inside an insensitive ancestor. */
68
93
  toBeDisabled: StateMatcher;
94
+ /** Asserts the widget and all of its ancestors are sensitive. */
69
95
  toBeEnabled: StateMatcher;
96
+ /** Asserts the widget and its ancestors are all visible, and that none of them is fully transparent. */
70
97
  toBeVisible: StateMatcher;
98
+ /** Asserts the widget's root is a window that is still in the toplevel list. */
71
99
  toBeRooted: StateMatcher;
100
+ /** Asserts the widget has neither a child nor label text. */
72
101
  toBeEmpty: StateMatcher;
102
+ /** Asserts the widget's accessible invalid state is set to anything other than false. */
73
103
  toBeInvalid: StateMatcher;
104
+ /** Asserts the widget's accessible invalid state is unset or false. */
74
105
  toBeValid: StateMatcher;
106
+ /** Asserts the widget's accessible required state. */
75
107
  toBeRequired: StateMatcher;
108
+ /** Asserts the widget holds the platform focus state. */
76
109
  toHaveFocus: StateMatcher;
110
+ /** Asserts a widget's numeric value, or its display value when a string is given. */
77
111
  toHaveValue: (received: unknown, expected?: number | string) => MatcherResult;
112
+ /** Asserts the widget's accessible role. */
78
113
  toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
114
+ /** Asserts the widget is, or is an ancestor of, the given widget. */
79
115
  toContainElement: (received: unknown, descendant: Gtk.Widget | null) => MatcherResult;
116
+ /**
117
+ * Asserts the widget carries every given style class, each a name, a space-separated list, or a pattern.
118
+ * Pass `{ exact: true }` last to require exactly that set, which rules out patterns.
119
+ */
80
120
  toHaveClass: (received: unknown, ...args: unknown[]) => MatcherResult;
121
+ /**
122
+ * Asserts a GObject property, named as in GObject and looked up by its camel-cased accessor, equals the
123
+ * expected value. With no expected value, asserts the property is set. GObject values compare by identity.
124
+ */
81
125
  toHaveObjectProperty: (this: MatcherContext, received: unknown, ...args: unknown[]) => MatcherResult;
82
126
  };
83
127
 
@@ -4,6 +4,7 @@ import { formatRole } from "./role-helpers.js";
4
4
  import { type Container, roots } from "./traversal.js";
5
5
  import { getWidgetNodeText } from "./widget-accessible-properties.js";
6
6
 
7
+ /** Produces the value of the `id` attribute printed first on a widget's opening tag. */
7
8
  type WidgetIdResolver = (widget: Gtk.Widget) => string;
8
9
 
9
10
  /**
@@ -14,7 +15,7 @@ type PrettyWidgetOptions = {
14
15
  /** Truncates the output once it exceeds this many characters. */
15
16
  maxLength?: number;
16
17
  /** Whether to apply ANSI color highlighting; defaults to the terminal capabilities. */
17
- highlight?: boolean;
18
+ shouldHighlight?: boolean;
18
19
  /** Resolves an `id` attribute to show for each widget. */
19
20
  getId?: WidgetIdResolver;
20
21
  /** Stops descending past this depth, replacing deeper children with a summary line. */
@@ -76,7 +77,7 @@ const buildAttrs = (widget: Gtk.Widget, getId: WidgetIdResolver | undefined): [s
76
77
  return [...idAttrs, ...otherAttrs];
77
78
  };
78
79
 
79
- const shouldHighlight = (): boolean => {
80
+ const isHighlightSupported = (): boolean => {
80
81
  if (typeof process === "undefined") {
81
82
  return false;
82
83
  }
@@ -206,8 +207,8 @@ const prettyWidget = (container: Container, options: PrettyWidgetOptions = {}):
206
207
  return "";
207
208
  }
208
209
 
209
- const isHighlight = options.highlight ?? shouldHighlight();
210
- const colors = createColors(isHighlight);
210
+ const shouldHighlight = options.shouldHighlight ?? isHighlightSupported();
211
+ const colors = createColors(shouldHighlight);
211
212
  let output = "";
212
213
 
213
214
  for (const root of roots(container)) {
package/src/queries.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  isWidgetChecked,
31
31
  } from "./widget-accessible-properties.js";
32
32
 
33
+ /** The built-in queries in their unbound form, each taking the container to search as its first argument. */
33
34
  type BuiltinQueries = QueryFamilies<[container: Container]>;
34
35
 
35
36
  const roleQueries = nameQueryFamily(
package/src/render.tsx CHANGED
@@ -165,7 +165,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
165
165
  options?: RenderOptions<Q>,
166
166
  ): Promise<RenderResult<Q>> => {
167
167
  installErrorHandler();
168
- applyEnableAnimations(options?.animations === true);
168
+ applyEnableAnimations(options?.areAnimationsEnabled === true);
169
169
  const baseElement: Container = options?.baseElement ?? TOPLEVELS;
170
170
  const Wrapper = options?.wrapper;
171
171
  const resolved = resolveContainer(options?.container);
@@ -184,7 +184,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
184
184
  const wrap = (node: ReactNode): ReactNode => {
185
185
  const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
186
186
 
187
- return options?.reactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
187
+ return options?.isReactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
188
188
  };
189
189
 
190
190
  await update(wrap(element), root);
@@ -20,10 +20,13 @@ type Method = "Role" | "LabelText" | "PlaceholderText" | "Text" | "DisplayValue"
20
20
  * `toString` that renders the full call.
21
21
  */
22
22
  type Suggestion = {
23
+ /** The query family, such as `Role`. */
23
24
  queryName: Method;
24
25
  /** The full query function name, such as `getByRole`. */
25
26
  queryMethod: string;
27
+ /** The variant the suggestion was asked for, which prefixes `Suggestion.queryMethod`. */
26
28
  variant: Variant;
29
+ /** Renders the suggested call with its arguments, such as `getByRole(Gtk.AccessibleRole.BUTTON)`. */
27
30
  toString: () => string;
28
31
  };
29
32
 
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,6 +12,7 @@ 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;
package/src/types.ts CHANGED
@@ -59,7 +59,9 @@ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
59
59
  type ByRoleValue = {
60
60
  /** The current value. */
61
61
  now?: number | undefined;
62
+ /** The lower bound of the value's range. */
62
63
  min?: number | undefined;
64
+ /** The upper bound of the value's range. */
63
65
  max?: number | undefined;
64
66
  /** Matcher for the value's textual representation. */
65
67
  text?: Matcher | undefined;
@@ -67,15 +69,23 @@ type ByRoleValue = {
67
69
 
68
70
  /** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
69
71
  type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
72
+ /** Matcher for the widget's accessible name. */
70
73
  name?: Matcher | undefined;
74
+ /** Required checked state; a mixed check button reads as neither, so it matches neither value. */
71
75
  checked?: boolean | undefined;
76
+ /** Required active state of a toggle button; any other widget matches neither value. */
72
77
  pressed?: boolean | undefined;
78
+ /** Required selected state of a row, list item, grid cell, option or tree item. */
73
79
  selected?: boolean | undefined;
80
+ /** Required expanded state of an expander or tree expander. */
74
81
  expanded?: boolean | undefined;
75
82
  /** Heading or hierarchy level. */
76
83
  level?: number | undefined;
84
+ /** Required busy state, which an unset state satisfies as false. */
77
85
  busy?: boolean | undefined;
86
+ /** Matcher for the widget's own accessible description, ignoring its `described-by` targets. */
78
87
  description?: Matcher | undefined;
88
+ /** Constraints on the widget's range value, each checked only when given. */
79
89
  value?: ByRoleValue | undefined;
80
90
  /** When true, include widgets excluded from the accessibility tree. */
81
91
  hidden?: boolean | undefined;
@@ -86,26 +96,39 @@ type WrapperComponent = ComponentType<{
86
96
  children: ReactNode;
87
97
  }>;
88
98
 
99
+ /** A custom query, taking the scope to search as its first argument and its matcher arguments after it. */
89
100
  type Query = (container: Container, ...args: never[]) => unknown;
101
+ /** Custom queries keyed by the name each is bound under. */
90
102
  type QueryMap = Record<string, Query>;
91
103
 
104
+ /** A query with its container argument already applied. */
92
105
  type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: infer A) => infer R
93
106
  ? (...args: A) => R
94
107
  : never;
95
108
 
109
+ /** Custom queries with their container argument already applied, keyed as they were passed in. */
96
110
  type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
97
111
 
112
+ /** What each variant of a query family yields for the widget type it matched. */
98
113
  type QueryFamilyReturns<T extends Gtk.Widget> = {
114
+ /** The single match, or null when nothing matched; throws when more than one matched. */
99
115
  queryBy: T | null;
116
+ /** Every match, empty when nothing matched. */
100
117
  queryAllBy: T[];
118
+ /** The single match; throws when nothing or more than one matched. */
101
119
  getBy: T;
120
+ /** Every match; throws when nothing matched. */
102
121
  getAllBy: T[];
122
+ /** The single match, retried until it appears or the timeout elapses. */
103
123
  findBy: Promise<T>;
124
+ /** Every match, retried until at least one appears or the timeout elapses. */
104
125
  findAllBy: Promise<T[]>;
105
126
  };
106
127
 
128
+ /** What a query family matches against, which fixes the arguments it takes. */
107
129
  type QueryKind = "role" | "text" | "name" | "value";
108
130
 
131
+ /** The arguments a query of the given kind takes after the family's leading ones. */
109
132
  type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
110
133
  ? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
111
134
  : Kind extends "name"
@@ -115,9 +138,8 @@ type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "rol
115
138
  : [text: Matcher, options?: MatcherOptions<T>];
116
139
 
117
140
  /**
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.
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.
121
143
  */
122
144
  type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
123
145
  [K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
@@ -125,6 +147,10 @@ type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unk
125
147
  ) => QueryFamilyReturns<T>[K];
126
148
  };
127
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
+ */
128
154
  type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
129
155
  QueryFamily<"LabelText", "text", Head> &
130
156
  QueryFamily<"Text", "text", Head> &
@@ -137,14 +163,16 @@ type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
137
163
  * React behavior toggles, error callbacks, and custom queries to bind.
138
164
  */
139
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. */
140
167
  container?: Gtk.Widget | RootElement | undefined;
141
168
  /** Root of the subtree that bound queries search. */
142
169
  baseElement?: Container | undefined;
170
+ /** Component wrapped around the rendered element, such as a context provider. */
143
171
  wrapper?: WrapperComponent | undefined;
144
172
  /** Render inside React StrictMode. */
145
- reactStrictMode?: boolean | undefined;
173
+ isReactStrictMode?: boolean | undefined;
146
174
  /** Enable widget animations during the test. */
147
- animations?: boolean | undefined;
175
+ areAnimationsEnabled?: boolean | undefined;
148
176
  /** Called for errors caught by React error boundaries. */
149
177
  onCaughtError?: ((error: unknown, errorInfo: ErrorInfo) => void) | undefined;
150
178
  /** Called for errors React recovered from automatically. */
@@ -153,17 +181,25 @@ type RenderOptions<Q extends QueryMap = Record<never, never>> = {
153
181
  queries?: Q | undefined;
154
182
  };
155
183
 
184
+ /** Console helpers bundled with the bound queries on {@link screen} and on a render result. */
156
185
  type DebugUtilities = {
186
+ /** Prints the widget tree of the given containers, or of the whole scope, to the console. */
157
187
  debug: (element?: Container | Container[], options?: PrettyWidgetOptions) => void;
188
+ /** Prints the accessible roles found in the scope, with the widgets carrying each one. */
158
189
  logRoles: () => void;
190
+ /** Captures a window, writes the image to a temporary file, and logs its path. */
159
191
  screenshot: (selector?: WindowSelector, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
160
192
  };
161
193
 
162
194
  /** A captured screenshot: base64-encoded image data, its MIME type, and pixel dimensions. */
163
195
  type ScreenshotResult = {
196
+ /** Base64-encoded image bytes. */
164
197
  data: string;
198
+ /** MIME type of the encoded image, always `image/png`. */
165
199
  mimeType: string;
200
+ /** Image width in pixels, after the scale factor is applied. */
166
201
  width: number;
202
+ /** Image height in pixels, after the scale factor is applied. */
167
203
  height: number;
168
204
  };
169
205
 
@@ -200,7 +236,9 @@ type RenderHookOptions<Props> = {
200
236
  type RenderHookResult<Result, Props> = {
201
237
  /** Holds the most recent value returned by the hook under `current`. */
202
238
  result: { current: Result };
239
+ /** Re-invokes the hook, keeping the previous props when none are given. */
203
240
  rerender: (newProps?: Props) => Promise<void>;
241
+ /** Unmounts the component that calls the hook. */
204
242
  unmount: () => Promise<void>;
205
243
  };
206
244
 
@@ -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,13 @@ 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
+ */
42
71
  const getController = <T extends Gtk.EventController>(
43
72
  widget: Gtk.Widget,
44
73
  controllerType: ControllerConstructor<T>,