@skyramp/mcp 0.0.65 → 0.1.0-rc.2
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/traceRecordingPrompt.js +30 -36
- package/build/prompts/architectPersona.js +19 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +42 -50
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +125 -0
- package/build/prompts/test-recommendation/recommendationSections.js +121 -4
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +151 -9
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +416 -61
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +455 -63
- package/build/prompts/testbot/testbot-prompts.js +111 -100
- package/build/prompts/testbot/testbot-prompts.test.js +142 -0
- package/build/resources/analysisResources.js +13 -5
- package/build/services/ScenarioGenerationService.js +2 -2
- package/build/services/ScenarioGenerationService.test.js +35 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/executeSkyrampTestTool.js +4 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +51 -21
- package/build/tools/generate-tests/generateContractRestTool.js +26 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
- package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
- package/build/tools/generate-tests/generateUIRestTool.js +69 -4
- package/build/tools/submitReportTool.js +27 -13
- package/build/tools/test-management/analyzeChangesTool.js +32 -10
- package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
- package/build/types/RepositoryAnalysis.js +25 -3
- package/build/types/TestRecommendation.js +5 -4
- package/build/types/TestTypes.js +44 -9
- package/build/utils/AnalysisStateManager.js +43 -9
- package/build/utils/AnalysisStateManager.test.js +35 -0
- package/build/utils/routeParsers.js +35 -0
- package/build/utils/routeParsers.test.js +66 -1
- package/build/utils/scenarioDrafting.js +207 -360
- package/build/utils/scenarioDrafting.test.js +191 -256
- package/build/utils/trace-parser.js +24 -6
- package/build/utils/trace-parser.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
- package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
- package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
- package/package.json +2 -2
- package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
|
@@ -46,9 +46,17 @@ var import_assertTool = require("./assertTool");
|
|
|
46
46
|
var import_types = require("./types");
|
|
47
47
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
48
48
|
class TraceRecordingBackend {
|
|
49
|
+
// true while page.reload() is in progress — suppresses spurious popup tracking
|
|
49
50
|
constructor(options) {
|
|
50
51
|
this._trackedActions = [];
|
|
51
52
|
this._initialized = false;
|
|
53
|
+
this._pageCount = 0;
|
|
54
|
+
// number of popup pages seen
|
|
55
|
+
this._currentPageAlias = "page";
|
|
56
|
+
// alias of the currently active page
|
|
57
|
+
this._pendingPopupAlias = null;
|
|
58
|
+
// popup alias to stamp on the NEXT tracked click
|
|
59
|
+
this._reloading = false;
|
|
52
60
|
this._options = options || {};
|
|
53
61
|
this._outputDir = options?.outputDir || process.cwd();
|
|
54
62
|
this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
|
|
@@ -72,6 +80,8 @@ class TraceRecordingBackend {
|
|
|
72
80
|
},
|
|
73
81
|
capabilities: ["testing"],
|
|
74
82
|
outputDir: this._tempDir,
|
|
83
|
+
snapshot: { mode: "full" },
|
|
84
|
+
// Always return full snapshots — incremental diffs cause empty snapshots after clicks
|
|
75
85
|
timeouts: {
|
|
76
86
|
action: 15e3,
|
|
77
87
|
// 15s — modals and dynamic content need more time
|
|
@@ -83,6 +93,7 @@ class TraceRecordingBackend {
|
|
|
83
93
|
await this._browserBackend.initialize(clientInfo);
|
|
84
94
|
this._initialized = true;
|
|
85
95
|
traceDebug("TraceRecordingBackend initialized");
|
|
96
|
+
this._setupPopupTracking();
|
|
86
97
|
}
|
|
87
98
|
async listTools() {
|
|
88
99
|
const browserTools = await this._browserBackend.listTools();
|
|
@@ -99,7 +110,6 @@ class TraceRecordingBackend {
|
|
|
99
110
|
}
|
|
100
111
|
const handler = (0, import_exportTool.createExportZipHandler)({
|
|
101
112
|
trackedActions: this._trackedActions,
|
|
102
|
-
rootPath: this._outputDir,
|
|
103
113
|
harPath: this._harPath
|
|
104
114
|
});
|
|
105
115
|
const exportResult = await handler(parsed);
|
|
@@ -115,8 +125,60 @@ class TraceRecordingBackend {
|
|
|
115
125
|
const result2 = await this._handleSelectOption(args || {});
|
|
116
126
|
return result2;
|
|
117
127
|
}
|
|
128
|
+
if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
|
|
129
|
+
const index = args?.index ?? 0;
|
|
130
|
+
this._currentPageAlias = index === 0 ? "page" : `page${index}`;
|
|
131
|
+
traceDebug(`Tab switched to index ${index} \u2192 pageAlias: ${this._currentPageAlias}`);
|
|
132
|
+
const tabResult = await this._browserBackend.callTool(name, args);
|
|
133
|
+
return tabResult;
|
|
134
|
+
}
|
|
135
|
+
if (name === "browser_navigate") {
|
|
136
|
+
const targetUrl = args?.url;
|
|
137
|
+
const currentUrl = this._browserBackend.context?.currentTab()?.page.url();
|
|
138
|
+
if (targetUrl && currentUrl && this._normalizeUrl(targetUrl) === this._normalizeUrl(currentUrl)) {
|
|
139
|
+
traceDebug(`Navigate to same URL detected (${targetUrl}), converting to reload`);
|
|
140
|
+
const ts = Date.now();
|
|
141
|
+
const reloadAlias = this._currentPageAlias;
|
|
142
|
+
const tab = this._browserBackend.context.currentTab();
|
|
143
|
+
this._reloading = true;
|
|
144
|
+
try {
|
|
145
|
+
await tab.page.reload({ waitUntil: "domcontentloaded" });
|
|
146
|
+
} finally {
|
|
147
|
+
this._reloading = false;
|
|
148
|
+
this._currentPageAlias = reloadAlias;
|
|
149
|
+
this._pendingPopupAlias = null;
|
|
150
|
+
}
|
|
151
|
+
this._trackedActions.push({
|
|
152
|
+
toolName: "browser_navigate",
|
|
153
|
+
args: { url: currentUrl },
|
|
154
|
+
code: "",
|
|
155
|
+
timestamp: ts,
|
|
156
|
+
pageAlias: this._currentPageAlias
|
|
157
|
+
});
|
|
158
|
+
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{ type: "text", text: `### Navigation (reload)
|
|
162
|
+
Reloaded current page: ${currentUrl}
|
|
163
|
+
` },
|
|
164
|
+
...snapResult.content || []
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
118
169
|
const timestampBeforeAction = Date.now();
|
|
119
|
-
const
|
|
170
|
+
const pageAliasBeforeAction = this._currentPageAlias;
|
|
171
|
+
let preClickHoverCode = null;
|
|
172
|
+
if (name === "browser_click" && args?.ref) {
|
|
173
|
+
const hoverResult = await this._browserBackend.callTool("browser_hover", {
|
|
174
|
+
element: args.element || "element",
|
|
175
|
+
ref: args.ref
|
|
176
|
+
});
|
|
177
|
+
if (!hoverResult.isError)
|
|
178
|
+
preClickHoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? null;
|
|
179
|
+
}
|
|
180
|
+
let result = await this._browserBackend.callTool(name, args);
|
|
181
|
+
result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode);
|
|
120
182
|
if (name === "browser_fill_form" && result.isError) {
|
|
121
183
|
const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
122
184
|
if (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout")) {
|
|
@@ -125,7 +187,7 @@ class TraceRecordingBackend {
|
|
|
125
187
|
const comboField = fields.find((f) => f.type === "combobox");
|
|
126
188
|
if (comboField) {
|
|
127
189
|
traceDebug("fill_form failed on combobox field, retrying with select fallback");
|
|
128
|
-
this._maybeTrackAction(name, args || {}, { content: result.content, isError: false }, timestampBeforeAction);
|
|
190
|
+
this._maybeTrackAction(name, args || {}, { content: result.content, isError: false }, timestampBeforeAction, pageAliasBeforeAction);
|
|
129
191
|
const selectResult = await this._handleSelectOption({
|
|
130
192
|
element: comboField.name || "dropdown",
|
|
131
193
|
ref: comboField.ref,
|
|
@@ -136,7 +198,7 @@ class TraceRecordingBackend {
|
|
|
136
198
|
}
|
|
137
199
|
}
|
|
138
200
|
}
|
|
139
|
-
this._maybeTrackAction(name, args || {}, result, timestampBeforeAction);
|
|
201
|
+
this._maybeTrackAction(name, args || {}, result, timestampBeforeAction, pageAliasBeforeAction);
|
|
140
202
|
return result;
|
|
141
203
|
}
|
|
142
204
|
serverClosed() {
|
|
@@ -471,7 +533,227 @@ ${details}` }]
|
|
|
471
533
|
}
|
|
472
534
|
return null;
|
|
473
535
|
}
|
|
474
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Improve ambiguous selectors in action code before tracking.
|
|
538
|
+
* Handles role-based selectors like getByRole("combobox") that match multiple elements.
|
|
539
|
+
* Also upgrades raw CSS locators (locator('.class')) to semantic selectors.
|
|
540
|
+
* preClickHoverCode: hover result taken BEFORE the click (needed for navigation clicks
|
|
541
|
+
* where the element ref is no longer valid after navigation).
|
|
542
|
+
*/
|
|
543
|
+
async _improveActionSelector(toolName, args, result, preClickHoverCode = null) {
|
|
544
|
+
if (result.isError)
|
|
545
|
+
return result;
|
|
546
|
+
const selectorTools = /* @__PURE__ */ new Set(["browser_click", "browser_hover", "browser_type", "browser_fill"]);
|
|
547
|
+
if (!selectorTools.has(toolName))
|
|
548
|
+
return result;
|
|
549
|
+
const parsed = (0, import_response.parseResponse)(result);
|
|
550
|
+
const code = parsed?.code ?? "";
|
|
551
|
+
if (!code)
|
|
552
|
+
return result;
|
|
553
|
+
const cssLocatorMatch = code.match(/await\s+page\.locator\(\s*(['"])[.#][^'"]+\1\s*\)\./);
|
|
554
|
+
if (cssLocatorMatch) {
|
|
555
|
+
const actionMatch = code.match(/\.(click|fill|hover|dblclick|check|uncheck)\s*\(/);
|
|
556
|
+
const action = actionMatch?.[1] ?? "click";
|
|
557
|
+
if (preClickHoverCode) {
|
|
558
|
+
const locatorMatch = preClickHoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
559
|
+
if (locatorMatch) {
|
|
560
|
+
const locatorExpr = locatorMatch[1].trim();
|
|
561
|
+
if (!locatorExpr.startsWith("locator(")) {
|
|
562
|
+
const improvedCode = `await page.${locatorExpr}.${action}();`;
|
|
563
|
+
traceDebug(`Improved CSS locator via pre-hover: ${locatorExpr}`);
|
|
564
|
+
return this._modifyResultCode(result, improvedCode);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const ref2 = args.ref;
|
|
569
|
+
const snapText2 = parsed?.snapshot ?? "";
|
|
570
|
+
if (ref2 && snapText2) {
|
|
571
|
+
const refLine2 = snapText2.split("\n").find((l) => l.includes(`[ref=${ref2}]`)) || "";
|
|
572
|
+
if (refLine2) {
|
|
573
|
+
const locatorData = this._extractLocatorForRef(refLine2);
|
|
574
|
+
if (locatorData) {
|
|
575
|
+
const locatorCode = this._locatorToCode(locatorData);
|
|
576
|
+
if (locatorCode) {
|
|
577
|
+
const improvedCode = `await page.${locatorCode}.${action}();`;
|
|
578
|
+
traceDebug(`Improved CSS locator from snapshot: ${locatorCode}`);
|
|
579
|
+
return this._modifyResultCode(result, improvedCode);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
const roleMatch = code.match(/getByRole\(\s*['"](\w+)['"]\s*\)(?!\s*\.(?:filter|nth|first|last))/);
|
|
587
|
+
if (!roleMatch)
|
|
588
|
+
return result;
|
|
589
|
+
const role = roleMatch[1];
|
|
590
|
+
const ref = args.ref;
|
|
591
|
+
if (!ref)
|
|
592
|
+
return result;
|
|
593
|
+
const snapText = parsed?.snapshot ?? "";
|
|
594
|
+
if (!snapText)
|
|
595
|
+
return result;
|
|
596
|
+
const snapLines = snapText.split("\n");
|
|
597
|
+
const rolePattern = new RegExp(`^\\s*-\\s*${role}\\s+`, "m");
|
|
598
|
+
const roleCount = snapLines.filter((l) => rolePattern.test(l)).length;
|
|
599
|
+
if (roleCount <= 1) {
|
|
600
|
+
traceDebug(`Role "${role}" is unique, no improvement needed`);
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
traceDebug(`Role "${role}" appears ${roleCount} times \u2014 attempting to disambiguate`);
|
|
604
|
+
const refPattern = `[ref=${ref}]`;
|
|
605
|
+
const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
|
|
606
|
+
if (refIdx < 0)
|
|
607
|
+
return result;
|
|
608
|
+
const refLine = snapLines[refIdx];
|
|
609
|
+
const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
|
|
610
|
+
if (textMatch && textMatch[1]) {
|
|
611
|
+
const visibleText = textMatch[1];
|
|
612
|
+
const textPattern = new RegExp(`^\\s*-\\s*${role}\\s+"[^"]*${visibleText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
|
|
613
|
+
const textCount = snapLines.filter((l) => textPattern.test(l)).length;
|
|
614
|
+
if (textCount === 1) {
|
|
615
|
+
const improvedCode = code.replace(
|
|
616
|
+
/getByRole\((\s*['"](\w+)['"]\s*)\)/,
|
|
617
|
+
`getByRole($1).filter({ hasText: "${visibleText}" })`
|
|
618
|
+
);
|
|
619
|
+
traceDebug(`Improved selector with text filter: ${visibleText}`);
|
|
620
|
+
return this._modifyResultCode(result, improvedCode);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const refIndent = refLine.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
624
|
+
for (let i = refIdx - 1; i >= 0; i--) {
|
|
625
|
+
const line = snapLines[i];
|
|
626
|
+
const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
627
|
+
if (indent >= refIndent)
|
|
628
|
+
continue;
|
|
629
|
+
const ancestorRefMatch = line.match(/\[ref=(\w+)\]/);
|
|
630
|
+
if (!ancestorRefMatch)
|
|
631
|
+
continue;
|
|
632
|
+
const ancestorRef = ancestorRefMatch[1];
|
|
633
|
+
try {
|
|
634
|
+
const ancestorHover = await this._browserBackend.callTool("browser_hover", {
|
|
635
|
+
element: "parent element",
|
|
636
|
+
ref: ancestorRef
|
|
637
|
+
});
|
|
638
|
+
if (!ancestorHover.isError) {
|
|
639
|
+
const ancestorCode = (0, import_response.parseResponse)(ancestorHover)?.code ?? "";
|
|
640
|
+
const testidMatch = ancestorCode.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
641
|
+
if (testidMatch) {
|
|
642
|
+
const testid = testidMatch[1];
|
|
643
|
+
const improvedCode = code.replace(
|
|
644
|
+
/await\s+page\./,
|
|
645
|
+
`await page.getByTestId("${testid}").`
|
|
646
|
+
);
|
|
647
|
+
traceDebug(`Improved selector by chaining from test-id: ${testid}`);
|
|
648
|
+
return this._modifyResultCode(result, improvedCode);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
if (indent === 0)
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
const roleRefs = [];
|
|
657
|
+
for (const line of snapLines) {
|
|
658
|
+
if (rolePattern.test(line)) {
|
|
659
|
+
const m = line.match(/\[ref=(\w+)\]/);
|
|
660
|
+
if (m)
|
|
661
|
+
roleRefs.push(m[1]);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const index = roleRefs.indexOf(ref);
|
|
665
|
+
if (index >= 0) {
|
|
666
|
+
const improvedCode = code.replace(
|
|
667
|
+
/getByRole\((\s*['"](\w+)['"]\s*)\)/,
|
|
668
|
+
`getByRole($1).nth(${index})`
|
|
669
|
+
);
|
|
670
|
+
traceDebug(`Improved selector with .nth(${index})`);
|
|
671
|
+
return this._modifyResultCode(result, improvedCode);
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
/** Helper to modify the code in a CallToolResult. */
|
|
676
|
+
_modifyResultCode(result, newCode) {
|
|
677
|
+
const modified = { ...result, content: [...result.content] };
|
|
678
|
+
for (let i = 0; i < modified.content.length; i++) {
|
|
679
|
+
if (modified.content[i].type === "text") {
|
|
680
|
+
const text = modified.content[i].text || "";
|
|
681
|
+
if (text.includes("```javascript") || text.includes("await page.")) {
|
|
682
|
+
const codeBlockMatch = text.match(/([\s\S]*```javascript\n)([\s\S]*?)(```[\s\S]*)/);
|
|
683
|
+
if (codeBlockMatch) {
|
|
684
|
+
modified.content[i] = {
|
|
685
|
+
...modified.content[i],
|
|
686
|
+
text: codeBlockMatch[1] + newCode + "\n" + codeBlockMatch[3]
|
|
687
|
+
};
|
|
688
|
+
} else {
|
|
689
|
+
const lines = text.split("\n");
|
|
690
|
+
const codeLineIdx = lines.findIndex((l) => l.includes("await page."));
|
|
691
|
+
if (codeLineIdx >= 0) {
|
|
692
|
+
lines[codeLineIdx] = newCode;
|
|
693
|
+
modified.content[i] = {
|
|
694
|
+
...modified.content[i],
|
|
695
|
+
text: lines.join("\n")
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return modified;
|
|
703
|
+
}
|
|
704
|
+
/** Convert a locator object to a Playwright code expression like getByRole('link', { name: 'X' }). */
|
|
705
|
+
_locatorToCode(parsed) {
|
|
706
|
+
const { locator } = parsed;
|
|
707
|
+
switch (locator.kind) {
|
|
708
|
+
case "test-id":
|
|
709
|
+
return `getByTestId('${locator.body}')`;
|
|
710
|
+
case "role": {
|
|
711
|
+
const name = locator.options?.name;
|
|
712
|
+
const exact = locator.options?.exact;
|
|
713
|
+
const opts = name ? `, { name: '${name}'${exact ? ", exact: true" : ""} }` : "";
|
|
714
|
+
return `getByRole('${locator.body}'${opts})`;
|
|
715
|
+
}
|
|
716
|
+
case "label":
|
|
717
|
+
return `getByLabel('${locator.body}')`;
|
|
718
|
+
case "text":
|
|
719
|
+
return `getByText('${locator.body}')`;
|
|
720
|
+
default:
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Listen for new pages (popups) on the browser context.
|
|
726
|
+
* When a popup opens, find the most recent click that triggered it and mark it
|
|
727
|
+
* with a popupAlias signal. Switch the current page alias to the new page.
|
|
728
|
+
*/
|
|
729
|
+
_setupPopupTracking() {
|
|
730
|
+
void (async () => {
|
|
731
|
+
try {
|
|
732
|
+
const context = this._browserBackend.context;
|
|
733
|
+
if (!context)
|
|
734
|
+
return;
|
|
735
|
+
const browserContext = await context.ensureBrowserContext();
|
|
736
|
+
let initialPageSeen = false;
|
|
737
|
+
browserContext.on("page", () => {
|
|
738
|
+
if (!initialPageSeen) {
|
|
739
|
+
initialPageSeen = true;
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (this._reloading) {
|
|
743
|
+
traceDebug("Ignoring spurious popup opened during reload");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
this._pageCount++;
|
|
747
|
+
const popupAlias = `page${this._pageCount}`;
|
|
748
|
+
this._currentPageAlias = popupAlias;
|
|
749
|
+
this._pendingPopupAlias = popupAlias;
|
|
750
|
+
traceDebug(`Popup page opened: ${popupAlias} (pending stamp)`);
|
|
751
|
+
});
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
})();
|
|
755
|
+
}
|
|
756
|
+
_maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
|
|
475
757
|
if (result.isError)
|
|
476
758
|
return;
|
|
477
759
|
if (toolName === "browser_press_key") {
|
|
@@ -482,13 +764,28 @@ ${details}` }]
|
|
|
482
764
|
const parsed = (0, import_response.parseResponse)(result);
|
|
483
765
|
const code = parsed?.code ?? "";
|
|
484
766
|
if (code || import_types.ARGS_ONLY_TOOLS.has(toolName)) {
|
|
767
|
+
const popupAlias = this._pendingPopupAlias ?? void 0;
|
|
768
|
+
this._pendingPopupAlias = null;
|
|
769
|
+
const pageAlias = popupAlias ? pageAliasBeforeAction ?? this._currentPageAlias : this._currentPageAlias;
|
|
485
770
|
this._trackedActions.push({
|
|
486
771
|
toolName,
|
|
487
772
|
args,
|
|
488
773
|
code,
|
|
489
|
-
timestamp: timestamp ?? Date.now()
|
|
774
|
+
timestamp: timestamp ?? Date.now(),
|
|
775
|
+
pageAlias,
|
|
776
|
+
popupAlias
|
|
490
777
|
});
|
|
491
|
-
traceDebug(`Tracked action: ${toolName} (total: ${this._trackedActions.length})`);
|
|
778
|
+
traceDebug(`Tracked action: ${toolName} on ${pageAlias}${popupAlias ? ` \u2192 popup:${popupAlias}` : ""} (total: ${this._trackedActions.length})`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/** Normalize a URL for comparison: strip trailing slash and fragment. */
|
|
782
|
+
_normalizeUrl(url) {
|
|
783
|
+
try {
|
|
784
|
+
const u = new URL(url);
|
|
785
|
+
u.hash = "";
|
|
786
|
+
return u.toString().replace(/\/$/, "");
|
|
787
|
+
} catch {
|
|
788
|
+
return url.replace(/\/$/, "");
|
|
492
789
|
}
|
|
493
790
|
}
|
|
494
791
|
async _autoExportAndClose() {
|
|
@@ -38,8 +38,8 @@ var import_fs = __toESM(require("fs"));
|
|
|
38
38
|
var import_path = __toESM(require("path"));
|
|
39
39
|
var import_zipBundle = require("playwright-core/lib/zipBundle");
|
|
40
40
|
var import_utils = require("playwright-core/lib/utils");
|
|
41
|
-
const
|
|
42
|
-
const
|
|
41
|
+
const DEFAULT_PAGE_ALIAS = "page";
|
|
42
|
+
const DEFAULT_FRAME_PATH = [];
|
|
43
43
|
function jsonlHeader(browserName, harPath) {
|
|
44
44
|
return JSON.stringify({
|
|
45
45
|
browserName,
|
|
@@ -53,10 +53,18 @@ function jsonlHeader(browserName, harPath) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
function extractLocatorFromCode(code) {
|
|
56
|
+
const contentFrameMatch = code.match(/await\s+page\.locator\((['"])(iframe[^\n]*?)\1\)(?:\.contentFrame\(\))+\.(.*?)\.(click|dblclick|fill|pressSequentially|check|uncheck|selectOption|hover|dragTo)\s*\(/s);
|
|
57
|
+
if (contentFrameMatch) {
|
|
58
|
+
return { locatorExpr: contentFrameMatch[3].trim(), framePath: [contentFrameMatch[2]] };
|
|
59
|
+
}
|
|
60
|
+
const chainedIframeMatch = code.match(/await\s+page\.locator\((['"])(iframe[^\n]*?)\1\)\.(get\w+\(.*?)\.(click|dblclick|fill|pressSequentially|check|uncheck|selectOption|hover|dragTo)\s*\(/s);
|
|
61
|
+
if (chainedIframeMatch) {
|
|
62
|
+
return { locatorExpr: chainedIframeMatch[3].trim(), framePath: [chainedIframeMatch[2]] };
|
|
63
|
+
}
|
|
56
64
|
const match = code.match(/await\s+page\.(.*?)\.(click|dblclick|fill|pressSequentially|check|uncheck|selectOption|hover|dragTo)\s*\(/s);
|
|
57
65
|
if (!match)
|
|
58
66
|
return void 0;
|
|
59
|
-
return match[1].trim();
|
|
67
|
+
return { locatorExpr: match[1].trim(), framePath: [] };
|
|
60
68
|
}
|
|
61
69
|
function locatorToJsonl(locatorExpr) {
|
|
62
70
|
const chainParts = locatorExpr.split(/\)\.(?=get)/);
|
|
@@ -169,25 +177,39 @@ function extractSelectOptions(code) {
|
|
|
169
177
|
}
|
|
170
178
|
function trackedActionToJsonl(action, pageGuid, timestamp) {
|
|
171
179
|
const { toolName, args, code } = action;
|
|
180
|
+
const pageAlias = action.pageAlias ?? DEFAULT_PAGE_ALIAS;
|
|
181
|
+
const signals = action.popupAlias ? [{ name: "popup", popupAlias: action.popupAlias }] : [];
|
|
172
182
|
const base = {
|
|
173
|
-
signals
|
|
183
|
+
signals,
|
|
174
184
|
timestamp: String(timestamp),
|
|
175
185
|
pageGuid,
|
|
176
|
-
pageAlias
|
|
177
|
-
framePath:
|
|
186
|
+
pageAlias,
|
|
187
|
+
framePath: action.framePath ?? DEFAULT_FRAME_PATH
|
|
178
188
|
};
|
|
179
189
|
if (toolName === "browser_navigate")
|
|
180
190
|
return JSON.stringify({ name: "navigate", url: args.url, ...base });
|
|
181
191
|
if (toolName === "browser_navigate_back")
|
|
182
192
|
return null;
|
|
193
|
+
if (toolName === "browser_wait_for") {
|
|
194
|
+
const text = args.text;
|
|
195
|
+
const textGone = args.textGone;
|
|
196
|
+
if (text)
|
|
197
|
+
return JSON.stringify({ name: "waitForSelector", selector: `text=${text}`, ...base });
|
|
198
|
+
if (textGone)
|
|
199
|
+
return JSON.stringify({ name: "waitForSelector", selector: `text=${textGone}`, state: "hidden", ...base });
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
183
202
|
if (toolName === "browser_press_key")
|
|
184
203
|
return JSON.stringify({ name: "press", key: args.key, selector: "", ...base });
|
|
185
204
|
if (!code)
|
|
186
205
|
return null;
|
|
187
|
-
const
|
|
188
|
-
if (!
|
|
206
|
+
const extracted = extractLocatorFromCode(code);
|
|
207
|
+
if (!extracted)
|
|
189
208
|
return null;
|
|
190
|
-
const
|
|
209
|
+
const { locatorExpr, framePath: codeFramePath } = extracted;
|
|
210
|
+
if (codeFramePath.length > 0)
|
|
211
|
+
base.framePath = codeFramePath;
|
|
212
|
+
const parsed = locatorToJsonl(locatorExpr);
|
|
191
213
|
if (!parsed)
|
|
192
214
|
return null;
|
|
193
215
|
const { selector, locator: locatorObj } = parsed;
|
|
@@ -236,8 +258,8 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
|
|
|
236
258
|
signals: [],
|
|
237
259
|
timestamp: String(timestamp),
|
|
238
260
|
pageGuid,
|
|
239
|
-
pageAlias:
|
|
240
|
-
framePath:
|
|
261
|
+
pageAlias: DEFAULT_PAGE_ALIAS,
|
|
262
|
+
framePath: DEFAULT_FRAME_PATH
|
|
241
263
|
};
|
|
242
264
|
const parts = code.split(":");
|
|
243
265
|
const selectorPart = parts[1] || "";
|
|
@@ -298,11 +320,27 @@ function fillFormToJsonl(action, pageGuid, baseTimestamp) {
|
|
|
298
320
|
signals: [],
|
|
299
321
|
timestamp: String(baseTimestamp + i),
|
|
300
322
|
pageGuid,
|
|
301
|
-
pageAlias:
|
|
302
|
-
framePath:
|
|
323
|
+
pageAlias: DEFAULT_PAGE_ALIAS,
|
|
324
|
+
framePath: DEFAULT_FRAME_PATH
|
|
303
325
|
});
|
|
304
326
|
});
|
|
305
327
|
}
|
|
328
|
+
function removeRedundantClicks(actions) {
|
|
329
|
+
return actions.filter((action, i) => {
|
|
330
|
+
if (action.toolName !== "browser_click")
|
|
331
|
+
return true;
|
|
332
|
+
const next = actions[i + 1];
|
|
333
|
+
if (!next || !["browser_type", "browser_fill"].includes(next.toolName))
|
|
334
|
+
return true;
|
|
335
|
+
const clickRef = action.args?.ref;
|
|
336
|
+
const fillRef = next.args?.ref;
|
|
337
|
+
if (!clickRef || !fillRef || clickRef !== fillRef)
|
|
338
|
+
return true;
|
|
339
|
+
if (next.args?.slowly || next.code.includes(".pressSequentially("))
|
|
340
|
+
return true;
|
|
341
|
+
return false;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
306
344
|
function deduplicateRetries(actions) {
|
|
307
345
|
const firstNavigate = actions.find((a) => a.toolName === "browser_navigate");
|
|
308
346
|
if (!firstNavigate)
|
|
@@ -316,16 +354,52 @@ function deduplicateRetries(actions) {
|
|
|
316
354
|
return lastRestartIdx > 0 ? actions.slice(lastRestartIdx) : actions;
|
|
317
355
|
}
|
|
318
356
|
function buildJsonlContent(actions, browserName, harPath) {
|
|
319
|
-
const deduplicated = deduplicateRetries(actions);
|
|
320
|
-
const pageGuid = `page@${import_crypto.default.randomBytes(16).toString("hex")}`;
|
|
357
|
+
const deduplicated = removeRedundantClicks(deduplicateRetries(actions));
|
|
321
358
|
const startTime = deduplicated[0]?.timestamp ?? Date.now();
|
|
359
|
+
const pageGuids = /* @__PURE__ */ new Map();
|
|
360
|
+
const getPageGuid = (alias) => {
|
|
361
|
+
if (!pageGuids.has(alias))
|
|
362
|
+
pageGuids.set(alias, `page@${import_crypto.default.randomBytes(16).toString("hex")}`);
|
|
363
|
+
return pageGuids.get(alias);
|
|
364
|
+
};
|
|
365
|
+
const mainGuid = getPageGuid(DEFAULT_PAGE_ALIAS);
|
|
322
366
|
const lines = [
|
|
323
367
|
jsonlHeader(browserName, harPath),
|
|
324
|
-
JSON.stringify({ name: "openPage", url: "about:blank", signals: [], timestamp: String(startTime - 1e3), pageGuid, pageAlias:
|
|
368
|
+
JSON.stringify({ name: "openPage", url: "about:blank", signals: [], timestamp: String(startTime - 1e3), pageGuid: mainGuid, pageAlias: DEFAULT_PAGE_ALIAS, framePath: DEFAULT_FRAME_PATH })
|
|
325
369
|
];
|
|
326
370
|
let actionCount = 0;
|
|
327
371
|
const skipped = [];
|
|
372
|
+
const openedAliases = /* @__PURE__ */ new Set([DEFAULT_PAGE_ALIAS]);
|
|
373
|
+
let hadPopupFills = false;
|
|
328
374
|
for (const action of deduplicated) {
|
|
375
|
+
const alias = action.pageAlias ?? DEFAULT_PAGE_ALIAS;
|
|
376
|
+
const pageGuid = getPageGuid(alias);
|
|
377
|
+
if (action.popupAlias && !openedAliases.has(action.popupAlias)) {
|
|
378
|
+
const popupGuid = getPageGuid(action.popupAlias);
|
|
379
|
+
lines.push(JSON.stringify({ name: "openPage", url: "about:blank", signals: [], timestamp: String(action.timestamp), pageGuid: popupGuid, pageAlias: action.popupAlias, framePath: DEFAULT_FRAME_PATH }));
|
|
380
|
+
openedAliases.add(action.popupAlias);
|
|
381
|
+
}
|
|
382
|
+
if (alias !== DEFAULT_PAGE_ALIAS && !openedAliases.has(alias)) {
|
|
383
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
384
|
+
try {
|
|
385
|
+
const obj = JSON.parse(lines[i]);
|
|
386
|
+
if (obj.name === "openPage" || obj.name === "closePage")
|
|
387
|
+
continue;
|
|
388
|
+
if (!obj.pageAlias || obj.pageAlias !== alias) {
|
|
389
|
+
obj.signals = [...obj.signals || [], { name: "popup", popupAlias: alias }];
|
|
390
|
+
lines[i] = JSON.stringify(obj);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const popupGuid = getPageGuid(alias);
|
|
398
|
+
lines.push(JSON.stringify({ name: "openPage", url: "about:blank", signals: [], timestamp: String(action.timestamp), pageGuid: popupGuid, pageAlias: alias, framePath: DEFAULT_FRAME_PATH }));
|
|
399
|
+
openedAliases.add(alias);
|
|
400
|
+
}
|
|
401
|
+
if ((action.toolName === "browser_type" || action.toolName === "browser_fill") && alias !== DEFAULT_PAGE_ALIAS)
|
|
402
|
+
hadPopupFills = true;
|
|
329
403
|
if (action.toolName === "browser_fill_form") {
|
|
330
404
|
const formLines = fillFormToJsonl(action, pageGuid, action.timestamp);
|
|
331
405
|
lines.push(...formLines);
|
|
@@ -340,6 +414,11 @@ function buildJsonlContent(actions, browserName, harPath) {
|
|
|
340
414
|
}
|
|
341
415
|
continue;
|
|
342
416
|
}
|
|
417
|
+
if (action.toolName === "browser_navigate" && alias === DEFAULT_PAGE_ALIAS && hadPopupFills) {
|
|
418
|
+
lines.push(JSON.stringify({ name: "waitForTimeout", duration: 3e4, signals: [], timestamp: String(action.timestamp - 1), pageGuid, pageAlias: alias, framePath: DEFAULT_FRAME_PATH }));
|
|
419
|
+
actionCount++;
|
|
420
|
+
hadPopupFills = false;
|
|
421
|
+
}
|
|
343
422
|
const line = trackedActionToJsonl(action, pageGuid, action.timestamp);
|
|
344
423
|
if (line) {
|
|
345
424
|
lines.push(line);
|
|
@@ -348,18 +427,47 @@ function buildJsonlContent(actions, browserName, harPath) {
|
|
|
348
427
|
skipped.push(action.toolName);
|
|
349
428
|
}
|
|
350
429
|
}
|
|
351
|
-
lines.push(JSON.stringify({ name: "closePage", signals: [], timestamp: String(Date.now()), pageGuid, pageAlias:
|
|
430
|
+
lines.push(JSON.stringify({ name: "closePage", signals: [], timestamp: String(Date.now()), pageGuid: mainGuid, pageAlias: DEFAULT_PAGE_ALIAS, framePath: DEFAULT_FRAME_PATH }));
|
|
352
431
|
return { jsonl: lines.join("\n") + "\n", actionCount, skipped };
|
|
353
432
|
}
|
|
433
|
+
const CROSS_PAGE_URL_BLOCKLIST = [
|
|
434
|
+
"/app_init",
|
|
435
|
+
"notes.services.box.com"
|
|
436
|
+
];
|
|
437
|
+
function filterHarEntries(harContent) {
|
|
438
|
+
try {
|
|
439
|
+
const har = JSON.parse(harContent);
|
|
440
|
+
const entries = har?.log?.entries ?? [];
|
|
441
|
+
har.log.entries = entries.filter((entry) => {
|
|
442
|
+
const url = entry?.request?.url ?? "";
|
|
443
|
+
if (CROSS_PAGE_URL_BLOCKLIST.some((pattern) => url.includes(pattern)))
|
|
444
|
+
return false;
|
|
445
|
+
const text = entry?.response?.content?.text;
|
|
446
|
+
if (!text || text.trim() === "")
|
|
447
|
+
return false;
|
|
448
|
+
try {
|
|
449
|
+
JSON.parse(text);
|
|
450
|
+
return true;
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return JSON.stringify(har);
|
|
456
|
+
} catch {
|
|
457
|
+
return harContent;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
354
460
|
async function writeSkyrampZip(outputZipPath, jsonlContent, harPath) {
|
|
355
461
|
const zipFile = new import_zipBundle.yazl.ZipFile();
|
|
356
462
|
const done = new import_utils.ManualPromise();
|
|
357
463
|
zipFile.on("error", (e) => done.reject(e));
|
|
358
464
|
zipFile.addBuffer(Buffer.from(jsonlContent, "utf-8"), "skyramp_playwright.txt");
|
|
359
|
-
if (await import_fs.default.promises.stat(harPath).then(() => true).catch(() => false))
|
|
360
|
-
|
|
361
|
-
|
|
465
|
+
if (await import_fs.default.promises.stat(harPath).then(() => true).catch(() => false)) {
|
|
466
|
+
const rawHar = await import_fs.default.promises.readFile(harPath, "utf-8");
|
|
467
|
+
zipFile.addBuffer(Buffer.from(filterHarEntries(rawHar), "utf-8"), "skyramp_network.har");
|
|
468
|
+
} else {
|
|
362
469
|
zipFile.addBuffer(Buffer.from('{"log":{"version":"1.2","creator":{"name":"Playwright","version":"1.0"},"entries":[]}}', "utf-8"), "skyramp_network.har");
|
|
470
|
+
}
|
|
363
471
|
zipFile.end();
|
|
364
472
|
await import_fs.default.promises.mkdir(import_path.default.dirname(outputZipPath), { recursive: true });
|
|
365
473
|
zipFile.outputStream.pipe(import_fs.default.createWriteStream(outputZipPath)).on("close", () => done.resolve()).on("error", (e) => done.reject(e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.1.0-rc.2",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
56
56
|
"@playwright/test": "^1.55.0",
|
|
57
|
-
"@skyramp/skyramp": "1.3.
|
|
57
|
+
"@skyramp/skyramp": "1.3.19",
|
|
58
58
|
"dockerode": "^4.0.6",
|
|
59
59
|
"fast-glob": "^3.3.3",
|
|
60
60
|
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.9.0.tgz",
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"global": "Usage: playwright-cli <command> [options]\nCommands:\n click <ref> perform click on a web page\n close close the page\n dblclick <ref> perform double click on a web page\n console <level> returns all console messages\n drag <startRef> <endRef> perform drag and drop between two elements\n evaluate <function> <ref> evaluate javascript expression on page or element\n upload-file upload one or multiple files\n handle-dialog <accept> <promptText> handle a dialog\n hover <ref> hover over element on page\n open <url> open url\n go-back go back to the previous page\n network-requests returns all network requests since loading the page\n press <key> press a key on the keyboard\n resize <width> <height> resize the browser window\n run-code <code> run playwright code snippet\n select-option <ref> <values> select an option in a dropdown\n snapshot capture accessibility snapshot of the current page, this is better than screenshot\n screenshot <ref> take a screenshot of the current page. you can't perform actions based on the screenshot, use browser_snapshot for actions.\n type <text> type text into editable element\n wait-for wait for text to appear or disappear or a specified time to pass\n tab <action> <index> close a browser tab\n mouse-click-xy <x> <y> click left mouse button at a given position\n mouse-drag-xy <startX> <startY> <endX> <endY> drag left mouse button to a given position\n mouse-move-xy <x> <y> move mouse to a given position\n pdf-save save page as pdf\n start-tracing start trace recording\n stop-tracing stop trace recording",
|
|
3
|
-
"commands": {
|
|
4
|
-
"click": "playwright-cli click <ref>\n\nPerform click on a web page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\nOptions:\n --button\tbutton to click, defaults to left\n --modifiers\tmodifier keys to press",
|
|
5
|
-
"close": "playwright-cli close \n\nClose the page\n",
|
|
6
|
-
"dblclick": "playwright-cli dblclick <ref>\n\nPerform double click on a web page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\nOptions:\n --button\tbutton to click, defaults to left\n --modifiers\tmodifier keys to press",
|
|
7
|
-
"console": "playwright-cli console <level>\n\nReturns all console messages\n\nArguments:\n <level>\tLevel of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".",
|
|
8
|
-
"drag": "playwright-cli drag <startRef> <endRef>\n\nPerform drag and drop between two elements\n\nArguments:\n <startRef>\tExact source element reference from the page snapshot\n <endRef>\tExact target element reference from the page snapshot\nOptions:\n --headed\trun browser in headed mode",
|
|
9
|
-
"evaluate": "playwright-cli evaluate <function> <ref>\n\nEvaluate JavaScript expression on page or element\n\nArguments:\n <function>\t() => { /* code */ } or (element) => { /* code */ } when element is provided\n <ref>\tExact target element reference from the page snapshot",
|
|
10
|
-
"upload-file": "playwright-cli upload-file \n\nUpload one or multiple files\n\nOptions:\n --paths\tthe absolute paths to the files to upload. can be single file or multiple files. if omitted, file chooser is cancelled.",
|
|
11
|
-
"handle-dialog": "playwright-cli handle-dialog <accept> <promptText>\n\nHandle a dialog\n\nArguments:\n <accept>\tWhether to accept the dialog.\n <promptText>\tThe text of the prompt in case of a prompt dialog.",
|
|
12
|
-
"hover": "playwright-cli hover <ref>\n\nHover over element on page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot",
|
|
13
|
-
"open": "playwright-cli open <url>\n\nOpen URL\n\nArguments:\n <url>\tThe URL to navigate to\nOptions:\n --headed\trun browser in headed mode",
|
|
14
|
-
"go-back": "playwright-cli go-back \n\nGo back to the previous page\n",
|
|
15
|
-
"network-requests": "playwright-cli network-requests \n\nReturns all network requests since loading the page\n\nOptions:\n --includeStatic\twhether to include successful static resources like images, fonts, scripts, etc. defaults to false.",
|
|
16
|
-
"press": "playwright-cli press <key>\n\nPress a key on the keyboard\n\nArguments:\n <key>\tName of the key to press or a character to generate, such as `ArrowLeft` or `a`",
|
|
17
|
-
"resize": "playwright-cli resize <width> <height>\n\nResize the browser window\n\nArguments:\n <width>\tWidth of the browser window\n <height>\tHeight of the browser window",
|
|
18
|
-
"run-code": "playwright-cli run-code <code>\n\nRun Playwright code snippet\n\nArguments:\n <code>\tA JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction.",
|
|
19
|
-
"select-option": "playwright-cli select-option <ref> <values>\n\nSelect an option in a dropdown\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\n <values>\tArray of values to select in the dropdown. This can be a single value or multiple values.",
|
|
20
|
-
"snapshot": "playwright-cli snapshot \n\nCapture accessibility snapshot of the current page, this is better than screenshot\n\nOptions:\n --filename\tsave snapshot to markdown file instead of returning it in the response.",
|
|
21
|
-
"screenshot": "playwright-cli screenshot <ref>\n\nTake a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.\n\nArguments:\n <ref>\tExact target element reference from the page snapshot.\nOptions:\n --filename\tfile name to save the screenshot to. defaults to `page-{timestamp}.{png|jpeg}` if not specified.\n --fullPage\twhen true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.",
|
|
22
|
-
"type": "playwright-cli type <text>\n\nType text into editable element\n\nArguments:\n <text>\tText to type into the element\nOptions:\n --submit\twhether to submit entered text (press enter after)",
|
|
23
|
-
"wait-for": "playwright-cli wait-for \n\nWait for text to appear or disappear or a specified time to pass\n\nOptions:\n --time\tthe time to wait in seconds\n --text\tthe text to wait for\n --textGone\tthe text to wait for to disappear",
|
|
24
|
-
"tab": "playwright-cli tab <action> <index>\n\nClose a browser tab\n\nArguments:\n <action>\tAction to perform on tabs, 'list' | 'new' | 'close' | 'select'\n <index>\tTab index. If omitted, current tab is closed.",
|
|
25
|
-
"mouse-click-xy": "playwright-cli mouse-click-xy <x> <y>\n\nClick left mouse button at a given position\n\nArguments:\n <x>\tX coordinate\n <y>\tY coordinate",
|
|
26
|
-
"mouse-drag-xy": "playwright-cli mouse-drag-xy <startX> <startY> <endX> <endY>\n\nDrag left mouse button to a given position\n\nArguments:\n <startX>\tStart X coordinate\n <startY>\tStart Y coordinate\n <endX>\tEnd X coordinate\n <endY>\tEnd Y coordinate",
|
|
27
|
-
"mouse-move-xy": "playwright-cli mouse-move-xy <x> <y>\n\nMove mouse to a given position\n\nArguments:\n <x>\tX coordinate\n <y>\tY coordinate",
|
|
28
|
-
"pdf-save": "playwright-cli pdf-save \n\nSave page as PDF\n\nOptions:\n --filename\tfile name to save the pdf to. defaults to `page-{timestamp}.pdf` if not specified.",
|
|
29
|
-
"start-tracing": "playwright-cli start-tracing \n\nStart trace recording\n",
|
|
30
|
-
"stop-tracing": "playwright-cli stop-tracing \n\nStop trace recording\n"
|
|
31
|
-
}
|
|
32
|
-
}
|