@gtkx/testing 0.21.0 → 1.0.0-rc.1

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 +48 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +32 -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 +103 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +4 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +11 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +18 -51
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +9 -40
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +7 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +24 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +29 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +119 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +7 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +9 -29
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +3 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +30 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +16 -11
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +10 -6
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +55 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +103 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +21 -25
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +69 -115
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +130 -177
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +212 -325
  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 +15 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +14 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +8 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +11 -41
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +129 -118
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +16 -51
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +39 -58
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +6 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +21 -109
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +20 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +89 -48
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +22 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +56 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +10 -8
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +28 -13
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +103 -172
  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 +8 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +33 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +7 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +65 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +7 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +28 -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 +10 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +3 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +48 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +31 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +105 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +44 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +38 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +9 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +173 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +6 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +27 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +4 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +106 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +6 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +2 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +17 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +107 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +13 -33
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +62 -73
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +33 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +215 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +9 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +15 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +21 -12
  150. package/src/act.ts +68 -0
  151. package/src/bound-queries.ts +36 -0
  152. package/src/build-queries.ts +185 -0
  153. package/src/cleanup-registry.ts +14 -0
  154. package/src/config.ts +28 -59
  155. package/src/editable.ts +30 -0
  156. package/src/errors.ts +130 -0
  157. package/src/fire-event.ts +11 -38
  158. package/src/frame-sync.ts +34 -0
  159. package/src/index.ts +56 -14
  160. package/src/matchers.ts +200 -0
  161. package/src/pretty-widget.ts +99 -138
  162. package/src/queries.ts +320 -420
  163. package/src/register-test-hooks.ts +16 -0
  164. package/src/render-hook.tsx +31 -44
  165. package/src/render.tsx +161 -149
  166. package/src/role-helpers.ts +47 -67
  167. package/src/screen.ts +21 -114
  168. package/src/screenshot.ts +104 -55
  169. package/src/suggestions.ts +87 -0
  170. package/src/traversal.ts +32 -18
  171. package/src/types.ts +115 -176
  172. package/src/user-event/adjustment.ts +49 -0
  173. package/src/user-event/click.ts +70 -0
  174. package/src/user-event/controller.ts +44 -0
  175. package/src/user-event/dispatch.ts +23 -0
  176. package/src/user-event/event-wrapper.ts +54 -0
  177. package/src/user-event/gesture.ts +154 -0
  178. package/src/user-event/index.ts +74 -0
  179. package/src/user-event/keyboard.ts +204 -0
  180. package/src/user-event/pointer.ts +34 -0
  181. package/src/user-event/selection.ts +122 -0
  182. package/src/user-event/state.ts +6 -0
  183. package/src/user-event/text.ts +137 -0
  184. package/src/wait-for.ts +79 -81
  185. package/src/widget-accessible-properties.ts +244 -0
  186. package/src/within.ts +26 -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,185 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { getConfig } from "./config.js";
