@gtkx/testing 1.0.0-rc.4 → 1.0.0

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 (182) hide show
  1. package/README.md +5 -5
  2. package/dist/accessible-native.d.ts +16 -0
  3. package/dist/accessible-native.d.ts.map +1 -0
  4. package/dist/accessible-native.js +206 -0
  5. package/dist/accessible-native.js.map +1 -0
  6. package/dist/build-queries.d.ts +27 -10
  7. package/dist/build-queries.d.ts.map +1 -1
  8. package/dist/build-queries.js +22 -24
  9. package/dist/build-queries.js.map +1 -1
  10. package/dist/cleanup-registry.d.ts +1 -1
  11. package/dist/cleanup-registry.d.ts.map +1 -1
  12. package/dist/cleanup-registry.js.map +1 -1
  13. package/dist/errors.d.ts +10 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +10 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/fire-event.d.ts +35 -6
  18. package/dist/fire-event.d.ts.map +1 -1
  19. package/dist/fire-event.js +23 -7
  20. package/dist/fire-event.js.map +1 -1
  21. package/dist/frame-sync.d.ts +2 -1
  22. package/dist/frame-sync.d.ts.map +1 -1
  23. package/dist/frame-sync.js +15 -5
  24. package/dist/frame-sync.js.map +1 -1
  25. package/dist/harness-window.d.ts +4 -0
  26. package/dist/harness-window.d.ts.map +1 -0
  27. package/dist/harness-window.js +6 -0
  28. package/dist/harness-window.js.map +1 -0
  29. package/dist/index.d.ts +14 -7
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +9 -5
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal.d.ts +4 -0
  34. package/dist/internal.d.ts.map +1 -0
  35. package/dist/internal.js +4 -0
  36. package/dist/internal.js.map +1 -0
  37. package/dist/matchers.d.ts +88 -46
  38. package/dist/matchers.d.ts.map +1 -1
  39. package/dist/matchers.js +156 -18
  40. package/dist/matchers.js.map +1 -1
  41. package/dist/pretty-format.d.ts +2 -0
  42. package/dist/pretty-format.d.ts.map +1 -0
  43. package/dist/pretty-format.js +2 -0
  44. package/dist/pretty-format.js.map +1 -0
  45. package/dist/pretty-widget.d.ts +5 -2
  46. package/dist/pretty-widget.d.ts.map +1 -1
  47. package/dist/pretty-widget.js +73 -56
  48. package/dist/pretty-widget.js.map +1 -1
  49. package/dist/production.d.ts +8 -0
  50. package/dist/production.d.ts.map +1 -0
  51. package/dist/production.js +34 -0
  52. package/dist/production.js.map +1 -0
  53. package/dist/queries.d.ts +98 -56
  54. package/dist/queries.d.ts.map +1 -1
  55. package/dist/queries.js +123 -83
  56. package/dist/queries.js.map +1 -1
  57. package/dist/query-helpers.d.ts +44 -0
  58. package/dist/query-helpers.d.ts.map +1 -0
  59. package/dist/query-helpers.js +57 -0
  60. package/dist/query-helpers.js.map +1 -0
  61. package/dist/render-children.d.ts +9 -0
  62. package/dist/render-children.d.ts.map +1 -0
  63. package/dist/render-children.js +11 -0
  64. package/dist/render-children.js.map +1 -0
  65. package/dist/render.d.ts.map +1 -1
  66. package/dist/render.js +11 -9
  67. package/dist/render.js.map +1 -1
  68. package/dist/role-helpers.d.ts +12 -3
  69. package/dist/role-helpers.d.ts.map +1 -1
  70. package/dist/role-helpers.js +13 -4
  71. package/dist/role-helpers.js.map +1 -1
  72. package/dist/role-naming.d.ts +5 -0
  73. package/dist/role-naming.d.ts.map +1 -0
  74. package/dist/role-naming.js +96 -0
  75. package/dist/role-naming.js.map +1 -0
  76. package/dist/screenshot.d.ts +12 -20
  77. package/dist/screenshot.d.ts.map +1 -1
  78. package/dist/screenshot.js +183 -91
  79. package/dist/screenshot.js.map +1 -1
  80. package/dist/traversal.d.ts +4 -1
  81. package/dist/traversal.d.ts.map +1 -1
  82. package/dist/traversal.js +25 -6
  83. package/dist/traversal.js.map +1 -1
  84. package/dist/types.d.ts +26 -14
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js.map +1 -1
  87. package/dist/user-event/adjustment.d.ts +3 -0
  88. package/dist/user-event/adjustment.d.ts.map +1 -1
  89. package/dist/user-event/adjustment.js +23 -2
  90. package/dist/user-event/adjustment.js.map +1 -1
  91. package/dist/user-event/click.d.ts +43 -8
  92. package/dist/user-event/click.d.ts.map +1 -1
  93. package/dist/user-event/click.js +175 -44
  94. package/dist/user-event/click.js.map +1 -1
  95. package/dist/user-event/column-header-click.d.ts +4 -0
  96. package/dist/user-event/column-header-click.d.ts.map +1 -0
  97. package/dist/user-event/column-header-click.js +65 -0
  98. package/dist/user-event/column-header-click.js.map +1 -0
  99. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  100. package/dist/user-event/event-wrapper.js +2 -1
  101. package/dist/user-event/event-wrapper.js.map +1 -1
  102. package/dist/user-event/index.d.ts +51 -10
  103. package/dist/user-event/index.d.ts.map +1 -1
  104. package/dist/user-event/index.js +43 -26
  105. package/dist/user-event/index.js.map +1 -1
  106. package/dist/user-event/indexed-children.d.ts +10 -0
  107. package/dist/user-event/indexed-children.d.ts.map +1 -0
  108. package/dist/user-event/indexed-children.js +50 -0
  109. package/dist/user-event/indexed-children.js.map +1 -0
  110. package/dist/user-event/keyboard.d.ts.map +1 -1
  111. package/dist/user-event/keyboard.js +33 -4
  112. package/dist/user-event/keyboard.js.map +1 -1
  113. package/dist/user-event/list-row-click.d.ts +4 -0
  114. package/dist/user-event/list-row-click.d.ts.map +1 -0
  115. package/dist/user-event/list-row-click.js +39 -0
  116. package/dist/user-event/list-row-click.js.map +1 -0
  117. package/dist/user-event/native-click.d.ts +6 -0
  118. package/dist/user-event/native-click.d.ts.map +1 -0
  119. package/dist/user-event/native-click.js +32 -0
  120. package/dist/user-event/native-click.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +1 -1
  122. package/dist/user-event/pointer.d.ts.map +1 -1
  123. package/dist/user-event/pointer.js +33 -9
  124. package/dist/user-event/pointer.js.map +1 -1
  125. package/dist/user-event/selection.d.ts +11 -8
  126. package/dist/user-event/selection.d.ts.map +1 -1
  127. package/dist/user-event/selection.js +49 -38
  128. package/dist/user-event/selection.js.map +1 -1
  129. package/dist/user-event/text.d.ts +6 -2
  130. package/dist/user-event/text.d.ts.map +1 -1
  131. package/dist/user-event/text.js +68 -18
  132. package/dist/user-event/text.js.map +1 -1
  133. package/dist/widget-accessible-properties.d.ts +14 -26
  134. package/dist/widget-accessible-properties.d.ts.map +1 -1
  135. package/dist/widget-accessible-properties.js +135 -190
  136. package/dist/widget-accessible-properties.js.map +1 -1
  137. package/dist/widget-getters.d.ts +8 -0
  138. package/dist/widget-getters.d.ts.map +1 -0
  139. package/dist/widget-getters.js +19 -0
  140. package/dist/widget-getters.js.map +1 -0
  141. package/dist/within.d.ts +3 -2
  142. package/dist/within.d.ts.map +1 -1
  143. package/dist/within.js +3 -2
  144. package/dist/within.js.map +1 -1
  145. package/package.json +12 -7
  146. package/src/accessible-native.ts +333 -0
  147. package/src/build-queries.ts +47 -41
  148. package/src/cleanup-registry.ts +1 -1
  149. package/src/errors.ts +12 -1
  150. package/src/fire-event.ts +61 -8
  151. package/src/frame-sync.ts +21 -5
  152. package/src/harness-window.ts +9 -0
  153. package/src/index.ts +18 -11
  154. package/src/internal.ts +11 -0
  155. package/src/matchers.ts +428 -70
  156. package/src/pretty-format.ts +1 -0
  157. package/src/pretty-widget.ts +97 -70
  158. package/src/production.ts +52 -0
  159. package/src/queries.ts +127 -89
  160. package/src/query-helpers.ts +97 -0
  161. package/src/render-children.ts +25 -0
  162. package/src/render.tsx +16 -11
  163. package/src/role-helpers.ts +14 -4
  164. package/src/role-naming.ts +99 -0
  165. package/src/screenshot.ts +272 -103
  166. package/src/traversal.ts +44 -6
  167. package/src/types.ts +28 -16
  168. package/src/user-event/adjustment.ts +29 -2
  169. package/src/user-event/click.ts +247 -52
  170. package/src/user-event/column-header-click.ts +90 -0
  171. package/src/user-event/event-wrapper.ts +3 -1
  172. package/src/user-event/index.ts +100 -37
  173. package/src/user-event/indexed-children.ts +75 -0
  174. package/src/user-event/keyboard.ts +52 -4
  175. package/src/user-event/list-row-click.ts +57 -0
  176. package/src/user-event/native-click.ts +46 -0
  177. package/src/user-event/pointer.ts +48 -9
  178. package/src/user-event/selection.ts +67 -41
  179. package/src/user-event/text.ts +92 -21
  180. package/src/widget-accessible-properties.ts +183 -230
  181. package/src/widget-getters.ts +32 -0
  182. package/src/within.ts +3 -2
