@gtkx/testing 1.0.0-rc.2 → 1.0.0-rc.3

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 (44) hide show
  1. package/dist/bound-queries.d.ts +2 -3
  2. package/dist/bound-queries.d.ts.map +1 -1
  3. package/dist/bound-queries.js.map +1 -1
  4. package/dist/index.d.ts +7 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/matchers.d.ts +81 -21
  9. package/dist/matchers.d.ts.map +1 -1
  10. package/dist/matchers.js +258 -21
  11. package/dist/matchers.js.map +1 -1
  12. package/dist/normalize.d.ts +10 -0
  13. package/dist/normalize.d.ts.map +1 -0
  14. package/dist/normalize.js +20 -0
  15. package/dist/normalize.js.map +1 -0
  16. package/dist/queries.d.ts +3 -21
  17. package/dist/queries.d.ts.map +1 -1
  18. package/dist/queries.js +46 -59
  19. package/dist/queries.js.map +1 -1
  20. package/dist/render.js +1 -1
  21. package/dist/render.js.map +1 -1
  22. package/dist/types.d.ts +29 -3
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/types.js.map +1 -1
  25. package/dist/user-event/controller.d.ts +2 -1
  26. package/dist/user-event/controller.d.ts.map +1 -1
  27. package/dist/user-event/controller.js +8 -1
  28. package/dist/user-event/controller.js.map +1 -1
  29. package/dist/widget-accessible-properties.d.ts +12 -2
  30. package/dist/widget-accessible-properties.d.ts.map +1 -1
  31. package/dist/widget-accessible-properties.js +112 -10
  32. package/dist/widget-accessible-properties.js.map +1 -1
  33. package/dist/within.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/bound-queries.ts +2 -3
  36. package/src/index.ts +11 -2
  37. package/src/matchers.ts +456 -53
  38. package/src/normalize.ts +28 -0
  39. package/src/queries.ts +78 -105
  40. package/src/render.tsx +1 -1
  41. package/src/types.ts +48 -2
  42. package/src/user-event/controller.ts +21 -1
  43. package/src/widget-accessible-properties.ts +172 -10
  44. package/src/within.ts +1 -1
package/src/queries.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
- import type { ByRoleOptions, ByRoleValue, Matcher, MatcherOptions, NormalizerFn, NormalizerOptions } from "./types.js";
2
+ import type {
3
+ ByRoleOptions,
4
+ ByRoleValue,
5
+ Matcher,
6
+ MatcherOptions,
7
+ NormalizerFn,
8
+ QueryFamilies,
9
+ } from "./types.js";
3
10
  import { buildQueries, type BuiltQueries, type QueryAllBy } from "./build-queries.js";
4
11
  import { multipleFoundError, notFoundError } from "./errors.js";
12
+ import { getDefaultNormalizer } from "./normalize.js";
5
13
  import { type Container, findAll, traverse } from "./traversal.js";
