@skyramp/mcp 0.1.7 → 0.2.0-rc.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/build/playwright/registerPlaywrightTools.js +12 -0
- package/build/playwright/traceRecordingPrompt.js +15 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +31 -0
- package/build/prompts/test-recommendation/recommendationSections.js +1 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +94 -0
- package/build/prompts/testbot/testbot-prompts.js +115 -11
- package/build/prompts/testbot/testbot-prompts.test.js +79 -0
- package/build/resources/testbotResource.js +1 -1
- package/build/services/ScenarioGenerationService.integration.test.js +158 -0
- package/build/services/ScenarioGenerationService.js +36 -3
- package/build/services/ScenarioGenerationService.test.js +158 -22
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +2 -0
- package/build/tools/test-management/analyzeChangesTool.js +7 -1
- package/build/utils/routeParsers.js +12 -0
- package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
- package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1161 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +250 -0
- package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +298 -0
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
- package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
- package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
- package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
- package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
- package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
- package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
- package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
- package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
- package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
- package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
- package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +129 -0
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +137 -0
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
- package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
- package/node_modules/playwright/package.json +1 -1
- package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var blueprint_exports = {};
|
|
20
|
+
__export(blueprint_exports, {
|
|
21
|
+
BlueprintInvariantError: () => BlueprintInvariantError,
|
|
22
|
+
applyPositionHintToNamelessElements: () => applyPositionHintToNamelessElements,
|
|
23
|
+
buildMap: () => buildMap,
|
|
24
|
+
buildOutline: () => buildOutline,
|
|
25
|
+
buildPageBlueprint: () => buildPageBlueprint,
|
|
26
|
+
deriveLogicalName: () => deriveLogicalName,
|
|
27
|
+
resolveAccessibleName: () => resolveAccessibleName,
|
|
28
|
+
resolveTemplate: () => resolveTemplate,
|
|
29
|
+
resolveXpath: () => resolveXpath,
|
|
30
|
+
validateElement: () => validateElement,
|
|
31
|
+
validateRepeatingElement: () => validateRepeatingElement,
|
|
32
|
+
xpathLiteralNode: () => xpathLiteralNode
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(blueprint_exports);
|
|
35
|
+
var import_serialization = require("./serialization");
|
|
36
|
+
var import_sectionGrouper = require("./sectionGrouper");
|
|
37
|
+
var import_logicalNameResolver = require("./logicalNameResolver");
|
|
38
|
+
var import_slug = require("./slug");
|
|
39
|
+
class BlueprintInvariantError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "BlueprintInvariantError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const CANONICAL_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
46
|
+
"button",
|
|
47
|
+
"link",
|
|
48
|
+
"textbox",
|
|
49
|
+
"searchbox",
|
|
50
|
+
"combobox",
|
|
51
|
+
"checkbox",
|
|
52
|
+
"radio",
|
|
53
|
+
"switch",
|
|
54
|
+
"slider",
|
|
55
|
+
"spinbutton",
|
|
56
|
+
"tab",
|
|
57
|
+
"tabpanel",
|
|
58
|
+
"menuitem",
|
|
59
|
+
"menu",
|
|
60
|
+
"menubar",
|
|
61
|
+
"option",
|
|
62
|
+
"listbox",
|
|
63
|
+
"heading",
|
|
64
|
+
"banner",
|
|
65
|
+
"navigation",
|
|
66
|
+
"main",
|
|
67
|
+
"dialog",
|
|
68
|
+
"alertdialog",
|
|
69
|
+
"region",
|
|
70
|
+
"form",
|
|
71
|
+
"search",
|
|
72
|
+
"toolbar",
|
|
73
|
+
"tree",
|
|
74
|
+
"treeitem",
|
|
75
|
+
"grid",
|
|
76
|
+
"gridcell",
|
|
77
|
+
"row",
|
|
78
|
+
"columnheader",
|
|
79
|
+
"rowheader"
|
|
80
|
+
]);
|
|
81
|
+
const ROLE_SUFFIX = {
|
|
82
|
+
button: "btn",
|
|
83
|
+
link: "link",
|
|
84
|
+
textbox: "input",
|
|
85
|
+
searchbox: "input",
|
|
86
|
+
combobox: "select",
|
|
87
|
+
checkbox: "checkbox",
|
|
88
|
+
radio: "radio",
|
|
89
|
+
heading: "heading",
|
|
90
|
+
tab: "tab",
|
|
91
|
+
menuitem: "item",
|
|
92
|
+
switch: "toggle",
|
|
93
|
+
spinbutton: "input",
|
|
94
|
+
slider: "slider",
|
|
95
|
+
option: "option"
|
|
96
|
+
};
|
|
97
|
+
function deriveLogicalName(role, accessibleName) {
|
|
98
|
+
const suffix = ROLE_SUFFIX[role] ?? role.toLowerCase();
|
|
99
|
+
const base = (0, import_slug.slug)(accessibleName);
|
|
100
|
+
if (!base) return "";
|
|
101
|
+
const baseTokens = base.split("_");
|
|
102
|
+
if (baseTokens[baseTokens.length - 1] === suffix) baseTokens.pop();
|
|
103
|
+
if (baseTokens[0] === suffix) baseTokens.shift();
|
|
104
|
+
const cleanedBase = baseTokens.join("_");
|
|
105
|
+
if (!cleanedBase) return "";
|
|
106
|
+
return `${cleanedBase}_${suffix}`;
|
|
107
|
+
}
|
|
108
|
+
function applyPositionHintToNamelessElements(raws) {
|
|
109
|
+
const sameRoleCountsBySection = /* @__PURE__ */ new Map();
|
|
110
|
+
return raws.map(({ role, accessibleName, sectionName }) => {
|
|
111
|
+
const derived = deriveLogicalName(role, accessibleName);
|
|
112
|
+
if (derived) return derived;
|
|
113
|
+
const key = `${sectionName}::${role}`;
|
|
114
|
+
const next = (sameRoleCountsBySection.get(key) ?? 0) + 1;
|
|
115
|
+
sameRoleCountsBySection.set(key, next);
|
|
116
|
+
return `${role}_${next}`;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function xpathLiteralNode(value) {
|
|
120
|
+
if (!value.includes('"'))
|
|
121
|
+
return `"${value}"`;
|
|
122
|
+
if (!value.includes("'"))
|
|
123
|
+
return `'${value}'`;
|
|
124
|
+
const parts = value.split('"');
|
|
125
|
+
const pieces = [];
|
|
126
|
+
for (let i = 0; i < parts.length; i++) {
|
|
127
|
+
if (parts[i].length > 0) {
|
|
128
|
+
const seg = parts[i];
|
|
129
|
+
pieces.push(seg.includes("'") ? `"${seg}"` : `'${seg}'`);
|
|
130
|
+
}
|
|
131
|
+
if (i < parts.length - 1)
|
|
132
|
+
pieces.push(`'"'`);
|
|
133
|
+
}
|
|
134
|
+
return pieces.length === 1 ? pieces[0] : `concat(${pieces.join(", ")})`;
|
|
135
|
+
}
|
|
136
|
+
async function domEvaluationScript() {
|
|
137
|
+
const DESTRUCTIVE_VERBS = /(delete|remove|destroy|submit|send|confirm|place\s*order|checkout|pay|purchase|buy|charge|cancel|close\s*account|deactivate|publish|approve|reject|logout|sign\s*out|save|create|update|add|edit|clear|reset|duplicate|clone|enable|disable|toggle|import|export|upload|download|invite|subscribe|unsubscribe|favorite|unfavorite|follow|unfollow|rename|archive|restore)/i;
|
|
138
|
+
const VIEW_VERBS = /(view|show|open|see|preview|browse|navigate|go\s*to|back|next|previous|search|filter|sort|paginate|expand|collapse|select|choose|display|refresh|reload|close|dismiss|cancel\s*close|copy)/i;
|
|
139
|
+
const SAFE_LANDMARK_DEFAULTS = /* @__PURE__ */ new Set(["navigation", "banner", "footer", "contentinfo"]);
|
|
140
|
+
const LIVE_REGION_ROLES = /* @__PURE__ */ new Set(["status", "alert", "log", "marquee"]);
|
|
141
|
+
function computeFramePath(el) {
|
|
142
|
+
let node = el;
|
|
143
|
+
while (node) {
|
|
144
|
+
if (node instanceof ShadowRoot)
|
|
145
|
+
return [];
|
|
146
|
+
node = node.parentNode || node.host;
|
|
147
|
+
if (node === document)
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
function hasShadowRootAncestor(el) {
|
|
153
|
+
let node = el;
|
|
154
|
+
while (node) {
|
|
155
|
+
if (node instanceof ShadowRoot)
|
|
156
|
+
return true;
|
|
157
|
+
node = node.parentNode;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function idUnique(id) {
|
|
162
|
+
try {
|
|
163
|
+
return document.querySelectorAll(`#${CSS.escape(id)}`).length === 1;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function testIdUnique(testId) {
|
|
169
|
+
try {
|
|
170
|
+
return document.querySelectorAll(`[data-testid=${CSS.escape(testId)}]`).length === 1;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function xpathLiteral(value) {
|
|
176
|
+
if (!value.includes('"'))
|
|
177
|
+
return `"${value}"`;
|
|
178
|
+
if (!value.includes("'"))
|
|
179
|
+
return `'${value}'`;
|
|
180
|
+
const parts = value.split('"');
|
|
181
|
+
const pieces = [];
|
|
182
|
+
for (let i = 0; i < parts.length; i++) {
|
|
183
|
+
if (parts[i].length > 0) {
|
|
184
|
+
const seg = parts[i];
|
|
185
|
+
pieces.push(seg.includes("'") ? `"${seg}"` : `'${seg}'`);
|
|
186
|
+
}
|
|
187
|
+
if (i < parts.length - 1)
|
|
188
|
+
pieces.push(`'"'`);
|
|
189
|
+
}
|
|
190
|
+
return pieces.length === 1 ? pieces[0] : `concat(${pieces.join(", ")})`;
|
|
191
|
+
}
|
|
192
|
+
function getXPath(el) {
|
|
193
|
+
const testId = el.getAttribute("data-testid");
|
|
194
|
+
if (testId && testIdUnique(testId))
|
|
195
|
+
return `//*[@data-testid=${xpathLiteral(testId)}]`;
|
|
196
|
+
if (el.id && idUnique(el.id))
|
|
197
|
+
return `//*[@id=${xpathLiteral(el.id)}]`;
|
|
198
|
+
const parts = [];
|
|
199
|
+
let node = el;
|
|
200
|
+
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
201
|
+
if (node.id && idUnique(node.id)) {
|
|
202
|
+
parts.unshift(`*[@id=${xpathLiteral(node.id)}]`);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const parent = node.parentElement;
|
|
206
|
+
const tagName = node.tagName.toLowerCase();
|
|
207
|
+
if (parent) {
|
|
208
|
+
const sameTagSiblings = Array.from(parent.children).filter((s) => s.tagName === node.tagName);
|
|
209
|
+
if (sameTagSiblings.length > 1) {
|
|
210
|
+
const idx = sameTagSiblings.indexOf(node) + 1;
|
|
211
|
+
parts.unshift(`${tagName}[${idx}]`);
|
|
212
|
+
} else {
|
|
213
|
+
parts.unshift(tagName);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
parts.unshift(tagName);
|
|
217
|
+
}
|
|
218
|
+
node = parent;
|
|
219
|
+
}
|
|
220
|
+
return "//" + parts.join("/");
|
|
221
|
+
}
|
|
222
|
+
const NAME_FROM_CONTENT_PROHIBITED = /* @__PURE__ */ new Set([
|
|
223
|
+
"combobox",
|
|
224
|
+
"listbox",
|
|
225
|
+
"menu",
|
|
226
|
+
"menubar",
|
|
227
|
+
"tree",
|
|
228
|
+
"treeitem",
|
|
229
|
+
"tablist",
|
|
230
|
+
"tabpanel",
|
|
231
|
+
"dialog",
|
|
232
|
+
"alertdialog",
|
|
233
|
+
"grid",
|
|
234
|
+
"rowgroup",
|
|
235
|
+
"group",
|
|
236
|
+
"region",
|
|
237
|
+
"form"
|
|
238
|
+
]);
|
|
239
|
+
function getAccessibleName(el, role) {
|
|
240
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
241
|
+
if (ariaLabel && ariaLabel.trim())
|
|
242
|
+
return ariaLabel.trim();
|
|
243
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
244
|
+
if (labelledBy) {
|
|
245
|
+
const names = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
246
|
+
if (names.length)
|
|
247
|
+
return names.join(" ");
|
|
248
|
+
}
|
|
249
|
+
const title = el.getAttribute("title");
|
|
250
|
+
if (title && title.trim())
|
|
251
|
+
return title.trim();
|
|
252
|
+
const placeholder = el.getAttribute("placeholder");
|
|
253
|
+
if (placeholder && placeholder.trim())
|
|
254
|
+
return placeholder.trim();
|
|
255
|
+
const alt = el.getAttribute("alt");
|
|
256
|
+
if (alt && alt.trim())
|
|
257
|
+
return alt.trim();
|
|
258
|
+
if (role && NAME_FROM_CONTENT_PROHIBITED.has(role))
|
|
259
|
+
return "";
|
|
260
|
+
const text = el.textContent?.trim().replace(/\s+/g, " ").slice(0, 60);
|
|
261
|
+
if (text)
|
|
262
|
+
return text;
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
const NATIVE_TAG_TO_ROLE = {
|
|
266
|
+
button: "button",
|
|
267
|
+
a: "link",
|
|
268
|
+
select: "combobox",
|
|
269
|
+
textarea: "textbox"
|
|
270
|
+
};
|
|
271
|
+
function getRole(el) {
|
|
272
|
+
const explicit = el.getAttribute("role");
|
|
273
|
+
const tag = el.tagName.toLowerCase();
|
|
274
|
+
if (explicit) {
|
|
275
|
+
const nativeFromTag = NATIVE_TAG_TO_ROLE[tag];
|
|
276
|
+
if (nativeFromTag === explicit)
|
|
277
|
+
return { role: explicit, widgetType: "native" };
|
|
278
|
+
if (tag === "input") {
|
|
279
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
280
|
+
if (type === "checkbox" && explicit === "checkbox" || type === "radio" && explicit === "radio" || type === "search" && explicit === "searchbox")
|
|
281
|
+
return { role: explicit, widgetType: "native" };
|
|
282
|
+
}
|
|
283
|
+
return { role: explicit, widgetType: "custom" };
|
|
284
|
+
}
|
|
285
|
+
if (NATIVE_TAG_TO_ROLE[tag])
|
|
286
|
+
return { role: NATIVE_TAG_TO_ROLE[tag], widgetType: "native" };
|
|
287
|
+
if (tag === "input") {
|
|
288
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
289
|
+
if (type === "checkbox")
|
|
290
|
+
return { role: "checkbox", widgetType: "native" };
|
|
291
|
+
if (type === "radio")
|
|
292
|
+
return { role: "radio", widgetType: "native" };
|
|
293
|
+
if (type === "search")
|
|
294
|
+
return { role: "searchbox", widgetType: "native" };
|
|
295
|
+
if (type === "submit" || type === "reset" || type === "button")
|
|
296
|
+
return { role: "button", widgetType: "native" };
|
|
297
|
+
return { role: "textbox", widgetType: "native" };
|
|
298
|
+
}
|
|
299
|
+
if (/^h[1-6]$/.test(tag))
|
|
300
|
+
return { role: "heading", widgetType: "native" };
|
|
301
|
+
return { role: tag, widgetType: "unknown" };
|
|
302
|
+
}
|
|
303
|
+
function inheritsRole(el, wantedRole) {
|
|
304
|
+
let node = el;
|
|
305
|
+
while (node) {
|
|
306
|
+
if (node.getAttribute && node.getAttribute("role") === wantedRole)
|
|
307
|
+
return true;
|
|
308
|
+
const tag = node.tagName?.toLowerCase();
|
|
309
|
+
if (wantedRole === "navigation" && tag === "nav")
|
|
310
|
+
return true;
|
|
311
|
+
node = node.parentElement;
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
function isInsideForm(el) {
|
|
316
|
+
let node = el;
|
|
317
|
+
while (node) {
|
|
318
|
+
const tag = node.tagName?.toLowerCase();
|
|
319
|
+
if (tag === "form" || node.getAttribute?.("role") === "form")
|
|
320
|
+
return true;
|
|
321
|
+
node = node.parentElement;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
function nearestLandmark(el) {
|
|
326
|
+
let node = el;
|
|
327
|
+
while (node) {
|
|
328
|
+
const tag = node.tagName?.toLowerCase();
|
|
329
|
+
const role = node.getAttribute?.("role");
|
|
330
|
+
if (tag === "nav" || role === "navigation")
|
|
331
|
+
return "navigation";
|
|
332
|
+
if (tag === "header" || role === "banner")
|
|
333
|
+
return "banner";
|
|
334
|
+
if (tag === "footer" || role === "contentinfo")
|
|
335
|
+
return "contentinfo";
|
|
336
|
+
if (tag === "aside" || role === "complementary")
|
|
337
|
+
return "complementary";
|
|
338
|
+
if (tag === "main" || role === "main")
|
|
339
|
+
return "main";
|
|
340
|
+
if (tag === "form" || role === "form")
|
|
341
|
+
return "form";
|
|
342
|
+
if (role === "dialog" || tag === "dialog" || role === "alertdialog")
|
|
343
|
+
return "dialog";
|
|
344
|
+
node = node.parentElement;
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function getMutability(el, role, accessibleName) {
|
|
349
|
+
if (inheritsRole(el, "navigation"))
|
|
350
|
+
return "immutable";
|
|
351
|
+
if (role === "link")
|
|
352
|
+
return "immutable";
|
|
353
|
+
if (role === "searchbox" || role === "tab" || role === "tabpanel")
|
|
354
|
+
return "immutable";
|
|
355
|
+
if (role === "menuitem") {
|
|
356
|
+
if (DESTRUCTIVE_VERBS.test(accessibleName))
|
|
357
|
+
return "mutable";
|
|
358
|
+
return "immutable";
|
|
359
|
+
}
|
|
360
|
+
if (DESTRUCTIVE_VERBS.test(accessibleName))
|
|
361
|
+
return "mutable";
|
|
362
|
+
if (role === "button" && VIEW_VERBS.test(accessibleName))
|
|
363
|
+
return "immutable";
|
|
364
|
+
if (isInsideForm(el) && (role === "textbox" || role === "checkbox" || role === "radio" || role === "combobox" || role === "spinbutton" || role === "slider" || role === "button"))
|
|
365
|
+
return "mutable";
|
|
366
|
+
const landmark = nearestLandmark(el);
|
|
367
|
+
if (landmark && SAFE_LANDMARK_DEFAULTS.has(landmark))
|
|
368
|
+
return "immutable";
|
|
369
|
+
if (el.tagName?.toLowerCase() === "input") {
|
|
370
|
+
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
371
|
+
if (type === "submit" || type === "reset")
|
|
372
|
+
return "mutable";
|
|
373
|
+
}
|
|
374
|
+
return "unknown";
|
|
375
|
+
}
|
|
376
|
+
const INTERACTIVE_SELECTOR = [
|
|
377
|
+
"button:not([disabled])",
|
|
378
|
+
"a[href]",
|
|
379
|
+
'input:not([type="hidden"]):not([disabled])',
|
|
380
|
+
"select:not([disabled])",
|
|
381
|
+
"textarea:not([disabled])",
|
|
382
|
+
'[role="button"]',
|
|
383
|
+
'[role="link"]',
|
|
384
|
+
'[role="checkbox"]',
|
|
385
|
+
'[role="radio"]',
|
|
386
|
+
'[role="combobox"]',
|
|
387
|
+
'[role="searchbox"]',
|
|
388
|
+
'[role="tab"]',
|
|
389
|
+
'[role="menuitem"]',
|
|
390
|
+
'[role="switch"]',
|
|
391
|
+
'[role="option"]',
|
|
392
|
+
// Phase B additions — required for blueprintDiff transient tagging and
|
|
393
|
+
// iframe fingerprinting. See design doc §3.3, §3.5.
|
|
394
|
+
'[role="status"]',
|
|
395
|
+
'[role="alert"]',
|
|
396
|
+
'[role="log"]',
|
|
397
|
+
'[role="marquee"]',
|
|
398
|
+
"iframe"
|
|
399
|
+
].join(", ");
|
|
400
|
+
const LANDMARKS = [
|
|
401
|
+
{ selector: 'nav, [role="navigation"]', landmark: "navigation", nameHint: "navbar" },
|
|
402
|
+
{ selector: "header:not(main header):not(article header):not(section header)", landmark: "banner", nameHint: "header" },
|
|
403
|
+
{ selector: 'main, [role="main"]', landmark: "main", nameHint: "main" },
|
|
404
|
+
{ selector: 'footer, [role="contentinfo"]', landmark: "footer", nameHint: "footer" },
|
|
405
|
+
{ selector: 'aside, [role="complementary"]', landmark: "sidebar", nameHint: "sidebar" },
|
|
406
|
+
{ selector: 'dialog, [role="dialog"]', landmark: "dialog", nameHint: "dialog" },
|
|
407
|
+
{ selector: '[role="alertdialog"]', landmark: "dialog", nameHint: "alert_dialog" },
|
|
408
|
+
{ selector: 'form, [role="form"]', landmark: "form", nameHint: "form" },
|
|
409
|
+
{ selector: '[role="search"]', landmark: "search", nameHint: "search" },
|
|
410
|
+
// Class/id heuristics
|
|
411
|
+
{ selector: '[class*="modal"]:not(dialog):not([role="dialog"])', landmark: "dialog", nameHint: "modal" },
|
|
412
|
+
{ selector: '[class*="sidebar"]:not(aside)', landmark: "sidebar", nameHint: "sidebar" },
|
|
413
|
+
{ selector: '[id*="navbar"],[class*="navbar"],[id*="nav-bar"],[class*="nav-bar"]', landmark: "navigation", nameHint: "navbar" },
|
|
414
|
+
{ selector: '[class*="toolbar"]', landmark: "toolbar", nameHint: "toolbar" }
|
|
415
|
+
];
|
|
416
|
+
const ANCESTOR_ID_MAX_DEPTH = 5;
|
|
417
|
+
const HEADING_ANCESTRY_MAX_DEPTH = 10;
|
|
418
|
+
let anchorKeyCounter = 0;
|
|
419
|
+
const nodeToAnchorKey = /* @__PURE__ */ new WeakMap();
|
|
420
|
+
function getAnchorKey(el) {
|
|
421
|
+
let key = nodeToAnchorKey.get(el);
|
|
422
|
+
if (!key) {
|
|
423
|
+
key = `ak${++anchorKeyCounter}`;
|
|
424
|
+
nodeToAnchorKey.set(el, key);
|
|
425
|
+
}
|
|
426
|
+
return key;
|
|
427
|
+
}
|
|
428
|
+
function isDynamicIdClient(id) {
|
|
429
|
+
if (/^react-aria\d+/.test(id)) return true;
|
|
430
|
+
if (/^mui-\d+/.test(id)) return true;
|
|
431
|
+
if (/^(mat|cdk)-[a-z]+-\d+$/.test(id)) return true;
|
|
432
|
+
if (/[-_]\d+$/.test(id)) return true;
|
|
433
|
+
if (/\d{2,}[_-][a-zA-Z]/.test(id)) return true;
|
|
434
|
+
if (/^\d+$/.test(id)) return true;
|
|
435
|
+
if (/\d{4,}$/.test(id)) return true;
|
|
436
|
+
if (/[-_][0-9a-f]{6,}$/i.test(id)) return true;
|
|
437
|
+
const m = id.match(/[-_]([0-9a-f]{3,5})$/i);
|
|
438
|
+
if (m && /[0-9]/.test(m[1])) return true;
|
|
439
|
+
if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
|
|
440
|
+
if (id.includes(":")) return true;
|
|
441
|
+
if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
function getNearestAncestorId(el) {
|
|
445
|
+
let cur = el.parentElement;
|
|
446
|
+
let depth = 0;
|
|
447
|
+
while (cur && depth < ANCESTOR_ID_MAX_DEPTH) {
|
|
448
|
+
const testId = cur.getAttribute("data-testid");
|
|
449
|
+
if (testId) return testId;
|
|
450
|
+
const id = cur.getAttribute("id");
|
|
451
|
+
if (id && !isDynamicIdClient(id)) return id;
|
|
452
|
+
cur = cur.parentElement;
|
|
453
|
+
depth++;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const CONTEXT_TEXT_MAX_ENTRIES = 10;
|
|
458
|
+
const CONTEXT_TEXT_MAX_LEN = 100;
|
|
459
|
+
const ROW_ANCESTOR_MAX_DEPTH = 6;
|
|
460
|
+
const ROW_TAG_NAMES = /* @__PURE__ */ new Set(["TR", "LI"]);
|
|
461
|
+
const ROW_ROLES = /* @__PURE__ */ new Set(["row", "listitem"]);
|
|
462
|
+
function findRowAncestor(el) {
|
|
463
|
+
let cur = el.parentElement;
|
|
464
|
+
let depth = 0;
|
|
465
|
+
while (cur && depth < ROW_ANCESTOR_MAX_DEPTH) {
|
|
466
|
+
if (ROW_TAG_NAMES.has(cur.tagName)) return cur;
|
|
467
|
+
const role = cur.getAttribute("role");
|
|
468
|
+
if (role && ROW_ROLES.has(role)) return cur;
|
|
469
|
+
cur = cur.parentElement;
|
|
470
|
+
depth++;
|
|
471
|
+
}
|
|
472
|
+
return el.parentElement ?? el;
|
|
473
|
+
}
|
|
474
|
+
function getRowContextText(el) {
|
|
475
|
+
const row = findRowAncestor(el);
|
|
476
|
+
const entries = [];
|
|
477
|
+
const walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, {
|
|
478
|
+
acceptNode(node) {
|
|
479
|
+
let p = node.parentElement;
|
|
480
|
+
while (p && p !== row) {
|
|
481
|
+
if (p.matches && p.matches(INTERACTIVE_SELECTOR))
|
|
482
|
+
return NodeFilter.FILTER_REJECT;
|
|
483
|
+
p = p.parentElement;
|
|
484
|
+
}
|
|
485
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
let cur;
|
|
489
|
+
while (cur = walker.nextNode()) {
|
|
490
|
+
const text = (cur.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
491
|
+
if (!text) continue;
|
|
492
|
+
if (text.length > CONTEXT_TEXT_MAX_LEN)
|
|
493
|
+
entries.push(text.slice(0, CONTEXT_TEXT_MAX_LEN - 1) + "\u2026");
|
|
494
|
+
else
|
|
495
|
+
entries.push(text);
|
|
496
|
+
if (entries.length >= CONTEXT_TEXT_MAX_ENTRIES) break;
|
|
497
|
+
}
|
|
498
|
+
return entries;
|
|
499
|
+
}
|
|
500
|
+
const BEM_ISH = /^[a-z][a-z-]+(__[a-z-]+)?(--[a-z-]+)?$/;
|
|
501
|
+
const FRAMEWORK_PREFIXES = ["data-radix-", "data-headlessui-", "MuiSelect-", "ant-select-"];
|
|
502
|
+
const TAILWIND_UTILITY = /^(bg|text|p|m|w|h|flex|grid|border|rounded|shadow)-|^(hover|focus|md|lg|sm|xl):/;
|
|
503
|
+
const CSS_MODULE_HASH = /_[a-z0-9]{5,}$/;
|
|
504
|
+
const NUMERIC_INDEX_SUFFIX = /-\d+$/;
|
|
505
|
+
const STATE_TOKENS = /* @__PURE__ */ new Set(["is-active", "is-open", "expanded", "selected", "active", "open"]);
|
|
506
|
+
function isStructuralClass(cls) {
|
|
507
|
+
if (!cls) return false;
|
|
508
|
+
if (FRAMEWORK_PREFIXES.some((p) => cls.startsWith(p))) return true;
|
|
509
|
+
if (STATE_TOKENS.has(cls)) return false;
|
|
510
|
+
if (TAILWIND_UTILITY.test(cls)) return false;
|
|
511
|
+
if (CSS_MODULE_HASH.test(cls)) return false;
|
|
512
|
+
if (NUMERIC_INDEX_SUFFIX.test(cls)) return false;
|
|
513
|
+
return BEM_ISH.test(cls);
|
|
514
|
+
}
|
|
515
|
+
function normalizeClassesClient(className) {
|
|
516
|
+
if (!className) return "";
|
|
517
|
+
const tokens = className.split(/\s+/).filter(Boolean).filter(isStructuralClass);
|
|
518
|
+
return Array.from(new Set(tokens)).sort().join(" ");
|
|
519
|
+
}
|
|
520
|
+
function extractTuple(element) {
|
|
521
|
+
const tag = element.tagName.toLowerCase();
|
|
522
|
+
const role = element.getAttribute("role") ?? "";
|
|
523
|
+
const ariaHasPopup = element.getAttribute("aria-haspopup") ?? "";
|
|
524
|
+
const classPattern = normalizeClassesClient(element.getAttribute("class") ?? "");
|
|
525
|
+
const dataAttrKeys = [];
|
|
526
|
+
for (const attr of Array.from(element.attributes)) {
|
|
527
|
+
if (attr.name.startsWith("data-"))
|
|
528
|
+
dataAttrKeys.push(attr.name);
|
|
529
|
+
}
|
|
530
|
+
dataAttrKeys.sort();
|
|
531
|
+
return { tag, role, ariaHasPopup, classPattern, dataAttrKeys };
|
|
532
|
+
}
|
|
533
|
+
function walkTwoLevels(root) {
|
|
534
|
+
const out = [];
|
|
535
|
+
for (const c of Array.from(root.children)) {
|
|
536
|
+
out.push(c);
|
|
537
|
+
for (const g of Array.from(c.children)) {
|
|
538
|
+
out.push(g);
|
|
539
|
+
if (out.length >= 20) return out;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function detectPortal(element) {
|
|
545
|
+
const controlsId = element.getAttribute("aria-controls");
|
|
546
|
+
if (controlsId) {
|
|
547
|
+
const target = document.getElementById(controlsId);
|
|
548
|
+
if (target && !element.contains(target))
|
|
549
|
+
return target;
|
|
550
|
+
}
|
|
551
|
+
let found = 0;
|
|
552
|
+
const walker = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_ELEMENT);
|
|
553
|
+
let cur;
|
|
554
|
+
while (cur = walker.nextNode()) {
|
|
555
|
+
const desc = cur;
|
|
556
|
+
if (element.contains(desc))
|
|
557
|
+
continue;
|
|
558
|
+
for (const attr of Array.from(desc.attributes)) {
|
|
559
|
+
if (attr.name.startsWith("data-radix-") || attr.name.startsWith("data-headlessui-"))
|
|
560
|
+
return desc;
|
|
561
|
+
}
|
|
562
|
+
if (++found >= 20)
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
function canonicalJson(v) {
|
|
568
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
569
|
+
if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]";
|
|
570
|
+
const o = v;
|
|
571
|
+
const keys = Object.keys(o).sort();
|
|
572
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(o[k])).join(",") + "}";
|
|
573
|
+
}
|
|
574
|
+
async function computeElementFingerprint(element) {
|
|
575
|
+
const root = extractTuple(element);
|
|
576
|
+
const descTuples = [];
|
|
577
|
+
const seen = /* @__PURE__ */ new Set();
|
|
578
|
+
for (const d of walkTwoLevels(element)) {
|
|
579
|
+
const t = extractTuple(d);
|
|
580
|
+
const key = canonicalJson(t);
|
|
581
|
+
if (seen.has(key)) continue;
|
|
582
|
+
seen.add(key);
|
|
583
|
+
descTuples.push(t);
|
|
584
|
+
if (descTuples.length >= 3) break;
|
|
585
|
+
}
|
|
586
|
+
const portalEl = detectPortal(element);
|
|
587
|
+
const portal = portalEl ? extractTuple(portalEl) : null;
|
|
588
|
+
const input = { root, descendants: descTuples, portal };
|
|
589
|
+
const bytes = new TextEncoder().encode(canonicalJson(input));
|
|
590
|
+
const buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
591
|
+
const arr = Array.from(new Uint8Array(buf));
|
|
592
|
+
return arr.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
593
|
+
}
|
|
594
|
+
const SECTIONING_TAG_NAMES = /* @__PURE__ */ new Set(["section", "article", "aside", "nav"]);
|
|
595
|
+
function getSectioningElementAccessibleName(sectioningEl) {
|
|
596
|
+
const labelledBy = sectioningEl.getAttribute("aria-labelledby");
|
|
597
|
+
if (labelledBy) {
|
|
598
|
+
const names = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
599
|
+
if (names.length) return names.join(" ");
|
|
600
|
+
}
|
|
601
|
+
const ariaLabel = sectioningEl.getAttribute("aria-label");
|
|
602
|
+
if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim();
|
|
603
|
+
const heading = sectioningEl.querySelector("h1, h2, h3, h4, h5, h6");
|
|
604
|
+
if (heading) return heading.textContent?.trim() ?? null;
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
function getNearestLandmarkAncestor(el) {
|
|
608
|
+
for (const { selector, landmark, nameHint } of LANDMARKS) {
|
|
609
|
+
const ancestor = el.closest(selector);
|
|
610
|
+
if (ancestor) return { landmark, nameHint, anchorKey: getAnchorKey(ancestor) };
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
function getNearestSectioningAncestor(el) {
|
|
615
|
+
let cur = el.parentElement;
|
|
616
|
+
while (cur) {
|
|
617
|
+
const tag = cur.tagName.toLowerCase();
|
|
618
|
+
if (SECTIONING_TAG_NAMES.has(tag)) {
|
|
619
|
+
return {
|
|
620
|
+
tagName: tag,
|
|
621
|
+
accessibleName: getSectioningElementAccessibleName(cur),
|
|
622
|
+
anchorKey: getAnchorKey(cur)
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
cur = cur.parentElement;
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
function getHeadingAncestry(el) {
|
|
630
|
+
const outermostFirst = [];
|
|
631
|
+
let cur = el.parentElement;
|
|
632
|
+
let depth = 0;
|
|
633
|
+
while (cur && depth < HEADING_ANCESTRY_MAX_DEPTH) {
|
|
634
|
+
let sib = cur.previousElementSibling;
|
|
635
|
+
while (sib) {
|
|
636
|
+
const tag2 = sib.tagName.toLowerCase();
|
|
637
|
+
const match = tag2.match(/^h([1-6])$/);
|
|
638
|
+
if (match) {
|
|
639
|
+
const level = parseInt(match[1], 10);
|
|
640
|
+
const text = sib.textContent?.trim() ?? "";
|
|
641
|
+
outermostFirst.unshift({ level, text, anchorKey: getAnchorKey(sib) });
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
sib = sib.previousElementSibling;
|
|
645
|
+
}
|
|
646
|
+
const tag = cur.tagName.toLowerCase();
|
|
647
|
+
if (SECTIONING_TAG_NAMES.has(tag)) break;
|
|
648
|
+
cur = cur.parentElement;
|
|
649
|
+
depth++;
|
|
650
|
+
}
|
|
651
|
+
return outermostFirst;
|
|
652
|
+
}
|
|
653
|
+
function getSectionAncestry(el) {
|
|
654
|
+
return {
|
|
655
|
+
landmarkAncestor: getNearestLandmarkAncestor(el),
|
|
656
|
+
sectioningAncestor: getNearestSectioningAncestor(el),
|
|
657
|
+
headingAncestry: getHeadingAncestry(el)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async function captureElement(el) {
|
|
661
|
+
const { role, widgetType } = getRole(el);
|
|
662
|
+
const accessibleName = getAccessibleName(el, role);
|
|
663
|
+
if (!accessibleName && !LIVE_REGION_ROLES.has(role))
|
|
664
|
+
return null;
|
|
665
|
+
const idAttr = el.getAttribute("id");
|
|
666
|
+
const testIdAttr = el.getAttribute("data-testid");
|
|
667
|
+
const fingerprint = widgetType === "native" ? null : await computeElementFingerprint(el);
|
|
668
|
+
const stableIdValue = idAttr && idUnique(idAttr) && !isDynamicIdClient(idAttr) ? idAttr : null;
|
|
669
|
+
return {
|
|
670
|
+
role,
|
|
671
|
+
accessibleName,
|
|
672
|
+
xpath: getXPath(el),
|
|
673
|
+
mutability: getMutability(el, role, accessibleName),
|
|
674
|
+
widgetType,
|
|
675
|
+
framePath: computeFramePath(el),
|
|
676
|
+
shadowRoot: hasShadowRootAncestor(el),
|
|
677
|
+
stableId: stableIdValue,
|
|
678
|
+
testId: testIdAttr || null,
|
|
679
|
+
fingerprint,
|
|
680
|
+
nearestAncestorId: getNearestAncestorId(el),
|
|
681
|
+
sectionAncestry: getSectionAncestry(el),
|
|
682
|
+
contextText: getRowContextText(el)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const allInteractive = document.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
686
|
+
const elements = [];
|
|
687
|
+
for (const el of Array.from(allInteractive)) {
|
|
688
|
+
const captured = await captureElement(el);
|
|
689
|
+
if (captured)
|
|
690
|
+
elements.push(captured);
|
|
691
|
+
}
|
|
692
|
+
let hashAccum = 0;
|
|
693
|
+
for (const el of elements) {
|
|
694
|
+
for (let i = 0; i < el.accessibleName.length; i++)
|
|
695
|
+
hashAccum = (hashAccum << 5) - hashAccum + el.accessibleName.charCodeAt(i) | 0;
|
|
696
|
+
}
|
|
697
|
+
const pageHash = `${elements.length}:${hashAccum}`;
|
|
698
|
+
return { elements, pageHash };
|
|
699
|
+
}
|
|
700
|
+
async function buildPageBlueprint(page) {
|
|
701
|
+
const [pageUrl, evalResult] = await Promise.all([
|
|
702
|
+
page.url(),
|
|
703
|
+
page.evaluate(domEvaluationScript)
|
|
704
|
+
]);
|
|
705
|
+
const unknownRoles = /* @__PURE__ */ new Set();
|
|
706
|
+
for (const el of evalResult.elements) {
|
|
707
|
+
if (!CANONICAL_ARIA_ROLES.has(el.role))
|
|
708
|
+
unknownRoles.add(el.role);
|
|
709
|
+
}
|
|
710
|
+
if (unknownRoles.size > 0) {
|
|
711
|
+
console.warn(`[dom-analyzer] Unknown ARIA roles encountered (not in canonical WAI-ARIA set): ${[...unknownRoles].join(", ")}.`);
|
|
712
|
+
}
|
|
713
|
+
const grouperInputs = evalResult.elements.map((el, idx) => ({
|
|
714
|
+
id: `el-${idx}`,
|
|
715
|
+
ancestry: el.sectionAncestry
|
|
716
|
+
}));
|
|
717
|
+
const { assignments: sectionNameByElementId, sections: resolvedSections } = (0, import_sectionGrouper.resolveSections)(grouperInputs);
|
|
718
|
+
const sectionNameByRawElement = /* @__PURE__ */ new Map();
|
|
719
|
+
evalResult.elements.forEach((el, idx) => {
|
|
720
|
+
sectionNameByRawElement.set(el, sectionNameByElementId[`el-${idx}`]);
|
|
721
|
+
});
|
|
722
|
+
const elementsBySection = /* @__PURE__ */ new Map();
|
|
723
|
+
for (const el of evalResult.elements) {
|
|
724
|
+
const sectionName = sectionNameByRawElement.get(el) ?? "page";
|
|
725
|
+
if (!elementsBySection.has(sectionName))
|
|
726
|
+
elementsBySection.set(sectionName, []);
|
|
727
|
+
elementsBySection.get(sectionName).push(el);
|
|
728
|
+
}
|
|
729
|
+
const derivedNamesByElement = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const [sectionName, elsInSection] of elementsBySection.entries()) {
|
|
731
|
+
const raws = elsInSection.map((el) => ({
|
|
732
|
+
role: el.role,
|
|
733
|
+
accessibleName: el.accessibleName,
|
|
734
|
+
sectionName
|
|
735
|
+
}));
|
|
736
|
+
const names = applyPositionHintToNamelessElements(raws);
|
|
737
|
+
elsInSection.forEach((el, i) => derivedNamesByElement.set(el, names[i]));
|
|
738
|
+
}
|
|
739
|
+
const collisionInputs = evalResult.elements.map((el) => ({
|
|
740
|
+
initialName: derivedNamesByElement.get(el) ?? deriveLogicalName(el.role, el.accessibleName),
|
|
741
|
+
sectionName: sectionNameByRawElement.get(el) ?? "page",
|
|
742
|
+
testId: el.testId,
|
|
743
|
+
stableId: el.stableId,
|
|
744
|
+
// already-filtered: idUnique + !isDynamicIdClient
|
|
745
|
+
nearestAncestorId: el.nearestAncestorId
|
|
746
|
+
}));
|
|
747
|
+
const finalNames = (0, import_logicalNameResolver.resolveCollisions)(collisionInputs);
|
|
748
|
+
const finalNameByElement = /* @__PURE__ */ new Map();
|
|
749
|
+
evalResult.elements.forEach((el, i) => finalNameByElement.set(el, finalNames[i]));
|
|
750
|
+
const sections = [];
|
|
751
|
+
for (const sectionInfo of resolvedSections) {
|
|
752
|
+
const rawsInSection = elementsBySection.get(sectionInfo.name) ?? [];
|
|
753
|
+
if (rawsInSection.length === 0)
|
|
754
|
+
continue;
|
|
755
|
+
const { singular, repeating } = collapseRepeatingElements(rawsInSection);
|
|
756
|
+
const singularElements = [];
|
|
757
|
+
for (const raw of singular) {
|
|
758
|
+
const logicalName = finalNameByElement.get(raw);
|
|
759
|
+
if (!raw.xpath.startsWith("//")) {
|
|
760
|
+
throw new BlueprintInvariantError(
|
|
761
|
+
`BlueprintElement '${logicalName}' has non-absolute xpath: '${raw.xpath}'. All xpaths must start with '//' (document-rooted descendant-or-self).`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
validateElement({ logicalName, widgetType: raw.widgetType, fingerprint: raw.fingerprint });
|
|
765
|
+
singularElements.push({
|
|
766
|
+
logicalName,
|
|
767
|
+
role: raw.role,
|
|
768
|
+
accessibleName: raw.accessibleName,
|
|
769
|
+
xpath: raw.xpath,
|
|
770
|
+
mutability: raw.mutability,
|
|
771
|
+
widgetType: raw.widgetType,
|
|
772
|
+
framePath: raw.framePath,
|
|
773
|
+
shadowRoot: raw.shadowRoot,
|
|
774
|
+
stableId: raw.stableId,
|
|
775
|
+
testId: raw.testId,
|
|
776
|
+
fingerprint: raw.fingerprint
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
const repeatingElements = [];
|
|
780
|
+
for (const rep of repeating) {
|
|
781
|
+
validateElement({ logicalName: rep.logicalName, widgetType: rep.widgetType, fingerprint: rep.fingerprint });
|
|
782
|
+
validateRepeatingElement(rep);
|
|
783
|
+
repeatingElements.push(rep);
|
|
784
|
+
}
|
|
785
|
+
if (singularElements.length === 0 && repeatingElements.length === 0)
|
|
786
|
+
continue;
|
|
787
|
+
sections.push({
|
|
788
|
+
name: sectionInfo.name,
|
|
789
|
+
landmark: sectionInfo.landmark,
|
|
790
|
+
landmarkOther: sectionInfo.landmarkOther,
|
|
791
|
+
elements: singularElements,
|
|
792
|
+
repeatingElements
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
schemaVersion: import_serialization.SCHEMA_VERSION,
|
|
797
|
+
url: (0, import_serialization.normalizeUrl)(pageUrl),
|
|
798
|
+
capturedAt: (0, import_serialization.validateTimestamp)((0, import_serialization.nowTimestamp)()),
|
|
799
|
+
pageHash: evalResult.pageHash,
|
|
800
|
+
sections
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function collapseRepeatingElements(elements) {
|
|
804
|
+
const singular = [];
|
|
805
|
+
const repeating = [];
|
|
806
|
+
const groups = /* @__PURE__ */ new Map();
|
|
807
|
+
for (const el of elements) {
|
|
808
|
+
const key = `${el.role}::${prefixKey(el.accessibleName)}`;
|
|
809
|
+
const arr = groups.get(key) ?? [];
|
|
810
|
+
arr.push(el);
|
|
811
|
+
groups.set(key, arr);
|
|
812
|
+
}
|
|
813
|
+
for (const group of groups.values()) {
|
|
814
|
+
if (group.length < 2) {
|
|
815
|
+
singular.push(...group);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const analysis = analyseGroup(group);
|
|
819
|
+
if (!analysis) {
|
|
820
|
+
singular.push(...group);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
repeating.push(analysis);
|
|
824
|
+
}
|
|
825
|
+
return { singular, repeating };
|
|
826
|
+
}
|
|
827
|
+
function prefixKey(name) {
|
|
828
|
+
const words = name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
829
|
+
return words.slice(0, 2).join("_");
|
|
830
|
+
}
|
|
831
|
+
function analyseGroup(group) {
|
|
832
|
+
const xpathTemplate = deriveXpathTemplate(group);
|
|
833
|
+
if (xpathTemplate) {
|
|
834
|
+
const variableNameResult = analyseByVariableName(group, xpathTemplate);
|
|
835
|
+
if (variableNameResult)
|
|
836
|
+
return variableNameResult;
|
|
837
|
+
}
|
|
838
|
+
const testIdResult = analyseByVaryingTestId(group);
|
|
839
|
+
if (testIdResult)
|
|
840
|
+
return testIdResult;
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
function analyseByVariableName(group, xpathTemplate) {
|
|
844
|
+
const tokenLists = group.map((el) => el.accessibleName.split(/\s+/));
|
|
845
|
+
const tokenLengths = new Set(tokenLists.map((t) => t.length));
|
|
846
|
+
if (tokenLengths.size !== 1)
|
|
847
|
+
return null;
|
|
848
|
+
const length = tokenLists[0].length;
|
|
849
|
+
const templateTokens = [];
|
|
850
|
+
const variableIndices = [];
|
|
851
|
+
for (let i = 0; i < length; i++) {
|
|
852
|
+
const first = tokenLists[0][i];
|
|
853
|
+
const allSame = tokenLists.every((t) => t[i] === first);
|
|
854
|
+
if (allSame) {
|
|
855
|
+
templateTokens.push(first);
|
|
856
|
+
} else {
|
|
857
|
+
templateTokens.push(`__VAR__${variableIndices.length}__`);
|
|
858
|
+
variableIndices.push(i);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (variableIndices.length === 0)
|
|
862
|
+
return null;
|
|
863
|
+
if (variableIndices.length > 2)
|
|
864
|
+
return null;
|
|
865
|
+
const paramNames = variableIndices.map((_, i) => `param${i}`);
|
|
866
|
+
const firstVarIdx = variableIndices[0];
|
|
867
|
+
const contextBefore = firstVarIdx > 0 ? templateTokens[firstVarIdx - 1] : "";
|
|
868
|
+
const contextAfter = firstVarIdx < length - 1 ? templateTokens[firstVarIdx + 1] : "";
|
|
869
|
+
const firstParamName = guessParamName(contextBefore, contextAfter) || "id";
|
|
870
|
+
paramNames[0] = firstParamName;
|
|
871
|
+
if (paramNames.length > 1)
|
|
872
|
+
paramNames[1] = "value";
|
|
873
|
+
const accessibleNameTemplate = templateTokens.map((t) => {
|
|
874
|
+
const m = t.match(/^__VAR__(\d+)__$/);
|
|
875
|
+
return m ? `{${paramNames[parseInt(m[1], 10)]}}` : t;
|
|
876
|
+
}).join(" ");
|
|
877
|
+
const parameters = [...paramNames, "row"];
|
|
878
|
+
const indexParameter = "row";
|
|
879
|
+
const items = group.map((el, itemIdx) => {
|
|
880
|
+
const tokens = tokenLists[itemIdx];
|
|
881
|
+
const params = {};
|
|
882
|
+
variableIndices.forEach((tokenIdx, paramIdx) => {
|
|
883
|
+
params[paramNames[paramIdx]] = tokens[tokenIdx];
|
|
884
|
+
});
|
|
885
|
+
params["row"] = String(itemIdx + 1);
|
|
886
|
+
return {
|
|
887
|
+
parameters: params,
|
|
888
|
+
...el.contextText.length > 0 ? { contextText: el.contextText } : {}
|
|
889
|
+
};
|
|
890
|
+
});
|
|
891
|
+
const rep = group[0];
|
|
892
|
+
const nameForLogical = templateTokens.map((t) => t.match(/^__VAR__\d+__$/) ? "" : t).join(" ").trim();
|
|
893
|
+
const baseLogicalName = deriveLogicalName(rep.role, nameForLogical);
|
|
894
|
+
return {
|
|
895
|
+
logicalName: baseLogicalName,
|
|
896
|
+
role: rep.role,
|
|
897
|
+
accessibleNameTemplate,
|
|
898
|
+
parameters,
|
|
899
|
+
xpathPattern: xpathTemplate,
|
|
900
|
+
indexParameter,
|
|
901
|
+
items,
|
|
902
|
+
mutability: rep.mutability,
|
|
903
|
+
widgetType: rep.widgetType,
|
|
904
|
+
framePath: rep.framePath,
|
|
905
|
+
shadowRoot: rep.shadowRoot,
|
|
906
|
+
stableId: null,
|
|
907
|
+
testId: null,
|
|
908
|
+
// Repeating-group shortcut: members share widgetType by construction
|
|
909
|
+
// (same role + similar structure), so group[0]'s fingerprint is representative.
|
|
910
|
+
fingerprint: rep.fingerprint
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function analyseByVaryingTestId(group) {
|
|
914
|
+
if (group.some((el) => !el.testId))
|
|
915
|
+
return null;
|
|
916
|
+
const firstName = group[0].accessibleName;
|
|
917
|
+
if (!group.every((el) => el.accessibleName === firstName))
|
|
918
|
+
return null;
|
|
919
|
+
const testIds = group.map((el) => el.testId);
|
|
920
|
+
const commonPrefix = longestCommonPrefix(testIds);
|
|
921
|
+
const commonSuffix = longestCommonSuffix(testIds);
|
|
922
|
+
const slugs = [];
|
|
923
|
+
for (const tid of testIds) {
|
|
924
|
+
const middle = tid.slice(commonPrefix.length, tid.length - commonSuffix.length);
|
|
925
|
+
if (!middle)
|
|
926
|
+
return null;
|
|
927
|
+
slugs.push(middle);
|
|
928
|
+
}
|
|
929
|
+
if (new Set(slugs).size !== slugs.length)
|
|
930
|
+
return null;
|
|
931
|
+
const firstToken = firstName.toLowerCase().split(/\s+/)[0];
|
|
932
|
+
const slugParamName = guessParamName(firstToken, "") || "itemSlug";
|
|
933
|
+
const testIdPattern = `${commonPrefix}{${slugParamName}}${commonSuffix}`;
|
|
934
|
+
const xpathBySlug = `//*[@data-testid=${xpathLiteralNode(testIdPattern)}]`;
|
|
935
|
+
const parameters = [slugParamName];
|
|
936
|
+
const indexParameter = slugParamName;
|
|
937
|
+
const accessibleNameTemplate = firstName;
|
|
938
|
+
const items = group.map((el, itemIdx) => ({
|
|
939
|
+
parameters: {
|
|
940
|
+
[slugParamName]: slugs[itemIdx]
|
|
941
|
+
},
|
|
942
|
+
...el.contextText.length > 0 ? { contextText: el.contextText } : {}
|
|
943
|
+
}));
|
|
944
|
+
const rep = group[0];
|
|
945
|
+
const baseLogicalName = deriveLogicalName(rep.role, firstName);
|
|
946
|
+
return {
|
|
947
|
+
logicalName: baseLogicalName,
|
|
948
|
+
role: rep.role,
|
|
949
|
+
accessibleNameTemplate,
|
|
950
|
+
parameters,
|
|
951
|
+
xpathPattern: xpathBySlug,
|
|
952
|
+
indexParameter,
|
|
953
|
+
items,
|
|
954
|
+
mutability: rep.mutability,
|
|
955
|
+
widgetType: rep.widgetType,
|
|
956
|
+
framePath: rep.framePath,
|
|
957
|
+
shadowRoot: rep.shadowRoot,
|
|
958
|
+
stableId: null,
|
|
959
|
+
testId: null,
|
|
960
|
+
// Repeating-group shortcut: members share widgetType by construction
|
|
961
|
+
// (same role + similar structure), so group[0]'s fingerprint is representative.
|
|
962
|
+
fingerprint: rep.fingerprint
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function longestCommonPrefix(strs) {
|
|
966
|
+
if (strs.length === 0)
|
|
967
|
+
return "";
|
|
968
|
+
let prefix = strs[0];
|
|
969
|
+
for (const s of strs.slice(1)) {
|
|
970
|
+
let i = 0;
|
|
971
|
+
while (i < prefix.length && i < s.length && prefix[i] === s[i])
|
|
972
|
+
i++;
|
|
973
|
+
prefix = prefix.slice(0, i);
|
|
974
|
+
if (!prefix)
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
return prefix;
|
|
978
|
+
}
|
|
979
|
+
function longestCommonSuffix(strs) {
|
|
980
|
+
if (strs.length === 0)
|
|
981
|
+
return "";
|
|
982
|
+
let suffix = strs[0];
|
|
983
|
+
for (const s of strs.slice(1)) {
|
|
984
|
+
let i = 0;
|
|
985
|
+
while (i < suffix.length && i < s.length && suffix[suffix.length - 1 - i] === s[s.length - 1 - i])
|
|
986
|
+
i++;
|
|
987
|
+
suffix = suffix.slice(suffix.length - i);
|
|
988
|
+
if (!suffix)
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
return suffix;
|
|
992
|
+
}
|
|
993
|
+
function deriveXpathTemplate(group) {
|
|
994
|
+
const xpaths = group.map((el) => el.xpath);
|
|
995
|
+
const splits = xpaths.map((xp) => xp.split("/"));
|
|
996
|
+
const lens = new Set(splits.map((s) => s.length));
|
|
997
|
+
if (lens.size !== 1)
|
|
998
|
+
return null;
|
|
999
|
+
const length = splits[0].length;
|
|
1000
|
+
const template = [];
|
|
1001
|
+
let varyingSegmentCount = 0;
|
|
1002
|
+
for (let i = 0; i < length; i++) {
|
|
1003
|
+
const values = splits.map((s) => s[i]);
|
|
1004
|
+
const unique = new Set(values);
|
|
1005
|
+
if (unique.size === 1) {
|
|
1006
|
+
template.push(values[0]);
|
|
1007
|
+
} else {
|
|
1008
|
+
const match = values[0].match(/^([a-z0-9]+)\[(\d+)\](.*)$/);
|
|
1009
|
+
if (!match)
|
|
1010
|
+
return null;
|
|
1011
|
+
const tagPrefix = match[1];
|
|
1012
|
+
const suffix = match[3];
|
|
1013
|
+
for (const v of values) {
|
|
1014
|
+
const m = v.match(/^([a-z0-9]+)\[(\d+)\](.*)$/);
|
|
1015
|
+
if (!m || m[1] !== tagPrefix || m[3] !== suffix)
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
template.push(`${tagPrefix}[{row}]${suffix}`);
|
|
1019
|
+
varyingSegmentCount++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (varyingSegmentCount === 0 || varyingSegmentCount > 1)
|
|
1023
|
+
return null;
|
|
1024
|
+
return template.join("/");
|
|
1025
|
+
}
|
|
1026
|
+
function guessParamName(before, after) {
|
|
1027
|
+
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1028
|
+
const tokens = [normalize(before), normalize(after)].filter(Boolean);
|
|
1029
|
+
const NOUNS = {
|
|
1030
|
+
order: "orderId",
|
|
1031
|
+
orders: "orderId",
|
|
1032
|
+
product: "productId",
|
|
1033
|
+
products: "productId",
|
|
1034
|
+
user: "userId",
|
|
1035
|
+
users: "userId",
|
|
1036
|
+
tab: "tabName",
|
|
1037
|
+
row: "rowIndex",
|
|
1038
|
+
item: "itemId",
|
|
1039
|
+
items: "itemId"
|
|
1040
|
+
};
|
|
1041
|
+
for (const tok of tokens) {
|
|
1042
|
+
if (NOUNS[tok])
|
|
1043
|
+
return NOUNS[tok];
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
const TEMPLATE_TOKEN_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
1048
|
+
function extractTokens(template) {
|
|
1049
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1050
|
+
let match;
|
|
1051
|
+
const re = new RegExp(TEMPLATE_TOKEN_RE);
|
|
1052
|
+
while ((match = re.exec(template)) !== null)
|
|
1053
|
+
tokens.add(match[1]);
|
|
1054
|
+
return tokens;
|
|
1055
|
+
}
|
|
1056
|
+
function setsEqual(a, b) {
|
|
1057
|
+
if (a.size !== b.size)
|
|
1058
|
+
return false;
|
|
1059
|
+
for (const v of a) {
|
|
1060
|
+
if (!b.has(v))
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
function validateElement(el) {
|
|
1066
|
+
if ((el.widgetType === "custom" || el.widgetType === "unknown") && el.fingerprint === null) {
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`fingerprint required for widgetType='${el.widgetType}' on element '${el.logicalName}'`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function validateRepeatingElement(rep) {
|
|
1073
|
+
if (!rep.xpathPattern.startsWith("//")) {
|
|
1074
|
+
throw new BlueprintInvariantError(
|
|
1075
|
+
`RepeatingBlueprintElement '${rep.logicalName}' has non-absolute xpathPattern: '${rep.xpathPattern}'. All xpaths must start with '//' (document-rooted descendant-or-self).`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const declared = new Set(rep.parameters);
|
|
1079
|
+
const tokensFromName = extractTokens(rep.accessibleNameTemplate);
|
|
1080
|
+
const tokensFromXpath = extractTokens(rep.xpathPattern);
|
|
1081
|
+
const allTokens = /* @__PURE__ */ new Set([...tokensFromName, ...tokensFromXpath]);
|
|
1082
|
+
if (!setsEqual(allTokens, declared)) {
|
|
1083
|
+
const missing = [...declared].filter((p) => !allTokens.has(p));
|
|
1084
|
+
const extra = [...allTokens].filter((t) => !declared.has(t));
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
`Repeating element "${rep.logicalName}" has template/parameters mismatch. Declared parameters: [${rep.parameters.join(", ")}]. Tokens in templates: [${[...allTokens].join(", ")}]. ` + (missing.length ? `Missing in templates: [${missing.join(", ")}]. ` : "") + (extra.length ? `Extra tokens in templates: [${extra.join(", ")}].` : "")
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (!declared.has(rep.indexParameter))
|
|
1090
|
+
throw new Error(`Repeating element "${rep.logicalName}" indexParameter "${rep.indexParameter}" not in parameters [${rep.parameters.join(", ")}]`);
|
|
1091
|
+
for (let i = 0; i < rep.items.length; i++) {
|
|
1092
|
+
const itemKeys = new Set(Object.keys(rep.items[i].parameters));
|
|
1093
|
+
if (!setsEqual(itemKeys, declared)) {
|
|
1094
|
+
const missing = [...declared].filter((p) => !itemKeys.has(p));
|
|
1095
|
+
const extra = [...itemKeys].filter((k) => !declared.has(k));
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`Repeating element "${rep.logicalName}" item[${i}] has parameter-key mismatch. Declared: [${rep.parameters.join(", ")}]. Item keys: [${[...itemKeys].join(", ")}]. ` + (missing.length ? `Missing: [${missing.join(", ")}]. ` : "") + (extra.length ? `Extra: [${extra.join(", ")}].` : "")
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function resolveTemplate(template, params) {
|
|
1103
|
+
return template.replace(TEMPLATE_TOKEN_RE, (_, name) => {
|
|
1104
|
+
if (!(name in params))
|
|
1105
|
+
throw new Error(`resolveTemplate: no value for {${name}} in params keys [${Object.keys(params).join(", ")}]`);
|
|
1106
|
+
return params[name];
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function resolveXpath(rep, params) {
|
|
1110
|
+
return resolveTemplate(rep.xpathPattern, params);
|
|
1111
|
+
}
|
|
1112
|
+
function resolveAccessibleName(rep, params) {
|
|
1113
|
+
return resolveTemplate(rep.accessibleNameTemplate, params);
|
|
1114
|
+
}
|
|
1115
|
+
function buildMap(blueprint) {
|
|
1116
|
+
const map = {};
|
|
1117
|
+
for (const section of blueprint.sections) {
|
|
1118
|
+
for (const el of section.elements)
|
|
1119
|
+
map[el.logicalName] = el.xpath;
|
|
1120
|
+
for (const rep of section.repeatingElements)
|
|
1121
|
+
map[rep.logicalName] = rep.xpathPattern;
|
|
1122
|
+
}
|
|
1123
|
+
return map;
|
|
1124
|
+
}
|
|
1125
|
+
function buildOutline(blueprint) {
|
|
1126
|
+
const lines = [];
|
|
1127
|
+
for (const section of blueprint.sections) {
|
|
1128
|
+
const landmarkLabel = section.landmark === "other" && section.landmarkOther ? section.landmarkOther : section.landmark;
|
|
1129
|
+
lines.push(`${section.name} (role: ${landmarkLabel})`);
|
|
1130
|
+
const total = section.elements.length + section.repeatingElements.length;
|
|
1131
|
+
let idx = 0;
|
|
1132
|
+
for (const el of section.elements) {
|
|
1133
|
+
idx++;
|
|
1134
|
+
const branch = idx === total ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
1135
|
+
lines.push(` ${branch} ${el.logicalName} [${el.mutability}]`);
|
|
1136
|
+
}
|
|
1137
|
+
for (const rep of section.repeatingElements) {
|
|
1138
|
+
idx++;
|
|
1139
|
+
const branch = idx === total ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
1140
|
+
lines.push(` ${branch} ${rep.logicalName} [repeating \xD7 ${rep.items.length}]`);
|
|
1141
|
+
lines.push(` template: "${rep.accessibleNameTemplate}"`);
|
|
1142
|
+
}
|
|
1143
|
+
lines.push("");
|
|
1144
|
+
}
|
|
1145
|
+
return lines.join("\n").trimEnd();
|
|
1146
|
+
}
|
|
1147
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1148
|
+
0 && (module.exports = {
|
|
1149
|
+
BlueprintInvariantError,
|
|
1150
|
+
applyPositionHintToNamelessElements,
|
|
1151
|
+
buildMap,
|
|
1152
|
+
buildOutline,
|
|
1153
|
+
buildPageBlueprint,
|
|
1154
|
+
deriveLogicalName,
|
|
1155
|
+
resolveAccessibleName,
|
|
1156
|
+
resolveTemplate,
|
|
1157
|
+
resolveXpath,
|
|
1158
|
+
validateElement,
|
|
1159
|
+
validateRepeatingElement,
|
|
1160
|
+
xpathLiteralNode
|
|
1161
|
+
});
|