@skyramp/mcp 0.2.1 → 0.2.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/registerPlaywrightTools.js +9 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +106 -5
- package/build/prompts/test-recommendation/scopeAssessment.test.js +128 -1
- package/build/prompts/testbot/testbot-prompts.js +2 -2
- package/build/prompts/testbot/testbot-prompts.test.js +21 -0
- package/build/tools/test-management/analyzeChangesTool.js +8 -2
- package/build/tools/test-management/uiAnalyzeChangesTool.js +8 -2
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +47 -0
- package/build/utils/dartRouteExtractor.js +319 -0
- package/build/utils/dartRouteExtractor.test.js +307 -0
- package/build/utils/uiPageEnumerator.js +67 -0
- package/build/utils/uiPageEnumerator.test.js +222 -0
- package/node_modules/playwright/lib/mcp/skyramp/index.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +313 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +146 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +222 -2
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +27 -14
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var skyRampImport_exports = {};
|
|
30
|
+
__export(skyRampImport_exports, {
|
|
31
|
+
actionsFromFile: () => actionsFromFile,
|
|
32
|
+
actionsFromPath: () => actionsFromPath,
|
|
33
|
+
actionsFromZip: () => actionsFromZip,
|
|
34
|
+
parseJsonl: () => parseJsonl
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(skyRampImport_exports);
|
|
37
|
+
var import_fs = __toESM(require("fs"));
|
|
38
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
39
|
+
const SKYRAMP_ACTIVITIES_FILE = "skyramp_playwright.txt";
|
|
40
|
+
const ACTION_NAMES = /* @__PURE__ */ new Set([
|
|
41
|
+
"marker",
|
|
42
|
+
"comment",
|
|
43
|
+
"beginBlock",
|
|
44
|
+
"endBlock",
|
|
45
|
+
"check",
|
|
46
|
+
"click",
|
|
47
|
+
"hover",
|
|
48
|
+
"closePage",
|
|
49
|
+
"fill",
|
|
50
|
+
"pressSequentially",
|
|
51
|
+
"navigate",
|
|
52
|
+
"openPage",
|
|
53
|
+
"press",
|
|
54
|
+
"select",
|
|
55
|
+
"uncheck",
|
|
56
|
+
"setInputFiles",
|
|
57
|
+
"assertText",
|
|
58
|
+
"assertValue",
|
|
59
|
+
"assertChecked",
|
|
60
|
+
"assertVisible",
|
|
61
|
+
"assertSnapshot",
|
|
62
|
+
"visualSnapshot",
|
|
63
|
+
"assertTableCell",
|
|
64
|
+
"dragTo",
|
|
65
|
+
"dragAndDrop",
|
|
66
|
+
"diagramNodeAdd",
|
|
67
|
+
"diagramLinkAdd",
|
|
68
|
+
"selectArea",
|
|
69
|
+
"tableSnapshot",
|
|
70
|
+
"domSnapshot",
|
|
71
|
+
"fileChooser",
|
|
72
|
+
"penTool",
|
|
73
|
+
"mouse.wheel",
|
|
74
|
+
"mouse.move",
|
|
75
|
+
"mouse.down",
|
|
76
|
+
"mouse.up",
|
|
77
|
+
"modalOpen",
|
|
78
|
+
"modalClose",
|
|
79
|
+
"iframeLoad",
|
|
80
|
+
"assertApiRequest",
|
|
81
|
+
"waitForSelector",
|
|
82
|
+
"waitForTimeout"
|
|
83
|
+
]);
|
|
84
|
+
function isActionName(name) {
|
|
85
|
+
return ACTION_NAMES.has(name);
|
|
86
|
+
}
|
|
87
|
+
async function actionsFromZip(zipPath) {
|
|
88
|
+
const zip = new import_utils.ZipFile(zipPath);
|
|
89
|
+
try {
|
|
90
|
+
const entries = await zip.entries();
|
|
91
|
+
const entry = entries.find((e) => e === SKYRAMP_ACTIVITIES_FILE) ?? entries.find((e) => e.endsWith(".jsonl")) ?? entries.find((e) => e.endsWith(".txt"));
|
|
92
|
+
if (!entry)
|
|
93
|
+
throw new Error(`No activities file found in ${zipPath}. Expected '${SKYRAMP_ACTIVITIES_FILE}' or a .jsonl/.txt file.`);
|
|
94
|
+
const buffer = await zip.read(entry);
|
|
95
|
+
return parseJsonl(buffer.toString("utf-8"));
|
|
96
|
+
} finally {
|
|
97
|
+
zip.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function actionsFromFile(jsonlPath) {
|
|
101
|
+
return parseJsonl(import_fs.default.readFileSync(jsonlPath, "utf-8"));
|
|
102
|
+
}
|
|
103
|
+
async function actionsFromPath(tracePath) {
|
|
104
|
+
return tracePath.endsWith(".zip") ? await actionsFromZip(tracePath) : actionsFromFile(tracePath);
|
|
105
|
+
}
|
|
106
|
+
function parseJsonl(content) {
|
|
107
|
+
const result = [];
|
|
108
|
+
for (const line of content.split("\n")) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (!trimmed)
|
|
111
|
+
continue;
|
|
112
|
+
const action = parseLine(trimmed);
|
|
113
|
+
if (action)
|
|
114
|
+
result.push(action);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function parseLine(line) {
|
|
119
|
+
let entry;
|
|
120
|
+
try {
|
|
121
|
+
entry = JSON.parse(line);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (!entry.name || !isActionName(entry.name))
|
|
126
|
+
return null;
|
|
127
|
+
const { pageGuid, pageAlias, framePath, locator: _locator, ...actionFields } = entry;
|
|
128
|
+
const frame = {
|
|
129
|
+
pageGuid: pageGuid ?? "",
|
|
130
|
+
pageAlias: pageAlias ?? "page",
|
|
131
|
+
framePath: framePath ?? []
|
|
132
|
+
};
|
|
133
|
+
const ts = typeof entry.timestamp === "number" ? entry.timestamp : typeof entry.timestamp === "string" && /^\d+$/.test(entry.timestamp.trim()) ? Number(entry.timestamp.trim()) : Date.now();
|
|
134
|
+
return {
|
|
135
|
+
frame,
|
|
136
|
+
action: actionFields,
|
|
137
|
+
startTime: ts
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
141
|
+
0 && (module.exports = {
|
|
142
|
+
actionsFromFile,
|
|
143
|
+
actionsFromPath,
|
|
144
|
+
actionsFromZip,
|
|
145
|
+
parseJsonl
|
|
146
|
+
});
|
|
@@ -44,6 +44,9 @@ var import_skyRampExport = require("../test/skyRampExport");
|
|
|
44
44
|
var import_exportTool = require("./exportTool");
|
|
45
45
|
var import_assertTool = require("./assertTool");
|
|
46
46
|
var import_assertApiRequestTool = require("./assertApiRequestTool");
|
|
47
|
+
var import_loadTraceTool = require("./loadTraceTool");
|
|
48
|
+
var import_skyRampImport = require("./skyRampImport");
|
|
49
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
47
50
|
var import_types = require("./types");
|
|
48
51
|
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
|
|
49
52
|
class TraceRecordingBackend {
|
|
@@ -116,7 +119,7 @@ class TraceRecordingBackend {
|
|
|
116
119
|
}
|
|
117
120
|
async listTools() {
|
|
118
121
|
const browserTools = await this._browserBackend.listTools();
|
|
119
|
-
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)()];
|
|
122
|
+
return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)()];
|
|
120
123
|
}
|
|
121
124
|
async callTool(name, args, progress) {
|
|
122
125
|
if (!this._initialized)
|
|
@@ -139,6 +142,10 @@ class TraceRecordingBackend {
|
|
|
139
142
|
}
|
|
140
143
|
return exportResult;
|
|
141
144
|
}
|
|
145
|
+
if (name === import_loadTraceTool.loadTraceSchema.name) {
|
|
146
|
+
const parsed = import_loadTraceTool.loadTraceSchema.inputSchema.parse(args || {});
|
|
147
|
+
return this._handleLoadTrace(parsed);
|
|
148
|
+
}
|
|
142
149
|
if (name === import_assertTool.assertToolSchema.name) {
|
|
143
150
|
const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
|
|
144
151
|
return this._handleAssert(parsed);
|
|
@@ -313,6 +320,216 @@ Reloaded current page: ${currentUrl}
|
|
|
313
320
|
serverClosed() {
|
|
314
321
|
void this._autoExportAndClose().catch(import_log.logUnhandledError);
|
|
315
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Load a prior Skyramp trace and replay it against the live browser, honoring
|
|
325
|
+
* an optional stop point, then seed _trackedActions with the replayed actions
|
|
326
|
+
* so the model can continue recording and export a combined trace.
|
|
327
|
+
*/
|
|
328
|
+
async _handleLoadTrace(params) {
|
|
329
|
+
const stopParams = ["stopAtStep", "stopAtUrl", "stopBefore"].filter((k) => {
|
|
330
|
+
const v = params[k];
|
|
331
|
+
return v !== void 0 && v !== "";
|
|
332
|
+
});
|
|
333
|
+
if (stopParams.length > 1)
|
|
334
|
+
return { content: [{ type: "text", text: `### Error
|
|
335
|
+
Pass at most ONE of stopAtStep / stopAtUrl / stopBefore (got ${stopParams.join(", ")}). Omit all three to replay the whole trace.` }], isError: true };
|
|
336
|
+
let loadedActions;
|
|
337
|
+
try {
|
|
338
|
+
loadedActions = await (0, import_skyRampImport.actionsFromPath)(params.path);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return { content: [{ type: "text", text: `### Error
|
|
341
|
+
Failed to load trace from ${params.path}: ${e.message}` }], isError: true };
|
|
342
|
+
}
|
|
343
|
+
if (!loadedActions.length)
|
|
344
|
+
return { content: [{ type: "text", text: `### Error
|
|
345
|
+
No actions found in ${params.path}.` }], isError: true };
|
|
346
|
+
if (params.dryRun) {
|
|
347
|
+
const stepList = loadedActions.map((a, i) => (0, import_loadTraceTool.describeStep)(a, i)).join("\n");
|
|
348
|
+
return {
|
|
349
|
+
content: [{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: `### Trace loaded (dry run)
|
|
352
|
+
${loadedActions.length} steps in ${params.path}. Nothing was replayed.
|
|
353
|
+
|
|
354
|
+
Steps:
|
|
355
|
+
${stepList}
|
|
356
|
+
|
|
357
|
+
Call skyramp_load_trace again with stopAtStep / stopAtUrl / stopBefore (or none of them to replay all) to replay.`
|
|
358
|
+
}]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
await this._browserBackend.context.ensureTab();
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
let result;
|
|
366
|
+
try {
|
|
367
|
+
result = await (0, import_loadTraceTool.replayActions)(loadedActions, { pageForAlias: (alias) => this._pageForAlias(alias) }, params);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return { content: [{ type: "text", text: `### Error
|
|
370
|
+
Replay failed: ${e.message}` }], isError: true };
|
|
371
|
+
}
|
|
372
|
+
for (let i = 0; i < result.stopIndex; i++)
|
|
373
|
+
this._seedTrackedAction(loadedActions[i]);
|
|
374
|
+
if (result.stopIndex > 0)
|
|
375
|
+
this._currentPageAlias = loadedActions[result.stopIndex - 1].frame.pageAlias || "page";
|
|
376
|
+
const remaining = (0, import_loadTraceTool.listStepsFrom)(loadedActions, result.stopIndex);
|
|
377
|
+
const stoppedNote = (0, import_loadTraceTool.describeStopReason)(result);
|
|
378
|
+
const errorNote = result.error ? `
|
|
379
|
+
|
|
380
|
+
\u26A0\uFE0F Replay stopped at step ${result.error.stepIndex} (${result.error.actionName}): ${result.error.message}
|
|
381
|
+
You can continue recording from this point or fix the issue.` : "";
|
|
382
|
+
return {
|
|
383
|
+
content: [{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: `### Trace replayed
|
|
386
|
+
${result.completedCount} of ${loadedActions.length} steps replayed from ${params.path}. ${stoppedNote}${errorNote}
|
|
387
|
+
|
|
388
|
+
Remaining steps (NOT replayed):
|
|
389
|
+
${remaining}
|
|
390
|
+
|
|
391
|
+
Continue recording with browser_* tools, then call skyramp_export_zip to write the combined trace.`
|
|
392
|
+
}],
|
|
393
|
+
isError: result.stopReason === "error"
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Resolve a JSONL page alias to a live page. Numeric aliases ('page',
|
|
398
|
+
* 'pageN') map by tab index; semantic aliases ('popupPage') match by the
|
|
399
|
+
* popup URL pattern, falling back to the most recently opened tab.
|
|
400
|
+
*/
|
|
401
|
+
_pageForAlias(alias) {
|
|
402
|
+
const tabs = this._browserBackend.context?.tabs() ?? [];
|
|
403
|
+
if (!tabs.length)
|
|
404
|
+
return void 0;
|
|
405
|
+
const numeric = alias === "page" ? 0 : /^page(\d+)$/.exec(alias)?.[1];
|
|
406
|
+
if (numeric !== void 0 && numeric !== null) {
|
|
407
|
+
const idx = Number(numeric);
|
|
408
|
+
return (tabs[idx] ?? tabs[tabs.length - 1]).page;
|
|
409
|
+
}
|
|
410
|
+
const popupTab = tabs.find((t) => /chrome-extension:\/\/[a-p]{32}\/popup\//.test(t.page.url()));
|
|
411
|
+
return (popupTab ?? tabs[tabs.length - 1]).page;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Convert a replayed ActionInContext back into a TrackedAction so it merges
|
|
415
|
+
* with freshly-recorded actions. The export pipeline (buildJsonlContent)
|
|
416
|
+
* re-derives the JSONL selector from the `code` field, so we regenerate clean
|
|
417
|
+
* Playwright code from the stored internal: selector via asLocator.
|
|
418
|
+
*/
|
|
419
|
+
_seedTrackedAction(action) {
|
|
420
|
+
const a = action.action;
|
|
421
|
+
const pageAlias = action.frame.pageAlias || "page";
|
|
422
|
+
if (a.name === "navigate") {
|
|
423
|
+
this._trackedActions.push({
|
|
424
|
+
toolName: "browser_navigate",
|
|
425
|
+
args: { url: a.url },
|
|
426
|
+
code: "",
|
|
427
|
+
timestamp: action.startTime,
|
|
428
|
+
pageAlias
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const locatorExpr = a.selector ? (0, import_utils.asLocator)("javascript", a.selector) : "";
|
|
433
|
+
const seeded = this._seedTrackedActionFields(a, locatorExpr);
|
|
434
|
+
if (!seeded)
|
|
435
|
+
return;
|
|
436
|
+
this._trackedActions.push({
|
|
437
|
+
...seeded,
|
|
438
|
+
timestamp: action.startTime,
|
|
439
|
+
pageAlias,
|
|
440
|
+
framePath: action.frame.framePath?.length ? action.frame.framePath : void 0
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Build the { toolName, code, args } triple a seeded (replayed) action must
|
|
445
|
+
* carry so it round-trips through skyRampExport.buildJsonlContent exactly as
|
|
446
|
+
* a freshly-recorded action would. toolName, code and args are emitted
|
|
447
|
+
* together (not via separate mappers) because export keys on all three:
|
|
448
|
+
* trackedActionToJsonl dispatches on toolName, derives the locator from code,
|
|
449
|
+
* and reads specific args (checked, values, type). A mismatch silently
|
|
450
|
+
* mis-exports (e.g. hover as click) or drops the action.
|
|
451
|
+
*
|
|
452
|
+
* Code uses single quotes to match the live recorder's format
|
|
453
|
+
* (extractFillText / extractSelectOptions only match single-quoted calls).
|
|
454
|
+
* Returns null for unsupported actions so they are skipped, never defaulted.
|
|
455
|
+
*/
|
|
456
|
+
_seedTrackedActionFields(a, locatorExpr) {
|
|
457
|
+
const sq = (s) => `'${String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}'`;
|
|
458
|
+
const SELECTOR_REQUIRED = /* @__PURE__ */ new Set(["click", "hover", "fill", "pressSequentially", "check", "uncheck", "select"]);
|
|
459
|
+
if (SELECTOR_REQUIRED.has(a.name) && !locatorExpr)
|
|
460
|
+
return null;
|
|
461
|
+
const root = a.frameUrlMatch ? `page.frames().find(f => f.url().includes(${sq(a.frameUrlMatch)}))!` : "page";
|
|
462
|
+
switch (a.name) {
|
|
463
|
+
case "click": {
|
|
464
|
+
const isDouble = (a.clickCount ?? 1) > 1;
|
|
465
|
+
const verb = isDouble ? "dblclick" : "click";
|
|
466
|
+
const args = {};
|
|
467
|
+
if (a.button && a.button !== "left")
|
|
468
|
+
args.button = a.button;
|
|
469
|
+
const mods = (0, import_loadTraceTool.decodeModifiers)(a.modifiers);
|
|
470
|
+
if (mods.length)
|
|
471
|
+
args.modifiers = mods;
|
|
472
|
+
return { toolName: "browser_click", code: `await ${root}.${locatorExpr}.${verb}();`, args };
|
|
473
|
+
}
|
|
474
|
+
case "hover":
|
|
475
|
+
return { toolName: "browser_hover", code: `await ${root}.${locatorExpr}.hover();`, args: {} };
|
|
476
|
+
case "fill":
|
|
477
|
+
return { toolName: "browser_type", code: `await ${root}.${locatorExpr}.fill(${sq(a.text ?? "")});`, args: { text: a.text ?? "" } };
|
|
478
|
+
case "pressSequentially":
|
|
479
|
+
return { toolName: "browser_type", code: `await ${root}.${locatorExpr}.pressSequentially(${sq(a.text ?? "")});`, args: { text: a.text ?? "", slowly: true } };
|
|
480
|
+
case "press": {
|
|
481
|
+
const args = { key: a.key };
|
|
482
|
+
const mods = (0, import_loadTraceTool.decodeModifiers)(a.modifiers);
|
|
483
|
+
if (mods.length)
|
|
484
|
+
args.modifiers = mods;
|
|
485
|
+
return { toolName: "browser_press_key", code: "", args };
|
|
486
|
+
}
|
|
487
|
+
case "assertApiRequest":
|
|
488
|
+
return { toolName: "browser_assert_api_request", code: "", args: {} };
|
|
489
|
+
case "waitForTimeout":
|
|
490
|
+
return { toolName: "browser_wait_for", code: "", args: { time: (a.duration ?? 0) / 1e3 } };
|
|
491
|
+
case "check":
|
|
492
|
+
return { toolName: "browser_set_checked", code: `await ${root}.${locatorExpr}.check();`, args: { checked: true } };
|
|
493
|
+
case "uncheck":
|
|
494
|
+
return { toolName: "browser_set_checked", code: `await ${root}.${locatorExpr}.uncheck();`, args: { checked: false } };
|
|
495
|
+
case "select":
|
|
496
|
+
return { toolName: "browser_select_option", code: `await ${root}.${locatorExpr}.selectOption(${sq((a.options ?? [])[0] ?? "")});`, args: { values: a.options ?? [] } };
|
|
497
|
+
case "waitForSelector": {
|
|
498
|
+
const sel = a.selector ?? "";
|
|
499
|
+
const m = /^text=(.*)$/.exec(sel);
|
|
500
|
+
if (!m)
|
|
501
|
+
return null;
|
|
502
|
+
return a.state === "hidden" ? { toolName: "browser_wait_for", code: "", args: { textGone: m[1] } } : { toolName: "browser_wait_for", code: "", args: { text: m[1] } };
|
|
503
|
+
}
|
|
504
|
+
case "dragAndDrop":
|
|
505
|
+
case "dragTo": {
|
|
506
|
+
const srcSel = a.source ?? a.selector;
|
|
507
|
+
const tgtSel = a.target ?? a.targetSelector;
|
|
508
|
+
if (!srcSel || !tgtSel)
|
|
509
|
+
return null;
|
|
510
|
+
const srcExpr = (0, import_utils.asLocator)("javascript", srcSel);
|
|
511
|
+
const tgtExpr = (0, import_utils.asLocator)("javascript", tgtSel);
|
|
512
|
+
return { toolName: "browser_drag", code: `await page.${srcExpr}.dragTo(page.${tgtExpr});`, args: {} };
|
|
513
|
+
}
|
|
514
|
+
// Assertions are encoded the same way _handleAssert encodes them, so
|
|
515
|
+
// assertActionToJsonl in skyRampExport parses them identically. That
|
|
516
|
+
// parser dispatches on toolName === 'browser_assert' and keys on args.type.
|
|
517
|
+
// Assertions pass selector/expected as structured args (unambiguous even
|
|
518
|
+
// when the value contains ':'). assertActionToJsonl reads these directly;
|
|
519
|
+
// the legacy colon-encoded code string is kept only as a human-readable
|
|
520
|
+
// breadcrumb / backward-compat fallback.
|
|
521
|
+
case "assertText":
|
|
522
|
+
return { toolName: "browser_assert", code: `assertText:${a.selector}:${a.text}:${a.substring ?? true}`, args: { type: "text", selector: a.selector, expected: a.text, substring: a.substring ?? true } };
|
|
523
|
+
case "assertValue":
|
|
524
|
+
return { toolName: "browser_assert", code: `assertValue:${a.selector}:${a.value}`, args: { type: "value", selector: a.selector, expected: a.value } };
|
|
525
|
+
case "assertChecked":
|
|
526
|
+
return { toolName: "browser_assert", code: `assertChecked:${a.selector}:${!!a.checked}`, args: { type: "checked", selector: a.selector, checked: !!a.checked } };
|
|
527
|
+
case "assertVisible":
|
|
528
|
+
return { toolName: "browser_assert", code: `assertVisible:${a.selector}`, args: { type: "visible", selector: a.selector } };
|
|
529
|
+
default:
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
316
533
|
/**
|
|
317
534
|
* Handle browser_select_option with automatic fallback for custom dropdowns.
|
|
318
535
|
* 1. Try native selectOption first.
|
|
@@ -473,7 +690,10 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
|
|
|
473
690
|
const assertName = params.type === "value" ? "assertValue" : "assertText";
|
|
474
691
|
this._trackedActions.push({
|
|
475
692
|
toolName: "browser_assert",
|
|
476
|
-
|
|
693
|
+
// Pass selector/expected as structured args so assertActionToJsonl can
|
|
694
|
+
// decode unambiguously (the colon-encoded code below corrupts values
|
|
695
|
+
// that contain ':', e.g. URLs). The code string is a fallback only.
|
|
696
|
+
args: { type: params.type, ref: params.ref, selector: parsed.selector, expected: params.expected, ...params.type === "text" ? { substring: params.substring } : {} },
|
|
477
697
|
code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
|
|
478
698
|
timestamp
|
|
479
699
|
});
|
|
@@ -226,7 +226,7 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
|
|
|
226
226
|
return null;
|
|
227
227
|
}
|
|
228
228
|
if (toolName === "browser_press_key")
|
|
229
|
-
return JSON.stringify({ name: "press", key: args.key, selector: "", ...base });
|
|
229
|
+
return JSON.stringify({ name: "press", key: args.key, modifiers: modifiersArrayToMask(args.modifiers), selector: "", ...base });
|
|
230
230
|
if (!code)
|
|
231
231
|
return null;
|
|
232
232
|
const extracted = extractLocatorFromCode(code);
|
|
@@ -304,23 +304,34 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
|
|
|
304
304
|
signals: [],
|
|
305
305
|
timestamp: String(timestamp),
|
|
306
306
|
pageGuid,
|
|
307
|
-
pageAlias: DEFAULT_PAGE_ALIAS,
|
|
308
|
-
framePath: DEFAULT_FRAME_PATH
|
|
307
|
+
pageAlias: action.pageAlias ?? DEFAULT_PAGE_ALIAS,
|
|
308
|
+
framePath: action.framePath ?? DEFAULT_FRAME_PATH
|
|
309
309
|
};
|
|
310
|
-
const parts = code.split(":");
|
|
311
|
-
const selectorPart = parts[1] || "";
|
|
312
310
|
let selector = "";
|
|
313
311
|
let expected = "";
|
|
314
312
|
let substring = true;
|
|
315
|
-
if (
|
|
316
|
-
selector =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
313
|
+
if (args.selector !== void 0) {
|
|
314
|
+
selector = args.selector;
|
|
315
|
+
if (assertType === "text") {
|
|
316
|
+
expected = args.expected ?? "";
|
|
317
|
+
substring = args.substring !== false;
|
|
318
|
+
} else if (assertType === "value") {
|
|
319
|
+
expected = args.expected ?? "";
|
|
320
|
+
} else if (assertType === "checked") {
|
|
321
|
+
expected = String(args.checked === true || args.expected === "true" || args.expected === true);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const parts = code.split(":");
|
|
325
|
+
if (assertType === "visible") {
|
|
326
|
+
selector = parts.slice(1).join(":");
|
|
327
|
+
} else if (assertType === "text") {
|
|
328
|
+
substring = parts[parts.length - 1] === "true";
|
|
329
|
+
expected = parts[parts.length - 2] || "";
|
|
330
|
+
selector = parts.slice(1, parts.length - 2).join(":");
|
|
331
|
+
} else if (assertType === "value" || assertType === "checked") {
|
|
332
|
+
expected = parts[parts.length - 1] || "";
|
|
333
|
+
selector = parts.slice(1, parts.length - 1).join(":");
|
|
334
|
+
}
|
|
324
335
|
}
|
|
325
336
|
const locator = selectorToLocator(selector);
|
|
326
337
|
switch (assertType) {
|
|
@@ -330,6 +341,8 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
|
|
|
330
341
|
return JSON.stringify({ name: "assertVisible", selector, locator, ...base });
|
|
331
342
|
case "value":
|
|
332
343
|
return JSON.stringify({ name: "assertValue", selector, value: expected, locator, ...base });
|
|
344
|
+
case "checked":
|
|
345
|
+
return JSON.stringify({ name: "assertChecked", selector, checked: expected === "true", locator, ...base });
|
|
333
346
|
default:
|
|
334
347
|
return null;
|
|
335
348
|
}
|