6
14
  import {
7
15
  getWidgetAccessibleName,
8
16
  getWidgetBusyState,
9
- getWidgetCheckedState,
10
17
  getWidgetDescription,
11
18
  getWidgetDisplayValue,
12
19
  getWidgetExpandedState,
@@ -20,30 +27,10 @@ import {
20
27
  getWidgetSelectedState,
21
28
  getWidgetValue,
22
29
  isInaccessible,
30
+ isWidgetChecked,
23
31
  } from "./widget-accessible-properties.js";
24
32
 
25
- type QueryFamilyReturns = {
26
- queryBy: Gtk.Widget | null;
27
- queryAllBy: Gtk.Widget[];
28
- getBy: Gtk.Widget;
29
- getAllBy: Gtk.Widget[];
30
- findBy: Promise<Gtk.Widget>;
31
- findAllBy: Promise<Gtk.Widget[]>;
32
- };
33
-
34
- type NamedFamily<Suffix extends string, Args extends unknown[]> = {
35
- [K in keyof QueryFamilyReturns as `${K & string}${Suffix}`]: (
36
- container: Container,
37
- ...args: Args
38
- ) => QueryFamilyReturns[K];
39
- };
40
-
41
- type BuiltinQueries = NamedFamily<"Role", [role: Gtk.AccessibleRole, options?: ByRoleOptions]> &
42
- NamedFamily<"LabelText", [text: Matcher, options?: MatcherOptions]> &
43
- NamedFamily<"Text", [text: Matcher, options?: MatcherOptions]> &
44
- NamedFamily<"Name", [name: Matcher, options?: MatcherOptions]> &
45
- NamedFamily<"PlaceholderText", [text: Matcher, options?: MatcherOptions]> &
46
- NamedFamily<"DisplayValue", [value: Matcher, options?: MatcherOptions]>;
33
+ type BuiltinQueries = QueryFamilies<[container: Container]>;
47
34
 
48
35
  const roleQueries = nameQueryFamily(
49
36
  "Role",
@@ -112,167 +99,132 @@ const displayValueQueries = nameQueryFamily(
112
99
  ),
113
100
  );
114
101
 
115
- const builtinQueries: BuiltinQueries = {
102
+ const builtinQueries = {
116
103
  ...roleQueries,
117
104
  ...labelTextQueries,
118
105
  ...textQueries,
119
106
  ...nameQueries,
120
107
  ...placeholderTextQueries,
121
108
  ...displayValueQueries,
122
- };
109
+ } as BuiltinQueries;
123
110
 
124
111
  /**
125
112
  * Returns the single widget with a matching accessible role and options, or null when none match.
126
113
  * Throws when more than one matches.
127
114
  */
128
- const queryByRole: BuiltinQueries["queryByRole"] = roleQueries.queryByRole;
115
+ const queryByRole: BuiltinQueries["queryByRole"] = builtinQueries.queryByRole;
129
116
  /** Returns every widget with a matching accessible role and options. Throws when none match. */
130
- const getAllByRole: BuiltinQueries["getAllByRole"] = roleQueries.getAllByRole;
117
+ const getAllByRole: BuiltinQueries["getAllByRole"] = builtinQueries.getAllByRole;
131
118
  /**
132
119
  * Returns the single widget with a matching accessible role and options.
133
120
  * Throws when none or more than one matches.
134
121
  */
135
- const getByRole: BuiltinQueries["getByRole"] = roleQueries.getByRole;
122
+ const getByRole: BuiltinQueries["getByRole"] = builtinQueries.getByRole;
136
123
  /**
137
124
  * Waits for and returns every widget with a matching accessible role and options, retrying until at
138
125
  * least one appears or the timeout elapses.
139
126
  */
140
- const findAllByRole: BuiltinQueries["findAllByRole"] = roleQueries.findAllByRole;
127
+ const findAllByRole: BuiltinQueries["findAllByRole"] = builtinQueries.findAllByRole;
141
128
  /**
142
129
  * Waits for and returns the single widget with a matching accessible role and options, retrying
143
130
  * until it appears or the timeout elapses. Rejects when none or more than one matches.
144
131
  */
145
- const findByRole: BuiltinQueries["findByRole"] = roleQueries.findByRole;
132
+ const findByRole: BuiltinQueries["findByRole"] = builtinQueries.findByRole;
146
133
  /**
147
134
  * Returns the single widget with matching associated label text, or null when none match.
148
135
  * Throws when more than one matches.
149
136
  */
150
- const queryByLabelText: BuiltinQueries["queryByLabelText"] = labelTextQueries.queryByLabelText;
137
+ const queryByLabelText: BuiltinQueries["queryByLabelText"] = builtinQueries.queryByLabelText;
151
138
  /** Returns every widget with matching associated label text. Throws when none match. */
152
- const getAllByLabelText: BuiltinQueries["getAllByLabelText"] = labelTextQueries.getAllByLabelText;
139
+ const getAllByLabelText: BuiltinQueries["getAllByLabelText"] = builtinQueries.getAllByLabelText;
153
140
  /** Returns the single widget with matching associated label text. Throws when none or more than one matches. */
154
- const getByLabelText: BuiltinQueries["getByLabelText"] = labelTextQueries.getByLabelText;
141
+ const getByLabelText: BuiltinQueries["getByLabelText"] = builtinQueries.getByLabelText;
155
142
  /**
156
143
  * Waits for and returns every widget with matching associated label text, retrying until at least
157
144
  * one appears or the timeout elapses.
158
145
  */
159
- const findAllByLabelText: BuiltinQueries["findAllByLabelText"] = labelTextQueries.findAllByLabelText;
146
+ const findAllByLabelText: BuiltinQueries["findAllByLabelText"] = builtinQueries.findAllByLabelText;
160
147
  /**
161
148
  * Waits for and returns the single widget with matching associated label text, retrying until it
162
149
  * appears or the timeout elapses. Rejects when none or more than one matches.
163
150
  */
164
- const findByLabelText: BuiltinQueries["findByLabelText"] = labelTextQueries.findByLabelText;
151
+ const findByLabelText: BuiltinQueries["findByLabelText"] = builtinQueries.findByLabelText;
165
152
  /**
166
153
  * Returns the single widget with matching rendered text content, or null when none match.
167
154
  * Throws when more than one matches.
168
155
  */
169
- const queryByText: BuiltinQueries["queryByText"] = textQueries.queryByText;
156
+ const queryByText: BuiltinQueries["queryByText"] = builtinQueries.queryByText;
170
157
  /** Returns every widget with matching rendered text content. Throws when none match. */
171
- const getAllByText: BuiltinQueries["getAllByText"] = textQueries.getAllByText;
158
+ const getAllByText: BuiltinQueries["getAllByText"] = builtinQueries.getAllByText;
172
159
  /** Returns the single widget with matching rendered text content. Throws when none or more than one matches. */
173
- const getByText: BuiltinQueries["getByText"] = textQueries.getByText;
160
+ const getByText: BuiltinQueries["getByText"] = builtinQueries.getByText;
174
161
  /**
175
162
  * Waits for and returns every widget with matching rendered text content, retrying until at least
176
163
  * one appears or the timeout elapses.
177
164
  */
178
- const findAllByText: BuiltinQueries["findAllByText"] = textQueries.findAllByText;
165
+ const findAllByText: BuiltinQueries["findAllByText"] = builtinQueries.findAllByText;
179
166
  /**
180
167
  * Waits for and returns the single widget with matching rendered text content, retrying until it
181
168
  * appears or the timeout elapses. Rejects when none or more than one matches.
182
169
  */
183
- const findByText: BuiltinQueries["findByText"] = textQueries.findByText;
170
+ const findByText: BuiltinQueries["findByText"] = builtinQueries.findByText;
184
171
  /**
185
172
  * Returns the single widget with a matching widget name, or null when none match.
186
173
  * Throws when more than one matches.
187
174
  */
188
- const queryByName: BuiltinQueries["queryByName"] = nameQueries.queryByName;
175
+ const queryByName: BuiltinQueries["queryByName"] = builtinQueries.queryByName;
189
176
  /** Returns every widget with a matching widget name. Throws when none match. */
190
- const getAllByName: BuiltinQueries["getAllByName"] = nameQueries.getAllByName;
177
+ const getAllByName: BuiltinQueries["getAllByName"] = builtinQueries.getAllByName;
191
178
  /** Returns the single widget with a matching widget name. Throws when none or more than one matches. */
192
- const getByName: BuiltinQueries["getByName"] = nameQueries.getByName;
179
+ const getByName: BuiltinQueries["getByName"] = builtinQueries.getByName;
193
180
  /**
194
181
  * Waits for and returns every widget with a matching widget name, retrying until at least one
195
182
  * appears or the timeout elapses.
196
183
  */
197
- const findAllByName: BuiltinQueries["findAllByName"] = nameQueries.findAllByName;
184
+ const findAllByName: BuiltinQueries["findAllByName"] = builtinQueries.findAllByName;
198
185
  /**
199
186
  * Waits for and returns the single widget with a matching widget name, retrying until it appears or
200
187
  * the timeout elapses. Rejects when none or more than one matches.
201
188
  */
202
- const findByName: BuiltinQueries["findByName"] = nameQueries.findByName;
203
-
189
+ const findByName: BuiltinQueries["findByName"] = builtinQueries.findByName;
204
190
  /**
205
191
  * Returns the single widget with matching placeholder text, or null when none match.
206
192
  * Throws when more than one matches.
207
193
  */
208
- const queryByPlaceholderText: BuiltinQueries["queryByPlaceholderText"] =
209
- placeholderTextQueries.queryByPlaceholderText;
210
-
194
+ const queryByPlaceholderText: BuiltinQueries["queryByPlaceholderText"] = builtinQueries.queryByPlaceholderText;
211
195
  /** Returns every widget with matching placeholder text. Throws when none match. */
212
- const getAllByPlaceholderText: BuiltinQueries["getAllByPlaceholderText"] =
213
- placeholderTextQueries.getAllByPlaceholderText;
214
-
196
+ const getAllByPlaceholderText: BuiltinQueries["getAllByPlaceholderText"] = builtinQueries.getAllByPlaceholderText;
215
197
  /** Returns the single widget with matching placeholder text. Throws when none or more than one matches. */
216
- const getByPlaceholderText: BuiltinQueries["getByPlaceholderText"] = placeholderTextQueries.getByPlaceholderText;
217
-
198
+ const getByPlaceholderText: BuiltinQueries["getByPlaceholderText"] = builtinQueries.getByPlaceholderText;
218
199
  /**
219
200
  * Waits for and returns every widget with matching placeholder text, retrying until at least one
220
201
  * appears or the timeout elapses.
221
202
  */
222
- const findAllByPlaceholderText: BuiltinQueries["findAllByPlaceholderText"] =
223
- placeholderTextQueries.findAllByPlaceholderText;
224
-
203
+ const findAllByPlaceholderText: BuiltinQueries["findAllByPlaceholderText"] = builtinQueries.findAllByPlaceholderText;
225
204
  /**
226
205
  * Waits for and returns the single widget with matching placeholder text, retrying until it appears
227
206
  * or the timeout elapses. Rejects when none or more than one matches.
228
207
  */
229
- const findByPlaceholderText: BuiltinQueries["findByPlaceholderText"] =
230
- placeholderTextQueries.findByPlaceholderText;
231
-
208
+ const findByPlaceholderText: BuiltinQueries["findByPlaceholderText"] = builtinQueries.findByPlaceholderText;
232
209
  /**
233
210
  * Returns the single widget with a matching display value, or null when none match.
234
211
  * Throws when more than one matches.
235
212
  */
236
- const queryByDisplayValue: BuiltinQueries["queryByDisplayValue"] = displayValueQueries.queryByDisplayValue;
213
+ const queryByDisplayValue: BuiltinQueries["queryByDisplayValue"] = builtinQueries.queryByDisplayValue;
237
214
  /** Returns every widget with a matching display value. Throws when none match. */
238
- const getAllByDisplayValue: BuiltinQueries["getAllByDisplayValue"] = displayValueQueries.getAllByDisplayValue;
215
+ const getAllByDisplayValue: BuiltinQueries["getAllByDisplayValue"] = builtinQueries.getAllByDisplayValue;
239
216
  /** Returns the single widget with a matching display value. Throws when none or more than one matches. */
240
- const getByDisplayValue: BuiltinQueries["getByDisplayValue"] = displayValueQueries.getByDisplayValue;
217
+ const getByDisplayValue: BuiltinQueries["getByDisplayValue"] = builtinQueries.getByDisplayValue;
241
218
  /**
242
219
  * Waits for and returns every widget with a matching display value, retrying until at least one
243
220
  * appears or the timeout elapses.
244
221
  */
245
- const findAllByDisplayValue: BuiltinQueries["findAllByDisplayValue"] = displayValueQueries.findAllByDisplayValue;
222
+ const findAllByDisplayValue: BuiltinQueries["findAllByDisplayValue"] = builtinQueries.findAllByDisplayValue;
246
223
  /**
247
224
  * Waits for and returns the single widget with a matching display value, retrying until it appears
248
225
  * or the timeout elapses. Rejects when none or more than one matches.
249
226
  */
250
- const findByDisplayValue: BuiltinQueries["findByDisplayValue"] = displayValueQueries.findByDisplayValue;
251
-
252
- /**
253
- * Builds the default text normalizer, which optionally trims surrounding whitespace and collapses
254
- * runs of whitespace into single spaces.
255
- * @param options Toggles for trimming and whitespace collapsing.
256
- * @returns A function that normalizes a string for comparison against a matcher.
257
- */
258
- const getDefaultNormalizer = ({
259
- trim = true,
260
- collapseWhitespace = true,
261
- }: NormalizerOptions = {}): NormalizerFn => {
262
- return (text: string): string => {
263
- let result = text;
264
-
265
- if (trim) {
266
- result = result.trim();
267
- }
268
-
269
- if (collapseWhitespace) {
270
- result = result.replaceAll(/\s+/g, " ");
271
- }
272
-
273
- return result;
274
- };
275
- };
227
+ const findByDisplayValue: BuiltinQueries["findByDisplayValue"] = builtinQueries.findByDisplayValue;
276
228
 
277
229
  const buildNormalizer = (options?: MatcherOptions): NormalizerFn => {
278
230
  const { normalizer, trim, collapseWhitespace } = options ?? {};
@@ -361,7 +313,7 @@ const hasMatchingAccessibleValue = (widget: Gtk.Widget, value: ByRoleValue, opti
361
313
 
362
314
  const hasMatchingBooleanStates = (widget: Gtk.Widget, options: ByRoleOptions): boolean => {
363
315
  const stateChecks: [boolean | undefined, () => boolean | null][] = [
364
- [options.checked, () => getWidgetCheckedState(widget)],
316
+ [options.checked, () => isWidgetChecked(widget)],
365
317
  [options.pressed, () => getWidgetPressedState(widget)],
366
318
  [options.expanded, () => getWidgetExpandedState(widget)],
367
319
  [options.selected, () => getWidgetSelectedState(widget)],
@@ -393,20 +345,27 @@ const hasMatchingAccessibleStates = (widget: Gtk.Widget, options: ByRoleOptions)
393
345
  hasMatchingDescriptionState(widget, options) &&
394
346
  hasMatchingValueState(widget, options);
395
347
 
348
+ const isMatchingWidgetType = (widget: Gtk.Widget, options?: MatcherOptions): boolean =>
349
+ options?.as === undefined || widget instanceof options.as;
350
+
396
351
  const hasMatchingByRoleOptions = (widget: Gtk.Widget, options?: ByRoleOptions): boolean => {
397
352
  if (!options) {
398
353
  return true;
399
354
  }
400
355
 
401
- return hasMatchingAccessibleName(widget, options) && hasMatchingAccessibleStates(widget, options);
356
+ return (
357
+ isMatchingWidgetType(widget, options) &&
358
+ hasMatchingAccessibleName(widget, options) &&
359
+ hasMatchingAccessibleStates(widget, options)
360
+ );
402
361
  };
403
362
 
404
- function nameQueryFamily<Suffix extends string, Args extends unknown[]>(
405
- suffix: Suffix,
363
+ function nameQueryFamily<Args extends unknown[]>(
364
+ suffix: string,
406
365
  queryAllBy: QueryAllBy<Args>,
407
366
  built: BuiltQueries<Args>,
408
- ): NamedFamily<Suffix, Args> {
409
- const family = {
367
+ ): Record<string, unknown> {
368
+ return {
410
369
  [`queryBy${suffix}`]: built.queryBy,
411
370
  [`queryAllBy${suffix}`]: queryAllBy,
412
371
  [`getBy${suffix}`]: built.getBy,
@@ -414,8 +373,6 @@ function nameQueryFamily<Suffix extends string, Args extends unknown[]>(
414
373
  [`findBy${suffix}`]: built.findBy,
415
374
  [`findAllBy${suffix}`]: built.findAllBy,
416
375
  };
417
-
418
- return family as NamedFamily<Suffix, Args>;
419
376
  }
420
377
 
421
378
  /**
@@ -502,7 +459,7 @@ function queryAllByLabelText(container: Container, text: Matcher, options?: Matc
502
459
  collectLabelMatches(results, widget, text, options);
503
460
  }
504
461
 
505
- return [...results];
462
+ return [...results].filter((widget) => isMatchingWidgetType(widget, options));
506
463
  }
507
464
 
508
465
  /**
@@ -513,7 +470,11 @@ function queryAllByLabelText(container: Container, text: Matcher, options?: Matc
513
470
  * @returns Every matching widget, or an empty array when none match.
514
471
  */
515
472
  function queryAllByText(container: Container, text: Matcher, options?: MatcherOptions): Gtk.Widget[] {
516
- return findAll(container, (widget) => isTextMatch(getWidgetLabelText(widget), text, widget, options));
473
+ return findAll(
474
+ container,
475
+ (widget) =>
476
+ isMatchingWidgetType(widget, options) && isTextMatch(getWidgetLabelText(widget), text, widget, options),
477
+ );
517
478
  }
518
479
 
519
480
  /**
@@ -524,7 +485,10 @@ function queryAllByText(container: Container, text: Matcher, options?: MatcherOp
524
485
  * @returns Every matching widget, or an empty array when none match.
525
486
  */
526
487
  function queryAllByName(container: Container, name: Matcher, options?: MatcherOptions): Gtk.Widget[] {
527
- return findAll(container, (widget) => isTextMatch(getWidgetName(widget), name, widget, options));
488
+ return findAll(
489
+ container,
490
+ (widget) => isMatchingWidgetType(widget, options) && isTextMatch(getWidgetName(widget), name, widget, options),
491
+ );
528
492
  }
529
493
 
530
494
  /**
@@ -539,7 +503,12 @@ function queryAllByPlaceholderText(
539
503
  text: Matcher,
540
504
  options?: MatcherOptions,
541
505
  ): Gtk.Widget[] {
542
- return findAll(container, (widget) => isTextMatch(getWidgetPlaceholderText(widget), text, widget, options));
506
+ return findAll(
507
+ container,
508
+ (widget) =>
509
+ isMatchingWidgetType(widget, options) &&
510
+ isTextMatch(getWidgetPlaceholderText(widget), text, widget, options),
511
+ );
543
512
  }
544
513
 
545
514
  /**
@@ -550,7 +519,12 @@ function queryAllByPlaceholderText(
550
519
  * @returns Every matching widget, or an empty array when none match.
551
520
  */
552
521
  function queryAllByDisplayValue(container: Container, value: Matcher, options?: MatcherOptions): Gtk.Widget[] {
553
- return findAll(container, (widget) => isTextMatch(getWidgetDisplayValue(widget), value, widget, options));
522
+ return findAll(
523
+ container,
524
+ (widget) =>
525
+ isMatchingWidgetType(widget, options) &&
526
+ isTextMatch(getWidgetDisplayValue(widget), value, widget, options),
527
+ );
554
528
  }
555
529
 
556
530
  export {
@@ -585,7 +559,6 @@ export {
585
559
  getByDisplayValue,
586
560
  findAllByDisplayValue,
587
561
  findByDisplayValue,
588
- getDefaultNormalizer,
589
562
  queryAllByRole,
590
563
  queryAllByLabelText,
591
564
  queryAllByText,
package/src/render.tsx CHANGED
@@ -97,7 +97,7 @@ const resolveContainer = (container: RenderOptions["container"]): ResolvedContai
97
97
  }
98
98
 
99
99
  const window = new Gtk.Window({ defaultWidth: HARNESS_WINDOW_WIDTH, defaultHeight: HARNESS_WINDOW_HEIGHT });
100
- window.setTitlebar(new Gtk.HeaderBar({ showTitleButtons: false }));
100
+ window.setDecorated(false);
101
101
 
102
102
  return { containerInfo: window, window };
103
103
  };
package/src/types.ts CHANGED
@@ -13,6 +13,11 @@ type MatcherFunction = (content: string, widget: Gtk.Widget) => boolean;
13
13
  type Matcher = string | number | RegExp | MatcherFunction;
14
14
  /** Normalizes a widget's text before it is compared against a matcher. */
15
15
  type NormalizerFn = (text: string) => string;
16
+ /**
17
+ * A widget class usable as a query's `as` constraint, such as `Gtk.Button`. Abstract classes and
18
+ * generated GInterface pseudo-classes are accepted.
19
+ */
20
+ type WidgetType<T extends Gtk.Widget = Gtk.Widget> = abstract new (...args: never[]) => T;
16
21
 
17
22
  /** Options controlling the default text normalizer. */
18
23
  type NormalizerOptions = {
@@ -35,7 +40,7 @@ type WaitForOptions = {
35
40
  };
36
41
 
37
42
  /** Options controlling text matching and, for asynchronous queries, polling behavior. */
38
- type MatcherOptions = {
43
+ type MatcherOptions<T extends Gtk.Widget = Gtk.Widget> = {
39
44
  /** When true (the default), require an exact match; when false, match case-insensitively as a substring. */
40
45
  exact?: boolean | undefined;
41
46
  /** Custom normalizer replacing the default; cannot be combined with `trim` or `collapseWhitespace`. */
@@ -46,6 +51,8 @@ type MatcherOptions = {
46
51
  collapseWhitespace?: boolean | undefined;
47
52
  /** Whether to include a suggested better query in error messages. */
48
53
  suggest?: boolean | undefined;
54
+ /** Restricts matches to instances of this widget class, and narrows the query's return type to it. */
55
+ as?: WidgetType<T> | undefined;
49
56
  } & WaitForOptions;
50
57
 
51
58
  /** Constraints on a widget's numeric range value used by role queries. */
@@ -59,7 +66,7 @@ type ByRoleValue = {
59
66
  };
60
67
 
61
68
  /** Options for role queries: an accessible name matcher plus accessible state and value constraints. */
62
- type ByRoleOptions = MatcherOptions & {
69
+ type ByRoleOptions<T extends Gtk.Widget = Gtk.Widget> = MatcherOptions<T> & {
63
70
  name?: Matcher | undefined;
64
71
  checked?: boolean | undefined;
65
72
  pressed?: boolean | undefined;
@@ -88,6 +95,43 @@ type BoundQuery<Q extends Query> = Q extends (container: Container, ...args: inf
88
95
 
89
96
  type BoundCustomQueries<Q extends QueryMap> = { [K in keyof Q]: BoundQuery<Q[K]> };
90
97
 
98
+ type QueryFamilyReturns<T extends Gtk.Widget> = {
99
+ queryBy: T | null;
100
+ queryAllBy: T[];
101
+ getBy: T;
102
+ getAllBy: T[];
103
+ findBy: Promise<T>;
104
+ findAllBy: Promise<T[]>;
105
+ };
106
+
107
+ type QueryKind = "role" | "text" | "name" | "value";
108
+
109
+ type QueryArgs<Kind extends QueryKind, T extends Gtk.Widget> = Kind extends "role"
110
+ ? [role: Gtk.AccessibleRole, options?: ByRoleOptions<T>]
111
+ : Kind extends "name"
112
+ ? [name: Matcher, options?: MatcherOptions<T>]
113
+ : Kind extends "value"
114
+ ? [value: Matcher, options?: MatcherOptions<T>]
115
+ : [text: Matcher, options?: MatcherOptions<T>];
116
+
117
+ /**
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.
121
+ */
122
+ type QueryFamily<Suffix extends string, Kind extends QueryKind, Head extends unknown[]> = {
123
+ [K in keyof QueryFamilyReturns<Gtk.Widget> as `${K & string}${Suffix}`]: <T extends Gtk.Widget = Gtk.Widget>(
124
+ ...args: [...Head, ...QueryArgs<Kind, T>]
125
+ ) => QueryFamilyReturns<T>[K];
126
+ };
127
+
128
+ type QueryFamilies<Head extends unknown[]> = QueryFamily<"Role", "role", Head> &
129
+ QueryFamily<"LabelText", "text", Head> &
130
+ QueryFamily<"Text", "text", Head> &
131
+ QueryFamily<"Name", "name", Head> &
132
+ QueryFamily<"PlaceholderText", "text", Head> &
133
+ QueryFamily<"DisplayValue", "value", Head>;
134
+
91
135
  /**
92
136
  * Options for {@link render}: the container and base element to mount into, an optional wrapper,
93
137
  * React behavior toggles, error callbacks, and custom queries to bind.
@@ -169,9 +213,11 @@ export {
169
213
  type MatcherOptions,
170
214
  type ByRoleValue,
171
215
  type ByRoleOptions,
216
+ type WidgetType,
172
217
  type WrapperComponent,
173
218
  type QueryMap,
174
219
  type BoundCustomQueries,
220
+ type QueryFamilies,
175
221
  type RenderOptions,
176
222
  type DebugUtilities,
177
223
  type ScreenshotResult,
@@ -39,6 +39,19 @@ const getAllControllers = <T extends Gtk.EventController>(
39
39
  return controllers;
40
40
  };
41
41
 
42
+ const getController = <T extends Gtk.EventController>(
43
+ widget: Gtk.Widget,
44
+ controllerType: ControllerConstructor<T>,
45
+ ): T => {
46
+ const [controller] = getAllControllers(widget, controllerType);
47
+
48
+ if (controller === undefined) {
49
+ throw new Error(`No ${controllerType.name} controller is attached to the widget`);
50
+ }
51
+
52
+ return controller;
53
+ };
54
+
42
55
  const getOrCreateControllers = <T extends Gtk.EventController>(
43
56
  widget: Gtk.Widget,
44
57
  controllerType: ControllerConstructor<T>,
@@ -55,4 +68,11 @@ const getOrCreateControllers = <T extends Gtk.EventController>(
55
68
  return [controller];
56
69
  };
57
70
 
58
- export { queryAllControllers, queryController, getAllControllers, getOrCreateControllers, type ControllerConstructor };
71
+ export {
72
+ queryAllControllers,
73
+ queryController,
74
+ getAllControllers,
75
+ getController,
76
+ getOrCreateControllers,
77
+ type ControllerConstructor,
78
+ };