@ingcreators/annot-playwright 0.4.0 → 0.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # @ingcreators/annot-playwright
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 266b05a: **Fix `writeFile is not a function` crash on `page.screenshot({ annot:
8
+ { path } })`** — adds `/^node:/` to the Vite library build's
9
+ `rollupOptions.external` so Node builtins (`node:fs/promises`, …)
10
+ stay as native ESM imports in `dist/index.js` instead of being
11
+ inlined under Vite's browser-compat externalisation.
12
+
13
+ ### Root cause
14
+
15
+ `screenshot-patch.ts` imports `writeFile` from `node:fs/promises`
16
+ and calls it when `path` is set on the screenshot opts. Vite's
17
+ default behaviour for Node builtins in a library build is to
18
+ externalise them with a browser-compat shim that destructures
19
+ incorrectly at the consumer side — the destructured name resolves
20
+ to `undefined` and the runtime call throws:
21
+
22
+ ```
23
+ TypeError: (0 , d.writeFile) is not a function
24
+ at _Page.N (.../node_modules/@ingcreators/annot-playwright/dist/index.js:156:36)
25
+ ```
26
+
27
+ The local build emitted a `Module 'node:fs/promises' has been
28
+ externalized for browser compatibility` warning — same root cause.
29
+
30
+ ### Fix
31
+
32
+ `packages/playwright/vite.config.ts` now mirrors the pattern used
33
+ by `@ingcreators/annot-product-docs`, `-product-docs-astro`,
34
+ `-product-docs-xlsx`, and `-mcp`: `/^node:/` in `external` keeps
35
+ Node builtins as `import { writeFile } from "node:fs/promises"`
36
+ in the output bundle, which Node resolves natively at runtime.
37
+
38
+ ### Affected consumers
39
+
40
+ Every consumer of `@ingcreators/annot-playwright@0.4.0` that passes
41
+ `path` on `page.screenshot({ annot: { … } })` /
42
+ `locator.screenshot({ annot: { … } })`. The workflow-app docs-tour
43
+ CI workflow (`.github/workflows/docs-tour.yml` in the `annot` repo,
44
+ plus the dogfooded `examples/workflow-app/` tour) is the first
45
+ known case in the wild.
46
+
47
+ ### Verification
48
+ - `pnpm --filter @ingcreators/annot-playwright build` succeeds
49
+ without the `externalized for browser compatibility` warning.
50
+ - `dist/index.js` line 4 reads
51
+ `import { writeFile as d } from "node:fs/promises";` (literal
52
+ native import — no compat shim).
53
+ - `pnpm --filter @ingcreators/annot-playwright typecheck` passes.
54
+
55
+ - Updated dependencies [6124d59]
56
+ - Updated dependencies [0c7ac26]
57
+ - Updated dependencies [64dc6e8]
58
+ - Updated dependencies [691bec5]
59
+ - Updated dependencies [9697f27]
60
+ - @ingcreators/annot-annotator@0.6.0
61
+ - @ingcreators/annot-core@0.3.0
62
+
3
63
  ## 0.4.0
4
64
 
5
65
  ### Minor Changes
