@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 CHANGED
@@ -31,36 +31,36 @@ npm run dev
31
31
 
32
32
  ```tsx
33
33
  import {
34
- GtkApplicationWindow,
35
- GtkBox,
36
- GtkButton,
37
- GtkLabel,
38
- quit,
39
- render,
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
- 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
- );
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.* virtual components
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
- * Manages Xvfb virtual display instances for headless GTK testing.
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
- * Manages Xvfb virtual display instances for headless GTK testing.
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 { existsSync, readdirSync, renameSync } from "node:fs";
2
- import { join } from "node:path";
3
- const GTKX_STATE_DIR = process.env.GTKX_STATE_DIR;
4
- const MAX_CLAIM_ATTEMPTS = 100;
5
- const CLAIM_RETRY_DELAY_MS = 100;
6
- if (!GTKX_STATE_DIR) {
7
- throw new Error("GTKX_STATE_DIR not set - gtkx plugin must be used");
8
- }
9
- const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
10
- const sleepSync = (ms) => {
11
- Atomics.wait(sleepBuffer, 0, 0, ms);
12
- };
13
- const tryClaimDisplay = () => {
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
- renameSync(availablePath, claimedPath);
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
- const releaseDisplay = (display) => {
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 cleanup = () => {
57
- releaseDisplay(display);
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
- process.on("exit", cleanup);
60
- process.on("SIGTERM", cleanup);
61
- process.on("SIGINT", cleanup);
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.14.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.17"
39
+ "vitest": "^4.0.18"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "vitest": ">=4"