@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,30 @@
|
|
|
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 slug_exports = {};
|
|
20
|
+
__export(slug_exports, {
|
|
21
|
+
slug: () => slug
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(slug_exports);
|
|
24
|
+
function slug(input) {
|
|
25
|
+
return input.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "_").replace(/^_+|_+$/g, "");
|
|
26
|
+
}
|
|
27
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
28
|
+
0 && (module.exports = {
|
|
29
|
+
slug
|
|
30
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_slug = require("./slug");
|
|
3
|
+
const cases = [];
|
|
4
|
+
function test(name, run) {
|
|
5
|
+
cases.push({ name, run });
|
|
6
|
+
}
|
|
7
|
+
function assertEqual(actual, expected, msg) {
|
|
8
|
+
const a = JSON.stringify(actual);
|
|
9
|
+
const e = JSON.stringify(expected);
|
|
10
|
+
if (a !== e) throw new Error(`${msg ?? "assertEqual"} \u2014 expected ${e}, got ${a}`);
|
|
11
|
+
}
|
|
12
|
+
test("slug: lowercases and replaces spaces with underscores", () => {
|
|
13
|
+
assertEqual((0, import_slug.slug)("Add Product"), "add_product");
|
|
14
|
+
});
|
|
15
|
+
test("slug: replaces punctuation with underscores", () => {
|
|
16
|
+
assertEqual((0, import_slug.slug)("Next: Select Country"), "next_select_country");
|
|
17
|
+
});
|
|
18
|
+
test("slug: collapses runs of separators", () => {
|
|
19
|
+
assertEqual((0, import_slug.slug)("Foo --- Bar"), "foo_bar");
|
|
20
|
+
});
|
|
21
|
+
test("slug: strips leading and trailing underscores", () => {
|
|
22
|
+
assertEqual((0, import_slug.slug)(" Hello "), "hello");
|
|
23
|
+
assertEqual((0, import_slug.slug)("_leading"), "leading");
|
|
24
|
+
assertEqual((0, import_slug.slug)("trailing_"), "trailing");
|
|
25
|
+
});
|
|
26
|
+
test("slug: empty input returns empty string", () => {
|
|
27
|
+
assertEqual((0, import_slug.slug)(""), "");
|
|
28
|
+
});
|
|
29
|
+
test("slug: all-non-alphanumeric input returns empty string", () => {
|
|
30
|
+
assertEqual((0, import_slug.slug)("---"), "");
|
|
31
|
+
assertEqual((0, import_slug.slug)(" "), "");
|
|
32
|
+
});
|
|
33
|
+
test("slug: preserves digits", () => {
|
|
34
|
+
assertEqual((0, import_slug.slug)("Order 12"), "order_12");
|
|
35
|
+
});
|
|
36
|
+
test("slug: is idempotent on already-slug-like input", () => {
|
|
37
|
+
assertEqual((0, import_slug.slug)("already_slug"), "already_slug");
|
|
38
|
+
});
|
|
39
|
+
test("slug: preserves CJK (Japanese)", () => {
|
|
40
|
+
assertEqual((0, import_slug.slug)("\u8CC7\u7523\u7BA1\u7406"), "\u8CC7\u7523\u7BA1\u7406");
|
|
41
|
+
assertEqual((0, import_slug.slug)("\u8CC7\u7523\u60C5\u5831"), "\u8CC7\u7523\u60C5\u5831");
|
|
42
|
+
assertEqual((0, import_slug.slug)("\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9"), "\u4E00\u62EC\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9");
|
|
43
|
+
});
|
|
44
|
+
test("slug: preserves Cyrillic and lowercases it", () => {
|
|
45
|
+
assertEqual((0, import_slug.slug)("\u0420\u0443\u0441\u0441\u043A\u0438\u0439"), "\u0440\u0443\u0441\u0441\u043A\u0438\u0439");
|
|
46
|
+
});
|
|
47
|
+
test("slug: preserves Arabic", () => {
|
|
48
|
+
assertEqual((0, import_slug.slug)("\u0627\u0644\u0639\u0631\u0628\u064A\u0629"), "\u0627\u0644\u0639\u0631\u0628\u064A\u0629");
|
|
49
|
+
});
|
|
50
|
+
test("slug: preserves Korean", () => {
|
|
51
|
+
assertEqual((0, import_slug.slug)("\uD55C\uAD6D\uC5B4"), "\uD55C\uAD6D\uC5B4");
|
|
52
|
+
});
|
|
53
|
+
test("slug: preserves accented Latin", () => {
|
|
54
|
+
assertEqual((0, import_slug.slug)("r\xE9sum\xE9 caf\xE9"), "r\xE9sum\xE9_caf\xE9");
|
|
55
|
+
});
|
|
56
|
+
test("slug: mixed CJK + ASCII digits and whitespace", () => {
|
|
57
|
+
assertEqual((0, import_slug.slug)("\u901A\u77E5 (1063)"), "\u901A\u77E5_1063");
|
|
58
|
+
});
|
|
59
|
+
test("slug: emoji and symbols are still separators", () => {
|
|
60
|
+
assertEqual((0, import_slug.slug)("\u{1F600} emoji test"), "emoji_test");
|
|
61
|
+
assertEqual((0, import_slug.slug)("foo \u2192 bar"), "foo_bar");
|
|
62
|
+
});
|
|
63
|
+
test("slug: output cannot contain the :: separator used by collision groups", () => {
|
|
64
|
+
assertEqual((0, import_slug.slug)("a::b"), "a_b");
|
|
65
|
+
assertEqual((0, import_slug.slug)("foo:bar:baz"), "foo_bar_baz");
|
|
66
|
+
});
|
|
67
|
+
let failed = 0;
|
|
68
|
+
for (const { name, run } of cases) {
|
|
69
|
+
try {
|
|
70
|
+
run();
|
|
71
|
+
console.log(" \u2713", name);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
failed++;
|
|
74
|
+
console.log(" \u2717", name);
|
|
75
|
+
console.log(" ", e.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (failed > 0) {
|
|
79
|
+
console.log(`
|
|
80
|
+
${failed}/${cases.length} failed`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
console.log(`
|
|
84
|
+
${cases.length} passed`);
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
InferredWidgetContractSchema: () => InferredWidgetContractSchema,
|
|
22
|
+
WidgetContractSchema: () => WidgetContractSchema,
|
|
23
|
+
cacheInferredContract: () => cacheInferredContract,
|
|
24
|
+
lookupContract: () => lookupContract,
|
|
25
|
+
validateContract: () => validateContract
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(widgetContract_exports);
|
|
28
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
29
|
+
var import_curatedWidgets = require("./curatedWidgets");
|
|
30
|
+
const ClickStepSchema = import_mcpBundle.z.object({
|
|
31
|
+
kind: import_mcpBundle.z.literal("click"),
|
|
32
|
+
target: import_mcpBundle.z.enum(["trigger", "option"]),
|
|
33
|
+
matchBy: import_mcpBundle.z.enum(["accessibleName", "role"]).optional(),
|
|
34
|
+
param: import_mcpBundle.z.string().optional()
|
|
35
|
+
});
|
|
36
|
+
const FillStepSchema = import_mcpBundle.z.discriminatedUnion("target", [
|
|
37
|
+
import_mcpBundle.z.object({ kind: import_mcpBundle.z.literal("fill"), target: import_mcpBundle.z.literal("activeInput"), param: import_mcpBundle.z.string() }),
|
|
38
|
+
import_mcpBundle.z.object({ kind: import_mcpBundle.z.literal("fill"), target: import_mcpBundle.z.literal("inputByLabel"), label: import_mcpBundle.z.string(), param: import_mcpBundle.z.string() })
|
|
39
|
+
]);
|
|
40
|
+
const WaitForStepSchema = import_mcpBundle.z.object({
|
|
41
|
+
kind: import_mcpBundle.z.literal("waitFor"),
|
|
42
|
+
location: import_mcpBundle.z.enum(["inDom", "portal", "frame"]),
|
|
43
|
+
signal: import_mcpBundle.z.enum(["visible", "present"])
|
|
44
|
+
});
|
|
45
|
+
const SelectOptionStepSchema = import_mcpBundle.z.object({
|
|
46
|
+
kind: import_mcpBundle.z.literal("selectOption"),
|
|
47
|
+
by: import_mcpBundle.z.enum(["accessibleName", "index"]),
|
|
48
|
+
param: import_mcpBundle.z.string()
|
|
49
|
+
});
|
|
50
|
+
const EnterFrameStepSchema = import_mcpBundle.z.object({
|
|
51
|
+
kind: import_mcpBundle.z.literal("enterFrame"),
|
|
52
|
+
framePath: import_mcpBundle.z.array(import_mcpBundle.z.string())
|
|
53
|
+
});
|
|
54
|
+
const ExitFrameStepSchema = import_mcpBundle.z.object({ kind: import_mcpBundle.z.literal("exitFrame") });
|
|
55
|
+
const HandoffStepSchema = import_mcpBundle.z.object({ kind: import_mcpBundle.z.literal("handoff") });
|
|
56
|
+
const ContractStepSchema = import_mcpBundle.z.union([
|
|
57
|
+
ClickStepSchema,
|
|
58
|
+
FillStepSchema,
|
|
59
|
+
WaitForStepSchema,
|
|
60
|
+
SelectOptionStepSchema,
|
|
61
|
+
EnterFrameStepSchema,
|
|
62
|
+
ExitFrameStepSchema,
|
|
63
|
+
HandoffStepSchema
|
|
64
|
+
]);
|
|
65
|
+
const WidgetContractSchema = import_mcpBundle.z.object({
|
|
66
|
+
source: import_mcpBundle.z.enum(["curated", "inferred", "unknown"]),
|
|
67
|
+
confidence: import_mcpBundle.z.enum(["high", "medium", "low"]),
|
|
68
|
+
widgetIdentification: import_mcpBundle.z.string().max(100),
|
|
69
|
+
parameters: import_mcpBundle.z.array(import_mcpBundle.z.string()),
|
|
70
|
+
steps: import_mcpBundle.z.array(ContractStepSchema)
|
|
71
|
+
});
|
|
72
|
+
const InferredWidgetContractSchema = import_mcpBundle.z.object({
|
|
73
|
+
source: import_mcpBundle.z.enum(["inferred", "unknown"]),
|
|
74
|
+
confidence: import_mcpBundle.z.enum(["high", "medium", "low"]),
|
|
75
|
+
widgetIdentification: import_mcpBundle.z.string().max(100),
|
|
76
|
+
parameters: import_mcpBundle.z.array(import_mcpBundle.z.string()),
|
|
77
|
+
steps: import_mcpBundle.z.array(ContractStepSchema)
|
|
78
|
+
});
|
|
79
|
+
function validateContract(contract) {
|
|
80
|
+
const params = new Set(contract.parameters);
|
|
81
|
+
let enterFrameCount = 0;
|
|
82
|
+
let sawWaitFor = false;
|
|
83
|
+
for (const step of contract.steps) {
|
|
84
|
+
const stepParam = step.param;
|
|
85
|
+
if (stepParam !== void 0 && !params.has(stepParam))
|
|
86
|
+
throw new Error(`step param '${stepParam}' not declared in parameters[]`);
|
|
87
|
+
if (step.kind === "enterFrame") {
|
|
88
|
+
if (step.framePath.length === 0)
|
|
89
|
+
throw new Error("EnterFrameStep.framePath must be non-empty");
|
|
90
|
+
enterFrameCount++;
|
|
91
|
+
if (enterFrameCount > 1)
|
|
92
|
+
throw new Error("at most one EnterFrameStep per contract (provisional limit)");
|
|
93
|
+
}
|
|
94
|
+
if (step.kind === "waitFor") sawWaitFor = true;
|
|
95
|
+
if (step.kind === "handoff" && !sawWaitFor) {
|
|
96
|
+
console.warn("widgetContract: HandoffStep not preceded by WaitForStep \u2014 agent may act on pre-widget-open DOM state");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function lookupContract(fingerprint, inferredCache) {
|
|
101
|
+
const curated = import_curatedWidgets.CURATED[fingerprint];
|
|
102
|
+
if (curated) return { status: "found", contract: curated };
|
|
103
|
+
const inferred = inferredCache.get(fingerprint);
|
|
104
|
+
if (inferred) return { status: "found", contract: inferred };
|
|
105
|
+
return { status: "needs_inference_marker" };
|
|
106
|
+
}
|
|
107
|
+
function cacheInferredContract(fingerprint, contract, inferredCache) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(import_curatedWidgets.CURATED, fingerprint))
|
|
109
|
+
return { ok: false, error: "fingerprint collides with curated entry; cannot overwrite" };
|
|
110
|
+
if (contract.source === "curated")
|
|
111
|
+
return { ok: false, error: "source: 'curated' is reserved for the curated library; use 'inferred' or 'unknown'" };
|
|
112
|
+
try {
|
|
113
|
+
validateContract(contract);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return { ok: false, error: e.message };
|
|
116
|
+
}
|
|
117
|
+
inferredCache.set(fingerprint, contract);
|
|
118
|
+
return { ok: true };
|
|
119
|
+
}
|
|
120
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
121
|
+
0 && (module.exports = {
|
|
122
|
+
InferredWidgetContractSchema,
|
|
123
|
+
WidgetContractSchema,
|
|
124
|
+
cacheInferredContract,
|
|
125
|
+
lookupContract,
|
|
126
|
+
validateContract
|
|
127
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_widgetContract = require("./widgetContract");
|
|
3
|
+
var import_curatedWidgets = require("./curatedWidgets");
|
|
4
|
+
const cases = [];
|
|
5
|
+
function test(name, run) {
|
|
6
|
+
cases.push({ name, run });
|
|
7
|
+
}
|
|
8
|
+
function assertEqual(actual, expected, msg) {
|
|
9
|
+
const a = JSON.stringify(actual);
|
|
10
|
+
const e = JSON.stringify(expected);
|
|
11
|
+
if (a !== e)
|
|
12
|
+
throw new Error(`${msg ?? "assertEqual"} \u2014 expected ${e}, got ${a}`);
|
|
13
|
+
}
|
|
14
|
+
function assertThrows(fn, substring) {
|
|
15
|
+
let threw = false;
|
|
16
|
+
let message = "";
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
} catch (e) {
|
|
20
|
+
threw = true;
|
|
21
|
+
message = e.message;
|
|
22
|
+
}
|
|
23
|
+
if (!threw) throw new Error("expected function to throw");
|
|
24
|
+
if (substring && !message.includes(substring))
|
|
25
|
+
throw new Error(`thrown message "${message}" does not include "${substring}"`);
|
|
26
|
+
}
|
|
27
|
+
const curatedFingerprints = Object.keys(import_curatedWidgets.CURATED);
|
|
28
|
+
const firstCurated = curatedFingerprints[0];
|
|
29
|
+
test("lookupContract: curated takes precedence over inferred", () => {
|
|
30
|
+
const cache = /* @__PURE__ */ new Map();
|
|
31
|
+
cache.set(firstCurated, {
|
|
32
|
+
source: "inferred",
|
|
33
|
+
confidence: "low",
|
|
34
|
+
widgetIdentification: "cache-shadow attempt",
|
|
35
|
+
parameters: [],
|
|
36
|
+
steps: []
|
|
37
|
+
});
|
|
38
|
+
const result = (0, import_widgetContract.lookupContract)(firstCurated, cache);
|
|
39
|
+
if (result.status !== "found") throw new Error("expected found");
|
|
40
|
+
assertEqual(result.contract.source, "curated");
|
|
41
|
+
});
|
|
42
|
+
test("lookupContract: inferred returned when no curated entry exists", () => {
|
|
43
|
+
const cache = /* @__PURE__ */ new Map();
|
|
44
|
+
const inferred = {
|
|
45
|
+
source: "inferred",
|
|
46
|
+
confidence: "high",
|
|
47
|
+
widgetIdentification: "CustomDatePicker",
|
|
48
|
+
parameters: ["date"],
|
|
49
|
+
steps: [{ kind: "click", target: "trigger" }]
|
|
50
|
+
};
|
|
51
|
+
cache.set("0000000000000001", inferred);
|
|
52
|
+
const result = (0, import_widgetContract.lookupContract)("0000000000000001", cache);
|
|
53
|
+
if (result.status !== "found") throw new Error("expected found");
|
|
54
|
+
assertEqual(result.contract.source, "inferred");
|
|
55
|
+
});
|
|
56
|
+
test("lookupContract: unknown fingerprint returns needs_inference_marker", () => {
|
|
57
|
+
const cache = /* @__PURE__ */ new Map();
|
|
58
|
+
const result = (0, import_widgetContract.lookupContract)("ffffffffffffffff", cache);
|
|
59
|
+
assertEqual(result.status, "needs_inference_marker");
|
|
60
|
+
});
|
|
61
|
+
test("cacheInferredContract: refuses to overwrite curated entry", () => {
|
|
62
|
+
const cache = /* @__PURE__ */ new Map();
|
|
63
|
+
const result = (0, import_widgetContract.cacheInferredContract)(firstCurated, {
|
|
64
|
+
source: "inferred",
|
|
65
|
+
confidence: "high",
|
|
66
|
+
widgetIdentification: "X",
|
|
67
|
+
parameters: [],
|
|
68
|
+
steps: []
|
|
69
|
+
}, cache);
|
|
70
|
+
assertEqual(result, { ok: false, error: "fingerprint collides with curated entry; cannot overwrite" });
|
|
71
|
+
});
|
|
72
|
+
test("cacheInferredContract: accepts valid inferred contract", () => {
|
|
73
|
+
const cache = /* @__PURE__ */ new Map();
|
|
74
|
+
const result = (0, import_widgetContract.cacheInferredContract)("0000000000000002", {
|
|
75
|
+
source: "inferred",
|
|
76
|
+
confidence: "high",
|
|
77
|
+
widgetIdentification: "X",
|
|
78
|
+
parameters: ["v"],
|
|
79
|
+
steps: [{ kind: "fill", target: "inputByLabel", label: "Name", param: "v" }]
|
|
80
|
+
}, cache);
|
|
81
|
+
assertEqual(result, { ok: true });
|
|
82
|
+
assertEqual(cache.size, 1);
|
|
83
|
+
});
|
|
84
|
+
test("cacheInferredContract: overwrites previous inferred contract (mutable)", () => {
|
|
85
|
+
const cache = /* @__PURE__ */ new Map();
|
|
86
|
+
(0, import_widgetContract.cacheInferredContract)("0000000000000003", {
|
|
87
|
+
source: "inferred",
|
|
88
|
+
confidence: "low",
|
|
89
|
+
widgetIdentification: "bad",
|
|
90
|
+
parameters: [],
|
|
91
|
+
steps: []
|
|
92
|
+
}, cache);
|
|
93
|
+
(0, import_widgetContract.cacheInferredContract)("0000000000000003", {
|
|
94
|
+
source: "inferred",
|
|
95
|
+
confidence: "high",
|
|
96
|
+
widgetIdentification: "good",
|
|
97
|
+
parameters: [],
|
|
98
|
+
steps: []
|
|
99
|
+
}, cache);
|
|
100
|
+
assertEqual(cache.get("0000000000000003").widgetIdentification, "good");
|
|
101
|
+
});
|
|
102
|
+
test("cacheInferredContract: accepts unknown stub", () => {
|
|
103
|
+
const cache = /* @__PURE__ */ new Map();
|
|
104
|
+
const result = (0, import_widgetContract.cacheInferredContract)("0000000000000004", {
|
|
105
|
+
source: "unknown",
|
|
106
|
+
confidence: "low",
|
|
107
|
+
widgetIdentification: "",
|
|
108
|
+
parameters: [],
|
|
109
|
+
steps: []
|
|
110
|
+
}, cache);
|
|
111
|
+
assertEqual(result, { ok: true });
|
|
112
|
+
});
|
|
113
|
+
test("cacheInferredContract: rejects source=curated (defense in depth)", () => {
|
|
114
|
+
const cache = /* @__PURE__ */ new Map();
|
|
115
|
+
const result = (0, import_widgetContract.cacheInferredContract)("0000000000000005", {
|
|
116
|
+
source: "curated",
|
|
117
|
+
confidence: "high",
|
|
118
|
+
widgetIdentification: "forged",
|
|
119
|
+
parameters: [],
|
|
120
|
+
steps: []
|
|
121
|
+
}, cache);
|
|
122
|
+
if (result.ok)
|
|
123
|
+
throw new Error("expected reject when source=curated");
|
|
124
|
+
if (!result.error.includes("source: 'curated'"))
|
|
125
|
+
throw new Error(`expected error message to mention curated, got: ${result.error}`);
|
|
126
|
+
assertEqual(cache.size, 0);
|
|
127
|
+
});
|
|
128
|
+
test("validateContract: step.param must exist in parameters[]", () => {
|
|
129
|
+
const c = {
|
|
130
|
+
source: "inferred",
|
|
131
|
+
confidence: "high",
|
|
132
|
+
widgetIdentification: "X",
|
|
133
|
+
parameters: ["v"],
|
|
134
|
+
steps: [{ kind: "click", target: "option", matchBy: "accessibleName", param: "w" }]
|
|
135
|
+
};
|
|
136
|
+
assertThrows(() => (0, import_widgetContract.validateContract)(c), "param 'w'");
|
|
137
|
+
});
|
|
138
|
+
test("validateContract: EnterFrameStep.framePath must be non-empty", () => {
|
|
139
|
+
const c = {
|
|
140
|
+
source: "inferred",
|
|
141
|
+
confidence: "high",
|
|
142
|
+
widgetIdentification: "X",
|
|
143
|
+
parameters: [],
|
|
144
|
+
steps: [{ kind: "enterFrame", framePath: [] }, { kind: "exitFrame" }]
|
|
145
|
+
};
|
|
146
|
+
assertThrows(() => (0, import_widgetContract.validateContract)(c), "framePath");
|
|
147
|
+
});
|
|
148
|
+
test("validateContract: rule 1 (missing param) is reported before rule 2 (empty framePath)", () => {
|
|
149
|
+
const c = {
|
|
150
|
+
source: "inferred",
|
|
151
|
+
confidence: "high",
|
|
152
|
+
widgetIdentification: "X",
|
|
153
|
+
parameters: [],
|
|
154
|
+
steps: [
|
|
155
|
+
{ kind: "click", target: "option", matchBy: "accessibleName", param: "v" },
|
|
156
|
+
{ kind: "enterFrame", framePath: [] }
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
assertThrows(() => (0, import_widgetContract.validateContract)(c), "param 'v'");
|
|
160
|
+
});
|
|
161
|
+
test("validateContract: at most one EnterFrameStep", () => {
|
|
162
|
+
const c = {
|
|
163
|
+
source: "inferred",
|
|
164
|
+
confidence: "high",
|
|
165
|
+
widgetIdentification: "X",
|
|
166
|
+
parameters: [],
|
|
167
|
+
steps: [
|
|
168
|
+
{ kind: "enterFrame", framePath: ["iframe"] },
|
|
169
|
+
{ kind: "exitFrame" },
|
|
170
|
+
{ kind: "enterFrame", framePath: ["iframe"] }
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
assertThrows(() => (0, import_widgetContract.validateContract)(c), "EnterFrameStep");
|
|
174
|
+
});
|
|
175
|
+
test("validateContract: HandoffStep-before-WaitForStep is a warning, not an error", () => {
|
|
176
|
+
const c = {
|
|
177
|
+
source: "inferred",
|
|
178
|
+
confidence: "high",
|
|
179
|
+
widgetIdentification: "X",
|
|
180
|
+
parameters: [],
|
|
181
|
+
steps: [{ kind: "click", target: "trigger" }, { kind: "handoff" }]
|
|
182
|
+
};
|
|
183
|
+
(0, import_widgetContract.validateContract)(c);
|
|
184
|
+
});
|
|
185
|
+
test("validateContract: widgetIdentification length is NOT enforced here (Zod schema concern)", () => {
|
|
186
|
+
const c = {
|
|
187
|
+
source: "inferred",
|
|
188
|
+
confidence: "high",
|
|
189
|
+
widgetIdentification: "a".repeat(200),
|
|
190
|
+
parameters: [],
|
|
191
|
+
steps: []
|
|
192
|
+
};
|
|
193
|
+
(0, import_widgetContract.validateContract)(c);
|
|
194
|
+
});
|
|
195
|
+
let failed = 0;
|
|
196
|
+
for (const { name, run } of cases) {
|
|
197
|
+
try {
|
|
198
|
+
run();
|
|
199
|
+
console.log(" \u2713", name);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
failed++;
|
|
202
|
+
console.log(" \u2717", name);
|
|
203
|
+
console.log(" ", e.message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (failed > 0) {
|
|
207
|
+
console.log(`
|
|
208
|
+
${failed}/${cases.length} failed`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
console.log(`
|
|
212
|
+
${cases.length} passed`);
|
|
@@ -184,6 +184,7 @@ class PersistentContextFactory {
|
|
|
184
184
|
(0, import_log.testDebug)("lock user data dir", userDataDir);
|
|
185
185
|
const browserType = playwright[this.config.browser.browserName];
|
|
186
186
|
for (let i = 0; i < 5; i++) {
|
|
187
|
+
const hasLoadExtension = this.config.browser.launchOptions?.args?.some((arg) => arg.startsWith("--load-extension=")) ?? false;
|
|
187
188
|
const launchOptions = {
|
|
188
189
|
tracesDir,
|
|
189
190
|
...this.config.browser.launchOptions,
|
|
@@ -191,7 +192,8 @@ class PersistentContextFactory {
|
|
|
191
192
|
handleSIGINT: false,
|
|
192
193
|
handleSIGTERM: false,
|
|
193
194
|
ignoreDefaultArgs: [
|
|
194
|
-
"--disable-extensions"
|
|
195
|
+
"--disable-extensions",
|
|
196
|
+
...hasLoadExtension ? ["--disable-component-extensions-with-background-pages"] : []
|
|
195
197
|
],
|
|
196
198
|
assistantMode: true,
|
|
197
199
|
...options.forceHeadless !== void 0 ? { headless: options.forceHeadless === "headless" } : {}
|
|
@@ -248,7 +248,7 @@ function configFromEnv() {
|
|
|
248
248
|
options.timeoutAction = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION);
|
|
249
249
|
options.timeoutNavigation = numberParser(process.env.PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION);
|
|
250
250
|
options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
|
|
251
|
-
options.userDataDir = envToString(process.env.
|
|
251
|
+
options.userDataDir = envToString(process.env.PLAYWRIGHT_USER_DATA_DIR);
|
|
252
252
|
options.viewportSize = resolutionParser("--viewport-size", process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE);
|
|
253
253
|
return configFromCLIOptions(options);
|
|
254
254
|
}
|
|
@@ -45,6 +45,18 @@ class Context {
|
|
|
45
45
|
constructor(options) {
|
|
46
46
|
this._tabs = [];
|
|
47
47
|
this._abortController = new AbortController();
|
|
48
|
+
// DOM Analyzer state — stored per session so the LLM can query it lazily.
|
|
49
|
+
// Keyed by (normalizedEntryUrl, storageStateHash, probeButtonsMode) —
|
|
50
|
+
// see crawler.ts and tools/sitemap.ts for cache-key composition.
|
|
51
|
+
this.sitemapCache = /* @__PURE__ */ new Map();
|
|
52
|
+
/**
|
|
53
|
+
* Phase B widget-contract inferred cache — per-session, in-memory.
|
|
54
|
+
* Key: 16-hex fingerprint. Value: agent-authored WidgetContract.
|
|
55
|
+
* Curated contracts are NOT stored here — they live in curatedWidgets.ts.
|
|
56
|
+
* Mutable for non-curated fingerprints; cacheInferredContract refuses to
|
|
57
|
+
* overwrite curated entries (see widgetContract.ts).
|
|
58
|
+
*/
|
|
59
|
+
this.inferredContracts = /* @__PURE__ */ new Map();
|
|
48
60
|
this.config = options.config;
|
|
49
61
|
this.sessionLog = options.sessionLog;
|
|
50
62
|
this.options = options;
|
|
@@ -185,7 +197,11 @@ class Context {
|
|
|
185
197
|
if (this.sessionLog)
|
|
186
198
|
await InputRecorder.create(this, browserContext);
|
|
187
199
|
if (!this.config.allowUnrestrictedFileAccess) {
|
|
188
|
-
|
|
200
|
+
const allowedProtocols = ["http:", "https:", "about:", "data:"];
|
|
201
|
+
const launchArgs = this.config.browser.launchOptions?.args;
|
|
202
|
+
if (launchArgs?.some((arg) => arg.startsWith("--load-extension=")))
|
|
203
|
+
allowedProtocols.push("chrome-extension:");
|
|
204
|
+
browserContext._setAllowedProtocols(allowedProtocols);
|
|
189
205
|
browserContext._setAllowedDirectories(allRootPaths(this._clientInfo));
|
|
190
206
|
}
|
|
191
207
|
await this._setupRequestInterception(browserContext);
|
|
@@ -27,10 +27,12 @@ module.exports = __toCommonJS(tab_exports);
|
|
|
27
27
|
var import_events = require("events");
|
|
28
28
|
var import_utils = require("playwright-core/lib/utils");
|
|
29
29
|
var import_utils2 = require("./tools/utils");
|
|
30
|
+
var import_extensionFrames = require("./tools/extensionFrames");
|
|
30
31
|
var import_log = require("../log");
|
|
31
32
|
var import_dialogs = require("./tools/dialogs");
|
|
32
33
|
var import_files = require("./tools/files");
|
|
33
34
|
var import_transform = require("../../transform/transform");
|
|
35
|
+
var import_blueprintCache = require("../../dom-analyzer/blueprintCache");
|
|
34
36
|
const TabEvents = {
|
|
35
37
|
modalState: "modalState"
|
|
36
38
|
};
|
|
@@ -45,6 +47,19 @@ class Tab extends import_events.EventEmitter {
|
|
|
45
47
|
this._needsFullSnapshot = false;
|
|
46
48
|
this._eventEntries = [];
|
|
47
49
|
this._recentEventEntries = [];
|
|
50
|
+
/**
|
|
51
|
+
* Refs for elements inside chrome-extension iframes that live under closed
|
|
52
|
+
* shadow DOMs on the host page. Keyed by refs of the form `ef<frameIdx>-<seq>`
|
|
53
|
+
* and populated by captureSnapshot(); consumed by refLocators().
|
|
54
|
+
*/
|
|
55
|
+
this._extensionFrameRefs = /* @__PURE__ */ new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Per-tab cache of the most-recent PageBlueprint per URL. Used by
|
|
58
|
+
* browser_blueprint to return a delta against the prior capture at the
|
|
59
|
+
* same URL instead of the full payload. Cleared on tab close (not on
|
|
60
|
+
* navigation — same-URL revisits should reuse the prior blueprint).
|
|
61
|
+
*/
|
|
62
|
+
this.blueprintCache = new import_blueprintCache.BlueprintCache(5);
|
|
48
63
|
this.context = context;
|
|
49
64
|
this.page = page;
|
|
50
65
|
this._onPageClose = onPageClose;
|
|
@@ -148,6 +163,7 @@ class Tab extends import_events.EventEmitter {
|
|
|
148
163
|
}
|
|
149
164
|
_onClose() {
|
|
150
165
|
this._clearCollectedArtifacts();
|
|
166
|
+
this.blueprintCache.clear();
|
|
151
167
|
this._onPageClose(this);
|
|
152
168
|
}
|
|
153
169
|
async headerSnapshot() {
|
|
@@ -211,6 +227,20 @@ class Tab extends import_events.EventEmitter {
|
|
|
211
227
|
if (tabSnapshot) {
|
|
212
228
|
tabSnapshot.events = this._recentEventEntries;
|
|
213
229
|
this._recentEventEntries = [];
|
|
230
|
+
try {
|
|
231
|
+
const extSnap = await (0, import_extensionFrames.enumerateExtensionFrames)(this.page);
|
|
232
|
+
if (extSnap.lines.length) {
|
|
233
|
+
tabSnapshot.ariaSnapshot = tabSnapshot.ariaSnapshot ? `${tabSnapshot.ariaSnapshot}
|
|
234
|
+
${extSnap.lines.join("\n")}` : extSnap.lines.join("\n");
|
|
235
|
+
if (tabSnapshot.ariaSnapshotDiff !== void 0) {
|
|
236
|
+
tabSnapshot.ariaSnapshotDiff = tabSnapshot.ariaSnapshotDiff ? `${tabSnapshot.ariaSnapshotDiff}
|
|
237
|
+
${extSnap.lines.join("\n")}` : extSnap.lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this._extensionFrameRefs = extSnap.refMap;
|
|
241
|
+
} catch {
|
|
242
|
+
this._extensionFrameRefs = /* @__PURE__ */ new Map();
|
|
243
|
+
}
|
|
214
244
|
}
|
|
215
245
|
this._needsFullSnapshot = !tabSnapshot;
|
|
216
246
|
return tabSnapshot ?? {
|
|
@@ -248,6 +278,14 @@ class Tab extends import_events.EventEmitter {
|
|
|
248
278
|
async refLocators(params) {
|
|
249
279
|
await this._initializedPromise;
|
|
250
280
|
return Promise.all(params.map(async (param) => {
|
|
281
|
+
if (param.ref.startsWith("ef")) {
|
|
282
|
+
const ext = this._extensionFrameRefs.get(param.ref);
|
|
283
|
+
if (!ext)
|
|
284
|
+
throw new Error(`Extension-frame ref ${param.ref} not found. Call browser_snapshot to refresh.`);
|
|
285
|
+
const escapedMatch = ext.urlMatch.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
286
|
+
const resolved = `frames().find(f => f.url().includes('${escapedMatch}')).${ext.expr}`;
|
|
287
|
+
return { locator: ext.locator, resolved };
|
|
288
|
+
}
|
|
251
289
|
try {
|
|
252
290
|
let locator = this.page.locator(`aria-ref=${param.ref}`);
|
|
253
291
|
if (param.element)
|