libretto 0.6.10 → 0.6.12
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/README.md +4 -0
- package/README.template.md +4 -0
- package/dist/cli/cli.js +4 -3
- package/dist/cli/commands/ai.js +3 -2
- package/dist/cli/commands/browser.js +17 -17
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +20 -34
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +81 -9
- package/dist/cli/commands/status.js +5 -4
- package/dist/cli/core/ai-model.js +6 -3
- package/dist/cli/core/browser.js +300 -121
- package/dist/cli/core/config.js +4 -2
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +535 -89
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +72 -6
- package/dist/cli/core/experiments.js +66 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/snapshot-analyzer.js +4 -3
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/router.js +4 -1
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/package.json +4 -2
- package/skills/libretto/SKILL.md +3 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +4 -3
- package/src/cli/commands/ai.ts +3 -2
- package/src/cli/commands/browser.ts +13 -11
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +18 -36
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +99 -11
- package/src/cli/commands/status.ts +5 -4
- package/src/cli/core/ai-model.ts +6 -3
- package/src/cli/core/browser.ts +369 -147
- package/src/cli/core/config.ts +3 -1
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +686 -106
- package/src/cli/core/daemon/ipc.ts +330 -214
- package/src/cli/core/daemon/snapshot.ts +106 -8
- package/src/cli/core/experiments.ts +85 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/snapshot-analyzer.ts +4 -3
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +85 -0
- package/src/cli/router.ts +4 -1
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
const MAX_ATTRIBUTE_NODE_LOOKUPS = 300;
|
|
2
|
+
const REFS_BY_ROLE = /* @__PURE__ */ new Set([
|
|
3
|
+
"RootWebArea",
|
|
4
|
+
"main",
|
|
5
|
+
"navigation",
|
|
6
|
+
"banner",
|
|
7
|
+
"contentinfo",
|
|
8
|
+
"form",
|
|
9
|
+
"search",
|
|
10
|
+
"article",
|
|
11
|
+
"section",
|
|
12
|
+
"region",
|
|
13
|
+
"heading",
|
|
14
|
+
"button",
|
|
15
|
+
"link",
|
|
16
|
+
"textbox",
|
|
17
|
+
"textField",
|
|
18
|
+
"checkbox",
|
|
19
|
+
"radio",
|
|
20
|
+
"switch",
|
|
21
|
+
"combobox",
|
|
22
|
+
"listbox",
|
|
23
|
+
"menuitem",
|
|
24
|
+
"tab",
|
|
25
|
+
"slider"
|
|
26
|
+
]);
|
|
27
|
+
const INTERESTING_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
28
|
+
"data-testid",
|
|
29
|
+
"data-test",
|
|
30
|
+
"data-qa",
|
|
31
|
+
"data-cy",
|
|
32
|
+
"id",
|
|
33
|
+
"name",
|
|
34
|
+
"type",
|
|
35
|
+
"placeholder",
|
|
36
|
+
"href",
|
|
37
|
+
"src",
|
|
38
|
+
"aria-label",
|
|
39
|
+
"aria-expanded",
|
|
40
|
+
"aria-pressed",
|
|
41
|
+
"aria-selected",
|
|
42
|
+
"aria-checked",
|
|
43
|
+
"role",
|
|
44
|
+
"title",
|
|
45
|
+
"alt",
|
|
46
|
+
"onclick",
|
|
47
|
+
"tabindex"
|
|
48
|
+
]);
|
|
49
|
+
const STATE_PROPERTY_NAMES = [
|
|
50
|
+
"level",
|
|
51
|
+
"disabled",
|
|
52
|
+
"checked",
|
|
53
|
+
"expanded",
|
|
54
|
+
"selected",
|
|
55
|
+
"pressed",
|
|
56
|
+
"focused",
|
|
57
|
+
"required",
|
|
58
|
+
"invalid",
|
|
59
|
+
"readonly",
|
|
60
|
+
"multiline",
|
|
61
|
+
"autocomplete",
|
|
62
|
+
"haspopup",
|
|
63
|
+
"value"
|
|
64
|
+
];
|
|
65
|
+
async function snapshot(page) {
|
|
66
|
+
const cdp = await page.context().newCDPSession(page);
|
|
67
|
+
try {
|
|
68
|
+
await enableIfSupported(cdp, "DOM.enable");
|
|
69
|
+
await enableIfSupported(cdp, "Accessibility.enable");
|
|
70
|
+
await enableIfSupported(cdp, "Runtime.enable");
|
|
71
|
+
const [title, frames] = await Promise.all([
|
|
72
|
+
page.title().catch(() => ""),
|
|
73
|
+
getFrameInfos(cdp)
|
|
74
|
+
]);
|
|
75
|
+
const snapshotFrames = [];
|
|
76
|
+
let nextRef = 1;
|
|
77
|
+
for (const [index, frame] of frames.entries()) {
|
|
78
|
+
const frameSnapshot = await captureFrameSnapshot(cdp, frame, index);
|
|
79
|
+
if (frameSnapshot.ok) {
|
|
80
|
+
nextRef = assignRefs(frameSnapshot.roots, nextRef);
|
|
81
|
+
snapshotFrames.push({
|
|
82
|
+
status: "ok",
|
|
83
|
+
id: frame.id,
|
|
84
|
+
index,
|
|
85
|
+
url: frame.url,
|
|
86
|
+
name: frame.name,
|
|
87
|
+
parentId: frame.parentId,
|
|
88
|
+
roots: frameSnapshot.roots.map(toSnapshotNode)
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
snapshotFrames.push({
|
|
92
|
+
status: "unavailable",
|
|
93
|
+
id: frame.id,
|
|
94
|
+
index,
|
|
95
|
+
url: frame.url,
|
|
96
|
+
name: frame.name,
|
|
97
|
+
parentId: frame.parentId,
|
|
98
|
+
error: frameSnapshot.error
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { title, url: page.url(), frames: snapshotFrames };
|
|
103
|
+
} finally {
|
|
104
|
+
await cdp.detach().catch(() => {
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function findSnapshotNodeByRef(snapshotTree, ref) {
|
|
109
|
+
const normalizedRef = normalizeRequestedRef(ref);
|
|
110
|
+
const matchingNode = findNodeByRef(snapshotTree, normalizedRef);
|
|
111
|
+
if (!matchingNode) {
|
|
112
|
+
throw new Error(`Snapshot ref "${ref}" was not found.`);
|
|
113
|
+
}
|
|
114
|
+
return matchingNode;
|
|
115
|
+
}
|
|
116
|
+
function scopeSnapshotToRef(snapshotTree, ref) {
|
|
117
|
+
const matchingNode = findSnapshotNodeByRef(snapshotTree, ref);
|
|
118
|
+
return {
|
|
119
|
+
...snapshotTree,
|
|
120
|
+
frames: snapshotTree.frames.flatMap((frame) => {
|
|
121
|
+
if (frame.status !== "ok") return [];
|
|
122
|
+
if (!frameContainsNode(frame.roots, matchingNode)) return [];
|
|
123
|
+
return [{ ...frame, roots: [matchingNode] }];
|
|
124
|
+
})
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function normalizeRequestedRef(ref) {
|
|
128
|
+
return ref.trim();
|
|
129
|
+
}
|
|
130
|
+
function findNodeByRef(snapshotTree, ref) {
|
|
131
|
+
const exact = findNode(snapshotTree, (node) => node.ref === ref);
|
|
132
|
+
if (exact) return exact;
|
|
133
|
+
const numericSuffix = ref.match(/^[a-zA-Z]+(\d+)$/)?.[1];
|
|
134
|
+
if (!numericSuffix) return null;
|
|
135
|
+
return findNode(
|
|
136
|
+
snapshotTree,
|
|
137
|
+
(node) => node.ref?.match(/^[a-zA-Z]+(\d+)$/)?.[1] === numericSuffix
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
function findNode(snapshotTree, predicate) {
|
|
141
|
+
for (const frame of snapshotTree.frames) {
|
|
142
|
+
if (frame.status !== "ok") continue;
|
|
143
|
+
for (const root of frame.roots) {
|
|
144
|
+
const node = findNodeInTree(root, predicate);
|
|
145
|
+
if (node) return node;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function findNodeInTree(node, predicate) {
|
|
151
|
+
if (predicate(node)) return node;
|
|
152
|
+
for (const child of node.children) {
|
|
153
|
+
const match = findNodeInTree(child, predicate);
|
|
154
|
+
if (match) return match;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function frameContainsNode(roots, target) {
|
|
159
|
+
return roots.some((root) => findNodeInTree(root, (node) => node === target));
|
|
160
|
+
}
|
|
161
|
+
async function enableIfSupported(cdp, method) {
|
|
162
|
+
try {
|
|
163
|
+
await cdp.send(method);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function captureFrameSnapshot(cdp, frame, frameIndex) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await cdp.send("Accessibility.getFullAXTree", {
|
|
170
|
+
frameId: frame.id
|
|
171
|
+
});
|
|
172
|
+
const rawNodes = parseAxNodes(response);
|
|
173
|
+
const attributeMap = await readAttributesByBackendNodeId(cdp, rawNodes);
|
|
174
|
+
return { ok: true, roots: buildSnapshotTree(rawNodes, attributeMap) };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (frameIndex !== 0) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const response = await cdp.send("Accessibility.getFullAXTree");
|
|
184
|
+
const rawNodes = parseAxNodes(response);
|
|
185
|
+
const attributeMap = await readAttributesByBackendNodeId(cdp, rawNodes);
|
|
186
|
+
return { ok: true, roots: buildSnapshotTree(rawNodes, attributeMap) };
|
|
187
|
+
} catch (fallbackError) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function getFrameInfos(cdp) {
|
|
196
|
+
try {
|
|
197
|
+
const response = await cdp.send("Page.getFrameTree");
|
|
198
|
+
const frames = parseFrameTree(response);
|
|
199
|
+
return frames.length > 0 ? frames : [{ id: "main", url: "", name: null, parentId: null }];
|
|
200
|
+
} catch {
|
|
201
|
+
return [{ id: "main", url: "", name: null, parentId: null }];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function parseFrameTree(response) {
|
|
205
|
+
const root = readRecord(response).frameTree;
|
|
206
|
+
const frames = [];
|
|
207
|
+
function visit(value, inheritedParentId) {
|
|
208
|
+
const tree = readRecord(value);
|
|
209
|
+
const frame = readRecord(tree.frame);
|
|
210
|
+
const id = readString(frame.id);
|
|
211
|
+
if (!id) return;
|
|
212
|
+
frames.push({
|
|
213
|
+
id,
|
|
214
|
+
url: readString(frame.url) ?? "",
|
|
215
|
+
name: readString(frame.name),
|
|
216
|
+
parentId: readString(frame.parentId) ?? inheritedParentId
|
|
217
|
+
});
|
|
218
|
+
const childFrames = Array.isArray(tree.childFrames) ? tree.childFrames : [];
|
|
219
|
+
for (const child of childFrames) visit(child, id);
|
|
220
|
+
}
|
|
221
|
+
visit(root, null);
|
|
222
|
+
return frames;
|
|
223
|
+
}
|
|
224
|
+
function parseAxNodes(response) {
|
|
225
|
+
const nodes = readRecord(response).nodes;
|
|
226
|
+
if (!Array.isArray(nodes)) return [];
|
|
227
|
+
return nodes.map(parseAxNode);
|
|
228
|
+
}
|
|
229
|
+
function parseAxNode(value) {
|
|
230
|
+
const record = readRecord(value);
|
|
231
|
+
const nodeId = readString(record.nodeId) ?? "";
|
|
232
|
+
const childIds = Array.isArray(record.childIds) ? record.childIds.map(readString).filter((id) => id !== null) : [];
|
|
233
|
+
return {
|
|
234
|
+
nodeId,
|
|
235
|
+
parentId: readString(record.parentId),
|
|
236
|
+
ignored: readBoolean(record.ignored) ?? false,
|
|
237
|
+
role: readAxValueString(record.role) ?? "unknown",
|
|
238
|
+
name: readAxValueString(record.name),
|
|
239
|
+
value: readAxPrimitive(record.value),
|
|
240
|
+
description: readAxValueString(record.description),
|
|
241
|
+
properties: parseAxProperties(record.properties),
|
|
242
|
+
childIds,
|
|
243
|
+
backendDOMNodeId: readNumber(record.backendDOMNodeId)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseAxProperties(value) {
|
|
247
|
+
if (!Array.isArray(value)) return [];
|
|
248
|
+
const properties = [];
|
|
249
|
+
for (const item of value) {
|
|
250
|
+
const record = readRecord(item);
|
|
251
|
+
const name = readString(record.name);
|
|
252
|
+
if (!name) continue;
|
|
253
|
+
properties.push({ name, value: readAxPrimitive(record.value) });
|
|
254
|
+
}
|
|
255
|
+
return properties;
|
|
256
|
+
}
|
|
257
|
+
async function readAttributesByBackendNodeId(cdp, nodes) {
|
|
258
|
+
const backendNodeIds = unique(
|
|
259
|
+
nodes.filter(shouldReadAttributes).sort((a, b) => attributeLookupPriority(a) - attributeLookupPriority(b)).map((node) => node.backendDOMNodeId).filter((id) => id !== null)
|
|
260
|
+
).slice(0, MAX_ATTRIBUTE_NODE_LOOKUPS);
|
|
261
|
+
const result = /* @__PURE__ */ new Map();
|
|
262
|
+
await Promise.all(
|
|
263
|
+
backendNodeIds.map(async (backendNodeId) => {
|
|
264
|
+
const attributes = await readAttributesForBackendNodeId(
|
|
265
|
+
cdp,
|
|
266
|
+
backendNodeId
|
|
267
|
+
);
|
|
268
|
+
if (Object.keys(attributes).length > 0) {
|
|
269
|
+
result.set(backendNodeId, attributes);
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function shouldReadAttributes(node) {
|
|
276
|
+
if (node.ignored) return false;
|
|
277
|
+
if (node.backendDOMNodeId === null) return false;
|
|
278
|
+
if (REFS_BY_ROLE.has(node.role)) return true;
|
|
279
|
+
if (node.role === "generic" || node.role === "group") return true;
|
|
280
|
+
if (node.name && node.name.trim().length > 0) return true;
|
|
281
|
+
return node.properties.some(
|
|
282
|
+
(property) => STATE_PROPERTY_NAMES.includes(property.name)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
function attributeLookupPriority(node) {
|
|
286
|
+
if (REFS_BY_ROLE.has(node.role)) return 0;
|
|
287
|
+
if (node.name && node.name.trim().length > 0) return 1;
|
|
288
|
+
if (node.properties.some(
|
|
289
|
+
(property) => STATE_PROPERTY_NAMES.includes(property.name)
|
|
290
|
+
)) {
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
return 2;
|
|
294
|
+
}
|
|
295
|
+
async function readAttributesForBackendNodeId(cdp, backendNodeId) {
|
|
296
|
+
try {
|
|
297
|
+
const response = await cdp.send("DOM.describeNode", {
|
|
298
|
+
backendNodeId,
|
|
299
|
+
depth: 0,
|
|
300
|
+
pierce: false
|
|
301
|
+
});
|
|
302
|
+
const node = readRecord(readRecord(response).node);
|
|
303
|
+
const rawAttributes = Array.isArray(node.attributes) ? node.attributes : [];
|
|
304
|
+
const attributes = {};
|
|
305
|
+
for (let i = 0; i < rawAttributes.length - 1; i += 2) {
|
|
306
|
+
const name = readString(rawAttributes[i]);
|
|
307
|
+
const value = readString(rawAttributes[i + 1]);
|
|
308
|
+
if (name && value !== null && INTERESTING_ATTRIBUTES.has(name)) {
|
|
309
|
+
if (name === "tabindex" && Number(value) < 0) continue;
|
|
310
|
+
attributes[name] = value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const cursor = await readComputedCursorForBackendNodeId(cdp, backendNodeId);
|
|
314
|
+
if (cursor === "pointer") attributes.cursor = cursor;
|
|
315
|
+
return attributes;
|
|
316
|
+
} catch {
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function readComputedCursorForBackendNodeId(cdp, backendDOMNodeId) {
|
|
321
|
+
let objectId = null;
|
|
322
|
+
try {
|
|
323
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
324
|
+
backendNodeId: backendDOMNodeId
|
|
325
|
+
});
|
|
326
|
+
objectId = readString(readRecord(readRecord(resolved).object).objectId);
|
|
327
|
+
if (!objectId) return null;
|
|
328
|
+
const response = await cdp.send("Runtime.callFunctionOn", {
|
|
329
|
+
objectId,
|
|
330
|
+
functionDeclaration: "function() { return getComputedStyle(this).cursor; }",
|
|
331
|
+
returnByValue: true,
|
|
332
|
+
silent: true
|
|
333
|
+
});
|
|
334
|
+
return readString(readRecord(readRecord(response).result).value);
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
} finally {
|
|
338
|
+
if (objectId) {
|
|
339
|
+
await cdp.send("Runtime.releaseObject", { objectId }).catch(() => {
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function buildSnapshotTree(rawNodes, attributesByBackendNodeId) {
|
|
345
|
+
const byId = /* @__PURE__ */ new Map();
|
|
346
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
347
|
+
for (const rawNode of rawNodes) {
|
|
348
|
+
if (!rawNode.nodeId) continue;
|
|
349
|
+
byId.set(rawNode.nodeId, {
|
|
350
|
+
nodeId: rawNode.nodeId,
|
|
351
|
+
ignored: rawNode.ignored,
|
|
352
|
+
role: rawNode.role,
|
|
353
|
+
name: rawNode.name,
|
|
354
|
+
value: rawNode.value,
|
|
355
|
+
description: rawNode.description,
|
|
356
|
+
properties: Object.fromEntries(
|
|
357
|
+
rawNode.properties.map((property) => [property.name, property.value])
|
|
358
|
+
),
|
|
359
|
+
attributes: rawNode.backendDOMNodeId !== null ? attributesByBackendNodeId.get(rawNode.backendDOMNodeId) ?? {} : {},
|
|
360
|
+
childIds: rawNode.childIds,
|
|
361
|
+
children: [],
|
|
362
|
+
parent: null,
|
|
363
|
+
ref: null,
|
|
364
|
+
subtreeSize: 1
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
for (const node of byId.values()) {
|
|
368
|
+
for (const childId of node.childIds) {
|
|
369
|
+
const child = byId.get(childId);
|
|
370
|
+
if (!child) continue;
|
|
371
|
+
child.parent = node;
|
|
372
|
+
node.children.push(child);
|
|
373
|
+
childIds.add(childId);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const rawNode of rawNodes) {
|
|
377
|
+
if (!rawNode.parentId || childIds.has(rawNode.nodeId)) continue;
|
|
378
|
+
const node = byId.get(rawNode.nodeId);
|
|
379
|
+
const parent = byId.get(rawNode.parentId);
|
|
380
|
+
if (!node || !parent) continue;
|
|
381
|
+
node.parent = parent;
|
|
382
|
+
parent.children.push(node);
|
|
383
|
+
childIds.add(rawNode.nodeId);
|
|
384
|
+
}
|
|
385
|
+
const roots = [...byId.values()].filter((node) => !childIds.has(node.nodeId));
|
|
386
|
+
for (const root of roots) annotateSubtreeSize(root);
|
|
387
|
+
return roots;
|
|
388
|
+
}
|
|
389
|
+
function annotateSubtreeSize(node) {
|
|
390
|
+
node.subtreeSize = 1;
|
|
391
|
+
for (const child of node.children) {
|
|
392
|
+
node.subtreeSize += annotateSubtreeSize(child);
|
|
393
|
+
}
|
|
394
|
+
return node.subtreeSize;
|
|
395
|
+
}
|
|
396
|
+
function assignRefs(nodes, nextRef) {
|
|
397
|
+
for (const node of nodes) {
|
|
398
|
+
if (shouldAssignRef(node)) {
|
|
399
|
+
node.ref = `l${nextRef}`;
|
|
400
|
+
nextRef += 1;
|
|
401
|
+
}
|
|
402
|
+
nextRef = assignRefs(node.children, nextRef);
|
|
403
|
+
}
|
|
404
|
+
return nextRef;
|
|
405
|
+
}
|
|
406
|
+
function toSnapshotNode(node) {
|
|
407
|
+
return {
|
|
408
|
+
nodeId: node.nodeId,
|
|
409
|
+
ignored: node.ignored,
|
|
410
|
+
role: node.role,
|
|
411
|
+
name: node.name,
|
|
412
|
+
value: node.value,
|
|
413
|
+
description: node.description,
|
|
414
|
+
properties: node.properties,
|
|
415
|
+
attributes: node.attributes,
|
|
416
|
+
children: node.children.map(toSnapshotNode),
|
|
417
|
+
ref: node.ref,
|
|
418
|
+
subtreeSize: node.subtreeSize
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function shouldAssignRef(node) {
|
|
422
|
+
if (node.ignored) return false;
|
|
423
|
+
if (node.role === "StaticText" || node.role === "InlineTextBox") return false;
|
|
424
|
+
if (node.role === "none" || node.role === "presentation") return false;
|
|
425
|
+
if (REFS_BY_ROLE.has(node.role)) return true;
|
|
426
|
+
if (Object.keys(node.attributes).length > 0) return true;
|
|
427
|
+
return Boolean(node.name);
|
|
428
|
+
}
|
|
429
|
+
function readRecord(value) {
|
|
430
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
431
|
+
}
|
|
432
|
+
function readString(value) {
|
|
433
|
+
return typeof value === "string" ? value : null;
|
|
434
|
+
}
|
|
435
|
+
function readNumber(value) {
|
|
436
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
437
|
+
}
|
|
438
|
+
function readBoolean(value) {
|
|
439
|
+
return typeof value === "boolean" ? value : null;
|
|
440
|
+
}
|
|
441
|
+
function readAxValueString(value) {
|
|
442
|
+
const rawValue = readRecord(value).value;
|
|
443
|
+
if (typeof rawValue === "string") return rawValue;
|
|
444
|
+
if (typeof rawValue === "number" || typeof rawValue === "boolean") {
|
|
445
|
+
return String(rawValue);
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function readAxPrimitive(value) {
|
|
450
|
+
const rawValue = readRecord(value).value;
|
|
451
|
+
if (typeof rawValue === "string" || typeof rawValue === "number" || typeof rawValue === "boolean") {
|
|
452
|
+
return rawValue;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
function unique(values) {
|
|
457
|
+
return [...new Set(values)];
|
|
458
|
+
}
|
|
459
|
+
export {
|
|
460
|
+
findSnapshotNodeByRef,
|
|
461
|
+
scopeSnapshotToRef,
|
|
462
|
+
snapshot
|
|
463
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Snapshot } from './types.js';
|
|
2
|
+
import { RenderedSnapshotFrame, RenderedSnapshotNode, RenderedSnapshotChild } from './render-snapshot.js';
|
|
3
|
+
|
|
4
|
+
type SnapshotDiff = {
|
|
5
|
+
before: Snapshot;
|
|
6
|
+
after: Snapshot;
|
|
7
|
+
changed: boolean;
|
|
8
|
+
pageChanged: boolean;
|
|
9
|
+
frames: SnapshotFrameDiff[];
|
|
10
|
+
};
|
|
11
|
+
type SnapshotFrameDiff = {
|
|
12
|
+
type: "context";
|
|
13
|
+
frame: RenderedSnapshotFrame;
|
|
14
|
+
children: SnapshotDiffChild[];
|
|
15
|
+
} | {
|
|
16
|
+
type: "modified";
|
|
17
|
+
before: RenderedSnapshotFrame;
|
|
18
|
+
after: RenderedSnapshotFrame;
|
|
19
|
+
} | {
|
|
20
|
+
type: "added";
|
|
21
|
+
frame: RenderedSnapshotFrame;
|
|
22
|
+
} | {
|
|
23
|
+
type: "removed";
|
|
24
|
+
frame: RenderedSnapshotFrame;
|
|
25
|
+
};
|
|
26
|
+
type SnapshotDiffChild = SnapshotNodeDiff | SnapshotTextDiff;
|
|
27
|
+
type SnapshotNodeDiff = {
|
|
28
|
+
kind: "node";
|
|
29
|
+
type: "context";
|
|
30
|
+
node: RenderedSnapshotNode;
|
|
31
|
+
children: SnapshotDiffChild[];
|
|
32
|
+
} | {
|
|
33
|
+
kind: "node";
|
|
34
|
+
type: "modified";
|
|
35
|
+
before: RenderedSnapshotNode;
|
|
36
|
+
after: RenderedSnapshotNode;
|
|
37
|
+
children: SnapshotDiffChild[];
|
|
38
|
+
} | {
|
|
39
|
+
kind: "node";
|
|
40
|
+
type: "added";
|
|
41
|
+
node: RenderedSnapshotNode;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "node";
|
|
44
|
+
type: "removed";
|
|
45
|
+
node: RenderedSnapshotNode;
|
|
46
|
+
};
|
|
47
|
+
type SnapshotTextDiff = {
|
|
48
|
+
kind: "text";
|
|
49
|
+
type: "modified";
|
|
50
|
+
before: Extract<RenderedSnapshotChild, {
|
|
51
|
+
kind: "text";
|
|
52
|
+
}>;
|
|
53
|
+
after: Extract<RenderedSnapshotChild, {
|
|
54
|
+
kind: "text";
|
|
55
|
+
}>;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "text";
|
|
58
|
+
type: "added";
|
|
59
|
+
node: Extract<RenderedSnapshotChild, {
|
|
60
|
+
kind: "text";
|
|
61
|
+
}>;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "text";
|
|
64
|
+
type: "removed";
|
|
65
|
+
node: Extract<RenderedSnapshotChild, {
|
|
66
|
+
kind: "text";
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
69
|
+
declare function diffSnapshots(before: Snapshot, after: Snapshot): SnapshotDiff;
|
|
70
|
+
declare function renderSnapshotDiff(diff: SnapshotDiff): string;
|
|
71
|
+
|
|
72
|
+
export { type SnapshotDiff, type SnapshotDiffChild, type SnapshotFrameDiff, type SnapshotNodeDiff, type SnapshotTextDiff, diffSnapshots, renderSnapshotDiff };
|