@cevek/screentest 0.2.3 → 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 +129 -47
- package/dist/runner.d.ts +27 -11
- package/dist/runner.js +53 -2
- package/dist/templates/Dockerfile.npm.tests +3 -4
- package/dist/templates/Dockerfile.pnpm.tests +3 -4
- package/dist/templates/Dockerfile.yarn.tests +3 -4
- package/dist/templates/vitest.screentest.config.ts +4 -1
- package/package.json +1 -1
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
|
`);
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BrowserContext,
|
|
3
|
+
BrowserContextOptions,
|
|
4
|
+
Locator,
|
|
5
|
+
Page,
|
|
6
|
+
PageScreenshotOptions,
|
|
7
|
+
} from 'playwright';
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Extra Playwright screenshot options the caller can pass through to
|
|
5
|
-
* `compareSnapshot` (e.g. `clip`, `mask`, `omitBackground`, `fullPage: false
|
|
6
|
-
* `type` and `path` are reserved — the runner
|
|
7
|
-
* into `node_modules/.cache/screentest/actual
|
|
11
|
+
* `compareSnapshot` (e.g. `clip`, `mask`, `omitBackground`, `fullPage: false`,
|
|
12
|
+
* `style` for Locator targets). `type` and `path` are reserved — the runner
|
|
13
|
+
* always shoots PNG and writes into `node_modules/.cache/screentest/actual/`.
|
|
14
|
+
*
|
|
15
|
+
* For `Locator` targets, page-only options (`fullPage`, `clip`) are silently
|
|
16
|
+
* ignored by Playwright.
|
|
8
17
|
*/
|
|
9
|
-
export type CompareSnapshotOptions = Omit<PageScreenshotOptions, 'type' | 'path'
|
|
18
|
+
export type CompareSnapshotOptions = Omit<PageScreenshotOptions, 'type' | 'path'> & {
|
|
19
|
+
/** Locator-only: CSS injected before screenshotting (e.g. hide scrollbars). */
|
|
20
|
+
style?: string;
|
|
21
|
+
};
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
24
|
* Capture a PNG, save under
|
|
13
25
|
* `<project>/node_modules/.cache/screentest/actual/<...path>.png`, and
|
|
14
26
|
* compare its SHA256 against the hash in `<project>/snapshot.json`.
|
|
15
27
|
*
|
|
28
|
+
* Pass a `Page` for a full-page screenshot, or a `Locator` to capture just
|
|
29
|
+
* that element's bounding box.
|
|
30
|
+
*
|
|
16
31
|
* The snapshot path is built automatically from the surrounding
|
|
17
32
|
* `describe(...)` + `it(...)` chain, plus `name` as the leaf. Prefix `name`
|
|
18
33
|
* with `/` to opt out of auto-grouping and use an absolute path.
|
|
19
34
|
*
|
|
20
35
|
* Pass `opts` to forward extra Playwright screenshot options (clip, mask,
|
|
21
|
-
* etc.). Defaults: `fullPage: true
|
|
36
|
+
* etc.). Defaults: `fullPage: true` (Page only), `animations: 'disabled'`,
|
|
22
37
|
* `caret: 'hide'`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* await compareSnapshot(page, 'page'); // full page
|
|
41
|
+
* await compareSnapshot(page.locator('h1'), 'h1'); // element only
|
|
23
42
|
*/
|
|
24
43
|
export function compareSnapshot(
|
|
25
|
-
|
|
44
|
+
target: Page | Locator,
|
|
26
45
|
name: string,
|
|
27
46
|
opts?: CompareSnapshotOptions,
|
|
28
47
|
): Promise<void>;
|
|
@@ -31,10 +50,7 @@ export function compareSnapshot(
|
|
|
31
50
|
* Freeze `Date` inside `ctx`. `when` may be an ISO string, unix-ms number,
|
|
32
51
|
* or `Date`. Must be called BEFORE the first navigation in the context.
|
|
33
52
|
*/
|
|
34
|
-
export function freezeDate(
|
|
35
|
-
ctx: BrowserContext,
|
|
36
|
-
when: string | number | Date,
|
|
37
|
-
): Promise<void>;
|
|
53
|
+
export function freezeDate(ctx: BrowserContext, when: string | number | Date): Promise<void>;
|
|
38
54
|
|
|
39
55
|
/**
|
|
40
56
|
* Wait for `document.fonts.ready` and every `<img>` to reach `complete`.
|
package/dist/runner.js
CHANGED
|
@@ -64,6 +64,9 @@ function initScript(opts) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/runner/snapshot.ts
|
|
67
|
+
function isPage(target) {
|
|
68
|
+
return typeof target.goto === "function";
|
|
69
|
+
}
|
|
67
70
|
function isGroup(x) {
|
|
68
71
|
return Array.isArray(x.items);
|
|
69
72
|
}
|
|
@@ -91,12 +94,51 @@ function findExpected(snap, path) {
|
|
|
91
94
|
);
|
|
92
95
|
return leaf ? leaf.hash : void 0;
|
|
93
96
|
}
|
|
97
|
+
var fileLocks = /* @__PURE__ */ new Map();
|
|
98
|
+
async function withFileLock(absPath, fn) {
|
|
99
|
+
const prev = fileLocks.get(absPath) ?? Promise.resolve();
|
|
100
|
+
let release;
|
|
101
|
+
const next = new Promise((r) => {
|
|
102
|
+
release = r;
|
|
103
|
+
});
|
|
104
|
+
fileLocks.set(
|
|
105
|
+
absPath,
|
|
106
|
+
prev.then(() => next)
|
|
107
|
+
);
|
|
108
|
+
try {
|
|
109
|
+
await prev;
|
|
110
|
+
return await fn();
|
|
111
|
+
} finally {
|
|
112
|
+
release();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function insertNullEntry(snapshotFile, path) {
|
|
116
|
+
await withFileLock(snapshotFile, async () => {
|
|
117
|
+
const doc = await loadSnapshot(snapshotFile);
|
|
118
|
+
const testName = path[path.length - 1];
|
|
119
|
+
const groupPath = path.slice(0, -1);
|
|
120
|
+
let items = doc.groups;
|
|
121
|
+
for (const g of groupPath) {
|
|
122
|
+
let found = items.find((n) => isGroup(n) && n.name === g);
|
|
123
|
+
if (!found) {
|
|
124
|
+
found = { name: g, items: [] };
|
|
125
|
+
items.push(found);
|
|
126
|
+
}
|
|
127
|
+
items = found.items;
|
|
128
|
+
}
|
|
129
|
+
const existing = items.find((n) => !isGroup(n) && n.name === testName);
|
|
130
|
+
if (existing) return;
|
|
131
|
+
items.push({ name: testName, hash: null });
|
|
132
|
+
await mkdir(dirname(snapshotFile), { recursive: true });
|
|
133
|
+
await writeFile(snapshotFile, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
134
|
+
});
|
|
135
|
+
}
|
|
94
136
|
function currentTestChain() {
|
|
95
137
|
const full = expect.getState().currentTestName ?? "";
|
|
96
138
|
if (!full) return [];
|
|
97
139
|
return full.split(" > ");
|
|
98
140
|
}
|
|
99
|
-
async function compareSnapshot(
|
|
141
|
+
async function compareSnapshot(target, name, opts = {}) {
|
|
100
142
|
const explicit = name.split("/").filter(Boolean);
|
|
101
143
|
const path = name.startsWith("/") ? explicit : [...currentTestChain(), ...explicit];
|
|
102
144
|
if (path.length === 0) throw new Error("compareSnapshot: empty name");
|
|
@@ -104,18 +146,27 @@ async function compareSnapshot(page, name, opts = {}) {
|
|
|
104
146
|
const fileRelative = `${path.join("/")}.png`;
|
|
105
147
|
const absFile = join2(paths.actualDir, fileRelative);
|
|
106
148
|
await mkdir(dirname(absFile), { recursive: true });
|
|
149
|
+
const page = isPage(target) ? target : target.page();
|
|
107
150
|
await waitForVisualStable(page);
|
|
108
|
-
const actualBuf = await
|
|
151
|
+
const actualBuf = isPage(target) ? await target.screenshot({
|
|
109
152
|
fullPage: true,
|
|
110
153
|
animations: "disabled",
|
|
111
154
|
caret: "hide",
|
|
112
155
|
...opts,
|
|
113
156
|
type: "png"
|
|
114
157
|
// always — we hash PNG bytes
|
|
158
|
+
}) : await target.screenshot({
|
|
159
|
+
animations: "disabled",
|
|
160
|
+
caret: "hide",
|
|
161
|
+
...opts,
|
|
162
|
+
type: "png"
|
|
115
163
|
});
|
|
116
164
|
await writeFile(absFile, actualBuf);
|
|
117
165
|
const actualHash = createHash("sha256").update(actualBuf).digest("hex");
|
|
118
166
|
const expectedHash = findExpected(await loadSnapshot(paths.snapshotFile), path);
|
|
167
|
+
if (expectedHash === void 0) {
|
|
168
|
+
await insertNullEntry(paths.snapshotFile, path);
|
|
169
|
+
}
|
|
119
170
|
if (expectedHash === void 0 || expectedHash === null) {
|
|
120
171
|
expect.soft(
|
|
121
172
|
null,
|
|
@@ -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",
|