3
+ import { runWithExpensiveErrorDiagnosticsDisabled, suggestionError } from "./errors.js";
4
+ import { getSuggestedQuery, type Method, type Variant } from "./suggestions.js";
5
+ import type { Container } from "./traversal.js";
6
+ import type { MatcherOptions, WaitForOptions } from "./types.js";
7
+ import { waitFor } from "./wait-for.js";
8
+
9
+ export type QueryAllBy<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
10
+
11
+ type MultipleErrorBuilder<Args extends unknown[]> = (
12
+ container: Container,
13
+ matches: Gtk.Widget[],
14
+ ...args: Args
15
+ ) => Error;
16
+ type MissingErrorBuilder<Args extends unknown[]> = (container: Container, ...args: Args) => Error;
17
+
18
+ export type BuiltQueries<Args extends unknown[]> = {
19
+ queryBy: (container: Container, ...args: Args) => Gtk.Widget | null;
20
+ getAllBy: QueryAllBy<Args>;
21
+ getBy: (container: Container, ...args: Args) => Gtk.Widget;
22
+ findAllBy: (container: Container, ...args: Args) => Promise<Gtk.Widget[]>;
23
+ findBy: (container: Container, ...args: Args) => Promise<Gtk.Widget>;
24
+ };
25
+
26
+ const extractWaitForOptions = (args: unknown[]): WaitForOptions => {
27
+ const last = args[args.length - 1];
28
+ if (last && typeof last === "object" && !(last instanceof RegExp)) {
29
+ const { timeout, interval, onTimeout } = last as WaitForOptions;
30
+ return { timeout, interval, onTimeout };
31
+ }
32
+ return {};
33
+ };
34
+
35
+ const extractSuggestOption = (args: unknown[]): boolean | undefined => {
36
+ const last = args[args.length - 1];
37
+ if (last && typeof last === "object" && !(last instanceof RegExp)) {
38
+ return (last as MatcherOptions).suggest;
39
+ }
40
+ return undefined;
41
+ };
42
+
43
+ const maybeThrowSuggestion = (options: {
44
+ container: Container;
45
+ match: Gtk.Widget;
46
+ queryName: Method;
47
+ variant: Variant;
48
+ suggest: boolean | undefined;
49
+ }): void => {
50
+ const { container, match, queryName, variant, suggest } = options;
51
+ const shouldSuggest = suggest ?? getConfig().throwSuggestions;
52
+ if (!shouldSuggest) return;
53
+
54
+ const suggestion = getSuggestedQuery(match, variant);
55
+ if (suggestion && suggestion.queryName !== queryName) {
56
+ throw suggestionError(suggestion.toString(), container);
57
+ }
58
+ };
59
+
60
+ type FindQuery<Args extends unknown[]> = (container: Container, ...args: Args) => unknown;
61
+
62
+ const reRunForDiagnostics = <Args extends unknown[]>(
63
+ query: FindQuery<Args>,
64
+ container: Container,
65
+ args: Args,
66
+ fallback: Error,
67
+ ): Error => {
68
+ try {
69
+ query(container, ...args);
70
+ } catch (error) {
71
+ if (error instanceof Error) return error;
72
+ }
73
+ return fallback;
74
+ };
75
+
76
+ const findOptions = <Args extends unknown[]>(
77
+ query: FindQuery<Args>,
78
+ container: Container,
79
+ args: Args,
80
+ ): WaitForOptions => {
81
+ const userOptions = extractWaitForOptions(args);
82
+ const onTimeout =
83
+ userOptions.onTimeout ?? ((fallback: Error) => reRunForDiagnostics(query, container, args, fallback));
84
+ return {
85
+ stackTraceError: new Error("STACK_TRACE_MESSAGE"),
86
+ onTimeout,
87
+ timeout: userOptions.timeout,
88
+ interval: userOptions.interval,
89
+ };
90
+ };
91
+
92
+ type SingleQuery<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget | null;
93
+
94
+ const singleFrom =
95
+ <Args extends unknown[]>(
96
+ allQuery: QueryAllBy<Args>,
97
+ getMultipleError: MultipleErrorBuilder<Args>,
98
+ ): SingleQuery<Args> =>
99
+ (container, ...args) => {
100
+ const matches = allQuery(container, ...args);
101
+ if (matches.length > 1) throw getMultipleError(container, matches, ...args);
102
+ return matches[0] ?? null;
103
+ };
104
+
105
+ const allOrThrow =
106
+ <Args extends unknown[]>(
107
+ allQuery: QueryAllBy<Args>,
108
+ getMissingError: MissingErrorBuilder<Args>,
109
+ ): QueryAllBy<Args> =>
110
+ (container, ...args) => {
111
+ const matches = allQuery(container, ...args);
112
+ if (matches.length === 0) throw getMissingError(container, ...args);
113
+ return matches;
114
+ };
115
+
116
+ const wrapSingleWithSuggestion =
117
+ <Args extends unknown[]>(query: SingleQuery<Args>, queryName: Method, variant: Variant): SingleQuery<Args> =>
118
+ (container, ...args) => {
119
+ const match = query(container, ...args);
120
+ if (match) {
121
+ maybeThrowSuggestion({ container, match, queryName, variant, suggest: extractSuggestOption(args) });
122
+ }
123
+ return match;
124
+ };
125
+
126
+ const wrapAllWithSuggestion =
127
+ <Args extends unknown[]>(query: QueryAllBy<Args>, queryName: Method, variant: Variant): QueryAllBy<Args> =>
128
+ (container, ...args) => {
129
+ const matches = query(container, ...args);
130
+ const [first] = matches;
131
+ if (first !== undefined) {
132
+ maybeThrowSuggestion({ container, match: first, queryName, variant, suggest: extractSuggestOption(args) });
133
+ }
134
+ return matches;
135
+ };
136
+
137
+ const requireSingle =
138
+ <Args extends unknown[]>(query: SingleQuery<Args>, getMissingError: MissingErrorBuilder<Args>) =>
139
+ (container: Container, ...args: Args): Gtk.Widget => {
140
+ const match = query(container, ...args);
141
+ if (match === null) throw getMissingError(container, ...args);
142
+ return match;
143
+ };
144
+
145
+ export const buildQueries = <Args extends unknown[]>(
146
+ queryName: Method,
147
+ queryAllBy: QueryAllBy<Args>,
148
+ getMultipleError: MultipleErrorBuilder<Args>,
149
+ getMissingError: MissingErrorBuilder<Args>,
150
+ ): BuiltQueries<Args> => {
151
+ const getAllBy = allOrThrow(queryAllBy, getMissingError);
152
+ const getBy = requireSingle(singleFrom(getAllBy, getMultipleError), getMissingError);
153
+
154
+ const queryByWithSuggestion = wrapSingleWithSuggestion(
155
+ singleFrom(queryAllBy, getMultipleError),
156
+ queryName,
157
+ "query",
158
+ );
159
+ const getAllByWithSuggestion = wrapAllWithSuggestion(getAllBy, queryName, "getAll");
160
+ const getByWithSuggestion: (container: Container, ...args: Args) => Gtk.Widget = (container, ...args) => {
161
+ const match = getBy(container, ...args);
162
+ maybeThrowSuggestion({ container, match, queryName, variant: "get", suggest: extractSuggestOption(args) });
163
+ return match;
164
+ };
165
+
166
+ const findAllBy = (container: Container, ...args: Args): Promise<Gtk.Widget[]> =>
167
+ waitFor(
168
+ () => runWithExpensiveErrorDiagnosticsDisabled(() => getAllByWithSuggestion(container, ...args)),
169
+ findOptions(getAllByWithSuggestion, container, args),
170
+ );
171
+
172
+ const findBy = (container: Container, ...args: Args): Promise<Gtk.Widget> =>
173
+ waitFor(
174
+ () => runWithExpensiveErrorDiagnosticsDisabled(() => getByWithSuggestion(container, ...args)),
175
+ findOptions(getByWithSuggestion, container, args),
176
+ );
177
+
178
+ return {
179
+ queryBy: queryByWithSuggestion,
180
+ getAllBy: getAllByWithSuggestion,
181
+ getBy: getByWithSuggestion,
182
+ findAllBy,
183
+ findBy,
184
+ };
185
+ };
@@ -0,0 +1,14 @@
1
+ export type CleanupFunction = () => void | Promise<void>;
2
+
3
+ const cleanupQueue = new Set<CleanupFunction>();
4
+
5
+ export const addToCleanupQueue = (fn: CleanupFunction): void => {
6
+ cleanupQueue.add(fn);
7
+ };
8
+
9
+ export const runCleanup = async (): Promise<void> => {
10
+ for (const fn of cleanupQueue) {
11
+ await fn();
12
+ }
13
+ cleanupQueue.clear();
14
+ };
package/src/config.ts CHANGED
@@ -1,89 +1,58 @@
1
1
  import type { Container } from "./traversal.js";