package/src/matchers.ts CHANGED
@@ -1,8 +1,25 @@
1
- /// <reference types="@vitest/expect" />
1
+ import type { SyncExpectationResult } from "@vitest/expect";
2
2
  import * as GObject from "@gtkx/gi/gobject";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
+ import type { ByRoleOptions, Matcher, MatcherOptions } from "./types.js";
5
+ import {
6
+ ACCESSIBLE_NUMBER_TOLERANCE,
7
+ type AccessibleAttributeValue,
8
+ isAccessibleNumberMatch,
9
+ isAccessibleNumberProperty,
10
+ readAccessibleProperty,
11
+ readAccessibleState,
12
+ } from "./accessible-native.js";
4
13
  import { getDefaultNormalizer } from "./normalize.js";
14
+ import {
15
+ queryAllByDisplayValue,
16
+ queryAllByLabelText,
17
+ queryAllByPlaceholderText,
18
+ queryAllByRole,
19
+ queryAllByText,
20
+ } from "./queries.js";
5
21
  import { formatRole } from "./role-helpers.js";
22
+ import { type Container, descendants } from "./traversal.js";
6
23
  import {
7
24
  type CheckedState,
8
25
  getWidgetAccessibleName,
@@ -10,18 +27,16 @@ import {
10
27
  getWidgetDescribedByText,
11
28
  getWidgetDisplayValue,
12
29
  getWidgetErrorMessage,
13
- getWidgetExpandedState,
14
30
  getWidgetInvalidState,
15
31
  getWidgetLabelText,
16
- getWidgetPlaceholderText,
17
32
  getWidgetPressedState,
18
33
  getWidgetRequiredState,
19
- getWidgetSelectedState,
20
34
  getWidgetSelection,
21
35
  getWidgetTextContent,
22
- getWidgetValue,
36
+ getWidgetValueNow,
23
37
  hasDisplayValue,
24
38
  isWidgetDisabled,
39
+ isWidgetValueMatch,
25
40
  isWidgetVisible,
26
41
  } from "./widget-accessible-properties.js";
27
42
 
@@ -40,15 +55,60 @@ type TextContentOptions = {
40
55
  /** The expected value for a style class: an exact class name or a regular expression. */
41
56
  type ClassExpectation = string | RegExp;
42
57
  /** The outcome of a matcher: whether it passed, and the failure text built on demand. */
43
- type MatcherResult = { pass: boolean; message: () => string };
58
+ type MatcherResult = Pick<SyncExpectationResult, "message" | "pass">;
59
+
44
60
  /** The matcher state bound as `this`, supplying the test runner's deep equality check. */
45
- type MatcherContext = { equals: (actual: unknown, expected: unknown) => boolean };
61
+ type MatcherContext = {
62
+ /** Compares a received value against an expected one, resolving asymmetric matchers. */
63
+ equals: (actual: unknown, expected: unknown) => boolean;
64
+ };
65
+
46
66
  /** Compares text read from the received widget; with no expected value, asserts the text is non-empty. */
47
67
  type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
48
68
  /** Asserts a single state of the received widget, taking no expected value. */
49
69
  type StateMatcher = (received: unknown) => MatcherResult;
70
+ /** Trailing arguments of the `ByRole` query family, as a containment matcher takes them. */
71
+ type RoleQueryArgs = [role: Gtk.AccessibleRole, options?: ByRoleOptions];
72
+ /** Trailing arguments of the text-based query families, as a containment matcher takes them. */
73
+ type TextQueryArgs = [text: Matcher, options?: MatcherOptions];
74
+ /** Counts the descendants a query family finds under the received widget. */
75
+ type ContainmentMatcher<Args extends unknown[]> = (received: unknown, ...args: Args) => MatcherResult;
50
76
  type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
51
77
  type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
78
+ type QueryAll<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
79
+
80
+ type BooleanAccessibleState =
81
+ Gtk.AccessibleState.BUSY |
82
+ Gtk.AccessibleState.DISABLED |
83
+ Gtk.AccessibleState.EXPANDED |
84
+ Gtk.AccessibleState.HIDDEN |
85
+ Gtk.AccessibleState.SELECTED |
86
+ Gtk.AccessibleState.VISITED;
87
+
88
+ type TristateAccessibleState = Gtk.AccessibleState.CHECKED | Gtk.AccessibleState.PRESSED;
89
+
90
+ type StringAccessibleProperty =
91
+ Gtk.AccessibleProperty.DESCRIPTION |
92
+ Gtk.AccessibleProperty.HELP_TEXT |
93
+ Gtk.AccessibleProperty.KEY_SHORTCUTS |
94
+ Gtk.AccessibleProperty.LABEL |
95
+ Gtk.AccessibleProperty.PLACEHOLDER |
96
+ Gtk.AccessibleProperty.ROLE_DESCRIPTION |
97
+ Gtk.AccessibleProperty.VALUE_TEXT;
98
+
99
+ type BooleanAccessibleProperty =
100
+ Gtk.AccessibleProperty.HAS_POPUP |
101
+ Gtk.AccessibleProperty.MODAL |
102
+ Gtk.AccessibleProperty.MULTI_LINE |
103
+ Gtk.AccessibleProperty.MULTI_SELECTABLE |
104
+ Gtk.AccessibleProperty.READ_ONLY |
105
+ Gtk.AccessibleProperty.REQUIRED;
106
+
107
+ type NumberAccessibleProperty =
108
+ Gtk.AccessibleProperty.LEVEL |
109
+ Gtk.AccessibleProperty.VALUE_MAX |
110
+ Gtk.AccessibleProperty.VALUE_MIN |
111
+ Gtk.AccessibleProperty.VALUE_NOW;
52
112
 
53
113
  /** A text matcher that also takes normalization options for the text it reads. */
54
114
  type TextContentMatcher = (
@@ -72,23 +132,35 @@ type MatcherImplementations = {
72
132
  toHaveAccessibleDescription: TextMatcher;
73
133
  /** Asserts the joined accessible names of the widget's `error-message` targets, read only while it is invalid. */
74
134
  toHaveAccessibleErrorMessage: TextMatcher;
75
- /** Asserts the widget's placeholder text; with no argument, that it has some. */
76
- toHavePlaceholderText: TextMatcher;
77
135
  /** Asserts the text currently selected in an editable widget. */
78
136
  toHaveSelection: TextMatcher;
137
+ /**
138
+ * Asserts an accessible state GTK holds for the widget, taking the value type that state carries.
139
+ * With no expected value, asserts only that the state is set to something determinate.
140
+ */
141
+ toHaveAccessibleState: (
142
+ received: unknown,
143
+ state: Gtk.AccessibleState,
144
+ expected?: boolean | number,
145
+ ) => MatcherResult;
146
+ /**
147
+ * Asserts an accessible property GTK holds for the widget, taking the value type that property
148
+ * carries. With no expected value, asserts only that the property is set. Numeric properties
149
+ * compare the way `toHaveValue` and the `value` option of the `ByRole` queries do, within 0.001.
150
+ */
151
+ toHaveAccessibleProperty: (
152
+ received: unknown,
153
+ property: Gtk.AccessibleProperty,
154
+ expected?: AccessibleAttributeValue,
155
+ ) => MatcherResult;
79
156
  /** Asserts the widget's checked state is checked rather than unchecked or mixed. */
80
157
  toBeChecked: StateMatcher;
81
158
  /** Asserts the widget's checked state is mixed. */
82
159
  toBePartiallyChecked: StateMatcher;
83
160
  /** Asserts a `Gtk.ToggleButton` is active, and throws for any other widget. */
84
161
  toBePressed: StateMatcher;
85
- /** Asserts a `Gtk.Expander` or the row behind a `Gtk.TreeExpander` is expanded, and throws for anything else. */
86
- toBeExpanded: StateMatcher;
87
- /**
88
- * Asserts a `Gtk.ListBoxRow` or `Gtk.FlowBoxChild` is selected, or that a widget in a selectable role
89
- * carries the selected state flag, and throws for anything else.
90
- */
91
- toBeSelected: StateMatcher;
162
+ /** Asserts the widget's accessible pressed state is mixed, and throws when it exposes none. */
163
+ toBePartiallyPressed: StateMatcher;
92
164
  /** Asserts the widget is insensitive, or sits inside an insensitive ancestor. */
93
165
  toBeDisabled: StateMatcher;
94
166
  /** Asserts the widget and all of its ancestors are sensitive. */
@@ -98,7 +170,7 @@ type MatcherImplementations = {
98
170
  /** Asserts the widget's root is a window that is still in the toplevel list. */
99
171
  toBeRooted: StateMatcher;
100
172
  /** Asserts the widget has neither a child nor label text. */
101
- toBeEmpty: StateMatcher;
173
+ toBeEmptyWidget: StateMatcher;
102
174
  /** Asserts the widget's accessible invalid state is set to anything other than false. */
103
175
  toBeInvalid: StateMatcher;
104
176
  /** Asserts the widget's accessible invalid state is unset or false. */
@@ -107,12 +179,45 @@ type MatcherImplementations = {
107
179
  toBeRequired: StateMatcher;
108
180
  /** Asserts the widget holds the platform focus state. */
109
181
  toHaveFocus: StateMatcher;
110
- /** Asserts a widget's numeric value, or its display value when a string is given. */
182
+ /**
183
+ * Asserts a widget's numeric value, or its display value when a string is given.
184
+ *
185
+ * Numbers come from the accessibility tree, which GTK keeps to a resolution of 0.001: it drops an
186
+ * update whose value is nearer than that to the one already published, so a widget that moves by
187
+ * less keeps its previous value. Numbers therefore match within 0.001, the same tolerance the
188
+ * `value` option of the `ByRole` queries and `toHaveAccessibleProperty` apply. That option reads
189
+ * the same tree, where the maximum of a paged widget such as a scrollbar is the last value it can
190
+ * reach, its adjustment's upper bound less one page.
191
+ */
111
192
  toHaveValue: (received: unknown, expected?: number | string) => MatcherResult;
112
193
  /** Asserts the widget's accessible role. */
113
194
  toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
114
195
  /** Asserts the widget is, or is an ancestor of, the given widget. */
115
196
  toContainElement: (received: unknown, descendant: Gtk.Widget | null) => MatcherResult;
197
+ /** Asserts the widget comes before the given widget in the widget tree, neither containing the other. */
198
+ toAppearBefore: (received: unknown, other: Gtk.Widget) => MatcherResult;
199
+ /** Asserts the widget comes after the given widget in the widget tree, neither containing the other. */
200
+ toAppearAfter: (received: unknown, other: Gtk.Widget) => MatcherResult;
201
+ /** Asserts at least one widget under the received one matches the `ByRole` query. */
202
+ toContainAnyByRole: ContainmentMatcher<RoleQueryArgs>;
203
+ /** Asserts exactly one widget under the received one matches the `ByRole` query. */
204
+ toContainOneByRole: ContainmentMatcher<RoleQueryArgs>;
205
+ /** Asserts at least one widget under the received one matches the `ByText` query. */
206
+ toContainAnyByText: ContainmentMatcher<TextQueryArgs>;
207
+ /** Asserts exactly one widget under the received one matches the `ByText` query. */
208
+ toContainOneByText: ContainmentMatcher<TextQueryArgs>;
209
+ /** Asserts at least one widget under the received one matches the `ByLabelText` query. */
210
+ toContainAnyByLabelText: ContainmentMatcher<TextQueryArgs>;
211
+ /** Asserts exactly one widget under the received one matches the `ByLabelText` query. */
212
+ toContainOneByLabelText: ContainmentMatcher<TextQueryArgs>;
213
+ /** Asserts at least one widget under the received one matches the `ByPlaceholderText` query. */
214
+ toContainAnyByPlaceholderText: ContainmentMatcher<TextQueryArgs>;
215
+ /** Asserts exactly one widget under the received one matches the `ByPlaceholderText` query. */
216
+ toContainOneByPlaceholderText: ContainmentMatcher<TextQueryArgs>;
217
+ /** Asserts at least one widget under the received one matches the `ByDisplayValue` query. */
218
+ toContainAnyByDisplayValue: ContainmentMatcher<TextQueryArgs>;
219
+ /** Asserts exactly one widget under the received one matches the `ByDisplayValue` query. */
220
+ toContainOneByDisplayValue: ContainmentMatcher<TextQueryArgs>;
116
221
  /**
117
222
  * Asserts the widget carries every given style class, each a name, a space-separated list, or a pattern.
118
223
  * Pass `{ exact: true }` last to require exactly that set, which rules out patterns.
@@ -127,7 +232,18 @@ type MatcherImplementations = {
127
232
 
128
233
  type ExpectExtend = { extend: (m: MatcherImplementations) => void };
129
234
 
235
+ type AttributeExpectation = {
236
+ label: string;
237
+ comparison: string;
238
+ expected: AccessibleAttributeValue;
239
+ actual: AccessibleAttributeValue | null;
240
+ isPass: boolean;
241
+ isReadingRounded: boolean;
242
+ };
243
+
130
244
  const registration = { isRegistered: false };
245
+ const MIXED_TRISTATE: number = Gtk.AccessibleTristate.MIXED;
246
+ const ROUNDED_READING = ", which the accessibility tree rounds to six significant digits";
131
247
  const displayValueMatcher: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
132
248
  const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
133
249
 
@@ -143,13 +259,70 @@ const toHaveAccessibleErrorMessage: TextMatcher = textMatcher(
143
259
  "exact",
144
260
  );
145
261
 
146
- const toHavePlaceholderText: TextMatcher = textMatcher("toHavePlaceholderText", getWidgetPlaceholderText, "exact");
147
262
  const toHaveSelection: TextMatcher = textMatcher("toHaveSelection", getWidgetSelection, "exact");
148
263
  const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
149
- const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
150
- const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
151
264
  const toBeRequired: StateMatcher = booleanStateMatcher("toBeRequired", "required", getWidgetRequiredState);
152
265
 
266
+ const toContainAnyByRole: ContainmentMatcher<RoleQueryArgs> = containmentMatcher(
267
+ "toContainAnyByRole",
268
+ false,
269
+ queryAllByRole,
270
+ );
271
+
272
+ const toContainOneByRole: ContainmentMatcher<RoleQueryArgs> = containmentMatcher(
273
+ "toContainOneByRole",
274
+ true,
275
+ queryAllByRole,
276
+ );
277
+
278
+ const toContainAnyByText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
279
+ "toContainAnyByText",
280
+ false,
281
+ queryAllByText,
282
+ );
283
+
284
+ const toContainOneByText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
285
+ "toContainOneByText",
286
+ true,
287
+ queryAllByText,
288
+ );
289
+
290
+ const toContainAnyByLabelText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
291
+ "toContainAnyByLabelText",
292
+ false,
293
+ queryAllByLabelText,
294
+ );
295
+
296
+ const toContainOneByLabelText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
297
+ "toContainOneByLabelText",
298
+ true,
299
+ queryAllByLabelText,
300
+ );
301
+
302
+ const toContainAnyByPlaceholderText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
303
+ "toContainAnyByPlaceholderText",
304
+ false,
305
+ queryAllByPlaceholderText,
306
+ );
307
+
308
+ const toContainOneByPlaceholderText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
309
+ "toContainOneByPlaceholderText",
310
+ true,
311
+ queryAllByPlaceholderText,
312
+ );
313
+
314
+ const toContainAnyByDisplayValue: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
315
+ "toContainAnyByDisplayValue",
316
+ false,
317
+ queryAllByDisplayValue,
318
+ );
319
+
320
+ const toContainOneByDisplayValue: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
321
+ "toContainOneByDisplayValue",
322
+ true,
323
+ queryAllByDisplayValue,
324
+ );
325
+
153
326
  /** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
154
327
  const matchers: MatcherImplementations = {
155
328
  toHaveDisplayValue,
@@ -157,18 +330,18 @@ const matchers: MatcherImplementations = {
157
330
  toHaveAccessibleName,
158
331
  toHaveAccessibleDescription,
159
332
  toHaveAccessibleErrorMessage,
160
- toHavePlaceholderText,
161
333
  toHaveSelection,
334
+ toHaveAccessibleState,
335
+ toHaveAccessibleProperty,
162
336
  toBeChecked,
163
337
  toBePartiallyChecked,
164
338
  toBePressed,
165
- toBeExpanded,
166
- toBeSelected,
339
+ toBePartiallyPressed,
167
340
  toBeDisabled,
168
341
  toBeEnabled,
169
342
  toBeVisible,
170
343
  toBeRooted,
171
- toBeEmpty,
344
+ toBeEmptyWidget,
172
345
  toBeInvalid,
173
346
  toBeValid,
174
347
  toBeRequired,
@@ -176,6 +349,18 @@ const matchers: MatcherImplementations = {
176
349
  toHaveValue,
177
350
  toHaveRole,
178
351
  toContainElement,
352
+ toAppearBefore,
353
+ toAppearAfter,
354
+ toContainAnyByRole,
355
+ toContainOneByRole,
356
+ toContainAnyByText,
357
+ toContainOneByText,
358
+ toContainAnyByLabelText,
359
+ toContainOneByLabelText,
360
+ toContainAnyByPlaceholderText,
361
+ toContainOneByPlaceholderText,
362
+ toContainAnyByDisplayValue,
363
+ toContainOneByDisplayValue,
179
364
  toHaveClass,
180
365
  toHaveObjectProperty,
181
366
  };
@@ -244,6 +429,75 @@ const stateResult = (widget: Gtk.Widget, stateName: string, isPass: boolean): Ma
244
429
  message: () => `expected widget ${negationPrefix(isPass)}to be ${stateName}\n${describeWidget(widget)}`,
245
430
  });
246
431
 
432
+ const describeAttributeValue = (value: AccessibleAttributeValue | null): string =>
433
+ value === null ? "unset" : JSON.stringify(value);
434
+
435
+ const attributeSetResult = (
436
+ widget: Gtk.Widget,
437
+ label: string,
438
+ actual: AccessibleAttributeValue | null,
439
+ ): MatcherResult => {
440
+ const isSet = actual !== null;
441
+
442
+ return {
443
+ pass: isSet,
444
+ message: () => `expected widget ${negationPrefix(isSet)}to have ${label} set\n${describeWidget(widget)}`,
445
+ };
446
+ };
447
+
448
+ const describeReading = (expectation: AttributeExpectation): string =>
449
+ `${describeAttributeValue(expectation.actual)}${expectation.isReadingRounded ? ROUNDED_READING : ""}`;
450
+
451
+ const attributeValueResult = (widget: Gtk.Widget, expectation: AttributeExpectation): MatcherResult => ({
452
+ pass: expectation.isPass,
453
+ message: () =>
454
+ `expected widget ${negationPrefix(expectation.isPass)}to have ${expectation.label} ` +
455
+ `${expectation.comparison} ${describeAttributeValue(expectation.expected)}, but received ` +
456
+ `${describeReading(expectation)}\n${describeWidget(widget)}`,
457
+ });
458
+
459
+ const orderResult = (widget: Gtk.Widget, other: Gtk.Widget, position: string, isPass: boolean): MatcherResult => ({
460
+ pass: isPass,
461
+ message: () =>
462
+ `expected widget ${negationPrefix(isPass)}to appear ${position} ${describeWidget(other)} ` +
463
+ `in the widget tree\n${describeWidget(widget)}`,
464
+ });
465
+
466
+ const getTreeRoot = (widget: Gtk.Widget): Gtk.Widget => {
467
+ let root = widget;
468
+ let parent = root.getParent();
469
+
470
+ while (parent) {
471
+ root = parent;
472
+ parent = root.getParent();
473
+ }
474
+
475
+ return root;
476
+ };
477
+
478
+ const isSeparateWidget = (first: Gtk.Widget, second: Gtk.Widget): boolean =>
479
+ first !== second && !first.isAncestor(second) && !second.isAncestor(first);
480
+
481
+ const isPrecedingWidget = (root: Gtk.Widget, first: Gtk.Widget, second: Gtk.Widget): boolean => {
482
+ for (const widget of descendants(root)) {
483
+ if (widget === first) {
484
+ return true;
485
+ }
486
+
487
+ if (widget === second) {
488
+ return false;
489
+ }
490
+ }
491
+
492
+ return false;
493
+ };
494
+
495
+ const isAppearingBefore = (first: Gtk.Widget, second: Gtk.Widget): boolean => {
496
+ const root = getTreeRoot(first);
497
+
498
+ return isSeparateWidget(first, second) && root === getTreeRoot(second) && isPrecedingWidget(root, first, second);
499
+ };
500
+
247
501
  const camelCase = (name: string): string =>
248
502
  name.replaceAll(/-([a-z])/g, (_match, letter: string) => letter.toUpperCase());
249
503
 
@@ -426,6 +680,117 @@ function booleanStateMatcher(
426
680
  };
427
681
  }
428
682
 
683
+ function containmentMatcher<Args extends unknown[]>(
684
+ matcherName: string,
685
+ isSingle: boolean,
686
+ queryAll: QueryAll<Args>,
687
+ ): ContainmentMatcher<Args> {
688
+ const quantity = isSingle ? "exactly one widget" : "any widget";
689
+
690
+ return (received: unknown, ...args: Args): MatcherResult => {
691
+ const widget = asWidget(received, matcherName);
692
+ const matches = queryAll(widget, ...args);
693
+ const isPass = isSingle ? matches.length === 1 : matches.length > 0;
694
+
695
+ return {
696
+ pass: isPass,
697
+ message: () =>
698
+ `expected widget ${negationPrefix(isPass)}to contain ${quantity} matching ${matcherName}, ` +
699
+ `but found ${String(matches.length)}\n${describeWidget(widget)}`,
700
+ };
701
+ };
702
+ }
703
+
704
+ function toHaveAccessibleState(
705
+ received: unknown,
706
+ state: Gtk.AccessibleState,
707
+ expected?: boolean | number,
708
+ ): MatcherResult {
709
+ const widget = asWidget(received, "toHaveAccessibleState");
710
+ const actual = readAccessibleState(widget, state);
711
+ const label = `accessible state ${Gtk.AccessibleState[state]}`;
712
+
713
+ if (expected === undefined) {
714
+ return attributeSetResult(widget, label, actual);
715
+ }
716
+
717
+ const wanted = typeof expected === "boolean" ? Number(expected) : expected;
718
+
719
+ return attributeValueResult(widget, {
720
+ label,
721
+ comparison: "equal to",
722
+ expected: wanted,
723
+ actual,
724
+ isPass: actual === wanted,
725
+ isReadingRounded: false,
726
+ });
727
+ }
728
+
729
+ const propertyLabel = (property: Gtk.AccessibleProperty): string =>
730
+ `accessible property ${Gtk.AccessibleProperty[property]}`;
731
+
732
+ const propertyExpectation = (
733
+ widget: Gtk.Widget,
734
+ property: Gtk.AccessibleProperty,
735
+ expected: AccessibleAttributeValue,
736
+ actual: AccessibleAttributeValue | null,
737
+ ): AttributeExpectation => {
738
+ const label = propertyLabel(property);
739
+
740
+ if (typeof expected === "number" && isAccessibleNumberProperty(property)) {
741
+ return {
742
+ label,
743
+ comparison: `within ${String(ACCESSIBLE_NUMBER_TOLERANCE)} of`,
744
+ expected,
745
+ actual,
746
+ isPass: isAccessibleNumberMatch(widget, property, expected),
747
+ isReadingRounded: actual !== null,
748
+ };
749
+ }
750
+
751
+ return { label, comparison: "equal to", expected, actual, isPass: actual === expected, isReadingRounded: false };
752
+ };
753
+
754
+ function toHaveAccessibleProperty(
755
+ received: unknown,
756
+ property: Gtk.AccessibleProperty,
757
+ expected?: AccessibleAttributeValue,
758
+ ): MatcherResult {
759
+ const widget = asWidget(received, "toHaveAccessibleProperty");
760
+ const actual = readAccessibleProperty(widget, property);
761
+
762
+ if (expected === undefined) {
763
+ return attributeSetResult(widget, propertyLabel(property), actual);
764
+ }
765
+
766
+ return attributeValueResult(widget, propertyExpectation(widget, property, expected, actual));
767
+ }
768
+
769
+ function toBePartiallyPressed(received: unknown): MatcherResult {
770
+ const widget = asWidget(received, "toBePartiallyPressed");
771
+ const state = readAccessibleState(widget, Gtk.AccessibleState.PRESSED);
772
+
773
+ if (state === null) {
774
+ throw notApplicable("toBePartiallyPressed", "pressed state", widget);
775
+ }
776
+
777
+ return stateResult(widget, "partially pressed", state === MIXED_TRISTATE);
778
+ }
779
+
780
+ function toAppearBefore(received: unknown, other: Gtk.Widget): MatcherResult {
781
+ const widget = asWidget(received, "toAppearBefore");
782
+ const target = asWidget(other, "toAppearBefore");
783
+
784
+ return orderResult(widget, target, "before", isAppearingBefore(widget, target));
785
+ }
786
+
787
+ function toAppearAfter(received: unknown, other: Gtk.Widget): MatcherResult {
788
+ const widget = asWidget(received, "toAppearAfter");
789
+ const target = asWidget(other, "toAppearAfter");
790
+
791
+ return orderResult(widget, target, "after", isAppearingBefore(target, widget));
792
+ }
793
+
429
794
  function readCheckedState(widget: Gtk.Widget, matcherName: string): CheckedState {
430
795
  const state = getWidgetCheckedState(widget);
431
796
 
@@ -458,6 +823,9 @@ function toHaveDisplayValue(received: unknown, expected?: TextExpectation): Matc
458
823
  return displayValueMatcher(received, expected);
459
824
  }
460
825
 
826
+ const describeValueExpectation = (expected: number | undefined): string =>
827
+ expected === undefined ? "set" : `within ${String(ACCESSIBLE_NUMBER_TOLERANCE)} of ${String(expected)}`;
828
+
461
829
  function toHaveValue(received: unknown, expected?: number | string): MatcherResult {
462
830
  const widget = asWidget(received, "toHaveValue");
463
831
 
@@ -465,19 +833,19 @@ function toHaveValue(received: unknown, expected?: number | string): MatcherResu
465
833
  return displayValueMatcher(received, expected);
466
834
  }
467
835
 
468
- const actual = getWidgetValue(widget).now;
836
+ const actual = getWidgetValueNow(widget);
469
837
 
470
838
  if (actual === null) {
471
839
  throw notApplicable("toHaveValue", "numeric value", widget);
472
840
  }
473
841
 
474
- const isPass = expected === undefined || actual === expected;
842
+ const isPass = expected === undefined || isWidgetValueMatch(widget, "now", expected);
475
843
 
476
844
  return {
477
845
  pass: isPass,
478
846
  message: () =>
479
- `expected widget ${negationPrefix(isPass)}to have value ${String(expected)}, ` +
480
- `but received ${String(actual)}\n${describeWidget(widget)}`,
847
+ `expected widget ${negationPrefix(isPass)}to have value ${describeValueExpectation(expected)}, ` +
848
+ `but received ${String(actual)}${ROUNDED_READING}\n${describeWidget(widget)}`,
481
849
  };
482
850
  }
483
851
 
@@ -509,8 +877,8 @@ function toBeRooted(received: unknown): MatcherResult {
509
877
  return stateResult(widget, "rooted in a window", isWidgetRooted(widget));
510
878
  }
511
879
 
512
- function toBeEmpty(received: unknown): MatcherResult {
513
- const widget = asWidget(received, "toBeEmpty");
880
+ function toBeEmptyWidget(received: unknown): MatcherResult {
881
+ const widget = asWidget(received, "toBeEmptyWidget");
514
882
 
515
883
  return stateResult(widget, "empty", widget.getFirstChild() === null && getWidgetLabelText(widget) === null);
516
884
  }
@@ -599,8 +967,6 @@ function toHaveObjectProperty(this: MatcherContext, received: unknown, ...args:
599
967
  };
600
968
  }
601
969
 
602
- /* eslint-disable-next-line unicorn/no-this-outside-of-class --
603
- expect.extend invokes matchers with the matcher state as `this` */
604
970
  const isPass = isEqualValue(this, actual, expected);
605
971
 
606
972
  return {
@@ -635,18 +1001,30 @@ declare module "@vitest/expect" {
635
1001
  toHaveAccessibleName(expected?: TextExpectation): void;
636
1002
  toHaveAccessibleDescription(expected?: TextExpectation): void;
637
1003
  toHaveAccessibleErrorMessage(expected?: TextExpectation): void;
638
- toHavePlaceholderText(expected?: TextExpectation): void;
639
1004
  toHaveSelection(expected?: TextExpectation): void;
1005
+ /* eslint-disable-next-line unicorn/consistent-boolean-name -- expected is the jest-dom matcher argument name */
1006
+ toHaveAccessibleState(state: BooleanAccessibleState, expected?: boolean): void;
1007
+ toHaveAccessibleState(state: TristateAccessibleState, expected?: Gtk.AccessibleTristate): void;
1008
+ toHaveAccessibleState(state: Gtk.AccessibleState.INVALID, expected?: Gtk.AccessibleInvalidState): void;
1009
+ toHaveAccessibleProperty(property: StringAccessibleProperty, expected?: string): void;
1010
+ /* eslint-disable-next-line unicorn/consistent-boolean-name -- expected is the jest-dom matcher argument name */
1011
+ toHaveAccessibleProperty(property: BooleanAccessibleProperty, expected?: boolean): void;
1012
+ toHaveAccessibleProperty(property: NumberAccessibleProperty, expected?: number): void;
1013
+ toHaveAccessibleProperty(
1014
+ property: Gtk.AccessibleProperty.AUTOCOMPLETE,
1015
+ expected?: Gtk.AccessibleAutocomplete,
1016
+ ): void;
1017
+ toHaveAccessibleProperty(property: Gtk.AccessibleProperty.ORIENTATION, expected?: Gtk.Orientation): void;
1018
+ toHaveAccessibleProperty(property: Gtk.AccessibleProperty.SORT, expected?: Gtk.AccessibleSort): void;
640
1019
  toBeChecked(): void;
641
1020
  toBePartiallyChecked(): void;
642
1021
  toBePressed(): void;
643
- toBeExpanded(): void;
644
- toBeSelected(): void;
1022
+ toBePartiallyPressed(): void;
645
1023
  toBeDisabled(): void;
646
1024
  toBeEnabled(): void;
647
1025
  toBeVisible(): void;
648
1026
  toBeRooted(): void;
649
- toBeEmpty(): void;
1027
+ toBeEmptyWidget(): void;
650
1028
  toBeInvalid(): void;
651
1029
  toBeValid(): void;
652
1030
  toBeRequired(): void;
@@ -654,6 +1032,18 @@ declare module "@vitest/expect" {
654
1032
  toHaveValue(expected?: number | string): void;
655
1033
  toHaveRole(expected: Gtk.AccessibleRole): void;
656
1034
  toContainElement(descendant: Gtk.Widget | null): void;
1035
+ toAppearBefore(other: Gtk.Widget): void;
1036
+ toAppearAfter(other: Gtk.Widget): void;
1037
+ toContainAnyByRole(...args: RoleQueryArgs): void;
1038
+ toContainOneByRole(...args: RoleQueryArgs): void;
1039
+ toContainAnyByText(...args: TextQueryArgs): void;
1040
+ toContainOneByText(...args: TextQueryArgs): void;
1041
+ toContainAnyByLabelText(...args: TextQueryArgs): void;
1042
+ toContainOneByLabelText(...args: TextQueryArgs): void;
1043
+ toContainAnyByPlaceholderText(...args: TextQueryArgs): void;
1044
+ toContainOneByPlaceholderText(...args: TextQueryArgs): void;
1045
+ toContainAnyByDisplayValue(...args: TextQueryArgs): void;
1046
+ toContainOneByDisplayValue(...args: TextQueryArgs): void;
657
1047
  toHaveClass(...args: (ClassExpectation | { exact: boolean })[]): void;
658
1048
  toHaveObjectProperty(name: string, expected?: unknown): void;
659
1049
  }
@@ -663,36 +1053,4 @@ declare module "@vitest/expect" {
663
1053
  /* eslint-enable @typescript-eslint/consistent-type-definitions */
664
1054
  }
665
1055
 
666
- export {
667
- toHaveDisplayValue,
668
- toHaveTextContent,
669
- toHaveAccessibleName,
670
- toHaveAccessibleDescription,
671
- toHaveAccessibleErrorMessage,
672
- toHavePlaceholderText,
673
- toHaveSelection,
674
- toBeChecked,
675
- toBePartiallyChecked,
676
- toBePressed,
677
- toBeExpanded,
678
- toBeSelected,
679
- toBeDisabled,
680
- toBeEnabled,
681
- toBeVisible,
682
- toBeRooted,
683
- toBeEmpty,
684
- toBeInvalid,
685
- toBeValid,
686
- toBeRequired,
687
- toHaveFocus,
688
- toHaveValue,
689
- toHaveRole,
690
- toContainElement,
691
- toHaveClass,
692
- toHaveObjectProperty,
693
- matchers,
694
- registerMatchers,
695
- type ClassExpectation,
696
- type TextContentOptions,
697
- type TextExpectation,
698
- };
1056
+ export { matchers, registerMatchers, type ClassExpectation, type TextContentOptions, type TextExpectation };
@@ -0,0 +1 @@
1
+ export * as prettyFormat from "@vitest/pretty-format";