@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/queries.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  getWidgetDescription,
18
18
  getWidgetDisplayValue,
19
19
  getWidgetExpandedState,
20
- getWidgetLabelledByText,
20
+ getWidgetExternalLabelText,
21
21
  getWidgetLabelText,
22
22
  getWidgetLevel,
23
23
  getWidgetName,
@@ -25,18 +25,21 @@ import {
25
25
  getWidgetPlaceholderText,
26
26
  getWidgetPressedState,
27
27
  getWidgetSelectedState,
28
- getWidgetValue,
28
+ getWidgetValueText,
29
29
  isInaccessible,
30
30
  isWidgetChecked,
31
+ isWidgetValueMatch,
32
+ namingLabelText,
33
+ type WidgetValueField,
31
34
  } from "./widget-accessible-properties.js";
32
35
 
36
+ /** The built-in queries in their unbound form, each taking the container to search as its first argument. */
33
37
  type BuiltinQueries = QueryFamilies<[container: Container]>;
34
38
 
35
39
  const roleQueries = nameQueryFamily(
36
40
  "Role",
37
41
  queryAllByRole,
38
42
  buildQueries<[role: Gtk.AccessibleRole, options?: ByRoleOptions]>(
39
- "Role",
40
43
  queryAllByRole,
41
44
  (container, matches, role, options) =>
42
45
  multipleFoundError(container, { queryType: "role", role, options }, matches),
@@ -48,7 +51,6 @@ const labelTextQueries = nameQueryFamily(
48
51
  "LabelText",
49
52
  queryAllByLabelText,
50
53
  buildQueries<[text: Matcher, options?: MatcherOptions]>(
51
- "LabelText",
52
54
  queryAllByLabelText,
53
55
  (container, matches, text) => multipleFoundError(container, { queryType: "labelText", text }, matches),
54
56
  (container, text) => notFoundError(container, { queryType: "labelText", text }),
@@ -59,7 +61,6 @@ const textQueries = nameQueryFamily(
59
61
  "Text",
60
62
  queryAllByText,
61
63
  buildQueries<[text: Matcher, options?: MatcherOptions]>(
62
- "Text",
63
64
  queryAllByText,
64
65
  (container, matches, text) => multipleFoundError(container, { queryType: "text", text }, matches),
65
66
  (container, text) => notFoundError(container, { queryType: "text", text }),
@@ -70,7 +71,6 @@ const nameQueries = nameQueryFamily(
70
71
  "Name",
71
72
  queryAllByName,
72
73
  buildQueries<[name: Matcher, options?: MatcherOptions]>(
73
- "Name",
74
74
  queryAllByName,
75
75
  (container, matches, name) => multipleFoundError(container, { queryType: "name", name }, matches),
76
76
  (container, name) => notFoundError(container, { queryType: "name", name }),
@@ -81,7 +81,6 @@ const placeholderTextQueries = nameQueryFamily(
81
81
  "PlaceholderText",
82
82
  queryAllByPlaceholderText,
83
83
  buildQueries<[text: Matcher, options?: MatcherOptions]>(
84
- "PlaceholderText",
85
84
  queryAllByPlaceholderText,
86
85
  (container, matches, text) => multipleFoundError(container, { queryType: "placeholderText", text }, matches),
87
86
  (container, text) => notFoundError(container, { queryType: "placeholderText", text }),
@@ -92,7 +91,6 @@ const displayValueQueries = nameQueryFamily(
92
91
  "DisplayValue",
93
92
  queryAllByDisplayValue,
94
93
  buildQueries<[value: Matcher, options?: MatcherOptions]>(
95
- "DisplayValue",
96
94
  queryAllByDisplayValue,
97
95
  (container, matches, value) => multipleFoundError(container, { queryType: "displayValue", value }, matches),
98
96
  (container, value) => notFoundError(container, { queryType: "displayValue", value }),
@@ -109,120 +107,159 @@ const builtinQueries = {
109
107
  } as BuiltinQueries;
110
108
 
111
109
  /**
112
- * Returns the single widget with a matching accessible role and options, or null when none match.
113
- * Throws when more than one matches.
110
+ * Returns the single mapped widget with a matching accessible role and options, or null when none
111
+ * match. Widgets that are not mapped are never matched. Throws when more than one matches.
114
112
  */
115
113
  const queryByRole: BuiltinQueries["queryByRole"] = builtinQueries.queryByRole;
116
- /** Returns every widget with a matching accessible role and options. Throws when none match. */
114
+ /**
115
+ * Returns every mapped widget with a matching accessible role and options. Widgets that are not
116
+ * mapped are never matched. Throws when none match.
117
+ */
117
118
  const getAllByRole: BuiltinQueries["getAllByRole"] = builtinQueries.getAllByRole;
118
119
  /**
119
- * Returns the single widget with a matching accessible role and options.
120
- * Throws when none or more than one matches.
120
+ * Returns the single mapped widget with a matching accessible role and options. Widgets that are not
121
+ * mapped are never matched. Throws when none or more than one matches.
121
122
  */
122
123
  const getByRole: BuiltinQueries["getByRole"] = builtinQueries.getByRole;
123
124
  /**
124
- * Waits for and returns every widget with a matching accessible role and options, retrying until at
125
- * least one appears or the timeout elapses.
125
+ * Waits for and returns every mapped widget with a matching accessible role and options, retrying
126
+ * until at least one appears or the timeout elapses. Widgets that are not mapped are never matched.
126
127
  */
127
128
  const findAllByRole: BuiltinQueries["findAllByRole"] = builtinQueries.findAllByRole;
128
129
  /**
129
- * Waits for and returns the single widget with a matching accessible role and options, retrying
130
- * until it appears or the timeout elapses. Rejects when none or more than one matches.
130
+ * Waits for and returns the single mapped widget with a matching accessible role and options,
131
+ * retrying until it appears or the timeout elapses. Widgets that are not mapped are never matched.
132
+ * Rejects when none or more than one matches.
131
133
  */
132
134
  const findByRole: BuiltinQueries["findByRole"] = builtinQueries.findByRole;
133
135
  /**
134
- * Returns the single widget with matching associated label text, or null when none match.
135
- * Throws when more than one matches.
136
+ * Returns the single mapped widget with matching associated label text, or null when none match.
137
+ * Widgets that are not mapped are never matched. Throws when more than one matches.
136
138
  */
137
139
  const queryByLabelText: BuiltinQueries["queryByLabelText"] = builtinQueries.queryByLabelText;
138
- /** Returns every widget with matching associated label text. Throws when none match. */
140
+ /**
141
+ * Returns every mapped widget with matching associated label text. Widgets that are not mapped are
142
+ * never matched. Throws when none match.
143
+ */
139
144
  const getAllByLabelText: BuiltinQueries["getAllByLabelText"] = builtinQueries.getAllByLabelText;
140
- /** Returns the single widget with matching associated label text. Throws when none or more than one matches. */
145
+ /**
146
+ * Returns the single mapped widget with matching associated label text. Widgets that are not mapped
147
+ * are never matched. Throws when none or more than one matches.
148
+ */
141
149
  const getByLabelText: BuiltinQueries["getByLabelText"] = builtinQueries.getByLabelText;
142
150
  /**
143
- * Waits for and returns every widget with matching associated label text, retrying until at least
144
- * one appears or the timeout elapses.
151
+ * Waits for and returns every mapped widget with matching associated label text, retrying until at
152
+ * least one appears or the timeout elapses. Widgets that are not mapped are never matched.
145
153
  */
146
154
  const findAllByLabelText: BuiltinQueries["findAllByLabelText"] = builtinQueries.findAllByLabelText;
147
155
  /**
148
- * Waits for and returns the single widget with matching associated label text, retrying until it
149
- * appears or the timeout elapses. Rejects when none or more than one matches.
156
+ * Waits for and returns the single mapped widget with matching associated label text, retrying until
157
+ * it appears or the timeout elapses. Widgets that are not mapped are never matched. Rejects when
158
+ * none or more than one matches.
150
159
  */
151
160
  const findByLabelText: BuiltinQueries["findByLabelText"] = builtinQueries.findByLabelText;
152
161
  /**
153
- * Returns the single widget with matching rendered text content, or null when none match.
154
- * Throws when more than one matches.
162
+ * Returns the single mapped widget with matching rendered text content, or null when none match.
163
+ * Widgets that are not mapped are never matched. Throws when more than one matches.
155
164
  */
156
165
  const queryByText: BuiltinQueries["queryByText"] = builtinQueries.queryByText;
157
- /** Returns every widget with matching rendered text content. Throws when none match. */
166
+ /**
167
+ * Returns every mapped widget with matching rendered text content. Widgets that are not mapped are
168
+ * never matched. Throws when none match.
169
+ */
158
170
  const getAllByText: BuiltinQueries["getAllByText"] = builtinQueries.getAllByText;
159
- /** Returns the single widget with matching rendered text content. Throws when none or more than one matches. */
171
+ /**
172
+ * Returns the single mapped widget with matching rendered text content. Widgets that are not mapped
173
+ * are never matched. Throws when none or more than one matches.
174
+ */
160
175
  const getByText: BuiltinQueries["getByText"] = builtinQueries.getByText;
161
176
  /**
162
- * Waits for and returns every widget with matching rendered text content, retrying until at least
163
- * one appears or the timeout elapses.
177
+ * Waits for and returns every mapped widget with matching rendered text content, retrying until at
178
+ * least one appears or the timeout elapses. Widgets that are not mapped are never matched.
164
179
  */
165
180
  const findAllByText: BuiltinQueries["findAllByText"] = builtinQueries.findAllByText;
166
181
  /**
167
- * Waits for and returns the single widget with matching rendered text content, retrying until it
168
- * appears or the timeout elapses. Rejects when none or more than one matches.
182
+ * Waits for and returns the single mapped widget with matching rendered text content, retrying until
183
+ * it appears or the timeout elapses. Widgets that are not mapped are never matched. Rejects when
184
+ * none or more than one matches.
169
185
  */
170
186
  const findByText: BuiltinQueries["findByText"] = builtinQueries.findByText;
171
187
  /**
172
- * Returns the single widget with a matching widget name, or null when none match.
173
- * Throws when more than one matches.
188
+ * Returns the single mapped widget with a matching widget name, or null when none match. Widgets
189
+ * that are not mapped are never matched. Throws when more than one matches.
174
190
  */
175
191
  const queryByName: BuiltinQueries["queryByName"] = builtinQueries.queryByName;
176
- /** Returns every widget with a matching widget name. Throws when none match. */
192
+ /**
193
+ * Returns every mapped widget with a matching widget name. Widgets that are not mapped are never
194
+ * matched. Throws when none match.
195
+ */
177
196
  const getAllByName: BuiltinQueries["getAllByName"] = builtinQueries.getAllByName;
178
- /** Returns the single widget with a matching widget name. Throws when none or more than one matches. */
197
+ /**
198
+ * Returns the single mapped widget with a matching widget name. Widgets that are not mapped are
199
+ * never matched. Throws when none or more than one matches.
200
+ */
179
201
  const getByName: BuiltinQueries["getByName"] = builtinQueries.getByName;
180
202
  /**
181
- * Waits for and returns every widget with a matching widget name, retrying until at least one
182
- * appears or the timeout elapses.
203
+ * Waits for and returns every mapped widget with a matching widget name, retrying until at least one
204
+ * appears or the timeout elapses. Widgets that are not mapped are never matched.
183
205
  */
184
206
  const findAllByName: BuiltinQueries["findAllByName"] = builtinQueries.findAllByName;
185
207
  /**
186
- * Waits for and returns the single widget with a matching widget name, retrying until it appears or
187
- * the timeout elapses. Rejects when none or more than one matches.
208
+ * Waits for and returns the single mapped widget with a matching widget name, retrying until it
209
+ * appears or the timeout elapses. Widgets that are not mapped are never matched. Rejects when none
210
+ * or more than one matches.
188
211
  */
189
212
  const findByName: BuiltinQueries["findByName"] = builtinQueries.findByName;
190
213
  /**
191
- * Returns the single widget with matching placeholder text, or null when none match.
192
- * Throws when more than one matches.
214
+ * Returns the single mapped widget with matching placeholder text, or null when none match. Widgets
215
+ * that are not mapped are never matched. Throws when more than one matches.
193
216
  */
194
217
  const queryByPlaceholderText: BuiltinQueries["queryByPlaceholderText"] = builtinQueries.queryByPlaceholderText;
195
- /** Returns every widget with matching placeholder text. Throws when none match. */
218
+ /**
219
+ * Returns every mapped widget with matching placeholder text. Widgets that are not mapped are never
220
+ * matched. Throws when none match.
221
+ */
196
222
  const getAllByPlaceholderText: BuiltinQueries["getAllByPlaceholderText"] = builtinQueries.getAllByPlaceholderText;
197
- /** Returns the single widget with matching placeholder text. Throws when none or more than one matches. */
223
+ /**
224
+ * Returns the single mapped widget with matching placeholder text. Widgets that are not mapped are
225
+ * never matched. Throws when none or more than one matches.
226
+ */
198
227
  const getByPlaceholderText: BuiltinQueries["getByPlaceholderText"] = builtinQueries.getByPlaceholderText;
199
228
  /**
200
- * Waits for and returns every widget with matching placeholder text, retrying until at least one
201
- * appears or the timeout elapses.
229
+ * Waits for and returns every mapped widget with matching placeholder text, retrying until at least
230
+ * one appears or the timeout elapses. Widgets that are not mapped are never matched.
202
231
  */
203
232
  const findAllByPlaceholderText: BuiltinQueries["findAllByPlaceholderText"] = builtinQueries.findAllByPlaceholderText;
204
233
  /**
205
- * Waits for and returns the single widget with matching placeholder text, retrying until it appears
206
- * or the timeout elapses. Rejects when none or more than one matches.
234
+ * Waits for and returns the single mapped widget with matching placeholder text, retrying until it
235
+ * appears or the timeout elapses. Widgets that are not mapped are never matched. Rejects when none
236
+ * or more than one matches.
207
237
  */
208
238
  const findByPlaceholderText: BuiltinQueries["findByPlaceholderText"] = builtinQueries.findByPlaceholderText;
209
239
  /**
210
- * Returns the single widget with a matching display value, or null when none match.
211
- * Throws when more than one matches.
240
+ * Returns the single mapped widget with a matching display value, or null when none match. Widgets
241
+ * that are not mapped are never matched. Throws when more than one matches.
212
242
  */
213
243
  const queryByDisplayValue: BuiltinQueries["queryByDisplayValue"] = builtinQueries.queryByDisplayValue;
214
- /** Returns every widget with a matching display value. Throws when none match. */
244
+ /**
245
+ * Returns every mapped widget with a matching display value. Widgets that are not mapped are never
246
+ * matched. Throws when none match.
247
+ */
215
248
  const getAllByDisplayValue: BuiltinQueries["getAllByDisplayValue"] = builtinQueries.getAllByDisplayValue;
216
- /** Returns the single widget with a matching display value. Throws when none or more than one matches. */
249
+ /**
250
+ * Returns the single mapped widget with a matching display value. Widgets that are not mapped are
251
+ * never matched. Throws when none or more than one matches.
252
+ */
217
253
  const getByDisplayValue: BuiltinQueries["getByDisplayValue"] = builtinQueries.getByDisplayValue;
218
254
  /**
219
- * Waits for and returns every widget with a matching display value, retrying until at least one
220
- * appears or the timeout elapses.
255
+ * Waits for and returns every mapped widget with a matching display value, retrying until at least
256
+ * one appears or the timeout elapses. Widgets that are not mapped are never matched.
221
257
  */
222
258
  const findAllByDisplayValue: BuiltinQueries["findAllByDisplayValue"] = builtinQueries.findAllByDisplayValue;
223
259
  /**
224
- * Waits for and returns the single widget with a matching display value, retrying until it appears
225
- * or the timeout elapses. Rejects when none or more than one matches.
260
+ * Waits for and returns the single mapped widget with a matching display value, retrying until it
261
+ * appears or the timeout elapses. Widgets that are not mapped are never matched. Rejects when none
262
+ * or more than one matches.
226
263
  */
227
264
  const findByDisplayValue: BuiltinQueries["findByDisplayValue"] = builtinQueries.findByDisplayValue;
228
265
 
@@ -290,25 +327,23 @@ const hasMatchingAccessibleName = (widget: Gtk.Widget, options: ByRoleOptions):
290
327
  return isTextMatch(text, options.name, widget, options);
291
328
  };
292
329
 
293
- const isNumericValueMatch = (expected: number | undefined, actual: number | null): boolean =>
294
- expected === undefined || actual === expected;
330
+ const isNumericValueMatch = (widget: Gtk.Widget, field: WidgetValueField, expected: number | undefined): boolean =>
331
+ expected === undefined || isWidgetValueMatch(widget, field, expected);
295
332
 
296
333
  const hasMatchingAccessibleValue = (widget: Gtk.Widget, value: ByRoleValue, options: ByRoleOptions): boolean => {
297
- const actual = getWidgetValue(widget);
298
-
299
- const numericChecks: [number | undefined, number | null][] = [
300
- [value.now, actual.now],
301
- [value.min, actual.min],
302
- [value.max, actual.max],
334
+ const numericChecks: [WidgetValueField, number | undefined][] = [
335
+ ["now", value.now],
336
+ ["min", value.min],
337
+ ["max", value.max],
303
338
  ];
304
339
 
305
- for (const [expected, current] of numericChecks) {
306
- if (!isNumericValueMatch(expected, current)) {
340
+ for (const [field, expected] of numericChecks) {
341
+ if (!isNumericValueMatch(widget, field, expected)) {
307
342
  return false;
308
343
  }
309
344
  }
310
345
 
311
- return value.text === undefined || isTextMatch(actual.text, value.text, widget, options);
346
+ return value.text === undefined || isTextMatch(getWidgetValueText(widget), value.text, widget, options);
312
347
  };
313
348
 
314
349
  const hasMatchingBooleanStates = (widget: Gtk.Widget, options: ByRoleOptions): boolean => {
@@ -365,19 +400,22 @@ function nameQueryFamily<Args extends unknown[]>(
365
400
  queryAllBy: QueryAllBy<Args>,
366
401
  built: BuiltQueries<Args>,
367
402
  ): Record<string, unknown> {
403
+ const [queryBy, getAllBy, getBy, findAllBy, findBy] = built;
404
+
368
405
  return {
369
- [`queryBy${suffix}`]: built.queryBy,
406
+ [`queryBy${suffix}`]: queryBy,
370
407
  [`queryAllBy${suffix}`]: queryAllBy,
371
- [`getBy${suffix}`]: built.getBy,
372
- [`getAllBy${suffix}`]: built.getAllBy,
373
- [`findBy${suffix}`]: built.findBy,
374
- [`findAllBy${suffix}`]: built.findAllBy,
408
+ [`getBy${suffix}`]: getBy,
409
+ [`getAllBy${suffix}`]: getAllBy,
410
+ [`findBy${suffix}`]: findBy,
411
+ [`findAllBy${suffix}`]: findAllBy,
375
412
  };
376
413
  }
377
414
 
378
415
  /**
379
416
  * Finds every widget under the container whose accessible role matches `role` and that satisfies the given options.
380
- * Widgets excluded from the accessibility tree are skipped unless `options.hidden` is set.
417
+ * Widgets that are not mapped are never matched, and widgets excluded from the accessibility tree are
418
+ * skipped unless `options.hidden` is set.
381
419
  * @param container Widget subtree to search.
382
420
  * @param role Accessible role to match.
383
421
  * @param options Additional accessible name, state, and value constraints.
@@ -410,7 +448,7 @@ const collectMnemonicMatch = (
410
448
  return null;
411
449
  }
412
450
 
413
- const labelText = widget.getLabel();
451
+ const labelText = namingLabelText(widget);
414
452
 
415
453
  if (!labelText || !isTextMatch(labelText, text, widget, options)) {
416
454
  return null;
@@ -419,6 +457,12 @@ const collectMnemonicMatch = (
419
457
  return widget.getMnemonicWidget();
420
458
  };
421
459
 
460
+ const labellingText = (widget: Gtk.Widget): string | null => {
461
+ const ownLabel = widget.getAccessibleRole() === Gtk.AccessibleRole.LABEL ? null : getWidgetOwnLabel(widget);
462
+
463
+ return ownLabel ?? getWidgetExternalLabelText(widget);
464
+ };
465
+
422
466
  const collectLabelMatches = (
423
467
  results: Set<Gtk.Widget>,
424
468
  widget: Gtk.Widget,
@@ -431,22 +475,16 @@ const collectLabelMatches = (
431
475
  results.add(mnemonicTarget);
432
476
  }
433
477
 
434
- const ownLabel = getWidgetOwnLabel(widget);
435
-
436
- if (ownLabel !== null && isTextMatch(ownLabel, text, widget, options)) {
437
- results.add(widget);
438
- }
439
-
440
- const labelledByText = getWidgetLabelledByText(widget);
478
+ const labelText = labellingText(widget);
441
479
 
442
- if (labelledByText !== null && isTextMatch(labelledByText, text, widget, options)) {
480
+ if (labelText !== null && isTextMatch(labelText, text, widget, options)) {
443
481
  results.add(widget);
444
482
  }
445
483
  };
446
484
 
447
485
  /**
448
486
  * Finds every widget associated with a label whose text matches: a Gtk.Label mnemonic target, the
449
- * widget's own accessible label, or its labelled-by relation.
487
+ * widget's own accessible label, or its labelled-by relation. Widgets that are not mapped are never matched.
450
488
  * @param container Widget subtree to search.
451
489
  * @param text Matcher for the label text.
452
490
  * @param options Text matching options.
@@ -463,7 +501,7 @@ function queryAllByLabelText(container: Container, text: Matcher, options?: Matc
463
501
  }
464
502
 
465
503
  /**
466
- * Finds every widget whose rendered text content matches.
504
+ * Finds every widget whose rendered text content matches. Widgets that are not mapped are never matched.
467
505
  * @param container Widget subtree to search.
468
506
  * @param text Matcher for the widget text.
469
507
  * @param options Text matching options.
@@ -478,7 +516,7 @@ function queryAllByText(container: Container, text: Matcher, options?: MatcherOp
478
516
  }
479
517
 
480
518
  /**
481
- * Finds every widget whose widget name matches.
519
+ * Finds every widget whose widget name matches. Widgets that are not mapped are never matched.
482
520
  * @param container Widget subtree to search.
483
521
  * @param name Matcher for the widget name.
484
522
  * @param options Text matching options.
@@ -492,7 +530,7 @@ function queryAllByName(container: Container, name: Matcher, options?: MatcherOp
492
530
  }
493
531
 
494
532
  /**
495
- * Finds every widget whose placeholder text matches.
533
+ * Finds every widget whose placeholder text matches. Widgets that are not mapped are never matched.
496
534
  * @param container Widget subtree to search.
497
535
  * @param text Matcher for the placeholder text.
498
536
  * @param options Text matching options.
@@ -512,7 +550,7 @@ function queryAllByPlaceholderText(
512
550
  }
513
551
 
514
552
  /**
515
- * Finds every widget whose current display value matches.
553
+ * Finds every widget whose current display value matches. Widgets that are not mapped are never matched.
516
554
  * @param container Widget subtree to search.
517
555
  * @param value Matcher for the display value.
518
556
  * @param options Text matching options.
@@ -529,6 +567,8 @@ function queryAllByDisplayValue(container: Container, value: Matcher, options?:
529
567
 
530
568
  export {
531
569
  builtinQueries,
570
+ isMatchingWidgetType,
571
+ isTextMatch,
532
572
  queryByRole,
533
573
  getAllByRole,
534
574
  getByRole,
@@ -565,5 +605,4 @@ export {
565
605
  queryAllByName,
566
606
  queryAllByPlaceholderText,
567
607
  queryAllByDisplayValue,
568
- type BuiltinQueries,
569
608
  };
@@ -0,0 +1,97 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { camelCase } from "@gtkx/utils";
3
+ import type { Matcher, MatcherOptions } from "./types.js";
4
+ import { buildQueries } from "./build-queries.js";
5
+ import { getElementError } from "./errors.js";
6
+ import { isMatchingWidgetType, isTextMatch } from "./queries.js";
7
+ import { type Container, findAll } from "./traversal.js";
8
+
9
+ /** The helpers used to build custom queries, bundled the way DOM Testing Library exposes them. */
10
+ type QueryHelpers = {
11
+ /** Derives a query family's variants from its `queryAllBy` function. */
12
+ buildQueries: typeof buildQueries;
13
+ /** Builds the error a failing query throws. */
14
+ getElementError: typeof getElementError;
15
+ /** Finds every widget whose GObject property matches. */
16
+ queryAllByObjectProperty: typeof queryAllByObjectProperty;
17
+ /** Finds the single widget whose GObject property matches. */
18
+ queryByObjectProperty: typeof queryByObjectProperty;
19
+ };
20
+
21
+ /** The query-building helpers, bundled so they can be reached under a single import. */
22
+ const queryHelpers: QueryHelpers = {
23
+ buildQueries,
24
+ getElementError,
25
+ queryAllByObjectProperty,
26
+ queryByObjectProperty,
27
+ };
28
+
29
+ const readObjectProperty = (widget: Gtk.Widget, property: string): string | null => {
30
+ const value: unknown = Reflect.get(widget, camelCase(property));
31
+
32
+ if (typeof value === "string") {
33
+ return value;
34
+ }
35
+
36
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
37
+ return String(value);
38
+ }
39
+
40
+ return null;
41
+ };
42
+
43
+ /**
44
+ * Finds every widget carrying a GObject property whose value matches, the counterpart of DOM Testing
45
+ * Library's `queryAllByAttribute`. The property name may be given in kebab-case or camelCase, and
46
+ * widgets that are not mapped are never matched.
47
+ *
48
+ * @param property Name of the GObject property to read.
49
+ * @param container Widget subtree to search.
50
+ * @param text Matcher for the property's value.
51
+ * @param options Text matching options.
52
+ * @returns Every matching widget, or an empty array when none match.
53
+ */
54
+ function queryAllByObjectProperty(
55
+ property: string,
56
+ container: Container,
57
+ text: Matcher,
58
+ options?: MatcherOptions,
59
+ ): Gtk.Widget[] {
60
+ return findAll(
61
+ container,
62
+ (widget) =>
63
+ isMatchingWidgetType(widget, options) &&
64
+ isTextMatch(readObjectProperty(widget, property), text, widget, options),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Returns the single widget carrying a GObject property whose value matches, or null when none do,
70
+ * the counterpart of DOM Testing Library's `queryByAttribute`.
71
+ *
72
+ * @param property Name of the GObject property to read.
73
+ * @param container Widget subtree to search.
74
+ * @param text Matcher for the property's value.
75
+ * @param options Text matching options.
76
+ * @returns The matching widget, or null when none match.
77
+ * @throws When more than one widget matches.
78
+ */
79
+ function queryByObjectProperty(
80
+ property: string,
81
+ container: Container,
82
+ text: Matcher,
83
+ options?: MatcherOptions,
84
+ ): Gtk.Widget | null {
85
+ const matches = queryAllByObjectProperty(property, container, text, options);
86
+
87
+ if (matches.length > 1) {
88
+ throw getElementError(
89
+ `Found ${String(matches.length)} elements with the '${property}' property, but expected only one`,
90
+ container,
91
+ );
92
+ }
93
+
94
+ return matches[0] ?? null;
95
+ }
96
+
97
+ export { queryAllByObjectProperty, queryByObjectProperty, queryHelpers, type QueryHelpers };
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ import type { RenderOptions } from "./types.js";
3
+ import { render } from "./render.js";
4
+
5
+ type ChildrenBuilder<T> = (items: T[]) => ReactNode;
6
+
7
+ type RenderChildrenResult<T> = {
8
+ rerender: (items: T[]) => Promise<void>;
9
+ };
10
+
11
+ async function renderChildren<T>(
12
+ initial: T[],
13
+ build: ChildrenBuilder<T>,
14
+ options?: RenderOptions,
15
+ ): Promise<RenderChildrenResult<T>> {
16
+ const { rerender } = await render(build(initial), options);
17
+
18
+ return {
19
+ rerender: async (items: T[]) => {
20
+ await rerender(build(items));
21
+ },
22
+ };
23
+ }
24
+
25
+ export { renderChildren, type ChildrenBuilder };
package/src/render.tsx CHANGED
@@ -5,23 +5,28 @@ import {
5
5
  isRootElement,
6
6
  type ReconcilerRoot,
7
7
  setReconcilerErrorHandler,
8
+ settleAccessible,
8
9
  } from "@gtkx/react/internal";
9
10
  import { type ErrorInfo, type ReactNode, StrictMode } from "react";
10
11
  import type { RenderResult } from "./bound-queries.js";
11
- import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
12
+ import type { QueryMap, RenderOptions, ScreenshotOptions } from "./types.js";
12
13
  import { runInAct } from "./act.js";
13
14
  import { addToCleanupQueue, runCleanup } from "./cleanup-registry.js";
14
15
  import { scheduleAfterLayout } from "./frame-sync.js";
16
+ import { createHarnessWindow } from "./harness-window.js";
15
17
  import { logWidget, type PrettyWidgetOptions } from "./pretty-widget.js";
16
18
  import { logRoles } from "./role-helpers.js";
17
19
  import { clearScreen, setScreen } from "./screen.js";
18
- import { captureAndSaveScreenshot } from "./screenshot.js";
19
- import { type Container, TOPLEVELS, traverse } from "./traversal.js";
20
+ import { captureScreen } from "./screenshot.js";
21
+ import { type Container, roots, TOPLEVELS } from "./traversal.js";
20
22
  import { resetClipboard } from "./user-event/index.js";
21
23
  import { within } from "./within.js";
22
24
 
25
+ /** A mounted render tracked so cleanup can unmount it. */
23
26
  type ActiveRender = {
27
+ /** Root that rerenders drive and that cleanup tears the tree down through. */
24
28
  root: ReconcilerRoot;
29
+ /** Harness window created for the render, null when it renders into a container the caller supplied. */
25
30
  window: Gtk.Window | null;
26
31
  };
27
32
 
@@ -37,12 +42,13 @@ type ReconcilerErrorState = {
37
42
 
38
43
  const reconcilerErrors: ReconcilerErrorState = { lastError: null, isHandlerInstalled: false };
39
44
  const activeRenders: Set<ActiveRender> = new Set();
40
- const HARNESS_WINDOW_WIDTH = 800;
41
- const HARNESS_WINDOW_HEIGHT = 600;
42
45
 
43
46
  const flushLayout = (window: Gtk.Window | null): Promise<void> =>
44
- new Promise((resolve) => {
45
- scheduleAfterLayout(window, resolve);
47
+ new Promise<void>((resolve) => {
48
+ scheduleAfterLayout(window, () => {
49
+ settleAccessible();
50
+ resolve();
51
+ });
46
52
  });
47
53
 
48
54
  const update = async (element: ReactNode, root: ReconcilerRoot): Promise<void> => {
@@ -96,7 +102,7 @@ const resolveContainer = (container: RenderOptions["container"]): ResolvedContai
96
102
  return { containerInfo: container, window: null };
97
103
  }
98
104
 
99
- const window = new Gtk.Window({ defaultWidth: HARNESS_WINDOW_WIDTH, defaultHeight: HARNESS_WINDOW_HEIGHT });
105
+ const window = createHarnessWindow();
100
106
  window.setDecorated(false);
101
107
 
102
108
  return { containerInfo: window, window };
@@ -107,7 +113,7 @@ const firstToplevelWidget = (baseElement: Container): Gtk.Widget => {
107
113
  return baseElement;
108
114
  }
109
115
 
110
- const [first] = traverse(baseElement);
116
+ const [first] = roots(baseElement);
111
117
 
112
118
  if (first) {
113
119
  return first;
@@ -165,7 +171,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
165
171
  options?: RenderOptions<Q>,
166
172
  ): Promise<RenderResult<Q>> => {
167
173
  installErrorHandler();
168
- applyEnableAnimations(options?.animations === true);
174
+ applyEnableAnimations(options?.areAnimationsEnabled === true);
169
175
  const baseElement: Container = options?.baseElement ?? TOPLEVELS;
170
176
  const Wrapper = options?.wrapper;
171
177
  const resolved = resolveContainer(options?.container);
@@ -184,7 +190,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
184
190
  const wrap = (node: ReactNode): ReactNode => {
185
191
  const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
186
192
 
187
- return options?.reactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
193
+ return options?.isReactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
188
194
  };
189
195
 
190
196
  await update(wrap(element), root);
@@ -209,8 +215,7 @@ const render = async <Q extends QueryMap = Record<never, never>>(
209
215
  logRoles: () => {
210
216
  logRoles(baseElement);
211
217
  },
212
- screenshot: (selector?: WindowSelector, screenshotOptions?: ScreenshotOptions) =>
213
- captureAndSaveScreenshot(selector, screenshotOptions),
218
+ screenshot: (screenshotOptions?: ScreenshotOptions) => captureScreen(screenshotOptions),
214
219
  };
215
220
 
216
221
  setScreen(result);