@gtkx/vitest 0.10.4 → 0.11.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 +103 -0
- package/dist/index.d.ts +0 -1
- package/dist/plugin.d.ts +1 -32
- package/dist/plugin.js +107 -48
- package/dist/{worker-setup.js → setup.js} +14 -12
- package/package.json +4 -4
- package/dist/global-setup.d.ts +0 -3
- package/dist/global-setup.js +0 -89
- /package/dist/{worker-setup.d.ts → setup.d.ts} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/eugeniodepalo/gtkx/main/logo.svg" alt="GTKX" width="60" height="60">
|
|
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://www.npmjs.com/package/@gtkx/react"><img src="https://img.shields.io/npm/v/@gtkx/react.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/eugeniodepalo/gtkx/actions"><img src="https://img.shields.io/github/actions/workflow/status/eugeniodepalo/gtkx/ci.yml" alt="CI"></a>
|
|
14
|
+
<a href="https://github.com/eugeniodepalo/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License"></a>
|
|
15
|
+
<a href="https://github.com/eugeniodepalo/gtkx/discussions"><img src="https://img.shields.io/badge/discussions-GitHub-blue" alt="GitHub Discussions"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
GTKX lets you write Linux desktop applications using React. Your components render as native GTK4 widgets through a Rust FFI bridge—no webviews, no Electron, just native performance with the developer experience you already know.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @gtkx/cli create my-app
|
|
26
|
+
cd my-app
|
|
27
|
+
npm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import {
|
|
34
|
+
GtkApplicationWindow,
|
|
35
|
+
GtkBox,
|
|
36
|
+
GtkButton,
|
|
37
|
+
GtkLabel,
|
|
38
|
+
quit,
|
|
39
|
+
render,
|
|
40
|
+
} from "@gtkx/react";
|
|
41
|
+
import * as Gtk from "@gtkx/ffi/gtk";
|
|
42
|
+
import { useState } from "react";
|
|
43
|
+
|
|
44
|
+
const App = () => {
|
|
45
|
+
const [count, setCount] = useState(0);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<GtkApplicationWindow
|
|
49
|
+
title="Counter"
|
|
50
|
+
defaultWidth={300}
|
|
51
|
+
defaultHeight={200}
|
|
52
|
+
onCloseRequest={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
|
+
};
|
|
65
|
+
|
|
66
|
+
render(<App />, "com.example.counter");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **React 19** — Hooks, concurrent features, and the component model you know
|
|
72
|
+
- **Native GTK4 widgets** — Real native controls, not web components in a webview
|
|
73
|
+
- **Adwaita support** — Modern GNOME styling with Libadwaita components
|
|
74
|
+
- **Hot Module Replacement** — Fast refresh during development
|
|
75
|
+
- **TypeScript first** — Full type safety with auto-generated bindings
|
|
76
|
+
- **CSS-in-JS styling** — Familiar styling patterns adapted for GTK
|
|
77
|
+
- **Testing utilities** — Component testing similar to Testing Library
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
Explore complete applications in the [`examples/`](./examples) directory:
|
|
82
|
+
|
|
83
|
+
- **[gtk-demo](./examples/gtk-demo)** — Full replica of the official GTK demo app
|
|
84
|
+
- **[hello-world](./examples/hello-world)** — Minimal application showing a counter
|
|
85
|
+
- **[todo](./examples/todo)** — Full-featured todo application with Adwaita styling and testing
|
|
86
|
+
- **[deploying](./examples/deploying)** — Example of packaging and distributing a GTKX app
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
Visit [https://eugeniodepalo.github.io/gtkx](https://eugeniodepalo.github.io/gtkx/) for the full documentation.
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
Contributions are welcome! Please see the [contributing guidelines](./CONTRIBUTING.md) and check out the [good first issues](https://github.com/eugeniodepalo/gtkx/labels/good%20first%20issue).
|
|
95
|
+
|
|
96
|
+
## Community
|
|
97
|
+
|
|
98
|
+
- [GitHub Discussions](https://github.com/eugeniodepalo/gtkx/discussions) — Questions, ideas, and general discussion
|
|
99
|
+
- [Issue Tracker](https://github.com/eugeniodepalo/gtkx/issues) — Bug reports and feature requests
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[MPL-2.0](./LICENSE)
|
package/dist/index.d.ts
CHANGED
package/dist/plugin.d.ts
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
1
|
import type { Plugin } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Additional setup files to run after the GTKX worker setup.
|
|
5
|
-
* These files will be loaded after the display is configured.
|
|
6
|
-
*/
|
|
7
|
-
setupFiles?: string[];
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Vitest plugin for GTKX applications.
|
|
11
|
-
*
|
|
12
|
-
* This plugin configures Vitest to run GTK tests with proper display isolation:
|
|
13
|
-
* - Starts Xvfb instances for headless display (one per worker)
|
|
14
|
-
* - Sets GTK environment variables automatically
|
|
15
|
-
* - Configures test pool for process isolation
|
|
16
|
-
*
|
|
17
|
-
* When using `@gtkx/testing`, no additional setup is needed - the `render()`
|
|
18
|
-
* function handles GTK application lifecycle automatically.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```typescript
|
|
22
|
-
* // vitest.config.ts
|
|
23
|
-
* import gtkx from "@gtkx/vitest";
|
|
24
|
-
*
|
|
25
|
-
* export default defineConfig({
|
|
26
|
-
* plugins: [gtkx()],
|
|
27
|
-
* test: {
|
|
28
|
-
* include: ["tests/**\/*.test.{ts,tsx}"],
|
|
29
|
-
* },
|
|
30
|
-
* });
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
declare const gtkx: (options?: GtkxOptions) => Plugin;
|
|
2
|
+
declare const gtkx: () => Plugin;
|
|
34
3
|
export default gtkx;
|
package/dist/plugin.js
CHANGED
|
@@ -1,62 +1,121 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
return
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { availableParallelism, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const getStateDir = () => join(tmpdir(), `gtkx-vitest-${process.pid}`);
|
|
6
|
+
const getBaseDisplay = () => {
|
|
7
|
+
const slot = process.pid % 500;
|
|
8
|
+
return 50 + slot * 10;
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
10
|
+
const waitForDisplay = (display, timeout = 5000) => new Promise((resolve) => {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const check = () => {
|
|
13
|
+
const lockFile = `/tmp/.X${display}-lock`;
|
|
14
|
+
if (existsSync(lockFile)) {
|
|
15
|
+
resolve(true);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (Date.now() - start > timeout) {
|
|
19
|
+
resolve(false);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setTimeout(check, 50);
|
|
23
|
+
};
|
|
24
|
+
check();
|
|
25
|
+
});
|
|
26
|
+
const startXvfb = async (display) => {
|
|
27
|
+
const xvfb = spawn("Xvfb", [`:${display}`, "-screen", "0", "1024x768x24"], {
|
|
28
|
+
stdio: "ignore",
|
|
29
|
+
detached: true,
|
|
30
|
+
});
|
|
31
|
+
xvfb.unref();
|
|
32
|
+
const ready = await waitForDisplay(display);
|
|
33
|
+
if (!ready) {
|
|
34
|
+
xvfb.kill();
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return xvfb;
|
|
38
|
+
};
|
|
39
|
+
const gtkx = () => {
|
|
40
|
+
const workerSetupPath = join(import.meta.dirname, "setup.js");
|
|
41
|
+
const stateDir = getStateDir();
|
|
42
|
+
const xvfbProcesses = [];
|
|
43
|
+
let handlersRegistered = false;
|
|
44
|
+
let tornDown = false;
|
|
45
|
+
const setup = async (vitest) => {
|
|
46
|
+
const configuredWorkers = vitest.config.maxWorkers;
|
|
47
|
+
const maxWorkers = typeof configuredWorkers === "number" ? configuredWorkers : availableParallelism();
|
|
48
|
+
if (existsSync(stateDir)) {
|
|
49
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(stateDir, { recursive: true });
|
|
52
|
+
const baseDisplay = getBaseDisplay();
|
|
53
|
+
const displays = [];
|
|
54
|
+
const results = await Promise.all(Array.from({ length: maxWorkers }, (_, i) => startXvfb(baseDisplay + i)));
|
|
55
|
+
for (let i = 0; i < results.length; i++) {
|
|
56
|
+
const xvfb = results[i];
|
|
57
|
+
if (xvfb) {
|
|
58
|
+
xvfbProcesses.push(xvfb);
|
|
59
|
+
displays.push(baseDisplay + i);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (displays.length === 0) {
|
|
63
|
+
throw new Error("Failed to start any Xvfb instances");
|
|
64
|
+
}
|
|
65
|
+
for (const display of displays) {
|
|
66
|
+
writeFileSync(join(stateDir, `display-${display}.available`), "");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const teardown = () => {
|
|
70
|
+
if (tornDown) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
tornDown = true;
|
|
74
|
+
for (const xvfb of xvfbProcesses) {
|
|
75
|
+
try {
|
|
76
|
+
xvfb.kill("SIGTERM");
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
if (existsSync(stateDir)) {
|
|
81
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const reporter = {
|
|
85
|
+
onInit() {
|
|
86
|
+
if (handlersRegistered) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
handlersRegistered = true;
|
|
90
|
+
process.on("exit", teardown);
|
|
91
|
+
process.on("SIGTERM", teardown);
|
|
92
|
+
process.on("SIGINT", teardown);
|
|
93
|
+
},
|
|
94
|
+
async onTestRunStart(specifications) {
|
|
95
|
+
const firstSpec = specifications[0];
|
|
96
|
+
if (firstSpec && xvfbProcesses.length === 0) {
|
|
97
|
+
await setup(firstSpec.project.vitest);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
onTestRunEnd() {
|
|
101
|
+
teardown();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
38
104
|
return {
|
|
39
105
|
name: "gtkx",
|
|
40
106
|
config(config) {
|
|
41
|
-
const
|
|
42
|
-
const projectRoot = config.root ?? process.cwd();
|
|
43
|
-
const stateDir = getStateDir(projectRoot);
|
|
107
|
+
const setupFiles = config.test?.setupFiles ?? [];
|
|
44
108
|
process.env.GTKX_STATE_DIR = stateDir;
|
|
45
109
|
return {
|
|
46
110
|
test: {
|
|
47
|
-
|
|
48
|
-
...(Array.isArray(existingGlobalSetup) ? existingGlobalSetup : [existingGlobalSetup]),
|
|
49
|
-
globalSetupPath,
|
|
50
|
-
],
|
|
51
|
-
setupFiles: [workerSetupPath, ...userSetupFiles],
|
|
111
|
+
setupFiles: [workerSetupPath, ...(Array.isArray(setupFiles) ? setupFiles : [setupFiles])],
|
|
52
112
|
pool: "forks",
|
|
53
|
-
maxWorkers: 1,
|
|
54
|
-
},
|
|
55
|
-
esbuild: {
|
|
56
|
-
jsx: "automatic",
|
|
57
113
|
},
|
|
58
114
|
};
|
|
59
115
|
},
|
|
116
|
+
configureVitest({ vitest }) {
|
|
117
|
+
vitest.config.reporters.push(reporter);
|
|
118
|
+
},
|
|
60
119
|
};
|
|
61
120
|
};
|
|
62
121
|
export default gtkx;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
1
|
import { existsSync, readdirSync, renameSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
const GTKX_STATE_DIR = process.env.GTKX_STATE_DIR;
|
|
@@ -7,8 +6,9 @@ const CLAIM_RETRY_DELAY_MS = 100;
|
|
|
7
6
|
if (!GTKX_STATE_DIR) {
|
|
8
7
|
throw new Error("GTKX_STATE_DIR not set - gtkx plugin must be used");
|
|
9
8
|
}
|
|
9
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
10
10
|
const sleepSync = (ms) => {
|
|
11
|
-
|
|
11
|
+
Atomics.wait(sleepBuffer, 0, 0, ms);
|
|
12
12
|
};
|
|
13
13
|
const tryClaimDisplay = () => {
|
|
14
14
|
if (!existsSync(GTKX_STATE_DIR)) {
|
|
@@ -45,21 +45,23 @@ const releaseDisplay = (display) => {
|
|
|
45
45
|
}
|
|
46
46
|
catch { }
|
|
47
47
|
};
|
|
48
|
-
process.env.GDK_BACKEND = "x11";
|
|
49
|
-
process.env.GSK_RENDERER = "cairo";
|
|
50
|
-
process.env.LIBGL_ALWAYS_SOFTWARE = "1";
|
|
51
|
-
process.env.NO_AT_BRIDGE = "1";
|
|
52
48
|
const display = claimDisplay();
|
|
53
49
|
if (display === null) {
|
|
54
|
-
throw new Error("Failed to claim display -
|
|
50
|
+
throw new Error("Failed to claim display - ensure gtkx plugin is configured");
|
|
55
51
|
}
|
|
52
|
+
process.env.GDK_BACKEND = "x11";
|
|
53
|
+
process.env.GSK_RENDERER = "cairo";
|
|
54
|
+
process.env.LIBGL_ALWAYS_SOFTWARE = "1";
|
|
56
55
|
process.env.DISPLAY = `:${display}`;
|
|
57
|
-
|
|
58
|
-
process.on("SIGTERM", () => {
|
|
56
|
+
const cleanup = () => {
|
|
59
57
|
releaseDisplay(display);
|
|
60
|
-
|
|
58
|
+
};
|
|
59
|
+
process.on("exit", cleanup);
|
|
60
|
+
process.on("SIGTERM", () => {
|
|
61
|
+
cleanup();
|
|
62
|
+
process.exit(143);
|
|
61
63
|
});
|
|
62
64
|
process.on("SIGINT", () => {
|
|
63
|
-
|
|
64
|
-
process.exit(
|
|
65
|
+
cleanup();
|
|
66
|
+
process.exit(130);
|
|
65
67
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/vitest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Vitest plugin for GTKX applications with Xvfb display isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtk",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"vitest": "^4.0.
|
|
35
|
+
"vitest": "^4.0.16"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"vitest": ">=
|
|
38
|
+
"vitest": ">=4"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"build": "tsc -b"
|
|
41
|
+
"build": "tsc -b && cp ../../README.md ."
|
|
42
42
|
}
|
|
43
43
|
}
|
package/dist/global-setup.d.ts
DELETED
package/dist/global-setup.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
const getBaseDisplay = () => {
|
|
5
|
-
const pid = process.pid;
|
|
6
|
-
const slot = pid % 500;
|
|
7
|
-
return 50 + slot * 10;
|
|
8
|
-
};
|
|
9
|
-
const xvfbProcesses = [];
|
|
10
|
-
let currentStateDir = null;
|
|
11
|
-
const waitForDisplay = (display, timeout = 5000) => {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
const start = Date.now();
|
|
14
|
-
const check = () => {
|
|
15
|
-
const lockFile = `/tmp/.X${display}-lock`;
|
|
16
|
-
if (existsSync(lockFile)) {
|
|
17
|
-
resolve(true);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (Date.now() - start > timeout) {
|
|
21
|
-
resolve(false);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
setTimeout(check, 50);
|
|
25
|
-
};
|
|
26
|
-
check();
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
const startXvfb = async (display) => {
|
|
30
|
-
const xvfb = spawn("Xvfb", [`:${display}`, "-screen", "0", "1024x768x24"], {
|
|
31
|
-
stdio: "ignore",
|
|
32
|
-
detached: true,
|
|
33
|
-
});
|
|
34
|
-
xvfb.unref();
|
|
35
|
-
const ready = await waitForDisplay(display);
|
|
36
|
-
if (!ready) {
|
|
37
|
-
xvfb.kill();
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return xvfb;
|
|
41
|
-
};
|
|
42
|
-
export const setup = async (project) => {
|
|
43
|
-
const config = project.config;
|
|
44
|
-
const maxWorkers = typeof config.maxWorkers === "number"
|
|
45
|
-
? config.maxWorkers
|
|
46
|
-
: typeof config.maxWorkers === "string"
|
|
47
|
-
? Number.parseInt(config.maxWorkers, 10)
|
|
48
|
-
: 4;
|
|
49
|
-
const stateDir = process.env.GTKX_STATE_DIR;
|
|
50
|
-
if (!stateDir) {
|
|
51
|
-
throw new Error("Expected GTKX_STATE_DIR environment variable to be set");
|
|
52
|
-
}
|
|
53
|
-
currentStateDir = stateDir;
|
|
54
|
-
if (existsSync(stateDir)) {
|
|
55
|
-
rmSync(stateDir, { recursive: true });
|
|
56
|
-
}
|
|
57
|
-
mkdirSync(stateDir, { recursive: true });
|
|
58
|
-
const baseDisplay = getBaseDisplay();
|
|
59
|
-
const displays = [];
|
|
60
|
-
const pids = [];
|
|
61
|
-
for (let i = 0; i < maxWorkers; i++) {
|
|
62
|
-
const display = baseDisplay + i;
|
|
63
|
-
const xvfb = await startXvfb(display);
|
|
64
|
-
if (xvfb) {
|
|
65
|
-
xvfbProcesses.push(xvfb);
|
|
66
|
-
displays.push(display);
|
|
67
|
-
pids.push(xvfb.pid ?? 0);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (displays.length === 0) {
|
|
71
|
-
throw new Error("Failed to start any Xvfb instances");
|
|
72
|
-
}
|
|
73
|
-
const state = { displays, pids };
|
|
74
|
-
writeFileSync(join(stateDir, "state.json"), JSON.stringify(state));
|
|
75
|
-
for (const display of displays) {
|
|
76
|
-
writeFileSync(join(stateDir, `display-${display}.available`), "");
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
export const teardown = async () => {
|
|
80
|
-
for (const xvfb of xvfbProcesses) {
|
|
81
|
-
try {
|
|
82
|
-
xvfb.kill("SIGTERM");
|
|
83
|
-
}
|
|
84
|
-
catch { }
|
|
85
|
-
}
|
|
86
|
-
if (currentStateDir && existsSync(currentStateDir)) {
|
|
87
|
-
rmSync(currentStateDir, { recursive: true });
|
|
88
|
-
}
|
|
89
|
-
};
|
|
File without changes
|