@gtkx/testing 2.0.0-beta.1 → 2.0.0-beta.10

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 (112) hide show
  1. package/README.md +43 -40
  2. package/dist/accessible-native.d.ts +1 -1
  3. package/dist/accessible-native.d.ts.map +1 -1
  4. package/dist/accessible-native.js +29 -8
  5. package/dist/accessible-native.js.map +1 -1
  6. package/dist/build-queries.d.ts +9 -9
  7. package/dist/build-queries.d.ts.map +1 -1
  8. package/dist/build-queries.js +2 -1
  9. package/dist/build-queries.js.map +1 -1
  10. package/dist/fire-event.d.ts +3 -3
  11. package/dist/fire-event.d.ts.map +1 -1
  12. package/dist/fire-event.js +14 -4
  13. package/dist/fire-event.js.map +1 -1
  14. package/dist/matchers.d.ts +52 -56
  15. package/dist/matchers.d.ts.map +1 -1
  16. package/dist/matchers.js +4 -16
  17. package/dist/matchers.js.map +1 -1
  18. package/dist/queries.d.ts +7 -7
  19. package/dist/queries.d.ts.map +1 -1
  20. package/dist/queries.js +18 -10
  21. package/dist/queries.js.map +1 -1
  22. package/dist/query-helpers.d.ts +2 -2
  23. package/dist/query-helpers.d.ts.map +1 -1
  24. package/dist/query-helpers.js +10 -3
  25. package/dist/query-helpers.js.map +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +4 -0
  28. package/dist/render.js.map +1 -1
  29. package/dist/role-helpers.d.ts +1 -1
  30. package/dist/role-helpers.d.ts.map +1 -1
  31. package/dist/role-helpers.js +2 -1
  32. package/dist/role-helpers.js.map +1 -1
  33. package/dist/screenshot.d.ts +1 -1
  34. package/dist/screenshot.d.ts.map +1 -1
  35. package/dist/screenshot.js +25 -5
  36. package/dist/screenshot.js.map +1 -1
  37. package/dist/suggestions.d.ts +1 -1
  38. package/dist/suggestions.d.ts.map +1 -1
  39. package/dist/suggestions.js +3 -1
  40. package/dist/suggestions.js.map +1 -1
  41. package/dist/traversal.d.ts +4 -4
  42. package/dist/traversal.d.ts.map +1 -1
  43. package/dist/traversal.js +25 -7
  44. package/dist/traversal.js.map +1 -1
  45. package/dist/types.d.ts +10 -10
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js.map +1 -1
  48. package/dist/user-event/click.d.ts +17 -4
  49. package/dist/user-event/click.d.ts.map +1 -1
  50. package/dist/user-event/click.js +28 -9
  51. package/dist/user-event/click.js.map +1 -1
  52. package/dist/user-event/controller.d.ts +5 -5
  53. package/dist/user-event/controller.d.ts.map +1 -1
  54. package/dist/user-event/controller.js +5 -2
  55. package/dist/user-event/controller.js.map +1 -1
  56. package/dist/user-event/index.d.ts +22 -22
  57. package/dist/user-event/index.d.ts.map +1 -1
  58. package/dist/user-event/index.js +28 -25
  59. package/dist/user-event/index.js.map +1 -1
  60. package/dist/user-event/keyboard.d.ts.map +1 -1
  61. package/dist/user-event/keyboard.js +23 -2
  62. package/dist/user-event/keyboard.js.map +1 -1
  63. package/dist/user-event/list-row-click.d.ts +1 -1
  64. package/dist/user-event/list-row-click.d.ts.map +1 -1
  65. package/dist/user-event/list-row-click.js +2 -1
  66. package/dist/user-event/list-row-click.js.map +1 -1
  67. package/dist/user-event/pointer.d.ts.map +1 -1
  68. package/dist/user-event/pointer.js +9 -11
  69. package/dist/user-event/pointer.js.map +1 -1
  70. package/dist/user-event/selection.d.ts +2 -2
  71. package/dist/user-event/selection.d.ts.map +1 -1
  72. package/dist/user-event/selection.js +8 -2
  73. package/dist/user-event/selection.js.map +1 -1
  74. package/dist/wait-for.d.ts +2 -2
  75. package/dist/wait-for.d.ts.map +1 -1
  76. package/dist/wait-for.js +6 -4
  77. package/dist/wait-for.js.map +1 -1
  78. package/dist/widget-accessible-properties.d.ts +4 -3
  79. package/dist/widget-accessible-properties.d.ts.map +1 -1
  80. package/dist/widget-accessible-properties.js +24 -17
  81. package/dist/widget-accessible-properties.js.map +1 -1
  82. package/dist/widget-getters.d.ts +7 -9
  83. package/dist/widget-getters.d.ts.map +1 -1
  84. package/dist/widget-getters.js.map +1 -1
  85. package/dist/widget-target.d.ts +4 -0
  86. package/dist/widget-target.d.ts.map +1 -0
  87. package/dist/widget-target.js +9 -0
  88. package/dist/widget-target.js.map +1 -0
  89. package/package.json +13 -10
  90. package/src/accessible-native.ts +45 -9
  91. package/src/build-queries.ts +60 -30
  92. package/src/fire-event.ts +32 -8
  93. package/src/matchers.ts +63 -85
  94. package/src/queries.ts +64 -30
  95. package/src/query-helpers.ts +17 -9
  96. package/src/render.tsx +7 -2
  97. package/src/role-helpers.ts +3 -1
  98. package/src/screenshot.ts +48 -5
  99. package/src/suggestions.ts +3 -1
  100. package/src/traversal.ts +35 -11
  101. package/src/types.ts +14 -10
  102. package/src/user-event/click.ts +37 -17
  103. package/src/user-event/controller.ts +8 -5
  104. package/src/user-event/index.ts +133 -47
  105. package/src/user-event/keyboard.ts +32 -3
  106. package/src/user-event/list-row-click.ts +2 -2
  107. package/src/user-event/pointer.ts +11 -15
  108. package/src/user-event/selection.ts +12 -4
  109. package/src/wait-for.ts +9 -6
  110. package/src/widget-accessible-properties.ts +39 -25
  111. package/src/widget-getters.ts +11 -13
  112. package/src/widget-target.ts +11 -0
