@gtkx/testing 0.3.1 → 0.3.3
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 +53 -157
- package/dist/fire-event.d.ts +3 -3
- package/dist/fire-event.js +5 -3
- package/dist/render.js +1 -2
- package/dist/timing.d.ts +3 -0
- package/dist/timing.js +3 -0
- package/dist/user-event.js +7 -13
- package/dist/widget.d.ts +6 -0
- package/dist/widget.js +6 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,242 +5,138 @@
|
|
|
5
5
|
<h1 align="center">GTKX</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>Build native GTK4 desktop
|
|
8
|
+
<strong>Build native GTK4 desktop apps with React</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://eugeniodepalo.github.io/gtkx">Documentation</a> ·
|
|
13
13
|
<a href="#quick-start">Quick Start</a> ·
|
|
14
|
-
<a href="#examples">Examples</a>
|
|
14
|
+
<a href="#examples">Examples</a> ·
|
|
15
|
+
<a href="#contributing">Contributing</a>
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
19
|
-
GTKX
|
|
20
|
+
GTKX lets you build native Linux desktop applications using React and TypeScript. Write familiar React code that renders as native GTK4 widgets—no Electron, no web views.
|
|
20
21
|
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
|
-
- **React
|
|
24
|
-
- **Hot
|
|
25
|
-
- **Native
|
|
26
|
-
- **CLI
|
|
27
|
-
- **CSS-in-JS
|
|
28
|
-
- **Testing
|
|
24
|
+
- **React** — Hooks, state, props, and components you already know
|
|
25
|
+
- **Hot Reload** — Edit code and see changes instantly via Vite
|
|
26
|
+
- **Native** — Direct FFI bindings to GTK4 via Rust and libffi
|
|
27
|
+
- **CLI** — `npx @gtkx/cli@latest create` scaffolds a ready-to-go project
|
|
28
|
+
- **CSS-in-JS** — Emotion-style `css` template literals for GTK styling
|
|
29
|
+
- **Testing** — Testing Library-style `screen`, `userEvent`, and queries
|
|
29
30
|
|
|
30
31
|
## Quick Start
|
|
31
32
|
|
|
32
|
-
Create a new GTKX app with a single command:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx @gtkx/cli@latest create
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
This launches an interactive wizard that sets up your project with TypeScript, your preferred package manager, and optional testing support.
|
|
39
|
-
|
|
40
|
-
You can also pass options directly:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npx @gtkx/cli@latest create my-app --app-id com.example.myapp --pm pnpm --testing vitest
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Then start developing with HMR:
|
|
47
|
-
|
|
48
33
|
```bash
|
|
34
|
+
npx @gtkx/cli@latest create my-app
|
|
49
35
|
cd my-app
|
|
50
36
|
npm run dev
|
|
51
37
|
```
|
|
52
38
|
|
|
53
|
-
Edit your code and see changes instantly
|
|
54
|
-
|
|
55
|
-
### Manual Setup
|
|
39
|
+
Edit your code and see changes instantly—no restart needed.
|
|
56
40
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm install @gtkx/cli @gtkx/react @gtkx/ffi react
|
|
61
|
-
npm install -D @types/react typescript
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Create your first app:
|
|
41
|
+
### Example
|
|
65
42
|
|
|
66
43
|
```tsx
|
|
67
|
-
|
|
44
|
+
import { render, ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
45
|
+
import { Orientation } from "@gtkx/ffi/gtk";
|
|
68
46
|
import { useState } from "react";
|
|
69
|
-
import * as Gtk from "@gtkx/ffi/gtk";
|
|
70
|
-
import { ApplicationWindow, Box, Button, Label, quit } from "@gtkx/react";
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
const App = () => {
|
|
73
49
|
const [count, setCount] = useState(0);
|
|
74
50
|
|
|
75
51
|
return (
|
|
76
|
-
<ApplicationWindow
|
|
77
|
-
|
|
78
|
-
defaultWidth={400}
|
|
79
|
-
defaultHeight={300}
|
|
80
|
-
onCloseRequest={quit}
|
|
81
|
-
>
|
|
82
|
-
<Box
|
|
83
|
-
orientation={Gtk.Orientation.VERTICAL}
|
|
84
|
-
spacing={12}
|
|
85
|
-
marginStart={20}
|
|
86
|
-
marginEnd={20}
|
|
87
|
-
marginTop={20}
|
|
88
|
-
marginBottom={20}
|
|
89
|
-
>
|
|
52
|
+
<ApplicationWindow title="Counter" onCloseRequest={quit}>
|
|
53
|
+
<Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
|
|
90
54
|
<Label.Root label={`Count: ${count}`} />
|
|
91
55
|
<Button label="Increment" onClicked={() => setCount((c) => c + 1)} />
|
|
92
56
|
</Box>
|
|
93
57
|
</ApplicationWindow>
|
|
94
58
|
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export const appId = "org.example.MyApp";
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
```tsx
|
|
101
|
-
// src/index.tsx
|
|
102
|
-
import { render } from "@gtkx/react";
|
|
103
|
-
import App, { appId } from "./app.js";
|
|
104
|
-
|
|
105
|
-
render(<App />, appId);
|
|
106
|
-
```
|
|
59
|
+
};
|
|
107
60
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npx gtkx dev src/app.tsx
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Or without HMR (production):
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
npx tsc -b && node dist/index.js
|
|
61
|
+
render(<App />, "org.example.Counter");
|
|
118
62
|
```
|
|
119
63
|
|
|
120
64
|
## Styling
|
|
121
65
|
|
|
122
|
-
Use `@gtkx/css` for CSS-in-JS styling:
|
|
123
|
-
|
|
124
66
|
```tsx
|
|
125
67
|
import { css } from "@gtkx/css";
|
|
126
68
|
import { Button } from "@gtkx/react";
|
|
127
69
|
|
|
128
|
-
const
|
|
70
|
+
const primary = css`
|
|
129
71
|
padding: 16px 32px;
|
|
130
72
|
border-radius: 24px;
|
|
131
73
|
background: linear-gradient(135deg, #3584e4, #9141ac);
|
|
132
74
|
color: white;
|
|
133
|
-
font-weight: bold;
|
|
134
75
|
`;
|
|
135
76
|
|
|
136
|
-
|
|
77
|
+
<Button label="Click me" cssClasses={[primary]} />
|
|
137
78
|
```
|
|
138
79
|
|
|
139
|
-
GTK also provides built-in
|
|
80
|
+
GTK also provides built-in classes like `suggested-action`, `destructive-action`, `card`, and `heading`.
|
|
140
81
|
|
|
141
82
|
## Testing
|
|
142
83
|
|
|
143
|
-
Use `@gtkx/testing` for Testing Library-style component tests:
|
|
144
|
-
|
|
145
84
|
```tsx
|
|
146
|
-
import { cleanup, render, screen, userEvent
|
|
85
|
+
import { cleanup, render, screen, userEvent } from "@gtkx/testing";
|
|
147
86
|
import { AccessibleRole } from "@gtkx/ffi/gtk";
|
|
148
|
-
import { App } from "./app.js";
|
|
149
87
|
|
|
150
|
-
|
|
151
|
-
afterEach(async () => {
|
|
152
|
-
await cleanup();
|
|
153
|
-
});
|
|
88
|
+
afterEach(() => cleanup());
|
|
154
89
|
|
|
155
|
-
test("increments count
|
|
90
|
+
test("increments count", async () => {
|
|
156
91
|
await render(<App />);
|
|
157
92
|
|
|
158
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
159
|
-
name: "Increment",
|
|
160
|
-
});
|
|
93
|
+
const button = await screen.findByRole(AccessibleRole.BUTTON, { name: "Increment" });
|
|
161
94
|
await userEvent.click(button);
|
|
162
95
|
|
|
163
96
|
await screen.findByText("Count: 1");
|
|
164
97
|
});
|
|
165
|
-
|
|
166
|
-
test("can also use fireEvent for low-level signals", async () => {
|
|
167
|
-
await render(<App />);
|
|
168
|
-
|
|
169
|
-
const button = await screen.findByRole(AccessibleRole.BUTTON, {
|
|
170
|
-
name: "Increment",
|
|
171
|
-
});
|
|
172
|
-
fireEvent(button, "clicked");
|
|
173
|
-
|
|
174
|
-
await screen.findByText("Count: 1");
|
|
175
|
-
});
|
|
176
98
|
```
|
|
177
99
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
**Queries** - Find elements in the rendered tree (all async):
|
|
181
|
-
|
|
182
|
-
- `findBy*` / `findAllBy*` - Waits for element to appear
|
|
183
|
-
|
|
184
|
-
Query types: `ByRole`, `ByText`, `ByLabelText`, `ByTestId`
|
|
185
|
-
|
|
186
|
-
**User Interactions**:
|
|
100
|
+
Queries: `findByRole`, `findByText`, `findByLabelText`, `findByTestId`
|
|
187
101
|
|
|
188
|
-
|
|
189
|
-
- `userEvent.dblClick(element)` - Simulate double click
|
|
190
|
-
- `userEvent.activate(element)` - Activate element (e.g., press Enter in input)
|
|
191
|
-
- `userEvent.type(element, text)` - Type text into input
|
|
192
|
-
- `userEvent.clear(element)` - Clear input text
|
|
193
|
-
- `userEvent.tab(element, options?)` - Simulate Tab navigation
|
|
194
|
-
- `userEvent.selectOptions(element, values)` - Select options in ComboBox/ListBox
|
|
195
|
-
- `userEvent.deselectOptions(element, values)` - Deselect options in ListBox
|
|
196
|
-
|
|
197
|
-
**Low-level Events**:
|
|
198
|
-
|
|
199
|
-
- `fireEvent(element, signalName, ...args)` - Emit any GTK signal with optional arguments
|
|
200
|
-
|
|
201
|
-
**Utilities**:
|
|
202
|
-
|
|
203
|
-
- `waitFor(callback)` - Wait for condition
|
|
204
|
-
- `waitForElementToBeRemoved(element)` - Wait for element removal
|
|
102
|
+
User events: `click`, `dblClick`, `type`, `clear`, `tab`, `selectOptions`
|
|
205
103
|
|
|
206
104
|
## Examples
|
|
207
105
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
106
|
+
| Example | Description |
|
|
107
|
+
| ------- | ----------- |
|
|
108
|
+
| [gtk4-demo](examples/gtk4-demo) | Widget showcase |
|
|
109
|
+
| [todo](examples/todo) | Todo app with tests |
|
|
211
110
|
|
|
212
111
|
```bash
|
|
213
|
-
cd examples/gtk4-demo
|
|
214
|
-
pnpm dev
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Todo App
|
|
218
|
-
|
|
219
|
-
A todo app demonstrating `@gtkx/testing` with realistic component tests:
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
cd examples/todo
|
|
223
|
-
pnpm dev
|
|
224
|
-
pnpm test
|
|
112
|
+
cd examples/gtk4-demo && pnpm dev
|
|
225
113
|
```
|
|
226
114
|
|
|
227
115
|
## Packages
|
|
228
116
|
|
|
229
|
-
| Package
|
|
230
|
-
|
|
|
231
|
-
| [@gtkx/cli](packages/cli)
|
|
232
|
-
| [@gtkx/react](packages/react)
|
|
233
|
-
| [@gtkx/ffi](packages/ffi)
|
|
234
|
-
| [@gtkx/native](packages/native)
|
|
235
|
-
| [@gtkx/css](packages/css)
|
|
236
|
-
| [@gtkx/testing](packages/testing) | Testing utilities
|
|
237
|
-
| [@gtkx/gir](packages/gir)
|
|
117
|
+
| Package | Description |
|
|
118
|
+
| ------- | ----------- |
|
|
119
|
+
| [@gtkx/cli](packages/cli) | CLI with HMR dev server |
|
|
120
|
+
| [@gtkx/react](packages/react) | React reconciler and JSX components |
|
|
121
|
+
| [@gtkx/ffi](packages/ffi) | TypeScript bindings for GTK4/GLib/GIO |
|
|
122
|
+
| [@gtkx/native](packages/native) | Rust native module (libffi bridge) |
|
|
123
|
+
| [@gtkx/css](packages/css) | CSS-in-JS styling |
|
|
124
|
+
| [@gtkx/testing](packages/testing) | Testing utilities |
|
|
125
|
+
| [@gtkx/gir](packages/gir) | GObject Introspection parser |
|
|
238
126
|
|
|
239
127
|
## Requirements
|
|
240
128
|
|
|
241
129
|
- Node.js 20+
|
|
242
|
-
- GTK4
|
|
243
|
-
- Linux
|
|
130
|
+
- GTK4 (`gtk4-devel` on Fedora, `libgtk-4-dev` on Ubuntu)
|
|
131
|
+
- Linux
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
136
|
+
|
|
137
|
+
- [Report a bug](https://github.com/eugeniodepalo/gtkx/issues/new?template=bug_report.md)
|
|
138
|
+
- [Request a feature](https://github.com/eugeniodepalo/gtkx/issues/new?template=feature_request.md)
|
|
139
|
+
- [Read the Code of Conduct](CODE_OF_CONDUCT.md)
|
|
244
140
|
|
|
245
141
|
## License
|
|
246
142
|
|
package/dist/fire-event.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ import type { Arg } from "@gtkx/native";
|
|
|
9
9
|
* @param args - Additional arguments to pass to the signal handlers
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
|
-
* fireEvent(button, "clicked")
|
|
12
|
+
* await fireEvent(button, "clicked")
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
-
* fireEvent(widget, "custom-signal", { type: { type: "int", size: 32 }, value: 42 })
|
|
15
|
+
* await fireEvent(widget, "custom-signal", { type: { type: "int", size: 32 }, value: 42 })
|
|
16
16
|
*/
|
|
17
|
-
export declare const fireEvent: (element: Gtk.Widget, signalName: string, ...args: Arg[]) => void
|
|
17
|
+
export declare const fireEvent: (element: Gtk.Widget, signalName: string, ...args: Arg[]) => Promise<void>;
|
package/dist/fire-event.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { call } from "@gtkx/native";
|
|
2
|
+
import { tick } from "./timing.js";
|
|
2
3
|
/**
|
|
3
4
|
* Low-level utility to emit GTK signals on widgets. For common interactions
|
|
4
5
|
* like clicking or typing, use `userEvent` instead.
|
|
@@ -8,11 +9,12 @@ import { call } from "@gtkx/native";
|
|
|
8
9
|
* @param args - Additional arguments to pass to the signal handlers
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
|
-
* fireEvent(button, "clicked")
|
|
12
|
+
* await fireEvent(button, "clicked")
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
|
-
* fireEvent(widget, "custom-signal", { type: { type: "int", size: 32 }, value: 42 })
|
|
15
|
+
* await fireEvent(widget, "custom-signal", { type: { type: "int", size: 32 }, value: 42 })
|
|
15
16
|
*/
|
|
16
|
-
export const fireEvent = (element, signalName, ...args) => {
|
|
17
|
+
export const fireEvent = async (element, signalName, ...args) => {
|
|
17
18
|
call("libgobject-2.0.so.0", "g_signal_emit_by_name", [{ type: { type: "gobject" }, value: element.id }, { type: { type: "string" }, value: signalName }, ...args], { type: "undefined" });
|
|
19
|
+
await tick();
|
|
18
20
|
};
|
package/dist/render.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { getCurrentApp, getInterface, start, stop } from "@gtkx/ffi";
|
|
3
3
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
4
|
-
import { ApplicationWindow, reconciler } from "@gtkx/react";
|
|
4
|
+
import { ApplicationWindow, ROOT_NODE_CONTAINER, reconciler } from "@gtkx/react";
|
|
5
5
|
import * as queries from "./queries.js";
|
|
6
6
|
import { setScreenRoot } from "./screen.js";
|
|
7
7
|
import { tick } from "./timing.js";
|
|
8
8
|
import { hasLabel } from "./widget.js";
|
|
9
|
-
const ROOT_NODE_CONTAINER = Symbol.for("ROOT_NODE_CONTAINER");
|
|
10
9
|
const APP_ID = "com.gtkx.testing";
|
|
11
10
|
let container = null;
|
|
12
11
|
const getWidgetLabel = (widget) => {
|
package/dist/timing.d.ts
CHANGED
package/dist/timing.js
CHANGED
package/dist/user-event.js
CHANGED
|
@@ -19,26 +19,20 @@ const click = async (element) => {
|
|
|
19
19
|
const toggleButton = element;
|
|
20
20
|
toggleButton.setActive(!toggleButton.getActive());
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
await tick();
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
|
-
fireEvent(element, "clicked");
|
|
25
|
+
await fireEvent(element, "clicked");
|
|
26
26
|
}
|
|
27
|
-
await tick();
|
|
28
27
|
};
|
|
29
28
|
const dblClick = async (element) => {
|
|
30
|
-
fireEvent(element, "clicked");
|
|
31
|
-
await
|
|
32
|
-
fireEvent(element, "clicked");
|
|
33
|
-
await tick();
|
|
29
|
+
await fireEvent(element, "clicked");
|
|
30
|
+
await fireEvent(element, "clicked");
|
|
34
31
|
};
|
|
35
32
|
const tripleClick = async (element) => {
|
|
36
|
-
fireEvent(element, "clicked");
|
|
37
|
-
await
|
|
38
|
-
fireEvent(element, "clicked");
|
|
39
|
-
await tick();
|
|
40
|
-
fireEvent(element, "clicked");
|
|
41
|
-
await tick();
|
|
33
|
+
await fireEvent(element, "clicked");
|
|
34
|
+
await fireEvent(element, "clicked");
|
|
35
|
+
await fireEvent(element, "clicked");
|
|
42
36
|
};
|
|
43
37
|
const activate = async (element) => {
|
|
44
38
|
element.activate();
|
package/dist/widget.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import type * as Gtk from "@gtkx/ffi/gtk";
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a widget has an editable accessible role (text box, search box, or spin button).
|
|
4
|
+
*/
|
|
2
5
|
export declare const isEditable: (widget: Gtk.Widget) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a widget has an accessible role that supports labels.
|
|
8
|
+
*/
|
|
3
9
|
export declare const hasLabel: (widget: Gtk.Widget) => boolean;
|
package/dist/widget.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { getInterface } from "@gtkx/ffi";
|
|
2
2
|
import { Accessible, AccessibleRole } from "@gtkx/ffi/gtk";
|
|
3
3
|
const EDITABLE_ROLES = new Set([AccessibleRole.TEXT_BOX, AccessibleRole.SEARCH_BOX, AccessibleRole.SPIN_BUTTON]);
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a widget has an editable accessible role (text box, search box, or spin button).
|
|
6
|
+
*/
|
|
4
7
|
export const isEditable = (widget) => {
|
|
5
8
|
const role = getInterface(widget, Accessible).getAccessibleRole();
|
|
6
9
|
return EDITABLE_ROLES.has(role);
|
|
@@ -15,6 +18,9 @@ const LABEL_ROLES = new Set([
|
|
|
15
18
|
AccessibleRole.MENU_ITEM_CHECKBOX,
|
|
16
19
|
AccessibleRole.MENU_ITEM_RADIO,
|
|
17
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a widget has an accessible role that supports labels.
|
|
23
|
+
*/
|
|
18
24
|
export const hasLabel = (widget) => {
|
|
19
25
|
const role = getInterface(widget, Accessible).getAccessibleRole();
|
|
20
26
|
return LABEL_ROLES.has(role);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/testing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Testing utilities for GTKX applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@gtkx/ffi": "0.3.
|
|
36
|
-
"@gtkx/
|
|
37
|
-
"@gtkx/
|
|
35
|
+
"@gtkx/ffi": "0.3.3",
|
|
36
|
+
"@gtkx/react": "0.3.3",
|
|
37
|
+
"@gtkx/native": "0.3.3"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc -b && cp ../../README.md .",
|