@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 join3, relative, sep } from "path";
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(join3(dir, e.name), [...prefix, e.name]));
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: join3(dir, e.name) });
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
- function ensureDockerImage() {
15240
- const r = spawnSync("docker", ["image", "inspect", DOCKER_IMAGE], { stdio: "pipe" });
15241
- if (r.status !== 0) {
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
- `Docker image "${DOCKER_IMAGE}" not found.
15244
- Build it first: pnpm exec screentest init && docker build -f Dockerfile.tests -t screentest-tests .
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
- DOCKER_IMAGE
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 fs4.mkdir(paths.cacheDir, { recursive: true });
15362
+ await fs5.mkdir(paths.cacheDir, { recursive: true });
15281
15363
  try {
15282
- await fs4.access(paths.snapshotFile);
15364
+ await fs5.access(paths.snapshotFile);
15283
15365
  } catch {
15284
- await fs4.writeFile(paths.snapshotFile, '{\n "groups": []\n}\n');
15366
+ await fs5.writeFile(paths.snapshotFile, '{\n "groups": []\n}\n');
15285
15367
  }
15286
- await fs4.rm(paths.actualDir, { recursive: true, force: true });
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 && !ensureDockerImage()) {
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 join4, resolve as resolve6 } from "path";
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(join4(cwd, "pnpm-lock.yaml"))) return "pnpm";
15329
- if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn";
15330
- if (existsSync2(join4(cwd, "package-lock.json"))) return "npm";
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(join4(cwd, "package.json"), "utf8")
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 = join4(templatesDir, from);
15357
- const dst = join4(cwd, to);
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
- # Only the bits needed to run vitest.
11
- COPY vitest.screentest.config.ts ./
12
- COPY tsconfig*.json ./
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
- # Only the bits needed to run vitest.
21
- COPY vitest.screentest.config.ts ./
22
- COPY tsconfig*.json ./
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
- # Only the bits needed to run vitest.
17
- COPY vitest.screentest.config.ts ./
18
- COPY tsconfig*.json ./
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.4",
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",