@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,226 @@
|
|
|
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 sitemap_exports = {};
|
|
30
|
+
__export(sitemap_exports, {
|
|
31
|
+
default: () => sitemap_default
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(sitemap_exports);
|
|
34
|
+
var crypto = __toESM(require("crypto"));
|
|
35
|
+
var fs = __toESM(require("fs"));
|
|
36
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
37
|
+
var import_tool = require("./tool");
|
|
38
|
+
var import_crawler = require("../../../dom-analyzer/crawler");
|
|
39
|
+
var import_blueprint = require("../../../dom-analyzer/blueprint");
|
|
40
|
+
var import_serialization = require("../../../dom-analyzer/serialization");
|
|
41
|
+
function hashStorageState(path) {
|
|
42
|
+
if (!path)
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const contents = fs.readFileSync(path, "utf-8");
|
|
46
|
+
return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
|
|
47
|
+
} catch {
|
|
48
|
+
const pathHash = crypto.createHash("sha256").update(path).digest("hex").slice(0, 16);
|
|
49
|
+
return `unreadable:${pathHash}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function cacheKey(entryUrl, storageStateHash, probeButtons) {
|
|
53
|
+
return `${entryUrl}::${storageStateHash ?? "no-auth"}::${probeButtons}`;
|
|
54
|
+
}
|
|
55
|
+
function humanDuration(ms) {
|
|
56
|
+
if (ms < 6e4)
|
|
57
|
+
return `${Math.round(ms / 1e3)}s`;
|
|
58
|
+
return `${Math.round(ms / 6e4)}m`;
|
|
59
|
+
}
|
|
60
|
+
const sitemapBuild = (0, import_tool.defineTool)({
|
|
61
|
+
capability: "core",
|
|
62
|
+
schema: {
|
|
63
|
+
name: "browser_sitemap_build",
|
|
64
|
+
title: "Build Sitemap (graph of PageBlueprints)",
|
|
65
|
+
description: [
|
|
66
|
+
"Crawl an application starting from a URL and build a Sitemap \u2014 a graph of PageBlueprints (one per URL)",
|
|
67
|
+
"connected by navigation edges. The Sitemap is cached in the session.",
|
|
68
|
+
"",
|
|
69
|
+
"Call once at the start of a session. Subsequent calls within the TTL (~30 minutes) reuse the cached",
|
|
70
|
+
"Sitemap unless `refresh: true` is passed. Use `browser_sitemap_query` to read already-crawled pages",
|
|
71
|
+
"rather than re-calling this tool.",
|
|
72
|
+
"",
|
|
73
|
+
"Depth defaults to 5, maxPages to 50 (the real bound in practice).",
|
|
74
|
+
"",
|
|
75
|
+
'\u26A0\uFE0F SAFETY: By default, `probeButtons: "immutable-only"` skips destructive-looking buttons',
|
|
76
|
+
"(Delete / Submit / Place Order / etc.) and buttons inside forms to prevent side effects during",
|
|
77
|
+
'crawling. Use `probeButtons: "all"` only against dev / staging environments \u2014 it will click every',
|
|
78
|
+
"unique button, which can submit forms, create records, send notifications, or mutate server state."
|
|
79
|
+
].join("\n"),
|
|
80
|
+
inputSchema: import_mcpBundle.z.object({
|
|
81
|
+
url: import_mcpBundle.z.string().describe("Entry URL to start crawling from"),
|
|
82
|
+
depth: import_mcpBundle.z.number().optional().default(5).describe("Max crawl depth (default: 5)"),
|
|
83
|
+
maxPages: import_mcpBundle.z.number().optional().default(50).describe("Max pages to visit (default: 50)"),
|
|
84
|
+
sameOriginOnly: import_mcpBundle.z.boolean().optional().default(true).describe("Only follow same-origin links (default: true)"),
|
|
85
|
+
probeButtons: import_mcpBundle.z.enum(["immutable-only", "all", "none"]).optional().default("immutable-only").describe('Button-probing safety mode. Default: immutable-only (safe; skips destructive-verb buttons and buttons inside forms). Use "all" only against dev/staging \u2014 probes every button. "none" disables button probing entirely.'),
|
|
86
|
+
playwrightStoragePath: import_mcpBundle.z.string().optional().describe("Path to a Playwright storageState.json file \u2014 cookies, localStorage, sessionStorage per origin. Use this to crawl apps behind a login."),
|
|
87
|
+
refresh: import_mcpBundle.z.boolean().optional().default(false).describe("Force a full re-crawl, bypassing the cache")
|
|
88
|
+
}),
|
|
89
|
+
type: "readOnly"
|
|
90
|
+
},
|
|
91
|
+
handle: async (context, params, response) => {
|
|
92
|
+
const { url, depth, maxPages, sameOriginOnly, probeButtons, playwrightStoragePath, refresh } = params;
|
|
93
|
+
const normalizedUrl = (0, import_serialization.normalizeUrl)(url);
|
|
94
|
+
const storageStateHash = hashStorageState(playwrightStoragePath);
|
|
95
|
+
const probeMode = probeButtons;
|
|
96
|
+
const key = cacheKey(normalizedUrl, storageStateHash, probeMode);
|
|
97
|
+
const cached = context.sitemapCache.get(key);
|
|
98
|
+
if (cached && !refresh) {
|
|
99
|
+
const age = Date.now() - new Date(cached.sitemap.cachedAt).getTime();
|
|
100
|
+
if (age < import_serialization.CRAWL_TTL_MS) {
|
|
101
|
+
const pageCount2 = Object.keys(cached.sitemap.pages).length;
|
|
102
|
+
response.addTextResult(
|
|
103
|
+
`Reusing cached Sitemap from ${humanDuration(age)} ago.
|
|
104
|
+
Entry: ${cached.sitemap.entryUrl}
|
|
105
|
+
Pages: ${pageCount2}
|
|
106
|
+
Edges: ${cached.sitemap.edges.length}
|
|
107
|
+
Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
|
|
108
|
+
Probe mode: ${probeMode}
|
|
109
|
+
|
|
110
|
+
Use browser_sitemap_query to read any page. Pass refresh=true to recrawl.`
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let sitemap;
|
|
116
|
+
if (depth > 0) {
|
|
117
|
+
sitemap = await (0, import_crawler.crawl)(url, {
|
|
118
|
+
depth,
|
|
119
|
+
maxPages,
|
|
120
|
+
sameOriginOnly,
|
|
121
|
+
probeButtons: probeMode,
|
|
122
|
+
playwrightStoragePath
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
sitemap = await (0, import_crawler.crawlSinglePage)(url, { playwrightStoragePath });
|
|
126
|
+
}
|
|
127
|
+
context.sitemapCache.set(key, { sitemap, storageStateHash, probeButtonsMode: probeMode });
|
|
128
|
+
const pageCount = Object.keys(sitemap.pages).length;
|
|
129
|
+
const pageList = Object.keys(sitemap.pages).map((u) => {
|
|
130
|
+
const bp = sitemap.pages[u];
|
|
131
|
+
const elCount = bp.sections.reduce((sum, s) => sum + s.elements.length + s.repeatingElements.length, 0);
|
|
132
|
+
return ` - ${u} (${elCount} elements across ${bp.sections.length} sections)`;
|
|
133
|
+
}).join("\n");
|
|
134
|
+
response.addTextResult(
|
|
135
|
+
`Sitemap built for ${sitemap.entryUrl}
|
|
136
|
+
Pages: ${pageCount}
|
|
137
|
+
Edges: ${sitemap.edges.length}
|
|
138
|
+
Storage state: ${storageStateHash ? `auth hash ${storageStateHash}` : "none"}
|
|
139
|
+
Probe mode: ${probeMode}
|
|
140
|
+
|
|
141
|
+
Pages:
|
|
142
|
+
${pageList}
|
|
143
|
+
|
|
144
|
+
Use browser_sitemap_query to read page blueprints, edges, or derived views (mapJson, outline).`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const sitemapQuery = (0, import_tool.defineTool)({
|
|
149
|
+
capability: "core",
|
|
150
|
+
schema: {
|
|
151
|
+
name: "browser_sitemap_query",
|
|
152
|
+
title: "Query the cached Sitemap",
|
|
153
|
+
description: [
|
|
154
|
+
"Query the Sitemap cached by browser_sitemap_build. Use this instead of re-calling sitemap_build.",
|
|
155
|
+
"",
|
|
156
|
+
"Modes:",
|
|
157
|
+
" page \u2014 returns the full canonical PageBlueprint for the given URL",
|
|
158
|
+
" (sections with enrichment, logical names, XPaths)",
|
|
159
|
+
" edges \u2014 returns navigation edges originating from the given URL",
|
|
160
|
+
" mapJson \u2014 returns the derived flat logicalName \u2192 xpath map for the URL",
|
|
161
|
+
" outline \u2014 returns the derived textual section-to-element hierarchy for the URL",
|
|
162
|
+
" overview \u2014 (when url is omitted) returns page list + edge list summary"
|
|
163
|
+
].join("\n"),
|
|
164
|
+
inputSchema: import_mcpBundle.z.object({
|
|
165
|
+
type: import_mcpBundle.z.enum(["page", "edges", "mapJson", "outline"]).optional().describe("Query mode. Omit with url to get overview."),
|
|
166
|
+
url: import_mcpBundle.z.string().optional().describe("Page URL to query. Omit to get overview across all pages.")
|
|
167
|
+
}),
|
|
168
|
+
type: "readOnly"
|
|
169
|
+
},
|
|
170
|
+
handle: async (context, params, response) => {
|
|
171
|
+
if (context.sitemapCache.size === 0) {
|
|
172
|
+
response.addError("No Sitemap available. Run browser_sitemap_build first.");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const latest = [...context.sitemapCache.values()].sort(
|
|
176
|
+
(a, b) => new Date(b.sitemap.cachedAt).getTime() - new Date(a.sitemap.cachedAt).getTime()
|
|
177
|
+
)[0];
|
|
178
|
+
const sitemap = latest.sitemap;
|
|
179
|
+
const { type, url } = params;
|
|
180
|
+
if (!url || !type) {
|
|
181
|
+
const overview = {
|
|
182
|
+
schemaVersion: sitemap.schemaVersion,
|
|
183
|
+
entryUrl: sitemap.entryUrl,
|
|
184
|
+
cachedAt: sitemap.cachedAt,
|
|
185
|
+
pages: Object.keys(sitemap.pages).map((u) => ({
|
|
186
|
+
url: u,
|
|
187
|
+
sections: sitemap.pages[u].sections.length,
|
|
188
|
+
elements: sitemap.pages[u].sections.reduce(
|
|
189
|
+
(s, sec) => s + sec.elements.length + sec.repeatingElements.length,
|
|
190
|
+
0
|
|
191
|
+
),
|
|
192
|
+
pageHash: sitemap.pages[u].pageHash
|
|
193
|
+
})),
|
|
194
|
+
edges: sitemap.edges
|
|
195
|
+
};
|
|
196
|
+
response.addTextResult(JSON.stringify(overview, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const normalizedQueryUrl = (0, import_serialization.normalizeUrl)(url);
|
|
200
|
+
const blueprint = sitemap.pages[normalizedQueryUrl];
|
|
201
|
+
if (!blueprint) {
|
|
202
|
+
response.addError(
|
|
203
|
+
`Page not found in Sitemap: ${normalizedQueryUrl}. Available pages: ${Object.keys(sitemap.pages).join(", ")}`
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (type === "page") {
|
|
208
|
+
response.addTextResult(JSON.stringify(blueprint, null, 2));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (type === "edges") {
|
|
212
|
+
const edges = sitemap.edges.filter((e) => e.from === normalizedQueryUrl);
|
|
213
|
+
response.addTextResult(JSON.stringify({ url: normalizedQueryUrl, edges }, null, 2));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (type === "mapJson") {
|
|
217
|
+
response.addTextResult(JSON.stringify((0, import_blueprint.buildMap)(blueprint), null, 2));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (type === "outline") {
|
|
221
|
+
response.addTextResult((0, import_blueprint.buildOutline)(blueprint));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
var sitemap_default = [sitemapBuild, sitemapQuery];
|
|
@@ -30,7 +30,7 @@ const snapshot = (0, import_tool.defineTool)({
|
|
|
30
30
|
schema: {
|
|
31
31
|
name: "browser_snapshot",
|
|
32
32
|
title: "Page snapshot",
|
|
33
|
-
description: "Capture the ARIA accessibility tree of the current page. Returns element refs required by all interaction tools (browser_click, browser_type, browser_hover, etc.). Call before any interaction and after every action that changes the page to get fresh refs. If any interaction tool fails with a stale ref error, call this first to refresh.",
|
|
33
|
+
description: "Capture the ARIA accessibility tree of the current page. Returns element refs required by all interaction tools (browser_click, browser_type, browser_hover, etc.). Call before any interaction and after every action that changes the page to get fresh refs. If any interaction tool fails with a stale ref error, call this first to refresh. For semantic identity (role/accessibleName/testId/stableId) and recommendation grounding, use browser_blueprint instead \u2014 refs from this tool are ephemeral and must not appear in generated test code.",
|
|
34
34
|
inputSchema: import_mcpBundle.z.object({
|
|
35
35
|
filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
|
|
36
36
|
}),
|
|
@@ -55,7 +55,7 @@ const click = (0, import_tool.defineTabTool)({
|
|
|
55
55
|
schema: {
|
|
56
56
|
name: "browser_click",
|
|
57
57
|
title: "Click",
|
|
58
|
-
description: "Click an element on the page. Always click the actual interactive element (button, link, input) \u2014 never a container or wrapper div. NEVER click a text field before typing \u2014 browser_type auto-focuses; a prior click injects extra network requests that corrupt the trace. NEVER click a row or link in a file list to access contextual actions \u2014 use browser_hover on the row instead.",
|
|
58
|
+
description: "Click an element on the page. Always click the actual interactive element (button, link, input) \u2014 never a container or wrapper div. NEVER click a text field before typing \u2014 browser_type auto-focuses; a prior click injects extra network requests that corrupt the trace. NEVER click a row or link in a file list to access contextual actions \u2014 use browser_hover on the row instead. When recording a UI trace for capture-act-capture, capture browser_blueprint before this click and again after \u2014 the diff between them grounds your post-action assertions.",
|
|
59
59
|
inputSchema: clickSchema,
|
|
60
60
|
type: "input"
|
|
61
61
|
},
|
|
@@ -0,0 +1,168 @@
|
|
|
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 widgetContract_exports = {};
|
|
20
|
+
__export(widgetContract_exports, {
|
|
21
|
+
default: () => widgetContract_default
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(widgetContract_exports);
|
|
24
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
25
|
+
var import_tool = require("./tool");
|
|
26
|
+
var import_widgetContract = require("../../../dom-analyzer/widgetContract");
|
|
27
|
+
const widgetContractLookup = (0, import_tool.defineTabTool)({
|
|
28
|
+
capability: "core",
|
|
29
|
+
schema: {
|
|
30
|
+
name: "browser_widget_contract_lookup",
|
|
31
|
+
title: "Look up a widget interaction contract by fingerprint",
|
|
32
|
+
description: [
|
|
33
|
+
"Returns the interaction contract for a widget's fingerprint.",
|
|
34
|
+
"",
|
|
35
|
+
"Only call for elements whose `widgetType` is `custom` or `unknown` \u2014 native widgets don't",
|
|
36
|
+
"need contracts. Pass the element's ref along with the fingerprint.",
|
|
37
|
+
"",
|
|
38
|
+
'On `status: "needs_inference"`, synthesize a WidgetContract from your own reasoning using',
|
|
39
|
+
"the returned context (outerHtml, ancestor chain, aria subtree, optional portal candidate),",
|
|
40
|
+
"submit it via `browser_widget_contract_cache`, then execute the contract using existing",
|
|
41
|
+
"browser tools (`browser_click`, `browser_type`, etc.).",
|
|
42
|
+
"",
|
|
43
|
+
'On `status: "error", reason: "stale_ref"`, re-snapshot and retry.',
|
|
44
|
+
"",
|
|
45
|
+
'On `status: "error", reason: "context_gather_failed"`, inspect the message; the error may be transient (retry) or structural (give up and try a different ref).',
|
|
46
|
+
"",
|
|
47
|
+
"Do not speculatively pre-infer contracts \u2014 call only when about to interact."
|
|
48
|
+
].join("\n"),
|
|
49
|
+
inputSchema: import_mcpBundle.z.object({
|
|
50
|
+
fingerprint: import_mcpBundle.z.string().regex(/^[0-9a-f]{16}$/).describe("16-hex fingerprint from the element's PageBlueprint entry"),
|
|
51
|
+
ref: import_mcpBundle.z.string().describe('Snapshot ref for the specific element instance (e.g. "e42")')
|
|
52
|
+
}),
|
|
53
|
+
type: "readOnly"
|
|
54
|
+
},
|
|
55
|
+
handle: async (tab, params, response) => {
|
|
56
|
+
const { fingerprint, ref } = params;
|
|
57
|
+
const pure = (0, import_widgetContract.lookupContract)(fingerprint, tab.context.inferredContracts);
|
|
58
|
+
if (pure.status === "found") {
|
|
59
|
+
response.addTextResult(JSON.stringify({ status: "found", contract: pure.contract }, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let locator;
|
|
63
|
+
try {
|
|
64
|
+
({ locator } = await tab.refLocator({ ref, element: "widget-fingerprint-target" }));
|
|
65
|
+
} catch {
|
|
66
|
+
response.addTextResult(JSON.stringify({ status: "error", reason: "stale_ref" }, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let gather;
|
|
70
|
+
let ariaRaw;
|
|
71
|
+
try {
|
|
72
|
+
gather = await locator.evaluate((el) => {
|
|
73
|
+
function truncate(s, max) {
|
|
74
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
75
|
+
}
|
|
76
|
+
const outerHtml = truncate(el.outerHTML, 2048);
|
|
77
|
+
const ancestorChain = [];
|
|
78
|
+
let cur = el.parentElement;
|
|
79
|
+
let depth = 0;
|
|
80
|
+
while (cur && depth < 8) {
|
|
81
|
+
const role = cur.getAttribute("role");
|
|
82
|
+
const tag = cur.tagName.toLowerCase();
|
|
83
|
+
ancestorChain.push(role ? `${tag}[role='${role}']` : tag);
|
|
84
|
+
if (tag === "body") break;
|
|
85
|
+
cur = cur.parentElement;
|
|
86
|
+
depth++;
|
|
87
|
+
}
|
|
88
|
+
let portalEl = null;
|
|
89
|
+
const ctrl = el.getAttribute("aria-controls");
|
|
90
|
+
if (ctrl) {
|
|
91
|
+
const t = document.getElementById(ctrl);
|
|
92
|
+
if (t && !el.contains(t)) portalEl = t;
|
|
93
|
+
}
|
|
94
|
+
if (!portalEl) {
|
|
95
|
+
let found = 0;
|
|
96
|
+
const walker = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_ELEMENT);
|
|
97
|
+
let cur2;
|
|
98
|
+
while (cur2 = walker.nextNode()) {
|
|
99
|
+
const desc = cur2;
|
|
100
|
+
if (el.contains(desc)) continue;
|
|
101
|
+
for (const attr of Array.from(desc.attributes)) {
|
|
102
|
+
if (attr.name.startsWith("data-radix-") || attr.name.startsWith("data-headlessui-")) {
|
|
103
|
+
portalEl = desc;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (portalEl) break;
|
|
108
|
+
if (++found >= 20) break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const portal = portalEl ? {
|
|
112
|
+
selector: portalEl.tagName.toLowerCase() + (portalEl.id ? `#${portalEl.id}` : ""),
|
|
113
|
+
outerHtml: truncate(portalEl.outerHTML, 2048)
|
|
114
|
+
} : void 0;
|
|
115
|
+
return { outerHtml, ancestorChain, portal };
|
|
116
|
+
});
|
|
117
|
+
ariaRaw = await locator.ariaSnapshot();
|
|
118
|
+
} catch (e) {
|
|
119
|
+
response.addTextResult(JSON.stringify({
|
|
120
|
+
status: "error",
|
|
121
|
+
reason: "context_gather_failed",
|
|
122
|
+
message: e.message
|
|
123
|
+
}, null, 2));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const ariaSnapshot = ariaRaw.length <= 1024 ? ariaRaw : ariaRaw.slice(0, 1023) + "\u2026";
|
|
127
|
+
const result = {
|
|
128
|
+
status: "needs_inference",
|
|
129
|
+
fingerprint,
|
|
130
|
+
ref,
|
|
131
|
+
element: { outerHtml: gather.outerHtml, ancestorChain: gather.ancestorChain },
|
|
132
|
+
ariaSnapshot,
|
|
133
|
+
...gather.portal ? { portalCandidate: gather.portal } : {}
|
|
134
|
+
};
|
|
135
|
+
response.addTextResult(JSON.stringify(result, null, 2));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const widgetContractCache = (0, import_tool.defineTabTool)({
|
|
139
|
+
capability: "core",
|
|
140
|
+
schema: {
|
|
141
|
+
name: "browser_widget_contract_cache",
|
|
142
|
+
title: "Cache an inferred widget contract",
|
|
143
|
+
description: [
|
|
144
|
+
"Writes an inferred widget contract into the session's cache.",
|
|
145
|
+
"",
|
|
146
|
+
"Refuses to overwrite curated entries. Mutable for inferred entries \u2014 resubmit to",
|
|
147
|
+
'correct a bad inference. Submit a contract with `source: "unknown"` and empty steps',
|
|
148
|
+
"to mark the fingerprint as unresolvable for the session.",
|
|
149
|
+
"",
|
|
150
|
+
"Execute the contract yourself after a successful write; the tool does not return it back."
|
|
151
|
+
].join("\n"),
|
|
152
|
+
inputSchema: import_mcpBundle.z.object({
|
|
153
|
+
fingerprint: import_mcpBundle.z.string().regex(/^[0-9a-f]{16}$/).describe("16-hex fingerprint of the widget being cached \u2014 must match the fingerprint that browser_widget_contract_lookup returned in needs_inference status (or that you read from a PageBlueprint element's fingerprint field)."),
|
|
154
|
+
// InferredWidgetContractSchema (not WidgetContractSchema): narrows
|
|
155
|
+
// `source` to {'inferred','unknown'}. Curated contracts come only
|
|
156
|
+
// from the curated library — agents must not be able to mislabel an
|
|
157
|
+
// inferred entry as authoritative via this tool surface.
|
|
158
|
+
contract: import_widgetContract.InferredWidgetContractSchema.describe(`The widget's interaction contract. Set source to "inferred" for a real contract you authored, or "unknown" with empty steps to mark the widget as unresolvable for this session. Curated contracts come from the library \u2014 you cannot submit source: "curated".`)
|
|
159
|
+
}),
|
|
160
|
+
type: "readOnly"
|
|
161
|
+
},
|
|
162
|
+
handle: async (tab, params, response) => {
|
|
163
|
+
const { fingerprint, contract } = params;
|
|
164
|
+
const result = (0, import_widgetContract.cacheInferredContract)(fingerprint, contract, tab.context.inferredContracts);
|
|
165
|
+
response.addTextResult(JSON.stringify(result));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
var widgetContract_default = [widgetContractLookup, widgetContractCache];
|
|
@@ -44,16 +44,22 @@ var import_mouse = __toESM(require("./tools/mouse"));
|
|
|
44
44
|
var import_navigate = __toESM(require("./tools/navigate"));
|
|
45
45
|
var import_network = __toESM(require("./tools/network"));
|
|
46
46
|
var import_open = __toESM(require("./tools/open"));
|
|
47
|
+
var import_pageBlueprint = __toESM(require("./tools/pageBlueprint"));
|
|
47
48
|
var import_pdf = __toESM(require("./tools/pdf"));
|
|
48
49
|
var import_runCode = __toESM(require("./tools/runCode"));
|
|
50
|
+
var import_sitemap = __toESM(require("./tools/sitemap"));
|
|
49
51
|
var import_snapshot = __toESM(require("./tools/snapshot"));
|
|
50
52
|
var import_screenshot = __toESM(require("./tools/screenshot"));
|
|
51
53
|
var import_tabs = __toESM(require("./tools/tabs"));
|
|
52
54
|
var import_tracing = __toESM(require("./tools/tracing"));
|
|
53
55
|
var import_wait = __toESM(require("./tools/wait"));
|
|
54
56
|
var import_verify = __toESM(require("./tools/verify"));
|
|
57
|
+
var import_widgetContract = __toESM(require("./tools/widgetContract"));
|
|
55
58
|
const browserTools = [
|
|
56
59
|
...import_common.default,
|
|
60
|
+
...import_sitemap.default,
|
|
61
|
+
...import_pageBlueprint.default,
|
|
62
|
+
...import_widgetContract.default,
|
|
57
63
|
...import_console.default,
|
|
58
64
|
...import_dialogs.default,
|
|
59
65
|
...import_evaluate.default,
|
|
@@ -66,11 +66,22 @@ class TraceRecordingBackend {
|
|
|
66
66
|
return this._trackedActions;
|
|
67
67
|
}
|
|
68
68
|
async initialize(clientInfo) {
|
|
69
|
-
const
|
|
69
|
+
const userDataDir = this._options.userDataDir || process.env.PLAYWRIGHT_USER_DATA_DIR || void 0;
|
|
70
|
+
const loadExtension = this._options.loadExtension ?? splitExtensionPaths(process.env.PLAYWRIGHT_LOAD_EXTENSION);
|
|
71
|
+
const persistent = !!userDataDir || !!loadExtension?.length;
|
|
72
|
+
const extensionArgs = loadExtension?.length ? [
|
|
73
|
+
`--disable-extensions-except=${loadExtension.join(",")}`,
|
|
74
|
+
`--load-extension=${loadExtension.join(",")}`
|
|
75
|
+
] : void 0;
|
|
76
|
+
const headless = loadExtension?.length ? false : this._options.headless ?? (!!process.env.CI || import_os.default.platform() === "linux" && !process.env.DISPLAY);
|
|
70
77
|
const config = await (0, import_config.resolveConfig)({
|
|
71
78
|
browser: {
|
|
72
|
-
isolated:
|
|
73
|
-
|
|
79
|
+
isolated: !persistent,
|
|
80
|
+
...persistent && userDataDir ? { userDataDir } : {},
|
|
81
|
+
launchOptions: {
|
|
82
|
+
headless,
|
|
83
|
+
...extensionArgs ? { args: extensionArgs } : {}
|
|
84
|
+
},
|
|
74
85
|
contextOptions: {
|
|
75
86
|
viewport: { width: 1280, height: 900 },
|
|
76
87
|
recordHar: { path: this._harPath, mode: "minimal" },
|
|
@@ -88,11 +99,18 @@ class TraceRecordingBackend {
|
|
|
88
99
|
navigation: 6e4
|
|
89
100
|
}
|
|
90
101
|
});
|
|
102
|
+
if (loadExtension?.length && config.browser.launchOptions?.channel === "chrome")
|
|
103
|
+
delete config.browser.launchOptions.channel;
|
|
91
104
|
const factory = (0, import_browserContextFactory.contextFactory)(config);
|
|
92
105
|
this._browserBackend = new import_browserServerBackend.BrowserServerBackend(config, factory);
|
|
93
106
|
await this._browserBackend.initialize(clientInfo);
|
|
94
107
|
this._initialized = true;
|
|
95
|
-
traceDebug("TraceRecordingBackend initialized"
|
|
108
|
+
traceDebug("TraceRecordingBackend initialized", {
|
|
109
|
+
persistent,
|
|
110
|
+
userDataDir,
|
|
111
|
+
loadExtension,
|
|
112
|
+
channel: config.browser.launchOptions?.channel
|
|
113
|
+
});
|
|
96
114
|
this._browserBackend.context.onBrowserContextCreated = (browserContext) => this._installPopupListener(browserContext);
|
|
97
115
|
}
|
|
98
116
|
async listTools() {
|
|
@@ -130,8 +148,10 @@ class TraceRecordingBackend {
|
|
|
130
148
|
}
|
|
131
149
|
if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
|
|
132
150
|
const index = args?.index ?? 0;
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
const targetUrl = this._browserBackend.context?.tabs()[index]?.page.url();
|
|
152
|
+
const urlAlias = targetUrl ? aliasForUrl(targetUrl) : null;
|
|
153
|
+
this._currentPageAlias = urlAlias ?? (index === 0 ? "page" : `page${index}`);
|
|
154
|
+
traceDebug(`Tab switched to index ${index} (url=${targetUrl}) \u2192 pageAlias: ${this._currentPageAlias}`);
|
|
135
155
|
const tabResult = await this._browserBackend.callTool(name, args);
|
|
136
156
|
return tabResult;
|
|
137
157
|
}
|
|
@@ -731,7 +751,7 @@ ${details}` }]
|
|
|
731
751
|
*/
|
|
732
752
|
_installPopupListener(browserContext) {
|
|
733
753
|
let initialPageSeen = false;
|
|
734
|
-
browserContext.on("page", () => {
|
|
754
|
+
browserContext.on("page", (newPage) => {
|
|
735
755
|
if (!initialPageSeen) {
|
|
736
756
|
initialPageSeen = true;
|
|
737
757
|
return;
|
|
@@ -745,16 +765,23 @@ ${details}` }]
|
|
|
745
765
|
this._currentPageAlias = popupAlias;
|
|
746
766
|
this._pendingPopupAlias = popupAlias;
|
|
747
767
|
traceDebug(`Popup page opened: ${popupAlias} (pending stamp)`);
|
|
768
|
+
void newPage.waitForLoadState("domcontentloaded").then(() => {
|
|
769
|
+
const settled = newPage.url();
|
|
770
|
+
const semantic = aliasForUrl(settled);
|
|
771
|
+
if (!semantic)
|
|
772
|
+
return;
|
|
773
|
+
if (this._currentPageAlias === popupAlias)
|
|
774
|
+
this._currentPageAlias = semantic;
|
|
775
|
+
if (this._pendingPopupAlias === popupAlias)
|
|
776
|
+
this._pendingPopupAlias = semantic;
|
|
777
|
+
traceDebug(`Popup alias upgraded: ${popupAlias} \u2192 ${semantic} (url=${settled})`);
|
|
778
|
+
}).catch(() => {
|
|
779
|
+
});
|
|
748
780
|
});
|
|
749
781
|
}
|
|
750
782
|
_maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
|
|
751
783
|
if (result.isError)
|
|
752
784
|
return;
|
|
753
|
-
if (toolName === "browser_press_key") {
|
|
754
|
-
const key = String(args.key || "").toLowerCase();
|
|
755
|
-
if (key.includes("control+") || key.includes("meta+") || key === "tab" || key === "enter" || key === "escape")
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
785
|
const parsed = (0, import_response.parseResponse)(result);
|
|
759
786
|
const code = parsed?.code ?? "";
|
|
760
787
|
if (code || import_types.ARGS_ONLY_TOOLS.has(toolName)) {
|
|
@@ -811,6 +838,19 @@ ${details}` }]
|
|
|
811
838
|
this._cleanupTempDir(false);
|
|
812
839
|
}
|
|
813
840
|
}
|
|
841
|
+
function splitExtensionPaths(value) {
|
|
842
|
+
if (!value)
|
|
843
|
+
return void 0;
|
|
844
|
+
const parts = value.split(/[,;]/).map((p) => p.trim()).filter(Boolean);
|
|
845
|
+
return parts.length ? parts : void 0;
|
|
846
|
+
}
|
|
847
|
+
function aliasForUrl(url) {
|
|
848
|
+
if (!url)
|
|
849
|
+
return null;
|
|
850
|
+
if (/^chrome-extension:\/\/[a-p]{32}\/popup\//.test(url))
|
|
851
|
+
return "popupPage";
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
814
854
|
// Annotate the CommonJS export names for ESM import in node:
|
|
815
855
|
0 && (module.exports = {
|
|
816
856
|
TraceRecordingBackend
|