@skyramp/mcp 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/playwright/traceRecordingPrompt.js +2 -0
- package/build/prompts/testbot/testbot-prompts.js +1 -0
- package/node_modules/playwright/lib/mcp/browser/tools/screenshot.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +45 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +6 -22
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +48 -0
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
|
@@ -54,6 +54,8 @@ Call \`browser_assert\` when assertions are needed. Always provide the \`expecte
|
|
|
54
54
|
- \`type: "text"\` — verify an element contains expected text
|
|
55
55
|
- \`type: "value"\` — verify an input field has an expected value
|
|
56
56
|
|
|
57
|
+
**Visual / screenshot assertions.** When the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (as opposed to its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline image on every run. Do NOT use \`browser_take_screenshot\` for this: it only captures a throwaway image, is dropped at export (it produces no assertion), and will NOT appear in the generated test. Use \`browser_take_screenshot\` only to view the page yourself while recording — never to record a visual check. \`filename\` is optional: if the instruction names a baseline file (e.g. \`page-001.png\`) pass it as \`filename\`, otherwise omit it and \`browser_visual_snapshot\` auto-generates one per \`snapshotType\` (\`page-NNN.png\` for a page, \`el-NNN.png\` for an element, \`region-NNN.png\` for a region).
|
|
58
|
+
|
|
57
59
|
### Tips
|
|
58
60
|
- **Custom dropdowns (Radix, MUI, etc.)**: click the combobox trigger → \`browser_snapshot\` → click the option. Do NOT use \`browser_select_option\` — it only works on native \`<select>\` elements.
|
|
59
61
|
|
|
@@ -462,6 +462,7 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
462
462
|
- \`skyramp_export_zip\` outputPath: \`${repositoryPath}/.skyramp/<test_name>_trace.zip\`
|
|
463
463
|
- \`skyramp_ui_test_generation\`: set \`modularizeCode: false\`
|
|
464
464
|
- **\`browser_assert\` — MANDATORY**: at least one per page navigated. Call multiple assertions in the same tool call batch when checking independent elements. If you navigate to 2 pages, assert on both. Each assertion should verify a business outcome (state change, computed value, error condition) — not just that an element is visible.
|
|
465
|
+
- **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
|
|
465
466
|
- **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
|
|
466
467
|
If \`browser_navigate\` fails (app not running / connection refused), move to \`additionalRecommendations\` with the failure reason.
|
|
467
468
|
Record at most 2-3 UI traces per run to stay within tool call budget. Quality over quantity: 1 great test is better than 3 mediocre ones — do not pad to reach the count.
|
|
@@ -40,7 +40,7 @@ const screenshot = (0, import_tool.defineTabTool)({
|
|
|
40
40
|
schema: {
|
|
41
41
|
name: "browser_take_screenshot",
|
|
42
42
|
title: "Take a screenshot",
|
|
43
|
-
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot
|
|
43
|
+
description: `Take a diagnostic screenshot of the current page for your own inspection while recording. This is NOT a test assertion: the image is not recorded into the trace and will NOT appear in the generated test (it is dropped at export). You can't perform actions based on the screenshot \u2014 use browser_snapshot for actions. To ASSERT how a page/element/region looks (visual regression \u2014 e.g. an instruction to "take a screenshot" or capture a baseline), use browser_visual_snapshot instead, which records a toHaveScreenshot() assertion.`,
|
|
44
44
|
inputSchema: screenshotSchema,
|
|
45
45
|
type: "readOnly"
|
|
46
46
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
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 resultCode_exports = {};
|
|
20
|
+
__export(resultCode_exports, {
|
|
21
|
+
rewriteResultCodeText: () => rewriteResultCodeText
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(resultCode_exports);
|
|
24
|
+
function rewriteResultCodeText(text, newCode) {
|
|
25
|
+
if (!text.includes("await page."))
|
|
26
|
+
return null;
|
|
27
|
+
const fence = text.match(/```(?:js|javascript)\n/);
|
|
28
|
+
if (fence && fence.index !== void 0) {
|
|
29
|
+
const codeStart = fence.index + fence[0].length;
|
|
30
|
+
const codeEnd = text.indexOf("\n```", codeStart);
|
|
31
|
+
if (codeEnd >= 0)
|
|
32
|
+
return text.slice(0, codeStart) + newCode + text.slice(codeEnd);
|
|
33
|
+
}
|
|
34
|
+
const lines = text.split("\n");
|
|
35
|
+
const codeLineIdx = lines.findIndex((l) => l.includes("await page."));
|
|
36
|
+
if (codeLineIdx >= 0) {
|
|
37
|
+
lines[codeLineIdx] = newCode;
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
43
|
+
0 && (module.exports = {
|
|
44
|
+
rewriteResultCodeText
|
|
45
|
+
});
|
|
@@ -48,6 +48,7 @@ var import_assertTool = require("./assertTool");
|
|
|
48
48
|
var import_assertApiRequestTool = require("./assertApiRequestTool");
|
|
49
49
|
var import_loadTraceTool = require("./loadTraceTool");
|
|
50
50
|
var import_skyRampImport = require("./skyRampImport");
|
|
51
|
+
var import_resultCode = require("./resultCode");
|
|
51
52
|
var import_mouseActionTool = require("./mouseActionTool");
|
|
52
53
|
var import_visualSnapshotTool = require("./visualSnapshotTool");
|
|
53
54
|
var import_gojsTool = require("./gojsTool");
|
|
@@ -1847,28 +1848,11 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
|
|
|
1847
1848
|
_modifyResultCode(result, newCode) {
|
|
1848
1849
|
const modified = { ...result, content: [...result.content] };
|
|
1849
1850
|
for (let i = 0; i < modified.content.length; i++) {
|
|
1850
|
-
if (modified.content[i].type
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
modified.content[i] = {
|
|
1856
|
-
...modified.content[i],
|
|
1857
|
-
text: codeBlockMatch[1] + newCode + "\n" + codeBlockMatch[3]
|
|
1858
|
-
};
|
|
1859
|
-
} else {
|
|
1860
|
-
const lines = text.split("\n");
|
|
1861
|
-
const codeLineIdx = lines.findIndex((l) => l.includes("await page."));
|
|
1862
|
-
if (codeLineIdx >= 0) {
|
|
1863
|
-
lines[codeLineIdx] = newCode;
|
|
1864
|
-
modified.content[i] = {
|
|
1865
|
-
...modified.content[i],
|
|
1866
|
-
text: lines.join("\n")
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1851
|
+
if (modified.content[i].type !== "text")
|
|
1852
|
+
continue;
|
|
1853
|
+
const rewritten = (0, import_resultCode.rewriteResultCodeText)(modified.content[i].text || "", newCode);
|
|
1854
|
+
if (rewritten !== null)
|
|
1855
|
+
modified.content[i] = { ...modified.content[i], text: rewritten };
|
|
1872
1856
|
}
|
|
1873
1857
|
return modified;
|
|
1874
1858
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_vitest = require("vitest");
|
|
3
|
+
var import_resultCode = require("../skyramp/resultCode");
|
|
4
|
+
const NEW_CODE = 'await page.locator("#viewer-cols").getByRole("button", { name: "TextBlock" }).click();';
|
|
5
|
+
function clickResult(snapshot, fence = "js") {
|
|
6
|
+
return "### Ran Playwright code\n\n```" + fence + '\nawait page.getByRole("button", { name: "TextBlock" }).click();\n```\n\n### Page state\n- Page Snapshot:\n```yaml\n' + snapshot + "\n```\n";
|
|
7
|
+
}
|
|
8
|
+
function bigSnapshot(chars) {
|
|
9
|
+
let s = "";
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (s.length < chars) {
|
|
12
|
+
s += `- text "\u5024 ${i} \u5186 ${i * 7}" [ref=e${i}]
|
|
13
|
+
`;
|
|
14
|
+
i++;
|
|
15
|
+
}
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
(0, import_vitest.describe)("rewriteResultCodeText", () => {
|
|
19
|
+
(0, import_vitest.it)("replaces code inside a ```js fence and keeps the snapshot intact", () => {
|
|
20
|
+
const snapshot = bigSnapshot(2e3);
|
|
21
|
+
const out = (0, import_resultCode.rewriteResultCodeText)(clickResult(snapshot, "js"), NEW_CODE);
|
|
22
|
+
(0, import_vitest.expect)(out).toContain(NEW_CODE);
|
|
23
|
+
(0, import_vitest.expect)(out).not.toContain('await page.getByRole("button", { name: "TextBlock" }).click();');
|
|
24
|
+
(0, import_vitest.expect)(out).toContain(snapshot);
|
|
25
|
+
(0, import_vitest.expect)(out).toContain("```yaml");
|
|
26
|
+
});
|
|
27
|
+
(0, import_vitest.it)("also handles a ```javascript fence", () => {
|
|
28
|
+
const out = (0, import_resultCode.rewriteResultCodeText)(clickResult(bigSnapshot(500), "javascript"), NEW_CODE);
|
|
29
|
+
(0, import_vitest.expect)(out).toContain(NEW_CODE);
|
|
30
|
+
});
|
|
31
|
+
(0, import_vitest.it)("falls back to line replacement when there is no fenced code block", () => {
|
|
32
|
+
const text = '### Result\nawait page.getByRole("button").click();\n\n### Page state\n' + bigSnapshot(500);
|
|
33
|
+
const out = (0, import_resultCode.rewriteResultCodeText)(text, NEW_CODE);
|
|
34
|
+
(0, import_vitest.expect)(out).toContain(NEW_CODE);
|
|
35
|
+
(0, import_vitest.expect)(out).not.toContain('await page.getByRole("button").click();');
|
|
36
|
+
});
|
|
37
|
+
(0, import_vitest.it)("returns null when there is no recorded code to rewrite", () => {
|
|
38
|
+
(0, import_vitest.expect)((0, import_resultCode.rewriteResultCodeText)("### Page state\n- Page Snapshot:\n" + bigSnapshot(500), NEW_CODE)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
(0, import_vitest.it)("is fast on a ~1.2MB result (no catastrophic backtracking)", () => {
|
|
41
|
+
const text = clickResult(bigSnapshot(12e5), "js");
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const out = (0, import_resultCode.rewriteResultCodeText)(text, NEW_CODE);
|
|
44
|
+
const elapsedMs = Date.now() - start;
|
|
45
|
+
(0, import_vitest.expect)(out).toContain(NEW_CODE);
|
|
46
|
+
(0, import_vitest.expect)(elapsedMs).toBeLessThan(2e3);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"dockerode": "^5.0.0",
|
|
72
72
|
"fast-glob": "^3.3.3",
|
|
73
73
|
"js-yaml": "^4.1.1",
|
|
74
|
-
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.
|
|
74
|
+
"playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.2.tgz",
|
|
75
75
|
"simple-git": "^3.30.0",
|
|
76
76
|
"typescript": "^5.8.3",
|
|
77
77
|
"zod": "^3.25.3"
|