@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 +60 -0
- package/dist/element-tree-adapter.d.ts +134 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +220 -85
- package/package.json +6 -6
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
140
|
+
function _(e, t) {
|
|
21
141
|
switch (e.type) {
|
|
22
|
-
case "rect": return
|
|
23
|
-
case "numberedBadge": return
|
|
24
|
-
case "callout": return
|
|
25
|
-
case "circle": return
|
|
26
|
-
case "arrow": return
|
|
27
|
-
case "text": return
|
|
28
|
-
case "
|
|
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
|
|
32
|
-
return
|
|
153
|
+
function v(e, t) {
|
|
154
|
+
return k(e.bbox, t) ? {
|
|
33
155
|
...e,
|
|
34
|
-
bbox:
|
|
156
|
+
bbox: D(e.bbox, t)
|
|
35
157
|
} : null;
|
|
36
158
|
}
|
|
37
|
-
function
|
|
38
|
-
return
|
|
159
|
+
function y(e, t) {
|
|
160
|
+
return k(e.bbox, t) ? {
|
|
39
161
|
...e,
|
|
40
|
-
bbox:
|
|
162
|
+
bbox: D(e.bbox, t),
|
|
41
163
|
imageWidth: t.width,
|
|
42
164
|
imageHeight: t.height
|
|
43
165
|
} : null;
|
|
44
166
|
}
|
|
45
|
-
function
|
|
46
|
-
return !
|
|
167
|
+
function b(e, t) {
|
|
168
|
+
return !k(e.targetBbox, t) || !A(e.at, t) ? null : {
|
|
47
169
|
...e,
|
|
48
|
-
at:
|
|
49
|
-
targetBbox:
|
|
170
|
+
at: O(e.at, t),
|
|
171
|
+
targetBbox: D(e.targetBbox, t)
|
|
50
172
|
};
|
|
51
173
|
}
|
|
52
|
-
function
|
|
53
|
-
return
|
|
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:
|
|
182
|
+
center: O(e.center, t)
|
|
61
183
|
} : null;
|
|
62
184
|
}
|
|
63
|
-
function
|
|
64
|
-
return !
|
|
185
|
+
function S(e, t) {
|
|
186
|
+
return !A(e.from, t) || !A(e.to, t) ? null : {
|
|
65
187
|
...e,
|
|
66
|
-
from:
|
|
67
|
-
to:
|
|
188
|
+
from: O(e.from, t),
|
|
189
|
+
to: O(e.to, t)
|
|
68
190
|
};
|
|
69
191
|
}
|
|
70
|
-
function
|
|
71
|
-
return
|
|
192
|
+
function C(e, t) {
|
|
193
|
+
return A(e.at, t) ? {
|
|
72
194
|
...e,
|
|
73
|
-
at:
|
|
195
|
+
at: O(e.at, t)
|
|
74
196
|
} : null;
|
|
75
197
|
}
|
|
76
|
-
function
|
|
198
|
+
function w(e) {
|
|
77
199
|
return e;
|
|
78
200
|
}
|
|
79
|
-
function
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
252
|
+
function F(e) {
|
|
118
253
|
let t = e;
|
|
119
|
-
if (t[
|
|
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
|
|
124
|
-
}, t[
|
|
258
|
+
return I(t) ? z.call(this, n, e) : n.call(this, e);
|
|
259
|
+
}, t[M] = !0;
|
|
125
260
|
}
|
|
126
|
-
function
|
|
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 (!
|
|
264
|
+
for (let t of Object.keys(e)) if (!P.has(t)) return !0;
|
|
130
265
|
return !1;
|
|
131
266
|
}
|
|
132
|
-
function
|
|
267
|
+
function L(e) {
|
|
133
268
|
return typeof e.boundingBox == "function";
|
|
134
269
|
}
|
|
135
|
-
function
|
|
136
|
-
return
|
|
270
|
+
function R(e) {
|
|
271
|
+
return L(e) ? e.page() : e;
|
|
137
272
|
}
|
|
138
|
-
async function
|
|
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:
|
|
276
|
+
page: R(this),
|
|
142
277
|
receiver: this
|
|
143
|
-
}, c = (await Promise.all(
|
|
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
|
|
147
|
-
rawBytes:
|
|
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
|
|
291
|
+
return r && await d(r, f), Buffer.from(f);
|
|
157
292
|
}
|
|
158
|
-
async function
|
|
159
|
-
if (
|
|
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
|
|
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
|
|
170
|
-
let { rawBytes: t, annot: n, editable: r, clip: a, contributions: o } = e, s =
|
|
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
|
|
314
|
+
let u;
|
|
180
315
|
if (a) {
|
|
181
|
-
let { kept: e, dropped: t } =
|
|
182
|
-
|
|
183
|
-
} else
|
|
184
|
-
let
|
|
185
|
-
if (
|
|
186
|
-
let e =
|
|
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 ?
|
|
327
|
+
return n.tags ? f(a, n.tags) : a;
|
|
193
328
|
}
|
|
194
|
-
if (
|
|
195
|
-
let e =
|
|
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 ?
|
|
339
|
+
return n.tags ? f(t, n.tags) : t;
|
|
205
340
|
}
|
|
206
|
-
function
|
|
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
|
|
210
|
-
let t = e.map(
|
|
211
|
-
console.warn(n),
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
373
|
+
annotateScreenshot: (e, n) => J(t, e, n)
|
|
239
374
|
});
|
|
240
375
|
},
|
|
241
376
|
page: async ({ page: e }, t) => {
|
|
242
|
-
|
|
377
|
+
F(Object.getPrototypeOf(e)), F(Object.getPrototypeOf(e.locator("html"))), await t(e);
|
|
243
378
|
}
|
|
244
379
|
});
|
|
245
|
-
async function
|
|
246
|
-
let o = await n.screenshot({ fullPage: i.fullPage }), { width: s, height: c } =
|
|
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
|
|
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 {
|
|
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.
|
|
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.
|
|
45
|
+
"@playwright/test": "^1.61.1",
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
|
-
"vite": "^8.0.
|
|
48
|
-
"vite-plugin-dts": "^5.0.
|
|
47
|
+
"vite": "^8.0.16",
|
|
48
|
+
"vite-plugin-dts": "^5.0.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@ingcreators/annot-annotator": "0.
|
|
52
|
-
"@ingcreators/annot-core": "0.
|
|
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"
|