@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.
Files changed (31) hide show
  1. package/build/index.js +2 -0
  2. package/build/playwright/registerPlaywrightTools.js +1 -1
  3. package/build/playwright/traceRecordingPrompt.js +9 -3
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -7
  5. package/build/prompts/test-maintenance/driftAnalysisSections.js +96 -34
  6. package/build/prompts/test-maintenance/enhanceAssertionSection.js +99 -0
  7. package/build/prompts/test-recommendation/recommendationSections.js +24 -9
  8. package/build/prompts/test-recommendation/test-recommendation-prompt.js +96 -27
  9. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +239 -2
  10. package/build/prompts/testbot/testbot-prompts.js +185 -120
  11. package/build/services/TestDiscoveryService.js +23 -0
  12. package/build/services/TestExecutionService.js +1 -1
  13. package/build/services/TestGenerationService.js +83 -12
  14. package/build/services/TestGenerationService.test.js +111 -2
  15. package/build/tool-phase-coverage.test.js +8 -2
  16. package/build/tool-phases.js +11 -13
  17. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +203 -0
  18. package/build/tools/generate-tests/generateContractRestTool.js +3 -73
  19. package/build/tools/generate-tests/generateIntegrationRestTool.js +11 -61
  20. package/build/tools/submitReportTool.js +11 -3
  21. package/build/tools/submitReportTool.test.js +1 -1
  22. package/build/tools/test-management/analyzeChangesTool.js +14 -4
  23. package/build/types/RepositoryAnalysis.js +1 -0
  24. package/build/utils/scenarioDrafting.js +121 -11
  25. package/build/utils/scenarioDrafting.test.js +266 -3
  26. package/node_modules/playwright/ThirdPartyNotices.txt +679 -3093
  27. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +52 -0
  28. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +290 -15
  29. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +60 -0
  30. package/package.json +2 -2
  31. 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
- return handler(parsed);
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
- const preSnapText = preSnapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
160
- const comboboxes = [...preSnapText.matchAll(/combobox[^\n]*\[ref=(\w+)\]/g)];
161
- const clickableCombo = comboboxes.find((m) => m[0].includes("cursor=pointer"));
162
- const comboRef = clickableCombo?.[1] || args.ref;
163
- const clickResult = await this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef });
164
- if (clickResult.isError)
165
- return { content: [{ type: "text", text: `### Error
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
- this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult);
168
- const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
169
- if (snapResult.isError)
170
- return snapResult;
171
- const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
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-rc.8",
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.16",
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",