@gtkx/testing 1.0.0-rc.3 → 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 (215) hide show
  1. package/README.md +8 -9
  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/act.d.ts +2 -0
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js.map +1 -1
  9. package/dist/build-queries.d.ts +27 -10
  10. package/dist/build-queries.d.ts.map +1 -1
  11. package/dist/build-queries.js +39 -30
  12. package/dist/build-queries.js.map +1 -1
  13. package/dist/cleanup-registry.d.ts +1 -1
  14. package/dist/cleanup-registry.d.ts.map +1 -1
  15. package/dist/cleanup-registry.js.map +1 -1
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +3 -3
  19. package/dist/config.js.map +1 -1
  20. package/dist/errors.d.ts +10 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +12 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/fire-event.d.ts +35 -6
  25. package/dist/fire-event.d.ts.map +1 -1
  26. package/dist/fire-event.js +23 -7
  27. package/dist/fire-event.js.map +1 -1
  28. package/dist/frame-sync.d.ts +2 -1
  29. package/dist/frame-sync.d.ts.map +1 -1
  30. package/dist/frame-sync.js +15 -5
  31. package/dist/frame-sync.js.map +1 -1
  32. package/dist/harness-window.d.ts +4 -0
  33. package/dist/harness-window.d.ts.map +1 -0
  34. package/dist/harness-window.js +6 -0
  35. package/dist/harness-window.js.map +1 -0
  36. package/dist/index.d.ts +14 -7
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +9 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal.d.ts +4 -0
  41. package/dist/internal.d.ts.map +1 -0
  42. package/dist/internal.js +4 -0
  43. package/dist/internal.js.map +1 -0
  44. package/dist/matchers.d.ts +125 -39
  45. package/dist/matchers.d.ts.map +1 -1
  46. package/dist/matchers.js +156 -18
  47. package/dist/matchers.js.map +1 -1
  48. package/dist/pretty-format.d.ts +2 -0
  49. package/dist/pretty-format.d.ts.map +1 -0
  50. package/dist/pretty-format.js +2 -0
  51. package/dist/pretty-format.js.map +1 -0
  52. package/dist/pretty-widget.d.ts +7 -3
  53. package/dist/pretty-widget.d.ts.map +1 -1
  54. package/dist/pretty-widget.js +74 -57
  55. package/dist/pretty-widget.js.map +1 -1
  56. package/dist/production.d.ts +8 -0
  57. package/dist/production.d.ts.map +1 -0
  58. package/dist/production.js +34 -0
  59. package/dist/production.js.map +1 -0
  60. package/dist/queries.d.ts +99 -56
  61. package/dist/queries.d.ts.map +1 -1
  62. package/dist/queries.js +123 -83
  63. package/dist/queries.js.map +1 -1
  64. package/dist/query-helpers.d.ts +44 -0
  65. package/dist/query-helpers.d.ts.map +1 -0
  66. package/dist/query-helpers.js +57 -0
  67. package/dist/query-helpers.js.map +1 -0
  68. package/dist/render-children.d.ts +9 -0
  69. package/dist/render-children.d.ts.map +1 -0
  70. package/dist/render-children.js +11 -0
  71. package/dist/render-children.js.map +1 -0
  72. package/dist/render.d.ts.map +1 -1
  73. package/dist/render.js +13 -11
  74. package/dist/render.js.map +1 -1
  75. package/dist/role-helpers.d.ts +12 -3
  76. package/dist/role-helpers.d.ts.map +1 -1
  77. package/dist/role-helpers.js +13 -4
  78. package/dist/role-helpers.js.map +1 -1
  79. package/dist/role-naming.d.ts +5 -0
  80. package/dist/role-naming.d.ts.map +1 -0
  81. package/dist/role-naming.js +96 -0
  82. package/dist/role-naming.js.map +1 -0
  83. package/dist/screenshot.d.ts +12 -20
  84. package/dist/screenshot.d.ts.map +1 -1
  85. package/dist/screenshot.js +183 -91
  86. package/dist/screenshot.js.map +1 -1
  87. package/dist/suggestions.d.ts +3 -0
  88. package/dist/suggestions.d.ts.map +1 -1
  89. package/dist/suggestions.js.map +1 -1
  90. package/dist/traversal.d.ts +9 -1
  91. package/dist/traversal.d.ts.map +1 -1
  92. package/dist/traversal.js +26 -6
  93. package/dist/traversal.js.map +1 -1
  94. package/dist/types.d.ts +67 -17
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/types.js.map +1 -1
  97. package/dist/user-event/adjustment.d.ts +16 -0
  98. package/dist/user-event/adjustment.d.ts.map +1 -1
  99. package/dist/user-event/adjustment.js +34 -2
  100. package/dist/user-event/adjustment.js.map +1 -1
  101. package/dist/user-event/click.d.ts +45 -3
  102. package/dist/user-event/click.d.ts.map +1 -1
  103. package/dist/user-event/click.js +177 -39
  104. package/dist/user-event/click.js.map +1 -1
  105. package/dist/user-event/column-header-click.d.ts +4 -0
  106. package/dist/user-event/column-header-click.d.ts.map +1 -0
  107. package/dist/user-event/column-header-click.js +65 -0
  108. package/dist/user-event/column-header-click.js.map +1 -0
  109. package/dist/user-event/controller.d.ts +29 -0
  110. package/dist/user-event/controller.d.ts.map +1 -1
  111. package/dist/user-event/controller.js +28 -0
  112. package/dist/user-event/controller.js.map +1 -1
  113. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  114. package/dist/user-event/event-wrapper.js +2 -1
  115. package/dist/user-event/event-wrapper.js.map +1 -1
  116. package/dist/user-event/gesture.d.ts +51 -0
  117. package/dist/user-event/gesture.d.ts.map +1 -1
  118. package/dist/user-event/gesture.js +47 -4
  119. package/dist/user-event/gesture.js.map +1 -1
  120. package/dist/user-event/index.d.ts +70 -2
  121. package/dist/user-event/index.d.ts.map +1 -1
  122. package/dist/user-event/index.js +43 -26
  123. package/dist/user-event/index.js.map +1 -1
  124. package/dist/user-event/indexed-children.d.ts +10 -0
  125. package/dist/user-event/indexed-children.d.ts.map +1 -0
  126. package/dist/user-event/indexed-children.js +50 -0
  127. package/dist/user-event/indexed-children.js.map +1 -0
  128. package/dist/user-event/keyboard.d.ts +4 -2
  129. package/dist/user-event/keyboard.d.ts.map +1 -1
  130. package/dist/user-event/keyboard.js +46 -16
  131. package/dist/user-event/keyboard.js.map +1 -1
  132. package/dist/user-event/list-row-click.d.ts +4 -0
  133. package/dist/user-event/list-row-click.d.ts.map +1 -0
  134. package/dist/user-event/list-row-click.js +39 -0
  135. package/dist/user-event/list-row-click.js.map +1 -0
  136. package/dist/user-event/native-click.d.ts +6 -0
  137. package/dist/user-event/native-click.d.ts.map +1 -0
  138. package/dist/user-event/native-click.js +32 -0
  139. package/dist/user-event/native-click.js.map +1 -0
  140. package/dist/user-event/pointer.d.ts +1 -1
  141. package/dist/user-event/pointer.d.ts.map +1 -1
  142. package/dist/user-event/pointer.js +37 -13
  143. package/dist/user-event/pointer.js.map +1 -1
  144. package/dist/user-event/selection.d.ts +18 -0
  145. package/dist/user-event/selection.d.ts.map +1 -1
  146. package/dist/user-event/selection.js +56 -30
  147. package/dist/user-event/selection.js.map +1 -1
  148. package/dist/user-event/state.d.ts +1 -1
  149. package/dist/user-event/state.d.ts.map +1 -1
  150. package/dist/user-event/state.js +1 -1
  151. package/dist/user-event/state.js.map +1 -1
  152. package/dist/user-event/text.d.ts +34 -3
  153. package/dist/user-event/text.d.ts.map +1 -1
  154. package/dist/user-event/text.js +94 -17
  155. package/dist/user-event/text.js.map +1 -1
  156. package/dist/wait-for.d.ts +2 -0
  157. package/dist/wait-for.d.ts.map +1 -1
  158. package/dist/wait-for.js.map +1 -1
  159. package/dist/widget-accessible-properties.d.ts +14 -10
  160. package/dist/widget-accessible-properties.d.ts.map +1 -1
  161. package/dist/widget-accessible-properties.js +135 -173
  162. package/dist/widget-accessible-properties.js.map +1 -1
  163. package/dist/widget-getters.d.ts +8 -0
  164. package/dist/widget-getters.d.ts.map +1 -0
  165. package/dist/widget-getters.js +19 -0
  166. package/dist/widget-getters.js.map +1 -0
  167. package/dist/within.d.ts +3 -2
  168. package/dist/within.d.ts.map +1 -1
  169. package/dist/within.js +3 -2
  170. package/dist/within.js.map +1 -1
  171. package/package.json +13 -8
  172. package/src/accessible-native.ts +333 -0
  173. package/src/act.ts +2 -0
  174. package/src/build-queries.ts +66 -48
  175. package/src/cleanup-registry.ts +1 -1
  176. package/src/config.ts +4 -3
  177. package/src/errors.ts +14 -3
  178. package/src/fire-event.ts +61 -8
  179. package/src/frame-sync.ts +21 -5
  180. package/src/harness-window.ts +9 -0
  181. package/src/index.ts +18 -11
  182. package/src/internal.ts +11 -0
  183. package/src/matchers.ts +466 -64
  184. package/src/pretty-format.ts +1 -0
  185. package/src/pretty-widget.ts +100 -72
  186. package/src/production.ts +52 -0
  187. package/src/queries.ts +128 -89
  188. package/src/query-helpers.ts +97 -0
  189. package/src/render-children.ts +25 -0
  190. package/src/render.tsx +18 -13
  191. package/src/role-helpers.ts +14 -4
  192. package/src/role-naming.ts +99 -0
  193. package/src/screenshot.ts +272 -103
  194. package/src/suggestions.ts +3 -0
  195. package/src/traversal.ts +49 -6
  196. package/src/types.ts +69 -19
  197. package/src/user-event/adjustment.ts +42 -2
  198. package/src/user-event/click.ts +248 -46
  199. package/src/user-event/column-header-click.ts +90 -0
  200. package/src/user-event/controller.ts +29 -0
  201. package/src/user-event/event-wrapper.ts +3 -1
  202. package/src/user-event/gesture.ts +57 -6
  203. package/src/user-event/index.ts +119 -29
  204. package/src/user-event/indexed-children.ts +75 -0
  205. package/src/user-event/keyboard.ts +70 -20
  206. package/src/user-event/list-row-click.ts +57 -0
  207. package/src/user-event/native-click.ts +46 -0
  208. package/src/user-event/pointer.ts +52 -13
  209. package/src/user-event/selection.ts +74 -33
  210. package/src/user-event/state.ts +2 -2
  211. package/src/user-event/text.ts +121 -23
  212. package/src/wait-for.ts +2 -0
  213. package/src/widget-accessible-properties.ts +183 -214
  214. package/src/widget-getters.ts +32 -0
  215. 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
 
