@gtkx/testing 1.0.0-rc.4 → 1.1.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 (212) 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/act.d.ts +1 -3
  7. package/dist/act.d.ts.map +1 -1
  8. package/dist/act.js +1 -1
  9. package/dist/act.js.map +1 -1
  10. package/dist/build-queries.d.ts +27 -10
  11. package/dist/build-queries.d.ts.map +1 -1
  12. package/dist/build-queries.js +22 -24
  13. package/dist/build-queries.js.map +1 -1
  14. package/dist/cleanup-registry.d.ts +1 -1
  15. package/dist/cleanup-registry.d.ts.map +1 -1
  16. package/dist/cleanup-registry.js.map +1 -1
  17. package/dist/config.d.ts +8 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +1 -0
  20. package/dist/config.js.map +1 -1
  21. package/dist/errors.d.ts +10 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +10 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/fire-event.d.ts +35 -6
  26. package/dist/fire-event.d.ts.map +1 -1
  27. package/dist/fire-event.js +23 -7
  28. package/dist/fire-event.js.map +1 -1
  29. package/dist/frame-sync.d.ts +3 -2
  30. package/dist/frame-sync.d.ts.map +1 -1
  31. package/dist/frame-sync.js +19 -9
  32. package/dist/frame-sync.js.map +1 -1
  33. package/dist/harness-window.d.ts +5 -0
  34. package/dist/harness-window.d.ts.map +1 -0
  35. package/dist/harness-window.js +17 -0
  36. package/dist/harness-window.js.map +1 -0
  37. package/dist/index.d.ts +14 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +9 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal.d.ts +5 -0
  42. package/dist/internal.d.ts.map +1 -0
  43. package/dist/internal.js +5 -0
  44. package/dist/internal.js.map +1 -0
  45. package/dist/matchers.d.ts +88 -46
  46. package/dist/matchers.d.ts.map +1 -1
  47. package/dist/matchers.js +178 -20
  48. package/dist/matchers.js.map +1 -1
  49. package/dist/pretty-format.d.ts +2 -0
  50. package/dist/pretty-format.d.ts.map +1 -0
  51. package/dist/pretty-format.js +2 -0
  52. package/dist/pretty-format.js.map +1 -0
  53. package/dist/pretty-widget.d.ts +5 -2
  54. package/dist/pretty-widget.d.ts.map +1 -1
  55. package/dist/pretty-widget.js +75 -57
  56. package/dist/pretty-widget.js.map +1 -1
  57. package/dist/production.d.ts +8 -0
  58. package/dist/production.d.ts.map +1 -0
  59. package/dist/production.js +34 -0
  60. package/dist/production.js.map +1 -0
  61. package/dist/queries.d.ts +98 -56
  62. package/dist/queries.d.ts.map +1 -1
  63. package/dist/queries.js +123 -83
  64. package/dist/queries.js.map +1 -1
  65. package/dist/query-helpers.d.ts +44 -0
  66. package/dist/query-helpers.d.ts.map +1 -0
  67. package/dist/query-helpers.js +57 -0
  68. package/dist/query-helpers.js.map +1 -0
  69. package/dist/render-children.d.ts +9 -0
  70. package/dist/render-children.d.ts.map +1 -0
  71. package/dist/render-children.js +11 -0
  72. package/dist/render-children.js.map +1 -0
  73. package/dist/render.d.ts +12 -1
  74. package/dist/render.d.ts.map +1 -1
  75. package/dist/render.js +52 -16
  76. package/dist/render.js.map +1 -1
  77. package/dist/role-helpers.d.ts +12 -3
  78. package/dist/role-helpers.d.ts.map +1 -1
  79. package/dist/role-helpers.js +15 -5
  80. package/dist/role-helpers.js.map +1 -1
  81. package/dist/role-naming.d.ts +5 -0
  82. package/dist/role-naming.d.ts.map +1 -0
  83. package/dist/role-naming.js +96 -0
  84. package/dist/role-naming.js.map +1 -0
  85. package/dist/screenshot.d.ts +12 -20
  86. package/dist/screenshot.d.ts.map +1 -1
  87. package/dist/screenshot.js +179 -93
  88. package/dist/screenshot.js.map +1 -1
  89. package/dist/timers.d.ts +9 -0
  90. package/dist/timers.d.ts.map +1 -0
  91. package/dist/timers.js +21 -0
  92. package/dist/timers.js.map +1 -0
  93. package/dist/traversal.d.ts +7 -1
  94. package/dist/traversal.d.ts.map +1 -1
  95. package/dist/traversal.js +40 -6
  96. package/dist/traversal.js.map +1 -1
  97. package/dist/types.d.ts +32 -15
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/user-event/adjustment.d.ts +3 -0
  101. package/dist/user-event/adjustment.d.ts.map +1 -1
  102. package/dist/user-event/adjustment.js +25 -3
  103. package/dist/user-event/adjustment.js.map +1 -1
  104. package/dist/user-event/click.d.ts +44 -8
  105. package/dist/user-event/click.d.ts.map +1 -1
  106. package/dist/user-event/click.js +176 -44
  107. package/dist/user-event/click.js.map +1 -1
  108. package/dist/user-event/column-header-click.d.ts +4 -0
  109. package/dist/user-event/column-header-click.d.ts.map +1 -0
  110. package/dist/user-event/column-header-click.js +55 -0
  111. package/dist/user-event/column-header-click.js.map +1 -0
  112. package/dist/user-event/event-wrapper.d.ts.map +1 -1
  113. package/dist/user-event/event-wrapper.js +34 -20
  114. package/dist/user-event/event-wrapper.js.map +1 -1
  115. package/dist/user-event/index.d.ts +80 -13
  116. package/dist/user-event/index.d.ts.map +1 -1
  117. package/dist/user-event/index.js +44 -26
  118. package/dist/user-event/index.js.map +1 -1
  119. package/dist/user-event/indexed-children.d.ts +10 -0
  120. package/dist/user-event/indexed-children.d.ts.map +1 -0
  121. package/dist/user-event/indexed-children.js +50 -0
  122. package/dist/user-event/indexed-children.js.map +1 -0
  123. package/dist/user-event/keyboard.d.ts.map +1 -1
  124. package/dist/user-event/keyboard.js +115 -31
  125. package/dist/user-event/keyboard.js.map +1 -1
  126. package/dist/user-event/list-row-click.d.ts +4 -0
  127. package/dist/user-event/list-row-click.d.ts.map +1 -0
  128. package/dist/user-event/list-row-click.js +38 -0
  129. package/dist/user-event/list-row-click.js.map +1 -0
  130. package/dist/user-event/native-click.d.ts +6 -0
  131. package/dist/user-event/native-click.d.ts.map +1 -0
  132. package/dist/user-event/native-click.js +36 -0
  133. package/dist/user-event/native-click.js.map +1 -0
  134. package/dist/user-event/notebook-tab-click.d.ts +5 -0
  135. package/dist/user-event/notebook-tab-click.d.ts.map +1 -0
  136. package/dist/user-event/notebook-tab-click.js +39 -0
  137. package/dist/user-event/notebook-tab-click.js.map +1 -0
  138. package/dist/user-event/pointer.d.ts +1 -1
  139. package/dist/user-event/pointer.d.ts.map +1 -1
  140. package/dist/user-event/pointer.js +33 -9
  141. package/dist/user-event/pointer.js.map +1 -1
  142. package/dist/user-event/selection.d.ts +11 -8
  143. package/dist/user-event/selection.d.ts.map +1 -1
  144. package/dist/user-event/selection.js +49 -38
  145. package/dist/user-event/selection.js.map +1 -1
  146. package/dist/user-event/text.d.ts +12 -5
  147. package/dist/user-event/text.d.ts.map +1 -1
  148. package/dist/user-event/text.js +151 -46
  149. package/dist/user-event/text.js.map +1 -1
  150. package/dist/wait-for.d.ts.map +1 -1
  151. package/dist/wait-for.js +4 -3
  152. package/dist/wait-for.js.map +1 -1
  153. package/dist/widget-accessible-properties.d.ts +14 -26
  154. package/dist/widget-accessible-properties.d.ts.map +1 -1
  155. package/dist/widget-accessible-properties.js +135 -190
  156. package/dist/widget-accessible-properties.js.map +1 -1
  157. package/dist/widget-getters.d.ts +11 -0
  158. package/dist/widget-getters.d.ts.map +1 -0
  159. package/dist/widget-getters.js +46 -0
  160. package/dist/widget-getters.js.map +1 -0
  161. package/dist/window-state.d.ts +10 -0
  162. package/dist/window-state.d.ts.map +1 -0
  163. package/dist/window-state.js +41 -0
  164. package/dist/window-state.js.map +1 -0
  165. package/dist/within.d.ts +3 -2
  166. package/dist/within.d.ts.map +1 -1
  167. package/dist/within.js +3 -2
  168. package/dist/within.js.map +1 -1
  169. package/package.json +21 -8
  170. package/src/accessible-native.ts +333 -0
  171. package/src/act.ts +1 -1
  172. package/src/build-queries.ts +47 -41
  173. package/src/cleanup-registry.ts +1 -1
  174. package/src/config.ts +10 -1
  175. package/src/errors.ts +12 -1
  176. package/src/fire-event.ts +61 -8
  177. package/src/frame-sync.ts +30 -9
  178. package/src/harness-window.ts +23 -0
  179. package/src/index.ts +18 -11
  180. package/src/internal.ts +12 -0
  181. package/src/matchers.ts +457 -72
  182. package/src/pretty-format.ts +1 -0
  183. package/src/pretty-widget.ts +98 -70
  184. package/src/production.ts +51 -0
  185. package/src/queries.ts +127 -89
  186. package/src/query-helpers.ts +97 -0
  187. package/src/render-children.ts +25 -0
  188. package/src/render.tsx +77 -17
  189. package/src/role-helpers.ts +16 -5
  190. package/src/role-naming.ts +99 -0
  191. package/src/screenshot.ts +266 -104
  192. package/src/timers.ts +31 -0
  193. package/src/traversal.ts +66 -6
  194. package/src/types.ts +34 -17
  195. package/src/user-event/adjustment.ts +31 -3
  196. package/src/user-event/click.ts +248 -52
  197. package/src/user-event/column-header-click.ts +76 -0
  198. package/src/user-event/event-wrapper.ts +41 -23
  199. package/src/user-event/index.ts +130 -40
  200. package/src/user-event/indexed-children.ts +75 -0
  201. package/src/user-event/keyboard.ts +161 -43
  202. package/src/user-event/list-row-click.ts +54 -0
  203. package/src/user-event/native-click.ts +51 -0
  204. package/src/user-event/notebook-tab-click.ts +57 -0
  205. package/src/user-event/pointer.ts +48 -9
  206. package/src/user-event/selection.ts +67 -41
  207. package/src/user-event/text.ts +192 -46
  208. package/src/wait-for.ts +4 -4
  209. package/src/widget-accessible-properties.ts +183 -230
  210. package/src/widget-getters.ts +85 -0
  211. package/src/window-state.ts +70 -0
  212. 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,20 +27,20 @@ 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";
