@cevek/screentest 0.2.5 → 0.3.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 +52 -36
- package/dist/global-setup.js +7 -0
- package/dist/index.js +296 -167
- package/dist/runner.js +13 -2
- package/dist/templates/Dockerfile.firefox-server +25 -0
- package/dist/templates/launch-server.mjs +22 -0
- package/package.json +1 -1
- package/dist/templates/Dockerfile.npm.tests +0 -16
- package/dist/templates/Dockerfile.pnpm.tests +0 -27
- package/dist/templates/Dockerfile.yarn.tests +0 -20
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# @cevek/screentest
|
|
2
2
|
|
|
3
|
-
Local desktop tool for visual screenshot-test review
|
|
4
|
-
helpers and Docker-based
|
|
3
|
+
Local desktop tool for visual screenshot-test review **plus** the runner
|
|
4
|
+
helpers and Docker-based browser harness needed to capture the
|
|
5
5
|
screenshots. Everything ships in one npm package — no copy-paste.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# pick your package manager — all three are supported:
|
|
11
10
|
npm i -D @cevek/screentest playwright vitest && npx playwright install firefox
|
|
12
11
|
# pnpm add -D @cevek/screentest playwright vitest && pnpm exec playwright install firefox
|
|
13
12
|
# yarn add -D @cevek/screentest playwright vitest && yarn playwright install firefox
|
|
@@ -15,26 +14,31 @@ npm i -D @cevek/screentest playwright vitest && npx playwright install firefox
|
|
|
15
14
|
|
|
16
15
|
`playwright` and `vitest` are peer-deps — keep them in your devDependencies.
|
|
17
16
|
|
|
18
|
-
`screentest init` detects your package manager (from your lockfile or
|
|
19
|
-
`packageManager` field) and writes a matching `Dockerfile.tests`.
|
|
20
|
-
|
|
21
17
|
## Quick start
|
|
22
18
|
|
|
23
19
|
```bash
|
|
24
|
-
# 1. Scaffold
|
|
20
|
+
# 1. Scaffold vitest.screentest.config.ts + tests/example.test.ts
|
|
25
21
|
npx screentest init
|
|
26
22
|
|
|
27
|
-
# 2.
|
|
28
|
-
|
|
23
|
+
# 2. Start the shared Firefox daemon (one-time per machine — image builds
|
|
24
|
+
# on first run, ~2 min; cached forever after).
|
|
25
|
+
npx screentest serve
|
|
29
26
|
|
|
30
27
|
# 3. Run.
|
|
31
28
|
APP_URL=http://localhost:5050 npx screentest test
|
|
32
29
|
```
|
|
33
30
|
|
|
31
|
+
The daemon lives outside your project — one container serves every
|
|
32
|
+
project on your machine. It listens on `ws://localhost:5180`, holds
|
|
33
|
+
~300MB RAM at idle, and stays up across reboots-of-your-app until you
|
|
34
|
+
`screentest stop` it.
|
|
35
|
+
|
|
36
|
+
Your **tests run natively on the host** (your normal vitest + TS + path
|
|
37
|
+
aliases + watch mode); only the Firefox rendering happens in the
|
|
38
|
+
container, over a WebSocket. Editing tests never needs an image rebuild.
|
|
39
|
+
|
|
34
40
|
`screentest init` creates:
|
|
35
41
|
|
|
36
|
-
- `Dockerfile.tests` — playwright-noble + your project deps + vitest CMD
|
|
37
|
-
(auto-picked for pnpm / npm / yarn based on your lockfile)
|
|
38
42
|
- `vitest.screentest.config.ts` — separate vitest config wired with the
|
|
39
43
|
package's globalSetup. Kept under its own name so your normal
|
|
40
44
|
`vitest.config.ts` (unit tests) is untouched.
|
|
@@ -49,33 +53,38 @@ and screenshot tests via `screentest test` stay completely separate.
|
|
|
49
53
|
## Commands
|
|
50
54
|
|
|
51
55
|
```bash
|
|
52
|
-
screentest
|
|
53
|
-
|
|
56
|
+
screentest serve # start the shared Firefox browser-server daemon
|
|
57
|
+
screentest stop # stop it
|
|
58
|
+
screentest status # is it running?
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
Open the review UI on a pre-built `doc.json`. Used internally by `screentest
|
|
57
|
-
test` and `screentest review`.
|
|
58
|
-
|
|
59
61
|
```bash
|
|
60
|
-
screentest test
|
|
62
|
+
screentest test
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
auto-launch the review UI.
|
|
65
|
-
|
|
66
|
-
launch.
|
|
65
|
+
Run vitest on the host, connect to the daemon for browser rendering.
|
|
66
|
+
On failure (or new snapshots) auto-launch the review UI. Set `CI=1` to
|
|
67
|
+
skip the auto-UI and just propagate the exit code.
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
screentest review
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
Skip vitest,
|
|
73
|
+
Skip vitest, regenerate `doc.json` from cached actuals, open the UI.
|
|
73
74
|
|
|
74
75
|
```bash
|
|
75
76
|
screentest init
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
Scaffold
|
|
79
|
+
Scaffold vitest config + example test (see above).
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
screentest <doc-json-path> [--port 5174] [--no-open]
|
|
83
|
+
[--worker-url URL] [--token TOKEN]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Open the review UI on a pre-built `doc.json`. Used internally by
|
|
87
|
+
`screentest test` and `screentest review`.
|
|
79
88
|
|
|
80
89
|
## Writing tests
|
|
81
90
|
|
|
@@ -102,26 +111,33 @@ describe('team', () => {
|
|
|
102
111
|
await page.goto(`${APP_URL}/team`);
|
|
103
112
|
await page.locator('input[name="username"]').waitFor({ state: 'visible' });
|
|
104
113
|
await compareSnapshot(page, 'login'); // → team / login flow / login
|
|
114
|
+
|
|
115
|
+
// Snapshot just one element instead of the whole page:
|
|
116
|
+
await compareSnapshot(page.locator('header'), 'header');
|
|
105
117
|
});
|
|
106
118
|
});
|
|
107
119
|
```
|
|
108
120
|
|
|
109
|
-
Snapshot paths are auto-derived from `describe(...)` + `it(...)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
Snapshot paths are auto-derived from `describe(...)` + `it(...)` plus the
|
|
122
|
+
`compareSnapshot` leaf name. Hashes go into `tests/snapshot.json` by
|
|
123
|
+
default; override with `SCREENTEST_SNAPSHOT_FILE=path/to/snapshot.json`.
|
|
124
|
+
Actuals + the generated `doc.json` go to
|
|
125
|
+
`node_modules/.cache/screentest/` (gitignored by convention).
|
|
126
|
+
|
|
127
|
+
> **Upgrading from < 0.3:** an existing `<projectRoot>/snapshot.json` keeps
|
|
128
|
+
> working — the runner picks it up if `tests/snapshot.json` isn't there yet.
|
|
113
129
|
|
|
114
130
|
## Docker Desktop host networking
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
app at `localhost:*`. On macOS / Windows Docker Desktop enable it
|
|
118
|
-
`Settings → Resources → Network → Enable host networking`. On
|
|
119
|
-
(CI) it works out of the box.
|
|
132
|
+
The daemon container uses `--network=host` so its Firefox can reach
|
|
133
|
+
your app at `localhost:*`. On macOS / Windows Docker Desktop enable it
|
|
134
|
+
once: `Settings → Resources → Network → Enable host networking`. On
|
|
135
|
+
Linux Docker (CI) it works out of the box.
|
|
120
136
|
|
|
121
137
|
## Cloudflare Worker (blob store)
|
|
122
138
|
|
|
123
|
-
Accepted screenshots are stored in your Cloudflare Worker (R2-backed).
|
|
124
|
-
the [worker template](https://github.com/x-cevek/screentest/tree/main/cloudflare-worker)
|
|
139
|
+
Accepted screenshots are stored in your Cloudflare Worker (R2-backed).
|
|
140
|
+
See the [worker template](https://github.com/x-cevek/screentest/tree/main/cloudflare-worker)
|
|
125
141
|
for a 100-line implementation + `wrangler deploy` instructions.
|
|
126
142
|
|
|
127
143
|
Once deployed:
|
|
@@ -136,12 +152,12 @@ Or pass `--worker-url` / `--token` to `screentest` directly.
|
|
|
136
152
|
## CI
|
|
137
153
|
|
|
138
154
|
```yaml
|
|
139
|
-
- run:
|
|
155
|
+
- run: npx screentest serve # builds image + starts daemon
|
|
140
156
|
- run: CI=1 npx screentest test
|
|
141
157
|
```
|
|
142
158
|
|
|
143
|
-
In `CI=1` the orchestrator exits with vitest's code and never tries to
|
|
144
|
-
the UI.
|
|
159
|
+
In `CI=1` the orchestrator exits with vitest's code and never tries to
|
|
160
|
+
open the UI.
|
|
145
161
|
|
|
146
162
|
## Input format reference
|
|
147
163
|
|
package/dist/global-setup.js
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { firefox } from "playwright";
|
|
3
3
|
var server;
|
|
4
4
|
async function setup(project) {
|
|
5
|
+
const externalWs = process.env.SCREENTEST_FIREFOX_WS;
|
|
6
|
+
if (externalWs) {
|
|
7
|
+
project.provide("wsEndpoint", externalWs);
|
|
8
|
+
console.log(`[screentest] using external firefox daemon: ${externalWs}`);
|
|
9
|
+
return async () => {
|
|
10
|
+
};
|
|
11
|
+
}
|
|
5
12
|
server = await firefox.launchServer();
|
|
6
13
|
const wsEndpoint = server.wsEndpoint();
|
|
7
14
|
project.provide("wsEndpoint", wsEndpoint);
|
package/dist/index.js
CHANGED
|
@@ -14554,7 +14554,15 @@ var groupItemSchema = external_exports.lazy(
|
|
|
14554
14554
|
items: external_exports.array(external_exports.union([groupItemSchema, diffItemSchema])).min(0)
|
|
14555
14555
|
})
|
|
14556
14556
|
);
|
|
14557
|
+
var projectInfoSchema = external_exports.object({
|
|
14558
|
+
/** Absolute path to the project root the doc.json was generated from. */
|
|
14559
|
+
path: external_exports.string(),
|
|
14560
|
+
/** Current git branch, or `null` if not a git repo (or `git` not on PATH). */
|
|
14561
|
+
branch: external_exports.string().nullable()
|
|
14562
|
+
});
|
|
14557
14563
|
var docSchema = external_exports.object({
|
|
14564
|
+
/** Optional for backward compatibility with pre-0.3 doc.json files. */
|
|
14565
|
+
project: projectInfoSchema.optional(),
|
|
14558
14566
|
groups: external_exports.array(groupItemSchema)
|
|
14559
14567
|
});
|
|
14560
14568
|
var snapshotTestSchema = external_exports.object({
|
|
@@ -15092,14 +15100,227 @@ async function runUI(opts) {
|
|
|
15092
15100
|
}
|
|
15093
15101
|
|
|
15094
15102
|
// src/orchestrator.ts
|
|
15095
|
-
import { spawn
|
|
15103
|
+
import { spawn as spawn2 } from "child_process";
|
|
15096
15104
|
import { promises as fs5 } from "fs";
|
|
15097
|
-
import {
|
|
15105
|
+
import { dirname as dirname5 } from "path";
|
|
15106
|
+
|
|
15107
|
+
// src/daemon.ts
|
|
15108
|
+
import { spawn, spawnSync } from "child_process";
|
|
15109
|
+
import { existsSync as existsSync2 } from "fs";
|
|
15110
|
+
import { dirname as dirname4, resolve as resolve5 } from "path";
|
|
15111
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15112
|
+
import { createConnection } from "net";
|
|
15113
|
+
var IMAGE_NAME = "cevek/screentest-firefox";
|
|
15114
|
+
var IMAGE_TAG = "pw-1.60.0";
|
|
15115
|
+
var FULL_IMAGE = `${IMAGE_NAME}:${IMAGE_TAG}`;
|
|
15116
|
+
var CONTAINER_NAME = "screentest-firefox";
|
|
15117
|
+
var PORT = 5180;
|
|
15118
|
+
var WS_PATH = "screentest-firefox";
|
|
15119
|
+
var WS_ENDPOINT = `ws://localhost:${PORT}/${WS_PATH}`;
|
|
15120
|
+
function run(cmd, argv) {
|
|
15121
|
+
return new Promise((resolveExit) => {
|
|
15122
|
+
const p = spawn(cmd, argv, { stdio: "inherit" });
|
|
15123
|
+
p.on("exit", (code) => resolveExit(code ?? 0));
|
|
15124
|
+
});
|
|
15125
|
+
}
|
|
15126
|
+
function daemonState() {
|
|
15127
|
+
const ps = spawnSync(
|
|
15128
|
+
"docker",
|
|
15129
|
+
["ps", "-a", "-f", `name=^${CONTAINER_NAME}$`, "--format", "{{.Status}} {{.Image}}"],
|
|
15130
|
+
{ stdio: "pipe", encoding: "utf8" }
|
|
15131
|
+
);
|
|
15132
|
+
const line = ps.stdout.trim();
|
|
15133
|
+
if (line.startsWith("Up")) {
|
|
15134
|
+
const tab = line.indexOf(" ");
|
|
15135
|
+
const image = tab === -1 ? "" : line.slice(tab + 1).trim();
|
|
15136
|
+
return { state: "running", image };
|
|
15137
|
+
}
|
|
15138
|
+
const img = spawnSync("docker", ["image", "inspect", FULL_IMAGE], { stdio: "pipe" });
|
|
15139
|
+
if (img.status !== 0) return { state: "missing-image" };
|
|
15140
|
+
return { state: "stopped" };
|
|
15141
|
+
}
|
|
15142
|
+
function resolveTemplatesDir() {
|
|
15143
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
15144
|
+
const candidates = [
|
|
15145
|
+
resolve5(here, "./templates"),
|
|
15146
|
+
resolve5(here, "../templates"),
|
|
15147
|
+
resolve5(here, "../../server/templates")
|
|
15148
|
+
];
|
|
15149
|
+
for (const c of candidates) {
|
|
15150
|
+
if (existsSync2(c)) return c;
|
|
15151
|
+
}
|
|
15152
|
+
throw new Error("templates/ directory not found relative to daemon.ts");
|
|
15153
|
+
}
|
|
15154
|
+
async function buildImage() {
|
|
15155
|
+
const templates = resolveTemplatesDir();
|
|
15156
|
+
process.stderr.write(`Building Docker image ${FULL_IMAGE} (~2 min first time, cached after).
|
|
15157
|
+
`);
|
|
15158
|
+
const code = await run("docker", [
|
|
15159
|
+
"build",
|
|
15160
|
+
"-f",
|
|
15161
|
+
`${templates}/Dockerfile.firefox-server`,
|
|
15162
|
+
"-t",
|
|
15163
|
+
FULL_IMAGE,
|
|
15164
|
+
templates
|
|
15165
|
+
]);
|
|
15166
|
+
if (code !== 0) {
|
|
15167
|
+
process.stderr.write(`docker build failed (exit ${code})
|
|
15168
|
+
`);
|
|
15169
|
+
return false;
|
|
15170
|
+
}
|
|
15171
|
+
return true;
|
|
15172
|
+
}
|
|
15173
|
+
function probePort() {
|
|
15174
|
+
return new Promise((resolveProbe) => {
|
|
15175
|
+
const s = createConnection({ host: "127.0.0.1", port: PORT });
|
|
15176
|
+
s.once("connect", () => {
|
|
15177
|
+
s.end();
|
|
15178
|
+
resolveProbe(true);
|
|
15179
|
+
});
|
|
15180
|
+
s.once("error", () => resolveProbe(false));
|
|
15181
|
+
});
|
|
15182
|
+
}
|
|
15183
|
+
async function waitForReady(timeoutMs = 1e4) {
|
|
15184
|
+
const start = Date.now();
|
|
15185
|
+
while (Date.now() - start < timeoutMs) {
|
|
15186
|
+
if (await probePort()) return true;
|
|
15187
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
15188
|
+
}
|
|
15189
|
+
return false;
|
|
15190
|
+
}
|
|
15191
|
+
async function startDaemon() {
|
|
15192
|
+
const s = daemonState();
|
|
15193
|
+
if (s.state === "running") {
|
|
15194
|
+
if (s.image !== FULL_IMAGE) {
|
|
15195
|
+
process.stderr.write(
|
|
15196
|
+
`screentest-firefox daemon is running, but on a different image:
|
|
15197
|
+
running: ${s.image}
|
|
15198
|
+
this @cevek/screentest wants: ${FULL_IMAGE}
|
|
15199
|
+
\u2192 stop it first: screentest stop
|
|
15200
|
+
\u2192 then re-run: screentest serve
|
|
15201
|
+
`
|
|
15202
|
+
);
|
|
15203
|
+
return 1;
|
|
15204
|
+
}
|
|
15205
|
+
process.stdout.write(`screentest-firefox daemon already running at ${WS_ENDPOINT}
|
|
15206
|
+
`);
|
|
15207
|
+
return 0;
|
|
15208
|
+
}
|
|
15209
|
+
if (s.state === "missing-image") {
|
|
15210
|
+
if (!await buildImage()) return 1;
|
|
15211
|
+
}
|
|
15212
|
+
spawnSync("docker", ["rm", "-f", CONTAINER_NAME], { stdio: "pipe" });
|
|
15213
|
+
process.stderr.write(`Starting screentest-firefox daemon (port ${PORT})\u2026
|
|
15214
|
+
`);
|
|
15215
|
+
const code = await run("docker", [
|
|
15216
|
+
"run",
|
|
15217
|
+
"-d",
|
|
15218
|
+
"--rm",
|
|
15219
|
+
"--name",
|
|
15220
|
+
CONTAINER_NAME,
|
|
15221
|
+
"--network=host",
|
|
15222
|
+
FULL_IMAGE
|
|
15223
|
+
]);
|
|
15224
|
+
if (code !== 0) {
|
|
15225
|
+
process.stderr.write(`docker run failed (exit ${code})
|
|
15226
|
+
`);
|
|
15227
|
+
return code;
|
|
15228
|
+
}
|
|
15229
|
+
if (!await waitForReady()) {
|
|
15230
|
+
process.stderr.write(`Daemon container started but port ${PORT} never opened. Logs:
|
|
15231
|
+
`);
|
|
15232
|
+
spawnSync("docker", ["logs", CONTAINER_NAME], { stdio: "inherit" });
|
|
15233
|
+
return 1;
|
|
15234
|
+
}
|
|
15235
|
+
process.stdout.write(`screentest-firefox daemon ready at ${WS_ENDPOINT}
|
|
15236
|
+
`);
|
|
15237
|
+
return 0;
|
|
15238
|
+
}
|
|
15239
|
+
async function stopDaemon() {
|
|
15240
|
+
const s = daemonState();
|
|
15241
|
+
if (s.state !== "running") {
|
|
15242
|
+
process.stdout.write("screentest-firefox daemon is not running\n");
|
|
15243
|
+
return 0;
|
|
15244
|
+
}
|
|
15245
|
+
const code = await run("docker", ["stop", CONTAINER_NAME]);
|
|
15246
|
+
if (code === 0) process.stdout.write("screentest-firefox daemon stopped\n");
|
|
15247
|
+
return code;
|
|
15248
|
+
}
|
|
15249
|
+
async function printStatus() {
|
|
15250
|
+
const s = daemonState();
|
|
15251
|
+
if (s.state === "running") {
|
|
15252
|
+
const match = s.image === FULL_IMAGE ? "(matches this @cevek/screentest)" : "(MISMATCH!)";
|
|
15253
|
+
process.stdout.write(
|
|
15254
|
+
`screentest-firefox daemon: running
|
|
15255
|
+
ws endpoint: ${WS_ENDPOINT}
|
|
15256
|
+
image: ${s.image} ${match}
|
|
15257
|
+
expected: ${FULL_IMAGE}
|
|
15258
|
+
`
|
|
15259
|
+
);
|
|
15260
|
+
if (s.image !== FULL_IMAGE) {
|
|
15261
|
+
process.stdout.write(
|
|
15262
|
+
` \u2192 daemon is running an image pinned by a different version of this
|
|
15263
|
+
package. Stop it (screentest stop) and re-serve from the project
|
|
15264
|
+
whose Playwright version you want to use.
|
|
15265
|
+
`
|
|
15266
|
+
);
|
|
15267
|
+
}
|
|
15268
|
+
return 0;
|
|
15269
|
+
}
|
|
15270
|
+
if (s.state === "stopped") {
|
|
15271
|
+
process.stdout.write(
|
|
15272
|
+
`screentest-firefox daemon: stopped (image ${FULL_IMAGE} exists)
|
|
15273
|
+
start it with: screentest serve
|
|
15274
|
+
`
|
|
15275
|
+
);
|
|
15276
|
+
return 0;
|
|
15277
|
+
}
|
|
15278
|
+
process.stdout.write(
|
|
15279
|
+
`screentest-firefox daemon: not installed (image ${FULL_IMAGE} missing)
|
|
15280
|
+
build + start with: screentest serve
|
|
15281
|
+
`
|
|
15282
|
+
);
|
|
15283
|
+
return 0;
|
|
15284
|
+
}
|
|
15285
|
+
async function requireRunningDaemon() {
|
|
15286
|
+
const s = daemonState();
|
|
15287
|
+
if (s.state !== "running") {
|
|
15288
|
+
process.stderr.write(
|
|
15289
|
+
`screentest-firefox daemon is not running.
|
|
15290
|
+
Start it once with: screentest serve
|
|
15291
|
+
It then stays alive across projects until you stop it.
|
|
15292
|
+
`
|
|
15293
|
+
);
|
|
15294
|
+
return false;
|
|
15295
|
+
}
|
|
15296
|
+
if (s.image !== FULL_IMAGE) {
|
|
15297
|
+
process.stderr.write(
|
|
15298
|
+
`screentest-firefox daemon is running with a different Playwright version:
|
|
15299
|
+
running: ${s.image}
|
|
15300
|
+
this @cevek/screentest expects: ${FULL_IMAGE}
|
|
15301
|
+
\u2192 screentest stop && screentest serve
|
|
15302
|
+
(or align package versions across projects sharing the daemon)
|
|
15303
|
+
`
|
|
15304
|
+
);
|
|
15305
|
+
return false;
|
|
15306
|
+
}
|
|
15307
|
+
return true;
|
|
15308
|
+
}
|
|
15098
15309
|
|
|
15099
15310
|
// src/doc-builder.ts
|
|
15311
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
15100
15312
|
import { createHash as createHash2 } from "crypto";
|
|
15101
15313
|
import { promises as fs4 } from "fs";
|
|
15102
15314
|
import { join as join2, relative, sep } from "path";
|
|
15315
|
+
function detectBranch(projectRoot) {
|
|
15316
|
+
const r = spawnSync2("git", ["-C", projectRoot, "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
15317
|
+
stdio: "pipe",
|
|
15318
|
+
encoding: "utf8"
|
|
15319
|
+
});
|
|
15320
|
+
if (r.status !== 0) return null;
|
|
15321
|
+
const branch = r.stdout.trim();
|
|
15322
|
+
return branch || null;
|
|
15323
|
+
}
|
|
15103
15324
|
function isGroup(node) {
|
|
15104
15325
|
return Array.isArray(node.items);
|
|
15105
15326
|
}
|
|
@@ -15163,12 +15384,15 @@ function countLeaves(doc) {
|
|
|
15163
15384
|
for (const g of doc.groups) walk(g.items);
|
|
15164
15385
|
return n;
|
|
15165
15386
|
}
|
|
15166
|
-
async function generateDoc(snapshotFile, actualDir, docFile, cacheDir) {
|
|
15387
|
+
async function generateDoc(projectRoot, snapshotFile, actualDir, docFile, cacheDir) {
|
|
15167
15388
|
const snap = await readSnapshot(snapshotFile);
|
|
15168
15389
|
const expectedByPath = indexSnapshot(snap);
|
|
15169
15390
|
const actuals = await walkActual(actualDir);
|
|
15170
15391
|
const seen = /* @__PURE__ */ new Set();
|
|
15171
|
-
const doc = {
|
|
15392
|
+
const doc = {
|
|
15393
|
+
project: { path: projectRoot, branch: detectBranch(projectRoot) },
|
|
15394
|
+
groups: []
|
|
15395
|
+
};
|
|
15172
15396
|
const summary = { new: 0, change: 0, deleted: 0, unchanged: 0 };
|
|
15173
15397
|
for (const a of actuals) {
|
|
15174
15398
|
const buf = await fs4.readFile(a.absFile);
|
|
@@ -15219,135 +15443,42 @@ async function generateDoc(snapshotFile, actualDir, docFile, cacheDir) {
|
|
|
15219
15443
|
}
|
|
15220
15444
|
|
|
15221
15445
|
// src/runner/paths.ts
|
|
15222
|
-
import {
|
|
15446
|
+
import { existsSync as existsSync3 } from "fs";
|
|
15447
|
+
import { isAbsolute as isAbsolute2, join as join3, resolve as resolve6 } from "path";
|
|
15223
15448
|
function resolveRunnerPaths(cwd = process.cwd()) {
|
|
15224
|
-
const projectRoot =
|
|
15449
|
+
const projectRoot = resolve6(cwd);
|
|
15225
15450
|
const cacheDir = join3(projectRoot, "node_modules", ".cache", "screentest");
|
|
15226
15451
|
return {
|
|
15227
15452
|
projectRoot,
|
|
15228
|
-
snapshotFile:
|
|
15453
|
+
snapshotFile: resolveSnapshotFile(projectRoot),
|
|
15229
15454
|
cacheDir,
|
|
15230
15455
|
actualDir: join3(cacheDir, "actual"),
|
|
15231
15456
|
docFile: join3(cacheDir, "doc.json")
|
|
15232
15457
|
};
|
|
15233
15458
|
}
|
|
15459
|
+
function resolveSnapshotFile(projectRoot) {
|
|
15460
|
+
const envPath = process.env.SCREENTEST_SNAPSHOT_FILE;
|
|
15461
|
+
if (envPath) {
|
|
15462
|
+
return isAbsolute2(envPath) ? envPath : join3(projectRoot, envPath);
|
|
15463
|
+
}
|
|
15464
|
+
const legacyRoot = join3(projectRoot, "snapshot.json");
|
|
15465
|
+
const insideTests = join3(projectRoot, "tests", "snapshot.json");
|
|
15466
|
+
if (existsSync3(legacyRoot) && !existsSync3(insideTests)) return legacyRoot;
|
|
15467
|
+
return insideTests;
|
|
15468
|
+
}
|
|
15234
15469
|
|
|
15235
15470
|
// src/orchestrator.ts
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
p.on("exit", (code) => resolve7(code ?? 0));
|
|
15471
|
+
function run2(cmd, argv, env) {
|
|
15472
|
+
return new Promise((resolveExit) => {
|
|
15473
|
+
const p = spawn2(cmd, argv, { stdio: "inherit", env: env ?? process.env });
|
|
15474
|
+
p.on("exit", (code) => resolveExit(code ?? 0));
|
|
15241
15475
|
});
|
|
15242
15476
|
}
|
|
15243
|
-
async function exists(absPath) {
|
|
15244
|
-
try {
|
|
15245
|
-
await fs5.access(absPath);
|
|
15246
|
-
return true;
|
|
15247
|
-
} catch {
|
|
15248
|
-
return false;
|
|
15249
|
-
}
|
|
15250
|
-
}
|
|
15251
|
-
async function ensureDockerImageFresh(projectRoot) {
|
|
15252
|
-
const dockerfile = join4(projectRoot, "Dockerfile.tests");
|
|
15253
|
-
if (!await exists(dockerfile)) {
|
|
15254
|
-
process.stderr.write(
|
|
15255
|
-
`Dockerfile.tests not found at ${dockerfile}
|
|
15256
|
-
Did you run \`screentest init\` in this project?
|
|
15257
|
-
`
|
|
15258
|
-
);
|
|
15259
|
-
return false;
|
|
15260
|
-
}
|
|
15261
|
-
const inspect = spawnSync(
|
|
15262
|
-
"docker",
|
|
15263
|
-
["image", "inspect", "--format", "{{.Created}}", DOCKER_IMAGE],
|
|
15264
|
-
{ stdio: "pipe", encoding: "utf8" }
|
|
15265
|
-
);
|
|
15266
|
-
let reason = null;
|
|
15267
|
-
if (inspect.status !== 0) {
|
|
15268
|
-
reason = `Docker image "${DOCKER_IMAGE}" not found \u2014 building it (first build takes ~2 min).`;
|
|
15269
|
-
} else {
|
|
15270
|
-
const imageCreated = new Date(inspect.stdout.trim()).getTime();
|
|
15271
|
-
const inputs = [
|
|
15272
|
-
"package.json",
|
|
15273
|
-
"pnpm-lock.yaml",
|
|
15274
|
-
"package-lock.json",
|
|
15275
|
-
"yarn.lock",
|
|
15276
|
-
"Dockerfile.tests"
|
|
15277
|
-
];
|
|
15278
|
-
for (const f of inputs) {
|
|
15279
|
-
const abs = join4(projectRoot, f);
|
|
15280
|
-
try {
|
|
15281
|
-
const st = await fs5.stat(abs);
|
|
15282
|
-
if (st.mtimeMs > imageCreated) {
|
|
15283
|
-
reason = `${f} changed since the image was built \u2014 rebuilding.`;
|
|
15284
|
-
break;
|
|
15285
|
-
}
|
|
15286
|
-
} catch {
|
|
15287
|
-
}
|
|
15288
|
-
}
|
|
15289
|
-
}
|
|
15290
|
-
if (!reason) return true;
|
|
15291
|
-
process.stderr.write(`${reason}
|
|
15292
|
-
`);
|
|
15293
|
-
const code = await run("docker", [
|
|
15294
|
-
"build",
|
|
15295
|
-
"-f",
|
|
15296
|
-
"Dockerfile.tests",
|
|
15297
|
-
"-t",
|
|
15298
|
-
DOCKER_IMAGE,
|
|
15299
|
-
projectRoot
|
|
15300
|
-
]);
|
|
15301
|
-
if (code !== 0) {
|
|
15302
|
-
process.stderr.write(`docker build failed (exit ${code})
|
|
15303
|
-
`);
|
|
15304
|
-
return false;
|
|
15305
|
-
}
|
|
15306
|
-
return true;
|
|
15307
|
-
}
|
|
15308
|
-
async function runVitestInDocker(projectRoot, snapshotFile, cacheDir) {
|
|
15309
|
-
const testsDir = join4(projectRoot, "tests");
|
|
15310
|
-
const configFile = join4(projectRoot, "vitest.screentest.config.ts");
|
|
15311
|
-
const tsconfigFile = join4(projectRoot, "tsconfig.json");
|
|
15312
|
-
if (!await exists(testsDir)) {
|
|
15313
|
-
process.stderr.write(
|
|
15314
|
-
`tests/ not found at ${testsDir}
|
|
15315
|
-
Did you run \`screentest init\` in this project?
|
|
15316
|
-
`
|
|
15317
|
-
);
|
|
15318
|
-
return 1;
|
|
15319
|
-
}
|
|
15320
|
-
if (!await exists(configFile)) {
|
|
15321
|
-
process.stderr.write(
|
|
15322
|
-
`vitest.screentest.config.ts not found at ${configFile}
|
|
15323
|
-
Did you run \`screentest init\` in this project?
|
|
15324
|
-
`
|
|
15325
|
-
);
|
|
15326
|
-
return 1;
|
|
15327
|
-
}
|
|
15328
|
-
const dockerArgs = ["run", "--rm", "--init", "--network=host"];
|
|
15329
|
-
dockerArgs.push("-e", `APP_URL=${process.env.APP_URL || "http://localhost:5050"}`);
|
|
15330
|
-
if (process.env.CI) dockerArgs.push("-e", "CI=1");
|
|
15331
|
-
dockerArgs.push(
|
|
15332
|
-
"-v",
|
|
15333
|
-
`${cacheDir}:/work/node_modules/.cache/screentest`,
|
|
15334
|
-
"-v",
|
|
15335
|
-
`${snapshotFile}:/work/snapshot.json`,
|
|
15336
|
-
"-v",
|
|
15337
|
-
`${testsDir}:/work/tests:ro`,
|
|
15338
|
-
"-v",
|
|
15339
|
-
`${configFile}:/work/vitest.screentest.config.ts:ro`
|
|
15340
|
-
);
|
|
15341
|
-
if (await exists(tsconfigFile)) {
|
|
15342
|
-
dockerArgs.push("-v", `${tsconfigFile}:/work/tsconfig.json:ro`);
|
|
15343
|
-
}
|
|
15344
|
-
dockerArgs.push(DOCKER_IMAGE);
|
|
15345
|
-
return run("docker", dockerArgs);
|
|
15346
|
-
}
|
|
15347
15477
|
async function runOrchestrator(opts = {}) {
|
|
15348
15478
|
const paths = resolveRunnerPaths();
|
|
15349
15479
|
if (opts.reviewOnly) {
|
|
15350
15480
|
const total = await generateDoc(
|
|
15481
|
+
paths.projectRoot,
|
|
15351
15482
|
paths.snapshotFile,
|
|
15352
15483
|
paths.actualDir,
|
|
15353
15484
|
paths.docFile,
|
|
@@ -15357,36 +15488,38 @@ async function runOrchestrator(opts = {}) {
|
|
|
15357
15488
|
process.stdout.write("Nothing to review \u2014 all snapshots match.\n");
|
|
15358
15489
|
return 0;
|
|
15359
15490
|
}
|
|
15360
|
-
return await
|
|
15491
|
+
return await run2("node", [process.argv[1], paths.docFile]);
|
|
15361
15492
|
}
|
|
15362
15493
|
await fs5.mkdir(paths.cacheDir, { recursive: true });
|
|
15363
15494
|
try {
|
|
15364
15495
|
await fs5.access(paths.snapshotFile);
|
|
15365
15496
|
} catch {
|
|
15497
|
+
await fs5.mkdir(dirname5(paths.snapshotFile), { recursive: true });
|
|
15366
15498
|
await fs5.writeFile(paths.snapshotFile, '{\n "groups": []\n}\n');
|
|
15367
15499
|
}
|
|
15368
15500
|
await fs5.rm(paths.actualDir, { recursive: true, force: true });
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
testCode = await run("npx", ["vitest", "--config", "vitest.screentest.config.ts", "run"]);
|
|
15372
|
-
} else {
|
|
15373
|
-
if (opts.requireDockerImage !== false && !await ensureDockerImageFresh(paths.projectRoot)) {
|
|
15374
|
-
return 1;
|
|
15375
|
-
}
|
|
15376
|
-
testCode = await runVitestInDocker(paths.projectRoot, paths.snapshotFile, paths.cacheDir);
|
|
15501
|
+
if (!await requireRunningDaemon()) {
|
|
15502
|
+
return 1;
|
|
15377
15503
|
}
|
|
15504
|
+
const env = { ...process.env, SCREENTEST_FIREFOX_WS: WS_ENDPOINT };
|
|
15505
|
+
const testCode = await run2(
|
|
15506
|
+
"npx",
|
|
15507
|
+
["vitest", "--config", "vitest.screentest.config.ts", "run"],
|
|
15508
|
+
env
|
|
15509
|
+
);
|
|
15378
15510
|
if (testCode !== 0 && !process.env.CI) {
|
|
15379
15511
|
process.stderr.write(
|
|
15380
15512
|
"\n\u2192 Tests failed \u2014 launching screentest for review.\n Close the tool (Ctrl+C) when done; the original failure will still propagate.\n\n"
|
|
15381
15513
|
);
|
|
15382
15514
|
const total = await generateDoc(
|
|
15515
|
+
paths.projectRoot,
|
|
15383
15516
|
paths.snapshotFile,
|
|
15384
15517
|
paths.actualDir,
|
|
15385
15518
|
paths.docFile,
|
|
15386
15519
|
paths.cacheDir
|
|
15387
15520
|
);
|
|
15388
15521
|
if (total > 0) {
|
|
15389
|
-
const reviewCode = await
|
|
15522
|
+
const reviewCode = await run2("node", [process.argv[1], paths.docFile]);
|
|
15390
15523
|
if (reviewCode !== 0 && reviewCode !== 130) {
|
|
15391
15524
|
process.stderr.write(`
|
|
15392
15525
|
(screentest exited with code ${reviewCode})
|
|
@@ -15402,52 +15535,32 @@ async function runOrchestrator(opts = {}) {
|
|
|
15402
15535
|
}
|
|
15403
15536
|
|
|
15404
15537
|
// src/init.ts
|
|
15405
|
-
import { copyFile, mkdir
|
|
15406
|
-
import { existsSync as
|
|
15407
|
-
import { dirname as
|
|
15408
|
-
import { fileURLToPath as
|
|
15409
|
-
async function detectPackageManager(cwd) {
|
|
15410
|
-
if (existsSync2(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
15411
|
-
if (existsSync2(join5(cwd, "yarn.lock"))) return "yarn";
|
|
15412
|
-
if (existsSync2(join5(cwd, "package-lock.json"))) return "npm";
|
|
15413
|
-
try {
|
|
15414
|
-
const pkg = JSON.parse(
|
|
15415
|
-
await readFile(join5(cwd, "package.json"), "utf8")
|
|
15416
|
-
);
|
|
15417
|
-
const pm = pkg.packageManager ?? "";
|
|
15418
|
-
if (pm.startsWith("pnpm")) return "pnpm";
|
|
15419
|
-
if (pm.startsWith("yarn")) return "yarn";
|
|
15420
|
-
} catch {
|
|
15421
|
-
}
|
|
15422
|
-
return "npm";
|
|
15423
|
-
}
|
|
15538
|
+
import { copyFile, mkdir } from "fs/promises";
|
|
15539
|
+
import { existsSync as existsSync4 } from "fs";
|
|
15540
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "path";
|
|
15541
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
15424
15542
|
async function runInit() {
|
|
15425
|
-
const here =
|
|
15426
|
-
const templatesDir = [
|
|
15543
|
+
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
15544
|
+
const templatesDir = [resolve7(here, "./templates"), resolve7(here, "../templates")].find((p) => existsSync4(p)) ?? resolve7(here, "./templates");
|
|
15427
15545
|
const cwd = process.cwd();
|
|
15428
|
-
const pm = await detectPackageManager(cwd);
|
|
15429
|
-
process.stdout.write(`Detected package manager: ${pm}
|
|
15430
|
-
|
|
15431
|
-
`);
|
|
15432
15546
|
const targets = [
|
|
15433
|
-
{ from: `Dockerfile.${pm}.tests`, to: "Dockerfile.tests" },
|
|
15434
15547
|
{ from: "vitest.screentest.config.ts", to: "vitest.screentest.config.ts" },
|
|
15435
15548
|
{ from: "example.test.ts", to: "tests/example.test.ts" }
|
|
15436
15549
|
];
|
|
15437
15550
|
for (const { from, to } of targets) {
|
|
15438
|
-
const src =
|
|
15439
|
-
const dst =
|
|
15440
|
-
if (
|
|
15551
|
+
const src = join4(templatesDir, from);
|
|
15552
|
+
const dst = join4(cwd, to);
|
|
15553
|
+
if (existsSync4(dst)) {
|
|
15441
15554
|
process.stdout.write(` exists ${to}
|
|
15442
15555
|
`);
|
|
15443
15556
|
continue;
|
|
15444
15557
|
}
|
|
15445
|
-
if (!
|
|
15558
|
+
if (!existsSync4(src)) {
|
|
15446
15559
|
process.stdout.write(` missing ${from} (package install incomplete?)
|
|
15447
15560
|
`);
|
|
15448
15561
|
continue;
|
|
15449
15562
|
}
|
|
15450
|
-
await mkdir(
|
|
15563
|
+
await mkdir(dirname6(dst), { recursive: true });
|
|
15451
15564
|
await copyFile(src, dst);
|
|
15452
15565
|
process.stdout.write(` wrote ${to}
|
|
15453
15566
|
`);
|
|
@@ -15455,8 +15568,8 @@ async function runInit() {
|
|
|
15455
15568
|
process.stdout.write(
|
|
15456
15569
|
`
|
|
15457
15570
|
Next steps:
|
|
15458
|
-
1.
|
|
15459
|
-
2. APP_URL=http://localhost:<
|
|
15571
|
+
1. screentest serve # one-time: starts the shared Firefox daemon
|
|
15572
|
+
2. APP_URL=http://localhost:<port> screentest test
|
|
15460
15573
|
`
|
|
15461
15574
|
);
|
|
15462
15575
|
return 0;
|
|
@@ -15470,14 +15583,20 @@ function printHelp() {
|
|
|
15470
15583
|
" screentest <doc-json-path> [--port 5174] [--no-open] [--worker-url URL] [--token TOKEN]",
|
|
15471
15584
|
" open the review UI on an existing doc.json",
|
|
15472
15585
|
"",
|
|
15473
|
-
" screentest test
|
|
15474
|
-
"
|
|
15475
|
-
"
|
|
15586
|
+
" screentest test run vitest on the host, connect to the shared",
|
|
15587
|
+
" Firefox daemon, on failure regenerate doc.json",
|
|
15588
|
+
" and open the review UI",
|
|
15476
15589
|
"",
|
|
15477
15590
|
" screentest review regenerate doc.json from cached actuals + open UI",
|
|
15478
15591
|
"",
|
|
15479
|
-
" screentest
|
|
15480
|
-
"
|
|
15592
|
+
" screentest serve start the shared Firefox browser-server daemon",
|
|
15593
|
+
" (builds the image on first run). One daemon per",
|
|
15594
|
+
" machine \u2014 used by every project",
|
|
15595
|
+
" screentest stop stop the daemon",
|
|
15596
|
+
" screentest status show whether the daemon is running",
|
|
15597
|
+
"",
|
|
15598
|
+
" screentest init scaffold vitest.screentest.config.ts and",
|
|
15599
|
+
" tests/example.test.ts into the current project",
|
|
15481
15600
|
"",
|
|
15482
15601
|
"Env vars (UI mode):",
|
|
15483
15602
|
" CLOUDFLARE_WORKER_URL default https://screentests.x-cevek.workers.dev",
|
|
@@ -15498,9 +15617,7 @@ async function main() {
|
|
|
15498
15617
|
}
|
|
15499
15618
|
const cmd = argv[0];
|
|
15500
15619
|
if (cmd === "test") {
|
|
15501
|
-
const
|
|
15502
|
-
const hostMode = rest.includes("--host");
|
|
15503
|
-
const code = await runOrchestrator({ hostMode });
|
|
15620
|
+
const code = await runOrchestrator();
|
|
15504
15621
|
process.exit(code);
|
|
15505
15622
|
return;
|
|
15506
15623
|
}
|
|
@@ -15509,6 +15626,18 @@ async function main() {
|
|
|
15509
15626
|
process.exit(code);
|
|
15510
15627
|
return;
|
|
15511
15628
|
}
|
|
15629
|
+
if (cmd === "serve") {
|
|
15630
|
+
process.exit(await startDaemon());
|
|
15631
|
+
return;
|
|
15632
|
+
}
|
|
15633
|
+
if (cmd === "stop") {
|
|
15634
|
+
process.exit(await stopDaemon());
|
|
15635
|
+
return;
|
|
15636
|
+
}
|
|
15637
|
+
if (cmd === "status") {
|
|
15638
|
+
process.exit(await printStatus());
|
|
15639
|
+
return;
|
|
15640
|
+
}
|
|
15512
15641
|
if (cmd === "init") {
|
|
15513
15642
|
const code = await runInit();
|
|
15514
15643
|
process.exit(code);
|
package/dist/runner.js
CHANGED
|
@@ -5,18 +5,29 @@ import { dirname, join as join2 } from "path";
|
|
|
5
5
|
import { expect } from "vitest";
|
|
6
6
|
|
|
7
7
|
// src/runner/paths.ts
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { isAbsolute, join, resolve } from "path";
|
|
9
10
|
function resolveRunnerPaths(cwd = process.cwd()) {
|
|
10
11
|
const projectRoot = resolve(cwd);
|
|
11
12
|
const cacheDir = join(projectRoot, "node_modules", ".cache", "screentest");
|
|
12
13
|
return {
|
|
13
14
|
projectRoot,
|
|
14
|
-
snapshotFile:
|
|
15
|
+
snapshotFile: resolveSnapshotFile(projectRoot),
|
|
15
16
|
cacheDir,
|
|
16
17
|
actualDir: join(cacheDir, "actual"),
|
|
17
18
|
docFile: join(cacheDir, "doc.json")
|
|
18
19
|
};
|
|
19
20
|
}
|
|
21
|
+
function resolveSnapshotFile(projectRoot) {
|
|
22
|
+
const envPath = process.env.SCREENTEST_SNAPSHOT_FILE;
|
|
23
|
+
if (envPath) {
|
|
24
|
+
return isAbsolute(envPath) ? envPath : join(projectRoot, envPath);
|
|
25
|
+
}
|
|
26
|
+
const legacyRoot = join(projectRoot, "snapshot.json");
|
|
27
|
+
const insideTests = join(projectRoot, "tests", "snapshot.json");
|
|
28
|
+
if (existsSync(legacyRoot) && !existsSync(insideTests)) return legacyRoot;
|
|
29
|
+
return insideTests;
|
|
30
|
+
}
|
|
20
31
|
|
|
21
32
|
// src/runner/stabilize.ts
|
|
22
33
|
async function freezeDate(ctx, when) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Universal screentest Firefox browser-server image.
|
|
2
|
+
#
|
|
3
|
+
# Used by `screentest serve` to run a long-lived headless Firefox in a Linux
|
|
4
|
+
# container. Your tests run on the host (native vitest) and connect via
|
|
5
|
+
# WebSocket — so PNG bytes are baked by Linux Firefox (CI-parity) while the
|
|
6
|
+
# rest of your toolchain (TypeScript, deps, watch mode) stays on the host.
|
|
7
|
+
#
|
|
8
|
+
# The image is project-agnostic: it ONLY contains Playwright + Firefox + a
|
|
9
|
+
# 10-line launch script. Rebuild only when the Playwright version pinned
|
|
10
|
+
# below moves.
|
|
11
|
+
|
|
12
|
+
FROM mcr.microsoft.com/playwright:v1.60.0-noble
|
|
13
|
+
|
|
14
|
+
WORKDIR /work
|
|
15
|
+
|
|
16
|
+
# Browsers are pre-installed at /ms-playwright in the base image; we only
|
|
17
|
+
# need the JS package to use `firefox.launchServer(...)`.
|
|
18
|
+
RUN npm init -y >/dev/null \
|
|
19
|
+
&& npm install --no-audit --no-fund --no-progress playwright@1.60.0
|
|
20
|
+
|
|
21
|
+
COPY launch-server.mjs ./
|
|
22
|
+
|
|
23
|
+
EXPOSE 5180
|
|
24
|
+
|
|
25
|
+
CMD ["node", "launch-server.mjs"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Long-lived headless Firefox browser-server. Tests on the host connect via
|
|
2
|
+
// WS, see playwright.dev/docs/api/class-browsertype#browser-type-launch-server
|
|
3
|
+
import { firefox } from 'playwright';
|
|
4
|
+
|
|
5
|
+
const port = Number.parseInt(process.env.SCREENTEST_PORT ?? '5180', 10);
|
|
6
|
+
const wsPath = process.env.SCREENTEST_WS_PATH ?? 'screentest-firefox';
|
|
7
|
+
|
|
8
|
+
const server = await firefox.launchServer({
|
|
9
|
+
port,
|
|
10
|
+
host: '0.0.0.0',
|
|
11
|
+
wsPath,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log(`[screentest] firefox browser-server ready: ${server.wsEndpoint()}`);
|
|
15
|
+
|
|
16
|
+
const shutdown = async (signal) => {
|
|
17
|
+
console.log(`[screentest] received ${signal}, closing browser-server`);
|
|
18
|
+
await server.close();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
};
|
|
21
|
+
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
22
|
+
process.on('SIGINT', () => void shutdown('SIGINT'));
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.3.0",
|
|
7
7
|
"description": "Local desktop tool for visual screenshot-test review — CLI, runner helpers, and review UI shipped as a single package.",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"type": "module",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
FROM mcr.microsoft.com/playwright:v1.60.0-noble
|
|
2
|
-
|
|
3
|
-
WORKDIR /work
|
|
4
|
-
|
|
5
|
-
# Install dependencies. --ignore-scripts skips your project's postinstall
|
|
6
|
-
# hooks (which usually scan src/ — not needed for the test image).
|
|
7
|
-
COPY package.json package-lock.json ./
|
|
8
|
-
RUN npm ci --ignore-scripts
|
|
9
|
-
|
|
10
|
-
# tests/, vitest.screentest.config.ts, tsconfig.json, and snapshot.json are
|
|
11
|
-
# bind-mounted by `screentest test` at run-time — no rebuild needed for test
|
|
12
|
-
# or config edits. Rebuild only when package.json/package-lock.json change.
|
|
13
|
-
|
|
14
|
-
# Run vitest directly (skipping `npm exec`) — keeps the test image simple
|
|
15
|
-
# and matches how the host orchestrator invokes it.
|
|
16
|
-
CMD ["./node_modules/.bin/vitest", "--config", "vitest.screentest.config.ts", "run"]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
FROM mcr.microsoft.com/playwright:v1.60.0-noble
|
|
2
|
-
|
|
3
|
-
WORKDIR /work
|
|
4
|
-
|
|
5
|
-
# corepack lets us use whatever pnpm version your project's `packageManager`
|
|
6
|
-
# field pins (pnpm 11 by default in modern projects). If your project uses
|
|
7
|
-
# npm or yarn instead, swap the install commands accordingly.
|
|
8
|
-
RUN corepack enable
|
|
9
|
-
|
|
10
|
-
# Install dependencies. Flags:
|
|
11
|
-
# --ignore-scripts skips your project's postinstall hooks (which
|
|
12
|
-
# usually scan src/ — not needed for the test image).
|
|
13
|
-
# --config.minimumReleaseAge=0 bypass pnpm 11's supply-chain policy for
|
|
14
|
-
# freshly-published packages (the test image is
|
|
15
|
-
# ephemeral, runs only what's already in lockfile).
|
|
16
|
-
COPY package.json pnpm-lock.yaml ./
|
|
17
|
-
COPY pnpm-workspace.yaml* ./
|
|
18
|
-
RUN pnpm install --frozen-lockfile --ignore-scripts --config.minimumReleaseAge=0
|
|
19
|
-
|
|
20
|
-
# tests/, vitest.screentest.config.ts, tsconfig.json, and snapshot.json are
|
|
21
|
-
# bind-mounted by `screentest test` at run-time — no rebuild needed for test
|
|
22
|
-
# or config edits. Rebuild only when package.json/pnpm-lock.yaml change.
|
|
23
|
-
|
|
24
|
-
# Run vitest directly (not via pnpm exec) — pnpm 11 re-runs its supply-chain
|
|
25
|
-
# policy on every exec, which would re-fail freshly-published packages
|
|
26
|
-
# even after install succeeded.
|
|
27
|
-
CMD ["./node_modules/.bin/vitest", "--config", "vitest.screentest.config.ts", "run"]
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
FROM mcr.microsoft.com/playwright:v1.60.0-noble
|
|
2
|
-
|
|
3
|
-
WORKDIR /work
|
|
4
|
-
|
|
5
|
-
# corepack lets yarn 2+/berry self-install from your project's
|
|
6
|
-
# `packageManager` field. For yarn 1 it's a no-op (yarn is bundled in newer
|
|
7
|
-
# Node images anyway).
|
|
8
|
-
RUN corepack enable
|
|
9
|
-
|
|
10
|
-
# Install dependencies. --ignore-scripts skips your project's postinstall
|
|
11
|
-
# hooks (which usually scan src/ — not needed for the test image).
|
|
12
|
-
COPY package.json yarn.lock ./
|
|
13
|
-
COPY .yarnrc.yml* ./
|
|
14
|
-
RUN yarn install --frozen-lockfile --ignore-scripts
|
|
15
|
-
|
|
16
|
-
# tests/, vitest.screentest.config.ts, tsconfig.json, and snapshot.json are
|
|
17
|
-
# bind-mounted by `screentest test` at run-time — no rebuild needed for test
|
|
18
|
-
# or config edits. Rebuild only when package.json/yarn.lock change.
|
|
19
|
-
|
|
20
|
-
CMD ["./node_modules/.bin/vitest", "--config", "vitest.screentest.config.ts", "run"]
|