@gtkx/testing 1.0.0-rc.1 → 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/dist/act.d.ts +6 -6
- package/dist/act.d.ts.map +1 -1
- package/dist/act.js +34 -26
- package/dist/act.js.map +1 -1
- package/dist/bound-queries.d.ts +4 -3
- package/dist/bound-queries.d.ts.map +1 -1
- package/dist/bound-queries.js.map +1 -1
- package/dist/build-queries.d.ts +5 -5
- package/dist/build-queries.d.ts.map +1 -1
- package/dist/build-queries.js +21 -9
- package/dist/build-queries.js.map +1 -1
- package/dist/cleanup-registry.d.ts +4 -3
- package/dist/cleanup-registry.d.ts.map +1 -1
- package/dist/cleanup-registry.js +13 -3
- package/dist/cleanup-registry.js.map +1 -1
- package/dist/config.d.ts +5 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -9
- package/dist/config.js.map +1 -1
- package/dist/editable.d.ts +6 -5
- package/dist/editable.d.ts.map +1 -1
- package/dist/editable.js +7 -5
- package/dist/editable.js.map +1 -1
- package/dist/errors.d.ts +7 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +64 -48
- package/dist/errors.js.map +1 -1
- package/dist/fire-event.d.ts +2 -1
- package/dist/fire-event.d.ts.map +1 -1
- package/dist/fire-event.js +2 -1
- package/dist/fire-event.js.map +1 -1
- package/dist/frame-sync.d.ts +2 -1
- package/dist/frame-sync.d.ts.map +1 -1
- package/dist/frame-sync.js +28 -21
- package/dist/frame-sync.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +38 -34
- package/dist/matchers.d.ts.map +1 -1
- package/dist/matchers.js +72 -58
- package/dist/matchers.js.map +1 -1
- package/dist/pretty-widget.d.ts +6 -4
- package/dist/pretty-widget.d.ts.map +1 -1
- package/dist/pretty-widget.js +64 -24
- package/dist/pretty-widget.js.map +1 -1
- package/dist/queries.d.ts +143 -86
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +231 -147
- package/dist/queries.js.map +1 -1
- package/dist/register-test-hooks.js +2 -1
- package/dist/register-test-hooks.js.map +1 -1
- package/dist/render-hook.d.ts +3 -2
- package/dist/render-hook.d.ts.map +1 -1
- package/dist/render-hook.js +2 -1
- package/dist/render-hook.js.map +1 -1
- package/dist/render.d.ts +3 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +24 -18
- package/dist/render.js.map +1 -1
- package/dist/role-helpers.d.ts +6 -5
- package/dist/role-helpers.d.ts.map +1 -1
- package/dist/role-helpers.js +16 -12
- package/dist/role-helpers.js.map +1 -1
- package/dist/screen.d.ts +4 -3
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +13 -11
- package/dist/screen.js.map +1 -1
- package/dist/screenshot.d.ts +4 -3
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +43 -36
- package/dist/screenshot.js.map +1 -1
- package/dist/suggestions.d.ts +11 -6
- package/dist/suggestions.d.ts.map +1 -1
- package/dist/suggestions.js +10 -5
- package/dist/suggestions.js.map +1 -1
- package/dist/traversal.d.ts +7 -7
- package/dist/traversal.d.ts.map +1 -1
- package/dist/traversal.js +17 -11
- package/dist/traversal.js.map +1 -1
- package/dist/types.d.ts +39 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/user-event/adjustment.d.ts +5 -3
- package/dist/user-event/adjustment.d.ts.map +1 -1
- package/dist/user-event/adjustment.js +6 -4
- package/dist/user-event/adjustment.js.map +1 -1
- package/dist/user-event/click.d.ts +6 -5
- package/dist/user-event/click.d.ts.map +1 -1
- package/dist/user-event/click.js +29 -21
- package/dist/user-event/click.js.map +1 -1
- package/dist/user-event/controller.d.ts +6 -5
- package/dist/user-event/controller.d.ts.map +1 -1
- package/dist/user-event/controller.js +9 -6
- package/dist/user-event/controller.js.map +1 -1
- package/dist/user-event/dispatch.d.ts +3 -3
- package/dist/user-event/dispatch.d.ts.map +1 -1
- package/dist/user-event/dispatch.js +3 -2
- package/dist/user-event/dispatch.js.map +1 -1
- package/dist/user-event/event-wrapper.d.ts +2 -1
- package/dist/user-event/event-wrapper.d.ts.map +1 -1
- package/dist/user-event/event-wrapper.js +27 -16
- package/dist/user-event/event-wrapper.js.map +1 -1
- package/dist/user-event/gesture.d.ts +18 -14
- package/dist/user-event/gesture.d.ts.map +1 -1
- package/dist/user-event/gesture.js +70 -37
- package/dist/user-event/gesture.js.map +1 -1
- package/dist/user-event/index.d.ts +17 -10
- package/dist/user-event/index.d.ts.map +1 -1
- package/dist/user-event/index.js +7 -3
- package/dist/user-event/index.js.map +1 -1
- package/dist/user-event/keyboard.d.ts +4 -3
- package/dist/user-event/keyboard.d.ts.map +1 -1
- package/dist/user-event/keyboard.js +89 -58
- package/dist/user-event/keyboard.js.map +1 -1
- package/dist/user-event/pointer.d.ts +7 -3
- package/dist/user-event/pointer.d.ts.map +1 -1
- package/dist/user-event/pointer.js +4 -1
- package/dist/user-event/pointer.js.map +1 -1
- package/dist/user-event/selection.d.ts +3 -2
- package/dist/user-event/selection.d.ts.map +1 -1
- package/dist/user-event/selection.js +26 -27
- package/dist/user-event/selection.js.map +1 -1
- package/dist/user-event/state.d.ts +3 -2
- package/dist/user-event/state.d.ts.map +1 -1
- package/dist/user-event/state.js +2 -1
- package/dist/user-event/state.js.map +1 -1
- package/dist/user-event/text.d.ts +8 -7
- package/dist/user-event/text.d.ts.map +1 -1
- package/dist/user-event/text.js +39 -33
- package/dist/user-event/text.js.map +1 -1
- package/dist/wait-for.d.ts +5 -5
- package/dist/wait-for.d.ts.map +1 -1
- package/dist/wait-for.js +45 -30
- package/dist/wait-for.js.map +1 -1
- package/dist/widget-accessible-properties.d.ts +26 -25
- package/dist/widget-accessible-properties.d.ts.map +1 -1
- package/dist/widget-accessible-properties.js +106 -61
- package/dist/widget-accessible-properties.js.map +1 -1
- package/dist/within.d.ts +2 -1
- package/dist/within.d.ts.map +1 -1
- package/dist/within.js +2 -1
- package/dist/within.js.map +1 -1
- package/package.json +11 -9
- package/src/act.ts +44 -35
- package/src/bound-queries.ts +5 -4
- package/src/build-queries.ts +84 -43
- package/src/cleanup-registry.ts +18 -5
- package/src/config.ts +16 -12
- package/src/editable.ts +13 -6
- package/src/errors.ts +106 -45
- package/src/fire-event.ts +3 -1
- package/src/frame-sync.ts +38 -21
- package/src/index.ts +2 -1
- package/src/matchers.ts +158 -107
- package/src/pretty-widget.ts +114 -42
- package/src/queries.ts +422 -233
- package/src/register-test-hooks.ts +4 -1
- package/src/render-hook.tsx +10 -4
- package/src/render.tsx +58 -33
- package/src/role-helpers.ts +29 -16
- package/src/screen.ts +17 -12
- package/src/screenshot.ts +61 -38
- package/src/suggestions.ts +29 -10
- package/src/traversal.ts +35 -15
- package/src/types.ts +65 -32
- package/src/user-event/adjustment.ts +23 -10
- package/src/user-event/click.ts +49 -20
- package/src/user-event/controller.ts +21 -7
- package/src/user-event/dispatch.ts +4 -2
- package/src/user-event/event-wrapper.ts +42 -16
- package/src/user-event/gesture.ts +135 -50
- package/src/user-event/index.ts +19 -12
- package/src/user-event/keyboard.ts +147 -61
- package/src/user-event/pointer.ts +20 -8
- package/src/user-event/selection.ts +69 -32
- package/src/user-event/state.ts +4 -2
- package/src/user-event/text.ts +73 -38
- package/src/wait-for.ts +77 -36
- package/src/widget-accessible-properties.ts +218 -74
- package/src/within.ts +7 -4
|
@@ -4,7 +4,10 @@ import { cleanup } from "./render.js";
|
|
|
4
4
|
|
|
5
5
|
const callRunnerHook = (name: "afterEach" | "afterAll", callback: () => unknown): void => {
|
|
6
6
|
const hook: unknown = Reflect.get(globalThis, name);
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
if (typeof hook === "function") {
|
|
9
|
+
(hook as (callback: () => unknown) => void)(callback);
|
|
10
|
+
}
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
const registerTestRuntimeHooks = (): void => {
|
package/src/render-hook.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
-
import { render } from "./render.js";
|
|
3
2
|
import type { RenderHookOptions, RenderHookResult } from "./types.js";
|
|
3
|
+
import { render } from "./render.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Renders a test component that calls the given hook, exposing its latest
|
|
@@ -10,10 +10,11 @@ import type { RenderHookOptions, RenderHookResult } from "./types.js";
|
|
|
10
10
|
* @param options Optional initial props and wrapper component.
|
|
11
11
|
* @returns A result whose `result.current` holds the hook's latest value.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
function renderHook<Result>(
|
|
14
14
|
callback: () => Result,
|
|
15
15
|
options?: RenderHookOptions<undefined>,
|
|
16
16
|
): Promise<RenderHookResult<Result, undefined>>;
|
|
17
|
+
|
|
17
18
|
/**
|
|
18
19
|
* Renders a test component that calls the given hook with props, exposing its
|
|
19
20
|
* latest return value along with rerender and unmount controls.
|
|
@@ -22,11 +23,12 @@ export function renderHook<Result>(
|
|
|
22
23
|
* @param options Initial props and optional wrapper component.
|
|
23
24
|
* @returns A result whose `result.current` holds the hook's latest value.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
+
function renderHook<Result, Props>(
|
|
26
27
|
callback: (props: Props) => Result,
|
|
27
28
|
options: RenderHookOptions<Props>,
|
|
28
29
|
): Promise<RenderHookResult<Result, Props>>;
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
async function renderHook<Result, Props>(
|
|
30
32
|
callback: (props: Props) => Result,
|
|
31
33
|
options?: RenderHookOptions<Props>,
|
|
32
34
|
): Promise<RenderHookResult<Result, Props>> {
|
|
@@ -37,6 +39,7 @@ export async function renderHook<Result, Props>(
|
|
|
37
39
|
const TestComponent = ({ props }: { props: Props }): null => {
|
|
38
40
|
const result = callback(props);
|
|
39
41
|
resultRef.current = result;
|
|
42
|
+
|
|
40
43
|
return null;
|
|
41
44
|
};
|
|
42
45
|
|
|
@@ -51,8 +54,11 @@ export async function renderHook<Result, Props>(
|
|
|
51
54
|
if (newProps !== undefined) {
|
|
52
55
|
currentProps = newProps;
|
|
53
56
|
}
|
|
57
|
+
|
|
54
58
|
await renderResult.rerender(<TestComponent props={currentProps} />);
|
|
55
59
|
},
|
|
56
60
|
unmount: renderResult.unmount,
|
|
57
61
|
};
|
|
58
62
|
}
|
|
63
|
+
|
|
64
|
+
export { renderHook };
|
package/src/render.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
1
|
import type { RootElement } from "@gtkx/react";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
3
|
import {
|
|
4
4
|
createReconcilerRoot,
|
|
5
5
|
isRootElement,
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
setReconcilerErrorHandler,
|
|
8
8
|
} from "@gtkx/react/internal";
|
|
9
9
|
import { type ErrorInfo, type ReactNode, StrictMode } from "react";
|
|
10
|
-
import { runInAct } from "./act.js";
|
|
11
10
|
import type { RenderResult } from "./bound-queries.js";
|
|
11
|
+
import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
|
|
12
|
+
import { runInAct } from "./act.js";
|
|
12
13
|
import { addToCleanupQueue, runCleanup } from "./cleanup-registry.js";
|
|
13
14
|
import { scheduleAfterLayout } from "./frame-sync.js";
|
|
14
15
|
import { logWidget, type PrettyWidgetOptions } from "./pretty-widget.js";
|
|
@@ -16,20 +17,26 @@ import { logRoles } from "./role-helpers.js";
|
|
|
16
17
|
import { clearScreen, setScreen } from "./screen.js";
|
|
17
18
|
import { captureAndSaveScreenshot } from "./screenshot.js";
|
|
18
19
|
import { type Container, TOPLEVELS, traverse } from "./traversal.js";
|
|
19
|
-
import type { QueryMap, RenderOptions, ScreenshotOptions, WindowSelector } from "./types.js";
|
|
20
20
|
import { resetClipboard } from "./user-event/index.js";
|
|
21
21
|
import { within } from "./within.js";
|
|
22
22
|
|
|
23
|
-
let lastRenderError: Error | null = null;
|
|
24
|
-
let errorHandlerInstalled = false;
|
|
25
|
-
|
|
26
23
|
type ActiveRender = {
|
|
27
24
|
root: ReconcilerRoot;
|
|
28
25
|
window: Gtk.Window | null;
|
|
29
26
|
};
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
type ResolvedContainer = {
|
|
29
|
+
containerInfo: Gtk.Widget | RootElement;
|
|
30
|
+
window: Gtk.Window | null;
|
|
31
|
+
};
|
|
32
32
|
|
|
33
|
+
type ReconcilerErrorState = {
|
|
34
|
+
lastError: Error | null;
|
|
35
|
+
isHandlerInstalled: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const reconcilerErrors: ReconcilerErrorState = { lastError: null, isHandlerInstalled: false };
|
|
39
|
+
const activeRenders: Set<ActiveRender> = new Set();
|
|
33
40
|
const HARNESS_WINDOW_WIDTH = 800;
|
|
34
41
|
const HARNESS_WINDOW_HEIGHT = 600;
|
|
35
42
|
|
|
@@ -43,15 +50,18 @@ const update = async (element: ReactNode, root: ReconcilerRoot): Promise<void> =
|
|
|
43
50
|
root.update(element);
|
|
44
51
|
});
|
|
45
52
|
|
|
46
|
-
if (
|
|
47
|
-
const captured =
|
|
48
|
-
|
|
53
|
+
if (reconcilerErrors.lastError) {
|
|
54
|
+
const captured = reconcilerErrors.lastError;
|
|
55
|
+
reconcilerErrors.lastError = null;
|
|
49
56
|
throw captured;
|
|
50
57
|
}
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
const disposeActiveRender = async (active: ActiveRender): Promise<void> => {
|
|
54
|
-
if (!activeRenders.delete(active))
|
|
61
|
+
if (!activeRenders.delete(active)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
await active.root.unmount(async (root) => {
|
|
56
66
|
await update(null, root);
|
|
57
67
|
active.window?.destroy();
|
|
@@ -59,42 +69,50 @@ const disposeActiveRender = async (active: ActiveRender): Promise<void> => {
|
|
|
59
69
|
};
|
|
60
70
|
|
|
61
71
|
const disposeAllActiveRenders = async (): Promise<void> => {
|
|
62
|
-
for (const active of
|
|
72
|
+
for (const active of activeRenders) {
|
|
63
73
|
await disposeActiveRender(active);
|
|
64
74
|
}
|
|
65
75
|
};
|
|
66
76
|
|
|
67
77
|
const handleError = (error: unknown): void => {
|
|
68
|
-
|
|
78
|
+
reconcilerErrors.lastError = error instanceof Error ? error : new Error(String(error));
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
const installErrorHandler = (): void => {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
82
|
+
if (reconcilerErrors.isHandlerInstalled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
window: Gtk.Window | null;
|
|
86
|
+
setReconcilerErrorHandler(handleError);
|
|
87
|
+
reconcilerErrors.isHandlerInstalled = true;
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
const resolveContainer = (container: RenderOptions["container"]): ResolvedContainer => {
|
|
83
91
|
if (isRootElement(container)) {
|
|
84
92
|
return { containerInfo: container, window: null };
|
|
85
93
|
}
|
|
94
|
+
|
|
86
95
|
if (container instanceof Gtk.Widget) {
|
|
87
96
|
return { containerInfo: container, window: null };
|
|
88
97
|
}
|
|
98
|
+
|
|
89
99
|
const window = new Gtk.Window({ defaultWidth: HARNESS_WINDOW_WIDTH, defaultHeight: HARNESS_WINDOW_HEIGHT });
|
|
90
100
|
window.setTitlebar(new Gtk.HeaderBar({ showTitleButtons: false }));
|
|
101
|
+
|
|
91
102
|
return { containerInfo: window, window };
|
|
92
103
|
};
|
|
93
104
|
|
|
94
105
|
const firstToplevelWidget = (baseElement: Container): Gtk.Widget => {
|
|
95
|
-
if (baseElement instanceof Gtk.Widget)
|
|
106
|
+
if (baseElement instanceof Gtk.Widget) {
|
|
107
|
+
return baseElement;
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
const [first] = traverse(baseElement);
|
|
97
|
-
|
|
111
|
+
|
|
112
|
+
if (first) {
|
|
113
|
+
return first;
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
throw new Error("render() produced no widgets: ensure the element renders visible content");
|
|
99
117
|
};
|
|
100
118
|
|
|
@@ -103,8 +121,14 @@ const resolveResultContainer = (
|
|
|
103
121
|
container: RenderOptions["container"],
|
|
104
122
|
baseElement: Container,
|
|
105
123
|
): Gtk.Widget => {
|
|
106
|
-
if (resolved.window)
|
|
107
|
-
|
|
124
|
+
if (resolved.window) {
|
|
125
|
+
return resolved.window;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (container instanceof Gtk.Widget) {
|
|
129
|
+
return container;
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
return firstToplevelWidget(baseElement);
|
|
109
133
|
};
|
|
110
134
|
|
|
@@ -119,10 +143,11 @@ const renderErrorHandlers = <Q extends QueryMap>(options: RenderOptions<Q> | und
|
|
|
119
143
|
},
|
|
120
144
|
});
|
|
121
145
|
|
|
122
|
-
const applyEnableAnimations = (
|
|
146
|
+
const applyEnableAnimations = (areAnimationsEnabled: boolean): void => {
|
|
123
147
|
const settings = Gtk.Settings.getDefault();
|
|
148
|
+
|
|
124
149
|
if (settings) {
|
|
125
|
-
settings.gtkEnableAnimations =
|
|
150
|
+
settings.gtkEnableAnimations = areAnimationsEnabled;
|
|
126
151
|
}
|
|
127
152
|
};
|
|
128
153
|
|
|
@@ -135,38 +160,36 @@ const applyEnableAnimations = (enabled: boolean): void => {
|
|
|
135
160
|
* @param options Optional container, wrapper, custom queries, and other render settings.
|
|
136
161
|
* @returns A render result with bound queries, debug helpers, and lifecycle controls.
|
|
137
162
|
*/
|
|
138
|
-
|
|
163
|
+
const render = async <Q extends QueryMap = Record<never, never>>(
|
|
139
164
|
element: ReactNode,
|
|
140
165
|
options?: RenderOptions<Q>,
|
|
141
166
|
): Promise<RenderResult<Q>> => {
|
|
142
167
|
installErrorHandler();
|
|
143
|
-
|
|
144
168
|
applyEnableAnimations(options?.animations === true);
|
|
145
|
-
|
|
146
169
|
const baseElement: Container = options?.baseElement ?? TOPLEVELS;
|
|
147
170
|
const Wrapper = options?.wrapper;
|
|
148
|
-
|
|
149
171
|
const resolved = resolveContainer(options?.container);
|
|
172
|
+
|
|
150
173
|
const root = createReconcilerRoot({
|
|
151
174
|
containerInfo: resolved.containerInfo,
|
|
152
175
|
...renderErrorHandlers(options),
|
|
153
176
|
});
|
|
177
|
+
|
|
154
178
|
const active: ActiveRender = { root, window: resolved.window };
|
|
155
179
|
activeRenders.add(active);
|
|
156
|
-
|
|
157
180
|
addToCleanupQueue(disposeAllActiveRenders);
|
|
158
181
|
addToCleanupQueue(clearScreen);
|
|
159
182
|
addToCleanupQueue(resetClipboard);
|
|
160
183
|
|
|
161
184
|
const wrap = (node: ReactNode): ReactNode => {
|
|
162
185
|
const wrapped = Wrapper ? <Wrapper>{node}</Wrapper> : node;
|
|
186
|
+
|
|
163
187
|
return options?.reactStrictMode ? <StrictMode>{wrapped}</StrictMode> : wrapped;
|
|
164
188
|
};
|
|
165
189
|
|
|
166
190
|
await update(wrap(element), root);
|
|
167
191
|
resolved.window?.present();
|
|
168
192
|
await flushLayout(resolved.window);
|
|
169
|
-
|
|
170
193
|
const container = resolveResultContainer(resolved, options?.container, baseElement);
|
|
171
194
|
|
|
172
195
|
const result: RenderResult<Q> = {
|
|
@@ -199,6 +222,8 @@ export const render = async <Q extends QueryMap = Record<never, never>>(
|
|
|
199
222
|
* Unmounts every active render and runs all registered cleanup callbacks,
|
|
200
223
|
* resetting the screen and clipboard. Called automatically after each test.
|
|
201
224
|
*/
|
|
202
|
-
|
|
225
|
+
const cleanup = async (): Promise<void> => {
|
|
203
226
|
await runCleanup();
|
|
204
227
|
};
|
|
228
|
+
|
|
229
|
+
export { render, cleanup };
|
package/src/role-helpers.ts
CHANGED
|
@@ -3,14 +3,15 @@ import { sortStringsBy } from "@gtkx/utils";
|
|
|
3
3
|
import { type Container, traverse } from "./traversal.js";
|
|
4
4
|
import { getWidgetAccessibleName } from "./widget-accessible-properties.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
|
|
7
|
+
|
|
8
|
+
function enumNamesByValue(enumObject: Record<string, string | number>): Map<number, string> {
|
|
9
|
+
return new Map<number, string>(
|
|
8
10
|
Object.entries(enumObject)
|
|
9
11
|
.filter((entry): entry is [string, number] => typeof entry[1] === "number")
|
|
10
12
|
.map(([name, value]) => [value, name]),
|
|
11
13
|
);
|
|
12
|
-
|
|
13
|
-
const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
|
|
14
|
+
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Converts an accessible role enum value into its lowercase name, falling back
|
|
@@ -18,22 +19,30 @@ const ROLE_NAMES_BY_VALUE = enumNamesByValue(Gtk.AccessibleRole);
|
|
|
18
19
|
*
|
|
19
20
|
* @param role The accessible role to format.
|
|
20
21
|
*/
|
|
21
|
-
|
|
22
|
+
const formatRole = (role: Gtk.AccessibleRole): string => {
|
|
22
23
|
const name = ROLE_NAMES_BY_VALUE.get(role);
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
if (!name) {
|
|
26
|
+
return String(role);
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
return name.toLowerCase();
|
|
25
30
|
};
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
|
|
28
33
|
const names = [...roles].map((role) => formatRole(role).toUpperCase());
|
|
34
|
+
|
|
29
35
|
if (names.length <= 1) {
|
|
30
36
|
return names.join("");
|
|
31
37
|
}
|
|
38
|
+
|
|
32
39
|
if (names.length === 2) {
|
|
33
|
-
return
|
|
40
|
+
return names.join(" or ");
|
|
34
41
|
}
|
|
42
|
+
|
|
35
43
|
const head = names.slice(0, -1);
|
|
36
|
-
const last = names
|
|
44
|
+
const last = names.at(-1) ?? "";
|
|
45
|
+
|
|
37
46
|
return `${head.join(", ")}, or ${last}`;
|
|
38
47
|
};
|
|
39
48
|
|
|
@@ -43,13 +52,13 @@ export const formatRoleList = (roles: Iterable<Gtk.AccessibleRole>): string => {
|
|
|
43
52
|
* @param container The scope to traverse.
|
|
44
53
|
* @returns A map from role name to the widgets that have that role.
|
|
45
54
|
*/
|
|
46
|
-
|
|
47
|
-
const roles
|
|
55
|
+
const getRoles = (container: Container): Map<string, Gtk.Widget[]> => {
|
|
56
|
+
const roles: Map<string, Gtk.Widget[]> = new Map();
|
|
48
57
|
|
|
49
58
|
for (const widget of traverse(container)) {
|
|
50
59
|
const roleName = formatRole(widget.getAccessibleRole());
|
|
51
|
-
|
|
52
60
|
const existing = roles.get(roleName);
|
|
61
|
+
|
|
53
62
|
if (existing) {
|
|
54
63
|
existing.push(widget);
|
|
55
64
|
} else {
|
|
@@ -64,6 +73,7 @@ const formatWidgetPreview = (widget: Gtk.Widget, name: string | null): string =>
|
|
|
64
73
|
const tagName = widget.constructor.name;
|
|
65
74
|
const roleAttr = formatRole(widget.getAccessibleRole());
|
|
66
75
|
const nameDisplay = name ? `Name "${name}"` : 'Name ""';
|
|
76
|
+
|
|
67
77
|
return `${nameDisplay}: <${tagName} role="${roleAttr}">${name ?? ""}</${tagName}>`;
|
|
68
78
|
};
|
|
69
79
|
|
|
@@ -73,7 +83,7 @@ const formatWidgetPreview = (widget: Gtk.Widget, name: string | null): string =>
|
|
|
73
83
|
*
|
|
74
84
|
* @param container The scope to inspect.
|
|
75
85
|
*/
|
|
76
|
-
|
|
86
|
+
const prettyRoles = (container: Container): string => {
|
|
77
87
|
const roles = getRoles(container);
|
|
78
88
|
|
|
79
89
|
if (roles.size === 0) {
|
|
@@ -81,14 +91,15 @@ export const prettyRoles = (container: Container): string => {
|
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
const lines: string[] = [];
|
|
84
|
-
|
|
85
|
-
const sortedRoles = sortStringsBy([...roles.entries()], ([roleName]) => roleName);
|
|
94
|
+
const sortedRoles = sortStringsBy([...roles], ([roleName]) => roleName);
|
|
86
95
|
|
|
87
96
|
for (const [roleName, widgets] of sortedRoles) {
|
|
88
97
|
lines.push(`${roleName}:`);
|
|
98
|
+
|
|
89
99
|
for (const widget of widgets) {
|
|
90
100
|
lines.push(` ${formatWidgetPreview(widget, getWidgetAccessibleName(widget))}`);
|
|
91
101
|
}
|
|
102
|
+
|
|
92
103
|
lines.push("");
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -101,6 +112,8 @@ export const prettyRoles = (container: Container): string => {
|
|
|
101
112
|
*
|
|
102
113
|
* @param container The scope to inspect.
|
|
103
114
|
*/
|
|
104
|
-
|
|
115
|
+
const logRoles = (container: Container): void => {
|
|
105
116
|
console.log(prettyRoles(container));
|
|
106
117
|
};
|
|
118
|
+
|
|
119
|
+
export { formatRole, formatRoleList, getRoles, prettyRoles, logRoles };
|
package/src/screen.ts
CHANGED
|
@@ -2,31 +2,36 @@ import type { RenderResult, Screen } from "./bound-queries.js";
|
|
|
2
2
|
|
|
3
3
|
const NO_RENDER_MESSAGE = "No render has been performed: call render() before using screen queries";
|
|
4
4
|
|
|
5
|
-
const throwNoRender = (): never => {
|
|
6
|
-
throw new Error(NO_RENDER_MESSAGE);
|
|
7
|
-
};
|
|
8
|
-
|
|
9
5
|
const defaultScreen: RenderResult = new Proxy({} as RenderResult, {
|
|
10
6
|
get: (_target, property) => {
|
|
11
|
-
if (property === "baseElement" || property === "container")
|
|
7
|
+
if (property === "baseElement" || property === "container") {
|
|
8
|
+
return throwNoRender();
|
|
9
|
+
}
|
|
10
|
+
|
|
12
11
|
return throwNoRender;
|
|
13
12
|
},
|
|
14
13
|
});
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
const activeScreen: { current: RenderResult } = { current: defaultScreen };
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Queries and debug utilities bound to the most recent render, scoped to the
|
|
20
19
|
* current toplevel windows. Accessing it before any render throws.
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
get: (_target, property) => Reflect.get(activeScreen, property),
|
|
21
|
+
const screen: Screen = new Proxy({} as Screen, {
|
|
22
|
+
get: (_target, property): unknown => Reflect.get(activeScreen.current, property),
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const throwNoRender = (): never => {
|
|
26
|
+
throw new Error(NO_RENDER_MESSAGE);
|
|
28
27
|
};
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
activeScreen =
|
|
29
|
+
const setScreen = (result: RenderResult): void => {
|
|
30
|
+
activeScreen.current = result;
|
|
32
31
|
};
|
|
32
|
+
|
|
33
|
+
const clearScreen = (): void => {
|
|
34
|
+
activeScreen.current = defaultScreen;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { screen, setScreen, clearScreen };
|
package/src/screenshot.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import * as Gsk from "@gtkx/gi/gsk";
|
|
5
2
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
6
|
import type { ScreenshotOptions, ScreenshotResult, WindowSelector } from "./types.js";
|
|
7
7
|
import { waitFor } from "./wait-for.js";
|
|
8
8
|
|
|
9
|
+
const DEFAULT_SCREENSHOT_INTERVAL = 10;
|
|
10
|
+
|
|
9
11
|
const bytesToBase64 = (bytes: number[]): string => {
|
|
10
12
|
return Buffer.from(bytes).toString("base64");
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
const DEFAULT_SCREENSHOT_INTERVAL = 10;
|
|
14
|
-
|
|
15
15
|
const describeWidgetState = (widget: Gtk.Widget): string =>
|
|
16
|
-
`realized=${widget.getRealized()} mapped=${widget.getMapped()}
|
|
16
|
+
`realized=${String(widget.getRealized())} mapped=${String(widget.getMapped())} ` +
|
|
17
|
+
`visible=${String(widget.getVisible())}`;
|
|
17
18
|
|
|
18
19
|
const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult => {
|
|
19
20
|
const paintable = new Gtk.WidgetPaintable({ widget });
|
|
@@ -34,7 +35,6 @@ const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult =>
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const display = widget.getDisplay();
|
|
37
|
-
|
|
38
38
|
const renderer = new Gsk.CairoRenderer();
|
|
39
39
|
renderer.realizeForDisplay(display);
|
|
40
40
|
|
|
@@ -65,11 +65,11 @@ const captureSnapshot = (widget: Gtk.Widget, scale: number): ScreenshotResult =>
|
|
|
65
65
|
* @param options Optional scale, timeout, and retry interval.
|
|
66
66
|
* @returns The base64-encoded PNG data along with its mime type and dimensions.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions): Promise<ScreenshotResult> => {
|
|
69
69
|
const scale = options?.scale ?? 1;
|
|
70
70
|
|
|
71
71
|
if (!Number.isFinite(scale) || scale <= 0) {
|
|
72
|
-
throw new Error(`Screenshot scale must be a positive number, got ${scale}`);
|
|
72
|
+
throw new Error(`Screenshot scale must be a positive number, got ${String(scale)}`);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return waitFor(() => captureSnapshot(widget, scale), {
|
|
@@ -78,6 +78,47 @@ export const screenshot = async (widget: Gtk.Widget, options?: ScreenshotOptions
|
|
|
78
78
|
});
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
const firstToplevelWindow = (windows: Gtk.Widget[]): Gtk.Window => {
|
|
82
|
+
const [first] = windows;
|
|
83
|
+
|
|
84
|
+
if (!(first instanceof Gtk.Window)) {
|
|
85
|
+
throw new TypeError("First toplevel is not a Window");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return first;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const windowAtIndex = (windows: Gtk.Widget[], index: number): Gtk.Window => {
|
|
92
|
+
const indexed = windows[index];
|
|
93
|
+
|
|
94
|
+
if (!(indexed instanceof Gtk.Window)) {
|
|
95
|
+
throw new TypeError(`Window at index ${String(index)} not found`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return indexed;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const isWindow = (widget: Gtk.Widget): widget is Gtk.Window => widget instanceof Gtk.Window;
|
|
102
|
+
|
|
103
|
+
const hasMatchingTitle = (window: Gtk.Window, selector: string | RegExp): boolean => {
|
|
104
|
+
const title = window.getTitle() ?? "";
|
|
105
|
+
|
|
106
|
+
return selector instanceof RegExp ? selector.test(title) : title.includes(selector);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const describeTitleSelector = (selector: string | RegExp): string =>
|
|
110
|
+
selector instanceof RegExp ? selector.toString() : `"${selector}"`;
|
|
111
|
+
|
|
112
|
+
const windowByTitle = (windows: Gtk.Widget[], selector: string | RegExp): Gtk.Window => {
|
|
113
|
+
const found = windows.filter(isWindow).find((window) => hasMatchingTitle(window, selector));
|
|
114
|
+
|
|
115
|
+
if (!found) {
|
|
116
|
+
throw new Error(`No window found with title matching ${describeTitleSelector(selector)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return found;
|
|
120
|
+
};
|
|
121
|
+
|
|
81
122
|
const resolveWindow = (selector?: WindowSelector): Gtk.Window => {
|
|
82
123
|
const windows = Gtk.Window.listToplevels();
|
|
83
124
|
|
|
@@ -86,42 +127,21 @@ const resolveWindow = (selector?: WindowSelector): Gtk.Window => {
|
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
if (selector === undefined) {
|
|
89
|
-
|
|
90
|
-
if (!(first instanceof Gtk.Window)) {
|
|
91
|
-
throw new TypeError("First toplevel is not a Window");
|
|
92
|
-
}
|
|
93
|
-
return first;
|
|
130
|
+
return firstToplevelWindow(windows);
|
|
94
131
|
}
|
|
95
132
|
|
|
96
133
|
if (typeof selector === "number") {
|
|
97
|
-
|
|
98
|
-
if (!(indexed instanceof Gtk.Window)) {
|
|
99
|
-
throw new TypeError(`Window at index ${selector} not found`);
|
|
100
|
-
}
|
|
101
|
-
return indexed;
|
|
134
|
+
return windowAtIndex(windows, selector);
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
|
|
105
|
-
const found = windows.find((w): w is Gtk.Window => {
|
|
106
|
-
if (!(w instanceof Gtk.Window)) return false;
|
|
107
|
-
const title = w.getTitle() ?? "";
|
|
108
|
-
return isRegex ? selector.test(title) : title.includes(selector);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (!found) {
|
|
112
|
-
const pattern = isRegex ? selector.toString() : `"${selector}"`;
|
|
113
|
-
throw new Error(`No window found with title matching ${pattern}`);
|
|
114
|
-
}
|
|
115
|
-
return found;
|
|
137
|
+
return windowByTitle(windows, selector);
|
|
116
138
|
};
|
|
117
139
|
|
|
118
140
|
const saveScreenshotToTempFile = (result: ScreenshotResult): string => {
|
|
119
|
-
const dir = join(tmpdir(), "gtkx-screenshots");
|
|
120
|
-
|
|
121
|
-
mkdirSync(dir, { recursive: true });
|
|
122
|
-
}
|
|
123
|
-
const filepath = join(dir, `${Date.now()}-screenshot.png`);
|
|
141
|
+
const dir = mkdtempSync(join(tmpdir(), "gtkx-screenshots-"));
|
|
142
|
+
const filepath = join(dir, "screenshot.png");
|
|
124
143
|
writeFileSync(filepath, Buffer.from(result.data, "base64"));
|
|
144
|
+
|
|
125
145
|
return filepath;
|
|
126
146
|
};
|
|
127
147
|
|
|
@@ -130,7 +150,7 @@ const saveScreenshotToTempFile = (result: ScreenshotResult): string => {
|
|
|
130
150
|
*
|
|
131
151
|
* @param filepath Absolute path of the saved screenshot file.
|
|
132
152
|
*/
|
|
133
|
-
|
|
153
|
+
const logScreenshotPath = (filepath: string): void => {
|
|
134
154
|
console.log(`Screenshot saved: file://${filepath}`);
|
|
135
155
|
};
|
|
136
156
|
|
|
@@ -143,12 +163,15 @@ export const logScreenshotPath = (filepath: string): void => {
|
|
|
143
163
|
* @param options Optional scale, timeout, and retry interval.
|
|
144
164
|
* @returns The base64-encoded PNG data along with its mime type and dimensions.
|
|
145
165
|
*/
|
|
146
|
-
|
|
166
|
+
const captureAndSaveScreenshot = async (
|
|
147
167
|
selector?: WindowSelector,
|
|
148
168
|
options?: ScreenshotOptions,
|
|
149
169
|
): Promise<ScreenshotResult> => {
|
|
150
170
|
const target = resolveWindow(selector);
|
|
151
171
|
const result = await screenshot(target, options);
|
|
152
172
|
logScreenshotPath(saveScreenshotToTempFile(result));
|
|
173
|
+
|
|
153
174
|
return result;
|
|
154
175
|
};
|
|
176
|
+
|
|
177
|
+
export { screenshot, logScreenshotPath, captureAndSaveScreenshot };
|