@gtkx/testing 0.21.0 → 1.0.0-rc.2

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 (216) hide show
  1. package/README.md +137 -34
  2. package/dist/act.d.ts +19 -0
  3. package/dist/act.d.ts.map +1 -0
  4. package/dist/act.js +56 -0
  5. package/dist/act.js.map +1 -0
  6. package/dist/bound-queries.d.ts +33 -0
  7. package/dist/bound-queries.d.ts.map +1 -0
  8. package/dist/bound-queries.js +2 -0
  9. package/dist/bound-queries.js.map +1 -0
  10. package/dist/build-queries.d.ts +16 -0
  11. package/dist/build-queries.d.ts.map +1 -0
  12. package/dist/build-queries.js +115 -0
  13. package/dist/build-queries.js.map +1 -0
  14. package/dist/cleanup-registry.d.ts +5 -0
  15. package/dist/cleanup-registry.d.ts.map +1 -0
  16. package/dist/cleanup-registry.js +21 -0
  17. package/dist/cleanup-registry.js.map +1 -0
  18. package/dist/config.d.ts +20 -52
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +17 -46
  21. package/dist/config.js.map +1 -1
  22. package/dist/editable.d.ts +8 -0
  23. package/dist/editable.d.ts.map +1 -0
  24. package/dist/editable.js +26 -0
  25. package/dist/editable.js.map +1 -0
  26. package/dist/errors.d.ts +30 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +135 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fire-event.d.ts +8 -25
  31. package/dist/fire-event.d.ts.map +1 -1
  32. package/dist/fire-event.js +11 -30
  33. package/dist/fire-event.js.map +1 -1
  34. package/dist/frame-sync.d.ts +4 -0
  35. package/dist/frame-sync.d.ts.map +1 -0
  36. package/dist/frame-sync.js +37 -0
  37. package/dist/frame-sync.js.map +1 -0
  38. package/dist/index.d.ts +17 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +11 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/matchers.d.ts +59 -0
  43. package/dist/matchers.d.ts.map +1 -0
  44. package/dist/matchers.js +117 -0
  45. package/dist/matchers.js.map +1 -0
  46. package/dist/pretty-widget.d.ts +24 -26
  47. package/dist/pretty-widget.d.ts.map +1 -1
  48. package/dist/pretty-widget.js +111 -117
  49. package/dist/pretty-widget.js.map +1 -1
  50. package/dist/queries.d.ts +190 -180
  51. package/dist/queries.d.ts.map +1 -1
  52. package/dist/queries.js +291 -320
  53. package/dist/queries.js.map +1 -1
  54. package/dist/register-test-hooks.d.ts +2 -0
  55. package/dist/register-test-hooks.d.ts.map +1 -0
  56. package/dist/register-test-hooks.js +16 -0
  57. package/dist/register-test-hooks.js.map +1 -0
  58. package/dist/render-hook.d.ts +15 -35
  59. package/dist/render-hook.d.ts.map +1 -1
  60. package/dist/render-hook.js +9 -45
  61. package/dist/render-hook.js.map +1 -1
  62. package/dist/render.d.ts +13 -42
  63. package/dist/render.d.ts.map +1 -1
  64. package/dist/render.js +138 -121
  65. package/dist/render.js.map +1 -1
  66. package/dist/role-helpers.d.ts +19 -53
  67. package/dist/role-helpers.d.ts.map +1 -1
  68. package/dist/role-helpers.js +48 -63
  69. package/dist/role-helpers.js.map +1 -1
  70. package/dist/screen.d.ts +7 -63
  71. package/dist/screen.d.ts.map +1 -1
  72. package/dist/screen.js +24 -110
  73. package/dist/screen.js.map +1 -1
  74. package/dist/screenshot.d.ts +21 -23
  75. package/dist/screenshot.d.ts.map +1 -1
  76. package/dist/screenshot.js +98 -50
  77. package/dist/screenshot.js.map +1 -1
  78. package/dist/suggestions.d.ts +27 -0
  79. package/dist/suggestions.d.ts.map +1 -0
  80. package/dist/suggestions.js +61 -0
  81. package/dist/suggestions.js.map +1 -0
  82. package/dist/traversal.d.ts +12 -10
  83. package/dist/traversal.d.ts.map +1 -1
  84. package/dist/traversal.js +36 -15
  85. package/dist/traversal.js.map +1 -1
  86. package/dist/types.d.ts +115 -169
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/user-event/adjustment.d.ts +10 -0
  90. package/dist/user-event/adjustment.d.ts.map +1 -0
  91. package/dist/user-event/adjustment.js +35 -0
  92. package/dist/user-event/adjustment.js.map +1 -0
  93. package/dist/user-event/click.d.ts +8 -0
  94. package/dist/user-event/click.d.ts.map +1 -0
  95. package/dist/user-event/click.js +73 -0
  96. package/dist/user-event/click.js.map +1 -0
  97. package/dist/user-event/controller.d.ts +8 -0
  98. package/dist/user-event/controller.d.ts.map +1 -0
  99. package/dist/user-event/controller.js +31 -0
  100. package/dist/user-event/controller.js.map +1 -0
  101. package/dist/user-event/dispatch.d.ts +7 -0
  102. package/dist/user-event/dispatch.d.ts.map +1 -0
  103. package/dist/user-event/dispatch.js +11 -0
  104. package/dist/user-event/dispatch.js.map +1 -0
  105. package/dist/user-event/event-wrapper.d.ts +4 -0
  106. package/dist/user-event/event-wrapper.d.ts.map +1 -0
  107. package/dist/user-event/event-wrapper.js +59 -0
  108. package/dist/user-event/event-wrapper.js.map +1 -0
  109. package/dist/user-event/gesture.d.ts +35 -0
  110. package/dist/user-event/gesture.d.ts.map +1 -0
  111. package/dist/user-event/gesture.js +138 -0
  112. package/dist/user-event/gesture.js.map +1 -0
  113. package/dist/user-event/index.d.ts +51 -0
  114. package/dist/user-event/index.d.ts.map +1 -0
  115. package/dist/user-event/index.js +42 -0
  116. package/dist/user-event/index.js.map +1 -0
  117. package/dist/user-event/keyboard.d.ts +10 -0
  118. package/dist/user-event/keyboard.d.ts.map +1 -0
  119. package/dist/user-event/keyboard.js +204 -0
  120. package/dist/user-event/keyboard.js.map +1 -0
  121. package/dist/user-event/pointer.d.ts +10 -0
  122. package/dist/user-event/pointer.d.ts.map +1 -0
  123. package/dist/user-event/pointer.js +30 -0
  124. package/dist/user-event/pointer.js.map +1 -0
  125. package/dist/user-event/selection.d.ts +5 -0
  126. package/dist/user-event/selection.d.ts.map +1 -0
  127. package/dist/user-event/selection.js +105 -0
  128. package/dist/user-event/selection.js.map +1 -0
  129. package/dist/user-event/state.d.ts +7 -0
  130. package/dist/user-event/state.d.ts.map +1 -0
  131. package/dist/user-event/state.js +3 -0
  132. package/dist/user-event/state.js.map +1 -0
  133. package/dist/user-event/text.d.ts +18 -0
  134. package/dist/user-event/text.d.ts.map +1 -0
  135. package/dist/user-event/text.js +113 -0
  136. package/dist/user-event/text.js.map +1 -0
  137. package/dist/wait-for.d.ts +16 -36
  138. package/dist/wait-for.d.ts.map +1 -1
  139. package/dist/wait-for.js +74 -70
  140. package/dist/wait-for.js.map +1 -1
  141. package/dist/widget-accessible-properties.d.ts +34 -0
  142. package/dist/widget-accessible-properties.d.ts.map +1 -0
  143. package/dist/widget-accessible-properties.js +260 -0
  144. package/dist/widget-accessible-properties.js.map +1 -0
  145. package/dist/within.d.ts +10 -26
  146. package/dist/within.d.ts.map +1 -1
  147. package/dist/within.js +16 -25
  148. package/dist/within.js.map +1 -1
  149. package/package.json +23 -12
  150. package/src/act.ts +77 -0
  151. package/src/bound-queries.ts +37 -0
  152. package/src/build-queries.ts +226 -0
  153. package/src/cleanup-registry.ts +27 -0
  154. package/src/config.ts +35 -62
  155. package/src/editable.ts +37 -0
  156. package/src/errors.ts +191 -0
  157. package/src/fire-event.ts +13 -38
  158. package/src/frame-sync.ts +51 -0
  159. package/src/index.ts +58 -15
  160. package/src/matchers.ts +251 -0
  161. package/src/pretty-widget.ts +163 -130
  162. package/src/queries.ts +485 -396
  163. package/src/register-test-hooks.ts +19 -0
  164. package/src/render-hook.tsx +38 -45
  165. package/src/render.tsx +192 -155
  166. package/src/role-helpers.ts +65 -72
  167. package/src/screen.ts +26 -114
  168. package/src/screenshot.ts +128 -56
  169. package/src/suggestions.ts +106 -0
  170. package/src/traversal.ts +56 -22
  171. package/src/types.ts +144 -172
  172. package/src/user-event/adjustment.ts +62 -0
  173. package/src/user-event/click.ts +99 -0
  174. package/src/user-event/controller.ts +58 -0
  175. package/src/user-event/dispatch.ts +25 -0
  176. package/src/user-event/event-wrapper.ts +80 -0
  177. package/src/user-event/gesture.ts +239 -0
  178. package/src/user-event/index.ts +81 -0
  179. package/src/user-event/keyboard.ts +290 -0
  180. package/src/user-event/pointer.ts +46 -0
  181. package/src/user-event/selection.ts +159 -0
  182. package/src/user-event/state.ts +8 -0
  183. package/src/user-event/text.ts +172 -0
  184. package/src/wait-for.ts +117 -78
  185. package/src/widget-accessible-properties.ts +388 -0
  186. package/src/within.ts +29 -27
  187. package/dist/bind-queries.d.ts +0 -14
  188. package/dist/bind-queries.d.ts.map +0 -1
  189. package/dist/bind-queries.js +0 -29
  190. package/dist/bind-queries.js.map +0 -1
  191. package/dist/error-builder.d.ts +0 -28
  192. package/dist/error-builder.d.ts.map +0 -1
  193. package/dist/error-builder.js +0 -81
  194. package/dist/error-builder.js.map +0 -1
  195. package/dist/timing.d.ts +0 -18
  196. package/dist/timing.d.ts.map +0 -1
  197. package/dist/timing.js +0 -18
  198. package/dist/timing.js.map +0 -1
  199. package/dist/user-event.d.ts +0 -138
  200. package/dist/user-event.d.ts.map +0 -1
  201. package/dist/user-event.js +0 -411
  202. package/dist/user-event.js.map +0 -1
  203. package/dist/widget-text.d.ts +0 -69
  204. package/dist/widget-text.d.ts.map +0 -1
  205. package/dist/widget-text.js +0 -177
  206. package/dist/widget-text.js.map +0 -1
  207. package/dist/widget.d.ts +0 -3
  208. package/dist/widget.d.ts.map +0 -1
  209. package/dist/widget.js +0 -13
  210. package/dist/widget.js.map +0 -1
  211. package/src/bind-queries.ts +0 -52
  212. package/src/error-builder.ts +0 -102
  213. package/src/timing.ts +0 -17
  214. package/src/user-event.ts +0 -492
  215. package/src/widget-text.ts +0 -202
  216. package/src/widget.ts +0 -15
