@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.
Files changed (60) hide show
  1. package/build/playwright/registerPlaywrightTools.js +12 -0
  2. package/build/playwright/traceRecordingPrompt.js +15 -0
  3. package/build/prompts/test-recommendation/diffExecutionPlan.js +31 -0
  4. package/build/prompts/test-recommendation/recommendationSections.js +1 -2
  5. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +94 -0
  6. package/build/prompts/testbot/testbot-prompts.js +115 -11
  7. package/build/prompts/testbot/testbot-prompts.test.js +79 -0
  8. package/build/resources/testbotResource.js +1 -1
  9. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  10. package/build/services/ScenarioGenerationService.js +36 -3
  11. package/build/services/ScenarioGenerationService.test.js +158 -22
  12. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  13. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  14. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  15. package/build/tools/test-management/analyzeChangesTool.js +7 -1
  16. package/build/utils/routeParsers.js +12 -0
  17. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  18. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  19. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1161 -0
  20. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  21. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  22. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  23. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +250 -0
  24. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +298 -0
  25. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  26. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  27. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  28. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  29. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  30. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  31. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  32. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  33. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  34. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  35. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  36. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  37. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  38. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  39. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  40. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  41. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  42. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  43. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  44. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  45. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  46. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  47. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  48. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  49. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  50. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +129 -0
  51. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +137 -0
  52. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  53. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  54. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  55. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  56. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  57. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  58. package/node_modules/playwright/package.json +1 -1
  59. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  60. 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.PLAYWRIGHT_MCP_USER_DATA_DIR);
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
- browserContext._setAllowedProtocols(["http:", "https:", "about:", "data:"]);
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)