2
2
 
3
3
  /**
4
- * Configuration options for the testing library.
4
+ * Global configuration for the testing harness, controlling query behavior,
5
+ * error formatting, and the timeouts used by async utilities and actionability
6
+ * checks.
5
7
  */
6
8
  export type Config = {
7
- /**
8
- * Whether to show role suggestions in error messages when elements are not found.
9
- * @default true
10
- */
11
- showSuggestions: boolean;
9
+ throwSuggestions: boolean;
12
10
 
13
- /**
14
- * Custom error factory for query failures.
15
- * Allows customizing how errors are constructed.
16
- */
17
- getElementError: (message: string, container: Container) => Error;
11
+ /** Builds the error thrown when a query fails, given a message and optional container. */
12
+ getElementError: (message: string, container?: Container) => Error;
18
13
 
19
- /**
20
- * Default timeout in milliseconds for async utilities (waitFor, findBy* queries).
21
- * @default 1000
22
- */
14
+ /** Default timeout in milliseconds for async utilities such as waitFor. */
23
15
  asyncUtilTimeout: number;
16
+
17
+ /** Timeout in milliseconds for waiting on a widget to become actionable. */
18
+ actionabilityTimeout: number;
24
19
  };
25
20
 
26
- const defaultGetElementError = (message: string, _container: Container): Error => {
27
- return new Error(message);
21
+ /**
22
+ * Function form of a configuration update: receives the current config and
23
+ * returns the fields to override.
24
+ */
25
+ export type ConfigFn = (existingConfig: Config) => Partial<Config>;
26
+
27
+ const defaultGetElementError = (message: string, _container?: Container): Error => {
28
+ const error = new Error(message);
29
+ error.name = "GtkxElementError";
30
+ return error;
28
31
  };
29
32
 
30
33
  const defaultConfig: Config = {
31
- showSuggestions: true,
34
+ throwSuggestions: false,
32
35
  getElementError: defaultGetElementError,
33
36
  asyncUtilTimeout: 1000,
37
+ actionabilityTimeout: 500,
34
38
  };
35
39
 
36
40
  let currentConfig: Config = { ...defaultConfig };
37
41
 
38
42
  /**
39
- * Returns the current testing library configuration.
40
- *
41
- * @returns The current configuration object
42
- *
43
- * @example
44
- * ```tsx
45
- * import { getConfig } from "@gtkx/testing";
46
- *
47
- * const config = getConfig();
48
- * console.log(config.showSuggestions);
49
- * ```
43
+ * Returns the current global testing configuration.
50
44
  */
51
- export const getConfig = (): Readonly<Config> => {
45
+ export const getConfig = (): Config => {
52
46
  return currentConfig;
53
47
  };
54
48
 
55
49
  /**
56
- * Configures the testing library behavior.
57
- *
58
- * Accepts either a partial configuration object or a function that receives
59
- * the current configuration and returns updates.
50
+ * Merges the given overrides into the global testing configuration.
60
51
  *
61
- * @param newConfig - Partial configuration or updater function
62
- *
63
- * @example
64
- * ```tsx
65
- * import { configure } from "@gtkx/testing";
66
- *
67
- * // Disable role suggestions
68
- * configure({ showSuggestions: false });
69
- *
70
- * // Use updater function
71
- * configure((current) => ({
72
- * showSuggestions: !current.showSuggestions,
73
- * }));
74
- * ```
52
+ * @param newConfig Either a partial config object or a function that receives
53
+ * the current config and returns the fields to override.
75
54
  */
76
- export const configure = (newConfig: Partial<Config> | ((current: Config) => Partial<Config>)): void => {
55
+ export const configure = (newConfig: Partial<Config> | ConfigFn): void => {
77
56
  const updates = typeof newConfig === "function" ? newConfig(currentConfig) : newConfig;
78
57
  currentConfig = { ...currentConfig, ...updates };
79
58
  };
80
-
81
- /**
82
- * Resets configuration to defaults.
83
- * Primarily used for testing.
84
- *
85
- * @internal
86
- */
87
- export const resetConfig = (): void => {
88
- currentConfig = { ...defaultConfig };
89
- };
@@ -0,0 +1,30 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+
3
+ export type EditableTarget = Gtk.Editable | Gtk.TextView;
4
+
5
+ export const EDITABLE_ROLES: Set<Gtk.AccessibleRole> = new Set<Gtk.AccessibleRole>([
6
+ Gtk.AccessibleRole.TEXT_BOX,
7
+ Gtk.AccessibleRole.SEARCH_BOX,
8
+ Gtk.AccessibleRole.SPIN_BUTTON,
9
+ ]);
10
+
11
+ export const isEditable = (widget: unknown): widget is EditableTarget =>
12
+ widget instanceof Gtk.Editable || widget instanceof Gtk.TextView;
13
+
14
+ export const getEditableDelegate = (widget: Gtk.Widget): Gtk.Widget | null => {
15
+ if (!(widget instanceof Gtk.Editable)) return null;
16
+ return widget.getDelegate();
17
+ };
18
+
19
+ const readTextViewBufferText = (widget: Gtk.TextView): string => {
20
+ const buffer = widget.getBuffer();
21
+ const [start, end] = buffer.getBounds();
22
+ return buffer.getText(start, end, true);
23
+ };
24
+
25
+ export const readEditableText = (widget: EditableTarget): string => {
26
+ if (widget instanceof Gtk.TextView) {
27
+ return readTextViewBufferText(widget);
28
+ }
29
+ return widget.getText();
30
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,130 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { getConfig } from "./config.js";
3
+ import { prettyWidget } from "./pretty-widget.js";
4
+ import { formatRole, prettyRoles } from "./role-helpers.js";
5
+ import type { Container } from "./traversal.js";
6
+ import type { ByRoleOptions, ByRoleValue, Matcher } from "./types.js";
7
+
8
+ const formatTextMatcher = (text: Matcher): string => {
9
+ if (typeof text === "function") {
10
+ return "custom function";
11
+ }
12
+ if (text instanceof RegExp) {
13
+ return text.toString();
14
+ }
15
+ return `'${text}'`;
16
+ };
17
+
18
+ const formatByRoleValue = (value: ByRoleValue): string => {
19
+ const parts: string[] = [];
20
+ if (value.now !== undefined) parts.push(`now=${value.now}`);
21
+ if (value.min !== undefined) parts.push(`min=${value.min}`);
22
+ if (value.max !== undefined) parts.push(`max=${value.max}`);
23
+ if (value.text !== undefined) parts.push(`text ${formatTextMatcher(value.text)}`);
24
+ return parts.join(", ");
25
+ };
26
+
27
+ const formatByRoleDescription = (role: Gtk.AccessibleRole, options?: ByRoleOptions): string => {
28
+ const parts = [`role '${formatRole(role).toUpperCase()}'`];
29
+ if (options?.name) parts.push(`name ${formatTextMatcher(options.name)}`);
30
+ if (options?.checked !== undefined) parts.push(`checked=${options.checked}`);
31
+ if (options?.pressed !== undefined) parts.push(`pressed=${options.pressed}`);
32
+ if (options?.selected !== undefined) parts.push(`selected=${options.selected}`);
33
+ if (options?.expanded !== undefined) parts.push(`expanded=${options.expanded}`);
34
+ if (options?.level !== undefined) parts.push(`level=${options.level}`);
35
+ if (options?.busy !== undefined) parts.push(`busy=${options.busy}`);
36
+ if (options?.description) parts.push(`description ${formatTextMatcher(options.description)}`);
37
+ if (options?.value) parts.push(`value ${formatByRoleValue(options.value)}`);
38
+ if (options?.hidden !== undefined) parts.push(`hidden=${options.hidden}`);
39
+ return parts.join(" and ");
40
+ };
41
+
42
+ export type QueryDescriptor =
43
+ | { queryType: "role"; role: Gtk.AccessibleRole; options?: ByRoleOptions | undefined }
44
+ | { queryType: "text"; text: Matcher }
45
+ | { queryType: "labelText"; text: Matcher }
46
+ | { queryType: "name"; name: Matcher }
47
+ | { queryType: "placeholderText"; text: Matcher }
48
+ | { queryType: "displayValue"; value: Matcher };
49
+
50
+ const formatQueryDescription = (descriptor: QueryDescriptor): string => {
51
+ switch (descriptor.queryType) {
52
+ case "role":
53
+ return formatByRoleDescription(descriptor.role, descriptor.options);
54
+ case "text":
55
+ return `text ${formatTextMatcher(descriptor.text)}`;
56
+ case "labelText":
57
+ return `label text ${formatTextMatcher(descriptor.text)}`;
58
+ case "name":
59
+ return `name ${formatTextMatcher(descriptor.name)}`;
60
+ case "placeholderText":
61
+ return `placeholder text ${formatTextMatcher(descriptor.text)}`;
62
+ case "displayValue":
63
+ return `display value ${formatTextMatcher(descriptor.value)}`;
64
+ }
65
+ };
66
+
67
+ let expensiveErrorDiagnosticsDisabled = false;
68
+
69
+ export const runWithExpensiveErrorDiagnosticsDisabled = <T>(callback: () => T): T => {
70
+ const previous = expensiveErrorDiagnosticsDisabled;
71
+ expensiveErrorDiagnosticsDisabled = true;
72
+ try {
73
+ return callback();
74
+ } finally {
75
+ expensiveErrorDiagnosticsDisabled = previous;
76
+ }
77
+ };
78
+
79
+ const buildElementError = (container: Container, headLines: string[]): Error => {
80
+ const config = getConfig();
81
+ const lines = expensiveErrorDiagnosticsDisabled
82
+ ? headLines
83
+ : [...headLines, "", prettyWidget(container, { highlight: false })];
84
+ return config.getElementError(lines.join("\n"), container);
85
+ };
86
+
87
+ export const notFoundError = (container: Container, descriptor: QueryDescriptor): Error => {
88
+ const description = formatQueryDescription(descriptor);
89
+ const headLines = [`Unable to find an element with ${description}`];
90
+
91
+ if (!expensiveErrorDiagnosticsDisabled && descriptor.queryType === "role") {
92
+ headLines.push("", "Here are the accessible roles:", "", prettyRoles(container));
93
+ }
94
+
95
+ return buildElementError(container, headLines);
96
+ };
97
+
98
+ const allByVariantHint = (descriptor: QueryDescriptor): string => {
99
+ const variant = descriptor.queryType === "role" ? "getAllByRole" : "getAllBy*";
100
+ return (
101
+ "(If this is intentional, use the *AllBy* variant of the query, " +
102
+ `e.g. queryAllBy*/getAllBy*/findAllBy*, such as ${variant}.)`
103
+ );
104
+ };
105
+
106
+ export const multipleFoundError = (container: Container, descriptor: QueryDescriptor, matches: Gtk.Widget[]): Error => {
107
+ const description = formatQueryDescription(descriptor);
108
+ const headLines = [
109
+ `Found ${matches.length} elements with ${description}, but expected only one`,
110
+ "",
111
+ allByVariantHint(descriptor),
112
+ ];
113
+ if (!expensiveErrorDiagnosticsDisabled) {
114
+ const renderedMatches = matches.map((widget) => prettyWidget(widget, { highlight: false }));
115
+ headLines.push("", "Here are the matching elements:", "", ...renderedMatches);
116
+ }
117
+ return buildElementError(container, headLines);
118
+ };
119
+
120
+ export const suggestionError = (suggestion: string, container: Container): Error => {
121
+ const config = getConfig();
122
+ const message = `A better query is available, try this:\n${suggestion}\n`;
123
+ return config.getElementError(message, container);
124
+ };
125
+
126
+ export const timeoutError = (timeout: number, lastError: Error | null): Error => {
127
+ const baseMessage = `Timed out after ${timeout}ms`;
128
+ const message = lastError ? `${baseMessage}.\n\n${lastError.message}` : baseMessage;
129
+ return getConfig().getElementError(message);
130
+ };
package/src/fire-event.ts CHANGED
@@ -1,43 +1,16 @@
1
- import { type Object as GObject, signalEmitv, signalLookup, typeFromName, Value } from "@gtkx/ffi/gobject";
2
- import type * as Gtk from "@gtkx/ffi/gtk";
3
- import { tick } from "./timing.js";
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import { runInAct } from "./act.js";
4
3
 
5
4
  /**
6
- * Emits a GTK signal on a widget or event controller.
5
+ * Emits a GObject signal on a widget or object inside React's act environment,
6
+ * so any resulting state updates are flushed before the promise resolves.
7
7
  *
8
- * Low-level utility for triggering signals directly. Prefer {@link userEvent}
9
- * for common interactions like clicking and typing.
10
- *
11
- * @param element - The widget or event controller to emit the signal on
12
- * @param signalName - GTK signal name (e.g., "clicked", "activate", "drag-begin")
13
- * @param args - Additional signal arguments as GValues
14
- *
15
- * @example
16
- * ```tsx
17
- * import { fireEvent } from "@gtkx/testing";
18
- * import { Value } from "@gtkx/ffi/gobject";
19
- *
20
- * // Emit signal on widget
21
- * await fireEvent(widget, "clicked");
22
- *
23
- * // Emit signal on gesture controller
24
- * const gesture = widget.observeControllers().getObject(0) as Gtk.GestureDrag;
25
- * await fireEvent(gesture, "drag-begin", Value.newFromDouble(100), Value.newFromDouble(100));
26
- * ```
27
- *
28
- * @see {@link userEvent} for high-level user interactions
8
+ * @param element The GObject instance to emit the signal on.
9
+ * @param signalName Name of the signal to emit.
10
+ * @param args Arguments passed to the signal handlers.
29
11
  */
30
- export const fireEvent = async (
31
- element: Gtk.Widget | Gtk.EventController,
32
- signalName: string,
33
- ...args: Value[]
34
- ): Promise<void> => {
35
- const gtype = typeFromName((element.constructor as typeof GObject).glibTypeName);
36
- const signalId = signalLookup(signalName, gtype);
37
-
38
- const instanceValue = Value.newFromObject(element as GObject);
39
-
40
- signalEmitv([instanceValue, ...args], signalId, 0);
41
-
42
- await tick();
12
+ export const fireEvent = async (element: GObject.Object, signalName: string, ...args: unknown[]): Promise<void> => {
13
+ await runInAct(() => {
14
+ element.emit(signalName, ...args);
15
+ });
43
16
  };
@@ -0,0 +1,34 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+
3
+ const CLOCK_STALL_FALLBACK_MS = 500;
4
+
5
+ export const scheduleAfterLayout = (widget: Gtk.Widget | null, callback: () => void): void => {
6
+ let done = false;
7
+ let removeTick: (() => void) | null = null;
8
+ let fallback: ReturnType<typeof setTimeout> | null = null;
9
+
10
+ const finish = (): void => {
11
+ if (done) return;
12
+ done = true;
13
+ if (fallback !== null) clearTimeout(fallback);
14
+ removeTick?.();
15
+ removeTick = null;
16
+ fallback = null;
17
+ callback();
18
+ };
19
+
20
+ if (widget === null || widget.getFrameClock() === null || widget.getWidth() > 0) {
21
+ queueMicrotask(finish);
22
+ return;
23
+ }
24
+
25
+ fallback = setTimeout(finish, CLOCK_STALL_FALLBACK_MS);
26
+
27
+ const tickId = widget.addTickCallback(() => {
28
+ if (widget.getWidth() === 0) return true;
29
+ finish();
30
+ return false;
31
+ });
32
+
33
+ removeTick = () => widget.removeTickCallback(tickId);
34
+ };
package/src/index.ts CHANGED
@@ -1,51 +1,93 @@
1
- export type { Config } from "./config.js";
1
+ import "./register-test-hooks.js";
2
+
3
+ export { act } from "./act.js";
4
+ export type { BoundQueries, RenderResult, Screen } from "./bound-queries.js";
5
+ export type { Config, ConfigFn } from "./config.js";
2
6
  export { configure, getConfig } from "./config.js";
3
7
  export { fireEvent } from "./fire-event.js";
8
+ export type { TextExpectation } from "./matchers.js";
9
+ export { matchers, registerMatchers } from "./matchers.js";
4
10
  export type { PrettyWidgetOptions } from "./pretty-widget.js";
5
- export { prettyWidget } from "./pretty-widget.js";
11
+ export { logWidget, prettyWidget } from "./pretty-widget.js";
6
12
  export {
13
+ findAllByDisplayValue,
7
14
  findAllByLabelText,
8
15
  findAllByName,
16
+ findAllByPlaceholderText,
9
17
  findAllByRole,
10
18
  findAllByText,
19
+ findByDisplayValue,
11
20
  findByLabelText,
12
21
  findByName,
22
+ findByPlaceholderText,
13
23
  findByRole,
14
24
  findByText,
25
+ getAllByDisplayValue,
26
+ getAllByLabelText,
27
+ getAllByName,
28
+ getAllByPlaceholderText,
29
+ getAllByRole,
30
+ getAllByText,
31
+ getByDisplayValue,
32
+ getByLabelText,
33
+ getByName,
34
+ getByPlaceholderText,
35
+ getByRole,
36
+ getByText,
37
+ getDefaultNormalizer,
38
+ queryAllByDisplayValue,
15
39
  queryAllByLabelText,
16
40
  queryAllByName,
41
+ queryAllByPlaceholderText,
17
42
  queryAllByRole,
18
43
  queryAllByText,
44
+ queryByDisplayValue,
19
45
  queryByLabelText,
20
46
  queryByName,
47
+ queryByPlaceholderText,
21
48
  queryByRole,
22
49
  queryByText,
23
50
  } from "./queries.js";
24
51
  export { cleanup, render } from "./render.js";
25
52
  export { renderHook } from "./render-hook.js";
26
- export type { RoleInfo } from "./role-helpers.js";
27
- export { getRoles, logRoles, prettyRoles } from "./role-helpers.js";
53
+ export { formatRole, getRoles, logRoles, prettyRoles } from "./role-helpers.js";
28
54
  export { screen } from "./screen.js";
29
- export type { ScreenshotOptions } from "./screenshot.js";
30
- export { screenshot } from "./screenshot.js";
31
- export { tick } from "./timing.js";
55
+ export { captureAndSaveScreenshot, logScreenshotPath, screenshot } from "./screenshot.js";
56
+ export type { Method, Suggestion, Variant } from "./suggestions.js";
57
+ export { getSuggestedQuery } from "./suggestions.js";
32
58
  export type { Container } from "./traversal.js";
33
59
  export type {
34
- BoundQueries,
35
60
  ByRoleOptions,
61
+ ByRoleValue,
62
+ Matcher,
63
+ MatcherFunction,
64
+ MatcherOptions,
65
+ NormalizerFn,
36
66
  NormalizerOptions,
37
67
  RenderHookOptions,
38
68
  RenderHookResult,
39
69
  RenderOptions,
40
- RenderResult,
70
+ ScreenshotOptions,
41
71
  ScreenshotResult,
42
- TextMatch,
43
- TextMatchFunction,
44
- TextMatchOptions,
45
72
  WaitForOptions,
73
+ WindowSelector,
46
74
  WrapperComponent,
47
75
  } from "./types.js";
48
- export type { PointerInput, TabOptions } from "./user-event.js";
49
- export { userEvent } from "./user-event.js";
76
+ export type {
77
+ DragOffset,
78
+ DragOptions,
79
+ DropContent,
80
+ DropOptions,
81
+ PointerInput,
82
+ TabOptions,
83
+ TypeOptions,
84
+ UserEvent,
85
+ } from "./user-event/index.js";
86
+ export { userEvent } from "./user-event/index.js";
50
87
  export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
88
+ export {
89
+ getWidgetErrorMessage,
90
+ getWidgetInvalidState,
91
+ getWidgetNodeText,
92
+ } from "./widget-accessible-properties.js";
51
93
  export { within } from "./within.js";