@@ -0,0 +1,134 @@
1
+ import { ElementTree, ElementTreeViewport } from '@ingcreators/annot-core/element-tree';
2
+ /**
3
+ * Options for the pure YAML → tree conversion.
4
+ */
5
+ export interface PlaywrightYamlToElementTreeOptions {
6
+ /** Raw output of `page.locator(...).ariaSnapshot({ mode: "ai",
7
+ * boxes: true })`. Required. */
8
+ yaml: string;
9
+ /** Viewport dimensions + DPR at capture time. Required. */
10
+ viewport: ElementTreeViewport;
11
+ /** Page URL at capture time. Stored in `ElementTree.source.url`. */
12
+ url?: string;
13
+ /** Tool identifier (e.g. `"annot-playwright@0.4.0"`). Stored in
14
+ * `ElementTree.source.agent`. */
15
+ agent?: string;
16
+ /** Capture timestamp. Defaults to `new Date().toISOString()`. */
17
+ capturedAt?: string;
18
+ }
19
+ /**
20
+ * Convert Playwright's aria-snapshot YAML into an `ElementTree`.
21
+ *
22
+ * The YAML format Playwright emits in `mode: "ai"` is a
23
+ * 2-space-per-level indented bullet list:
24
+ *
25
+ * ```yaml
26
+ * - main:
27
+ * - heading "Sign in" [level=1] [ref=e2]
28
+ * - form [ref=e3]:
29
+ * - textbox "Email" [ref=e4] [box=100,200,300,40]
30
+ * - button "Sign in" [active] [ref=e6]
31
+ * ```
32
+ *
33
+ * Each line:
34
+ * - starts with `-` at some indent
35
+ * - role token (lowercase letters)
36
+ * - optional `"name"` (double-quoted)
37
+ * - zero or more `[token]` annotations: `[ref=eN]`, `[box=x,y,w,h]`,
38
+ * `[state]`, `[key=value]`
39
+ * - optional trailing `:` indicating a container
40
+ *
41
+ * Nodes without a `[ref=…]` (decorative containers) get a synthetic
42
+ * `ref` so every node in the resulting tree has a unique identifier.
43
+ */
44
+ export declare function playwrightYamlToElementTree(opts: PlaywrightYamlToElementTreeOptions): ElementTree;
45
+ /**
46
+ * Options for `attachAttributes` — the convenience that walks an
47
+ * already-built `ElementTree` and fills each node's `attributes` by
48
+ * resolving its `match: { role, name }` against the live page.
49
+ */
50
+ export interface AttachAttributesOptions {
51
+ /** Whitelist of HTML attribute names to capture. Same shape as
52
+ * `DEFAULT_ATTR_WHITELIST` in `@ingcreators/annot-product-docs`. */
53
+ whitelist: readonly string[];
54
+ }
55
+ /**
56
+ * Walk an ElementTree and attach HTML attribute snapshots to each
57
+ * node by resolving its role+name against the live page. Mutates
58
+ * the tree in place AND returns it for fluent chaining.
59
+ *
60
+ * Resolution strategy: for each node with both `role` and `name`,
61
+ * call `page.getByRole(role, { name, exact: true })`. When the
62
+ * locator resolves to exactly one element, evaluate the whitelist
63
+ * against it. Ambiguous / missing resolves are silently skipped —
64
+ * the drift detector raises those (intentionally not duplicating
65
+ * the diagnostic here).
66
+ */
67
+ export declare function attachAttributes(tree: ElementTree, page: AttachAttributesPage, options: AttachAttributesOptions): Promise<ElementTree>;
68
+ /**
69
+ * Options for `captureElementTree` — the high-level composition that
70
+ * runs an aria-snapshot, converts to an ElementTree, optionally
71
+ * attaches HTML attributes, and returns the canonical tree. Wraps
72
+ * `playwrightYamlToElementTree` + `attachAttributes` in one call so
73
+ * downstream callers (productDocs sync flows, screenshot resolvers,
74
+ * MCP tools) don't have to plumb the intermediate YAML.
75
+ *
76
+ * Phase 1e of `docs/plans/living-spec-authoring-roadmap.md`.
77
+ */
78
+ export interface CaptureElementTreeOptions {
79
+ /** Root locator for the snapshot. Defaults to the page body. */
80
+ rootLocator?: CaptureElementTreeLocator;
81
+ /** Page URL stored in `ElementTree.source.url`. Defaults to
82
+ * `page.url()` when the host page exposes that method. */
83
+ url?: string;
84
+ /** Agent string stored in `ElementTree.source.agent`. */
85
+ agent?: string;
86
+ /** Capture timestamp override (defaults to `new Date().toISOString()`). */
87
+ capturedAt?: string;
88
+ /** When set, walks the resulting tree and fills per-node `attributes`
89
+ * via `attachAttributes`. Pass `[]` to skip attribute capture
90
+ * entirely; pass a name list to opt in. */
91
+ attributeWhitelist?: readonly string[];
92
+ }
93
+ /**
94
+ * Structural subset of Playwright `Page` that `captureElementTree`
95
+ * needs: viewport reader + ariaSnapshot dispatch on a root locator
96
+ * + the role-resolver `attachAttributes` uses for attribute
97
+ * collection. Real Playwright `Page` instances satisfy this.
98
+ */
99
+ export interface CaptureElementTreePage extends AttachAttributesPage {
100
+ viewportSize(): {
101
+ width: number;
102
+ height: number;
103
+ } | null;
104
+ locator(selector: string): CaptureElementTreeLocator;
105
+ url(): string;
106
+ }
107
+ export interface CaptureElementTreeLocator {
108
+ ariaSnapshot(options: {
109
+ mode: "ai";
110
+ boxes: boolean;
111
+ }): Promise<string>;
112
+ }
113
+ /**
114
+ * One-shot capture composition: snapshot → ElementTree → attributes →
115
+ * return. The returned tree carries `source.kind: "playwright"` and
116
+ * `source.url` populated from the page when not overridden.
117
+ */
118
+ export declare function captureElementTree(page: CaptureElementTreePage, options?: CaptureElementTreeOptions): Promise<ElementTree>;
119
+ /**
120
+ * Structural subset of Playwright `Page` that `attachAttributes`
121
+ * needs. Real Playwright `Page` instances satisfy this; the trimmed
122
+ * shape lets unit tests pass a minimal mock without pulling in
123
+ * Playwright's full type graph.
124
+ */
125
+ export interface AttachAttributesPage {
126
+ getByRole(role: string, options: {
127
+ name: string;
128
+ exact: true;
129
+ }): AttachAttributesLocator;
130
+ }
131
+ export interface AttachAttributesLocator {
132
+ count(): Promise<number>;
133
+ evaluate<R, A>(fn: (el: Element, arg: A) => R, arg: A): Promise<R>;
134
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type { AnnotationStyle, BBox, BboxAnnotation, BboxArrowAnnotation, BboxCalloutAnnotation, BboxCircleAnnotation, BboxRectAnnotation, BboxRedactRegion, BboxTextAnnotation, Intent, Point, RawAnnotation, RedactStyle, } from '@ingcreators/annot-annotator';
2
2
  export { bboxAnnotationsToSvg } from '@ingcreators/annot-annotator';
3
3
  export { expect } from '@playwright/test';
4
+ export { type AttachAttributesLocator, type AttachAttributesOptions, type AttachAttributesPage, attachAttributes, type CaptureElementTreeLocator, type CaptureElementTreeOptions, type CaptureElementTreePage, captureElementTree, type PlaywrightYamlToElementTreeOptions, playwrightYamlToElementTree, } from './element-tree-adapter.js';
4
5
  export { type AnnotateScreenshotOptions, annotateScreenshot, type PageLike, type PlaywrightAnnotator, test, } from './fixture.js';
5
6
  export { type ArrowOptions, arrowBetween, type BoundingBox, type RectOptions, rectForBoundingBox, type TextOptions, textAt, } from './helpers.js';
6
7
  export { type Clip, describeAnnotation, type RebaseResult, rebaseAnnotations, } from './rebase.js';
package/dist/index.js CHANGED
@@ -1,15 +1,135 @@
1
1
  import { DEFAULT_ENCODE_OPTIONS as e, arrowBetween as t, bboxAnnotationsToSvg as n, bboxAnnotationsToSvg as r, createAnnotator as i, decodeAndEncodeImage as a, rectForBoundingBox as o, textAt as s } from "@ingcreators/annot-annotator";
2
2
  import { expect as c, test as l } from "@playwright/test";
3
- import { writePngWithTagsOnly as u } from "@ingcreators/annot-core/xmp-bytes";
3
+ import { walkTree as u } from "@ingcreators/annot-core/element-tree";
4
+ import { writeFile as d } from "node:fs/promises";
5
+ import { writePngWithTagsOnly as f } from "@ingcreators/annot-core/xmp-bytes";
6
+ //#region src/element-tree-adapter.ts
7
+ function p(e) {
8
+ let t = e.yaml.split(/\r?\n/), n = 0, r = () => (n++, `e9${String(n).padStart(5, "0")}`), i = [], a = [];
9
+ for (let e of t) {
10
+ if (!e.trim()) continue;
11
+ let t = e.trimStart(), n = e.length - t.length;
12
+ if (!t.startsWith("- ")) continue;
13
+ let s = t.slice(2), c = 0;
14
+ for (; c < s.length;) {
15
+ let e = s.charCodeAt(c);
16
+ if (e < 97 || e > 122) break;
17
+ c++;
18
+ }
19
+ if (c === 0) continue;
20
+ let l = s.slice(0, c);
21
+ s = s.slice(c);
22
+ let u;
23
+ if (s.startsWith(" \"")) {
24
+ let e = s.indexOf("\"", 2);
25
+ e > 1 && (u = s.slice(2, e), s = s.slice(e + 1));
26
+ }
27
+ let d = s.trimEnd(), f = /:$/.test(d);
28
+ f && (d = d.slice(0, -1));
29
+ let p = [], m, h, g = d.match(/\[ref=(e\d+)\]/);
30
+ g && (m = g[1]);
31
+ let _ = d.match(/\[box=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\]/);
32
+ _ && (h = {
33
+ x: Number(_[1]),
34
+ y: Number(_[2]),
35
+ width: Number(_[3]),
36
+ height: Number(_[4])
37
+ });
38
+ let v = /\[([A-Za-z][A-Za-z0-9_=,.-]*)\]/g, y;
39
+ for (; (y = v.exec(d)) !== null;) {
40
+ let e = y[1] ?? "";
41
+ e.startsWith("ref=") || e.startsWith("box=") || p.push(e);
42
+ }
43
+ let b = {
44
+ ref: m ?? r(),
45
+ role: l,
46
+ ...u !== void 0 && u.length > 0 ? { name: u } : {},
47
+ ...h === void 0 ? {} : { bbox: h },
48
+ ...p.length > 0 ? { states: p } : {}
49
+ };
50
+ for (; a.length > 0 && (a[a.length - 1]?.indent ?? -1) >= n;) o(a.pop());
51
+ let x = {
52
+ indent: n,
53
+ node: b,
54
+ children: []
55
+ };
56
+ a.length === 0 ? i.push(b) : a[a.length - 1].children.push(b), f && a.push(x);
57
+ }
58
+ for (; a.length > 0;) o(a.pop());
59
+ function o(e) {
60
+ e.children.length > 0 && (e.node.children = e.children);
61
+ }
62
+ let s;
63
+ return s = i.length === 1 ? i[0] : {
64
+ ref: r(),
65
+ role: "generic",
66
+ children: i
67
+ }, {
68
+ version: 1,
69
+ source: {
70
+ kind: "playwright",
71
+ capturedAt: e.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
72
+ ...e.agent === void 0 ? {} : { agent: e.agent },
73
+ ...e.url === void 0 ? {} : { url: e.url }
74
+ },
75
+ viewport: e.viewport,
76
+ root: s
77
+ };
78
+ }
79
+ async function m(e, t, n) {
80
+ let r = [];
81
+ u(e, (e) => {
82
+ !e.role || !e.name || r.push({
83
+ node: e,
84
+ role: e.role,
85
+ name: e.name
86
+ });
87
+ });
88
+ for (let { node: e, role: i, name: a } of r) {
89
+ let r = t.getByRole(i, {
90
+ name: a,
91
+ exact: !0
92
+ });
93
+ if (await r.count() !== 1) continue;
94
+ let o = await r.evaluate((e, t) => {
95
+ let n = {};
96
+ for (let r of t) {
97
+ let t = e.getAttribute(r);
98
+ t !== null && (n[r] = t);
99
+ }
100
+ return n;
101
+ }, n.whitelist);
102
+ Object.keys(o).length !== 0 && (e.attributes = o);
103
+ }
104
+ return e;
105
+ }
106
+ async function h(e, t = {}) {
107
+ let n = await (t.rootLocator ?? e.locator("body")).ariaSnapshot({
108
+ mode: "ai",
109
+ boxes: !0
110
+ }), r = e.viewportSize(), i = p({
111
+ yaml: n,
112
+ viewport: r ? {
113
+ width: r.width,
114
+ height: r.height,
115
+ scale: 1
116
+ } : {
117
+ width: 0,
118
+ height: 0,
119
+ scale: 1
120
+ },
121
+ url: t.url ?? e.url(),
122
+ ...t.agent === void 0 ? {} : { agent: t.agent },
123
+ ...t.capturedAt === void 0 ? {} : { capturedAt: t.capturedAt }
124
+ });
125
+ return t.attributeWhitelist !== void 0 && t.attributeWhitelist.length > 0 && await m(i, e, { whitelist: t.attributeWhitelist }), i;
126
+ }
4
127
  //#endregion
5
128
  //#region src/rebase.ts
6
- var d = (/* @__PURE__ */ ((e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t.exports))(((e, t) => {
7
- t.exports = {};
8
- })))();
9
- function f(e, t) {
129
+ function g(e, t) {
10
130
  let n = [], r = [];
11
131
  for (let i of e) {
12
- let e = p(i, t);
132
+ let e = _(i, t);
13
133
  e ? n.push(e) : r.push(i);
14
134
  }
15
135
  return {
@@ -17,66 +137,79 @@ function f(e, t) {
17
137
  dropped: r
18
138
  };
19
139
  }
20
- function p(e, t) {
140
+ function _(e, t) {
21
141
  switch (e.type) {
22
- case "rect": return m(e, t);
23
- case "numberedBadge": return h(e, t);
24
- case "callout": return g(e, t);
25
- case "circle": return _(e, t);
26
- case "arrow": return v(e, t);
27
- case "text": return y(e, t);
28
- case "raw": return b(e);
142
+ case "rect": return v(e, t);
143
+ case "numberedBadge": return y(e, t);
144
+ case "callout": return b(e, t);
145
+ case "circle": return x(e, t);
146
+ case "arrow": return S(e, t);
147
+ case "text": return C(e, t);
148
+ case "freehand": return T(e);
149
+ case "focusMask": return E(e, t);
150
+ case "raw": return w(e);
29
151
  }
30
152
  }
31
- function m(e, t) {
32
- return C(e.bbox, t) ? {
153
+ function v(e, t) {
154
+ return k(e.bbox, t) ? {
33
155
  ...e,
34
- bbox: x(e.bbox, t)
156
+ bbox: D(e.bbox, t)
35
157
  } : null;
36
158
  }
37
- function h(e, t) {
38
- return C(e.bbox, t) ? {
159
+ function y(e, t) {
160
+ return k(e.bbox, t) ? {
39
161
  ...e,
40
- bbox: x(e.bbox, t),
162
+ bbox: D(e.bbox, t),
41
163
  imageWidth: t.width,
42
164
  imageHeight: t.height
43
165
  } : null;
44
166
  }
45
- function g(e, t) {
46
- return !C(e.targetBbox, t) || !w(e.at, t) ? null : {
167
+ function b(e, t) {
168
+ return !k(e.targetBbox, t) || !A(e.at, t) ? null : {
47
169
  ...e,
48
- at: S(e.at, t),
49
- targetBbox: x(e.targetBbox, t)
170
+ at: O(e.at, t),
171
+ targetBbox: D(e.targetBbox, t)
50
172
  };
51
173
  }
52
- function _(e, t) {
53
- return C({
174
+ function x(e, t) {
175
+ return k({
54
176
  x: e.center.x - e.radius,
55
177
  y: e.center.y - e.radius,
56
178
  width: e.radius * 2,
57
179
  height: e.radius * 2
58
180
  }, t) ? {
59
181
  ...e,
60
- center: S(e.center, t)
182
+ center: O(e.center, t)
61
183
  } : null;
62
184
  }
63
- function v(e, t) {
64
- return !w(e.from, t) || !w(e.to, t) ? null : {
185
+ function S(e, t) {
186
+ return !A(e.from, t) || !A(e.to, t) ? null : {
65
187
  ...e,
66
- from: S(e.from, t),
67
- to: S(e.to, t)
188
+ from: O(e.from, t),
189
+ to: O(e.to, t)
68
190
  };
69
191
  }
70
- function y(e, t) {
71
- return w(e.at, t) ? {
192
+ function C(e, t) {
193
+ return A(e.at, t) ? {
72
194
  ...e,
73
- at: S(e.at, t)
195
+ at: O(e.at, t)
74
196
  } : null;
75
197
  }
76
- function b(e) {
198
+ function w(e) {
77
199
  return e;
78
200
  }
79
- function x(e, t) {
201
+ function T(e) {
202
+ return e;
203
+ }
204
+ function E(e, t) {
205
+ return k(e.cutout, t) ? {
206
+ ...e,
207
+ cutout: D(e.cutout, t),
208
+ imageWidth: t.width,
209
+ imageHeight: t.height
210
+ } : null;
211
+ }
212
+ function D(e, t) {
80
213
  return {
81
214
  x: e.x - t.x,
82
215
  y: e.y - t.y,
@@ -84,19 +217,19 @@ function x(e, t) {
84
217
  height: e.height
85
218
  };
86
219
  }
87
- function S(e, t) {
220
+ function O(e, t) {
88
221
  return {
89
222
  x: e.x - t.x,
90
223
  y: e.y - t.y
91
224
  };
92
225
  }
93
- function C(e, t) {
226
+ function k(e, t) {
94
227
  return e.x >= t.x && e.y >= t.y && e.x + e.width <= t.x + t.width && e.y + e.height <= t.y + t.height;
95
228
  }
96
- function w(e, t) {
229
+ function A(e, t) {
97
230
  return e.x >= t.x && e.y >= t.y && e.x <= t.x + t.width && e.y <= t.y + t.height;
98
231
  }
99
- function T(e) {
232
+ function j(e) {
100
233
  switch (e.type) {
101
234
  case "rect":
102
235
  case "numberedBadge": return `${e.type}@(${e.bbox.x},${e.bbox.y},${e.bbox.width},${e.bbox.height})`;
@@ -104,47 +237,49 @@ function T(e) {
104
237
  case "circle": return `circle@(${e.center.x},${e.center.y},r=${e.radius})`;
105
238
  case "arrow": return `arrow@(${e.from.x},${e.from.y})→(${e.to.x},${e.to.y})`;
106
239
  case "text": return `text@(${e.at.x},${e.at.y})`;
240
+ case "freehand": return `freehand[d=${e.path.slice(0, 32)}${e.path.length > 32 ? "…" : ""}]`;
241
+ case "focusMask": return `focusMask@(${e.cutout.x},${e.cutout.y},${e.cutout.width},${e.cutout.height})`;
107
242
  case "raw": return "raw[<svg fragment>]";
108
243
  }
109
244
  }
110
245
  //#endregion
111
246
  //#region src/screenshot-hooks.ts
112
- var E = Symbol.for("@ingcreators/annot:screenshot-patched"), D = [], O = new Set([
247
+ var M = Symbol.for("@ingcreators/annot:screenshot-patched"), N = [], P = /* @__PURE__ */ new Set([
113
248
  "overlays",
114
249
  "tags",
115
250
  "editable"
116
251
  ]);
117
- function k(e) {
252
+ function F(e) {
118
253
  let t = e;
119
- if (t[E]) return;
254
+ if (t[M]) return;
120
255
  let n = e.screenshot;
121
256
  e.screenshot = async function(e = {}) {
122
257
  let t = e?.annot;
123
- return A(t) ? N.call(this, n, e) : n.call(this, e);
124
- }, t[E] = !0;
258
+ return I(t) ? z.call(this, n, e) : n.call(this, e);
259
+ }, t[M] = !0;
125
260
  }
126
- function A(e) {
261
+ function I(e) {
127
262
  if (!e || e === !0) return !1;
128
263
  if (e.overlays && e.overlays.length > 0 || e.tags) return !0;
129
- for (let t of Object.keys(e)) if (!O.has(t)) return !0;
264
+ for (let t of Object.keys(e)) if (!P.has(t)) return !0;
130
265
  return !1;
131
266
  }
132
- function j(e) {
267
+ function L(e) {
133
268
  return typeof e.boundingBox == "function";
134
269
  }
135
- function M(e) {
136
- return j(e) ? e.page() : e;
270
+ function R(e) {
271
+ return L(e) ? e.page() : e;
137
272
  }
138
- async function N(e, t) {
273
+ async function z(e, t) {
139
274
  let { annot: n, path: r, ...i } = t, a = i, o = n.editable ?? !0, s = {
140
275
  annot: n,
141
- page: M(this),
276
+ page: R(this),
142
277
  receiver: this
143
- }, c = (await Promise.all(D.map((e) => e(s)))).filter((e) => e !== null), l = (n.overlays?.length ?? 0) > 0 || !!n.tags;
278
+ }, c = (await Promise.all(N.map((e) => e(s)))).filter((e) => e !== null), l = (n.overlays?.length ?? 0) > 0 || !!n.tags;
144
279
  if (c.length === 0 && !l) return e.call(this, t);
145
280
  for (let e of c) e.prepare && await e.prepare();
146
- let u = await P(this, t.clip), f = await I({
147
- rawBytes: F(await e.call(this, {
281
+ let u = await B(this, t.clip), f = await H({
282
+ rawBytes: V(await e.call(this, {
148
283
  ...a,
149
284
  path: void 0
150
285
  })),
@@ -153,21 +288,21 @@ async function N(e, t) {
153
288
  clip: u,
154
289
  contributions: c
155
290
  });
156
- return r && await (0, d.writeFile)(r, f), Buffer.from(f);
291
+ return r && await d(r, f), Buffer.from(f);
157
292
  }
158
- async function P(e, t) {
159
- if (j(e)) {
293
+ async function B(e, t) {
294
+ if (L(e)) {
160
295
  let t = await e.boundingBox();
161
296
  if (!t) throw Error("locator.screenshot({ annot }): locator has no bounding box (probably not visible). Re-test with a stable selector / waitFor().");
162
297
  return t;
163
298
  }
164
299
  return t ?? null;
165
300
  }
166
- function F(e) {
301
+ function V(e) {
167
302
  return e instanceof Uint8Array && !(e instanceof Buffer) ? e : new Uint8Array(e.buffer, e.byteOffset, e.byteLength);
168
303
  }
169
- async function I(e) {
170
- let { rawBytes: t, annot: n, editable: r, clip: a, contributions: o } = e, s = B(t), c = [], l = a ? {
304
+ async function H(e) {
305
+ let { rawBytes: t, annot: n, editable: r, clip: a, contributions: o } = e, s = K(t), c = [], l = a ? {
171
306
  width: a.x + a.width,
172
307
  height: a.y + a.height
173
308
  } : s;
@@ -176,23 +311,23 @@ async function I(e) {
176
311
  c.push(...t);
177
312
  }
178
313
  n.overlays && c.push(...n.overlays);
179
- let d;
314
+ let u;
180
315
  if (a) {
181
- let { kept: e, dropped: t } = f(c, a);
182
- d = e, t.length > 0 && R(t);
183
- } else d = c;
184
- let p = o.length > 0 || !!n.overlays;
185
- if (p && !r) {
186
- let e = L(d), r = `data:image/png;base64,${Buffer.from(t).toString("base64")}`, a = i({ loadSystemFonts: !0 }).toPng({
316
+ let { kept: e, dropped: t } = g(c, a);
317
+ u = e, t.length > 0 && W(t);
318
+ } else u = c;
319
+ let d = o.length > 0 || !!n.overlays;
320
+ if (d && !r) {
321
+ let e = U(u), r = `data:image/png;base64,${Buffer.from(t).toString("base64")}`, a = i({ loadSystemFonts: !0 }).toPng({
187
322
  originalDataUrl: r,
188
323
  annotationsSvg: e,
189
324
  width: s.width,
190
325
  height: s.height
191
326
  });
192
- return n.tags ? u(a, n.tags) : a;
327
+ return n.tags ? f(a, n.tags) : a;
193
328
  }
194
- if (p) {
195
- let e = L(d), r = `data:image/png;base64,${Buffer.from(t).toString("base64")}`;
329
+ if (d) {
330
+ let e = U(u), r = `data:image/png;base64,${Buffer.from(t).toString("base64")}`;
196
331
  return i({ loadSystemFonts: !0 }).toEditablePng({
197
332
  originalDataUrl: r,
198
333
  annotationsSvg: e,
@@ -201,16 +336,16 @@ async function I(e) {
201
336
  tags: n.tags
202
337
  });
203
338
  }
204
- return n.tags ? u(t, n.tags) : t;
339
+ return n.tags ? f(t, n.tags) : t;
205
340
  }
206
- function L(e) {
341
+ function U(e) {
207
342
  return e.length === 0 ? "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>" : `<svg xmlns="http://www.w3.org/2000/svg">${r(e)}</svg>`;
208
343
  }
209
- function R(e) {
210
- let t = e.map(T).join(", "), n = `annot fixture: dropped ${e.length} overlay(s) outside the screenshot clip — ${t}`;
211
- console.warn(n), z(n, e);
344
+ function W(e) {
345
+ let t = e.map(j).join(", "), n = `annot fixture: dropped ${e.length} overlay(s) outside the screenshot clip — ${t}`;
346
+ console.warn(n), G(n, e);
212
347
  }
213
- function z(e, t) {
348
+ function G(e, t) {
214
349
  try {
215
350
  let n = l.info?.();
216
351
  if (!n?.annotations) return;
@@ -220,7 +355,7 @@ function z(e, t) {
220
355
  });
221
356
  } catch {}
222
357
  }
223
- function B(e) {
358
+ function K(e) {
224
359
  if (e.length < 24) throw Error("Playwright annot fixture: raw screenshot too short to contain PNG IHDR.");
225
360
  let t = new DataView(e.buffer, e.byteOffset, e.byteLength);
226
361
  return {
@@ -230,20 +365,20 @@ function B(e) {
230
365
  }
231
366
  //#endregion
232
367
  //#region src/fixture.ts
233
- var V = l.extend({
368
+ var q = l.extend({
234
369
  annotator: async ({}, e) => {
235
370
  let t = i();
236
371
  await e({
237
372
  raw: t,
238
- annotateScreenshot: (e, n) => H(t, e, n)
373
+ annotateScreenshot: (e, n) => J(t, e, n)
239
374
  });
240
375
  },
241
376
  page: async ({ page: e }, t) => {
242
- k(Object.getPrototypeOf(e)), k(Object.getPrototypeOf(e.locator("html"))), await t(e);
377
+ F(Object.getPrototypeOf(e)), F(Object.getPrototypeOf(e.locator("html"))), await t(e);
243
378
  }
244
379
  });
245
- async function H(t, n, i) {
246
- let o = await n.screenshot({ fullPage: i.fullPage }), { width: s, height: c } = U(o), l = `data:image/png;base64,${o.toString("base64")}`, u = "annotations" in i && i.annotations !== void 0 ? r(i.annotations) : i.annotationsSvg ?? "", d = t.toPng({
380
+ async function J(t, n, i) {
381
+ let o = await n.screenshot({ fullPage: i.fullPage }), { width: s, height: c } = Y(o), l = `data:image/png;base64,${o.toString("base64")}`, u = "annotations" in i && i.annotations !== void 0 ? r(i.annotations) : i.annotationsSvg ?? "", d = t.toPng({
247
382
  originalDataUrl: l,
248
383
  annotationsSvg: u,
249
384
  width: s,
@@ -254,7 +389,7 @@ async function H(t, n, i) {
254
389
  ...i.encode
255
390
  })).bytes;
256
391
  }
257
- function U(e) {
392
+ function Y(e) {
258
393
  if (e.length < 24) throw Error("PNG too short to contain IHDR chunk");
259
394
  let t = new DataView(e.buffer, e.byteOffset, e.byteLength);
260
395
  return {
@@ -263,4 +398,4 @@ function U(e) {
263
398
  };
264
399
  }
265
400
  //#endregion
266
- export { E as ANNOT_PATCHED, D as annotSourceResolvers, H as annotateScreenshot, t as arrowBetween, n as bboxAnnotationsToSvg, T as describeAnnotation, c as expect, k as patchScreenshot, f as rebaseAnnotations, o as rectForBoundingBox, V as test, s as textAt };
401
+ export { M as ANNOT_PATCHED, N as annotSourceResolvers, J as annotateScreenshot, t as arrowBetween, m as attachAttributes, n as bboxAnnotationsToSvg, h as captureElementTree, j as describeAnnotation, c as expect, F as patchScreenshot, p as playwrightYamlToElementTree, g as rebaseAnnotations, o as rectForBoundingBox, q as test, s as textAt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingcreators/annot-playwright",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Playwright fixture for annotated screenshots — emit annotated PNGs from failing tests without leaving the test file. Pairs `test.extend({ annotator })` with the headless renderer from @ingcreators/annot-annotator.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ingcreators",
@@ -42,14 +42,14 @@
42
42
  "access": "public"
43
43
  },
44
44
  "devDependencies": {
45
- "@playwright/test": "^1.50.0",
45
+ "@playwright/test": "^1.61.1",
46
46
  "typescript": "^6.0.3",
47
- "vite": "^8.0.13",
48
- "vite-plugin-dts": "^5.0.1"
47
+ "vite": "^8.0.16",
48
+ "vite-plugin-dts": "^5.0.3"
49
49
  },
50
50
  "dependencies": {
51
- "@ingcreators/annot-annotator": "0.5.0",
52
- "@ingcreators/annot-core": "0.2.1"
51
+ "@ingcreators/annot-annotator": "0.6.0",
52
+ "@ingcreators/annot-core": "0.3.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@playwright/test": "^1.40.0"