@skyramp/mcp 0.1.8 → 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/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,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var crawler_exports = {};
|
|
30
|
+
__export(crawler_exports, {
|
|
31
|
+
crawl: () => crawl,
|
|
32
|
+
crawlSinglePage: () => crawlSinglePage
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(crawler_exports);
|
|
35
|
+
var playwright = __toESM(require("playwright-core"));
|
|
36
|
+
var import_blueprint = require("./blueprint");
|
|
37
|
+
var import_serialization = require("./serialization");
|
|
38
|
+
const DESTRUCTIVE_VERB_RE = /(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)/i;
|
|
39
|
+
function isSameOrigin(a, b) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(a).origin === new URL(b).origin;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isBogusHref(href) {
|
|
47
|
+
if (!href)
|
|
48
|
+
return true;
|
|
49
|
+
const trimmed = href.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
return true;
|
|
52
|
+
const stripped = trimmed.replace(/^["']|["']$/g, "");
|
|
53
|
+
if (!stripped || stripped === "#")
|
|
54
|
+
return true;
|
|
55
|
+
if (stripped.startsWith("#"))
|
|
56
|
+
return true;
|
|
57
|
+
if (/^javascript:/i.test(stripped))
|
|
58
|
+
return true;
|
|
59
|
+
if (/^mailto:/i.test(stripped))
|
|
60
|
+
return true;
|
|
61
|
+
if (/^tel:/i.test(stripped))
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function resolveUrl(href, base) {
|
|
66
|
+
try {
|
|
67
|
+
return new URL(href, base).href;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function substituteUrlSegments(url, params) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = new URL(url);
|
|
75
|
+
const segments = parsed.pathname.split("/");
|
|
76
|
+
for (let i = 0; i < segments.length; i++) {
|
|
77
|
+
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
78
|
+
if (paramValue && segments[i] === paramValue) {
|
|
79
|
+
segments[i] = `{${paramName}}`;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
parsed.pathname = segments.join("/");
|
|
85
|
+
const newParams = new URLSearchParams();
|
|
86
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
87
|
+
let replaced = value;
|
|
88
|
+
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
89
|
+
if (paramValue && replaced === paramValue) {
|
|
90
|
+
replaced = `{${paramName}}`;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
newParams.append(key, replaced);
|
|
95
|
+
}
|
|
96
|
+
parsed.search = newParams.toString();
|
|
97
|
+
return parsed.href.replace(/%7B/gi, "{").replace(/%7D/gi, "}");
|
|
98
|
+
} catch {
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function probeClickNavigations(page, fromUrl, blueprint, probeMode, sameOriginOnly, entryUrl, bogusHrefLinkLabels) {
|
|
103
|
+
if (probeMode === "none")
|
|
104
|
+
return [];
|
|
105
|
+
const results = [];
|
|
106
|
+
const seenXpaths = /* @__PURE__ */ new Set();
|
|
107
|
+
const candidates = [];
|
|
108
|
+
for (const section of blueprint.sections) {
|
|
109
|
+
for (const el of section.elements) {
|
|
110
|
+
if (el.role === "button") {
|
|
111
|
+
candidates.push({
|
|
112
|
+
logicalName: el.logicalName,
|
|
113
|
+
label: el.accessibleName,
|
|
114
|
+
role: "button",
|
|
115
|
+
xpath: el.xpath,
|
|
116
|
+
mutability: el.mutability
|
|
117
|
+
});
|
|
118
|
+
} else if (el.role === "link" && bogusHrefLinkLabels.has(el.accessibleName)) {
|
|
119
|
+
candidates.push({
|
|
120
|
+
logicalName: el.logicalName,
|
|
121
|
+
label: el.accessibleName,
|
|
122
|
+
role: "link",
|
|
123
|
+
xpath: el.xpath,
|
|
124
|
+
mutability: el.mutability
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const rep of section.repeatingElements) {
|
|
129
|
+
if (rep.role !== "button" && rep.role !== "link")
|
|
130
|
+
continue;
|
|
131
|
+
if (rep.items.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
const probedItem = rep.items[0].parameters;
|
|
134
|
+
const label = rep.accessibleNameTemplate.replace(
|
|
135
|
+
/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
|
|
136
|
+
(_, k) => probedItem[k] ?? `{${k}}`
|
|
137
|
+
);
|
|
138
|
+
if (rep.role === "link" && !bogusHrefLinkLabels.has(label))
|
|
139
|
+
continue;
|
|
140
|
+
candidates.push({
|
|
141
|
+
logicalName: rep.logicalName,
|
|
142
|
+
label,
|
|
143
|
+
role: rep.role,
|
|
144
|
+
xpath: (0, import_blueprint.resolveXpath)(rep, probedItem),
|
|
145
|
+
mutability: rep.mutability,
|
|
146
|
+
repeatingSource: {
|
|
147
|
+
parameters: [...rep.parameters],
|
|
148
|
+
probedItemParameters: { ...probedItem }
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
if (seenXpaths.has(candidate.xpath))
|
|
155
|
+
continue;
|
|
156
|
+
seenXpaths.add(candidate.xpath);
|
|
157
|
+
if (probeMode === "immutable-only") {
|
|
158
|
+
if (candidate.mutability !== "immutable")
|
|
159
|
+
continue;
|
|
160
|
+
if (DESTRUCTIVE_VERB_RE.test(candidate.label))
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await page.goto(fromUrl, { waitUntil: "networkidle" });
|
|
165
|
+
const beforeUrl = page.url();
|
|
166
|
+
await page.locator(`xpath=${candidate.xpath}`).click();
|
|
167
|
+
try {
|
|
168
|
+
await page.waitForURL((url) => url.toString() !== beforeUrl, { timeout: 2e3 });
|
|
169
|
+
} catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const afterUrl = page.url();
|
|
173
|
+
if (afterUrl === beforeUrl)
|
|
174
|
+
continue;
|
|
175
|
+
if (sameOriginOnly && !isSameOrigin(afterUrl, entryUrl))
|
|
176
|
+
continue;
|
|
177
|
+
const mutability = candidate.mutability === "mutable" ? "mutable" : "immutable";
|
|
178
|
+
results.push({
|
|
179
|
+
logicalName: candidate.logicalName,
|
|
180
|
+
label: candidate.label,
|
|
181
|
+
url: (0, import_serialization.normalizeUrl)(afterUrl),
|
|
182
|
+
mutability,
|
|
183
|
+
probedRole: candidate.role,
|
|
184
|
+
repeatingSource: candidate.repeatingSource
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
async function crawl(entryUrl, options) {
|
|
192
|
+
const { depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath } = options;
|
|
193
|
+
const normalizedEntryUrl = (0, import_serialization.normalizeUrl)(entryUrl);
|
|
194
|
+
const visited = /* @__PURE__ */ new Set();
|
|
195
|
+
const pages = {};
|
|
196
|
+
const edges = [];
|
|
197
|
+
const browser = await playwright.chromium.launch();
|
|
198
|
+
try {
|
|
199
|
+
const context = await browser.newContext(playwrightStoragePath ? { storageState: playwrightStoragePath } : void 0);
|
|
200
|
+
const page = await context.newPage();
|
|
201
|
+
const queue = [{ url: normalizedEntryUrl, fromUrl: null, edgeVia: null, depth: 0 }];
|
|
202
|
+
while (queue.length > 0 && Object.keys(pages).length < maxPages) {
|
|
203
|
+
const entry = queue.shift();
|
|
204
|
+
const { url, fromUrl, edgeVia, depth: currentDepth } = entry;
|
|
205
|
+
if (visited.has(url))
|
|
206
|
+
continue;
|
|
207
|
+
visited.add(url);
|
|
208
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
209
|
+
const resolvedUrl = (0, import_serialization.normalizeUrl)(page.url());
|
|
210
|
+
if (resolvedUrl !== url) {
|
|
211
|
+
if (visited.has(resolvedUrl))
|
|
212
|
+
continue;
|
|
213
|
+
visited.add(resolvedUrl);
|
|
214
|
+
}
|
|
215
|
+
let blueprint;
|
|
216
|
+
try {
|
|
217
|
+
blueprint = await (0, import_blueprint.buildPageBlueprint)(page);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof import_blueprint.BlueprintInvariantError) {
|
|
220
|
+
console.warn(`[dom-analyzer] Skipping blueprint for ${page.url()} \u2014 invariant violated: ${err.message}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
pages[resolvedUrl] = blueprint;
|
|
226
|
+
if (fromUrl && edgeVia) {
|
|
227
|
+
const via = {
|
|
228
|
+
logicalName: edgeVia.logicalName,
|
|
229
|
+
mutability: edgeVia.mutability
|
|
230
|
+
};
|
|
231
|
+
let toTemplate;
|
|
232
|
+
if (edgeVia.repeatingSource) {
|
|
233
|
+
const tmpl = substituteUrlSegments(
|
|
234
|
+
resolvedUrl,
|
|
235
|
+
edgeVia.repeatingSource.probedItemParameters
|
|
236
|
+
);
|
|
237
|
+
if (tmpl !== resolvedUrl) {
|
|
238
|
+
toTemplate = tmpl;
|
|
239
|
+
const usedParams = edgeVia.repeatingSource.parameters.filter(
|
|
240
|
+
(p) => tmpl.includes(`{${p}}`)
|
|
241
|
+
);
|
|
242
|
+
if (usedParams.length > 0)
|
|
243
|
+
via.parameters = usedParams;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
edges.push({
|
|
247
|
+
from: (0, import_serialization.normalizeUrl)(fromUrl),
|
|
248
|
+
to: resolvedUrl,
|
|
249
|
+
...toTemplate ? { toTemplate } : {},
|
|
250
|
+
via,
|
|
251
|
+
label: edgeVia.label,
|
|
252
|
+
action: edgeVia.action
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (currentDepth < depth) {
|
|
256
|
+
const aria = await page.locator("body").ariaSnapshot();
|
|
257
|
+
const bogusHrefLinkLabels = /* @__PURE__ */ new Set();
|
|
258
|
+
for (const { label, href } of extractLinksFromAriaSnapshot(aria)) {
|
|
259
|
+
if (isBogusHref(href)) {
|
|
260
|
+
bogusHrefLinkLabels.add(label);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const resolved = resolveUrl(href, resolvedUrl);
|
|
264
|
+
if (!resolved)
|
|
265
|
+
continue;
|
|
266
|
+
const normalized = (0, import_serialization.normalizeUrl)(resolved);
|
|
267
|
+
if (sameOriginOnly && !isSameOrigin(normalized, normalizedEntryUrl))
|
|
268
|
+
continue;
|
|
269
|
+
if (visited.has(normalized))
|
|
270
|
+
continue;
|
|
271
|
+
const linkMatch = findLinkByLabel(blueprint, label);
|
|
272
|
+
const logicalName = linkMatch?.logicalName ?? "unknown_link";
|
|
273
|
+
queue.push({
|
|
274
|
+
url: normalized,
|
|
275
|
+
fromUrl: resolvedUrl,
|
|
276
|
+
edgeVia: { logicalName, mutability: "immutable", action: "link", label },
|
|
277
|
+
depth: currentDepth + 1
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const clickNavs = await probeClickNavigations(
|
|
281
|
+
page,
|
|
282
|
+
resolvedUrl,
|
|
283
|
+
blueprint,
|
|
284
|
+
probeButtons,
|
|
285
|
+
sameOriginOnly,
|
|
286
|
+
normalizedEntryUrl,
|
|
287
|
+
bogusHrefLinkLabels
|
|
288
|
+
);
|
|
289
|
+
for (const nav of clickNavs) {
|
|
290
|
+
if (visited.has(nav.url))
|
|
291
|
+
continue;
|
|
292
|
+
queue.push({
|
|
293
|
+
url: nav.url,
|
|
294
|
+
fromUrl: resolvedUrl,
|
|
295
|
+
edgeVia: {
|
|
296
|
+
logicalName: nav.logicalName,
|
|
297
|
+
mutability: nav.mutability,
|
|
298
|
+
// A probed link that took us to a new URL is recorded as a
|
|
299
|
+
// 'link' action (matches the nav intent). Probed buttons stay
|
|
300
|
+
// as 'click'.
|
|
301
|
+
action: nav.probedRole === "link" ? "link" : "click",
|
|
302
|
+
label: nav.label,
|
|
303
|
+
repeatingSource: nav.repeatingSource
|
|
304
|
+
},
|
|
305
|
+
depth: currentDepth + 1
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
await context.close();
|
|
311
|
+
} finally {
|
|
312
|
+
await browser.close();
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
schemaVersion: import_serialization.SCHEMA_VERSION,
|
|
316
|
+
entryUrl: normalizedEntryUrl,
|
|
317
|
+
cachedAt: (0, import_serialization.nowTimestamp)(),
|
|
318
|
+
pages,
|
|
319
|
+
edges
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function crawlSinglePage(entryUrl, options) {
|
|
323
|
+
const browser = await playwright.chromium.launch();
|
|
324
|
+
try {
|
|
325
|
+
const context = await browser.newContext(options.playwrightStoragePath ? { storageState: options.playwrightStoragePath } : void 0);
|
|
326
|
+
const page = await context.newPage();
|
|
327
|
+
await page.goto(entryUrl, { waitUntil: "networkidle" });
|
|
328
|
+
let blueprint;
|
|
329
|
+
try {
|
|
330
|
+
blueprint = await (0, import_blueprint.buildPageBlueprint)(page);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof import_blueprint.BlueprintInvariantError) {
|
|
333
|
+
console.warn(`[dom-analyzer] Blueprint skipped \u2014 invariant violated: ${err.message}`);
|
|
334
|
+
await context.close();
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
const normalizedEntryUrl = (0, import_serialization.normalizeUrl)(page.url());
|
|
340
|
+
await context.close();
|
|
341
|
+
return {
|
|
342
|
+
schemaVersion: import_serialization.SCHEMA_VERSION,
|
|
343
|
+
entryUrl: normalizedEntryUrl,
|
|
344
|
+
cachedAt: (0, import_serialization.nowTimestamp)(),
|
|
345
|
+
pages: { [normalizedEntryUrl]: blueprint },
|
|
346
|
+
edges: []
|
|
347
|
+
};
|
|
348
|
+
} finally {
|
|
349
|
+
await browser.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function extractLinksFromAriaSnapshot(aria) {
|
|
353
|
+
const out = [];
|
|
354
|
+
const lines = aria.split("\n");
|
|
355
|
+
for (let i = 0; i < lines.length; i++) {
|
|
356
|
+
const line = lines[i];
|
|
357
|
+
const m = line.match(/-\s+link\s+"([^"]+)"/);
|
|
358
|
+
if (!m)
|
|
359
|
+
continue;
|
|
360
|
+
const label = m[1];
|
|
361
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
362
|
+
const urlMatch = lines[j].match(/-\s+\/url:\s+(\S+)/);
|
|
363
|
+
if (urlMatch) {
|
|
364
|
+
out.push({ label, href: urlMatch[1] });
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
function findLinkByLabel(blueprint, label) {
|
|
372
|
+
for (const section of blueprint.sections) {
|
|
373
|
+
for (const el of section.elements) {
|
|
374
|
+
if (el.role === "link" && el.accessibleName === label)
|
|
375
|
+
return { logicalName: el.logicalName };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
381
|
+
0 && (module.exports = {
|
|
382
|
+
crawl,
|
|
383
|
+
crawlSinglePage
|
|
384
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
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 curatedWidgets_exports = {};
|
|
20
|
+
__export(curatedWidgets_exports, {
|
|
21
|
+
CURATED: () => CURATED
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(curatedWidgets_exports);
|
|
24
|
+
var import_liveFingerprints = require("./liveFingerprints");
|
|
25
|
+
const CURATED = {
|
|
26
|
+
[(0, import_liveFingerprints.fp)("vue-treeselect")]: {
|
|
27
|
+
source: "curated",
|
|
28
|
+
confidence: "high",
|
|
29
|
+
widgetIdentification: "Vue-Treeselect",
|
|
30
|
+
parameters: ["value"],
|
|
31
|
+
steps: [
|
|
32
|
+
{ kind: "click", target: "trigger" },
|
|
33
|
+
{ kind: "waitFor", location: "inDom", signal: "visible" },
|
|
34
|
+
{ kind: "click", target: "option", matchBy: "accessibleName", param: "value" }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
[(0, import_liveFingerprints.fp)("radix-select")]: {
|
|
38
|
+
source: "curated",
|
|
39
|
+
confidence: "high",
|
|
40
|
+
widgetIdentification: "Radix Select",
|
|
41
|
+
parameters: ["value"],
|
|
42
|
+
steps: [
|
|
43
|
+
{ kind: "click", target: "trigger" },
|
|
44
|
+
{ kind: "waitFor", location: "portal", signal: "visible" },
|
|
45
|
+
{ kind: "handoff" }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
[(0, import_liveFingerprints.fp)("stripe-frame")]: {
|
|
49
|
+
source: "curated",
|
|
50
|
+
confidence: "high",
|
|
51
|
+
widgetIdentification: "Stripe CardNumberElement",
|
|
52
|
+
parameters: ["value"],
|
|
53
|
+
// framePath uses `title*="card"` rather than `src*="stripe.com"`:
|
|
54
|
+
// - Real Stripe Elements iframes have title like "Secure card number
|
|
55
|
+
// input frame" — `title*="card"` matches.
|
|
56
|
+
// - The widgets.html fixture (used for curating this fingerprint) has
|
|
57
|
+
// `title="Stripe card number"` — also matches.
|
|
58
|
+
// - `src*="stripe.com"` matches real Stripe but NOT the fixture
|
|
59
|
+
// (fixture uses `src="about:blank"` for offline testing). That mismatch
|
|
60
|
+
// made the curated contract unrunnable against the same DOM that
|
|
61
|
+
// produced its fingerprint. Title-based identification is stable
|
|
62
|
+
// across the fixture/production split.
|
|
63
|
+
steps: [
|
|
64
|
+
{ kind: "enterFrame", framePath: ['iframe[title*="card"]'] },
|
|
65
|
+
{ kind: "fill", target: "activeInput", param: "value" },
|
|
66
|
+
{ kind: "exitFrame" }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
71
|
+
0 && (module.exports = {
|
|
72
|
+
CURATED
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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 dynamicId_exports = {};
|
|
20
|
+
__export(dynamicId_exports, {
|
|
21
|
+
isDynamicId: () => isDynamicId
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(dynamicId_exports);
|
|
24
|
+
function isDynamicId(id) {
|
|
25
|
+
if (/^react-aria\d+/.test(id)) return true;
|
|
26
|
+
if (/^mui-\d+/.test(id)) return true;
|
|
27
|
+
if (/^(mat|cdk)-[a-z]+-\d+$/.test(id)) return true;
|
|
28
|
+
if (/[-_]\d+$/.test(id)) return true;
|
|
29
|
+
if (/\d{2,}[_-][a-zA-Z]/.test(id)) return true;
|
|
30
|
+
if (/^\d+$/.test(id)) return true;
|
|
31
|
+
if (/\d{4,}$/.test(id)) return true;
|
|
32
|
+
if (/[-_][0-9a-f]{6,}$/i.test(id)) return true;
|
|
33
|
+
const shortHexMatch = id.match(/[-_]([0-9a-f]{3,5})$/i);
|
|
34
|
+
if (shortHexMatch && /[0-9]/.test(shortHexMatch[1])) return true;
|
|
35
|
+
if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
|
|
36
|
+
if (id.includes(":")) return true;
|
|
37
|
+
if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
41
|
+
0 && (module.exports = {
|
|
42
|
+
isDynamicId
|
|
43
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_dynamicId = require("./dynamicId");
|
|
3
|
+
const cases = [];
|
|
4
|
+
function test(name, run) {
|
|
5
|
+
cases.push({ name, run });
|
|
6
|
+
}
|
|
7
|
+
function assertTrue(v, msg) {
|
|
8
|
+
if (!v) throw new Error(msg);
|
|
9
|
+
}
|
|
10
|
+
function assertFalse(v, msg) {
|
|
11
|
+
if (v) throw new Error(msg);
|
|
12
|
+
}
|
|
13
|
+
test("isDynamicId: react-aria prefix", () => {
|
|
14
|
+
assertTrue((0, import_dynamicId.isDynamicId)("react-aria9765209213-_r_nj_"), "react-aria");
|
|
15
|
+
});
|
|
16
|
+
test("isDynamicId: mui prefix", () => {
|
|
17
|
+
assertTrue((0, import_dynamicId.isDynamicId)("mui-6"), "mui-6");
|
|
18
|
+
});
|
|
19
|
+
test("isDynamicId: Angular Material mat-input", () => {
|
|
20
|
+
assertTrue((0, import_dynamicId.isDynamicId)("mat-input-3"), "mat-input-3");
|
|
21
|
+
assertTrue((0, import_dynamicId.isDynamicId)("cdk-overlay-0"), "cdk-overlay-0");
|
|
22
|
+
});
|
|
23
|
+
test("isDynamicId: generic numeric suffix after separator", () => {
|
|
24
|
+
assertTrue((0, import_dynamicId.isDynamicId)("item-42"), "item-42");
|
|
25
|
+
assertTrue((0, import_dynamicId.isDynamicId)("row_123"), "row_123");
|
|
26
|
+
});
|
|
27
|
+
test("isDynamicId: mid-string counter", () => {
|
|
28
|
+
assertTrue((0, import_dynamicId.isDynamicId)("_commonPopup27_iframe"), "_commonPopup27_iframe");
|
|
29
|
+
});
|
|
30
|
+
test("isDynamicId: pure numeric", () => {
|
|
31
|
+
assertTrue((0, import_dynamicId.isDynamicId)("12345"), "12345");
|
|
32
|
+
});
|
|
33
|
+
test("isDynamicId: long numeric sequence suffix", () => {
|
|
34
|
+
assertTrue((0, import_dynamicId.isDynamicId)("dropdown99924304"), "dropdown99924304");
|
|
35
|
+
});
|
|
36
|
+
test("isDynamicId: long hex suffix", () => {
|
|
37
|
+
assertTrue((0, import_dynamicId.isDynamicId)("component-a3f2b1"), "component-a3f2b1");
|
|
38
|
+
});
|
|
39
|
+
test("isDynamicId: short hex suffix with digit", () => {
|
|
40
|
+
assertTrue((0, import_dynamicId.isDynamicId)("input-093d"), "input-093d");
|
|
41
|
+
assertTrue((0, import_dynamicId.isDynamicId)("input-1a98"), "input-1a98");
|
|
42
|
+
});
|
|
43
|
+
test("isDynamicId: trailing 3+ digits after letter without separator", () => {
|
|
44
|
+
assertTrue((0, import_dynamicId.isDynamicId)("checkbox877"), "checkbox877");
|
|
45
|
+
assertTrue((0, import_dynamicId.isDynamicId)("button123"), "button123");
|
|
46
|
+
});
|
|
47
|
+
test("isDynamicId: colon ID (Radix UI)", () => {
|
|
48
|
+
assertTrue((0, import_dynamicId.isDynamicId)("radix-:ra2:"), "radix-:ra2:");
|
|
49
|
+
assertTrue((0, import_dynamicId.isDynamicId)("radix-:r0:"), "radix-:r0:");
|
|
50
|
+
});
|
|
51
|
+
test("isDynamicId: Vue Tables 2 search pattern", () => {
|
|
52
|
+
assertTrue((0, import_dynamicId.isDynamicId)("VueTables__search_abcde"), "VueTables__search");
|
|
53
|
+
});
|
|
54
|
+
test("isDynamicId: stable kebab-case names are not dynamic", () => {
|
|
55
|
+
assertFalse((0, import_dynamicId.isDynamicId)("vt-trigger"), "vt-trigger");
|
|
56
|
+
assertFalse((0, import_dynamicId.isDynamicId)("toast-region"), "toast-region");
|
|
57
|
+
assertFalse((0, import_dynamicId.isDynamicId)("ga_workflow-list"), "ga_workflow-list");
|
|
58
|
+
assertFalse((0, import_dynamicId.isDynamicId)("mui-component-select-role"), "mui-component-select-role");
|
|
59
|
+
});
|
|
60
|
+
test("isDynamicId: short hex suffix without digit is not dynamic", () => {
|
|
61
|
+
assertFalse((0, import_dynamicId.isDynamicId)("item-bad"), "item-bad (all letters)");
|
|
62
|
+
assertFalse((0, import_dynamicId.isDynamicId)("item-fee"), "item-fee (all letters)");
|
|
63
|
+
});
|
|
64
|
+
test("isDynamicId: single trailing digit after letter is not dynamic", () => {
|
|
65
|
+
assertFalse((0, import_dynamicId.isDynamicId)("item1"), "item1 (single digit)");
|
|
66
|
+
assertFalse((0, import_dynamicId.isDynamicId)("item12"), "item12 (2 digits, below 3-digit threshold)");
|
|
67
|
+
});
|
|
68
|
+
let failed = 0;
|
|
69
|
+
for (const { name, run } of cases) {
|
|
70
|
+
try {
|
|
71
|
+
run();
|
|
72
|
+
console.log(" \u2713", name);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
failed++;
|
|
75
|
+
console.log(" \u2717", name);
|
|
76
|
+
console.log(" ", e.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (failed > 0) {
|
|
80
|
+
console.log(`
|
|
81
|
+
${failed}/${cases.length} failed`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
console.log(`
|
|
85
|
+
${cases.length} passed`);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var fingerprint_exports = {};
|
|
30
|
+
__export(fingerprint_exports, {
|
|
31
|
+
BEM_ISH: () => BEM_ISH,
|
|
32
|
+
CSS_MODULE_HASH: () => CSS_MODULE_HASH,
|
|
33
|
+
FRAMEWORK_PREFIXES: () => FRAMEWORK_PREFIXES,
|
|
34
|
+
NUMERIC_INDEX_SUFFIX: () => NUMERIC_INDEX_SUFFIX,
|
|
35
|
+
STATE_TOKENS: () => STATE_TOKENS,
|
|
36
|
+
TAILWIND_UTILITY: () => TAILWIND_UTILITY,
|
|
37
|
+
canonicalJson: () => canonicalJson,
|
|
38
|
+
computeFingerprintFromInput: () => computeFingerprintFromInput,
|
|
39
|
+
normalizeClasses: () => normalizeClasses
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(fingerprint_exports);
|
|
42
|
+
var crypto = __toESM(require("crypto"));
|
|
43
|
+
const BEM_ISH = /^[a-z][a-z-]+(__[a-z-]+)?(--[a-z-]+)?$/;
|
|
44
|
+
const FRAMEWORK_PREFIXES = ["data-radix-", "data-headlessui-", "MuiSelect-", "ant-select-"];
|
|
45
|
+
const TAILWIND_UTILITY = /^(bg|text|p|m|w|h|flex|grid|border|rounded|shadow)-|^(hover|focus|md|lg|sm|xl):/;
|
|
46
|
+
const CSS_MODULE_HASH = /_[a-z0-9]{5,}$/;
|
|
47
|
+
const NUMERIC_INDEX_SUFFIX = /-\d+$/;
|
|
48
|
+
const STATE_TOKENS = /* @__PURE__ */ new Set(["is-active", "is-open", "expanded", "selected", "active", "open"]);
|
|
49
|
+
function isStructural(cls) {
|
|
50
|
+
if (!cls) return false;
|
|
51
|
+
if (FRAMEWORK_PREFIXES.some((p) => cls.startsWith(p))) return true;
|
|
52
|
+
if (STATE_TOKENS.has(cls)) return false;
|
|
53
|
+
if (TAILWIND_UTILITY.test(cls)) return false;
|
|
54
|
+
if (CSS_MODULE_HASH.test(cls)) return false;
|
|
55
|
+
if (NUMERIC_INDEX_SUFFIX.test(cls)) return false;
|
|
56
|
+
return BEM_ISH.test(cls);
|
|
57
|
+
}
|
|
58
|
+
function normalizeClasses(className) {
|
|
59
|
+
if (!className) return "";
|
|
60
|
+
const tokens = className.split(/\s+/).filter(Boolean);
|
|
61
|
+
const kept = tokens.filter(isStructural);
|
|
62
|
+
const deduped = Array.from(new Set(kept));
|
|
63
|
+
deduped.sort();
|
|
64
|
+
return deduped.join(" ");
|
|
65
|
+
}
|
|
66
|
+
function canonicalJson(value) {
|
|
67
|
+
if (value === null || typeof value !== "object")
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return "[" + value.map(canonicalJson).join(",") + "]";
|
|
71
|
+
const obj = value;
|
|
72
|
+
const keys = Object.keys(obj).sort();
|
|
73
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
|
|
74
|
+
}
|
|
75
|
+
function computeFingerprintFromInput(input) {
|
|
76
|
+
const json = canonicalJson(input);
|
|
77
|
+
return crypto.createHash("sha256").update(json).digest("hex").slice(0, 16);
|
|
78
|
+
}
|
|
79
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
80
|
+
0 && (module.exports = {
|
|
81
|
+
BEM_ISH,
|
|
82
|
+
CSS_MODULE_HASH,
|
|
83
|
+
FRAMEWORK_PREFIXES,
|
|
84
|
+
NUMERIC_INDEX_SUFFIX,
|
|
85
|
+
STATE_TOKENS,
|
|
86
|
+
TAILWIND_UTILITY,
|
|
87
|
+
canonicalJson,
|
|
88
|
+
computeFingerprintFromInput,
|
|
89
|
+
normalizeClasses
|
|
90
|
+
});
|