@skyramp/mcp 0.0.64-rc.8 → 0.0.64
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/index.js +2 -0
- package/build/playwright/registerPlaywrightTools.js +1 -1
- package/build/playwright/traceRecordingPrompt.js +9 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +96 -34
- package/build/prompts/test-maintenance/enhanceAssertionSection.js +99 -0
- package/build/prompts/test-recommendation/recommendationSections.js +24 -9
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +96 -27
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +239 -2
- package/build/prompts/testbot/testbot-prompts.js +185 -120
- package/build/services/TestDiscoveryService.js +23 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/services/TestGenerationService.js +83 -12
- package/build/services/TestGenerationService.test.js +111 -2
- package/build/tool-phase-coverage.test.js +8 -2
- package/build/tool-phases.js +11 -13
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +203 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -73
- package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -61
- package/build/tools/submitReportTool.js +11 -3
- package/build/tools/submitReportTool.test.js +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +14 -4
- package/build/types/RepositoryAnalysis.js +1 -0
- package/build/utils/scenarioDrafting.js +121 -11
- package/build/utils/scenarioDrafting.test.js +266 -3
- package/node_modules/playwright/ThirdPartyNotices.txt +679 -3093
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +52 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +290 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +60 -0
- package/package.json +2 -2
- package/build/tools/test-recommendation/recommendTestsTool.js +0 -274
|
@@ -0,0 +1,52 @@
|
|
|
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 assertTool_exports = {};
|
|
20
|
+
__export(assertTool_exports, {
|
|
21
|
+
assertMcpTool: () => assertMcpTool,
|
|
22
|
+
assertToolSchema: () => assertToolSchema
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(assertTool_exports);
|
|
25
|
+
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
|
|
26
|
+
var import_tool = require("../sdk/tool");
|
|
27
|
+
const assertToolSchema = {
|
|
28
|
+
name: "browser_assert",
|
|
29
|
+
title: "Assert element state",
|
|
30
|
+
description: [
|
|
31
|
+
"Assert that an element has expected text, value, or is visible.",
|
|
32
|
+
"Use this after key actions to verify the page state is correct.",
|
|
33
|
+
"The assertion is recorded in the trace for test generation.",
|
|
34
|
+
'Types: "text" checks element text content, "value" checks input field value.'
|
|
35
|
+
].join(" "),
|
|
36
|
+
inputSchema: import_mcpBundle.z.object({
|
|
37
|
+
type: import_mcpBundle.z.enum(["text", "value"]).describe('Type of assertion: "text" for text content, "value" for input field value'),
|
|
38
|
+
ref: import_mcpBundle.z.string().describe("Element reference from the latest snapshot"),
|
|
39
|
+
element: import_mcpBundle.z.string().describe("Human-readable description of the element being asserted"),
|
|
40
|
+
expected: import_mcpBundle.z.string().describe("Expected text or value to assert"),
|
|
41
|
+
substring: import_mcpBundle.z.boolean().optional().default(true).describe("For text assertions: match as substring (true) or exact match (false)")
|
|
42
|
+
}),
|
|
43
|
+
type: "readOnly"
|
|
44
|
+
};
|
|
45
|
+
function assertMcpTool() {
|
|
46
|
+
return (0, import_tool.toMcpTool)(assertToolSchema);
|
|
47
|
+
}
|
|
48
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
49
|
+
0 && (module.exports = {
|
|
50
|
+
assertMcpTool,
|
|
51
|
+
assertToolSchema
|
|
52
|
+
});
|
|
@@ -42,6 +42,7 @@ var import_response = require("../browser/response");
|
|
|
42
42
|
var import_log = require("../log");
|
|
43
43
|
var import_skyRampExport = require("../test/skyRampExport");
|
|
44
44
|
var import_exportTool = require("./exportTool");
|
|
45
|
+
var import_assertTool = require("./assertTool");
|
|
45
46
|
var import_types = require("./types");
|
|
46
47
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
47
48
|
class TraceRecordingBackend {
|
|
@@ -69,6 +70,7 @@ class TraceRecordingBackend {
|
|
|
69
70
|
serviceWorkers: "block"
|
|
70
71
|
}
|
|
71
72
|
},
|
|
73
|
+
capabilities: ["testing"],
|
|
72
74
|
outputDir: this._tempDir,
|
|
73
75
|
timeouts: {
|
|
74
76
|
action: 15e3,
|
|
@@ -84,7 +86,7 @@ class TraceRecordingBackend {
|
|
|
84
86
|
}
|
|
85
87
|
async listTools() {
|
|
86
88
|
const browserTools = await this._browserBackend.listTools();
|
|
87
|
-
return [...browserTools, (0, import_exportTool.exportZipMcpTool)()];
|
|
89
|
+
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)()];
|
|
88
90
|
}
|
|
89
91
|
async callTool(name, args, progress) {
|
|
90
92
|
if (!this._initialized)
|
|
@@ -100,7 +102,14 @@ class TraceRecordingBackend {
|
|
|
100
102
|
rootPath: this._outputDir,
|
|
101
103
|
harPath: this._harPath
|
|
102
104
|
});
|
|
103
|
-
|
|
105
|
+
const exportResult = await handler(parsed);
|
|
106
|
+
if (!exportResult.isError)
|
|
107
|
+
this._trackedActions = [];
|
|
108
|
+
return exportResult;
|
|
109
|
+
}
|
|
110
|
+
if (name === import_assertTool.assertToolSchema.name) {
|
|
111
|
+
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
112
|
+
return this._handleAssert(parsed);
|
|
104
113
|
}
|
|
105
114
|
if (name === "browser_select_option") {
|
|
106
115
|
const result2 = await this._handleSelectOption(args || {});
|
|
@@ -143,6 +152,36 @@ class TraceRecordingBackend {
|
|
|
143
152
|
const result = await this._browserBackend.callTool("browser_select_option", args);
|
|
144
153
|
const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
|
|
145
154
|
if (!result.isError) {
|
|
155
|
+
const code = (0, import_response.parseResponse)(result)?.code ?? "";
|
|
156
|
+
const hasCssSelector = code.includes("page.locator('select") || code.includes('page.locator("select') || code.includes(".selectOption(") && !code.includes("getByTestId") && !code.includes("getByRole") && !code.includes("getByLabel");
|
|
157
|
+
if (hasCssSelector && args.ref) {
|
|
158
|
+
traceDebug("selectOption used CSS selector, trying to resolve a better one via hover");
|
|
159
|
+
const hoverResult = await this._browserBackend.callTool("browser_hover", {
|
|
160
|
+
element: args.element || "select",
|
|
161
|
+
ref: args.ref
|
|
162
|
+
});
|
|
163
|
+
if (!hoverResult.isError) {
|
|
164
|
+
const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
|
|
165
|
+
const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
166
|
+
if (locatorMatch) {
|
|
167
|
+
const locatorExpr = locatorMatch[1].trim();
|
|
168
|
+
const parsed = this._codeToLocator(locatorExpr);
|
|
169
|
+
if (parsed && parsed.locator.kind !== "text") {
|
|
170
|
+
const values2 = args.values || [];
|
|
171
|
+
const selectCode = `await page.${locatorExpr}.selectOption(${JSON.stringify(values2.length === 1 ? values2[0] : values2)});`;
|
|
172
|
+
traceDebug(`Improved select selector: ${selectCode}`);
|
|
173
|
+
const timestamp = Date.now();
|
|
174
|
+
this._trackedActions.push({
|
|
175
|
+
toolName: "browser_select_option",
|
|
176
|
+
args,
|
|
177
|
+
code: selectCode,
|
|
178
|
+
timestamp
|
|
179
|
+
});
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
146
185
|
this._maybeTrackAction("browser_select_option", args, result);
|
|
147
186
|
return result;
|
|
148
187
|
}
|
|
@@ -156,19 +195,24 @@ class TraceRecordingBackend {
|
|
|
156
195
|
const preSnapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
157
196
|
if (preSnapResult.isError)
|
|
158
197
|
return preSnapResult;
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
let snapText = preSnapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
199
|
+
const dropdownAlreadyOpen = /listbox[^\n]*\[ref=\w+\]/.test(snapText) && /option\s+"/.test(snapText);
|
|
200
|
+
if (!dropdownAlreadyOpen) {
|
|
201
|
+
const comboboxes = [...snapText.matchAll(/combobox[^\n]*\[ref=(\w+)\]/g)];
|
|
202
|
+
const clickableCombo = comboboxes.find((m) => m[0].includes("cursor=pointer"));
|
|
203
|
+
const comboRef = clickableCombo?.[1] || args.ref;
|
|
204
|
+
const clickResult = await this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef });
|
|
205
|
+
if (clickResult.isError)
|
|
206
|
+
return { content: [{ type: "text", text: `### Error
|
|
166
207
|
Failed to open custom dropdown. The dropdown modal may not be visible yet. Try calling browser_snapshot first to check the page state, then ensure the form/modal is open before selecting an option.` }], isError: true };
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
208
|
+
this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult);
|
|
209
|
+
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
210
|
+
if (snapResult.isError)
|
|
211
|
+
return snapResult;
|
|
212
|
+
snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
213
|
+
} else {
|
|
214
|
+
traceDebug("Dropdown already open, skipping combobox click");
|
|
215
|
+
}
|
|
172
216
|
const targetValue = values[0];
|
|
173
217
|
const candidates = [targetValue];
|
|
174
218
|
const lower = targetValue.toLowerCase();
|
|
@@ -196,12 +240,243 @@ Failed to open custom dropdown. The dropdown modal may not be visible yet. Try c
|
|
|
196
240
|
Opened the custom dropdown but could not find a matching option for "${targetValue}". Available options:
|
|
197
241
|
${optionLines}` }], isError: true };
|
|
198
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle browser_assert by using browser_hover to resolve the ref to a
|
|
245
|
+
* proper Playwright selector (testid > role > text), then verify the
|
|
246
|
+
* assertion against the snapshot data.
|
|
247
|
+
*/
|
|
248
|
+
async _handleAssert(params) {
|
|
249
|
+
const timestamp = Date.now();
|
|
250
|
+
if (!params.expected)
|
|
251
|
+
return { content: [{ type: "text", text: '### Error\n"expected" parameter is required.' }], isError: true };
|
|
252
|
+
const hoverResult = await this._browserBackend.callTool("browser_hover", {
|
|
253
|
+
element: params.element,
|
|
254
|
+
ref: params.ref
|
|
255
|
+
});
|
|
256
|
+
if (hoverResult.isError) {
|
|
257
|
+
const errText = hoverResult.content?.[0]?.type === "text" ? hoverResult.content[0].text : "";
|
|
258
|
+
return { content: [{ type: "text", text: `### Assertion Failed
|
|
259
|
+
Could not resolve element ref=${params.ref}. ${errText}` }], isError: true };
|
|
260
|
+
}
|
|
261
|
+
const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
|
|
262
|
+
const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
263
|
+
if (!locatorMatch) {
|
|
264
|
+
return { content: [{ type: "text", text: `### Assertion Failed
|
|
265
|
+
Could not extract selector from hover result.` }], isError: true };
|
|
266
|
+
}
|
|
267
|
+
const locatorExpr = locatorMatch[1].trim();
|
|
268
|
+
let parsed = this._codeToLocator(locatorExpr);
|
|
269
|
+
if (!parsed) {
|
|
270
|
+
return { content: [{ type: "text", text: `### Assertion Failed
|
|
271
|
+
Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
272
|
+
}
|
|
273
|
+
const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
|
|
274
|
+
const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
|
|
275
|
+
const snapLines = snapText.split("\n");
|
|
276
|
+
const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
|
|
277
|
+
const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
|
|
278
|
+
const elementText = textMatch?.[1] || "";
|
|
279
|
+
const valueMatch = refLine.match(/\]:\s*(.+)$/);
|
|
280
|
+
const elementValue = valueMatch?.[1]?.trim() || "";
|
|
281
|
+
if (parsed.locator.kind === "text") {
|
|
282
|
+
const improved = await this._improveTextSelector(snapLines, params.ref, parsed);
|
|
283
|
+
if (improved) {
|
|
284
|
+
traceDebug(`Improved ambiguous text selector to: ${improved.selector}`);
|
|
285
|
+
parsed = improved;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
let passed = false;
|
|
289
|
+
let details = "";
|
|
290
|
+
if (params.type === "text") {
|
|
291
|
+
passed = elementText.includes(params.expected) || elementValue.includes(params.expected);
|
|
292
|
+
details = passed ? `Text assertion passed: "${params.element}" contains "${params.expected}".` : `Text assertion FAILED: "${params.element}" has text "${elementText}"${elementValue ? ` / value "${elementValue}"` : ""}, expected "${params.expected}".`;
|
|
293
|
+
} else {
|
|
294
|
+
passed = elementValue === params.expected || elementValue.includes(params.expected);
|
|
295
|
+
details = passed ? `Value assertion passed: "${params.element}" has value "${params.expected}".` : `Value assertion FAILED: "${params.element}" has value "${elementValue}", expected "${params.expected}".`;
|
|
296
|
+
}
|
|
297
|
+
if (passed) {
|
|
298
|
+
const selectorText = parsed.locator.kind === "text" ? parsed.locator.body : parsed.locator.next?.kind === "text" ? parsed.locator.next.body : parsed.locator.options?.name ?? null;
|
|
299
|
+
if (selectorText && selectorText === params.expected) {
|
|
300
|
+
traceDebug(`Skipped tautological assertion: selector already matches "${params.expected}"`);
|
|
301
|
+
} else {
|
|
302
|
+
const assertName = params.type === "value" ? "assertValue" : "assertText";
|
|
303
|
+
this._trackedActions.push({
|
|
304
|
+
toolName: "browser_assert",
|
|
305
|
+
args: { type: params.type, ref: params.ref, expected: params.expected },
|
|
306
|
+
code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
|
|
307
|
+
timestamp
|
|
308
|
+
});
|
|
309
|
+
traceDebug(`Assert: ${assertName} with selector ${parsed.selector}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: `### ${passed ? "Assertion Passed" : "Assertion Failed"}
|
|
314
|
+
${details}` }]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** Convert a Playwright locator expression to a selector + locator object for JSONL. */
|
|
318
|
+
_codeToLocator(expr) {
|
|
319
|
+
const testidMatch = expr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
320
|
+
if (testidMatch) {
|
|
321
|
+
return {
|
|
322
|
+
selector: `internal:testid=[data-testid="${testidMatch[1]}"s]`,
|
|
323
|
+
locator: { kind: "test-id", body: testidMatch[1], options: {} }
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const roleMatch = expr.match(/getByRole\(\s*['"]([^'"]+)['"](?:\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"][^}]*\})?\s*\)/);
|
|
327
|
+
if (roleMatch) {
|
|
328
|
+
return {
|
|
329
|
+
selector: roleMatch[2] ? `internal:role=${roleMatch[1]}[name="${roleMatch[2]}"i]` : `internal:role=${roleMatch[1]}`,
|
|
330
|
+
locator: { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, ...roleMatch[2] ? { name: roleMatch[2] } : {} } }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const labelMatch = expr.match(/getByLabel\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
334
|
+
if (labelMatch) {
|
|
335
|
+
return {
|
|
336
|
+
selector: `internal:label="${labelMatch[1]}"i`,
|
|
337
|
+
locator: { kind: "label", body: labelMatch[1], options: { exact: false } }
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const textMatch = expr.match(/getByText\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
341
|
+
if (textMatch) {
|
|
342
|
+
return {
|
|
343
|
+
selector: `internal:text="${textMatch[1]}"i`,
|
|
344
|
+
locator: { kind: "text", body: textMatch[1], options: { exact: false } }
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const ariaRefMatch = expr.match(/locator\(\s*['"]aria-ref=(\w+)['"]\s*\)/);
|
|
348
|
+
if (ariaRefMatch) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* When hover resolves to a text-based locator (e.g. getByText("$849.98")),
|
|
355
|
+
* check if that text appears multiple times in the snapshot (ambiguous) and
|
|
356
|
+
* hover ancestor refs to find a parent with a test-id, producing a chained
|
|
357
|
+
* selector like `testid >> text` that uniquely identifies the element.
|
|
358
|
+
*/
|
|
359
|
+
async _improveTextSelector(snapLines, ref, original) {
|
|
360
|
+
const textBody = original.locator.body;
|
|
361
|
+
const occurrences = snapLines.filter((l) => l.includes(textBody)).length;
|
|
362
|
+
if (occurrences <= 1)
|
|
363
|
+
return null;
|
|
364
|
+
traceDebug(`Text "${textBody}" appears ${occurrences} times in snapshot \u2014 looking for parent test-id`);
|
|
365
|
+
const refPattern = `[ref=${ref}]`;
|
|
366
|
+
const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
|
|
367
|
+
if (refIdx < 0)
|
|
368
|
+
return null;
|
|
369
|
+
const refIndent = snapLines[refIdx].match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
370
|
+
for (let i = refIdx - 1; i >= 0; i--) {
|
|
371
|
+
const line = snapLines[i];
|
|
372
|
+
const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
373
|
+
if (indent >= refIndent)
|
|
374
|
+
continue;
|
|
375
|
+
const ancestorRefMatch = line.match(/\[ref=(\w+)\]/);
|
|
376
|
+
if (!ancestorRefMatch)
|
|
377
|
+
continue;
|
|
378
|
+
const ancestorRef = ancestorRefMatch[1];
|
|
379
|
+
try {
|
|
380
|
+
const ancestorHover = await this._browserBackend.callTool("browser_hover", {
|
|
381
|
+
element: "parent element",
|
|
382
|
+
ref: ancestorRef
|
|
383
|
+
});
|
|
384
|
+
if (!ancestorHover.isError) {
|
|
385
|
+
const ancestorCode = (0, import_response.parseResponse)(ancestorHover)?.code ?? "";
|
|
386
|
+
const ancestorLocatorMatch = ancestorCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
|
|
387
|
+
if (ancestorLocatorMatch) {
|
|
388
|
+
const ancestorExpr = ancestorLocatorMatch[1].trim();
|
|
389
|
+
const testidMatch = ancestorExpr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
390
|
+
if (testidMatch) {
|
|
391
|
+
const testid = testidMatch[1];
|
|
392
|
+
const chainedSelector = `internal:testid=[data-testid="${testid}"s] >> internal:text="${textBody}"i`;
|
|
393
|
+
traceDebug(`Improved to chained selector: ${chainedSelector}`);
|
|
394
|
+
return {
|
|
395
|
+
selector: chainedSelector,
|
|
396
|
+
locator: {
|
|
397
|
+
kind: "test-id",
|
|
398
|
+
body: testid,
|
|
399
|
+
options: {},
|
|
400
|
+
next: { kind: "text", body: textBody, options: { exact: false } }
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
if (indent === 0)
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
static {
|
|
414
|
+
/** Extract selector and locator info from a snapshot line for assertion tracking. */
|
|
415
|
+
// Roles that map to valid Playwright getByRole() selectors.
|
|
416
|
+
// 'generic', 'paragraph', etc. are NOT valid for getByRole.
|
|
417
|
+
this.ASSERTABLE_ROLES = /* @__PURE__ */ new Set([
|
|
418
|
+
"button",
|
|
419
|
+
"link",
|
|
420
|
+
"heading",
|
|
421
|
+
"textbox",
|
|
422
|
+
"checkbox",
|
|
423
|
+
"radio",
|
|
424
|
+
"combobox",
|
|
425
|
+
"listbox",
|
|
426
|
+
"option",
|
|
427
|
+
"tab",
|
|
428
|
+
"tabpanel",
|
|
429
|
+
"dialog",
|
|
430
|
+
"alert",
|
|
431
|
+
"img",
|
|
432
|
+
"navigation",
|
|
433
|
+
"banner",
|
|
434
|
+
"main",
|
|
435
|
+
"form",
|
|
436
|
+
"table",
|
|
437
|
+
"row",
|
|
438
|
+
"cell",
|
|
439
|
+
"columnheader",
|
|
440
|
+
"rowheader",
|
|
441
|
+
"spinbutton",
|
|
442
|
+
"slider",
|
|
443
|
+
"switch",
|
|
444
|
+
"menu",
|
|
445
|
+
"menuitem"
|
|
446
|
+
]);
|
|
447
|
+
}
|
|
448
|
+
_extractLocatorForRef(refLine) {
|
|
449
|
+
const testidMatch = refLine.match(/testid="([^"]+)"/);
|
|
450
|
+
if (testidMatch) {
|
|
451
|
+
return {
|
|
452
|
+
selector: `internal:testid=[data-testid="${testidMatch[1]}"s]`,
|
|
453
|
+
locator: { kind: "test-id", body: testidMatch[1], options: {} }
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const roleMatch = refLine.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
|
|
457
|
+
if (roleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(roleMatch[1])) {
|
|
458
|
+
const role = roleMatch[1];
|
|
459
|
+
const name = roleMatch[2];
|
|
460
|
+
return {
|
|
461
|
+
selector: `internal:role=${role}[name="${name}"i]`,
|
|
462
|
+
locator: { kind: "role", body: role, options: { attrs: [], exact: false, name } }
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (roleMatch && roleMatch[2]) {
|
|
466
|
+
const text = roleMatch[2];
|
|
467
|
+
return {
|
|
468
|
+
selector: `internal:text="${text}"i`,
|
|
469
|
+
locator: { kind: "text", body: text, options: { exact: false } }
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
199
474
|
_maybeTrackAction(toolName, args, result, timestamp) {
|
|
200
475
|
if (result.isError)
|
|
201
476
|
return;
|
|
202
477
|
if (toolName === "browser_press_key") {
|
|
203
478
|
const key = String(args.key || "").toLowerCase();
|
|
204
|
-
if (key.includes("control+") || key.includes("meta+"))
|
|
479
|
+
if (key.includes("control+") || key.includes("meta+") || key === "tab" || key === "enter" || key === "escape")
|
|
205
480
|
return;
|
|
206
481
|
}
|
|
207
482
|
const parsed = (0, import_response.parseResponse)(result);
|
|
@@ -229,6 +229,58 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
|
|
|
229
229
|
return JSON.stringify({ name: "click", selector, button: "left", modifiers: 0, clickCount: 1, locator: locatorObj, ...base });
|
|
230
230
|
return null;
|
|
231
231
|
}
|
|
232
|
+
function assertActionToJsonl(action, pageGuid, timestamp) {
|
|
233
|
+
const { args, code } = action;
|
|
234
|
+
const assertType = args.type;
|
|
235
|
+
const base = {
|
|
236
|
+
signals: [],
|
|
237
|
+
timestamp: String(timestamp),
|
|
238
|
+
pageGuid,
|
|
239
|
+
pageAlias: PAGE_ALIAS,
|
|
240
|
+
framePath: FRAME_PATH
|
|
241
|
+
};
|
|
242
|
+
const parts = code.split(":");
|
|
243
|
+
const selectorPart = parts[1] || "";
|
|
244
|
+
let selector = "";
|
|
245
|
+
let expected = "";
|
|
246
|
+
let substring = true;
|
|
247
|
+
if (assertType === "visible") {
|
|
248
|
+
selector = parts.slice(1).join(":");
|
|
249
|
+
} else if (assertType === "text") {
|
|
250
|
+
substring = parts[parts.length - 1] === "true";
|
|
251
|
+
expected = parts[parts.length - 2] || "";
|
|
252
|
+
selector = parts.slice(1, parts.length - 2).join(":");
|
|
253
|
+
} else if (assertType === "value") {
|
|
254
|
+
expected = parts[parts.length - 1] || "";
|
|
255
|
+
selector = parts.slice(1, parts.length - 1).join(":");
|
|
256
|
+
}
|
|
257
|
+
const locator = selectorToLocator(selector);
|
|
258
|
+
switch (assertType) {
|
|
259
|
+
case "text":
|
|
260
|
+
return JSON.stringify({ name: "assertText", selector, text: expected, substring, locator, ...base });
|
|
261
|
+
case "visible":
|
|
262
|
+
return JSON.stringify({ name: "assertVisible", selector, locator, ...base });
|
|
263
|
+
case "value":
|
|
264
|
+
return JSON.stringify({ name: "assertValue", selector, value: expected, locator, ...base });
|
|
265
|
+
default:
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function selectorToLocator(selector) {
|
|
270
|
+
const testidMatch = selector.match(/internal:testid=\[data-testid="([^"]+)"/);
|
|
271
|
+
if (testidMatch)
|
|
272
|
+
return { kind: "test-id", body: testidMatch[1], options: {} };
|
|
273
|
+
const roleMatch = selector.match(/internal:role=(\w+)\[name="([^"]+)"([is])?\]/);
|
|
274
|
+
if (roleMatch)
|
|
275
|
+
return { kind: "role", body: roleMatch[1], options: { attrs: [], exact: roleMatch[3] === "s", name: roleMatch[2] } };
|
|
276
|
+
const roleOnlyMatch = selector.match(/internal:role=(\w+)$/);
|
|
277
|
+
if (roleOnlyMatch)
|
|
278
|
+
return { kind: "role", body: roleOnlyMatch[1], options: { attrs: [] } };
|
|
279
|
+
const textMatch = selector.match(/internal:text="([^"]+)"([is])?/);
|
|
280
|
+
if (textMatch)
|
|
281
|
+
return { kind: "text", body: textMatch[1], options: { exact: textMatch[2] === "s" } };
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
232
284
|
function fillFormToJsonl(action, pageGuid, baseTimestamp) {
|
|
233
285
|
const { args } = action;
|
|
234
286
|
if (!args.fields)
|
|
@@ -280,6 +332,14 @@ function buildJsonlContent(actions, browserName, harPath) {
|
|
|
280
332
|
actionCount += formLines.length;
|
|
281
333
|
continue;
|
|
282
334
|
}
|
|
335
|
+
if (action.toolName === "browser_assert") {
|
|
336
|
+
const assertLine = assertActionToJsonl(action, pageGuid, action.timestamp);
|
|
337
|
+
if (assertLine) {
|
|
338
|
+
lines.push(assertLine);
|
|
339
|
+
actionCount++;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
283
343
|
const line = trackedActionToJsonl(action, pageGuid, action.timestamp);
|
|
284
344
|
if (line) {
|
|
285
345
|
lines.push(line);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.64
|
|
3
|
+
"version": "0.0.64",
|
|
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.17",
|
|
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",
|