@cevek/screentest 0.2.2 → 0.2.4

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/runner.d.ts CHANGED
@@ -1,15 +1,50 @@
1
- import type { BrowserContext, BrowserContextOptions, Page } from 'playwright';
1
+ import type {
2
+ BrowserContext,
3
+ BrowserContextOptions,
4
+ Locator,
5
+ Page,
6
+ PageScreenshotOptions,
7
+ } from 'playwright';
2
8
 
3
9
  /**
4
- * Capture a full-page PNG, save under
10
+ * Extra Playwright screenshot options the caller can pass through to
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.
17
+ */
18
+ export type CompareSnapshotOptions = Omit<PageScreenshotOptions, 'type' | 'path'> & {
19
+ /** Locator-only: CSS injected before screenshotting (e.g. hide scrollbars). */
20
+ style?: string;
21
+ };
22
+
23
+ /**
24
+ * Capture a PNG, save under
5
25
  * `<project>/node_modules/.cache/screentest/actual/<...path>.png`, and
6
26
  * compare its SHA256 against the hash in `<project>/snapshot.json`.
7
27
  *
28
+ * Pass a `Page` for a full-page screenshot, or a `Locator` to capture just
29
+ * that element's bounding box.
30
+ *
8
31
  * The snapshot path is built automatically from the surrounding
9
32
  * `describe(...)` + `it(...)` chain, plus `name` as the leaf. Prefix `name`
10
33
  * with `/` to opt out of auto-grouping and use an absolute path.
34
+ *
35
+ * Pass `opts` to forward extra Playwright screenshot options (clip, mask,
36
+ * etc.). Defaults: `fullPage: true` (Page only), `animations: 'disabled'`,
37
+ * `caret: 'hide'`.
38
+ *
39
+ * @example
40
+ * await compareSnapshot(page, 'page'); // full page
41
+ * await compareSnapshot(page.locator('h1'), 'h1'); // element only
11
42
  */
12
- export function compareSnapshot(page: Page, name: string): Promise<void>;
43
+ export function compareSnapshot(
44
+ target: Page | Locator,
45
+ name: string,
46
+ opts?: CompareSnapshotOptions,
47
+ ): Promise<void>;
13
48
 
14
49
  /**
15
50
  * Freeze `Date` inside `ctx`. `when` may be an ISO string, unix-ms number,
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(page, name) {
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,16 +146,27 @@ async function compareSnapshot(page, name) {
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 page.screenshot({
151
+ const actualBuf = isPage(target) ? await target.screenshot({
109
152
  fullPage: true,
110
- type: "png",
111
153
  animations: "disabled",
112
- caret: "hide"
154
+ caret: "hide",
155
+ ...opts,
156
+ type: "png"
157
+ // always — we hash PNG bytes
158
+ }) : await target.screenshot({
159
+ animations: "disabled",
160
+ caret: "hide",
161
+ ...opts,
162
+ type: "png"
113
163
  });
114
164
  await writeFile(absFile, actualBuf);
115
165
  const actualHash = createHash("sha256").update(actualBuf).digest("hex");
116
166
  const expectedHash = findExpected(await loadSnapshot(paths.snapshotFile), path);
167
+ if (expectedHash === void 0) {
168
+ await insertNullEntry(paths.snapshotFile, path);
169
+ }
117
170
  if (expectedHash === void 0 || expectedHash === null) {
118
171
  expect.soft(
119
172
  null,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.2",
6
+ "version": "0.2.4",
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",