@gtkx/testing 0.1.20 → 0.1.22
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 +208 -0
- package/dist/fire-event.d.ts +10 -0
- package/dist/fire-event.js +24 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/queries.d.ts +23 -5
- package/dist/queries.js +202 -35
- package/dist/render.d.ts +2 -3
- package/dist/render.js +37 -18
- package/dist/screen.d.ts +23 -5
- package/dist/screen.js +22 -4
- package/dist/traversal.d.ts +0 -1
- package/dist/traversal.js +1 -1
- package/dist/types.d.ts +39 -6
- package/dist/user-event.d.ts +12 -0
- package/dist/user-event.js +37 -7
- package/dist/wait-for.d.ts +4 -0
- package/dist/wait-for.js +44 -2
- package/dist/widget.d.ts +11 -0
- package/dist/widget.js +5 -0
- package/package.json +6 -6
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.svg" alt="GTKX Logo" width="128" height="128">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">GTKX</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Build native GTK4 desktop applications with React and TypeScript</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://eugeniodepalo.github.io/gtkx">Documentation</a> ·
|
|
13
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
14
|
+
<a href="#examples">Examples</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
GTKX bridges React's component model with GTK4's native widget system. Write familiar React code and render it as native Linux desktop applications with full access to GTK4 widgets, signals, and styling.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **React Components** — Use React hooks, state, and component patterns you already know
|
|
24
|
+
- **Type-Safe** — Full TypeScript support with auto-generated types from GTK4 introspection data
|
|
25
|
+
- **Native Performance** — Direct FFI bindings to GTK4 via Rust and libffi
|
|
26
|
+
- **CSS-in-JS Styling** — Emotion-style `css` template literals for GTK widgets
|
|
27
|
+
- **Testing Library** — Familiar `screen`, `userEvent`, and query APIs for testing components
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Install dependencies
|
|
33
|
+
pnpm add @gtkx/react react
|
|
34
|
+
|
|
35
|
+
# For styling (optional)
|
|
36
|
+
pnpm add @gtkx/css
|
|
37
|
+
|
|
38
|
+
# For testing (optional)
|
|
39
|
+
pnpm add -D @gtkx/testing
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create your first app:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// index.tsx
|
|
46
|
+
import { render } from "@gtkx/react";
|
|
47
|
+
import { App } from "./app.js";
|
|
48
|
+
|
|
49
|
+
render(<App />, "org.example.MyApp");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// app.tsx
|
|
54
|
+
import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
55
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
56
|
+
import { useState } from "react";
|
|
57
|
+
|
|
58
|
+
export const App = () => {
|
|
59
|
+
const [count, setCount] = useState(0);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ApplicationWindow
|
|
63
|
+
title="My App"
|
|
64
|
+
defaultWidth={400}
|
|
65
|
+
defaultHeight={300}
|
|
66
|
+
onCloseRequest={quit}
|
|
67
|
+
>
|
|
68
|
+
<Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
|
|
69
|
+
<Label.Root label={`Count: ${count}`} />
|
|
70
|
+
<Button
|
|
71
|
+
label="Increment"
|
|
72
|
+
onClicked={() => setCount((c) => c + 1)}
|
|
73
|
+
/>
|
|
74
|
+
</Box>
|
|
75
|
+
</ApplicationWindow>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run with:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm tsx index.tsx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Styling
|
|
87
|
+
|
|
88
|
+
Use `@gtkx/css` for CSS-in-JS styling:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { css } from "@gtkx/css";
|
|
92
|
+
import { Button } from "@gtkx/react";
|
|
93
|
+
|
|
94
|
+
const primaryButton = css`
|
|
95
|
+
padding: 16px 32px;
|
|
96
|
+
border-radius: 24px;
|
|
97
|
+
background: linear-gradient(135deg, #3584e4, #9141ac);
|
|
98
|
+
color: white;
|
|
99
|
+
font-weight: bold;
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const MyButton = () => (
|
|
103
|
+
<Button label="Click me" cssClasses={[primaryButton]} />
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
GTK also provides built-in CSS classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
Use `@gtkx/testing` for Testing Library-style component tests:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { cleanup, render, screen, userEvent, fireEvent } from "@gtkx/testing";
|
|
115
|
+
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
116
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
117
|
+
import { App } from "./app.js";
|
|
118
|
+
|
|
119
|
+
describe("Counter", () => {
|
|
120
|
+
afterEach(() => cleanup());
|
|
121
|
+
|
|
122
|
+
it("increments count when clicking button", async () => {
|
|
123
|
+
render(<App />);
|
|
124
|
+
|
|
125
|
+
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
126
|
+
name: "Increment",
|
|
127
|
+
});
|
|
128
|
+
await userEvent.click(button);
|
|
129
|
+
|
|
130
|
+
await screen.findByText("Count: 1");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("can also use fireEvent for synchronous events", async () => {
|
|
134
|
+
render(<App />);
|
|
135
|
+
|
|
136
|
+
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
137
|
+
name: "Increment",
|
|
138
|
+
});
|
|
139
|
+
fireEvent.click(button);
|
|
140
|
+
|
|
141
|
+
await screen.findByText("Count: 1");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Available APIs
|
|
147
|
+
|
|
148
|
+
**Queries** - Find elements in the rendered tree:
|
|
149
|
+
- `getBy*` / `getAllBy*` - Throws if not found
|
|
150
|
+
- `queryBy*` / `queryAllBy*` - Returns null/empty array if not found
|
|
151
|
+
- `findBy*` / `findAllBy*` - Async, waits for element
|
|
152
|
+
|
|
153
|
+
Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
154
|
+
|
|
155
|
+
**User Interactions**:
|
|
156
|
+
- `userEvent.click(element)` - Simulate click
|
|
157
|
+
- `userEvent.dblClick(element)` - Simulate double click
|
|
158
|
+
- `userEvent.type(element, text)` - Type text into input
|
|
159
|
+
- `userEvent.clear(element)` - Clear input text
|
|
160
|
+
- `userEvent.setup()` - Create reusable instance
|
|
161
|
+
|
|
162
|
+
**Low-level Events**:
|
|
163
|
+
- `fireEvent(element, signalName)` - Emit any GTK signal
|
|
164
|
+
- `fireEvent.click(element)` - Emit clicked signal
|
|
165
|
+
- `fireEvent.activate(element)` - Emit activate signal
|
|
166
|
+
|
|
167
|
+
**Utilities**:
|
|
168
|
+
- `waitFor(callback)` - Wait for condition
|
|
169
|
+
- `waitForElementToBeRemoved(element)` - Wait for element removal
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
### Counter
|
|
174
|
+
|
|
175
|
+
A minimal counter app demonstrating state management:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
turbo start --filter=counter-example
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### GTK4 Demo
|
|
182
|
+
|
|
183
|
+
A comprehensive showcase of GTK4 widgets and features:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
turbo start --filter=gtk4-demo
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Packages
|
|
190
|
+
|
|
191
|
+
| Package | Description |
|
|
192
|
+
|---------|-------------|
|
|
193
|
+
| [@gtkx/react](packages/react) | React reconciler and JSX components |
|
|
194
|
+
| [@gtkx/ffi](packages/ffi) | TypeScript FFI bindings for GTK4 |
|
|
195
|
+
| [@gtkx/native](packages/native) | Rust native module for FFI bridge |
|
|
196
|
+
| [@gtkx/css](packages/css) | CSS-in-JS styling for GTK widgets |
|
|
197
|
+
| [@gtkx/testing](packages/testing) | Testing utilities for GTKX components |
|
|
198
|
+
| [@gtkx/gir](packages/gir) | GObject Introspection parser for codegen |
|
|
199
|
+
|
|
200
|
+
## Requirements
|
|
201
|
+
|
|
202
|
+
- Node.js 20+
|
|
203
|
+
- GTK4 development libraries
|
|
204
|
+
- Linux (GTK4 is Linux-native)
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
[MPL-2.0](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
type FireEventFunction = {
|
|
3
|
+
(element: Gtk.Widget, signalName: string): void;
|
|
4
|
+
click: (element: Gtk.Widget) => void;
|
|
5
|
+
activate: (element: Gtk.Widget) => void;
|
|
6
|
+
toggled: (element: Gtk.Widget) => void;
|
|
7
|
+
changed: (element: Gtk.Widget) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare const fireEvent: FireEventFunction;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { call } from "@gtkx/native";
|
|
2
|
+
import { getWidgetPtr } from "./widget.js";
|
|
3
|
+
const emitSignal = (widget, signalName) => {
|
|
4
|
+
call("libgobject-2.0.so.0", "g_signal_emit_by_name", [
|
|
5
|
+
{ type: { type: "gobject" }, value: getWidgetPtr(widget) },
|
|
6
|
+
{ type: { type: "string" }, value: signalName },
|
|
7
|
+
], { type: "undefined" });
|
|
8
|
+
};
|
|
9
|
+
export const fireEvent = Object.assign((element, signalName) => {
|
|
10
|
+
emitSignal(element, signalName);
|
|
11
|
+
}, {
|
|
12
|
+
click: (element) => {
|
|
13
|
+
emitSignal(element, "clicked");
|
|
14
|
+
},
|
|
15
|
+
activate: (element) => {
|
|
16
|
+
emitSignal(element, "activate");
|
|
17
|
+
},
|
|
18
|
+
toggled: (element) => {
|
|
19
|
+
emitSignal(element, "toggled");
|
|
20
|
+
},
|
|
21
|
+
changed: (element) => {
|
|
22
|
+
emitSignal(element, "changed");
|
|
23
|
+
},
|
|
24
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { fireEvent } from "./fire-event.js";
|
|
2
|
+
export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, findByLabelText, findByRole, findByTestId, findByText, getAllByLabelText, getAllByRole, getAllByTestId, getAllByText, getByLabelText, getByRole, getByTestId, getByText, queryAllByLabelText, queryAllByRole, queryAllByTestId, queryAllByText, queryByLabelText, queryByRole, queryByTestId, queryByText, } from "./queries.js";
|
|
3
|
+
export { cleanup, render, teardown } from "./render.jsx";
|
|
3
4
|
export { screen } from "./screen.js";
|
|
4
|
-
export type { ByRoleOptions, RenderResult, WaitForOptions } from "./types.js";
|
|
5
|
+
export type { ByRoleOptions, RenderOptions, RenderResult, TextMatchOptions, WaitForOptions, } from "./types.js";
|
|
6
|
+
export type { UserEventInstance, UserEventOptions } from "./user-event.js";
|
|
5
7
|
export { userEvent } from "./user-event.js";
|
|
6
|
-
export { waitFor } from "./wait-for.js";
|
|
8
|
+
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export { fireEvent } from "./fire-event.js";
|
|
2
|
+
export { findAllByLabelText, findAllByRole, findAllByTestId, findAllByText, findByLabelText, findByRole, findByTestId, findByText, getAllByLabelText, getAllByRole, getAllByTestId, getAllByText, getByLabelText, getByRole, getByTestId, getByText, queryAllByLabelText, queryAllByRole, queryAllByTestId, queryAllByText, queryByLabelText, queryByRole, queryByTestId, queryByText, } from "./queries.js";
|
|
3
|
+
export { cleanup, render, teardown } from "./render.jsx";
|
|
3
4
|
export { screen } from "./screen.js";
|
|
4
5
|
export { userEvent } from "./user-event.js";
|
|
5
|
-
export { waitFor } from "./wait-for.js";
|
|
6
|
+
export { waitFor, waitForElementToBeRemoved } from "./wait-for.js";
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
|
-
import type { ByRoleOptions } from "./types.js";
|
|
3
|
+
import type { ByRoleOptions, TextMatchOptions } from "./types.js";
|
|
4
4
|
type Container = Gtk.Application | Gtk.Widget;
|
|
5
|
+
export declare const getAllByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
5
6
|
export declare const getByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget;
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const queryAllByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
8
|
+
export declare const queryByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
|
|
9
|
+
export declare const getAllByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
10
|
+
export declare const getByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
11
|
+
export declare const queryAllByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
12
|
+
export declare const queryByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
13
|
+
export declare const getAllByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
14
|
+
export declare const getByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
15
|
+
export declare const queryAllByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
16
|
+
export declare const queryByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
17
|
+
export declare const getAllByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
18
|
+
export declare const getByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
19
|
+
export declare const queryAllByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
20
|
+
export declare const queryByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
8
21
|
export declare const findByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const
|
|
22
|
+
export declare const findAllByRole: (container: Container, role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
23
|
+
export declare const findByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
24
|
+
export declare const findAllByLabelText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
25
|
+
export declare const findByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
26
|
+
export declare const findAllByText: (container: Container, text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
27
|
+
export declare const findByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
28
|
+
export declare const findAllByTestId: (container: Container, testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
11
29
|
export {};
|
package/dist/queries.js
CHANGED
|
@@ -2,14 +2,22 @@ import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
|
2
2
|
import { call } from "@gtkx/native";
|
|
3
3
|
import { findAll } from "./traversal.js";
|
|
4
4
|
import { waitFor } from "./wait-for.js";
|
|
5
|
-
|
|
5
|
+
import { getWidgetPtr } from "./widget.js";
|
|
6
|
+
const DEFAULT_NORMALIZER = (text) => text.trim().replace(/\s+/g, " ");
|
|
7
|
+
const normalizeText = (text, options) => {
|
|
8
|
+
const normalizer = options?.normalizer ?? DEFAULT_NORMALIZER;
|
|
9
|
+
return normalizer(text);
|
|
10
|
+
};
|
|
11
|
+
const matchText = (actual, expected, options) => {
|
|
6
12
|
if (actual === null)
|
|
7
13
|
return false;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
const normalizedActual = normalizeText(actual, options);
|
|
15
|
+
const exact = options?.exact ?? true;
|
|
16
|
+
if (typeof expected === "string") {
|
|
17
|
+
const normalizedExpected = normalizeText(expected, options);
|
|
18
|
+
return exact ? normalizedActual === normalizedExpected : normalizedActual.includes(normalizedExpected);
|
|
19
|
+
}
|
|
20
|
+
return expected.test(normalizedActual);
|
|
13
21
|
};
|
|
14
22
|
const callGetter = (ptr, funcName) => {
|
|
15
23
|
const result = call("libgtk-4.so.1", funcName, [{ type: { type: "gobject" }, value: ptr }], {
|
|
@@ -62,56 +70,215 @@ const getWidgetText = (widget) => {
|
|
|
62
70
|
return null;
|
|
63
71
|
}
|
|
64
72
|
};
|
|
73
|
+
const getWidgetTestId = (widget) => {
|
|
74
|
+
const ptr = getWidgetPtr(widget);
|
|
75
|
+
if (!ptr)
|
|
76
|
+
return null;
|
|
77
|
+
const result = call("libgtk-4.so.1", "gtk_widget_get_name", [{ type: { type: "gobject" }, value: ptr }], {
|
|
78
|
+
type: "string",
|
|
79
|
+
borrowed: true,
|
|
80
|
+
});
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
const getWidgetCheckedState = (widget) => {
|
|
84
|
+
const ptr = getWidgetPtr(widget);
|
|
85
|
+
if (!ptr)
|
|
86
|
+
return undefined;
|
|
87
|
+
const accessible = widget;
|
|
88
|
+
const role = accessible.getAccessibleRole();
|
|
89
|
+
if (role === AccessibleRole.CHECKBOX || role === AccessibleRole.RADIO) {
|
|
90
|
+
const result = call("libgtk-4.so.1", "gtk_check_button_get_active", [{ type: { type: "gobject" }, value: ptr }], {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
});
|
|
93
|
+
return result === true;
|
|
94
|
+
}
|
|
95
|
+
if (role === AccessibleRole.TOGGLE_BUTTON) {
|
|
96
|
+
const result = call("libgtk-4.so.1", "gtk_toggle_button_get_active", [{ type: { type: "gobject" }, value: ptr }], { type: "boolean" });
|
|
97
|
+
return result === true;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
const getWidgetExpandedState = (widget) => {
|
|
102
|
+
const ptr = getWidgetPtr(widget);
|
|
103
|
+
if (!ptr)
|
|
104
|
+
return undefined;
|
|
105
|
+
const accessible = widget;
|
|
106
|
+
const role = accessible.getAccessibleRole();
|
|
107
|
+
if (role === AccessibleRole.BUTTON) {
|
|
108
|
+
const expanderPtr = call("libgtk-4.so.1", "gtk_widget_get_parent", [{ type: { type: "gobject" }, value: ptr }], { type: "gobject" });
|
|
109
|
+
if (!expanderPtr)
|
|
110
|
+
return undefined;
|
|
111
|
+
const result = call("libgtk-4.so.1", "gtk_expander_get_expanded", [{ type: { type: "gobject" }, value: expanderPtr }], { type: "boolean" });
|
|
112
|
+
return result === true;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
const matchByRoleOptions = (widget, options) => {
|
|
117
|
+
if (!options)
|
|
118
|
+
return true;
|
|
119
|
+
if (options.name !== undefined) {
|
|
120
|
+
const text = getWidgetText(widget);
|
|
121
|
+
if (!matchText(text, options.name, options))
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (options.checked !== undefined) {
|
|
125
|
+
const checked = getWidgetCheckedState(widget);
|
|
126
|
+
if (checked !== options.checked)
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (options.expanded !== undefined) {
|
|
130
|
+
const expanded = getWidgetExpandedState(widget);
|
|
131
|
+
if (expanded !== options.expanded)
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
65
136
|
const formatRole = (role) => AccessibleRole[role] ?? String(role);
|
|
66
|
-
|
|
137
|
+
const formatByRoleError = (role, options) => {
|
|
138
|
+
const parts = [`role "${formatRole(role)}"`];
|
|
139
|
+
if (options?.name)
|
|
140
|
+
parts.push(`name "${options.name}"`);
|
|
141
|
+
if (options?.checked !== undefined)
|
|
142
|
+
parts.push(`checked=${options.checked}`);
|
|
143
|
+
if (options?.pressed !== undefined)
|
|
144
|
+
parts.push(`pressed=${options.pressed}`);
|
|
145
|
+
if (options?.selected !== undefined)
|
|
146
|
+
parts.push(`selected=${options.selected}`);
|
|
147
|
+
if (options?.expanded !== undefined)
|
|
148
|
+
parts.push(`expanded=${options.expanded}`);
|
|
149
|
+
if (options?.level !== undefined)
|
|
150
|
+
parts.push(`level=${options.level}`);
|
|
151
|
+
return parts.join(" and ");
|
|
152
|
+
};
|
|
153
|
+
export const getAllByRole = (container, role, options) => {
|
|
67
154
|
const matches = findAll(container, (node) => {
|
|
68
155
|
const accessible = node;
|
|
69
156
|
if (accessible.getAccessibleRole() !== role)
|
|
70
157
|
return false;
|
|
71
|
-
|
|
72
|
-
const text = getWidgetText(node);
|
|
73
|
-
return matchText(text, options.name);
|
|
74
|
-
}
|
|
75
|
-
return true;
|
|
158
|
+
return matchByRoleOptions(node, options);
|
|
76
159
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!match) {
|
|
80
|
-
throw new Error(`Unable to find element with role "${formatRole(role)}"${nameInfo}`);
|
|
160
|
+
if (matches.length === 0) {
|
|
161
|
+
throw new Error(`Unable to find any elements with ${formatByRoleError(role, options)}`);
|
|
81
162
|
}
|
|
82
|
-
|
|
83
|
-
|
|
163
|
+
return matches;
|
|
164
|
+
};
|
|
165
|
+
export const getByRole = (container, role, options) => {
|
|
166
|
+
const matches = getAllByRole(container, role, options);
|
|
167
|
+
if (matches.length > 1) {
|
|
168
|
+
throw new Error(`Found ${matches.length} elements with ${formatByRoleError(role, options)}`);
|
|
84
169
|
}
|
|
85
|
-
return
|
|
170
|
+
return matches[0];
|
|
86
171
|
};
|
|
87
|
-
export const
|
|
172
|
+
export const queryAllByRole = (container, role, options) => {
|
|
173
|
+
return findAll(container, (node) => {
|
|
174
|
+
const accessible = node;
|
|
175
|
+
if (accessible.getAccessibleRole() !== role)
|
|
176
|
+
return false;
|
|
177
|
+
return matchByRoleOptions(node, options);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
export const queryByRole = (container, role, options) => {
|
|
181
|
+
const matches = queryAllByRole(container, role, options);
|
|
182
|
+
if (matches.length > 1) {
|
|
183
|
+
throw new Error(`Found ${matches.length} elements with ${formatByRoleError(role, options)}`);
|
|
184
|
+
}
|
|
185
|
+
return matches[0] ?? null;
|
|
186
|
+
};
|
|
187
|
+
export const getAllByLabelText = (container, text, options) => {
|
|
88
188
|
const matches = findAll(container, (node) => {
|
|
89
189
|
const widgetText = getWidgetText(node);
|
|
90
|
-
return matchText(widgetText, text);
|
|
190
|
+
return matchText(widgetText, text, options);
|
|
91
191
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
192
|
+
if (matches.length === 0) {
|
|
193
|
+
throw new Error(`Unable to find any elements with label text "${text}"`);
|
|
194
|
+
}
|
|
195
|
+
return matches;
|
|
196
|
+
};
|
|
197
|
+
export const getByLabelText = (container, text, options) => {
|
|
198
|
+
const matches = getAllByLabelText(container, text, options);
|
|
199
|
+
if (matches.length > 1) {
|
|
200
|
+
throw new Error(`Found ${matches.length} elements with label text "${text}"`);
|
|
95
201
|
}
|
|
96
|
-
|
|
202
|
+
return matches[0];
|
|
203
|
+
};
|
|
204
|
+
export const queryAllByLabelText = (container, text, options) => {
|
|
205
|
+
return findAll(container, (node) => {
|
|
206
|
+
const widgetText = getWidgetText(node);
|
|
207
|
+
return matchText(widgetText, text, options);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
export const queryByLabelText = (container, text, options) => {
|
|
211
|
+
const matches = queryAllByLabelText(container, text, options);
|
|
212
|
+
if (matches.length > 1) {
|
|
97
213
|
throw new Error(`Found ${matches.length} elements with label text "${text}"`);
|
|
98
214
|
}
|
|
99
|
-
return
|
|
215
|
+
return matches[0] ?? null;
|
|
100
216
|
};
|
|
101
|
-
export const
|
|
217
|
+
export const getAllByText = (container, text, options) => {
|
|
102
218
|
const matches = findAll(container, (node) => {
|
|
103
219
|
const widgetText = getWidgetText(node);
|
|
104
|
-
return matchText(widgetText, text);
|
|
220
|
+
return matchText(widgetText, text, options);
|
|
105
221
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw new Error(`Unable to find element with text "${text}"`);
|
|
222
|
+
if (matches.length === 0) {
|
|
223
|
+
throw new Error(`Unable to find any elements with text "${text}"`);
|
|
109
224
|
}
|
|
110
|
-
|
|
225
|
+
return matches;
|
|
226
|
+
};
|
|
227
|
+
export const getByText = (container, text, options) => {
|
|
228
|
+
const matches = getAllByText(container, text, options);
|
|
229
|
+
if (matches.length > 1) {
|
|
111
230
|
throw new Error(`Found ${matches.length} elements with text "${text}"`);
|
|
112
231
|
}
|
|
113
|
-
return
|
|
232
|
+
return matches[0];
|
|
233
|
+
};
|
|
234
|
+
export const queryAllByText = (container, text, options) => {
|
|
235
|
+
return findAll(container, (node) => {
|
|
236
|
+
const widgetText = getWidgetText(node);
|
|
237
|
+
return matchText(widgetText, text, options);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
export const queryByText = (container, text, options) => {
|
|
241
|
+
const matches = queryAllByText(container, text, options);
|
|
242
|
+
if (matches.length > 1) {
|
|
243
|
+
throw new Error(`Found ${matches.length} elements with text "${text}"`);
|
|
244
|
+
}
|
|
245
|
+
return matches[0] ?? null;
|
|
246
|
+
};
|
|
247
|
+
export const getAllByTestId = (container, testId, options) => {
|
|
248
|
+
const matches = findAll(container, (node) => {
|
|
249
|
+
const widgetTestId = getWidgetTestId(node);
|
|
250
|
+
return matchText(widgetTestId, testId, options);
|
|
251
|
+
});
|
|
252
|
+
if (matches.length === 0) {
|
|
253
|
+
throw new Error(`Unable to find any elements with test id "${testId}"`);
|
|
254
|
+
}
|
|
255
|
+
return matches;
|
|
256
|
+
};
|
|
257
|
+
export const getByTestId = (container, testId, options) => {
|
|
258
|
+
const matches = getAllByTestId(container, testId, options);
|
|
259
|
+
if (matches.length > 1) {
|
|
260
|
+
throw new Error(`Found ${matches.length} elements with test id "${testId}"`);
|
|
261
|
+
}
|
|
262
|
+
return matches[0];
|
|
263
|
+
};
|
|
264
|
+
export const queryAllByTestId = (container, testId, options) => {
|
|
265
|
+
return findAll(container, (node) => {
|
|
266
|
+
const widgetTestId = getWidgetTestId(node);
|
|
267
|
+
return matchText(widgetTestId, testId, options);
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
export const queryByTestId = (container, testId, options) => {
|
|
271
|
+
const matches = queryAllByTestId(container, testId, options);
|
|
272
|
+
if (matches.length > 1) {
|
|
273
|
+
throw new Error(`Found ${matches.length} elements with test id "${testId}"`);
|
|
274
|
+
}
|
|
275
|
+
return matches[0] ?? null;
|
|
114
276
|
};
|
|
115
277
|
export const findByRole = async (container, role, options) => waitFor(() => getByRole(container, role, options));
|
|
116
|
-
export const
|
|
117
|
-
export const
|
|
278
|
+
export const findAllByRole = async (container, role, options) => waitFor(() => getAllByRole(container, role, options));
|
|
279
|
+
export const findByLabelText = async (container, text, options) => waitFor(() => getByLabelText(container, text, options));
|
|
280
|
+
export const findAllByLabelText = async (container, text, options) => waitFor(() => getAllByLabelText(container, text, options));
|
|
281
|
+
export const findByText = async (container, text, options) => waitFor(() => getByText(container, text, options));
|
|
282
|
+
export const findAllByText = async (container, text, options) => waitFor(() => getAllByText(container, text, options));
|
|
283
|
+
export const findByTestId = async (container, testId, options) => waitFor(() => getByTestId(container, testId, options));
|
|
284
|
+
export const findAllByTestId = async (container, testId, options) => waitFor(() => getAllByTestId(container, testId, options));
|
package/dist/render.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { RenderResult } from "./types.js";
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const render: (element: ReactNode) => RenderResult;
|
|
2
|
+
import type { RenderOptions, RenderResult } from "./types.js";
|
|
3
|
+
export declare const render: (element: ReactNode, options?: RenderOptions) => RenderResult;
|
|
5
4
|
export declare const cleanup: () => void;
|
|
6
5
|
export declare const teardown: () => void;
|
package/dist/render.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
2
|
import { start, stop } from "@gtkx/ffi";
|
|
2
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
3
4
|
import { reconciler } from "@gtkx/react";
|
|
4
|
-
import { beforeAll } from "vitest";
|
|
5
5
|
import * as queries from "./queries.js";
|
|
6
6
|
import { setScreenRoot } from "./screen.js";
|
|
7
7
|
const APP_ID = "com.gtkx.testing";
|
|
8
8
|
let app = null;
|
|
9
9
|
let container = null;
|
|
10
|
-
export const setup = () => {
|
|
11
|
-
beforeAll(() => {
|
|
12
|
-
if (!app) {
|
|
13
|
-
app = start(APP_ID);
|
|
14
|
-
reconciler.setApp(app);
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
10
|
const hasGetLabel = (widget) => typeof widget.getLabel === "function";
|
|
19
11
|
const printWidgetTree = (root, indent = 0) => {
|
|
20
12
|
const accessible = root;
|
|
@@ -65,21 +57,48 @@ const ensureInitialized = () => {
|
|
|
65
57
|
}
|
|
66
58
|
return { app, container };
|
|
67
59
|
};
|
|
68
|
-
|
|
60
|
+
const wrapElement = (element, Wrapper) => {
|
|
61
|
+
if (!Wrapper)
|
|
62
|
+
return element;
|
|
63
|
+
return _jsx(Wrapper, { children: element });
|
|
64
|
+
};
|
|
65
|
+
export const render = (element, options) => {
|
|
69
66
|
const { app: application, container: fiberRoot } = ensureInitialized();
|
|
70
67
|
const instance = reconciler.getInstance();
|
|
71
|
-
|
|
68
|
+
const wrappedElement = wrapElement(element, options?.wrapper);
|
|
69
|
+
updateSync(instance, wrappedElement, fiberRoot);
|
|
72
70
|
setScreenRoot(application);
|
|
73
71
|
return {
|
|
74
72
|
container: application,
|
|
75
|
-
getByRole: (role,
|
|
76
|
-
getByLabelText: (text) => queries.getByLabelText(application, text),
|
|
77
|
-
getByText: (text) => queries.getByText(application, text),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
getByRole: (role, opts) => queries.getByRole(application, role, opts),
|
|
74
|
+
getByLabelText: (text, opts) => queries.getByLabelText(application, text, opts),
|
|
75
|
+
getByText: (text, opts) => queries.getByText(application, text, opts),
|
|
76
|
+
getByTestId: (testId, opts) => queries.getByTestId(application, testId, opts),
|
|
77
|
+
queryByRole: (role, opts) => queries.queryByRole(application, role, opts),
|
|
78
|
+
queryByLabelText: (text, opts) => queries.queryByLabelText(application, text, opts),
|
|
79
|
+
queryByText: (text, opts) => queries.queryByText(application, text, opts),
|
|
80
|
+
queryByTestId: (testId, opts) => queries.queryByTestId(application, testId, opts),
|
|
81
|
+
getAllByRole: (role, opts) => queries.getAllByRole(application, role, opts),
|
|
82
|
+
getAllByLabelText: (text, opts) => queries.getAllByLabelText(application, text, opts),
|
|
83
|
+
getAllByText: (text, opts) => queries.getAllByText(application, text, opts),
|
|
84
|
+
getAllByTestId: (testId, opts) => queries.getAllByTestId(application, testId, opts),
|
|
85
|
+
queryAllByRole: (role, opts) => queries.queryAllByRole(application, role, opts),
|
|
86
|
+
queryAllByLabelText: (text, opts) => queries.queryAllByLabelText(application, text, opts),
|
|
87
|
+
queryAllByText: (text, opts) => queries.queryAllByText(application, text, opts),
|
|
88
|
+
queryAllByTestId: (testId, opts) => queries.queryAllByTestId(application, testId, opts),
|
|
89
|
+
findByRole: (role, opts) => queries.findByRole(application, role, opts),
|
|
90
|
+
findByLabelText: (text, opts) => queries.findByLabelText(application, text, opts),
|
|
91
|
+
findByText: (text, opts) => queries.findByText(application, text, opts),
|
|
92
|
+
findByTestId: (testId, opts) => queries.findByTestId(application, testId, opts),
|
|
93
|
+
findAllByRole: (role, opts) => queries.findAllByRole(application, role, opts),
|
|
94
|
+
findAllByLabelText: (text, opts) => queries.findAllByLabelText(application, text, opts),
|
|
95
|
+
findAllByText: (text, opts) => queries.findAllByText(application, text, opts),
|
|
96
|
+
findAllByTestId: (testId, opts) => queries.findAllByTestId(application, testId, opts),
|
|
81
97
|
unmount: () => updateSync(instance, null, fiberRoot),
|
|
82
|
-
rerender: (newElement) =>
|
|
98
|
+
rerender: (newElement) => {
|
|
99
|
+
const wrapped = wrapElement(newElement, options?.wrapper);
|
|
100
|
+
updateSync(instance, wrapped, fiberRoot);
|
|
101
|
+
},
|
|
83
102
|
debug: () => {
|
|
84
103
|
const activeWindow = application.getActiveWindow();
|
|
85
104
|
if (activeWindow) {
|
package/dist/screen.d.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import type { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
|
-
import type { ByRoleOptions } from "./types.js";
|
|
3
|
+
import type { ByRoleOptions, TextMatchOptions } from "./types.js";
|
|
4
4
|
export declare const setScreenRoot: (root: Gtk.Application | null) => void;
|
|
5
5
|
export declare const screen: {
|
|
6
6
|
getByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget;
|
|
7
|
-
getByLabelText: (text: string | RegExp) => Gtk.Widget;
|
|
8
|
-
getByText: (text: string | RegExp) => Gtk.Widget;
|
|
7
|
+
getByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
8
|
+
getByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
9
|
+
getByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
10
|
+
queryByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
|
|
11
|
+
queryByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
12
|
+
queryByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
13
|
+
queryByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
14
|
+
getAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
15
|
+
getAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
16
|
+
getAllByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
17
|
+
getAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
18
|
+
queryAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
19
|
+
queryAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
20
|
+
queryAllByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
21
|
+
queryAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
9
22
|
findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
10
|
-
findByLabelText: (text: string | RegExp) => Promise<Gtk.Widget>;
|
|
11
|
-
findByText: (text: string | RegExp) => Promise<Gtk.Widget>;
|
|
23
|
+
findByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
24
|
+
findByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
25
|
+
findByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
26
|
+
findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
27
|
+
findAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
28
|
+
findAllByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
29
|
+
findAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
12
30
|
debug: () => void;
|
|
13
31
|
};
|
package/dist/screen.js
CHANGED
|
@@ -11,11 +11,29 @@ const getRoot = () => {
|
|
|
11
11
|
};
|
|
12
12
|
export const screen = {
|
|
13
13
|
getByRole: (role, options) => queries.getByRole(getRoot(), role, options),
|
|
14
|
-
getByLabelText: (text) => queries.getByLabelText(getRoot(), text),
|
|
15
|
-
getByText: (text) => queries.getByText(getRoot(), text),
|
|
14
|
+
getByLabelText: (text, options) => queries.getByLabelText(getRoot(), text, options),
|
|
15
|
+
getByText: (text, options) => queries.getByText(getRoot(), text, options),
|
|
16
|
+
getByTestId: (testId, options) => queries.getByTestId(getRoot(), testId, options),
|
|
17
|
+
queryByRole: (role, options) => queries.queryByRole(getRoot(), role, options),
|
|
18
|
+
queryByLabelText: (text, options) => queries.queryByLabelText(getRoot(), text, options),
|
|
19
|
+
queryByText: (text, options) => queries.queryByText(getRoot(), text, options),
|
|
20
|
+
queryByTestId: (testId, options) => queries.queryByTestId(getRoot(), testId, options),
|
|
21
|
+
getAllByRole: (role, options) => queries.getAllByRole(getRoot(), role, options),
|
|
22
|
+
getAllByLabelText: (text, options) => queries.getAllByLabelText(getRoot(), text, options),
|
|
23
|
+
getAllByText: (text, options) => queries.getAllByText(getRoot(), text, options),
|
|
24
|
+
getAllByTestId: (testId, options) => queries.getAllByTestId(getRoot(), testId, options),
|
|
25
|
+
queryAllByRole: (role, options) => queries.queryAllByRole(getRoot(), role, options),
|
|
26
|
+
queryAllByLabelText: (text, options) => queries.queryAllByLabelText(getRoot(), text, options),
|
|
27
|
+
queryAllByText: (text, options) => queries.queryAllByText(getRoot(), text, options),
|
|
28
|
+
queryAllByTestId: (testId, options) => queries.queryAllByTestId(getRoot(), testId, options),
|
|
16
29
|
findByRole: (role, options) => queries.findByRole(getRoot(), role, options),
|
|
17
|
-
findByLabelText: (text) => queries.findByLabelText(getRoot(), text),
|
|
18
|
-
findByText: (text) => queries.findByText(getRoot(), text),
|
|
30
|
+
findByLabelText: (text, options) => queries.findByLabelText(getRoot(), text, options),
|
|
31
|
+
findByText: (text, options) => queries.findByText(getRoot(), text, options),
|
|
32
|
+
findByTestId: (testId, options) => queries.findByTestId(getRoot(), testId, options),
|
|
33
|
+
findAllByRole: (role, options) => queries.findAllByRole(getRoot(), role, options),
|
|
34
|
+
findAllByLabelText: (text, options) => queries.findAllByLabelText(getRoot(), text, options),
|
|
35
|
+
findAllByText: (text, options) => queries.findAllByText(getRoot(), text, options),
|
|
36
|
+
findAllByTestId: (testId, options) => queries.findAllByTestId(getRoot(), testId, options),
|
|
19
37
|
debug: () => {
|
|
20
38
|
console.log("Screen debug - root:", getRoot());
|
|
21
39
|
},
|
package/dist/traversal.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
type Container = Gtk.Application | Gtk.Widget;
|
|
3
|
-
export declare const traverse: (container: Container) => Generator<Gtk.Widget>;
|
|
4
3
|
export declare const findAll: (container: Container, predicate: (node: Gtk.Widget) => boolean) => Gtk.Widget[];
|
|
5
4
|
export {};
|
package/dist/traversal.js
CHANGED
|
@@ -13,7 +13,7 @@ const traverseApplication = function* (app) {
|
|
|
13
13
|
yield* traverseWidgetTree(window);
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
const traverse = function* (container) {
|
|
17
17
|
if (isApplication(container)) {
|
|
18
18
|
yield* traverseApplication(container);
|
|
19
19
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
2
|
import type { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
export interface
|
|
3
|
+
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
export interface TextMatchOptions {
|
|
5
|
+
exact?: boolean;
|
|
6
|
+
normalizer?: (text: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export interface ByRoleOptions extends TextMatchOptions {
|
|
5
9
|
name?: string | RegExp;
|
|
10
|
+
checked?: boolean;
|
|
11
|
+
pressed?: boolean;
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
level?: number;
|
|
6
15
|
}
|
|
7
16
|
export interface WaitForOptions {
|
|
8
17
|
timeout?: number;
|
|
9
18
|
interval?: number;
|
|
19
|
+
onTimeout?: (error: Error) => Error;
|
|
20
|
+
}
|
|
21
|
+
export interface RenderOptions {
|
|
22
|
+
wrapper?: ComponentType<{
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}>;
|
|
10
25
|
}
|
|
11
26
|
export interface RenderResult {
|
|
12
27
|
container: Gtk.Application;
|
|
13
28
|
getByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget;
|
|
14
|
-
getByLabelText: (text: string | RegExp) => Gtk.Widget;
|
|
15
|
-
getByText: (text: string | RegExp) => Gtk.Widget;
|
|
29
|
+
getByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
30
|
+
getByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
31
|
+
getByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget;
|
|
32
|
+
queryByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget | null;
|
|
33
|
+
queryByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
34
|
+
queryByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
35
|
+
queryByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget | null;
|
|
36
|
+
getAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
37
|
+
getAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
38
|
+
getAllByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
39
|
+
getAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
40
|
+
queryAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Gtk.Widget[];
|
|
41
|
+
queryAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
42
|
+
queryAllByText: (text: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
43
|
+
queryAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Gtk.Widget[];
|
|
16
44
|
findByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget>;
|
|
17
|
-
findByLabelText: (text: string | RegExp) => Promise<Gtk.Widget>;
|
|
18
|
-
findByText: (text: string | RegExp) => Promise<Gtk.Widget>;
|
|
45
|
+
findByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
46
|
+
findByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
47
|
+
findByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget>;
|
|
48
|
+
findAllByRole: (role: AccessibleRole, options?: ByRoleOptions) => Promise<Gtk.Widget[]>;
|
|
49
|
+
findAllByLabelText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
50
|
+
findAllByText: (text: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
51
|
+
findAllByTestId: (testId: string | RegExp, options?: TextMatchOptions) => Promise<Gtk.Widget[]>;
|
|
19
52
|
unmount: () => void;
|
|
20
53
|
rerender: (element: ReactNode) => void;
|
|
21
54
|
debug: () => void;
|
package/dist/user-event.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
export interface UserEventOptions {
|
|
3
|
+
delay?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface UserEventInstance {
|
|
6
|
+
click: (element: Gtk.Widget) => Promise<void>;
|
|
7
|
+
dblClick: (element: Gtk.Widget) => Promise<void>;
|
|
8
|
+
type: (element: Gtk.Widget, text: string) => Promise<void>;
|
|
9
|
+
clear: (element: Gtk.Widget) => Promise<void>;
|
|
10
|
+
}
|
|
2
11
|
export declare const userEvent: {
|
|
12
|
+
setup: (options?: UserEventOptions) => UserEventInstance;
|
|
3
13
|
click: (element: Gtk.Widget) => Promise<void>;
|
|
14
|
+
dblClick: (element: Gtk.Widget) => Promise<void>;
|
|
4
15
|
type: (element: Gtk.Widget, text: string) => Promise<void>;
|
|
16
|
+
clear: (element: Gtk.Widget) => Promise<void>;
|
|
5
17
|
};
|
package/dist/user-event.js
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { call } from "@gtkx/native";
|
|
2
|
-
|
|
3
|
-
const hasGetText = (widget) => typeof widget.getText === "function";
|
|
4
|
-
const hasPtr = (widget) => widget.ptr !== undefined;
|
|
2
|
+
import { getWidgetPtr, hasGetText, hasSetText } from "./widget.js";
|
|
5
3
|
const emitSignal = (widget, signalName) => {
|
|
6
|
-
if (!hasPtr(widget)) {
|
|
7
|
-
throw new Error("Widget does not have a ptr property");
|
|
8
|
-
}
|
|
9
4
|
call("libgobject-2.0.so.0", "g_signal_emit_by_name", [
|
|
10
|
-
{ type: { type: "gobject" }, value: widget
|
|
5
|
+
{ type: { type: "gobject" }, value: getWidgetPtr(widget) },
|
|
11
6
|
{ type: { type: "string" }, value: signalName },
|
|
12
7
|
], { type: "undefined" });
|
|
13
8
|
};
|
|
9
|
+
const createUserEventInstance = (_options) => {
|
|
10
|
+
return {
|
|
11
|
+
click: async (element) => {
|
|
12
|
+
emitSignal(element, "clicked");
|
|
13
|
+
},
|
|
14
|
+
dblClick: async (element) => {
|
|
15
|
+
emitSignal(element, "clicked");
|
|
16
|
+
emitSignal(element, "clicked");
|
|
17
|
+
},
|
|
18
|
+
type: async (element, text) => {
|
|
19
|
+
if (!hasSetText(element)) {
|
|
20
|
+
throw new Error("Cannot type into element: no setText method available");
|
|
21
|
+
}
|
|
22
|
+
const currentText = hasGetText(element) ? element.getText() : "";
|
|
23
|
+
element.setText(currentText + text);
|
|
24
|
+
},
|
|
25
|
+
clear: async (element) => {
|
|
26
|
+
if (!hasSetText(element)) {
|
|
27
|
+
throw new Error("Cannot clear element: no setText method available");
|
|
28
|
+
}
|
|
29
|
+
element.setText("");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
14
33
|
export const userEvent = {
|
|
34
|
+
setup: (options) => createUserEventInstance(options),
|
|
15
35
|
click: async (element) => {
|
|
16
36
|
emitSignal(element, "clicked");
|
|
17
37
|
},
|
|
38
|
+
dblClick: async (element) => {
|
|
39
|
+
emitSignal(element, "clicked");
|
|
40
|
+
emitSignal(element, "clicked");
|
|
41
|
+
},
|
|
18
42
|
type: async (element, text) => {
|
|
19
43
|
if (!hasSetText(element)) {
|
|
20
44
|
throw new Error("Cannot type into element: no setText method available");
|
|
@@ -22,4 +46,10 @@ export const userEvent = {
|
|
|
22
46
|
const currentText = hasGetText(element) ? element.getText() : "";
|
|
23
47
|
element.setText(currentText + text);
|
|
24
48
|
},
|
|
49
|
+
clear: async (element) => {
|
|
50
|
+
if (!hasSetText(element)) {
|
|
51
|
+
throw new Error("Cannot clear element: no setText method available");
|
|
52
|
+
}
|
|
53
|
+
element.setText("");
|
|
54
|
+
},
|
|
25
55
|
};
|
package/dist/wait-for.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
1
2
|
import type { WaitForOptions } from "./types.js";
|
|
2
3
|
export declare const waitFor: <T>(callback: () => T, options?: WaitForOptions) => Promise<T>;
|
|
4
|
+
type ElementOrCallback = Gtk.Widget | (() => Gtk.Widget | null);
|
|
5
|
+
export declare const waitForElementToBeRemoved: (elementOrCallback: ElementOrCallback, options?: WaitForOptions) => Promise<void>;
|
|
6
|
+
export {};
|
package/dist/wait-for.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEFAULT_TIMEOUT = 1000;
|
|
2
2
|
const DEFAULT_INTERVAL = 50;
|
|
3
3
|
export const waitFor = async (callback, options) => {
|
|
4
|
-
const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL } = options ?? {};
|
|
4
|
+
const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
5
5
|
const startTime = Date.now();
|
|
6
6
|
let lastError = null;
|
|
7
7
|
while (Date.now() - startTime < timeout) {
|
|
@@ -13,5 +13,47 @@ export const waitFor = async (callback, options) => {
|
|
|
13
13
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
const timeoutError = new Error(`Timed out after ${timeout}ms. Last error: ${lastError?.message}`);
|
|
17
|
+
if (onTimeout) {
|
|
18
|
+
throw onTimeout(timeoutError);
|
|
19
|
+
}
|
|
20
|
+
throw timeoutError;
|
|
21
|
+
};
|
|
22
|
+
const getElement = (elementOrCallback) => {
|
|
23
|
+
if (typeof elementOrCallback === "function") {
|
|
24
|
+
return elementOrCallback();
|
|
25
|
+
}
|
|
26
|
+
return elementOrCallback;
|
|
27
|
+
};
|
|
28
|
+
const isElementRemoved = (element) => {
|
|
29
|
+
if (element === null)
|
|
30
|
+
return true;
|
|
31
|
+
try {
|
|
32
|
+
const parent = element.getParent();
|
|
33
|
+
return parent === null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const waitForElementToBeRemoved = async (elementOrCallback, options) => {
|
|
40
|
+
const { timeout = DEFAULT_TIMEOUT, interval = DEFAULT_INTERVAL, onTimeout } = options ?? {};
|
|
41
|
+
const initialElement = getElement(elementOrCallback);
|
|
42
|
+
if (initialElement === null) {
|
|
43
|
+
throw new Error("The element(s) given to waitForElementToBeRemoved are already removed. " +
|
|
44
|
+
"waitForElementToBeRemoved requires that the element is present before waiting for removal.");
|
|
45
|
+
}
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
while (Date.now() - startTime < timeout) {
|
|
48
|
+
const element = getElement(elementOrCallback);
|
|
49
|
+
if (isElementRemoved(element)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
53
|
+
}
|
|
54
|
+
const timeoutError = new Error(`Timed out after ${timeout}ms waiting for element to be removed.`);
|
|
55
|
+
if (onTimeout) {
|
|
56
|
+
throw onTimeout(timeoutError);
|
|
57
|
+
}
|
|
58
|
+
throw timeoutError;
|
|
17
59
|
};
|
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
export declare const getWidgetPtr: (widget: Gtk.Widget) => unknown;
|
|
3
|
+
type WidgetWithSetText = {
|
|
4
|
+
setText: (text: string) => void;
|
|
5
|
+
};
|
|
6
|
+
type WidgetWithGetText = {
|
|
7
|
+
getText: () => string;
|
|
8
|
+
};
|
|
9
|
+
export declare const hasSetText: (widget: unknown) => widget is WidgetWithSetText;
|
|
10
|
+
export declare const hasGetText: (widget: unknown) => widget is WidgetWithGetText;
|
|
11
|
+
export {};
|
package/dist/widget.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Testing utilities for GTKX applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@gtkx/ffi": "0.1.
|
|
40
|
-
"@gtkx/native": "0.1.
|
|
41
|
-
"@gtkx/react": "0.1.
|
|
39
|
+
"@gtkx/ffi": "0.1.22",
|
|
40
|
+
"@gtkx/native": "0.1.22",
|
|
41
|
+
"@gtkx/react": "0.1.22"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^19"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "tsc -b",
|
|
48
|
-
"test": "xvfb-run -a vitest run"
|
|
47
|
+
"build": "tsc -b && cp ../../README.md .",
|
|
48
|
+
"test": "GDK_BACKEND=x11 xvfb-run -a vitest run"
|
|
49
49
|
}
|
|
50
50
|
}
|