@cevek/screentest 0.2.4 → 0.2.5
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/dist/index.js
CHANGED
|
@@ -15093,32 +15093,13 @@ async function runUI(opts) {
|
|
|
15093
15093
|
|
|
15094
15094
|
// src/orchestrator.ts
|
|
15095
15095
|
import { spawn, spawnSync } from "child_process";
|
|
15096
|
+
import { promises as fs5 } from "fs";
|
|
15097
|
+
import { join as join4 } from "path";
|
|
15098
|
+
|
|
15099
|
+
// src/doc-builder.ts
|
|
15096
15100
|
import { createHash as createHash2 } from "crypto";
|
|
15097
15101
|
import { promises as fs4 } from "fs";
|
|
15098
|
-
import { join as
|
|
15099
|
-
|
|
15100
|
-
// src/runner/paths.ts
|
|
15101
|
-
import { join as join2, resolve as resolve5 } from "path";
|
|
15102
|
-
function resolveRunnerPaths(cwd = process.cwd()) {
|
|
15103
|
-
const projectRoot = resolve5(cwd);
|
|
15104
|
-
const cacheDir = join2(projectRoot, "node_modules", ".cache", "screentest");
|
|
15105
|
-
return {
|
|
15106
|
-
projectRoot,
|
|
15107
|
-
snapshotFile: join2(projectRoot, "snapshot.json"),
|
|
15108
|
-
cacheDir,
|
|
15109
|
-
actualDir: join2(cacheDir, "actual"),
|
|
15110
|
-
docFile: join2(cacheDir, "doc.json")
|
|
15111
|
-
};
|
|
15112
|
-
}
|
|
15113
|
-
|
|
15114
|
-
// src/orchestrator.ts
|
|
15115
|
-
var DOCKER_IMAGE = "screentest-tests";
|
|
15116
|
-
function run(cmd, argv) {
|
|
15117
|
-
return new Promise((resolve7) => {
|
|
15118
|
-
const p = spawn(cmd, argv, { stdio: "inherit" });
|
|
15119
|
-
p.on("exit", (code) => resolve7(code ?? 0));
|
|
15120
|
-
});
|
|
15121
|
-
}
|
|
15102
|
+
import { join as join2, relative, sep } from "path";
|
|
15122
15103
|
function isGroup(node) {
|
|
15123
15104
|
return Array.isArray(node.items);
|
|
15124
15105
|
}
|
|
@@ -15150,10 +15131,10 @@ async function walkActual(dir, prefix = []) {
|
|
|
15150
15131
|
const out = [];
|
|
15151
15132
|
for (const e of entries) {
|
|
15152
15133
|
if (e.isDirectory()) {
|
|
15153
|
-
out.push(...await walkActual(
|
|
15134
|
+
out.push(...await walkActual(join2(dir, e.name), [...prefix, e.name]));
|
|
15154
15135
|
} else if (e.isFile() && e.name.endsWith(".png")) {
|
|
15155
15136
|
const stem = e.name.slice(0, -4);
|
|
15156
|
-
out.push({ path: [...prefix, stem], absFile:
|
|
15137
|
+
out.push({ path: [...prefix, stem], absFile: join2(dir, e.name) });
|
|
15157
15138
|
}
|
|
15158
15139
|
}
|
|
15159
15140
|
return out;
|
|
@@ -15236,20 +15217,114 @@ async function generateDoc(snapshotFile, actualDir, docFile, cacheDir) {
|
|
|
15236
15217
|
);
|
|
15237
15218
|
return total;
|
|
15238
15219
|
}
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15220
|
+
|
|
15221
|
+
// src/runner/paths.ts
|
|
15222
|
+
import { join as join3, resolve as resolve5 } from "path";
|
|
15223
|
+
function resolveRunnerPaths(cwd = process.cwd()) {
|
|
15224
|
+
const projectRoot = resolve5(cwd);
|
|
15225
|
+
const cacheDir = join3(projectRoot, "node_modules", ".cache", "screentest");
|
|
15226
|
+
return {
|
|
15227
|
+
projectRoot,
|
|
15228
|
+
snapshotFile: join3(projectRoot, "snapshot.json"),
|
|
15229
|
+
cacheDir,
|
|
15230
|
+
actualDir: join3(cacheDir, "actual"),
|
|
15231
|
+
docFile: join3(cacheDir, "doc.json")
|
|
15232
|
+
};
|
|
15233
|
+
}
|
|
15234
|
+
|
|
15235
|
+
// src/orchestrator.ts
|
|
15236
|
+
var DOCKER_IMAGE = "screentest-tests";
|
|
15237
|
+
function run(cmd, argv) {
|
|
15238
|
+
return new Promise((resolve7) => {
|
|
15239
|
+
const p = spawn(cmd, argv, { stdio: "inherit" });
|
|
15240
|
+
p.on("exit", (code) => resolve7(code ?? 0));
|
|
15241
|
+
});
|
|
15242
|
+
}
|
|
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)) {
|
|
15242
15254
|
process.stderr.write(
|
|
15243
|
-
`
|
|
15244
|
-
|
|
15245
|
-
Or run host-side: pnpm exec screentest test --host
|
|
15255
|
+
`Dockerfile.tests not found at ${dockerfile}
|
|
15256
|
+
Did you run \`screentest init\` in this project?
|
|
15246
15257
|
`
|
|
15247
15258
|
);
|
|
15248
15259
|
return false;
|
|
15249
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
|
+
}
|
|
15250
15306
|
return true;
|
|
15251
15307
|
}
|
|
15252
|
-
async function runVitestInDocker(snapshotFile, cacheDir) {
|
|
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
|
+
}
|
|
15253
15328
|
const dockerArgs = ["run", "--rm", "--init", "--network=host"];
|
|
15254
15329
|
dockerArgs.push("-e", `APP_URL=${process.env.APP_URL || "http://localhost:5050"}`);
|
|
15255
15330
|
if (process.env.CI) dockerArgs.push("-e", "CI=1");
|
|
@@ -15258,8 +15333,15 @@ async function runVitestInDocker(snapshotFile, cacheDir) {
|
|
|
15258
15333
|
`${cacheDir}:/work/node_modules/.cache/screentest`,
|
|
15259
15334
|
"-v",
|
|
15260
15335
|
`${snapshotFile}:/work/snapshot.json`,
|
|
15261
|
-
|
|
15336
|
+
"-v",
|
|
15337
|
+
`${testsDir}:/work/tests:ro`,
|
|
15338
|
+
"-v",
|
|
15339
|
+
`${configFile}:/work/vitest.screentest.config.ts:ro`
|
|
15262
15340
|
);
|
|
15341
|
+
if (await exists(tsconfigFile)) {
|
|
15342
|
+
dockerArgs.push("-v", `${tsconfigFile}:/work/tsconfig.json:ro`);
|
|
15343
|
+
}
|
|
15344
|
+
dockerArgs.push(DOCKER_IMAGE);
|
|
15263
15345
|
return run("docker", dockerArgs);
|
|
15264
15346
|
}
|
|
15265
15347
|
async function runOrchestrator(opts = {}) {
|
|
@@ -15277,21 +15359,21 @@ async function runOrchestrator(opts = {}) {
|
|
|
15277
15359
|
}
|
|
15278
15360
|
return await run("node", [process.argv[1], paths.docFile]);
|
|
15279
15361
|
}
|
|
15280
|
-
await
|
|
15362
|
+
await fs5.mkdir(paths.cacheDir, { recursive: true });
|
|
15281
15363
|
try {
|
|
15282
|
-
await
|
|
15364
|
+
await fs5.access(paths.snapshotFile);
|
|
15283
15365
|
} catch {
|
|
15284
|
-
await
|
|
15366
|
+
await fs5.writeFile(paths.snapshotFile, '{\n "groups": []\n}\n');
|
|
15285
15367
|
}
|
|
15286
|
-
await
|
|
15368
|
+
await fs5.rm(paths.actualDir, { recursive: true, force: true });
|
|
15287
15369
|
let testCode;
|
|
15288
15370
|
if (opts.hostMode) {
|
|
15289
15371
|
testCode = await run("npx", ["vitest", "--config", "vitest.screentest.config.ts", "run"]);
|
|
15290
15372
|
} else {
|
|
15291
|
-
if (opts.requireDockerImage !== false && !
|
|
15373
|
+
if (opts.requireDockerImage !== false && !await ensureDockerImageFresh(paths.projectRoot)) {
|
|
15292
15374
|
return 1;
|
|
15293
15375
|
}
|
|
15294
|
-
testCode = await runVitestInDocker(paths.snapshotFile, paths.cacheDir);
|
|
15376
|
+
testCode = await runVitestInDocker(paths.projectRoot, paths.snapshotFile, paths.cacheDir);
|
|
15295
15377
|
}
|
|
15296
15378
|
if (testCode !== 0 && !process.env.CI) {
|
|
15297
15379
|
process.stderr.write(
|
|
@@ -15322,15 +15404,15 @@ async function runOrchestrator(opts = {}) {
|
|
|
15322
15404
|
// src/init.ts
|
|
15323
15405
|
import { copyFile, mkdir, readFile } from "fs/promises";
|
|
15324
15406
|
import { existsSync as existsSync2 } from "fs";
|
|
15325
|
-
import { dirname as dirname4, join as
|
|
15407
|
+
import { dirname as dirname4, join as join5, resolve as resolve6 } from "path";
|
|
15326
15408
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15327
15409
|
async function detectPackageManager(cwd) {
|
|
15328
|
-
if (existsSync2(
|
|
15329
|
-
if (existsSync2(
|
|
15330
|
-
if (existsSync2(
|
|
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";
|
|
15331
15413
|
try {
|
|
15332
15414
|
const pkg = JSON.parse(
|
|
15333
|
-
await readFile(
|
|
15415
|
+
await readFile(join5(cwd, "package.json"), "utf8")
|
|
15334
15416
|
);
|
|
15335
15417
|
const pm = pkg.packageManager ?? "";
|
|
15336
15418
|
if (pm.startsWith("pnpm")) return "pnpm";
|
|
@@ -15353,8 +15435,8 @@ async function runInit() {
|
|
|
15353
15435
|
{ from: "example.test.ts", to: "tests/example.test.ts" }
|
|
15354
15436
|
];
|
|
15355
15437
|
for (const { from, to } of targets) {
|
|
15356
|
-
const src =
|
|
15357
|
-
const dst =
|
|
15438
|
+
const src = join5(templatesDir, from);
|
|
15439
|
+
const dst = join5(cwd, to);
|
|
15358
15440
|
if (existsSync2(dst)) {
|
|
15359
15441
|
process.stdout.write(` exists ${to}
|
|
15360
15442
|
`);
|
|
@@ -7,10 +7,9 @@ WORKDIR /work
|
|
|
7
7
|
COPY package.json package-lock.json ./
|
|
8
8
|
RUN npm ci --ignore-scripts
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
COPY tests ./tests
|
|
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.
|
|
14
13
|
|
|
15
14
|
# Run vitest directly (skipping `npm exec`) — keeps the test image simple
|
|
16
15
|
# and matches how the host orchestrator invokes it.
|
|
@@ -17,10 +17,9 @@ COPY package.json pnpm-lock.yaml ./
|
|
|
17
17
|
COPY pnpm-workspace.yaml* ./
|
|
18
18
|
RUN pnpm install --frozen-lockfile --ignore-scripts --config.minimumReleaseAge=0
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
COPY tests ./tests
|
|
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.
|
|
24
23
|
|
|
25
24
|
# Run vitest directly (not via pnpm exec) — pnpm 11 re-runs its supply-chain
|
|
26
25
|
# policy on every exec, which would re-fail freshly-published packages
|
|
@@ -13,9 +13,8 @@ COPY package.json yarn.lock ./
|
|
|
13
13
|
COPY .yarnrc.yml* ./
|
|
14
14
|
RUN yarn install --frozen-lockfile --ignore-scripts
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
COPY tests ./tests
|
|
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.
|
|
20
19
|
|
|
21
20
|
CMD ["./node_modules/.bin/vitest", "--config", "vitest.screentest.config.ts", "run"]
|
|
@@ -6,8 +6,11 @@ export default defineConfig({
|
|
|
6
6
|
globalSetup: ['@cevek/screentest/global-setup'],
|
|
7
7
|
testTimeout: 10_000,
|
|
8
8
|
hookTimeout: 10_000,
|
|
9
|
+
// One file at a time: screenshot tests share a Firefox browserServer
|
|
10
|
+
// (launched in global-setup) and would collide if files ran in parallel.
|
|
11
|
+
// In vitest 4 `poolOptions` was removed; `fileParallelism: false` forces
|
|
12
|
+
// maxWorkers=1, which is the equivalent of the old `singleFork: true`.
|
|
9
13
|
pool: 'forks',
|
|
10
|
-
poolOptions: { forks: { singleFork: true } },
|
|
11
14
|
fileParallelism: false,
|
|
12
15
|
},
|
|
13
16
|
});
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.5",
|
|
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",
|