@@ -39,51 +54,196 @@ type TextContentOptions = {
39
54
 
40
55
  /** The expected value for a style class: an exact class name or a regular expression. */
41
56
  type ClassExpectation = string | RegExp;
42
- type MatcherResult = { pass: boolean; message: () => string };
43
- type MatcherContext = { equals: (actual: unknown, expected: unknown) => boolean };
57
+ /** The outcome of a matcher: whether it passed, and the failure text built on demand. */
58
+ type MatcherResult = Pick<SyncExpectationResult, "message" | "pass">;
59
+
60
+ /** The matcher state bound as `this`, supplying the test runner's deep equality check. */
61
+ type MatcherContext = {
62
+ /** Compares a received value against an expected one, resolving asymmetric matchers. */
63
+ equals: (actual: unknown, expected: unknown) => boolean;
64
+ };
65
+
66
+ /** Compares text read from the received widget; with no expected value, asserts the text is non-empty. */
44
67
  type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
68
+ /** Asserts a single state of the received widget, taking no expected value. */
45
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;
46
76
  type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
47
77
  type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
48
-
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;
112
+
113
+ /** A text matcher that also takes normalization options for the text it reads. */
49
114
  type TextContentMatcher = (
50
115
  received: unknown,
51
116
  expected?: TextExpectation,
52
117
  options?: TextContentOptions,
53
118
  ) => MatcherResult;
54
119
 
120
+ /** Signatures of the widget assertion matchers, keyed by the name each is registered under. */
55
121
  type MatcherImplementations = {
122
+ /** Asserts the text an editable widget or combo box shows; with no argument, that it is not empty. */
56
123
  toHaveDisplayValue: TextMatcher;
124
+ /** Asserts the widget's own text, or its descendants' when it has none, contains or matches the expectation. */
57
125
  toHaveTextContent: TextContentMatcher;
126
+ /** Asserts the widget's accessible name; with no argument, that it has one. */
58
127
  toHaveAccessibleName: TextMatcher;
128
+ /**
129
+ * Asserts the joined accessible names of the widget's `described-by` targets, falling back to its
130
+ * `description` and then to its tooltip.
131
+ */
59
132
  toHaveAccessibleDescription: TextMatcher;
133
+ /** Asserts the joined accessible names of the widget's `error-message` targets, read only while it is invalid. */
60
134
  toHaveAccessibleErrorMessage: TextMatcher;
61
- toHavePlaceholderText: TextMatcher;
135
+ /** Asserts the text currently selected in an editable widget. */
62
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;
156
+ /** Asserts the widget's checked state is checked rather than unchecked or mixed. */
63
157
  toBeChecked: StateMatcher;
158
+ /** Asserts the widget's checked state is mixed. */
64
159
  toBePartiallyChecked: StateMatcher;
160
+ /** Asserts a `Gtk.ToggleButton` is active, and throws for any other widget. */
65
161
  toBePressed: StateMatcher;
66
- toBeExpanded: StateMatcher;
67
- toBeSelected: StateMatcher;
162
+ /** Asserts the widget's accessible pressed state is mixed, and throws when it exposes none. */
163
+ toBePartiallyPressed: StateMatcher;
164
+ /** Asserts the widget is insensitive, or sits inside an insensitive ancestor. */
68
165
  toBeDisabled: StateMatcher;
166
+ /** Asserts the widget and all of its ancestors are sensitive. */
69
167
  toBeEnabled: StateMatcher;
168
+ /** Asserts the widget and its ancestors are all visible, and that none of them is fully transparent. */
70
169
  toBeVisible: StateMatcher;
170
+ /** Asserts the widget's root is a window that is still in the toplevel list. */
71
171
  toBeRooted: StateMatcher;
72
- toBeEmpty: StateMatcher;
172
+ /** Asserts the widget has neither a child nor label text. */
173
+ toBeEmptyWidget: StateMatcher;
174
+ /** Asserts the widget's accessible invalid state is set to anything other than false. */
73
175
  toBeInvalid: StateMatcher;
176
+ /** Asserts the widget's accessible invalid state is unset or false. */
74
177
  toBeValid: StateMatcher;
178
+ /** Asserts the widget's accessible required state. */
75
179
  toBeRequired: StateMatcher;
180
+ /** Asserts the widget holds the platform focus state. */
76
181
  toHaveFocus: StateMatcher;
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
+ */
77
192
  toHaveValue: (received: unknown, expected?: number | string) => MatcherResult;
193
+ /** Asserts the widget's accessible role. */
78
194
  toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
195
+ /** Asserts the widget is, or is an ancestor of, the given widget. */
79
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>;
221
+ /**
222
+ * Asserts the widget carries every given style class, each a name, a space-separated list, or a pattern.
223
+ * Pass `{ exact: true }` last to require exactly that set, which rules out patterns.
224
+ */
80
225
  toHaveClass: (received: unknown, ...args: unknown[]) => MatcherResult;
226
+ /**
227
+ * Asserts a GObject property, named as in GObject and looked up by its camel-cased accessor, equals the
228
+ * expected value. With no expected value, asserts the property is set. GObject values compare by identity.
229
+ */
81
230
  toHaveObjectProperty: (this: MatcherContext, received: unknown, ...args: unknown[]) => MatcherResult;
82
231
  };
83
232
 
84
233
  type ExpectExtend = { extend: (m: MatcherImplementations) => void };
85
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
+
86
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";
87
247
  const displayValueMatcher: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
88
248
  const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
89
249
 
@@ -99,13 +259,70 @@ const toHaveAccessibleErrorMessage: TextMatcher = textMatcher(
99
259
  "exact",
100
260
  );
101
261
 
102
- const toHavePlaceholderText: TextMatcher = textMatcher("toHavePlaceholderText", getWidgetPlaceholderText, "exact");
103
262
  const toHaveSelection: TextMatcher = textMatcher("toHaveSelection", getWidgetSelection, "exact");
104
263
  const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
105
- const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
106
- const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
107
264
  const toBeRequired: StateMatcher = booleanStateMatcher("toBeRequired", "required", getWidgetRequiredState);
108
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
+
109
326
  /** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
110
327
  const matchers: MatcherImplementations = {
111
328
  toHaveDisplayValue,
@@ -113,18 +330,18 @@ const matchers: MatcherImplementations = {
113
330
  toHaveAccessibleName,
114
331
  toHaveAccessibleDescription,
115
332
  toHaveAccessibleErrorMessage,
116
- toHavePlaceholderText,
117
333
  toHaveSelection,
334
+ toHaveAccessibleState,
335
+ toHaveAccessibleProperty,
118
336
  toBeChecked,
119
337
  toBePartiallyChecked,
120
338
  toBePressed,
121
- toBeExpanded,
122
- toBeSelected,
339
+ toBePartiallyPressed,
123
340
  toBeDisabled,
124
341
  toBeEnabled,
125
342
  toBeVisible,
126
343
  toBeRooted,
127
- toBeEmpty,
344
+ toBeEmptyWidget,
128
345
  toBeInvalid,
129
346
  toBeValid,
130
347
  toBeRequired,
@@ -132,6 +349,18 @@ const matchers: MatcherImplementations = {
132
349
  toHaveValue,
133
350
  toHaveRole,
134
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,
135
364
  toHaveClass,
136
365
  toHaveObjectProperty,
137
366
  };
@@ -200,6 +429,75 @@ const stateResult = (widget: Gtk.Widget, stateName: string, isPass: boolean): Ma
200
429
  message: () => `expected widget ${negationPrefix(isPass)}to be ${stateName}\n${describeWidget(widget)}`,
201
430
  });
202
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
+
203
501
  const camelCase = (name: string): string =>
204
502
  name.replaceAll(/-([a-z])/g, (_match, letter: string) => letter.toUpperCase());
205
503
 
@@ -382,6 +680,117 @@ function booleanStateMatcher(
382
680
  };
383
681
  }
384
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
+
385
794
  function readCheckedState(widget: Gtk.Widget, matcherName: string): CheckedState {
386
795
  const state = getWidgetCheckedState(widget);
387
796
 
@@ -414,6 +823,9 @@ function toHaveDisplayValue(received: unknown, expected?: TextExpectation): Matc
414
823
  return displayValueMatcher(received, expected);
415
824
  }
416
825
 
826
+ const describeValueExpectation = (expected: number | undefined): string =>
827
+ expected === undefined ? "set" : `within ${String(ACCESSIBLE_NUMBER_TOLERANCE)} of ${String(expected)}`;
828
+
417
829
  function toHaveValue(received: unknown, expected?: number | string): MatcherResult {
418
830
  const widget = asWidget(received, "toHaveValue");
419
831
 
@@ -421,19 +833,19 @@ function toHaveValue(received: unknown, expected?: number | string): MatcherResu
421
833
  return displayValueMatcher(received, expected);
422
834
  }
423
835
 
424
- const actual = getWidgetValue(widget).now;
836
+ const actual = getWidgetValueNow(widget);
425
837
 
426
838
  if (actual === null) {
427
839
  throw notApplicable("toHaveValue", "numeric value", widget);
428
840
  }
429
841
 
430
- const isPass = expected === undefined || actual === expected;
842
+ const isPass = expected === undefined || isWidgetValueMatch(widget, "now", expected);
431
843
 
432
844
  return {
433
845
  pass: isPass,
434
846
  message: () =>
435
- `expected widget ${negationPrefix(isPass)}to have value ${String(expected)}, ` +
436
- `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)}`,
437
849
  };
438
850
  }
439
851
 
@@ -465,8 +877,8 @@ function toBeRooted(received: unknown): MatcherResult {
465
877
  return stateResult(widget, "rooted in a window", isWidgetRooted(widget));
466
878
  }
467
879
 
468
- function toBeEmpty(received: unknown): MatcherResult {
469
- const widget = asWidget(received, "toBeEmpty");
880
+ function toBeEmptyWidget(received: unknown): MatcherResult {
881
+ const widget = asWidget(received, "toBeEmptyWidget");
470
882
 
471
883
  return stateResult(widget, "empty", widget.getFirstChild() === null && getWidgetLabelText(widget) === null);
472
884
  }
@@ -555,8 +967,6 @@ function toHaveObjectProperty(this: MatcherContext, received: unknown, ...args:
555
967
  };
556
968
  }
557
969
 
558
- /* eslint-disable-next-line unicorn/no-this-outside-of-class --
559
- expect.extend invokes matchers with the matcher state as `this` */
560
970
  const isPass = isEqualValue(this, actual, expected);
561
971
 
562
972
  return {
@@ -591,18 +1001,30 @@ declare module "@vitest/expect" {
591
1001
  toHaveAccessibleName(expected?: TextExpectation): void;
592
1002
  toHaveAccessibleDescription(expected?: TextExpectation): void;
593
1003
  toHaveAccessibleErrorMessage(expected?: TextExpectation): void;
594
- toHavePlaceholderText(expected?: TextExpectation): void;
595
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;
596
1019
  toBeChecked(): void;
597
1020
  toBePartiallyChecked(): void;
598
1021
  toBePressed(): void;
599
- toBeExpanded(): void;
600
- toBeSelected(): void;
1022
+ toBePartiallyPressed(): void;
601
1023
  toBeDisabled(): void;
602
1024
  toBeEnabled(): void;
603
1025
  toBeVisible(): void;
604
1026
  toBeRooted(): void;
605
- toBeEmpty(): void;
1027
+ toBeEmptyWidget(): void;
606
1028
  toBeInvalid(): void;
607
1029
  toBeValid(): void;
608
1030
  toBeRequired(): void;
@@ -610,6 +1032,18 @@ declare module "@vitest/expect" {
610
1032
  toHaveValue(expected?: number | string): void;
611
1033
  toHaveRole(expected: Gtk.AccessibleRole): void;
612
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;
613
1047
  toHaveClass(...args: (ClassExpectation | { exact: boolean })[]): void;
614
1048
  toHaveObjectProperty(name: string, expected?: unknown): void;
615
1049
  }
@@ -619,36 +1053,4 @@ declare module "@vitest/expect" {
619
1053
  /* eslint-enable @typescript-eslint/consistent-type-definitions */
620
1054
  }
621
1055
 
622
- export {
623
- toHaveDisplayValue,
624
- toHaveTextContent,
625
- toHaveAccessibleName,
626
- toHaveAccessibleDescription,
627
- toHaveAccessibleErrorMessage,
628
- toHavePlaceholderText,
629
- toHaveSelection,
630
- toBeChecked,
631
- toBePartiallyChecked,
632
- toBePressed,
633
- toBeExpanded,
634
- toBeSelected,
635
- toBeDisabled,
636
- toBeEnabled,
637
- toBeVisible,
638
- toBeRooted,
639
- toBeEmpty,
640
- toBeInvalid,
641
- toBeValid,
642
- toBeRequired,
643
- toHaveFocus,
644
- toHaveValue,
645
- toHaveRole,
646
- toContainElement,
647
- toHaveClass,
648
- toHaveObjectProperty,
649
- matchers,
650
- registerMatchers,
651
- type ClassExpectation,
652
- type TextContentOptions,
653
- type TextExpectation,
654
- };
1056
+ export { matchers, registerMatchers, type ClassExpectation, type TextContentOptions, type TextExpectation };