@cevek/screentest 0.2.3 → 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,28 +1,47 @@
1
- import type { BrowserContext, BrowserContextOptions, Page, PageScreenshotOptions } from 'playwright';
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 always shoots PNG and writes
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`, `animations: 'disabled'`,
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
- page: Page,
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(page, name, opts = {}) {
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 page.screenshot({
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,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.3",
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",