@gtkx/vitest 0.14.0 → 0.16.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/README.md +28 -27
- package/dist/plugin.d.ts +1 -2
- package/dist/plugin.js +1 -107
- package/dist/setup.js +27 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,36 +31,36 @@ npm run dev
|
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
33
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
GtkApplicationWindow,
|
|
35
|
+
GtkBox,
|
|
36
|
+
GtkButton,
|
|
37
|
+
GtkLabel,
|
|
38
|
+
quit,
|
|
39
|
+
render,
|
|
40
40
|
} from "@gtkx/react";
|
|
41
41
|
import * as Gtk from "@gtkx/ffi/gtk";
|
|
42
42
|
import { useState } from "react";
|
|
43
43
|
|
|
44
44
|
const App = () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
const [count, setCount] = useState(0);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<GtkApplicationWindow
|
|
49
|
+
title="Counter"
|
|
50
|
+
defaultWidth={300}
|
|
51
|
+
defaultHeight={200}
|
|
52
|
+
onClose={quit}
|
|
53
|
+
>
|
|
54
|
+
<GtkBox
|
|
55
|
+
orientation={Gtk.Orientation.VERTICAL}
|
|
56
|
+
spacing={20}
|
|
57
|
+
valign={Gtk.Align.CENTER}
|
|
58
|
+
>
|
|
59
|
+
<GtkLabel label={`Count: ${count}`} cssClasses={["title-1"]} />
|
|
60
|
+
<GtkButton label="Increment" onClicked={() => setCount((c) => c + 1)} />
|
|
61
|
+
</GtkBox>
|
|
62
|
+
</GtkApplicationWindow>
|
|
63
|
+
);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
render(<App />, "com.example.counter");
|
|
@@ -71,6 +71,7 @@ render(<App />, "com.example.counter");
|
|
|
71
71
|
- **React 19** — Hooks, concurrent features, and the component model you know
|
|
72
72
|
- **Native GTK4 widgets** — Real native controls, not web components in a webview
|
|
73
73
|
- **Adwaita support** — Modern GNOME styling with Libadwaita components
|
|
74
|
+
- **Declarative animations** — Framer Motion-like API using native Adwaita animations
|
|
74
75
|
- **Hot Module Replacement** — Fast refresh during development
|
|
75
76
|
- **TypeScript first** — Full type safety with auto-generated bindings
|
|
76
77
|
- **CSS-in-JS styling** — Familiar styling patterns adapted for GTK
|
|
@@ -80,11 +81,11 @@ render(<App />, "com.example.counter");
|
|
|
80
81
|
|
|
81
82
|
Explore complete applications in the [`examples/`](./examples) directory:
|
|
82
83
|
|
|
83
|
-
- **[browser](./examples/browser)** — Simple browser using WebKitWebView
|
|
84
84
|
- **[gtk-demo](./examples/gtk-demo)** — Full replica of the official GTK demo app
|
|
85
85
|
- **[hello-world](./examples/hello-world)** — Minimal application showing a counter
|
|
86
86
|
- **[todo](./examples/todo)** — Full-featured todo application with Adwaita styling and testing
|
|
87
|
-
- **[x-showcase](./examples/x-showcase)** — Showcase of all x
|
|
87
|
+
- **[x-showcase](./examples/x-showcase)** — Showcase of all x.\* virtual components
|
|
88
|
+
- **[browser](./examples/browser)** — Simple browser using WebKitWebView
|
|
88
89
|
- **[deploying](./examples/deploying)** — Example of packaging and distributing a GTKX app
|
|
89
90
|
|
|
90
91
|
## Documentation
|
package/dist/plugin.d.ts
CHANGED
|
@@ -2,8 +2,7 @@ import type { Plugin } from "vitest/config";
|
|
|
2
2
|
/**
|
|
3
3
|
* Creates the GTKX Vitest plugin for running GTK tests.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Each worker thread gets its own display to avoid interference.
|
|
5
|
+
* Each worker spawns its own Xvfb instance on a PID-based display number.
|
|
7
6
|
*
|
|
8
7
|
* @returns Vitest plugin configuration
|
|
9
8
|
*
|
package/dist/plugin.js
CHANGED
|
@@ -1,47 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { availableParallelism, tmpdir } from "node:os";
|
|
4
1
|
import { join } from "node:path";
|
|
5
|
-
const getRuntimeDir = () => process.env.XDG_RUNTIME_DIR ?? tmpdir();
|
|
6
|
-
const getStateDir = () => join(getRuntimeDir(), `gtkx-vitest-${process.pid}`);
|
|
7
|
-
const getBaseDisplay = () => {
|
|
8
|
-
const slot = process.pid % 500;
|
|
9
|
-
return 50 + slot * 10;
|
|
10
|
-
};
|
|
11
|
-
const waitForDisplay = (display, timeout = 5000) => new Promise((resolve) => {
|
|
12
|
-
const start = Date.now();
|
|
13
|
-
const check = () => {
|
|
14
|
-
const lockFile = `/tmp/.X${display}-lock`;
|
|
15
|
-
if (existsSync(lockFile)) {
|
|
16
|
-
resolve(true);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (Date.now() - start > timeout) {
|
|
20
|
-
resolve(false);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
setTimeout(check, 50);
|
|
24
|
-
};
|
|
25
|
-
check();
|
|
26
|
-
});
|
|
27
|
-
const startXvfb = async (display) => {
|
|
28
|
-
const xvfb = spawn("Xvfb", [`:${display}`, "-screen", "0", "1024x768x24"], {
|
|
29
|
-
stdio: "ignore",
|
|
30
|
-
detached: true,
|
|
31
|
-
});
|
|
32
|
-
xvfb.unref();
|
|
33
|
-
const ready = await waitForDisplay(display);
|
|
34
|
-
if (!ready) {
|
|
35
|
-
xvfb.kill();
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
return xvfb;
|
|
39
|
-
};
|
|
40
2
|
/**
|
|
41
3
|
* Creates the GTKX Vitest plugin for running GTK tests.
|
|
42
4
|
*
|
|
43
|
-
*
|
|
44
|
-
* Each worker thread gets its own display to avoid interference.
|
|
5
|
+
* Each worker spawns its own Xvfb instance on a PID-based display number.
|
|
45
6
|
*
|
|
46
7
|
* @returns Vitest plugin configuration
|
|
47
8
|
*
|
|
@@ -58,74 +19,10 @@ const startXvfb = async (display) => {
|
|
|
58
19
|
*/
|
|
59
20
|
const gtkx = () => {
|
|
60
21
|
const workerSetupPath = join(import.meta.dirname, "setup.js");
|
|
61
|
-
const stateDir = getStateDir();
|
|
62
|
-
const xvfbProcesses = [];
|
|
63
|
-
let handlersRegistered = false;
|
|
64
|
-
let tornDown = false;
|
|
65
|
-
const setup = async (vitest) => {
|
|
66
|
-
const configuredWorkers = vitest.config.maxWorkers;
|
|
67
|
-
const maxWorkers = typeof configuredWorkers === "number" ? configuredWorkers : availableParallelism();
|
|
68
|
-
if (existsSync(stateDir)) {
|
|
69
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
70
|
-
}
|
|
71
|
-
mkdirSync(stateDir, { recursive: true });
|
|
72
|
-
const baseDisplay = getBaseDisplay();
|
|
73
|
-
const displays = [];
|
|
74
|
-
const results = await Promise.all(Array.from({ length: maxWorkers }, (_, i) => startXvfb(baseDisplay + i)));
|
|
75
|
-
for (let i = 0; i < results.length; i++) {
|
|
76
|
-
const xvfb = results[i];
|
|
77
|
-
if (xvfb) {
|
|
78
|
-
xvfbProcesses.push(xvfb);
|
|
79
|
-
displays.push(baseDisplay + i);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (displays.length === 0) {
|
|
83
|
-
throw new Error("Failed to start any Xvfb instances");
|
|
84
|
-
}
|
|
85
|
-
for (const display of displays) {
|
|
86
|
-
writeFileSync(join(stateDir, `display-${display}.available`), "");
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
const teardown = () => {
|
|
90
|
-
if (tornDown) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
tornDown = true;
|
|
94
|
-
for (const xvfb of xvfbProcesses) {
|
|
95
|
-
try {
|
|
96
|
-
xvfb.kill("SIGTERM");
|
|
97
|
-
}
|
|
98
|
-
catch { }
|
|
99
|
-
}
|
|
100
|
-
if (existsSync(stateDir)) {
|
|
101
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
const reporter = {
|
|
105
|
-
onInit() {
|
|
106
|
-
if (handlersRegistered) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
handlersRegistered = true;
|
|
110
|
-
process.on("exit", teardown);
|
|
111
|
-
process.on("SIGTERM", teardown);
|
|
112
|
-
process.on("SIGINT", teardown);
|
|
113
|
-
},
|
|
114
|
-
async onTestRunStart(specifications) {
|
|
115
|
-
const firstSpec = specifications[0];
|
|
116
|
-
if (firstSpec && xvfbProcesses.length === 0) {
|
|
117
|
-
await setup(firstSpec.project.vitest);
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
onTestRunEnd() {
|
|
121
|
-
teardown();
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
22
|
return {
|
|
125
23
|
name: "gtkx",
|
|
126
24
|
config(config) {
|
|
127
25
|
const setupFiles = config.test?.setupFiles ?? [];
|
|
128
|
-
process.env.GTKX_STATE_DIR = stateDir;
|
|
129
26
|
return {
|
|
130
27
|
test: {
|
|
131
28
|
setupFiles: [workerSetupPath, ...(Array.isArray(setupFiles) ? setupFiles : [setupFiles])],
|
|
@@ -133,9 +30,6 @@ const gtkx = () => {
|
|
|
133
30
|
},
|
|
134
31
|
};
|
|
135
32
|
},
|
|
136
|
-
configureVitest({ vitest }) {
|
|
137
|
-
vitest.config.reporters.push(reporter);
|
|
138
|
-
},
|
|
139
33
|
};
|
|
140
34
|
};
|
|
141
35
|
export default gtkx;
|
package/dist/setup.js
CHANGED
|
@@ -1,61 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!existsSync(GTKX_STATE_DIR)) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
const files = readdirSync(GTKX_STATE_DIR).filter((f) => f.endsWith(".available"));
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
const display = Number.parseInt(file.replace("display-", "").replace(".available", ""), 10);
|
|
20
|
-
const availablePath = join(GTKX_STATE_DIR, file);
|
|
21
|
-
const claimedPath = join(GTKX_STATE_DIR, `display-${display}.claimed-${process.pid}`);
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { beforeAll } from "vitest";
|
|
4
|
+
const display = 100 + (process.pid % 5000);
|
|
5
|
+
const socketPath = `/tmp/.X11-unix/X${display}`;
|
|
6
|
+
const xvfb = spawn("Xvfb", [`:${display}`, "-screen", "0", "1024x768x24"], {
|
|
7
|
+
stdio: "ignore",
|
|
8
|
+
detached: true,
|
|
9
|
+
});
|
|
10
|
+
const xvfbPid = xvfb.pid;
|
|
11
|
+
xvfb.unref();
|
|
12
|
+
const killXvfb = () => {
|
|
13
|
+
if (xvfbPid !== undefined) {
|
|
22
14
|
try {
|
|
23
|
-
|
|
24
|
-
return display;
|
|
15
|
+
process.kill(xvfbPid, "SIGTERM");
|
|
25
16
|
}
|
|
26
17
|
catch { }
|
|
27
18
|
}
|
|
28
|
-
return null;
|
|
29
|
-
};
|
|
30
|
-
const claimDisplay = () => {
|
|
31
|
-
for (let attempt = 0; attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
|
|
32
|
-
const display = tryClaimDisplay();
|
|
33
|
-
if (display !== null) {
|
|
34
|
-
return display;
|
|
35
|
-
}
|
|
36
|
-
sleepSync(CLAIM_RETRY_DELAY_MS);
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
19
|
};
|
|
40
|
-
|
|
41
|
-
const claimedPath = join(GTKX_STATE_DIR, `display-${display}.claimed-${process.pid}`);
|
|
42
|
-
const availablePath = join(GTKX_STATE_DIR, `display-${display}.available`);
|
|
43
|
-
try {
|
|
44
|
-
renameSync(claimedPath, availablePath);
|
|
45
|
-
}
|
|
46
|
-
catch { }
|
|
47
|
-
};
|
|
48
|
-
const display = claimDisplay();
|
|
49
|
-
if (display === null) {
|
|
50
|
-
throw new Error("Failed to claim display - ensure gtkx plugin is configured");
|
|
51
|
-
}
|
|
20
|
+
process.on("exit", killXvfb);
|
|
52
21
|
process.env.GDK_BACKEND = "x11";
|
|
53
22
|
process.env.GSK_RENDERER = "cairo";
|
|
54
23
|
process.env.LIBGL_ALWAYS_SOFTWARE = "1";
|
|
55
24
|
process.env.DISPLAY = `:${display}`;
|
|
56
|
-
const
|
|
57
|
-
|
|
25
|
+
const waitForDisplay = async (timeout = 5000) => {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
while (Date.now() - start < timeout) {
|
|
28
|
+
if (existsSync(socketPath)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Xvfb display :${display} did not become available within ${timeout}ms`);
|
|
58
34
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
await waitForDisplay();
|
|
37
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/vitest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Vitest plugin for GTKX applications with Xvfb display isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"vitest": "^4.0.
|
|
39
|
+
"vitest": "^4.0.18"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"vitest": ">=4"
|