@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
@@ -1,20 +1,24 @@
1
1
  import * as Gtk from "@gtkx/gi/gtk";
2
- import { getAccessibleMetadata } from "@gtkx/react/internal";
2
+ import {
3
+ isAccessibleNumberMatch,
4
+ readAccessibleBooleanProperty,
5
+ readAccessibleFlag,
6
+ readAccessibleInt,
7
+ readAccessibleRelation,
8
+ readAccessibleState,
9
+ readAccessibleNumber as readNativeNumber,
10
+ readAccessibleString as readNativeString,
11
+ } from "./accessible-native.js";
3
12
  import { EDITABLE_ROLES, isEditable, readEditableText } from "./editable.js";
4
- import { descendants } from "./traversal.js";
13
+ import { isNameFromAuthor, isNameProhibited } from "./role-naming.js";
14
+ import { children, descendants, relationCandidates } from "./traversal.js";
15
+ import { callBooleanGetter, callStringGetter, getCallableMethod } from "./widget-getters.js";
5
16
 
6
- type WidgetValue = {
7
- now: number | null;
8
- min: number | null;
9
- max: number | null;
10
- text: string | null;
11
- };
12
-
13
- type ValueTriplet = { now: number | null; min: number | null; max: number | null };
14
- /** A widget's checked state: on, off, or the mixed state of an inconsistent check button. */
17
+ type WidgetValueField = "now" | "min" | "max";
15
18
  type CheckedState = "checked" | "unchecked" | "mixed";
16
19
 
17
- const DEFAULT_TEXT_GETTERS = ["getLabel", "getText", "getTitle"] as const;
20
+ const EDITABLE_TEXT_GETTER = "getText";
21
+ const DEFAULT_TEXT_GETTERS = ["getLabel", EDITABLE_TEXT_GETTER, "getTitle"];
18
22
 
