@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.
- package/README.md +137 -34
- package/dist/act.d.ts +19 -0
- package/dist/act.d.ts.map +1 -0
- package/dist/act.js +56 -0
- package/dist/act.js.map +1 -0
- package/dist/bound-queries.d.ts +33 -0
- package/dist/bound-queries.d.ts.map +1 -0
- package/dist/bound-queries.js +2 -0
- package/dist/bound-queries.js.map +1 -0
- package/dist/build-queries.d.ts +16 -0
- package/dist/build-queries.d.ts.map +1 -0
- package/dist/build-queries.js +115 -0
- package/dist/build-queries.js.map +1 -0
- package/dist/cleanup-registry.d.ts +5 -0
- package/dist/cleanup-registry.d.ts.map +1 -0
- package/dist/cleanup-registry.js +21 -0
- package/dist/cleanup-registry.js.map +1 -0
- package/dist/config.d.ts +20 -52
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -46
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +8 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +26 -0
- package/dist/editable.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/fire-event.d.ts +8 -25
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +11 -30
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +4 -0
- package/dist/frame-sync.d.ts.map +1 -0
- package/dist/frame-sync.js +37 -0
- package/dist/frame-sync.js.map +1 -0
- package/dist/index.d.ts +17 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +59 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +117 -0
- package/dist/matchers.js.map +1 -0
- package/dist/pretty-widget.d.ts +24 -26
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +111 -117
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +190 -180
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +291 -320
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.d.ts +2 -0
- package/dist/register-test-hooks.d.ts.map +1 -0
- package/dist/register-test-hooks.js +16 -0
- package/dist/register-test-hooks.js.map +1 -0
- package/dist/render-hook.d.ts +15 -35
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +9 -45
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +13 -42
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +138 -121
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +19 -53
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +48 -63
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +7 -63
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +24 -110
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +21 -23
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +98 -50
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +27 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/suggestions.js +61 -0
- package/dist/suggestions.js.map +1 -0
- package/dist/traversal.d.ts +12 -10
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +36 -15
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +115 -169
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +10 -0
- package/dist/user-event/adjustment.d.ts.map +1 -0
- package/dist/user-event/adjustment.js +35 -0
- package/dist/user-event/adjustment.js.map +1 -0
- package/dist/user-event/click.d.ts +8 -0
- package/dist/user-event/click.d.ts.map +1 -0
- package/dist/user-event/click.js +73 -0
- package/dist/user-event/click.js.map +1 -0
- package/dist/user-event/controller.d.ts +8 -0
- package/dist/user-event/controller.d.ts.map +1 -0
- package/dist/user-event/controller.js +31 -0
- package/dist/user-event/controller.js.map +1 -0
- package/dist/user-event/dispatch.d.ts +7 -0
- package/dist/user-event/dispatch.d.ts.map +1 -0
- package/dist/user-event/dispatch.js +11 -0
- package/dist/user-event/dispatch.js.map +1 -0
- package/dist/user-event/event-wrapper.d.ts +4 -0
- package/dist/user-event/event-wrapper.d.ts.map +1 -0
- package/dist/user-event/event-wrapper.js +59 -0
- package/dist/user-event/event-wrapper.js.map +1 -0
- package/dist/user-event/gesture.d.ts +35 -0
- package/dist/user-event/gesture.d.ts.map +1 -0
- package/dist/user-event/gesture.js +138 -0
- package/dist/user-event/gesture.js.map +1 -0
- package/dist/user-event/index.d.ts +51 -0
- package/dist/user-event/index.d.ts.map +1 -0
- package/dist/user-event/index.js +42 -0
- package/dist/user-event/index.js.map +1 -0
- package/dist/user-event/keyboard.d.ts +10 -0
- package/dist/user-event/keyboard.d.ts.map +1 -0
- package/dist/user-event/keyboard.js +204 -0
- package/dist/user-event/keyboard.js.map +1 -0
- package/dist/user-event/pointer.d.ts +10 -0
- package/dist/user-event/pointer.d.ts.map +1 -0
- package/dist/user-event/pointer.js +30 -0
- package/dist/user-event/pointer.js.map +1 -0
- package/dist/user-event/selection.d.ts +5 -0
- package/dist/user-event/selection.d.ts.map +1 -0
- package/dist/user-event/selection.js +105 -0
- package/dist/user-event/selection.js.map +1 -0
- package/dist/user-event/state.d.ts +7 -0
- package/dist/user-event/state.d.ts.map +1 -0
- package/dist/user-event/state.js +3 -0
- package/dist/user-event/state.js.map +1 -0
- package/dist/user-event/text.d.ts +18 -0
- package/dist/user-event/text.d.ts.map +1 -0
- package/dist/user-event/text.js +113 -0
- package/dist/user-event/text.js.map +1 -0
- package/dist/wait-for.d.ts +16 -36
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +74 -70
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +34 -0
- package/dist/widget-accessible-properties.d.ts.map +1 -0
- package/dist/widget-accessible-properties.js +260 -0
- package/dist/widget-accessible-properties.js.map +1 -0
- package/dist/within.d.ts +10 -26
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +16 -25
- package/dist/within.js.map +1 -1
- package/package.json +23 -12
- package/src/act.ts +77 -0
- package/src/bound-queries.ts +37 -0
- package/src/build-queries.ts +226 -0
- package/src/cleanup-registry.ts +27 -0
- package/src/config.ts +35 -62
- package/src/editable.ts +37 -0
- package/src/errors.ts +191 -0
- package/src/fire-event.ts +13 -38
- package/src/frame-sync.ts +51 -0
- package/src/index.ts +58 -15
- package/src/matchers.ts +251 -0
- package/src/pretty-widget.ts +163 -130
- package/src/queries.ts +485 -396
- package/src/register-test-hooks.ts +19 -0
- package/src/render-hook.tsx +38 -45
- package/src/render.tsx +192 -155
- package/src/role-helpers.ts +65 -72
- package/src/screen.ts +26 -114
- package/src/screenshot.ts +128 -56
- package/src/suggestions.ts +106 -0
- package/src/traversal.ts +56 -22
- package/src/types.ts +144 -172
- package/src/user-event/adjustment.ts +62 -0
- package/src/user-event/click.ts +99 -0
- package/src/user-event/controller.ts +58 -0
- package/src/user-event/dispatch.ts +25 -0
- package/src/user-event/event-wrapper.ts +80 -0
- package/src/user-event/gesture.ts +239 -0
- package/src/user-event/index.ts +81 -0
- package/src/user-event/keyboard.ts +290 -0
- package/src/user-event/pointer.ts +46 -0
- package/src/user-event/selection.ts +159 -0
- package/src/user-event/state.ts +8 -0
- package/src/user-event/text.ts +172 -0
- package/src/wait-for.ts +117 -78
- package/src/widget-accessible-properties.ts +388 -0
- package/src/within.ts +29 -27
- package/dist/bind-queries.d.ts +0 -14
- package/dist/bind-queries.d.ts.map +0 -1
- package/dist/bind-queries.js +0 -29
- package/dist/bind-queries.js.map +0 -1
- package/dist/error-builder.d.ts +0 -28
- package/dist/error-builder.d.ts.map +0 -1
- package/dist/error-builder.js +0 -81
- package/dist/error-builder.js.map +0 -1
- package/dist/timing.d.ts +0 -18
- package/dist/timing.d.ts.map +0 -1
- package/dist/timing.js +0 -18
- package/dist/timing.js.map +0 -1
- package/dist/user-event.d.ts +0 -138
- package/dist/user-event.d.ts.map +0 -1
- package/dist/user-event.js +0 -411
- package/dist/user-event.js.map +0 -1
- package/dist/widget-text.d.ts +0 -69
- package/dist/widget-text.d.ts.map +0 -1
- package/dist/widget-text.js +0 -177
- package/dist/widget-text.js.map +0 -1
- package/dist/widget.d.ts +0 -3
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -13
- package/dist/widget.js.map +0 -1
- package/src/bind-queries.ts +0 -52
- package/src/error-builder.ts +0 -102
- package/src/timing.ts +0 -17
- package/src/user-event.ts +0 -492
- package/src/widget-text.ts +0 -202
- package/src/widget.ts +0 -15
package/src/wait-for.ts
CHANGED
|
@@ -1,116 +1,155 @@
|
|
|
1
|
-
import type * as Gtk from "@gtkx/
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
50
|
+
await delay(interval);
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
if (onTimeout) {
|
|
44
|
-
throw onTimeout(timeoutError);
|
|
45
|
-
}
|
|
46
|
-
throw timeoutError;
|
|
54
|
+
return { status: "timedout", lastError };
|
|
47
55
|
};
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
* @
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
131
|
+
const waitForElementToBeRemoved = (
|
|
88
132
|
elementOrCallback: ElementOrCallback,
|
|
89
133
|
options?: WaitForOptions,
|
|
90
134
|
): Promise<void> => {
|
|
91
|
-
const
|
|
92
|
-
const { timeout = config.asyncUtilTimeout, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
135
|
+
const stackTraceError = new Error("STACK_TRACE_MESSAGE");
|
|
93
136
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
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 };
|
package/dist/bind-queries.d.ts
DELETED
|
@@ -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
|