package/src/wait-for.ts CHANGED
@@ -1,116 +1,155 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
2
- import { getConfig } from "./config.js";
3
- import { buildTimeoutError } from "./error-builder.js";
1
+ import type * as Gtk from "@gtkx/gi/gtk";
4
2
  import type { WaitForOptions } from "./types.js";
3
+ import { runWithActEnvironment } from "./act.js";
4
+ import { getConfig } from "./config.js";
5
+ import { timeoutError } from "./errors.js";
6
+
7
+ type PollResult<T> = { status: "resolved"; value: T } | { status: "timedout"; lastError: Error | null };
8
+ type RemovalTarget = Gtk.Widget | Gtk.Widget[] | null;
9
+ type ElementOrCallback = Gtk.Widget | Gtk.Widget[] | (() => RemovalTarget);
5
10
 
6
11
  const DEFAULT_INTERVAL = 50;
12
+ const ELEMENT_NOT_REMOVED = new Error("Element not yet removed");
7
13
 
8
- /**
9
- * Waits for a callback to succeed.
10
- *
11
- * Repeatedly calls the callback until it returns without throwing,
12
- * or until the timeout is reached.
13
- *
14
- * @param callback - Function to execute repeatedly
15
- * @param options - Timeout and interval configuration
16
- * @returns Promise resolving to the callback's return value
17
- *
18
- * @example
19
- * ```tsx
20
- * import { waitFor } from "@gtkx/testing";
21
- *
22
- * await waitFor(() => {
23
- * expect(counter.value).toBe(5);
24
- * }, { timeout: 2000 });
25
- * ```
26
- */
27
- export const waitFor = async <T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T> => {
28
- const config = getConfig();
29
- const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
14
+ const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
15
+
16
+ const copyStackTrace = (target: Error, source: Error): void => {
17
+ const { stack } = source;
18
+
19
+ if (stack === undefined) {
20
+ return;
21
+ }
22
+
23
+ const index = stack.indexOf(source.message);
24
+
25
+ if (index === -1) {
26
+ target.stack = stack;
27
+
28
+ return;
29
+ }
30
+
31
+ target.stack = stack.slice(0, index) + target.message + stack.slice(index + source.message.length);
32
+ };
33
+
34
+ const pollUntilSuccess = async <T>(
35
+ callback: () => T | Promise<T>,
36
+ timeout: number,
37
+ interval: number,
38
+ ): Promise<PollResult<T>> => {
30
39
  const startTime = Date.now();
31
40
  let lastError: Error | null = null;
32
41
 
33
42
  while (Date.now() - startTime < timeout) {
34
43
  try {
35
- return await callback();
44
+ const result = await callback();
45
+ await delay(0);
46
+
47
+ return { status: "resolved", value: result };
36
48
  } catch (error) {
37
49
  lastError = error as Error;
38
- await new Promise((resolve) => setTimeout(resolve, interval));
50
+ await delay(interval);
39
51
  }
40
52
  }
41
53
 
42
- const timeoutError = buildTimeoutError(timeout, lastError);
43
- if (onTimeout) {
44
- throw onTimeout(timeoutError);
45
- }
46
- throw timeoutError;
54
+ return { status: "timedout", lastError };
47
55
  };
48
56
 
49
- /** @internal */
50
- type ElementOrCallback = Gtk.Widget | (() => Gtk.Widget | null);
57
+ const buildTimeoutError = (
58
+ timeout: number,
59
+ lastError: Error | null,
60
+ stackTraceError: Error,
61
+ onTimeout: ((error: Error) => Error) | undefined,
62
+ ): Error => {
63
+ const error = timeoutError(timeout, lastError);
64
+ const finalError = onTimeout ? onTimeout(error) : error;
65
+ copyStackTrace(finalError, stackTraceError);
51
66
 
52
- const getElement = (elementOrCallback: ElementOrCallback): Gtk.Widget | null => {
53
- if (typeof elementOrCallback === "function") {
54
- return elementOrCallback();
67
+ return finalError;
68
+ };
69
+
70
+ /**
71
+ * Repeatedly invokes a callback until it succeeds without throwing or the
72
+ * timeout elapses, retrying on each rejection at a fixed interval.
73
+ *
74
+ * @param callback The assertion or query to retry; its resolved value is returned.
75
+ * @param options Optional timeout, interval, and timeout error customization.
76
+ * @returns The callback's result once it succeeds.
77
+ */
78
+ const waitFor = <T>(callback: () => T | Promise<T>, options?: WaitForOptions): Promise<T> => {
79
+ if (typeof callback !== "function") {
80
+ throw new TypeError("Received `callback` arg must be a function");
55
81
  }
56
- return elementOrCallback;
82
+
83
+ const stackTraceError = options?.stackTraceError ?? new Error("STACK_TRACE_MESSAGE");
84
+
85
+ return Promise.resolve(
86
+ runWithActEnvironment(false, async () => {
87
+ const config = getConfig();
88
+ const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
89
+ const result = await pollUntilSuccess(callback, timeout, interval);
90
+
91
+ if (result.status === "resolved") {
92
+ return result.value;
93
+ }
94
+
95
+ throw buildTimeoutError(timeout, result.lastError, stackTraceError, onTimeout);
96
+ }),
97
+ );
57
98
  };
58
99
 
59
- const isElementRemoved = (element: Gtk.Widget | null): boolean => {
60
- if (element === null) return true;
100
+ const getTarget = (elementOrCallback: ElementOrCallback): RemovalTarget =>
101
+ typeof elementOrCallback === "function" ? elementOrCallback() : elementOrCallback;
102
+
103
+ const isWidgetRemoved = (widget: Gtk.Widget | null): boolean => {
104
+ if (widget === null) {
105
+ return true;
106
+ }
61
107
 
62
108
  try {
63
- const parent = element.getParent();
64
- return parent === null;
109
+ return widget.getRoot() === null;
65
110
  } catch {
66
111
  return true;
67
112
  }
68
113
  };
69
114
 
115
+ const isTargetRemoved = (target: RemovalTarget): boolean => {
116
+ if (Array.isArray(target)) {
117
+ return target.every((widget) => isWidgetRemoved(widget));
118
+ }
119
+
120
+ return isWidgetRemoved(target);
121
+ };
122
+
70
123
  /**
71
- * Waits for an element to be removed from the widget tree.
72
- *
73
- * Polls until the element no longer has a parent or no longer exists.
74
- *
75
- * @param elementOrCallback - Element or function returning element to watch
76
- * @param options - Timeout and interval configuration
124
+ * Waits until the given widget or widgets are detached from the tree. Rejects
125
+ * immediately if the target is already absent when called.
77
126
  *
78
- * @example
79
- * ```tsx
80
- * import { waitForElementToBeRemoved } from "@gtkx/testing";
81
- *
82
- * const loader = await screen.findByRole(Gtk.AccessibleRole.PROGRESS_BAR);
83
- * await waitForElementToBeRemoved(loader);
84
- * // Loader is now gone
85
- * ```
127
+ * @param elementOrCallback The widget, array of widgets, or a function
128
+ * returning them to observe for removal.
129
+ * @param options Optional timeout and interval settings.
86
130
  */
87
- export const waitForElementToBeRemoved = async (
131
+ const waitForElementToBeRemoved = (
88
132
  elementOrCallback: ElementOrCallback,
89
133
  options?: WaitForOptions,
90
134
  ): Promise<void> => {
91
- const config = getConfig();
92
- const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
135
+ const stackTraceError = new Error("STACK_TRACE_MESSAGE");
93
136
 
94
- const initialElement = getElement(elementOrCallback);
95
- if (initialElement === null || isElementRemoved(initialElement)) {
96
- throw new Error(
97
- "Element already removed: waitForElementToBeRemoved requires the element to be present initially",
137
+ if (isTargetRemoved(getTarget(elementOrCallback))) {
138
+ return Promise.reject(
139
+ new Error(
140
+ "Element already removed: waitForElementToBeRemoved requires the element to be present initially",
141
+ ),
98
142
  );
99
143
  }
100
144
 
101
- const startTime = Date.now();
102
-
103
- while (Date.now() - startTime < timeout) {
104
- const element = getElement(elementOrCallback);
105
- if (isElementRemoved(element)) {
106
- return;
107
- }
108
- await new Promise((resolve) => setTimeout(resolve, interval));
109
- }
110
-
111
- const timeoutError = new Error(`Timed out after ${timeout}ms waiting for element to be removed`);
112
- if (onTimeout) {
113
- throw onTimeout(timeoutError);
114
- }
115
- throw timeoutError;
145
+ return waitFor(
146
+ () => {
147
+ if (!isTargetRemoved(getTarget(elementOrCallback))) {
148
+ throw ELEMENT_NOT_REMOVED;
149
+ }
150
+ },
151
+ { ...options, stackTraceError },
152
+ );
116
153
  };
154
+
155
+ export { waitFor, waitForElementToBeRemoved };
@@ -0,0 +1,388 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { getAccessibleMetadata } from "@gtkx/react/internal";
3
+ import { EDITABLE_ROLES, isEditable, readEditableText } from "./editable.js";
4
+ import { descendants } from "./traversal.js";
5
+
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
+
15
+ const DEFAULT_TEXT_GETTERS = ["getLabel", "getText", "getTitle"] as const;
16
+
17
+ const callStringGetter = (widget: Gtk.Widget, method: string): string | null => {
18
+ const fn: unknown = Reflect.get(widget, method);
19
+
20
+ if (typeof fn !== "function") {
21
+ return null;
22
+ }
23
+
24
+ const value = (fn as () => string | null).call(widget);
25
+
26
+ return value ?? null;
27
+ };
28
+
29
+ const readAccessibleString = (widget: Gtk.Widget, key: string): string | null => {
30
+ const value = getAccessibleMetadata(widget, key);
31
+
32
+ return typeof value === "string" ? value : null;
33
+ };
34
+
35
+ const readAccessibleNumber = (widget: Gtk.Widget, key: string): number | null => {
36
+ const value = getAccessibleMetadata(widget, key);
37
+
38
+ return typeof value === "number" ? value : null;
39
+ };
40
+
41
+ const readAccessibleWidgets = (widget: Gtk.Widget, key: string): Gtk.Widget[] | null => {
42
+ const value = getAccessibleMetadata(widget, key);
43
+
44
+ if (!Array.isArray(value)) {
45
+ return null;
46
+ }
47
+
48
+ const widgets = value.filter((item): item is Gtk.Widget => item instanceof Gtk.Widget);
49
+
50
+ return widgets.length > 0 ? widgets : null;
51
+ };
52
+
53
+ const readAccessibleBoolean = (widget: Gtk.Widget, key: string): boolean | null => {
54
+ const value = getAccessibleMetadata(widget, key);
55
+
56
+ return typeof value === "boolean" ? value : null;
57
+ };
58
+
59
+ const getLabelText = (widget: Gtk.Widget): string | null => {
60
+ if (widget instanceof Gtk.Label) {
61
+ return widget.getLabel();
62
+ }
63
+
64
+ if (widget instanceof Gtk.Inscription) {
65
+ return widget.getText() ?? null;
66
+ }
67
+
68
+ return null;
69
+ };
70
+
71
+ /**
72
+ * Returns a widget's own text by trying its label, text, and title getters in
73
+ * order, or null when none produce a value.
74
+ *
75
+ * @param widget The widget to read text from.
76
+ */
77
+ const getWidgetNodeText = (widget: Gtk.Widget): string | null => {
78
+ for (const getter of DEFAULT_TEXT_GETTERS) {
79
+ const value = callStringGetter(widget, getter);
80
+
81
+ if (value) {
82
+ return value;
83
+ }
84
+ }
85
+
86
+ return null;
87
+ };
88
+
89
+ const stripMnemonic = (text: string): string => text.replaceAll(/_(.)/g, "$1");
90
+
91
+ const namingLabelText = (widget: Gtk.Widget): string | null => {
92
+ const text = getLabelText(widget);
93
+
94
+ if (text === null) {
95
+ return null;
96
+ }
97
+
98
+ return widget instanceof Gtk.Label && widget.getUseUnderline() ? stripMnemonic(text) : text;
99
+ };
100
+
101
+ const isNamingLabelRole = (role: Gtk.AccessibleRole, shouldIncludePresentation: boolean): boolean =>
102
+ role === Gtk.AccessibleRole.LABEL ||
103
+ (shouldIncludePresentation && role === Gtk.AccessibleRole.PRESENTATION);
104
+
105
+ const collectLabels = (widget: Gtk.Widget): string[] => {
106
+ const isIncludePresentation = widget.getAccessibleRole() === Gtk.AccessibleRole.MENU_ITEM;
107
+ const labels: string[] = [];
108
+
109
+ for (const descendant of descendants(widget)) {
110
+ if (!isNamingLabelRole(descendant.getAccessibleRole(), isIncludePresentation)) {
111
+ continue;
112
+ }
113
+
114
+ const labelText = namingLabelText(descendant);
115
+
116
+ if (labelText) {
117
+ labels.push(labelText);
118
+ }
119
+ }
120
+
121
+ return labels;
122
+ };
123
+
124
+ const getWidgetLabelText = (widget: Gtk.Widget): string | null => {
125
+ if (widget.getAccessibleRole() !== Gtk.AccessibleRole.LABEL) {
126
+ return null;
127
+ }
128
+
129
+ return getLabelText(widget);
130
+ };
131
+
132
+ const tabPanelTitle = (widget: Gtk.Widget): string | null => {
133
+ const parent = widget.getParent();
134
+
135
+ if (parent instanceof Gtk.Stack) {
136
+ return parent.getPage(widget).getTitle() ?? null;
137
+ }
138
+
139
+ return null;
140
+ };
141
+
142
+ const getWidgetAccessibleName = (widget: Gtk.Widget): string | null => {
143
+ const role = widget.getAccessibleRole();
144
+
145
+ if (role === Gtk.AccessibleRole.TAB_PANEL) {
146
+ return tabPanelTitle(widget);
147
+ }
148
+
149
+ const accessibleLabel = readAccessibleString(widget, "accessibleLabel");
150
+
151
+ if (accessibleLabel) {
152
+ return accessibleLabel;
153
+ }
154
+
155
+ const ownText = getWidgetNodeText(widget);
156
+
157
+ if (ownText) {
158
+ return ownText;
159
+ }
160
+
161
+ const childLabels = collectLabels(widget);
162
+
163
+ if (childLabels.length > 0) {
164
+ return childLabels.join(" ");
165
+ }
166
+
167
+ return callStringGetter(widget, "getTooltipText");
168
+ };
169
+
170
+ const getWidgetName = (widget: Gtk.Widget): string | null => {
171
+ return widget.getName();
172
+ };
173
+
174
+ const getWidgetPlaceholderText = (widget: Gtk.Widget): string | null => {
175
+ if (!EDITABLE_ROLES.has(widget.getAccessibleRole()) || !(widget instanceof Gtk.Editable)) {
176
+ return null;
177
+ }
178
+
179
+ return callStringGetter(widget, "getPlaceholderText");
180
+ };
181
+
182
+ const getWidgetDisplayValue = (widget: Gtk.Widget): string | null => {
183
+ if (EDITABLE_ROLES.has(widget.getAccessibleRole()) && isEditable(widget)) {
184
+ return readEditableText(widget);
185
+ }
186
+
187
+ return null;
188
+ };
189
+
190
+ const getWidgetCheckedState = (widget: Gtk.Widget): boolean | null => {
191
+ if (widget instanceof Gtk.CheckButton) {
192
+ return widget.getActive();
193
+ }
194
+
195
+ if (widget instanceof Gtk.Switch) {
196
+ return widget.getActive();
197
+ }
198
+
199
+ if (widget instanceof Gtk.ToggleButton && widget.getAccessibleRole() === Gtk.AccessibleRole.RADIO) {
200
+ return widget.getActive();
201
+ }
202
+
203
+ return null;
204
+ };
205
+
206
+ const getWidgetPressedState = (widget: Gtk.Widget): boolean | null => {
207
+ if (widget instanceof Gtk.ToggleButton) {
208
+ return widget.getActive();
209
+ }
210
+
211
+ return null;
212
+ };
213
+
214
+ const getWidgetExpandedState = (widget: Gtk.Widget): boolean | null => {
215
+ if (widget instanceof Gtk.Expander) {
216
+ return widget.getExpanded();
217
+ }
218
+
219
+ if (widget instanceof Gtk.TreeExpander) {
220
+ return widget.getListRow()?.getExpanded() ?? null;
221
+ }
222
+
223
+ return null;
224
+ };
225
+
226
+ const getWidgetSelectedState = (widget: Gtk.Widget): boolean | null => {
227
+ if (widget instanceof Gtk.ListBoxRow) {
228
+ return widget.isSelected();
229
+ }
230
+
231
+ if (widget.getAccessibleRole() === Gtk.AccessibleRole.ROW) {
232
+ return (widget.getStateFlags() & Gtk.StateFlags.SELECTED) !== 0;
233
+ }
234
+
235
+ return null;
236
+ };
237
+
238
+ const getWidgetLevel = (widget: Gtk.Widget): number | null => {
239
+ return readAccessibleNumber(widget, "accessibleLevel");
240
+ };
241
+
242
+ const getWidgetInvalidState = (widget: Gtk.Widget): Gtk.AccessibleInvalidState | null => {
243
+ const value = readAccessibleNumber(widget, "accessibleInvalid");
244
+
245
+ return value ?? null;
246
+ };
247
+
248
+ const getWidgetErrorMessage = (widget: Gtk.Widget): Gtk.Widget[] | null =>
249
+ readAccessibleWidgets(widget, "accessibleErrorMessage");
250
+
251
+ const getWidgetBusyState = (widget: Gtk.Widget): boolean | null => {
252
+ return readAccessibleBoolean(widget, "accessibleBusy");
253
+ };
254
+
255
+ const getWidgetDescription = (widget: Gtk.Widget): string | null => {
256
+ return readAccessibleString(widget, "accessibleDescription");
257
+ };
258
+
259
+ const adjustmentValue = (adjustment: Gtk.Adjustment): ValueTriplet => ({
260
+ now: adjustment.getValue(),
261
+ min: adjustment.getLower(),
262
+ max: adjustment.getUpper(),
263
+ });
264
+
265
+ const adjustmentWidgetValue = (widget: Gtk.Widget): ValueTriplet | null => {
266
+ if (widget instanceof Gtk.Range) {
267
+ return adjustmentValue(widget.getAdjustment());
268
+ }
269
+
270
+ if (widget instanceof Gtk.Scrollbar) {
271
+ return adjustmentValue(widget.getAdjustment());
272
+ }
273
+
274
+ if (widget instanceof Gtk.SpinButton) {
275
+ return adjustmentValue(widget.getAdjustment());
276
+ }
277
+
278
+ if (widget instanceof Gtk.ScaleButton) {
279
+ return adjustmentValue(widget.getAdjustment());
280
+ }
281
+
282
+ return null;
283
+ };
284
+
285
+ const getWidgetLiveValue = (widget: Gtk.Widget): ValueTriplet | null => {
286
+ const adjustmentBased = adjustmentWidgetValue(widget);
287
+
288
+ if (adjustmentBased) {
289
+ return adjustmentBased;
290
+ }
291
+
292
+ if (widget instanceof Gtk.LevelBar) {
293
+ return { now: widget.getValue(), min: widget.getMinValue(), max: widget.getMaxValue() };
294
+ }
295
+
296
+ if (widget instanceof Gtk.ProgressBar) {
297
+ return { now: widget.getFraction(), min: 0, max: 1 };
298
+ }
299
+
300
+ return null;
301
+ };
302
+
303
+ const getWidgetValue = (widget: Gtk.Widget): WidgetValue => {
304
+ const text = readAccessibleString(widget, "accessibleValueText");
305
+ const live = getWidgetLiveValue(widget);
306
+
307
+ if (live) {
308
+ return { ...live, text };
309
+ }
310
+
311
+ return {
312
+ now: readAccessibleNumber(widget, "accessibleValueNow"),
313
+ min: readAccessibleNumber(widget, "accessibleValueMin"),
314
+ max: readAccessibleNumber(widget, "accessibleValueMax"),
315
+ text,
316
+ };
317
+ };
318
+
319
+ const getWidgetOwnLabel = (widget: Gtk.Widget): string | null => {
320
+ return readAccessibleString(widget, "accessibleLabel");
321
+ };
322
+
323
+ const collectAccessibleNames = (targets: Gtk.Widget[]): string[] => {
324
+ const texts: string[] = [];
325
+
326
+ for (const target of targets) {
327
+ const text = getWidgetAccessibleName(target);
328
+
329
+ if (text !== null) {
330
+ texts.push(text);
331
+ }
332
+ }
333
+
334
+ return texts;
335
+ };
336
+
337
+ const getWidgetLabelledByText = (widget: Gtk.Widget): string | null => {
338
+ const targets = readAccessibleWidgets(widget, "accessibleLabelledBy");
339
+
340
+ if (targets === null) {
341
+ return null;
342
+ }
343
+
344
+ const texts = collectAccessibleNames(targets);
345
+
346
+ return texts.length > 0 ? texts.join(" ") : null;
347
+ };
348
+
349
+ const isInaccessible = (widget: Gtk.Widget): boolean => {
350
+ let current: Gtk.Widget | null = widget;
351
+
352
+ while (current) {
353
+ if (!current.getVisible()) {
354
+ return true;
355
+ }
356
+
357
+ if (readAccessibleBoolean(current, "accessibleHidden") === true) {
358
+ return true;
359
+ }
360
+
361
+ current = current.getParent();
362
+ }
363
+
364
+ return false;
365
+ };
366
+
367
+ export {
368
+ getWidgetNodeText,
369
+ getWidgetLabelText,
370
+ getWidgetAccessibleName,
371
+ getWidgetName,
372
+ getWidgetPlaceholderText,
373
+ getWidgetDisplayValue,
374
+ getWidgetCheckedState,
375
+ getWidgetPressedState,
376
+ getWidgetExpandedState,
377
+ getWidgetSelectedState,
378
+ getWidgetLevel,
379
+ getWidgetInvalidState,
380
+ getWidgetErrorMessage,
381
+ getWidgetBusyState,
382
+ getWidgetDescription,
383
+ getWidgetValue,
384
+ getWidgetOwnLabel,
385
+ getWidgetLabelledByText,
386
+ isInaccessible,
387
+ type WidgetValue,
388
+ };
package/src/within.ts CHANGED
@@ -1,31 +1,33 @@
1
- import type * as Gtk from "@gtkx/ffi/gtk";
2
- import { bindQueries } from "./bind-queries.js";
3
- import type { BoundQueries } from "./types.js";
1
+ import type { BoundQueries } from "./bound-queries.js";
2
+ import type { Container } from "./traversal.js";
3
+ import type { BoundCustomQueries, QueryMap } from "./types.js";
4
+ import { builtinQueries } from "./queries.js";
5
+
6
+ const bindQuery =
7
+ <Args extends unknown[], Result>(query: (container: Container, ...args: Args) => Result, container: Container) =>
8
+ (...args: Args): Result =>
9
+ query(container, ...args);
10
+
11
+ const bindCustomQueries = <Q extends QueryMap>(customQueries: Q, container: Container): BoundCustomQueries<Q> => {
12
+ const entries = Object.entries(customQueries).map(([key, query]) => [key, bindQuery(query, container)] as const);
13
+
14
+ return Object.fromEntries(entries) as BoundCustomQueries<Q>;
15
+ };
4
16
 
5
17
  /**
6
- * Creates scoped query methods for a container widget.
7
- *
8
- * Use this to query within a specific section of your UI rather than
9
- * the entire application.
10
- *
11
- * @param container - The widget to scope queries to
12
- * @returns Object with query methods bound to the container
18
+ * Binds the built-in queries, and any custom queries, to a specific container
19
+ * so they search only within that scope.
13
20
  *
14
- * @example
15
- * ```tsx
16
- * import { render, within } from "@gtkx/testing";
17
- *
18
- * test("scoped queries", async () => {
19
- * await render(<MyPage />);
20
- *
21
- * const sidebar = await screen.findByRole(Gtk.AccessibleRole.NAVIGATION);
22
- * const sidebarQueries = within(sidebar);
23
- *
24
- * // Only searches within the sidebar
25
- * const navButton = await sidebarQueries.findByRole(Gtk.AccessibleRole.BUTTON);
26
- * });
27
- * ```
28
- *
29
- * @see {@link screen} for global queries
21
+ * @param container The scope the returned queries operate on.
22
+ * @param queries Optional custom queries to bind alongside the built-in ones.
23
+ * @returns The queries bound to the container.
30
24
  */
31
- export const within = (container: Gtk.Widget): BoundQueries => bindQueries(container);
25
+ const within = <Q extends QueryMap = Record<never, never>>(
26
+ container: Container,
27
+ queries?: Q,
28
+ ): BoundQueries & BoundCustomQueries<Q> => ({
29
+ ...bindCustomQueries(builtinQueries, container),
30
+ ...(queries ? bindCustomQueries(queries, container) : ({} as BoundCustomQueries<Q>)),
31
+ });
32
+
33
+ export { within };
@@ -1,14 +0,0 @@
1
- import type { Container } from "./traversal.js";
2
- import type { BoundQueries } from "./types.js";
3
- type ContainerOrGetter = Container | (() => Container);
4
- /**
5
- * Binds all query functions to a container.
6
- *
7
- * @param containerOrGetter - The container to bind queries to, or a function that returns it
8
- * @returns Object with all query methods bound to the container
9
- *
10
- * @internal
11
- */
12
- export declare const bindQueries: (containerOrGetter: ContainerOrGetter) => BoundQueries;
13
- export {};
14
- //# sourceMappingURL=bind-queries.d.ts.map