42
+ import { getTypeTag } from "./widget-getters.js";
43
+ import { isWindowActivated } from "./window-state.js";
27
44
 
28
45
  /** The expected value for a text matcher: an exact string or a regular expression. */
29
46
  type TextExpectation = string | RegExp;
@@ -40,15 +57,60 @@ type TextContentOptions = {
40
57
  /** The expected value for a style class: an exact class name or a regular expression. */
41
58
  type ClassExpectation = string | RegExp;
42
59
  /** The outcome of a matcher: whether it passed, and the failure text built on demand. */
43
- type MatcherResult = { pass: boolean; message: () => string };
60
+ type MatcherResult = Pick<SyncExpectationResult, "message" | "pass">;
61
+
44
62
  /** The matcher state bound as `this`, supplying the test runner's deep equality check. */
45
- type MatcherContext = { equals: (actual: unknown, expected: unknown) => boolean };
63
+ type MatcherContext = {
64
+ /** Compares a received value against an expected one, resolving asymmetric matchers. */
65
+ equals: (actual: unknown, expected: unknown) => boolean;
66
+ };
67
+
46
68
  /** Compares text read from the received widget; with no expected value, asserts the text is non-empty. */
47
69
  type TextMatcher = (received: unknown, expected?: TextExpectation) => MatcherResult;
48
70
  /** Asserts a single state of the received widget, taking no expected value. */
49
71
  type StateMatcher = (received: unknown) => MatcherResult;
72
+ /** Trailing arguments of the `ByRole` query family, as a containment matcher takes them. */
73
+ type RoleQueryArgs = [role: Gtk.AccessibleRole, options?: ByRoleOptions];
74
+ /** Trailing arguments of the text-based query families, as a containment matcher takes them. */
75
+ type TextQueryArgs = [text: Matcher, options?: MatcherOptions];
76
+ /** Counts the descendants a query family finds under the received widget. */
77
+ type ContainmentMatcher<Args extends unknown[]> = (received: unknown, ...args: Args) => MatcherResult;
50
78
  type TextMatcherContext = { matcherName: string; widget: Gtk.Widget; actual: string | null };
51
79
  type ClassArguments = { expected: ClassExpectation[]; isExact: boolean };
80
+ type QueryAll<Args extends unknown[]> = (container: Container, ...args: Args) => Gtk.Widget[];
81
+
82
+ type BooleanAccessibleState =
83
+ Gtk.AccessibleState.BUSY |
84
+ Gtk.AccessibleState.DISABLED |
85
+ Gtk.AccessibleState.EXPANDED |
86
+ Gtk.AccessibleState.HIDDEN |
87
+ Gtk.AccessibleState.SELECTED |
88
+ Gtk.AccessibleState.VISITED;
89
+
90
+ type TristateAccessibleState = Gtk.AccessibleState.CHECKED | Gtk.AccessibleState.PRESSED;
91
+
92
+ type StringAccessibleProperty =
93
+ Gtk.AccessibleProperty.DESCRIPTION |
94
+ Gtk.AccessibleProperty.HELP_TEXT |
95
+ Gtk.AccessibleProperty.KEY_SHORTCUTS |
96
+ Gtk.AccessibleProperty.LABEL |
97
+ Gtk.AccessibleProperty.PLACEHOLDER |
98
+ Gtk.AccessibleProperty.ROLE_DESCRIPTION |
99
+ Gtk.AccessibleProperty.VALUE_TEXT;
100
+
101
+ type BooleanAccessibleProperty =
102
+ Gtk.AccessibleProperty.HAS_POPUP |
103
+ Gtk.AccessibleProperty.MODAL |
104
+ Gtk.AccessibleProperty.MULTI_LINE |
105
+ Gtk.AccessibleProperty.MULTI_SELECTABLE |
106
+ Gtk.AccessibleProperty.READ_ONLY |
107
+ Gtk.AccessibleProperty.REQUIRED;
108
+
109
+ type NumberAccessibleProperty =
110
+ Gtk.AccessibleProperty.LEVEL |
111
+ Gtk.AccessibleProperty.VALUE_MAX |
112
+ Gtk.AccessibleProperty.VALUE_MIN |
113
+ Gtk.AccessibleProperty.VALUE_NOW;
52
114
 
53
115
  /** A text matcher that also takes normalization options for the text it reads. */
54
116
  type TextContentMatcher = (
@@ -72,23 +134,35 @@ type MatcherImplementations = {
72
134
  toHaveAccessibleDescription: TextMatcher;
73
135
  /** Asserts the joined accessible names of the widget's `error-message` targets, read only while it is invalid. */
74
136
  toHaveAccessibleErrorMessage: TextMatcher;
75
- /** Asserts the widget's placeholder text; with no argument, that it has some. */
76
- toHavePlaceholderText: TextMatcher;
77
137
  /** Asserts the text currently selected in an editable widget. */
78
138
  toHaveSelection: TextMatcher;
139
+ /**
140
+ * Asserts an accessible state GTK holds for the widget, taking the value type that state carries.
141
+ * With no expected value, asserts only that the state is set to something determinate.
142
+ */
143
+ toHaveAccessibleState: (
144
+ received: unknown,
145
+ state: Gtk.AccessibleState,
146
+ expected?: boolean | number,
147
+ ) => MatcherResult;
148
+ /**
149
+ * Asserts an accessible property GTK holds for the widget, taking the value type that property
150
+ * carries. With no expected value, asserts only that the property is set. Numeric properties
151
+ * compare the way `toHaveValue` and the `value` option of the `ByRole` queries do, within 0.001.
152
+ */
153
+ toHaveAccessibleProperty: (
154
+ received: unknown,
155
+ property: Gtk.AccessibleProperty,
156
+ expected?: AccessibleAttributeValue,
157
+ ) => MatcherResult;
79
158
  /** Asserts the widget's checked state is checked rather than unchecked or mixed. */
80
159
  toBeChecked: StateMatcher;
81
160
  /** Asserts the widget's checked state is mixed. */
82
161
  toBePartiallyChecked: StateMatcher;
83
162
  /** Asserts a `Gtk.ToggleButton` is active, and throws for any other widget. */
84
163
  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;
164
+ /** Asserts the widget's accessible pressed state is mixed, and throws when it exposes none. */
165
+ toBePartiallyPressed: StateMatcher;
92
166
  /** Asserts the widget is insensitive, or sits inside an insensitive ancestor. */
93
167
  toBeDisabled: StateMatcher;
94
168
  /** Asserts the widget and all of its ancestors are sensitive. */
@@ -98,7 +172,7 @@ type MatcherImplementations = {
98
172
  /** Asserts the widget's root is a window that is still in the toplevel list. */
99
173
  toBeRooted: StateMatcher;
100
174
  /** Asserts the widget has neither a child nor label text. */
101
- toBeEmpty: StateMatcher;
175
+ toBeEmptyWidget: StateMatcher;
102
176
  /** Asserts the widget's accessible invalid state is set to anything other than false. */
103
177
  toBeInvalid: StateMatcher;
104
178
  /** Asserts the widget's accessible invalid state is unset or false. */
@@ -107,12 +181,45 @@ type MatcherImplementations = {
107
181
  toBeRequired: StateMatcher;
108
182
  /** Asserts the widget holds the platform focus state. */
109
183
  toHaveFocus: StateMatcher;
110
- /** Asserts a widget's numeric value, or its display value when a string is given. */
184
+ /**
185
+ * Asserts a widget's numeric value, or its display value when a string is given.
186
+ *
187
+ * Numbers come from the accessibility tree, which GTK keeps to a resolution of 0.001: it drops an
188
+ * update whose value is nearer than that to the one already published, so a widget that moves by
189
+ * less keeps its previous value. Numbers therefore match within 0.001, the same tolerance the
190
+ * `value` option of the `ByRole` queries and `toHaveAccessibleProperty` apply. That option reads
191
+ * the same tree, where the maximum of a paged widget such as a scrollbar is the last value it can
192
+ * reach, its adjustment's upper bound less one page.
193
+ */
111
194
  toHaveValue: (received: unknown, expected?: number | string) => MatcherResult;
112
195
  /** Asserts the widget's accessible role. */
113
196
  toHaveRole: (received: unknown, expected: Gtk.AccessibleRole) => MatcherResult;
114
197
  /** Asserts the widget is, or is an ancestor of, the given widget. */
115
198
  toContainElement: (received: unknown, descendant: Gtk.Widget | null) => MatcherResult;
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;
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;
203
+ /** Asserts at least one widget under the received one matches the `ByRole` query. */
204
+ toContainAnyByRole: ContainmentMatcher<RoleQueryArgs>;
205
+ /** Asserts exactly one widget under the received one matches the `ByRole` query. */
206
+ toContainOneByRole: ContainmentMatcher<RoleQueryArgs>;
207
+ /** Asserts at least one widget under the received one matches the `ByText` query. */
208
+ toContainAnyByText: ContainmentMatcher<TextQueryArgs>;
209
+ /** Asserts exactly one widget under the received one matches the `ByText` query. */
210
+ toContainOneByText: ContainmentMatcher<TextQueryArgs>;
211
+ /** Asserts at least one widget under the received one matches the `ByLabelText` query. */
212
+ toContainAnyByLabelText: ContainmentMatcher<TextQueryArgs>;
213
+ /** Asserts exactly one widget under the received one matches the `ByLabelText` query. */
214
+ toContainOneByLabelText: ContainmentMatcher<TextQueryArgs>;
215
+ /** Asserts at least one widget under the received one matches the `ByPlaceholderText` query. */
216
+ toContainAnyByPlaceholderText: ContainmentMatcher<TextQueryArgs>;
217
+ /** Asserts exactly one widget under the received one matches the `ByPlaceholderText` query. */
218
+ toContainOneByPlaceholderText: ContainmentMatcher<TextQueryArgs>;
219
+ /** Asserts at least one widget under the received one matches the `ByDisplayValue` query. */
220
+ toContainAnyByDisplayValue: ContainmentMatcher<TextQueryArgs>;
221
+ /** Asserts exactly one widget under the received one matches the `ByDisplayValue` query. */
222
+ toContainOneByDisplayValue: ContainmentMatcher<TextQueryArgs>;
116
223
  /**
117
224
  * Asserts the widget carries every given style class, each a name, a space-separated list, or a pattern.
118
225
  * Pass `{ exact: true }` last to require exactly that set, which rules out patterns.
@@ -127,7 +234,18 @@ type MatcherImplementations = {
127
234
 
128
235
  type ExpectExtend = { extend: (m: MatcherImplementations) => void };
129
236
 
237
+ type AttributeExpectation = {
238
+ label: string;
239
+ comparison: string;
240
+ expected: AccessibleAttributeValue;
241
+ actual: AccessibleAttributeValue | null;
242
+ isPass: boolean;
243
+ isReadingRounded: boolean;
244
+ };
245
+
130
246
  const registration = { isRegistered: false };
247
+ const MIXED_TRISTATE: number = Gtk.AccessibleTristate.MIXED;
248
+ const ROUNDED_READING = ", which the accessibility tree rounds to six significant digits";
131
249
  const displayValueMatcher: TextMatcher = textMatcher("toHaveDisplayValue", getWidgetDisplayValue, "exact");
132
250
  const toHaveAccessibleName: TextMatcher = textMatcher("toHaveAccessibleName", getWidgetAccessibleName, "exact");
133
251
 
@@ -143,13 +261,70 @@ const toHaveAccessibleErrorMessage: TextMatcher = textMatcher(
143
261
  "exact",
144
262
  );
145
263
 
146
- const toHavePlaceholderText: TextMatcher = textMatcher("toHavePlaceholderText", getWidgetPlaceholderText, "exact");
147
264
  const toHaveSelection: TextMatcher = textMatcher("toHaveSelection", getWidgetSelection, "exact");
148
265
  const toBePressed: StateMatcher = booleanStateMatcher("toBePressed", "pressed", getWidgetPressedState);
149
- const toBeExpanded: StateMatcher = booleanStateMatcher("toBeExpanded", "expanded", getWidgetExpandedState);
150
- const toBeSelected: StateMatcher = booleanStateMatcher("toBeSelected", "selected", getWidgetSelectedState);
151
266
  const toBeRequired: StateMatcher = booleanStateMatcher("toBeRequired", "required", getWidgetRequiredState);
152
267
 
268
+ const toContainAnyByRole: ContainmentMatcher<RoleQueryArgs> = containmentMatcher(
269
+ "toContainAnyByRole",
270
+ false,
271
+ queryAllByRole,
272
+ );
273
+
274
+ const toContainOneByRole: ContainmentMatcher<RoleQueryArgs> = containmentMatcher(
275
+ "toContainOneByRole",
276
+ true,
277
+ queryAllByRole,
278
+ );
279
+
280
+ const toContainAnyByText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
281
+ "toContainAnyByText",
282
+ false,
283
+ queryAllByText,
284
+ );
285
+
286
+ const toContainOneByText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
287
+ "toContainOneByText",
288
+ true,
289
+ queryAllByText,
290
+ );
291
+
292
+ const toContainAnyByLabelText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
293
+ "toContainAnyByLabelText",
294
+ false,
295
+ queryAllByLabelText,
296
+ );
297
+
298
+ const toContainOneByLabelText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
299
+ "toContainOneByLabelText",
300
+ true,
301
+ queryAllByLabelText,
302
+ );
303
+
304
+ const toContainAnyByPlaceholderText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
305
+ "toContainAnyByPlaceholderText",
306
+ false,
307
+ queryAllByPlaceholderText,
308
+ );
309
+
310
+ const toContainOneByPlaceholderText: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
311
+ "toContainOneByPlaceholderText",
312
+ true,
313
+ queryAllByPlaceholderText,
314
+ );
315
+
316
+ const toContainAnyByDisplayValue: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
317
+ "toContainAnyByDisplayValue",
318
+ false,
319
+ queryAllByDisplayValue,
320
+ );
321
+
322
+ const toContainOneByDisplayValue: ContainmentMatcher<TextQueryArgs> = containmentMatcher(
323
+ "toContainOneByDisplayValue",
324
+ true,
325
+ queryAllByDisplayValue,
326
+ );
327
+
153
328
  /** The widget assertion matchers keyed by name, suitable for passing to `expect.extend`. */
154
329
  const matchers: MatcherImplementations = {
155
330
  toHaveDisplayValue,
@@ -157,18 +332,18 @@ const matchers: MatcherImplementations = {
157
332
  toHaveAccessibleName,
158
333
  toHaveAccessibleDescription,
159
334
  toHaveAccessibleErrorMessage,
160
- toHavePlaceholderText,
161
335
  toHaveSelection,
336
+ toHaveAccessibleState,
337
+ toHaveAccessibleProperty,
162
338
  toBeChecked,
163
339
  toBePartiallyChecked,
164
340
  toBePressed,
165
- toBeExpanded,
166
- toBeSelected,
341
+ toBePartiallyPressed,
167
342
  toBeDisabled,
168
343
  toBeEnabled,
169
344
  toBeVisible,
170
345
  toBeRooted,
171
- toBeEmpty,
346
+ toBeEmptyWidget,
172
347
  toBeInvalid,
173
348
  toBeValid,
174
349
  toBeRequired,
@@ -176,6 +351,18 @@ const matchers: MatcherImplementations = {
176
351
  toHaveValue,
177
352
  toHaveRole,
178
353
  toContainElement,
354
+ toAppearBefore,
355
+ toAppearAfter,
356
+ toContainAnyByRole,
357
+ toContainOneByRole,
358
+ toContainAnyByText,
359
+ toContainOneByText,
360
+ toContainAnyByLabelText,
361
+ toContainOneByLabelText,
362
+ toContainAnyByPlaceholderText,
363
+ toContainOneByPlaceholderText,
364
+ toContainAnyByDisplayValue,
365
+ toContainOneByDisplayValue,
179
366
  toHaveClass,
180
367
  toHaveObjectProperty,
181
368
  };
@@ -204,7 +391,7 @@ const describeWidget = (widget: Gtk.Widget): string => {
204
391
  };
205
392
 
206
393
  const describeObject = (object: GObject.Object): string =>
207
- object instanceof Gtk.Widget ? describeWidget(object) : `<${object.constructor.name}>`;
394
+ object instanceof Gtk.Widget ? describeWidget(object) : `<${getTypeTag(object)}>`;
208
395
 
209
396
  const isTextMatch = (actual: string, expected: TextExpectation, mode: "exact" | "substring"): boolean => {
210
397
  if (expected instanceof RegExp) {
@@ -244,6 +431,100 @@ const stateResult = (widget: Gtk.Widget, stateName: string, isPass: boolean): Ma
244
431
  message: () => `expected widget ${negationPrefix(isPass)}to be ${stateName}\n${describeWidget(widget)}`,
245
432
  });
246
433
 
434
+ const isHoldingFocus = (widget: Gtk.Widget, root: Gtk.Window): boolean => {
435
+ const focus = root.getFocus();
436
+
437
+ return focus !== null && (focus === widget || focus.isAncestor(widget));
438
+ };
439
+
440
+ const isFocusHeldByInactiveWindow = (widget: Gtk.Widget): boolean => {
441
+ const root = widget.getRoot();
442
+
443
+ return root instanceof Gtk.Window && !isWindowActivated(root) && isHoldingFocus(widget, root);
444
+ };
445
+
446
+ const focusResult = (widget: Gtk.Widget, isPass: boolean): MatcherResult => {
447
+ if (isPass || !isFocusHeldByInactiveWindow(widget)) {
448
+ return stateResult(widget, "focused", isPass);
449
+ }
450
+
451
+ return {
452
+ pass: false,
453
+ message: () =>
454
+ "expected widget to be focused: its window has given it the focus, " +
455
+ `but that window is not active\n${describeWidget(widget)}`,
456
+ };
457
+ };
458
+
459
+ const describeAttributeValue = (value: AccessibleAttributeValue | null): string =>
460
+ value === null ? "unset" : JSON.stringify(value);
461
+
462
+ const attributeSetResult = (
463
+ widget: Gtk.Widget,
464
+ label: string,
465
+ actual: AccessibleAttributeValue | null,
466
+ ): MatcherResult => {
467
+ const isSet = actual !== null;
468
+
469
+ return {
470
+ pass: isSet,
471
+ message: () => `expected widget ${negationPrefix(isSet)}to have ${label} set\n${describeWidget(widget)}`,
472
+ };
473
+ };
474
+
475
+ const describeReading = (expectation: AttributeExpectation): string =>
476
+ `${describeAttributeValue(expectation.actual)}${expectation.isReadingRounded ? ROUNDED_READING : ""}`;
477
+
478
+ const attributeValueResult = (widget: Gtk.Widget, expectation: AttributeExpectation): MatcherResult => ({
479
+ pass: expectation.isPass,
480
+ message: () =>
481
+ `expected widget ${negationPrefix(expectation.isPass)}to have ${expectation.label} ` +
482
+ `${expectation.comparison} ${describeAttributeValue(expectation.expected)}, but received ` +
483
+ `${describeReading(expectation)}\n${describeWidget(widget)}`,
484
+ });
485
+
486
+ const orderResult = (widget: Gtk.Widget, other: Gtk.Widget, position: string, isPass: boolean): MatcherResult => ({
487
+ pass: isPass,
488
+ message: () =>
489
+ `expected widget ${negationPrefix(isPass)}to appear ${position} ${describeWidget(other)} ` +
490
+ `in the widget tree\n${describeWidget(widget)}`,
491
+ });
492
+
493
+ const getTreeRoot = (widget: Gtk.Widget): Gtk.Widget => {
494
+ let root = widget;
495
+ let parent = root.getParent();
496
+
497
+ while (parent) {
498
+ root = parent;
499
+ parent = root.getParent();
500
+ }
501
+
502
+ return root;
503
+ };
504
+
505
+ const isSeparateWidget = (first: Gtk.Widget, second: Gtk.Widget): boolean =>
506
+ first !== second && !first.isAncestor(second) && !second.isAncestor(first);
507
+
508
+ const isPrecedingWidget = (root: Gtk.Widget, first: Gtk.Widget, second: Gtk.Widget): boolean => {
509
+ for (const widget of descendants(root)) {
510
+ if (widget === first) {
511
+ return true;
512
+ }
513
+
514
+ if (widget === second) {
515
+ return false;
516
+ }
517
+ }
518
+
519
+ return false;
520
+ };
521
+
522
+ const isAppearingBefore = (first: Gtk.Widget, second: Gtk.Widget): boolean => {
523
+ const root = getTreeRoot(first);
524
+
525
+ return isSeparateWidget(first, second) && root === getTreeRoot(second) && isPrecedingWidget(root, first, second);
526
+ };
527
+
247
528
  const camelCase = (name: string): string =>
248
529
  name.replaceAll(/-([a-z])/g, (_match, letter: string) => letter.toUpperCase());
249
530
 
@@ -426,6 +707,117 @@ function booleanStateMatcher(
426
707
  };
427
708
  }
428
709
 
710
+ function containmentMatcher<Args extends unknown[]>(
711
+ matcherName: string,
712
+ isSingle: boolean,
713
+ queryAll: QueryAll<Args>,
714
+ ): ContainmentMatcher<Args> {
715
+ const quantity = isSingle ? "exactly one widget" : "any widget";
716
+
717
+ return (received: unknown, ...args: Args): MatcherResult => {
718
+ const widget = asWidget(received, matcherName);
719
+ const matches = queryAll(widget, ...args);
720
+ const isPass = isSingle ? matches.length === 1 : matches.length > 0;
721
+
722
+ return {
723
+ pass: isPass,
724
+ message: () =>
725
+ `expected widget ${negationPrefix(isPass)}to contain ${quantity} matching ${matcherName}, ` +
726
+ `but found ${String(matches.length)}\n${describeWidget(widget)}`,
727
+ };
728
+ };
729
+ }
730
+
731
+ function toHaveAccessibleState(
732
+ received: unknown,
733
+ state: Gtk.AccessibleState,
734
+ expected?: boolean | number,
735
+ ): MatcherResult {
736
+ const widget = asWidget(received, "toHaveAccessibleState");
737
+ const actual = readAccessibleState(widget, state);
738
+ const label = `accessible state ${Gtk.AccessibleState[state]}`;
739
+
740
+ if (expected === undefined) {
741
+ return attributeSetResult(widget, label, actual);
742
+ }
743
+
744
+ const wanted = typeof expected === "boolean" ? Number(expected) : expected;
745
+
746
+ return attributeValueResult(widget, {
747
+ label,
748
+ comparison: "equal to",
749
+ expected: wanted,
750
+ actual,
751
+ isPass: actual === wanted,
752
+ isReadingRounded: false,
753
+ });
754
+ }
755
+
756
+ const propertyLabel = (property: Gtk.AccessibleProperty): string =>
757
+ `accessible property ${Gtk.AccessibleProperty[property]}`;
758
+
759
+ const propertyExpectation = (
760
+ widget: Gtk.Widget,
761
+ property: Gtk.AccessibleProperty,
762
+ expected: AccessibleAttributeValue,
763
+ actual: AccessibleAttributeValue | null,
764
+ ): AttributeExpectation => {
765
+ const label = propertyLabel(property);
766
+
767
+ if (typeof expected === "number" && isAccessibleNumberProperty(property)) {
768
+ return {
769
+ label,
770
+ comparison: `within ${String(ACCESSIBLE_NUMBER_TOLERANCE)} of`,
771
+ expected,
772
+ actual,
773
+ isPass: isAccessibleNumberMatch(widget, property, expected),
774
+ isReadingRounded: actual !== null,
775
+ };
776
+ }
777
+
778
+ return { label, comparison: "equal to", expected, actual, isPass: actual === expected, isReadingRounded: false };
779
+ };
780
+
781
+ function toHaveAccessibleProperty(
782
+ received: unknown,
783
+ property: Gtk.AccessibleProperty,
784
+ expected?: AccessibleAttributeValue,
785
+ ): MatcherResult {
786
+ const widget = asWidget(received, "toHaveAccessibleProperty");
787
+ const actual = readAccessibleProperty(widget, property);
788
+
789
+ if (expected === undefined) {
790
+ return attributeSetResult(widget, propertyLabel(property), actual);
791
+ }
792
+
793
+ return attributeValueResult(widget, propertyExpectation(widget, property, expected, actual));
794
+ }
795
+
796
+ function toBePartiallyPressed(received: unknown): MatcherResult {
797
+ const widget = asWidget(received, "toBePartiallyPressed");
798
+ const state = readAccessibleState(widget, Gtk.AccessibleState.PRESSED);
799
+
800
+ if (state === null) {
801
+ throw notApplicable("toBePartiallyPressed", "pressed state", widget);
802
+ }
803
+
804
+ return stateResult(widget, "partially pressed", state === MIXED_TRISTATE);
805
+ }
806
+
807
+ function toAppearBefore(received: unknown, other: Gtk.Widget): MatcherResult {
808
+ const widget = asWidget(received, "toAppearBefore");
809
+ const target = asWidget(other, "toAppearBefore");
810
+
811
+ return orderResult(widget, target, "before", isAppearingBefore(widget, target));
812
+ }
813
+
814
+ function toAppearAfter(received: unknown, other: Gtk.Widget): MatcherResult {
815
+ const widget = asWidget(received, "toAppearAfter");
816
+ const target = asWidget(other, "toAppearAfter");
817
+
818
+ return orderResult(widget, target, "after", isAppearingBefore(target, widget));
819
+ }
820
+
429
821
  function readCheckedState(widget: Gtk.Widget, matcherName: string): CheckedState {
430
822
  const state = getWidgetCheckedState(widget);
431
823
 
@@ -458,6 +850,9 @@ function toHaveDisplayValue(received: unknown, expected?: TextExpectation): Matc
458
850
  return displayValueMatcher(received, expected);
459
851
  }
460
852
 
853
+ const describeValueExpectation = (expected: number | undefined): string =>
854
+ expected === undefined ? "set" : `within ${String(ACCESSIBLE_NUMBER_TOLERANCE)} of ${String(expected)}`;
855
+
461
856
  function toHaveValue(received: unknown, expected?: number | string): MatcherResult {
462
857
  const widget = asWidget(received, "toHaveValue");
463
858
 
@@ -465,19 +860,19 @@ function toHaveValue(received: unknown, expected?: number | string): MatcherResu
465
860
  return displayValueMatcher(received, expected);
466
861
  }
467
862
 
468
- const actual = getWidgetValue(widget).now;
863
+ const actual = getWidgetValueNow(widget);
469
864
 
470
865
  if (actual === null) {
471
866
  throw notApplicable("toHaveValue", "numeric value", widget);
472
867
  }
473
868
 
474
- const isPass = expected === undefined || actual === expected;
869
+ const isPass = expected === undefined || isWidgetValueMatch(widget, "now", expected);
475
870
 
476
871
  return {
477
872
  pass: isPass,
478
873
  message: () =>
479
- `expected widget ${negationPrefix(isPass)}to have value ${String(expected)}, ` +
480
- `but received ${String(actual)}\n${describeWidget(widget)}`,
874
+ `expected widget ${negationPrefix(isPass)}to have value ${describeValueExpectation(expected)}, ` +
875
+ `but received ${String(actual)}${ROUNDED_READING}\n${describeWidget(widget)}`,
481
876
  };
482
877
  }
483
878
 
@@ -509,8 +904,8 @@ function toBeRooted(received: unknown): MatcherResult {
509
904
  return stateResult(widget, "rooted in a window", isWidgetRooted(widget));
510
905
  }
511
906
 
512
- function toBeEmpty(received: unknown): MatcherResult {
513
- const widget = asWidget(received, "toBeEmpty");
907
+ function toBeEmptyWidget(received: unknown): MatcherResult {
908
+ const widget = asWidget(received, "toBeEmptyWidget");
514
909
 
515
910
  return stateResult(widget, "empty", widget.getFirstChild() === null && getWidgetLabelText(widget) === null);
516
911
  }
@@ -532,7 +927,7 @@ function toBeValid(received: unknown): MatcherResult {
532
927
  function toHaveFocus(received: unknown): MatcherResult {
533
928
  const widget = asWidget(received, "toHaveFocus");
534
929
 
535
- return stateResult(widget, "focused", widget.getPlatformState(Gtk.AccessiblePlatformState.FOCUSED));
930
+ return focusResult(widget, widget.getPlatformState(Gtk.AccessiblePlatformState.FOCUSED));
536
931
  }
537
932
 
538
933
  function toHaveRole(received: unknown, expected: Gtk.AccessibleRole): MatcherResult {
@@ -599,8 +994,6 @@ function toHaveObjectProperty(this: MatcherContext, received: unknown, ...args:
599
994
  };
600
995
  }
601
996
 
602
- /* eslint-disable-next-line unicorn/no-this-outside-of-class --
603
- expect.extend invokes matchers with the matcher state as `this` */
604
997
  const isPass = isEqualValue(this, actual, expected);
605
998
 
606
999
  return {
@@ -635,18 +1028,30 @@ declare module "@vitest/expect" {
635
1028
  toHaveAccessibleName(expected?: TextExpectation): void;
636
1029
  toHaveAccessibleDescription(expected?: TextExpectation): void;
637
1030
  toHaveAccessibleErrorMessage(expected?: TextExpectation): void;
638
- toHavePlaceholderText(expected?: TextExpectation): void;
639
1031
  toHaveSelection(expected?: TextExpectation): void;
1032
+ /* 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;
1037
+ /* 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;
1040
+ toHaveAccessibleProperty(
1041
+ property: Gtk.AccessibleProperty.AUTOCOMPLETE,
1042
+ 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;
640
1046
  toBeChecked(): void;
641
1047
  toBePartiallyChecked(): void;
642
1048
  toBePressed(): void;
643
- toBeExpanded(): void;
644
- toBeSelected(): void;
1049
+ toBePartiallyPressed(): void;
645
1050
  toBeDisabled(): void;
646
1051
  toBeEnabled(): void;
647
1052
  toBeVisible(): void;
648
1053
  toBeRooted(): void;
649
- toBeEmpty(): void;
1054
+ toBeEmptyWidget(): void;
650
1055
  toBeInvalid(): void;
651
1056
  toBeValid(): void;
652
1057
  toBeRequired(): void;
@@ -654,6 +1059,18 @@ declare module "@vitest/expect" {
654
1059
  toHaveValue(expected?: number | string): void;
655
1060
  toHaveRole(expected: Gtk.AccessibleRole): void;
656
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;
657
1074
  toHaveClass(...args: (ClassExpectation | { exact: boolean })[]): void;
658
1075
  toHaveObjectProperty(name: string, expected?: unknown): void;
659
1076
  }
@@ -663,36 +1080,4 @@ declare module "@vitest/expect" {
663
1080
  /* eslint-enable @typescript-eslint/consistent-type-definitions */
664
1081
  }
665
1082
 
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
- };
1083
+ export { matchers, registerMatchers, type ClassExpectation, type TextContentOptions, type TextExpectation };