package/src/fire-event.ts CHANGED
@@ -7,13 +7,21 @@ import { runInAct } from "./act.js";
7
7
  */
8
8
  type WidgetEvent = {
9
9
  /** The GObject instance the signal is emitted on. */
10
- target: GObject.Object;
10
+ target: Pick<GObject.Object, "__type__">;
11
11
  /** Name of the signal to emit. */
12
12
  signalName: string;
13
13
  /** Arguments passed to the signal handlers. */
14
14
  args: unknown[];
15
15
  };
16
16
 
17
+ const requireObject = (target: object): GObject.Object => {
18
+ if (!(target instanceof GObject.Object)) {
19
+ throw new TypeError("Expected a GObject instance");
20
+ }
21
+
22
+ return target;
23
+ };
24
+
17
25
  /**
18
26
  * Records a signal emission without delivering it, so it can be inspected or held and passed to
19
27
  * {@link fireEvent} later.
@@ -23,18 +31,28 @@ type WidgetEvent = {
23
31
  * @param args Arguments to pass to the signal handlers.
24
32
  * @returns The recorded emission.
25
33
  */
26
- const createEvent = (target: GObject.Object, signalName: string, ...args: unknown[]): WidgetEvent => ({
27
- target,
34
+ const createEvent = (
35
+ target: Pick<GObject.Object, "__type__">,
36
+ signalName: string,
37
+ ...args: unknown[]
38
+ ): WidgetEvent => ({
39
+ target: requireObject(target),
28
40
  signalName,
29
41
  args,
30
42
  });
31
43
 
32
44
  const dispatchEvent = async (event: WidgetEvent): Promise<void> => {
45
+ const target = requireObject(event.target);
46
+
33
47
  await runInAct(() => {
34
- event.target.emit(event.signalName, ...event.args);
48
+ Reflect.apply(GObject.signalEmit, undefined, [target, event.signalName, ...event.args]);
35
49
  });
36
50
  };
37
51
 
52
+ const isWidgetEvent = (value: object): value is WidgetEvent =>
53
+ "target" in value && "signalName" in value && "args" in value &&
54
+ typeof value.signalName === "string" && Array.isArray(value.args);
55
+
38
56
  /**
39
57
  * Emits a recorded signal emission inside React's act environment, so any resulting state updates are
40
58
  * flushed before the promise resolves.
@@ -50,14 +68,18 @@ function fireEvent(event: WidgetEvent): Promise<void>;
50
68
  * @param signalName Name of the signal to emit.
51
69
  * @param args Arguments passed to the signal handlers.
52
70
  */
53
- function fireEvent(target: GObject.Object, signalName: string, ...args: unknown[]): Promise<void>;
71
+ function fireEvent(
72
+ target: Pick<GObject.Object, "__type__">,
73
+ signalName: string,
74
+ ...args: unknown[]
75
+ ): Promise<void>;
54
76
 
55
77
  function fireEvent(
56
- eventOrTarget: WidgetEvent | GObject.Object,
78
+ eventOrTarget: WidgetEvent | Pick<GObject.Object, "__type__">,
57
79
  signalName?: string,
58
80
  ...args: unknown[]
59
81
  ): Promise<void> {
60
- if (!(eventOrTarget instanceof GObject.Object)) {
82
+ if (signalName === undefined && isWidgetEvent(eventOrTarget)) {
61
83
  return dispatchEvent(eventOrTarget);
62
84
  }
63
85
 
@@ -65,7 +87,9 @@ function fireEvent(
65
87
  throw new Error("Unable to fire an event: pass a signal name, or an event built by createEvent.");
66
88
  }
67
89
 
68
- return dispatchEvent(createEvent(eventOrTarget, signalName, ...args));
90
+ const target = requireObject(eventOrTarget);
91
+
92
+ return dispatchEvent({ target, signalName, args });
69
93
  }
70
94
 
71
95
  export { createEvent, fireEvent, type WidgetEvent };
package/src/matchers.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SyncExpectationResult } from "@vitest/expect";
1
+ import type { SyncMatcherResult } from "vitest";
2
2
  import * as GObject from "@gtkx/gi/gobject";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
4
  import type { ByRoleOptions, Matcher, MatcherOptions } from "./types.js";
@@ -57,7 +57,7 @@ type TextContentOptions = {
57
57
  /** The expected value for a style class: an exact class name or a regular expression. */
58
58
  type ClassExpectation = string | RegExp;
59
59
  /** The outcome of a matcher: whether it passed, and the failure text built on demand. */
60
- type MatcherResult = Pick<SyncExpectationResult, "message" | "pass">;
60
+ type MatcherResult = Pick<SyncMatcherResult, "message" | "pass">;
61
61
 
62
62
  /** The matcher state bound as `this`, supplying the test runner's deep equality check. */
63
63
  type MatcherContext = {
@@ -77,7 +77,7 @@ type TextQueryArgs = [text: Matcher, options?: MatcherOptions];
77
77
  type ContainmentMatcher<Args extends unknown[]> = (received: unknown, ...args: Args) => MatcherResult;
78
78
  type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
79
79
  type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
80
- type QueryAll<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
80
+ type QueryAll<Args extends unknown[]> = (container: Container, ...args: Args) => object[];
81
81
 
82
82
  type BooleanAccessibleState =
83
83
  Gtk.AccessibleState.BUSY |
@@ -195,11 +195,11 @@ type MatcherImplementations = {
195
195
  /** Asserts the widget's accessible role. */
196
196
  toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
197
197
  /** Asserts the widget is, or is an ancestor of, the given widget. */
198
- toContainElement: (received: unknown, descendant: Gtk.Widget | null) => MatcherResult;
198
+ toContainElement: (received: unknown, descendant: Gtk.Accessible | null) => MatcherResult;
199
199
  /** Asserts the widget comes before the given widget in the widget tree, neither containing the other. */
200
- toAppearBefore: (received: unknown, other: Gtk.Widget) => MatcherResult;
200
+ toAppearBefore: (received: unknown, other: Gtk.Accessible) => MatcherResult;
201
201
  /** Asserts the widget comes after the given widget in the widget tree, neither containing the other. */
202
- toAppearAfter: (received: unknown, other: Gtk.Widget) => MatcherResult;
202
+ toAppearAfter: (received: unknown, other: Gtk.Accessible) => MatcherResult;
203
203
  /** Asserts at least one widget under the received one matches the `ByRole` query. */
204
204
  toContainAnyByRole: ContainmentMatcher<RoleQueryArgs>;
205
205
  /** Asserts exactly one widget under the received one matches the `ByRole` query. */
@@ -525,9 +525,6 @@ const isAppearingBefore = (first: Gtk.Widget, second: Gtk.Widget): boolean => {
525
525
  return isSeparateWidget(first, second) && root === getTreeRoot(second) && isPrecedingWidget(root, first, second);
526
526
  };
527
527
 
528
- const camelCase = (name: string): string =>
529
- name.replaceAll(/-([a-z])/g, (_match, letter: string) => letter.toUpperCase());
530
-
531
528
  const isAsymmetricMatcher = (value: unknown): boolean =>
532
529
  typeof value === "object" && value !== null && typeof Reflect.get(value, "asymmetricMatch") === "function";
533
530
 
@@ -588,26 +585,8 @@ function readErrorMessageText(widget: Gtk.Widget): string | null {
588
585
  return texts.length > 0 ? texts.join(" ") : null;
589
586
  }
590
587
 
591
- function assertReadableProperty(object: GObject.Object, name: string, property: string, value: unknown): void {
592
- if (typeof value === "function") {
593
- throw new TypeError(
594
- `toHaveObjectProperty: "${name}" is shadowed by a method of the same name; ` +
595
- `call ${property}() and assert on its result instead`,
596
- );
597
- }
598
-
599
- if (value === undefined && !Reflect.has(object, property)) {
600
- throw new TypeError(
601
- `toHaveObjectProperty: no readable property "${name}" on ${describeObject(object)}; ` +
602
- "construct-only and write-only properties cannot be read back",
603
- );
604
- }
605
- }
606
-
607
588
  function readObjectProperty(object: GObject.Object, name: string): unknown {
608
- const property = camelCase(name);
609
- const value: unknown = Reflect.get(object, property);
610
- assertReadableProperty(object, name, property, value);
589
+ const value: unknown = Reflect.apply(GObject.getProperty, undefined, [object, name]);
611
590
 
612
591
  return value;
613
592
  }
@@ -804,14 +783,14 @@ function toBePartiallyPressed(received: unknown): MatcherResult {
804
783
  return stateResult(widget, "partially pressed", state === MIXED_TRISTATE);
805
784
  }
806
785
 
807
- function toAppearBefore(received: unknown, other: Gtk.Widget): MatcherResult {
786
+ function toAppearBefore(received: unknown, other: Gtk.Accessible): MatcherResult {
808
787
  const widget = asWidget(received, "toAppearBefore");
809
788
  const target = asWidget(other, "toAppearBefore");
810
789
 
811
790
  return orderResult(widget, target, "before", isAppearingBefore(widget, target));
812
791
  }
813
792
 
814
- function toAppearAfter(received: unknown, other: Gtk.Widget): MatcherResult {
793
+ function toAppearAfter(received: unknown, other: Gtk.Accessible): MatcherResult {
815
794
  const widget = asWidget(received, "toAppearAfter");
816
795
  const target = asWidget(other, "toAppearAfter");
817
796
 
@@ -943,15 +922,16 @@ function toHaveRole(received: unknown, expected: Gtk.AccessibleRole): MatcherRes
943
922
  };
944
923
  }
945
924
 
946
- function toContainElement(received: unknown, descendant: Gtk.Widget | null): MatcherResult {
925
+ function toContainElement(received: unknown, descendant: Gtk.Accessible | null): MatcherResult {
947
926
  const widget = asWidget(received, "toContainElement");
948
- const isPass = descendant !== null && (descendant === widget || descendant.isAncestor(widget));
927
+ const target = descendant === null ? null : asWidget(descendant, "toContainElement");
928
+ const isPass = target !== null && (target === widget || target.isAncestor(widget));
949
929
 
950
930
  return {
951
931
  pass: isPass,
952
932
  message: () =>
953
933
  `expected widget ${negationPrefix(isPass)}to contain ` +
954
- `${descendant === null ? "null" : describeWidget(descendant)}\n${describeWidget(widget)}`,
934
+ `${target === null ? "null" : describeWidget(target)}\n${describeWidget(widget)}`,
955
935
  };
956
936
  }
957
937
 
@@ -1020,64 +1000,62 @@ const registerMatchers = (): void => {
1020
1000
  registration.isRegistered = true;
1021
1001
  };
1022
1002
 
1023
- declare module "@vitest/expect" {
1024
- /* eslint-disable @typescript-eslint/consistent-type-definitions -- declaration merging requires interfaces */
1025
- interface WidgetMatchers {
1026
- toHaveDisplayValue(expected?: TextExpectation): void;
1027
- toHaveTextContent(expected?: TextExpectation, options?: TextContentOptions): void;
1028
- toHaveAccessibleName(expected?: TextExpectation): void;
1029
- toHaveAccessibleDescription(expected?: TextExpectation): void;
1030
- toHaveAccessibleErrorMessage(expected?: TextExpectation): void;
1031
- toHaveSelection(expected?: TextExpectation): void;
1003
+ declare module "vitest" {
1004
+ /* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-unused-vars --
1005
+ declaration merging requires an interface with vitest's exact type parameters */
1006
+ interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> {
1007
+ toHaveDisplayValue(expected?: TextExpectation): R;
1008
+ toHaveTextContent(expected?: TextExpectation, options?: TextContentOptions): R;
1009
+ toHaveAccessibleName(expected?: TextExpectation): R;
1010
+ toHaveAccessibleDescription(expected?: TextExpectation): R;
1011
+ toHaveAccessibleErrorMessage(expected?: TextExpectation): R;
1012
+ toHaveSelection(expected?: TextExpectation): R;
1032
1013
  /* eslint-disable-next-line unicorn/consistent-boolean-name -- expected is the jest-dom matcher argument name */
1033
- toHaveAccessibleState(state: BooleanAccessibleState, expected?: boolean): void;
1034
- toHaveAccessibleState(state: TristateAccessibleState, expected?: Gtk.AccessibleTristate): void;
1035
- toHaveAccessibleState(state: Gtk.AccessibleState.INVALID, expected?: Gtk.AccessibleInvalidState): void;
1036
- toHaveAccessibleProperty(property: StringAccessibleProperty, expected?: string): void;
1014
+ toHaveAccessibleState(state: BooleanAccessibleState, expected?: boolean): R;
1015
+ toHaveAccessibleState(state: TristateAccessibleState, expected?: Gtk.AccessibleTristate): R;
1016
+ toHaveAccessibleState(state: Gtk.AccessibleState.INVALID, expected?: Gtk.AccessibleInvalidState): R;
1017
+ toHaveAccessibleProperty(property: StringAccessibleProperty, expected?: string): R;
1037
1018
  /* eslint-disable-next-line unicorn/consistent-boolean-name -- expected is the jest-dom matcher argument name */
1038
- toHaveAccessibleProperty(property: BooleanAccessibleProperty, expected?: boolean): void;
1039
- toHaveAccessibleProperty(property: NumberAccessibleProperty, expected?: number): void;
1019
+ toHaveAccessibleProperty(property: BooleanAccessibleProperty, expected?: boolean): R;
1020
+ toHaveAccessibleProperty(property: NumberAccessibleProperty, expected?: number): R;
1040
1021
  toHaveAccessibleProperty(
1041
1022
  property: Gtk.AccessibleProperty.AUTOCOMPLETE,
1042
1023
  expected?: Gtk.AccessibleAutocomplete,
1043
- ): void;
1044
- toHaveAccessibleProperty(property: Gtk.AccessibleProperty.ORIENTATION, expected?: Gtk.Orientation): void;
1045
- toHaveAccessibleProperty(property: Gtk.AccessibleProperty.SORT, expected?: Gtk.AccessibleSort): void;
1046
- toBeChecked(): void;
1047
- toBePartiallyChecked(): void;
1048
- toBePressed(): void;
1049
- toBePartiallyPressed(): void;
1050
- toBeDisabled(): void;
1051
- toBeEnabled(): void;
1052
- toBeVisible(): void;
1053
- toBeRooted(): void;
1054
- toBeEmptyWidget(): void;
1055
- toBeInvalid(): void;
1056
- toBeValid(): void;
1057
- toBeRequired(): void;
1058
- toHaveFocus(): void;
1059
- toHaveValue(expected?: number | string): void;
1060
- toHaveRole(expected: Gtk.AccessibleRole): void;
1061
- toContainElement(descendant: Gtk.Widget | null): void;
1062
- toAppearBefore(other: Gtk.Widget): void;
1063
- toAppearAfter(other: Gtk.Widget): void;
1064
- toContainAnyByRole(...args: RoleQueryArgs): void;
1065
- toContainOneByRole(...args: RoleQueryArgs): void;
1066
- toContainAnyByText(...args: TextQueryArgs): void;
1067
- toContainOneByText(...args: TextQueryArgs): void;
1068
- toContainAnyByLabelText(...args: TextQueryArgs): void;
1069
- toContainOneByLabelText(...args: TextQueryArgs): void;
1070
- toContainAnyByPlaceholderText(...args: TextQueryArgs): void;
1071
- toContainOneByPlaceholderText(...args: TextQueryArgs): void;
1072
- toContainAnyByDisplayValue(...args: TextQueryArgs): void;
1073
- toContainOneByDisplayValue(...args: TextQueryArgs): void;
1074
- toHaveClass(...args: (ClassExpectation | { exact: boolean })[]): void;
1075
- toHaveObjectProperty(name: string, expected?: unknown): void;
1024
+ ): R;
1025
+ toHaveAccessibleProperty(property: Gtk.AccessibleProperty.ORIENTATION, expected?: Gtk.Orientation): R;
1026
+ toHaveAccessibleProperty(property: Gtk.AccessibleProperty.SORT, expected?: Gtk.AccessibleSort): R;
1027
+ toBeChecked(): R;
1028
+ toBePartiallyChecked(): R;
1029
+ toBePressed(): R;
1030
+ toBePartiallyPressed(): R;
1031
+ toBeDisabled(): R;
1032
+ toBeEnabled(): R;
1033
+ toBeVisible(): R;
1034
+ toBeRooted(): R;
1035
+ toBeEmptyWidget(): R;
1036
+ toBeInvalid(): R;
1037
+ toBeValid(): R;
1038
+ toBeRequired(): R;
1039
+ toHaveFocus(): R;
1040
+ toHaveValue(expected?: number | string): R;
1041
+ toHaveRole(expected: Gtk.AccessibleRole): R;
1042
+ toContainElement(descendant: Gtk.Accessible | null): R;
1043
+ toAppearBefore(other: Gtk.Accessible): R;
1044
+ toAppearAfter(other: Gtk.Accessible): R;
1045
+ toContainAnyByRole(...args: RoleQueryArgs): R;
1046
+ toContainOneByRole(...args: RoleQueryArgs): R;
1047
+ toContainAnyByText(...args: TextQueryArgs): R;
1048
+ toContainOneByText(...args: TextQueryArgs): R;
1049
+ toContainAnyByLabelText(...args: TextQueryArgs): R;
1050
+ toContainOneByLabelText(...args: TextQueryArgs): R;
1051
+ toContainAnyByPlaceholderText(...args: TextQueryArgs): R;
1052
+ toContainOneByPlaceholderText(...args: TextQueryArgs): R;
1053
+ toContainAnyByDisplayValue(...args: TextQueryArgs): R;
1054
+ toContainOneByDisplayValue(...args: TextQueryArgs): R;
1055
+ toHaveClass(...args: (ClassExpectation | { exact: boolean })[]): R;
1056
+ toHaveObjectProperty(name: string, expected?: unknown): R;
1076
1057
  }
1077
-
1078
- interface Assertion extends WidgetMatchers {}
1079
- interface AsymmetricMatchersContaining extends WidgetMatchers {}
1080
- /* eslint-enable @typescript-eslint/consistent-type-definitions */
1058
+ /* eslint-enable @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-unused-vars */
1081
1059
  }
1082
1060
 
1083
1061
  export { matchers, registerMatchers, type ClassExpectation, type TextContentOptions, type TextExpectation };
package/src/queries.ts CHANGED
@@ -12,6 +12,7 @@ import { multipleFoundError, notFoundError } from "./errors.js";
12
12
  import { getDefaultNormalizer } from "./normalize.js";
13
13
  import { type Container, findAll, traverse } from "./traversal.js";
14
14
  import {
15
+ buildInaccessibilityCheck,
15
16
  getWidgetAccessibleName,
16
17
  getWidgetBusyState,
17
18
  getWidgetDescription,
@@ -26,7 +27,6 @@ import {
26
27
  getWidgetPressedState,
27
28
  getWidgetSelectedState,
28
29
  getWidgetValueText,
29
- isInaccessible,
30
30
  isWidgetChecked,
31
31
  isWidgetValueMatch,
32
32
  namingLabelText,
@@ -39,7 +39,7 @@ type BuiltinQueries = QueryFamilies<[container: Container]>;
39
39
  const roleQueries = nameQueryFamily(
40
40
  "Role",
41
41
  queryAllByRole,
42
- buildQueries<[role: Gtk.AccessibleRole, options?: ByRoleOptions]>(
42
+ buildQueries<[role: Gtk.AccessibleRole, options?: ByRoleOptions<Gtk.Widget>], Gtk.Widget>(
43
43
  queryAllByRole,
44
44
  (container, matches, role, options) =>
45
45
  multipleFoundError(container, { queryType: "role", role, options }, matches),
@@ -50,7 +50,7 @@ const roleQueries = nameQueryFamily(
50
50
  const labelTextQueries = nameQueryFamily(
51
51
  "LabelText",
52
52
  queryAllByLabelText,
53
- buildQueries<[text: Matcher, options?: MatcherOptions]>(
53
+ buildQueries<[text: Matcher, options?: MatcherOptions<Gtk.Widget>], Gtk.Widget>(
54
54
  queryAllByLabelText,
55
55
  (container, matches, text) => multipleFoundError(container, { queryType: "labelText", text }, matches),
56
56
  (container, text) => notFoundError(container, { queryType: "labelText", text }),
@@ -60,7 +60,7 @@ const labelTextQueries = nameQueryFamily(
60
60
  const textQueries = nameQueryFamily(
61
61
  "Text",
62
62
  queryAllByText,
63
- buildQueries<[text: Matcher, options?: MatcherOptions]>(
63
+ buildQueries<[text: Matcher, options?: MatcherOptions<Gtk.Widget>], Gtk.Widget>(
64
64
  queryAllByText,
65
65
  (container, matches, text) => multipleFoundError(container, { queryType: "text", text }, matches),
66
66
  (container, text) => notFoundError(container, { queryType: "text", text }),
@@ -70,7 +70,7 @@ const textQueries = nameQueryFamily(
70
70
  const nameQueries = nameQueryFamily(
71
71
  "Name",
72
72
  queryAllByName,
73
- buildQueries<[name: Matcher, options?: MatcherOptions]>(
73
+ buildQueries<[name: Matcher, options?: MatcherOptions<Gtk.Widget>], Gtk.Widget>(
74
74
  queryAllByName,
75
75
  (container, matches, name) => multipleFoundError(container, { queryType: "name", name }, matches),
76
76
  (container, name) => notFoundError(container, { queryType: "name", name }),
@@ -80,7 +80,7 @@ const nameQueries = nameQueryFamily(
80
80
  const placeholderTextQueries = nameQueryFamily(
81
81
  "PlaceholderText",
82
82
  queryAllByPlaceholderText,
83
- buildQueries<[text: Matcher, options?: MatcherOptions]>(
83
+ buildQueries<[text: Matcher, options?: MatcherOptions<Gtk.Widget>], Gtk.Widget>(
84
84
  queryAllByPlaceholderText,
85
85
  (container, matches, text) => multipleFoundError(container, { queryType: "placeholderText", text }, matches),
86
86
  (container, text) => notFoundError(container, { queryType: "placeholderText", text }),
@@ -90,7 +90,7 @@ const placeholderTextQueries = nameQueryFamily(
90
90
  const displayValueQueries = nameQueryFamily(
91
91
  "DisplayValue",
92
92
  queryAllByDisplayValue,
93
- buildQueries<[value: Matcher, options?: MatcherOptions]>(
93
+ buildQueries<[value: Matcher, options?: MatcherOptions<Gtk.Widget>], Gtk.Widget>(
94
94
  queryAllByDisplayValue,
95
95
  (container, matches, value) => multipleFoundError(container, { queryType: "displayValue", value }, matches),
96
96
  (container, value) => notFoundError(container, { queryType: "displayValue", value }),
@@ -284,25 +284,27 @@ const hasMatchingAccessibleStates = (widget: Gtk.Widget, options: ByRoleOptions)
284
284
  hasMatchingDescriptionState(widget, options) &&
285
285
  hasMatchingValueState(widget, options);
286
286
 
287
- const isMatchingWidgetType = (widget: Gtk.Widget, options?: MatcherOptions): boolean =>
287
+ const isMatchingWidgetType = <T extends Gtk.Accessible>(
288
+ widget: Gtk.Widget,
289
+ options?: MatcherOptions<T>,
290
+ ): widget is Gtk.Widget & T =>
288
291
  options?.as === undefined || widget instanceof options.as;
289
292
 
290
- const hasMatchingByRoleOptions = (widget: Gtk.Widget, options?: ByRoleOptions): boolean => {
293
+ const hasMatchingAccessibleOptions = (widget: Gtk.Widget, options?: ByRoleOptions): boolean => {
291
294
  if (!options) {
292
295
  return true;
293
296
  }
294
297
 
295
298
  return (
296
- isMatchingWidgetType(widget, options) &&
297
299
  hasMatchingAccessibleName(widget, options) &&
298
300
  hasMatchingAccessibleStates(widget, options)
299
301
  );
300
302
  };
301
303
 
302
- function nameQueryFamily<Args extends unknown[]>(
304
+ function nameQueryFamily<Args extends unknown[], Element extends Gtk.Accessible>(
303
305
  suffix: string,
304
- queryAllBy: QueryAllBy<Args>,
305
- built: BuiltQueries<Args>,
306
+ queryAllBy: QueryAllBy<Args, Element>,
307
+ built: BuiltQueries<Args, Element>,
306
308
  ): Record<string, unknown> {
307
309
  const [queryBy, getAllBy, getBy, findAllBy, findBy] = built;
308
310
 
@@ -325,22 +327,30 @@ function nameQueryFamily<Args extends unknown[]>(
325
327
  * @param options Additional accessible name, state, and value constraints.
326
328
  * @returns Every matching widget, or an empty array when none match.
327
329
  */
328
- function queryAllByRole(
330
+ function queryAllByRole<T extends Gtk.Accessible = Gtk.Widget>(
329
331
  container: Container,
330
332
  role: Gtk.AccessibleRole,
331
- options?: ByRoleOptions,
332
- ): Gtk.Widget[] {
333
- return findAll(container, (widget) => {
333
+ options?: ByRoleOptions<T>,
334
+ ): T[] {
335
+ const isInaccessible = buildInaccessibilityCheck();
336
+
337
+ const matches = findAll(container, (widget) => {
334
338
  if (widget.getAccessibleRole() !== role) {
335
339
  return false;
336
340
  }
337
341
 
342
+ if (!isMatchingWidgetType(widget, options)) {
343
+ return false;
344
+ }
345
+
338
346
  if (!options?.hidden && isInaccessible(widget)) {
339
347
  return false;
340
348
  }
341
349
 
342
- return hasMatchingByRoleOptions(widget, options);
350
+ return hasMatchingAccessibleOptions(widget, options);
343
351
  });
352
+
353
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
344
354
  }
345
355
 
346
356
  const collectMnemonicMatch = (
@@ -394,14 +404,18 @@ const collectLabelMatches = (
394
404
  * @param options Text matching options.
395
405
  * @returns Every matching widget, or an empty array when none match.
396
406
  */
397
- function queryAllByLabelText(container: Container, text: Matcher, options?: MatcherOptions): Gtk.Widget[] {
407
+ function queryAllByLabelText<T extends Gtk.Accessible = Gtk.Widget>(
408
+ container: Container,
409
+ text: Matcher,
410
+ options?: MatcherOptions<T>,
411
+ ): T[] {
398
412
  const results: Set<Gtk.Widget> = new Set();
399
413
 
400
414
  for (const widget of traverse(container)) {
401
415
  collectLabelMatches(results, widget, text, options);
402
416
  }
403
417
 
404
- return [...results].filter((widget) => isMatchingWidgetType(widget, options));
418
+ return [...results].filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
405
419
  }
406
420
 
407
421
  /**
@@ -411,12 +425,18 @@ function queryAllByLabelText(container: Container, text: Matcher, options?: Matc
411
425
  * @param options Text matching options.
412
426
  * @returns Every matching widget, or an empty array when none match.
413
427
  */
414
- function queryAllByText(container: Container, text: Matcher, options?: MatcherOptions): Gtk.Widget[] {
415
- return findAll(
428
+ function queryAllByText<T extends Gtk.Accessible = Gtk.Widget>(
429
+ container: Container,
430
+ text: Matcher,
431
+ options?: MatcherOptions<T>,
432
+ ): T[] {
433
+ const matches = findAll(
416
434
  container,
417
435
  (widget) =>
418
436
  isMatchingWidgetType(widget, options) && isTextMatch(getWidgetLabelText(widget), text, widget, options),
419
437
  );
438
+
439
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
420
440
  }
421
441
 
422
442
  /**
@@ -426,11 +446,17 @@ function queryAllByText(container: Container, text: Matcher, options?: MatcherOp
426
446
  * @param options Text matching options.
427
447
  * @returns Every matching widget, or an empty array when none match.
428
448
  */
429
- function queryAllByName(container: Container, name: Matcher, options?: MatcherOptions): Gtk.Widget[] {
430
- return findAll(
449
+ function queryAllByName<T extends Gtk.Accessible = Gtk.Widget>(
450
+ container: Container,
451
+ name: Matcher,
452
+ options?: MatcherOptions<T>,
453
+ ): T[] {
454
+ const matches = findAll(
431
455
  container,
432
456
  (widget) => isMatchingWidgetType(widget, options) && isTextMatch(getWidgetName(widget), name, widget, options),
433
457
  );
458
+
459
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
434
460
  }
435
461
 
436
462
  /**
@@ -440,17 +466,19 @@ function queryAllByName(container: Container, name: Matcher, options?: MatcherOp
440
466
  * @param options Text matching options.
441
467
  * @returns Every matching widget, or an empty array when none match.
442
468
  */
443
- function queryAllByPlaceholderText(
469
+ function queryAllByPlaceholderText<T extends Gtk.Accessible = Gtk.Widget>(
444
470
  container: Container,
445
471
  text: Matcher,
446
- options?: MatcherOptions,
447
- ): Gtk.Widget[] {
448
- return findAll(
472
+ options?: MatcherOptions<T>,
473
+ ): T[] {
474
+ const matches = findAll(
449
475
  container,
450
476
  (widget) =>
451
477
  isMatchingWidgetType(widget, options) &&
452
478
  isTextMatch(getWidgetPlaceholderText(widget), text, widget, options),
453
479
  );
480
+
481
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
454
482
  }
455
483
 
456
484
  /**
@@ -460,13 +488,19 @@ function queryAllByPlaceholderText(
460
488
  * @param options Text matching options.
461
489
  * @returns Every matching widget, or an empty array when none match.
462
490
  */
463
- function queryAllByDisplayValue(container: Container, value: Matcher, options?: MatcherOptions): Gtk.Widget[] {
464
- return findAll(
491
+ function queryAllByDisplayValue<T extends Gtk.Accessible = Gtk.Widget>(
492
+ container: Container,
493
+ value: Matcher,
494
+ options?: MatcherOptions<T>,
495
+ ): T[] {
496
+ const matches = findAll(
465
497
  container,
466
498
  (widget) =>
467
499
  isMatchingWidgetType(widget, options) &&
468
500
  isTextMatch(getWidgetDisplayValue(widget), value, widget, options),
469
501
  );
502
+
503
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
470
504
  }
471
505
 
472
506
  export {
@@ -1,5 +1,5 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
- import { camelCase } from "@gtkx/utils";
2
+ import { getProperty } from "@gtkx/gi/gobject";
3
3
  import type { Matcher, MatcherOptions } from "./types.js";
4
4
  import { buildQueries } from "./build-queries.js";
5
5
  import { getElementError } from "./errors.js";
@@ -27,7 +27,13 @@ const queryHelpers: QueryHelpers = {
27
27
  };
28
28
 
29
29
  const readObjectProperty = (widget: Gtk.Widget, property: string): string | null => {
30
- const value: unknown = Reflect.get(widget, camelCase(property));
30
+ let value: unknown;
31
+
32
+ try {
33
+ value = Reflect.apply(getProperty, undefined, [widget, property]);
34
+ } catch {
35
+ return null;
36
+ }
31
37
 
32
38
  if (typeof value === "string") {
33
39
  return value;
@@ -51,18 +57,20 @@ const readObjectProperty = (widget: Gtk.Widget, property: string): string | null
51
57
  * @param options Text matching options.
52
58
  * @returns Every matching widget, or an empty array when none match.
53
59
  */
54
- function queryAllByObjectProperty(
60
+ function queryAllByObjectProperty<T extends Gtk.Accessible = Gtk.Widget>(
55
61
  property: string,
56
62
  container: Container,
57
63
  text: Matcher,
58
- options?: MatcherOptions,
59
- ): Gtk.Widget[] {
60
- return findAll(
64
+ options?: MatcherOptions<T>,
65
+ ): T[] {
66
+ const matches = findAll(
61
67
  container,
62
68
  (widget) =>
63
69
  isMatchingWidgetType(widget, options) &&
64
70
  isTextMatch(readObjectProperty(widget, property), text, widget, options),
65
71
  );
72
+
73
+ return matches.filter((widget): widget is Gtk.Widget & T => isMatchingWidgetType(widget, options));
66
74
  }
67
75
 
68
76
  /**
@@ -76,12 +84,12 @@ function queryAllByObjectProperty(
76
84
  * @returns The matching widget, or null when none match.
77
85
  * @throws When more than one widget matches.
78
86
  */
79
- function queryByObjectProperty(
87
+ function queryByObjectProperty<T extends Gtk.Accessible = Gtk.Widget>(
80
88
  property: string,
81
89
  container: Container,
82
90
  text: Matcher,
83
- options?: MatcherOptions,
84
- ): Gtk.Widget | null {
91
+ options?: MatcherOptions<T>,
92
+ ): T | null {
85
93
  const matches = queryAllByObjectProperty(property, container, text, options);
86
94
 
87
95
  if (matches.length > 1) {
package/src/render.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type { RootElement } from "@gtkx/react";
1
+ import type { CaughtErrorInfo, RootElement } from "@gtkx/react";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
3
  import {
4
4
  createReconcilerRoot,
@@ -21,6 +21,7 @@ import { clearScreen, setScreen } from "./screen.js";
21
21
  import { captureScreen } from "./screenshot.js";
22
22
  import { type Container, roots, TOPLEVELS } from "./traversal.js";
23
23
  import { resetClipboard } from "./user-event/index.js";
24
+ import { requireWidget } from "./widget-target.js";
24
25
  import { findPresentedWindowFailure, findRenderedWindowFailure, mappedToplevels } from "./window-state.js";
25
26
  import { within } from "./within.js";
26
27
 
@@ -125,6 +126,10 @@ const resolveContainer = (container: RenderOptions["container"]): ResolvedContai
125
126
  return { containerInfo: container, window: null };
126
127
  }
127
128
 
129
+ if (container !== undefined) {
130
+ requireWidget(container);
131
+ }
132
+
128
133
  const window = createHarnessWindow();
129
134
  window.setDecorated(false);
130
135
 
@@ -188,7 +193,7 @@ const settleRender = async (
188
193
 
189
194
  const renderErrorHandlers = <Q extends QueryMap>(options: RenderOptions<Q> | undefined) => ({
190
195
  onUncaughtError: handleError,
191
- onCaughtError: (error: unknown, errorInfo: ErrorInfo): void => {
196
+ onCaughtError: (error: unknown, errorInfo: CaughtErrorInfo): void => {
192
197
  handleError(error);
193
198
  options?.onCaughtError?.(error, errorInfo);
194
199
  },