@gtkx/testing 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bind-queries.d.ts +13 -0
- package/dist/bind-queries.js +28 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +58 -0
- package/dist/error-builder.d.ts +27 -0
- package/dist/error-builder.js +80 -0
- package/dist/fire-event.js +2 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/pretty-widget.d.ts +36 -0
- package/dist/pretty-widget.js +150 -0
- package/dist/queries.d.ts +78 -3
- package/dist/queries.js +156 -177
- package/dist/render.js +4 -38
- package/dist/role-helpers.d.ts +66 -0
- package/dist/role-helpers.js +108 -0
- package/dist/screen.d.ts +39 -18
- package/dist/screen.js +78 -19
- package/dist/screenshot.d.ts +26 -0
- package/dist/screenshot.js +86 -0
- package/dist/traversal.d.ts +3 -2
- package/dist/traversal.js +2 -2
- package/dist/types.d.ts +37 -8
- package/dist/user-event.d.ts +39 -6
- package/dist/user-event.js +160 -13
- package/dist/widget-text.d.ts +29 -0
- package/dist/widget-text.js +146 -0
- package/dist/widget.d.ts +0 -1
- package/dist/widget.js +0 -13
- package/dist/within.js +2 -11
- package/package.json +5 -5
package/dist/screen.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
-
import
|
|
2
|
+
import { type ScreenshotOptions } from "./screenshot.js";
|
|
3
|
+
import type { ScreenshotResult } from "./types.js";
|
|
3
4
|
/** @internal */
|
|
4
5
|
export declare const setScreenRoot: (root: Gtk.Application | null) => void;
|
|
5
6
|
/**
|
|
@@ -23,22 +24,42 @@ export declare const setScreenRoot: (root: Gtk.Application | null) => void;
|
|
|
23
24
|
* @see {@link within} for scoped queries
|
|
24
25
|
*/
|
|
25
26
|
export declare const screen: {
|
|
26
|
-
/**
|
|
27
|
-
findByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
28
|
-
/** Find single element by label/text content */
|
|
29
|
-
findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
30
|
-
/** Find single element by visible text */
|
|
31
|
-
findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
32
|
-
/** Find single element by test ID (widget name) */
|
|
33
|
-
findByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
34
|
-
/** Find all elements by accessible role */
|
|
35
|
-
findAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
36
|
-
/** Find all elements by label/text content */
|
|
37
|
-
findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
38
|
-
/** Find all elements by visible text */
|
|
39
|
-
findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
40
|
-
/** Find all elements by test ID (widget name) */
|
|
41
|
-
findAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
42
|
-
/** Print debug info to console */
|
|
27
|
+
/** Print the widget tree to console for debugging */
|
|
43
28
|
debug: () => void;
|
|
29
|
+
/** Log all accessible roles to console for debugging */
|
|
30
|
+
logRoles: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Capture a screenshot of the application window, saving it to a temporary file and logging the file path.
|
|
33
|
+
*
|
|
34
|
+
* @param selector - Window selector: index (number), title substring (string), or title pattern (RegExp).
|
|
35
|
+
* If omitted, captures the first window.
|
|
36
|
+
* @param options - Optional timeout and interval configuration for waiting on widget rendering.
|
|
37
|
+
* @returns Screenshot result containing base64-encoded PNG data
|
|
38
|
+
* @throws Error if no windows are available or no matching window is found
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* await screen.screenshot(); // First window
|
|
43
|
+
* await screen.screenshot(0); // Window at index 0
|
|
44
|
+
* await screen.screenshot("Settings"); // Window with title containing "Settings"
|
|
45
|
+
* await screen.screenshot(/^My App/); // Window with title matching regex
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
screenshot: (selector?: number | string | RegExp, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
49
|
+
queryByRole: (role: Gtk.AccessibleRole, options?: import("./types.js").ByRoleOptions) => Gtk.Widget | null;
|
|
50
|
+
queryByLabelText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget | null;
|
|
51
|
+
queryByText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget | null;
|
|
52
|
+
queryByTestId: (testId: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget | null;
|
|
53
|
+
queryAllByRole: (role: Gtk.AccessibleRole, options?: import("./types.js").ByRoleOptions) => Gtk.Widget[];
|
|
54
|
+
queryAllByLabelText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget[];
|
|
55
|
+
queryAllByText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget[];
|
|
56
|
+
queryAllByTestId: (testId: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Gtk.Widget[];
|
|
57
|
+
findByRole: (role: Gtk.AccessibleRole, options?: import("./types.js").ByRoleOptions) => Promise<Gtk.Widget>;
|
|
58
|
+
findByLabelText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget>;
|
|
59
|
+
findByText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget>;
|
|
60
|
+
findByTestId: (testId: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget>;
|
|
61
|
+
findAllByRole: (role: Gtk.AccessibleRole, options?: import("./types.js").ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
62
|
+
findAllByLabelText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
63
|
+
findAllByText: (text: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
64
|
+
findAllByTestId: (testId: import("./types.js").TextMatch, options?: import("./types.js").TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
44
65
|
};
|
package/dist/screen.js
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { bindQueries } from "./bind-queries.js";
|
|
5
|
+
import { prettyWidget } from "./pretty-widget.js";
|
|
6
|
+
import { logRoles } from "./role-helpers.js";
|
|
7
|
+
import { screenshot as screenshotWidget } from "./screenshot.js";
|
|
8
|
+
const getScreenshotDir = () => {
|
|
9
|
+
const dir = join(tmpdir(), "gtkx-screenshots");
|
|
10
|
+
if (!existsSync(dir)) {
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return dir;
|
|
14
|
+
};
|
|
15
|
+
const saveAndLogScreenshot = (result) => {
|
|
16
|
+
const dir = getScreenshotDir();
|
|
17
|
+
const filename = `${Date.now()}-screenshot.png`;
|
|
18
|
+
const filepath = join(dir, filename);
|
|
19
|
+
const buffer = Buffer.from(result.data, "base64");
|
|
20
|
+
writeFileSync(filepath, buffer);
|
|
21
|
+
console.log(`Screenshot saved: file://${filepath}`);
|
|
22
|
+
};
|
|
2
23
|
let currentRoot = null;
|
|
3
24
|
/** @internal */
|
|
4
25
|
export const setScreenRoot = (root) => {
|
|
@@ -10,6 +31,7 @@ const getRoot = () => {
|
|
|
10
31
|
}
|
|
11
32
|
return currentRoot;
|
|
12
33
|
};
|
|
34
|
+
const boundQueries = bindQueries(getRoot);
|
|
13
35
|
/**
|
|
14
36
|
* Global query object for accessing rendered components.
|
|
15
37
|
*
|
|
@@ -31,24 +53,61 @@ const getRoot = () => {
|
|
|
31
53
|
* @see {@link within} for scoped queries
|
|
32
54
|
*/
|
|
33
55
|
export const screen = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** Find single element by label/text content */
|
|
37
|
-
findByLabelText: (text, options) => queries.findByLabelText(getRoot(), text, options),
|
|
38
|
-
/** Find single element by visible text */
|
|
39
|
-
findByText: (text, options) => queries.findByText(getRoot(), text, options),
|
|
40
|
-
/** Find single element by test ID (widget name) */
|
|
41
|
-
findByTestId: (testId, options) => queries.findByTestId(getRoot(), testId, options),
|
|
42
|
-
/** Find all elements by accessible role */
|
|
43
|
-
findAllByRole: (role, options) => queries.findAllByRole(getRoot(), role, options),
|
|
44
|
-
/** Find all elements by label/text content */
|
|
45
|
-
findAllByLabelText: (text, options) => queries.findAllByLabelText(getRoot(), text, options),
|
|
46
|
-
/** Find all elements by visible text */
|
|
47
|
-
findAllByText: (text, options) => queries.findAllByText(getRoot(), text, options),
|
|
48
|
-
/** Find all elements by test ID (widget name) */
|
|
49
|
-
findAllByTestId: (testId, options) => queries.findAllByTestId(getRoot(), testId, options),
|
|
50
|
-
/** Print debug info to console */
|
|
56
|
+
...boundQueries,
|
|
57
|
+
/** Print the widget tree to console for debugging */
|
|
51
58
|
debug: () => {
|
|
52
|
-
console.log(
|
|
59
|
+
console.log(prettyWidget(getRoot()));
|
|
60
|
+
},
|
|
61
|
+
/** Log all accessible roles to console for debugging */
|
|
62
|
+
logRoles: () => {
|
|
63
|
+
logRoles(getRoot());
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Capture a screenshot of the application window, saving it to a temporary file and logging the file path.
|
|
67
|
+
*
|
|
68
|
+
* @param selector - Window selector: index (number), title substring (string), or title pattern (RegExp).
|
|
69
|
+
* If omitted, captures the first window.
|
|
70
|
+
* @param options - Optional timeout and interval configuration for waiting on widget rendering.
|
|
71
|
+
* @returns Screenshot result containing base64-encoded PNG data
|
|
72
|
+
* @throws Error if no windows are available or no matching window is found
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* await screen.screenshot(); // First window
|
|
77
|
+
* await screen.screenshot(0); // Window at index 0
|
|
78
|
+
* await screen.screenshot("Settings"); // Window with title containing "Settings"
|
|
79
|
+
* await screen.screenshot(/^My App/); // Window with title matching regex
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
screenshot: async (selector, options) => {
|
|
83
|
+
const root = getRoot();
|
|
84
|
+
const windows = root.getWindows();
|
|
85
|
+
if (windows.length === 0) {
|
|
86
|
+
throw new Error("No windows available for screenshot");
|
|
87
|
+
}
|
|
88
|
+
let targetWindow;
|
|
89
|
+
if (selector === undefined) {
|
|
90
|
+
targetWindow = windows[0];
|
|
91
|
+
}
|
|
92
|
+
else if (typeof selector === "number") {
|
|
93
|
+
targetWindow = windows[selector];
|
|
94
|
+
if (!targetWindow) {
|
|
95
|
+
throw new Error(`Window at index ${selector} not found`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const isRegex = selector instanceof RegExp;
|
|
100
|
+
targetWindow = windows.find((w) => {
|
|
101
|
+
const title = w.getTitle() ?? "";
|
|
102
|
+
return isRegex ? selector.test(title) : title.includes(selector);
|
|
103
|
+
});
|
|
104
|
+
if (!targetWindow) {
|
|
105
|
+
const pattern = isRegex ? selector.toString() : `"${selector}"`;
|
|
106
|
+
throw new Error(`No window found with title matching ${pattern}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const result = await screenshotWidget(targetWindow, options);
|
|
110
|
+
saveAndLogScreenshot(result);
|
|
111
|
+
return result;
|
|
53
112
|
},
|
|
54
113
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
import type { ScreenshotResult, WaitForOptions } from "./types.js";
|
|
3
|
+
export type ScreenshotOptions = Pick<WaitForOptions, "timeout" | "interval">;
|
|
4
|
+
/**
|
|
5
|
+
* Captures a screenshot of a GTK widget as a PNG image.
|
|
6
|
+
*
|
|
7
|
+
* This function will retry multiple times if the widget hasn't finished
|
|
8
|
+
* rendering, waiting for GTK to complete its paint cycle.
|
|
9
|
+
*
|
|
10
|
+
* @param widget - The widget to capture (typically a Window)
|
|
11
|
+
* @param options - Optional timeout and interval configuration
|
|
12
|
+
* @returns Screenshot result containing base64-encoded PNG data and dimensions
|
|
13
|
+
* @throws Error if widget has no size, is not realized, or rendering fails after timeout
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { render, screenshot } from "@gtkx/testing";
|
|
18
|
+
* import * as Gtk from "@gtkx/ffi/gtk";
|
|
19
|
+
*
|
|
20
|
+
* const { container } = await render(<MyApp />);
|
|
21
|
+
* const window = container.getWindows()[0];
|
|
22
|
+
* const result = await screenshot(window);
|
|
23
|
+
* console.log(result.mimeType); // "image/png"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare const screenshot: (widget: Gtk.Widget, options?: ScreenshotOptions) => Promise<ScreenshotResult>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createRef } from "@gtkx/ffi";
|
|
2
|
+
import * as Gsk from "@gtkx/ffi/gsk";
|
|
3
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
|
+
import { tick } from "./timing.js";
|
|
5
|
+
import { waitFor } from "./wait-for.js";
|
|
6
|
+
const bytesToBase64 = (bytes) => {
|
|
7
|
+
return Buffer.from(bytes).toString("base64");
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_SCREENSHOT_TIMEOUT = 100;
|
|
10
|
+
const DEFAULT_SCREENSHOT_INTERVAL = 10;
|
|
11
|
+
const captureSnapshot = (widget) => {
|
|
12
|
+
const paintable = new Gtk.WidgetPaintable(widget);
|
|
13
|
+
const width = paintable.getIntrinsicWidth();
|
|
14
|
+
const height = paintable.getIntrinsicHeight();
|
|
15
|
+
if (width <= 0 || height <= 0) {
|
|
16
|
+
throw new Error("Widget has no size - ensure it is realized and visible");
|
|
17
|
+
}
|
|
18
|
+
const snapshot = new Gtk.Snapshot();
|
|
19
|
+
paintable.snapshot(snapshot, width, height);
|
|
20
|
+
const renderNode = snapshot.toNode();
|
|
21
|
+
if (!renderNode) {
|
|
22
|
+
throw new Error("Widget produced no render content");
|
|
23
|
+
}
|
|
24
|
+
const display = widget.getDisplay();
|
|
25
|
+
if (!display) {
|
|
26
|
+
throw new Error("Widget has no display - ensure it is realized");
|
|
27
|
+
}
|
|
28
|
+
const renderer = new Gsk.CairoRenderer();
|
|
29
|
+
renderer.realizeForDisplay(display);
|
|
30
|
+
try {
|
|
31
|
+
const texture = renderer.renderTexture(renderNode);
|
|
32
|
+
const pngBytes = texture.saveToPngBytes();
|
|
33
|
+
const sizeRef = createRef(0);
|
|
34
|
+
const data = pngBytes.getData(sizeRef);
|
|
35
|
+
if (!data) {
|
|
36
|
+
throw new Error("Failed to serialize screenshot to PNG");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
data: bytesToBase64(data),
|
|
40
|
+
mimeType: "image/png",
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
renderer.unrealize();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Captures a screenshot of a GTK widget as a PNG image.
|
|
51
|
+
*
|
|
52
|
+
* This function will retry multiple times if the widget hasn't finished
|
|
53
|
+
* rendering, waiting for GTK to complete its paint cycle.
|
|
54
|
+
*
|
|
55
|
+
* @param widget - The widget to capture (typically a Window)
|
|
56
|
+
* @param options - Optional timeout and interval configuration
|
|
57
|
+
* @returns Screenshot result containing base64-encoded PNG data and dimensions
|
|
58
|
+
* @throws Error if widget has no size, is not realized, or rendering fails after timeout
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { render, screenshot } from "@gtkx/testing";
|
|
63
|
+
* import * as Gtk from "@gtkx/ffi/gtk";
|
|
64
|
+
*
|
|
65
|
+
* const { container } = await render(<MyApp />);
|
|
66
|
+
* const window = container.getWindows()[0];
|
|
67
|
+
* const result = await screenshot(window);
|
|
68
|
+
* console.log(result.mimeType); // "image/png"
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export const screenshot = async (widget, options) => {
|
|
72
|
+
await tick();
|
|
73
|
+
return waitFor(() => captureSnapshot(widget), {
|
|
74
|
+
timeout: options?.timeout ?? DEFAULT_SCREENSHOT_TIMEOUT,
|
|
75
|
+
interval: options?.interval ?? DEFAULT_SCREENSHOT_INTERVAL,
|
|
76
|
+
onTimeout: (error) => {
|
|
77
|
+
const paintable = new Gtk.WidgetPaintable(widget);
|
|
78
|
+
const width = paintable.getIntrinsicWidth();
|
|
79
|
+
const height = paintable.getIntrinsicHeight();
|
|
80
|
+
if (width <= 0 || height <= 0) {
|
|
81
|
+
return new Error("Widget has no size - ensure it is realized and visible");
|
|
82
|
+
}
|
|
83
|
+
return new Error(`Widget produced no render content after waiting for paint cycle. ${error.message}`);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
};
|
package/dist/traversal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
-
type Container = Gtk.Application | Gtk.Widget;
|
|
2
|
+
export type Container = Gtk.Application | Gtk.Widget;
|
|
3
|
+
export declare const isApplication: (container: Container) => container is Gtk.Application;
|
|
4
|
+
export declare const traverse: (container: Container) => Generator<Gtk.Widget>;
|
|
3
5
|
export declare const findAll: (container: Container, predicate: (node: Gtk.Widget) => boolean) => Gtk.Widget[];
|
|
4
|
-
export {};
|
package/dist/traversal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const isApplication = (container) => "getWindows" in container && typeof container.getWindows === "function";
|
|
1
|
+
export const isApplication = (container) => "getWindows" in container && typeof container.getWindows === "function";
|
|
2
2
|
const traverseWidgetTree = function* (root) {
|
|
3
3
|
yield root;
|
|
4
4
|
let child = root.getFirstChild();
|
|
@@ -13,7 +13,7 @@ const traverseApplication = function* (app) {
|
|
|
13
13
|
yield* traverseWidgetTree(window);
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
-
const traverse = function* (container) {
|
|
16
|
+
export const traverse = function* (container) {
|
|
17
17
|
if (isApplication(container)) {
|
|
18
18
|
yield* traverseApplication(container);
|
|
19
19
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -90,21 +90,37 @@ export type RenderOptions = {
|
|
|
90
90
|
* @see {@link within} for scoped queries
|
|
91
91
|
*/
|
|
92
92
|
export type BoundQueries = {
|
|
93
|
-
/**
|
|
93
|
+
/** Query single element by accessible role (returns null if not found) */
|
|
94
|
+
queryByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
|
|
95
|
+
/** Query single element by label/text content (returns null if not found) */
|
|
96
|
+
queryByLabelText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
97
|
+
/** Query single element by visible text (returns null if not found) */
|
|
98
|
+
queryByText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
99
|
+
/** Query single element by test ID (returns null if not found) */
|
|
100
|
+
queryByTestId: (testId: TextMatch, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
101
|
+
/** Query all elements by accessible role (returns empty array if none found) */
|
|
102
|
+
queryAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
103
|
+
/** Query all elements by label/text content (returns empty array if none found) */
|
|
104
|
+
queryAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
|
|
105
|
+
/** Query all elements by visible text (returns empty array if none found) */
|
|
106
|
+
queryAllByText: (text: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
|
|
107
|
+
/** Query all elements by test ID (returns empty array if none found) */
|
|
108
|
+
queryAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Gtk.Widget[];
|
|
109
|
+
/** Find single element by accessible role (waits and throws if not found) */
|
|
94
110
|
findByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
95
|
-
/** Find single element by label/text content */
|
|
111
|
+
/** Find single element by label/text content (waits and throws if not found) */
|
|
96
112
|
findByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
97
|
-
/** Find single element by visible text */
|
|
113
|
+
/** Find single element by visible text (waits and throws if not found) */
|
|
98
114
|
findByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
99
|
-
/** Find single element by test ID (
|
|
115
|
+
/** Find single element by test ID (waits and throws if not found) */
|
|
100
116
|
findByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
101
|
-
/** Find all elements by accessible role */
|
|
117
|
+
/** Find all elements by accessible role (waits and throws if none found) */
|
|
102
118
|
findAllByRole: (role: Gtk.AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
103
|
-
/** Find all elements by label/text content */
|
|
119
|
+
/** Find all elements by label/text content (waits and throws if none found) */
|
|
104
120
|
findAllByLabelText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
105
|
-
/** Find all elements by visible text */
|
|
121
|
+
/** Find all elements by visible text (waits and throws if none found) */
|
|
106
122
|
findAllByText: (text: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
107
|
-
/** Find all elements by test ID (
|
|
123
|
+
/** Find all elements by test ID (waits and throws if none found) */
|
|
108
124
|
findAllByTestId: (testId: TextMatch, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
109
125
|
};
|
|
110
126
|
/**
|
|
@@ -122,3 +138,16 @@ export type RenderResult = BoundQueries & {
|
|
|
122
138
|
/** Print the widget tree to console for debugging */
|
|
123
139
|
debug: () => void;
|
|
124
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Result returned by {@link screenshot} and screen.screenshot.
|
|
143
|
+
*/
|
|
144
|
+
export type ScreenshotResult = {
|
|
145
|
+
/** Base64-encoded PNG image data */
|
|
146
|
+
data: string;
|
|
147
|
+
/** MIME type of the image (always "image/png") */
|
|
148
|
+
mimeType: string;
|
|
149
|
+
/** Width of the captured image in pixels */
|
|
150
|
+
width: number;
|
|
151
|
+
/** Height of the captured image in pixels */
|
|
152
|
+
height: number;
|
|
153
|
+
};
|
package/dist/user-event.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type TabOptions = {
|
|
|
6
6
|
/** Navigate backwards (Shift+Tab) instead of forwards */
|
|
7
7
|
shift?: boolean;
|
|
8
8
|
};
|
|
9
|
+
export type PointerInput = "click" | "down" | "up" | "[MouseLeft]" | "[MouseLeft>]" | "[/MouseLeft]";
|
|
9
10
|
/**
|
|
10
11
|
* User interaction utilities for testing.
|
|
11
12
|
*
|
|
@@ -47,12 +48,6 @@ export declare const userEvent: {
|
|
|
47
48
|
* Emits three consecutive clicked signals. Useful for text selection.
|
|
48
49
|
*/
|
|
49
50
|
tripleClick: (element: Gtk.Widget) => Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
* Activates a widget.
|
|
52
|
-
*
|
|
53
|
-
* Calls the widget's activate method.
|
|
54
|
-
*/
|
|
55
|
-
activate: (element: Gtk.Widget) => Promise<void>;
|
|
56
51
|
/**
|
|
57
52
|
* Simulates Tab key navigation.
|
|
58
53
|
*
|
|
@@ -94,4 +89,42 @@ export declare const userEvent: {
|
|
|
94
89
|
* @param values - Index or array of indices to deselect
|
|
95
90
|
*/
|
|
96
91
|
deselectOptions: (element: Gtk.Widget, values: number | number[]) => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Simulates mouse entering a widget (hover).
|
|
94
|
+
*
|
|
95
|
+
* Triggers the "enter" signal on the widget's EventControllerMotion.
|
|
96
|
+
*/
|
|
97
|
+
hover: (element: Gtk.Widget) => Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Simulates mouse leaving a widget (unhover).
|
|
100
|
+
*
|
|
101
|
+
* Triggers the "leave" signal on the widget's EventControllerMotion.
|
|
102
|
+
*/
|
|
103
|
+
unhover: (element: Gtk.Widget) => Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Simulates keyboard input.
|
|
106
|
+
*
|
|
107
|
+
* Supports special keys in braces: `{Enter}`, `{Tab}`, `{Escape}`, etc.
|
|
108
|
+
* Use `{Key>}` to hold a key down, `{/Key}` to release.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* await userEvent.keyboard(element, "hello");
|
|
113
|
+
* await userEvent.keyboard(element, "{Enter}");
|
|
114
|
+
* await userEvent.keyboard(element, "{Shift>}A{/Shift}");
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
keyboard: (element: Gtk.Widget, input: string) => Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Simulates pointer (mouse) input.
|
|
120
|
+
*
|
|
121
|
+
* Supports: `"click"`, `"[MouseLeft]"`, `"down"`, `"up"`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx
|
|
125
|
+
* await userEvent.pointer(element, "click");
|
|
126
|
+
* await userEvent.pointer(element, "[MouseLeft]");
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
pointer: (element: Gtk.Widget, input: PointerInput) => Promise<void>;
|
|
97
130
|
};
|
package/dist/user-event.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getNativeObject } from "@gtkx/ffi";
|
|
2
|
+
import * as Gdk from "@gtkx/ffi/gdk";
|
|
2
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
|
+
import { call } from "@gtkx/native";
|
|
3
5
|
import { fireEvent } from "./fire-event.js";
|
|
4
6
|
import { tick } from "./timing.js";
|
|
5
7
|
import { isEditable } from "./widget.js";
|
|
@@ -42,15 +44,11 @@ const tripleClick = async (element) => {
|
|
|
42
44
|
await fireEvent(element, "clicked");
|
|
43
45
|
await fireEvent(element, "clicked");
|
|
44
46
|
};
|
|
45
|
-
const activate = async (element) => {
|
|
46
|
-
element.activate();
|
|
47
|
-
await tick();
|
|
48
|
-
};
|
|
49
47
|
const tab = async (element, options) => {
|
|
50
48
|
const direction = options?.shift ? Gtk.DirectionType.TAB_BACKWARD : Gtk.DirectionType.TAB_FORWARD;
|
|
51
49
|
const root = element.getRoot();
|
|
52
50
|
if (root) {
|
|
53
|
-
getNativeObject(root.
|
|
51
|
+
getNativeObject(root.handle).childFocus(direction);
|
|
54
52
|
}
|
|
55
53
|
await tick();
|
|
56
54
|
};
|
|
@@ -58,7 +56,7 @@ const type = async (element, text) => {
|
|
|
58
56
|
if (!isEditable(element)) {
|
|
59
57
|
throw new Error("Cannot type into element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
60
58
|
}
|
|
61
|
-
const editable = getNativeObject(element.
|
|
59
|
+
const editable = getNativeObject(element.handle, Gtk.Editable);
|
|
62
60
|
const currentText = editable.getText();
|
|
63
61
|
editable.setText(currentText + text);
|
|
64
62
|
await tick();
|
|
@@ -67,7 +65,7 @@ const clear = async (element) => {
|
|
|
67
65
|
if (!isEditable(element)) {
|
|
68
66
|
throw new Error("Cannot clear element: expected editable widget (TEXT_BOX, SEARCH_BOX, or SPIN_BUTTON)");
|
|
69
67
|
}
|
|
70
|
-
getNativeObject(element.
|
|
68
|
+
getNativeObject(element.handle, Gtk.Editable)?.setText("");
|
|
71
69
|
await tick();
|
|
72
70
|
};
|
|
73
71
|
const SELECTABLE_ROLES = new Set([Gtk.AccessibleRole.COMBO_BOX, Gtk.AccessibleRole.LIST]);
|
|
@@ -155,6 +153,123 @@ const deselectOptions = async (element, values) => {
|
|
|
155
153
|
}
|
|
156
154
|
await tick();
|
|
157
155
|
};
|
|
156
|
+
const getOrCreateController = (element, controllerType) => {
|
|
157
|
+
const controllers = element.observeControllers();
|
|
158
|
+
const nItems = controllers.getNItems();
|
|
159
|
+
for (let i = 0; i < nItems; i++) {
|
|
160
|
+
const controller = controllers.getObject(i);
|
|
161
|
+
if (controller instanceof controllerType) {
|
|
162
|
+
return controller;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const controller = new controllerType();
|
|
166
|
+
element.addController(controller);
|
|
167
|
+
return controller;
|
|
168
|
+
};
|
|
169
|
+
const emitSignal = (target, signalName, ...args) => {
|
|
170
|
+
const signalArgs = args.map((arg) => {
|
|
171
|
+
if (arg.type === "float") {
|
|
172
|
+
return { type: { type: "float", size: 64 }, value: arg.value };
|
|
173
|
+
}
|
|
174
|
+
return { type: { type: "int", size: 32, unsigned: true }, value: arg.value };
|
|
175
|
+
});
|
|
176
|
+
call("libgobject-2.0.so.0", "g_signal_emit_by_name", [
|
|
177
|
+
{ type: { type: "gobject", ownership: "borrowed" }, value: target.handle },
|
|
178
|
+
{ type: { type: "string", ownership: "borrowed" }, value: signalName },
|
|
179
|
+
...signalArgs,
|
|
180
|
+
], { type: "undefined" });
|
|
181
|
+
};
|
|
182
|
+
const hover = async (element) => {
|
|
183
|
+
const controller = getOrCreateController(element, Gtk.EventControllerMotion);
|
|
184
|
+
emitSignal(controller, "enter", { type: "float", value: 0 }, { type: "float", value: 0 });
|
|
185
|
+
await tick();
|
|
186
|
+
};
|
|
187
|
+
const unhover = async (element) => {
|
|
188
|
+
const controller = getOrCreateController(element, Gtk.EventControllerMotion);
|
|
189
|
+
emitSignal(controller, "leave");
|
|
190
|
+
await tick();
|
|
191
|
+
};
|
|
192
|
+
const KEY_MAP = {
|
|
193
|
+
Enter: Gdk.KEY_Return,
|
|
194
|
+
Tab: Gdk.KEY_Tab,
|
|
195
|
+
Escape: Gdk.KEY_Escape,
|
|
196
|
+
Backspace: Gdk.KEY_BackSpace,
|
|
197
|
+
Delete: Gdk.KEY_Delete,
|
|
198
|
+
ArrowUp: Gdk.KEY_Up,
|
|
199
|
+
ArrowDown: Gdk.KEY_Down,
|
|
200
|
+
ArrowLeft: Gdk.KEY_Left,
|
|
201
|
+
ArrowRight: Gdk.KEY_Right,
|
|
202
|
+
Home: Gdk.KEY_Home,
|
|
203
|
+
End: Gdk.KEY_End,
|
|
204
|
+
PageUp: Gdk.KEY_Page_Up,
|
|
205
|
+
PageDown: Gdk.KEY_Page_Down,
|
|
206
|
+
Space: Gdk.KEY_space,
|
|
207
|
+
Shift: Gdk.KEY_Shift_L,
|
|
208
|
+
Control: Gdk.KEY_Control_L,
|
|
209
|
+
Alt: Gdk.KEY_Alt_L,
|
|
210
|
+
Meta: Gdk.KEY_Meta_L,
|
|
211
|
+
};
|
|
212
|
+
const parseKeyboardInput = (input) => {
|
|
213
|
+
const actions = [];
|
|
214
|
+
let i = 0;
|
|
215
|
+
while (i < input.length) {
|
|
216
|
+
if (input[i] === "{") {
|
|
217
|
+
const endBrace = input.indexOf("}", i);
|
|
218
|
+
if (endBrace === -1)
|
|
219
|
+
break;
|
|
220
|
+
let keyName = input.slice(i + 1, endBrace);
|
|
221
|
+
let press = true;
|
|
222
|
+
let release = true;
|
|
223
|
+
if (keyName.startsWith("/")) {
|
|
224
|
+
keyName = keyName.slice(1);
|
|
225
|
+
press = false;
|
|
226
|
+
}
|
|
227
|
+
else if (keyName.endsWith(">")) {
|
|
228
|
+
keyName = keyName.slice(0, -1);
|
|
229
|
+
release = false;
|
|
230
|
+
}
|
|
231
|
+
const keyval = KEY_MAP[keyName];
|
|
232
|
+
if (keyval === undefined) {
|
|
233
|
+
throw new Error(`Unknown key: {${keyName}}`);
|
|
234
|
+
}
|
|
235
|
+
if (press)
|
|
236
|
+
actions.push({ keyval, press: true });
|
|
237
|
+
if (release)
|
|
238
|
+
actions.push({ keyval, press: false });
|
|
239
|
+
i = endBrace + 1;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const keyval = input.charCodeAt(i);
|
|
243
|
+
actions.push({ keyval, press: true });
|
|
244
|
+
actions.push({ keyval, press: false });
|
|
245
|
+
i++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return actions;
|
|
249
|
+
};
|
|
250
|
+
const keyboard = async (element, input) => {
|
|
251
|
+
const controller = getOrCreateController(element, Gtk.EventControllerKey);
|
|
252
|
+
const actions = parseKeyboardInput(input);
|
|
253
|
+
for (const action of actions) {
|
|
254
|
+
const signalName = action.press ? "key-pressed" : "key-released";
|
|
255
|
+
emitSignal(controller, signalName, { type: "int", value: action.keyval }, { type: "int", value: 0 }, { type: "int", value: 0 });
|
|
256
|
+
}
|
|
257
|
+
await tick();
|
|
258
|
+
};
|
|
259
|
+
const pointer = async (element, input) => {
|
|
260
|
+
const controller = getOrCreateController(element, Gtk.GestureClick);
|
|
261
|
+
if (input === "[MouseLeft]" || input === "click") {
|
|
262
|
+
emitSignal(controller, "pressed", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
|
|
263
|
+
emitSignal(controller, "released", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
|
|
264
|
+
}
|
|
265
|
+
else if (input === "[MouseLeft>]" || input === "down") {
|
|
266
|
+
emitSignal(controller, "pressed", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
|
|
267
|
+
}
|
|
268
|
+
else if (input === "[/MouseLeft]" || input === "up") {
|
|
269
|
+
emitSignal(controller, "released", { type: "int", value: 1 }, { type: "float", value: 0 }, { type: "float", value: 0 });
|
|
270
|
+
}
|
|
271
|
+
await tick();
|
|
272
|
+
};
|
|
158
273
|
/**
|
|
159
274
|
* User interaction utilities for testing.
|
|
160
275
|
*
|
|
@@ -196,12 +311,6 @@ export const userEvent = {
|
|
|
196
311
|
* Emits three consecutive clicked signals. Useful for text selection.
|
|
197
312
|
*/
|
|
198
313
|
tripleClick,
|
|
199
|
-
/**
|
|
200
|
-
* Activates a widget.
|
|
201
|
-
*
|
|
202
|
-
* Calls the widget's activate method.
|
|
203
|
-
*/
|
|
204
|
-
activate,
|
|
205
314
|
/**
|
|
206
315
|
* Simulates Tab key navigation.
|
|
207
316
|
*
|
|
@@ -243,4 +352,42 @@ export const userEvent = {
|
|
|
243
352
|
* @param values - Index or array of indices to deselect
|
|
244
353
|
*/
|
|
245
354
|
deselectOptions,
|
|
355
|
+
/**
|
|
356
|
+
* Simulates mouse entering a widget (hover).
|
|
357
|
+
*
|
|
358
|
+
* Triggers the "enter" signal on the widget's EventControllerMotion.
|
|
359
|
+
*/
|
|
360
|
+
hover,
|
|
361
|
+
/**
|
|
362
|
+
* Simulates mouse leaving a widget (unhover).
|
|
363
|
+
*
|
|
364
|
+
* Triggers the "leave" signal on the widget's EventControllerMotion.
|
|
365
|
+
*/
|
|
366
|
+
unhover,
|
|
367
|
+
/**
|
|
368
|
+
* Simulates keyboard input.
|
|
369
|
+
*
|
|
370
|
+
* Supports special keys in braces: `{Enter}`, `{Tab}`, `{Escape}`, etc.
|
|
371
|
+
* Use `{Key>}` to hold a key down, `{/Key}` to release.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```tsx
|
|
375
|
+
* await userEvent.keyboard(element, "hello");
|
|
376
|
+
* await userEvent.keyboard(element, "{Enter}");
|
|
377
|
+
* await userEvent.keyboard(element, "{Shift>}A{/Shift}");
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
keyboard,
|
|
381
|
+
/**
|
|
382
|
+
* Simulates pointer (mouse) input.
|
|
383
|
+
*
|
|
384
|
+
* Supports: `"click"`, `"[MouseLeft]"`, `"down"`, `"up"`.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```tsx
|
|
388
|
+
* await userEvent.pointer(element, "click");
|
|
389
|
+
* await userEvent.pointer(element, "[MouseLeft]");
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
pointer,
|
|
246
393
|
};
|