19
23
  const SELECTABLE_ROLES: Set<Gtk.AccessibleRole> = new Set<Gtk.AccessibleRole>([
20
24
  Gtk.AccessibleRole.ROW,
@@ -24,55 +28,60 @@ const SELECTABLE_ROLES: Set<Gtk.AccessibleRole> = new Set<Gtk.AccessibleRole>([
24
28
  Gtk.AccessibleRole.TREE_ITEM,
25
29
  ]);
26
30
 
27
- const callStringGetter = (widget: Gtk.Widget, method: string): string | null => {
28
- const fn: unknown = Reflect.get(widget, method);
29
-
30
- if (typeof fn !== "function") {
31
- return null;
32
- }
31
+ const PRESSED_BY_TRISTATE: Map<number, boolean> = new Map<number, boolean>([
32
+ [Gtk.AccessibleTristate.FALSE, false],
33
+ [Gtk.AccessibleTristate.TRUE, true],
34
+ ]);
33
35
 
34
- const value = (fn as () => string | null).call(widget);
36
+ const CHECKED_BY_TRISTATE: Map<number, CheckedState> = new Map<number, CheckedState>([
37
+ [Gtk.AccessibleTristate.FALSE, "unchecked"],
38
+ [Gtk.AccessibleTristate.TRUE, "checked"],
39
+ [Gtk.AccessibleTristate.MIXED, "mixed"],
40
+ ]);
35
41
 
36
- return value ?? null;
42
+ const VALUE_PROPERTIES: Record<WidgetValueField, Gtk.AccessibleProperty> = {
43
+ now: Gtk.AccessibleProperty.VALUE_NOW,
44
+ min: Gtk.AccessibleProperty.VALUE_MIN,
45
+ max: Gtk.AccessibleProperty.VALUE_MAX,
37
46
  };
38
47
 
39
- const readAccessibleString = (widget: Gtk.Widget, key: string): string | null => {
40
- const value = getAccessibleMetadata(widget, key);
48
+ const readAccessibleString = (widget: Gtk.Widget, property: Gtk.AccessibleProperty): string | null =>
49
+ readNativeString(widget, property);
41
50
 
42
- return typeof value === "string" ? value : null;
43
- };
51
+ const readAccessibleNumber = (widget: Gtk.Widget, property: Gtk.AccessibleProperty): number | null =>
52
+ readNativeNumber(widget, property);
44
53
 
45
- const readAccessibleNumber = (widget: Gtk.Widget, key: string): number | null => {
46
- const value = getAccessibleMetadata(widget, key);
54
+ const readAccessibleWidgets = (widget: Gtk.Widget, relation: Gtk.AccessibleRelation): Gtk.Widget[] | null => {
55
+ const targets = readAccessibleRelation(widget, relation, relationCandidates(widget));
56
+ const widgets = targets.filter((target): target is Gtk.Widget => target instanceof Gtk.Widget);
47
57
 
48
- return typeof value === "number" ? value : null;
58
+ return widgets.length > 0 ? widgets : null;
49
59
  };
50
60
 
51
- const readAccessibleWidgets = (widget: Gtk.Widget, key: string): Gtk.Widget[] | null => {
52
- const value = getAccessibleMetadata(widget, key);
61
+ const readAccessibleBoolean = (widget: Gtk.Widget, state: Gtk.AccessibleState): boolean | null =>
62
+ readAccessibleFlag(widget, state);
53
63
 
54
- if (!Array.isArray(value)) {
55
- return null;
64
+ const getLabelText = (widget: Gtk.Widget): string | null => {
65
+ if (widget instanceof Gtk.Label) {
66
+ return widget.getLabel();
56
67
  }
57
68
 
58
- const widgets = value.filter((item): item is Gtk.Widget => item instanceof Gtk.Widget);
59
-
60
- return widgets.length > 0 ? widgets : null;
69
+ return readAccessibleString(widget, Gtk.AccessibleProperty.LABEL);
61
70
  };
62
71
 
63
- const readAccessibleBoolean = (widget: Gtk.Widget, key: string): boolean | null => {
64
- const value = getAccessibleMetadata(widget, key);
72
+ const stripMnemonic = (text: string): string => text.replaceAll(/_(.)/g, "$1");
73
+ const isUnderlineUsed = (widget: Gtk.Widget): boolean => callBooleanGetter(widget, "getUseUnderline") ?? false;
65
74
 
66
- return typeof value === "boolean" ? value : null;
67
- };
75
+ const readNamingText = (widget: Gtk.Widget, getter: string, value: string): string =>
76
+ getter !== EDITABLE_TEXT_GETTER && isUnderlineUsed(widget) ? stripMnemonic(value) : value;
68
77
 
69
- const getLabelText = (widget: Gtk.Widget): string | null => {
70
- if (widget instanceof Gtk.Label) {
71
- return widget.getLabel();
72
- }
78
+ const readFirstText = (widget: Gtk.Widget, getters: string[]): string | null => {
79
+ for (const getter of getters) {
80
+ const value = callStringGetter(widget, getter);
73
81
 
74
- if (widget instanceof Gtk.Inscription) {
75
- return widget.getText() ?? null;
82
+ if (value) {
83
+ return readNamingText(widget, getter, value);
84
+ }
76
85
  }
77
86
 
78
87
  return null;
@@ -84,19 +93,7 @@ const getLabelText = (widget: Gtk.Widget): string | null => {
84
93
  *
85
94
  * @param widget The widget to read text from.
86
95
  */
87
- const getWidgetNodeText = (widget: Gtk.Widget): string | null => {
88
- for (const getter of DEFAULT_TEXT_GETTERS) {
89
- const value = callStringGetter(widget, getter);
90
-
91
- if (value) {
92
- return value;
93
- }
94
- }
95
-
96
- return null;
97
- };
98
-
99
- const stripMnemonic = (text: string): string => text.replaceAll(/_(.)/g, "$1");
96
+ const getWidgetText = (widget: Gtk.Widget): string | null => readFirstText(widget, DEFAULT_TEXT_GETTERS);
100
97
 
101
98
  const namingLabelText = (widget: Gtk.Widget): string | null => {
102
99
  const text = getLabelText(widget);
@@ -105,7 +102,7 @@ const namingLabelText = (widget: Gtk.Widget): string | null => {
105
102
  return null;
106
103
  }
107
104
 
108
- return widget instanceof Gtk.Label && widget.getUseUnderline() ? stripMnemonic(text) : text;
105
+ return isUnderlineUsed(widget) ? stripMnemonic(text) : text;
109
106
  };
110
107
 
111
108
  const isNamingLabelRole = (role: Gtk.AccessibleRole, shouldIncludePresentation: boolean): boolean =>
@@ -136,26 +133,17 @@ const getWidgetLabelText = (widget: Gtk.Widget): string | null => {
136
133
  return null;
137
134
  }
138
135
 
139
- return getLabelText(widget);
140
- };
141
-
142
- const getChildren = function* (widget: Gtk.Widget): Generator<Gtk.Widget> {
143
- let child = widget.getFirstChild();
144
-
145
- while (child) {
146
- yield child;
147
- child = child.getNextSibling();
148
- }
136
+ return namingLabelText(widget);
149
137
  };
150
138
 
151
139
  const textContentParts = (widget: Gtk.Widget): string[] => {
152
- const own = getWidgetNodeText(widget);
140
+ const own = getWidgetText(widget);
153
141
 
154
142
  if (own !== null) {
155
143
  return [own];
156
144
  }
157
145
 
158
- return [...getChildren(widget)].flatMap((child) => textContentParts(child));
146
+ return [...children(widget)].flatMap((child) => textContentParts(child));
159
147
  };
160
148
 
161
149
  const getWidgetTextContent = (widget: Gtk.Widget): string | null => {
@@ -164,15 +152,15 @@ const getWidgetTextContent = (widget: Gtk.Widget): string | null => {
164
152
  return parts.length > 0 ? parts.join(" ") : null;
165
153
  };
166
154
 
167
- const isComboBoxFaceCandidate = (widget: Gtk.Widget): boolean =>
155
+ const isDropDownFaceCandidate = (widget: Gtk.Widget): boolean =>
168
156
  !(widget instanceof Gtk.Popover) && widget.getChildVisible();
169
157
 
170
- const comboBoxChildFaceText = (child: Gtk.Widget): string | null =>
171
- isComboBoxFaceCandidate(child) ? getWidgetLabelText(child) ?? comboBoxFaceText(child) : null;
158
+ const dropDownChildFaceText = (child: Gtk.Widget): string | null =>
159
+ isDropDownFaceCandidate(child) ? getWidgetLabelText(child) ?? dropDownFaceText(child) : null;
172
160
 
173
- const comboBoxFaceText = (widget: Gtk.Widget): string | null => {
174
- for (const child of getChildren(widget)) {
175
- const text = comboBoxChildFaceText(child);
161
+ const dropDownFaceText = (widget: Gtk.Widget): string | null => {
162
+ for (const child of children(widget)) {
163
+ const text = dropDownChildFaceText(child);
176
164
 
177
165
  if (text !== null) {
178
166
  return text;
@@ -192,67 +180,77 @@ const tabPanelTitle = (widget: Gtk.Widget): string | null => {
192
180
  return null;
193
181
  };
194
182
 
195
- const getWidgetAccessibleName = (widget: Gtk.Widget): string | null => {
196
- const role = widget.getAccessibleRole();
183
+ const nameFromAuthor = (widget: Gtk.Widget): string | null =>
184
+ getWidgetLabelledByText(widget) ?? readAccessibleString(widget, Gtk.AccessibleProperty.LABEL);
197
185
 
198
- if (role === Gtk.AccessibleRole.TAB_PANEL) {
199
- return tabPanelTitle(widget);
186
+ const nameFromContent = (widget: Gtk.Widget): string | null => {
187
+ const ownText = getWidgetText(widget);
188
+
189
+ if (ownText) {
190
+ return ownText;
200
191
  }
201
192
 
202
- const labelledByText = getWidgetLabelledByText(widget);
193
+ const childLabels = collectLabels(widget);
203
194
 
204
- if (labelledByText) {
205
- return labelledByText;
206
- }
195
+ return childLabels.length > 0 ? childLabels.join(" ") : null;
196
+ };
207
197
 
208
- const accessibleLabel = readAccessibleString(widget, "accessibleLabel");
198
+ const getWidgetAccessibleName = (widget: Gtk.Widget): string | null => {
199
+ const role = widget.getAccessibleRole();
209
200
 
210
- if (accessibleLabel) {
211
- return accessibleLabel;
201
+ if (isNameProhibited(role)) {
202
+ return null;
212
203
  }
213
204
 
214
- const ownText = getWidgetNodeText(widget);
215
-
216
- if (ownText) {
217
- return ownText;
205
+ if (role === Gtk.AccessibleRole.TAB_PANEL) {
206
+ return tabPanelTitle(widget);
218
207
  }
219
208
 
220
- const childLabels = collectLabels(widget);
209
+ const authored = isNameFromAuthor(role) ? nameFromAuthor(widget) : null;
221
210
 
222
- if (childLabels.length > 0) {
223
- return childLabels.join(" ");
211
+ if (authored) {
212
+ return authored;
224
213
  }
225
214
 
226
- return callStringGetter(widget, "getTooltipText");
215
+ return nameFromContent(widget) ?? callStringGetter(widget, "getTooltipText");
227
216
  };
228
217
 
229
218
  const getWidgetName = (widget: Gtk.Widget): string | null => {
230
219
  return widget.getName();
231
220
  };
232
221
 
233
- const getWidgetPlaceholderText = (widget: Gtk.Widget): string | null => {
234
- if (!EDITABLE_ROLES.has(widget.getAccessibleRole()) || !(widget instanceof Gtk.Editable)) {
235
- return null;
236
- }
222
+ const nonEmptyText = (text: string | null): string | null => (text === null || text === "" ? null : text);
237
223
 
238
- const fromGetter = callStringGetter(widget, "getPlaceholderText");
224
+ const readPlaceholderProperty = (widget: Gtk.Widget): string | null => {
225
+ const value: unknown = Reflect.get(widget, "placeholderText");
239
226
 
240
- if (fromGetter !== null) {
241
- return fromGetter;
242
- }
227
+ return typeof value === "string" ? nonEmptyText(value) : null;
228
+ };
243
229
 
244
- const fromAccessor: unknown = Reflect.get(widget, "placeholderText");
230
+ const getWidgetPlaceholderText = (widget: Gtk.Widget): string | null => {
231
+ if (!EDITABLE_ROLES.has(widget.getAccessibleRole())) {
232
+ return null;
233
+ }
245
234
 
246
- return typeof fromAccessor === "string" && fromAccessor !== "" ? fromAccessor : null;
235
+ return (
236
+ nonEmptyText(callStringGetter(widget, "getPlaceholderText")) ??
237
+ readPlaceholderProperty(widget) ??
238
+ nonEmptyText(readAccessibleString(widget, Gtk.AccessibleProperty.PLACEHOLDER))
239
+ );
247
240
  };
248
241
 
242
+ const comboBoxDisplayValue = (widget: Gtk.Widget): string | null =>
243
+ widget instanceof Gtk.DropDown
244
+ ? (dropDownFaceText(widget) ?? getWidgetValueText(widget))
245
+ : (getWidgetValueText(widget) ?? dropDownFaceText(widget));
246
+
249
247
  const getWidgetDisplayValue = (widget: Gtk.Widget): string | null => {
250
248
  if (EDITABLE_ROLES.has(widget.getAccessibleRole()) && isEditable(widget)) {
251
249
  return readEditableText(widget);
252
250
  }
253
251
 
254
252
  if (widget.getAccessibleRole() === Gtk.AccessibleRole.COMBO_BOX) {
255
- return comboBoxFaceText(widget);
253
+ return comboBoxDisplayValue(widget);
256
254
  }
257
255
 
258
256
  return null;
@@ -283,7 +281,7 @@ const hasZeroOpacityAncestor = (widget: Gtk.Widget): boolean => {
283
281
  const isWidgetVisible = (widget: Gtk.Widget): boolean => widget.isVisible() && !hasZeroOpacityAncestor(widget);
284
282
 
285
283
  const getWidgetRequiredState = (widget: Gtk.Widget): boolean | null =>
286
- readAccessibleBoolean(widget, "accessibleRequired");
284
+ readAccessibleBooleanProperty(widget, Gtk.AccessibleProperty.REQUIRED);
287
285
 
288
286
  const readTextViewSelection = (view: Gtk.TextView): string | null => {
289
287
  const buffer = view.getBuffer();
@@ -292,40 +290,48 @@ const readTextViewSelection = (view: Gtk.TextView): string | null => {
292
290
  return hasSelection ? buffer.getText(start, end, true) : null;
293
291
  };
294
292
 
295
- const readEditableSelection = (editable: Gtk.Editable): string | null => {
296
- const [hasSelection, start, end] = editable.getSelectionBounds();
293
+ const sliceSelectedText = (widget: Gtk.Widget, start: number, end: number): string | null => {
294
+ const chars = getCallableMethod<[number, number], string | null>(widget, "getChars");
297
295
 
298
- return hasSelection ? editable.getChars(start, end) : null;
296
+ if (chars) {
297
+ return chars(start, end) ?? null;
298
+ }
299
+
300
+ const text = callStringGetter(widget, "getText");
301
+
302
+ /* eslint-disable-next-line @typescript-eslint/no-misused-spread -- bounds are code point offsets */
303
+ return text === null ? null : [...text].slice(start, end).join("");
299
304
  };
300
305
 
301
- const getWidgetSelection = (widget: Gtk.Widget): string | null => {
302
- if (widget instanceof Gtk.TextView) {
303
- return readTextViewSelection(widget);
304
- }
306
+ const readBoundedSelection = (widget: Gtk.Widget): string | null => {
307
+ const bounds = getCallableMethod<[], [boolean, number, number]>(widget, "getSelectionBounds");
305
308
 
306
- if (widget instanceof Gtk.Editable) {
307
- return readEditableSelection(widget);
309
+ if (!bounds) {
310
+ return null;
308
311
  }
309
312
 
310
- return null;
313
+ const [hasSelection, start, end] = bounds();
314
+
315
+ return hasSelection ? sliceSelectedText(widget, start, end) : null;
311
316
  };
312
317
 
318
+ const getWidgetSelection = (widget: Gtk.Widget): string | null =>
319
+ widget instanceof Gtk.TextView ? readTextViewSelection(widget) : readBoundedSelection(widget);
320
+
313
321
  const checkedFromActive = (isActive: boolean): CheckedState => (isActive ? "checked" : "unchecked");
314
322
 
315
- const isCheckableToggle = (widget: Gtk.Widget): boolean =>
316
- widget instanceof Gtk.Switch ||
317
- (widget instanceof Gtk.ToggleButton && widget.getAccessibleRole() === Gtk.AccessibleRole.RADIO);
323
+ const isRadioToggle = (widget: Gtk.Widget): widget is Gtk.ToggleButton =>
324
+ widget instanceof Gtk.ToggleButton && widget.getAccessibleRole() === Gtk.AccessibleRole.RADIO;
318
325
 
319
326
  const getWidgetCheckedState = (widget: Gtk.Widget): CheckedState | null => {
320
- if (widget instanceof Gtk.CheckButton) {
321
- return widget.getInconsistent() ? "mixed" : checkedFromActive(widget.getActive());
322
- }
327
+ const tristate = readAccessibleState(widget, Gtk.AccessibleState.CHECKED);
328
+ const state = tristate === null ? undefined : CHECKED_BY_TRISTATE.get(tristate);
323
329
 
324
- if (isCheckableToggle(widget)) {
325
- return checkedFromActive(Reflect.get(widget, "active") === true);
330
+ if (state !== undefined) {
331
+ return state;
326
332
  }
327
333
 
328
- return null;
334
+ return isRadioToggle(widget) ? checkedFromActive(widget.getActive()) : null;
329
335
  };
330
336
 
331
337
  const isWidgetChecked = (widget: Gtk.Widget): boolean | null => {
@@ -335,28 +341,24 @@ const isWidgetChecked = (widget: Gtk.Widget): boolean | null => {
335
341
  };
336
342
 
337
343
  const getWidgetPressedState = (widget: Gtk.Widget): boolean | null => {
338
- if (widget instanceof Gtk.ToggleButton) {
339
- return widget.getActive();
340
- }
344
+ const tristate = readAccessibleState(widget, Gtk.AccessibleState.PRESSED);
341
345
 
342
- return null;
346
+ return tristate === null ? null : PRESSED_BY_TRISTATE.get(tristate) ?? null;
343
347
  };
344
348
 
345
349
  const getWidgetExpandedState = (widget: Gtk.Widget): boolean | null => {
346
- if (widget instanceof Gtk.Expander) {
347
- return widget.getExpanded();
348
- }
349
-
350
350
  if (widget instanceof Gtk.TreeExpander) {
351
351
  return widget.getListRow()?.getExpanded() ?? null;
352
352
  }
353
353
 
354
- return null;
354
+ return readAccessibleBoolean(widget, Gtk.AccessibleState.EXPANDED);
355
355
  };
356
356
 
357
357
  const getWidgetSelectedState = (widget: Gtk.Widget): boolean | null => {
358
- if (widget instanceof Gtk.ListBoxRow || widget instanceof Gtk.FlowBoxChild) {
359
- return widget.isSelected();
358
+ const selected = readAccessibleBoolean(widget, Gtk.AccessibleState.SELECTED);
359
+
360
+ if (selected !== null) {
361
+ return selected;
360
362
  }
361
363
 
362
364
  if (SELECTABLE_ROLES.has(widget.getAccessibleRole())) {
@@ -367,88 +369,37 @@ const getWidgetSelectedState = (widget: Gtk.Widget): boolean | null => {
367
369
  };
368
370
 
369
371
  const getWidgetLevel = (widget: Gtk.Widget): number | null => {
370
- return readAccessibleNumber(widget, "accessibleLevel");
372
+ return readAccessibleInt(widget, Gtk.AccessibleProperty.LEVEL);
371
373
  };
372
374
 
373
375
  const getWidgetInvalidState = (widget: Gtk.Widget): Gtk.AccessibleInvalidState | null => {
374
- const value = readAccessibleNumber(widget, "accessibleInvalid");
376
+ const value = readAccessibleState(widget, Gtk.AccessibleState.INVALID);
375
377
 
376
378
  return value ?? null;
377
379
  };
378
380
 
379
381
  const getWidgetErrorMessage = (widget: Gtk.Widget): Gtk.Widget[] | null =>
380
- readAccessibleWidgets(widget, "accessibleErrorMessage");
382
+ readAccessibleWidgets(widget, Gtk.AccessibleRelation.ERROR_MESSAGE);
381
383
 
382
384
  const getWidgetBusyState = (widget: Gtk.Widget): boolean | null => {
383
- return readAccessibleBoolean(widget, "accessibleBusy");
385
+ return readAccessibleBoolean(widget, Gtk.AccessibleState.BUSY);
384
386
  };
385
387
 
386
388
  const getWidgetDescription = (widget: Gtk.Widget): string | null => {
387
- return readAccessibleString(widget, "accessibleDescription");
388
- };
389
-
390
- const adjustmentValue = (adjustment: Gtk.Adjustment): ValueTriplet => ({
391
- now: adjustment.getValue(),
392
- min: adjustment.getLower(),
393
- max: adjustment.getUpper(),
394
- });
395
-
396
- const adjustmentWidgetValue = (widget: Gtk.Widget): ValueTriplet | null => {
397
- if (widget instanceof Gtk.Range) {
398
- return adjustmentValue(widget.getAdjustment());
399
- }
400
-
401
- if (widget instanceof Gtk.Scrollbar) {
402
- return adjustmentValue(widget.getAdjustment());
403
- }
404
-
405
- if (widget instanceof Gtk.SpinButton) {
406
- return adjustmentValue(widget.getAdjustment());
407
- }
408
-
409
- if (widget instanceof Gtk.ScaleButton) {
410
- return adjustmentValue(widget.getAdjustment());
411
- }
412
-
413
- return null;
414
- };
415
-
416
- const getWidgetLiveValue = (widget: Gtk.Widget): ValueTriplet | null => {
417
- const adjustmentBased = adjustmentWidgetValue(widget);
418
-
419
- if (adjustmentBased) {
420
- return adjustmentBased;
421
- }
422
-
423
- if (widget instanceof Gtk.LevelBar) {
424
- return { now: widget.getValue(), min: widget.getMinValue(), max: widget.getMaxValue() };
425
- }
426
-
427
- if (widget instanceof Gtk.ProgressBar) {
428
- return { now: widget.getFraction(), min: 0, max: 1 };
429
- }
430
-
431
- return null;
389
+ return readAccessibleString(widget, Gtk.AccessibleProperty.DESCRIPTION);
432
390
  };
433
391
 
434
- const getWidgetValue = (widget: Gtk.Widget): WidgetValue => {
435
- const text = readAccessibleString(widget, "accessibleValueText");
436
- const live = getWidgetLiveValue(widget);
392
+ const getWidgetValueNow = (widget: Gtk.Widget): number | null =>
393
+ readAccessibleNumber(widget, Gtk.AccessibleProperty.VALUE_NOW);
437
394
 
438
- if (live) {
439
- return { ...live, text };
440
- }
395
+ const getWidgetValueText = (widget: Gtk.Widget): string | null =>
396
+ readAccessibleString(widget, Gtk.AccessibleProperty.VALUE_TEXT);
441
397
 
442
- return {
443
- now: readAccessibleNumber(widget, "accessibleValueNow"),
444
- min: readAccessibleNumber(widget, "accessibleValueMin"),
445
- max: readAccessibleNumber(widget, "accessibleValueMax"),
446
- text,
447
- };
448
- };
398
+ const isWidgetValueMatch = (widget: Gtk.Widget, field: WidgetValueField, expected: number): boolean =>
399
+ isAccessibleNumberMatch(widget, VALUE_PROPERTIES[field], expected);
449
400
 
450
401
  const getWidgetOwnLabel = (widget: Gtk.Widget): string | null => {
451
- return readAccessibleString(widget, "accessibleLabel");
402
+ return readAccessibleString(widget, Gtk.AccessibleProperty.LABEL);
452
403
  };
453
404
 
454
405
  const collectAccessibleNames = (targets: Gtk.Widget[]): string[] => {
@@ -457,7 +408,7 @@ const collectAccessibleNames = (targets: Gtk.Widget[]): string[] => {
457
408
  for (const target of targets) {
458
409
  const text = getWidgetAccessibleName(target);
459
410
 
460
- if (text !== null) {
411
+ if (text) {
461
412
  texts.push(text);
462
413
  }
463
414
  }
@@ -465,8 +416,14 @@ const collectAccessibleNames = (targets: Gtk.Widget[]): string[] => {
465
416
  return texts;
466
417
  };
467
418
 
419
+ const isSelfLabelling = (widget: Gtk.Widget, targets: Gtk.Widget[]): boolean => {
420
+ const own = new Set(descendants(widget));
421
+
422
+ return targets.every((target) => own.has(target));
423
+ };
424
+
468
425
  const getWidgetLabelledByText = (widget: Gtk.Widget): string | null => {
469
- const targets = readAccessibleWidgets(widget, "accessibleLabelledBy");
426
+ const targets = readAccessibleWidgets(widget, Gtk.AccessibleRelation.LABELLED_BY);
470
427
 
471
428
  if (targets === null) {
472
429
  return null;
@@ -477,20 +434,26 @@ const getWidgetLabelledByText = (widget: Gtk.Widget): string | null => {
477
434
  return texts.length > 0 ? texts.join(" ") : null;
478
435
  };
479
436
 
437
+ const getWidgetExternalLabelText = (widget: Gtk.Widget): string | null => {
438
+ const targets = readAccessibleWidgets(widget, Gtk.AccessibleRelation.LABELLED_BY);
439
+
440
+ return targets === null || isSelfLabelling(widget, targets) ? null : getWidgetLabelledByText(widget);
441
+ };
442
+
480
443
  const isTooltipUsedAsName = (widget: Gtk.Widget): boolean =>
481
- readAccessibleString(widget, "accessibleLabel") === null &&
482
- getWidgetNodeText(widget) === null &&
444
+ readAccessibleString(widget, Gtk.AccessibleProperty.LABEL) === null &&
445
+ getWidgetText(widget) === null &&
483
446
  collectLabels(widget).length === 0;
484
447
 
485
448
  const getWidgetDescribedByText = (widget: Gtk.Widget): string | null => {
486
- const targets = readAccessibleWidgets(widget, "accessibleDescribedBy");
449
+ const targets = readAccessibleWidgets(widget, Gtk.AccessibleRelation.DESCRIBED_BY);
487
450
  const texts = targets === null ? [] : collectAccessibleNames(targets);
488
451
 
489
452
  if (texts.length > 0) {
490
453
  return texts.join(" ");
491
454
  }
492
455
 
493
- const description = readAccessibleString(widget, "accessibleDescription");
456
+ const description = readAccessibleString(widget, Gtk.AccessibleProperty.DESCRIPTION);
494
457
 
495
458
  if (description !== null) {
496
459
  return description;
@@ -499,15 +462,17 @@ const getWidgetDescribedByText = (widget: Gtk.Widget): string | null => {
499
462
  return isTooltipUsedAsName(widget) ? null : callStringGetter(widget, "getTooltipText");
500
463
  };
501
464
 
465
+ /**
466
+ * Returns whether a widget is excluded from the accessibility tree, because it or one of its
467
+ * ancestors is marked hidden.
468
+ *
469
+ * @param widget The widget to test.
470
+ */
502
471
  const isInaccessible = (widget: Gtk.Widget): boolean => {
503
472
  let current: Gtk.Widget | null = widget;
504
473
 
505
474
  while (current) {
506
- if (!current.getVisible()) {
507
- return true;
508
- }
509
-
510
- if (readAccessibleBoolean(current, "accessibleHidden") === true) {
475
+ if (readAccessibleBoolean(current, Gtk.AccessibleState.HIDDEN) === true) {
511
476
  return true;
512
477
  }
513
478
 
@@ -518,7 +483,8 @@ const isInaccessible = (widget: Gtk.Widget): boolean => {
518
483
  };
519
484
 
520
485
  export {
521
- getWidgetNodeText,
486
+ namingLabelText,
487
+ getWidgetText,
522
488
  getWidgetTextContent,
523
489
  getWidgetLabelText,
524
490
  getWidgetAccessibleName,
@@ -541,10 +507,13 @@ export {
541
507
  getWidgetErrorMessage,
542
508
  getWidgetBusyState,
543
509
  getWidgetDescription,
544
- getWidgetValue,
510
+ getWidgetValueNow,
511
+ getWidgetValueText,
545
512
  getWidgetOwnLabel,
513
+ getWidgetExternalLabelText,
546
514
  getWidgetLabelledByText,
547
515
  isInaccessible,
516
+ isWidgetValueMatch,
548
517
  type CheckedState,
549
- type WidgetValue,
518
+ type WidgetValueField,
550
519
  };
@@ -0,0 +1,32 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { getInstanceType, typeName } from "@gtkx/runtime";
3
+
4
+ const getWidgetMethod = (widget: Gtk.Widget, name: string): unknown => Reflect.get(widget, name);
5
+ const getWidgetTypeName = (widget: Gtk.Widget): string | null => typeName(getInstanceType(widget));
6
+
7
+ const hasWidgetMethod = (widget: Gtk.Widget, name: string): boolean =>
8
+ typeof getWidgetMethod(widget, name) === "function";
9
+
10
+ /* eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callers name the signature */
11
+ const getCallableMethod = <Args extends unknown[], Result>(
12
+ widget: Gtk.Widget,
13
+ name: string,
14
+ ): ((...args: Args) => Result) | null => {
15
+ const fn = getWidgetMethod(widget, name);
16
+
17
+ return typeof fn === "function" ? (fn as (...args: Args) => Result).bind(widget) : null;
18
+ };
19
+
20
+ const callBooleanGetter = (widget: Gtk.Widget, method: string): boolean | null => {
21
+ const value = getCallableMethod<[], unknown>(widget, method)?.();
22
+
23
+ return typeof value === "boolean" ? value : null;
24
+ };
25
+
26
+ const callStringGetter = (widget: Gtk.Widget, method: string): string | null => {
27
+ const value = getCallableMethod<[], unknown>(widget, method)?.();
28
+
29
+ return typeof value === "string" ? value : null;
30
+ };
31
+
32
+ export { callBooleanGetter, callStringGetter, getCallableMethod, getWidgetTypeName, hasWidgetMethod };