@skyramp/mcp 0.2.9 → 0.2.10-rc.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.
Files changed (76) hide show
  1. package/build/index.js +1 -1
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
  4. package/build/prompts/personas.d.ts +2 -2
  5. package/build/prompts/personas.js +2 -2
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
  7. package/build/prompts/sut-setup/shared.js +1 -3
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
  9. package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
  10. package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
  11. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
  14. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
  15. package/build/prompts/test-recommendation/recommendationShared.js +1 -1
  16. package/build/prompts/testbot/testbot-prompts.js +8 -8
  17. package/build/resources/testbotResource.js +1 -1
  18. package/build/services/TestDiscoveryService.d.ts +0 -1
  19. package/build/services/TestDiscoveryService.js +13 -33
  20. package/build/services/TestDiscoveryService.test.js +136 -1
  21. package/build/services/TestExecutionService.d.ts +1 -1
  22. package/build/tools/executeSkyrampTestTool.js +40 -71
  23. package/build/tools/submitReportTool.d.ts +20 -20
  24. package/build/tools/submitReportTool.js +113 -31
  25. package/build/tools/submitReportTool.test.js +411 -94
  26. package/build/tools/test-management/actionsTool.js +135 -53
  27. package/build/tools/test-management/actionsTool.test.d.ts +1 -0
  28. package/build/tools/test-management/actionsTool.test.js +297 -0
  29. package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
  30. package/build/tools/test-management/analyzeChangesTool.js +63 -152
  31. package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
  32. package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
  33. package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
  34. package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
  35. package/build/types/FrontendIntegration.d.ts +25 -0
  36. package/build/types/FrontendIntegration.js +19 -0
  37. package/build/types/RepositoryAnalysis.d.ts +8 -8
  38. package/build/types/TestAnalysis.d.ts +16 -50
  39. package/build/types/TestAnalysis.js +0 -28
  40. package/build/types/TestTypes.d.ts +3 -1
  41. package/build/types/TestTypes.js +3 -1
  42. package/build/utils/AnalysisStateManager.d.ts +13 -1
  43. package/build/utils/docker.test.js +1 -1
  44. package/build/utils/featureFlags.d.ts +1 -1
  45. package/build/utils/featureFlags.js +1 -1
  46. package/build/utils/frontendIntegration.d.ts +9 -0
  47. package/build/utils/frontendIntegration.js +237 -0
  48. package/build/utils/frontendIntegration.test.d.ts +1 -0
  49. package/build/utils/frontendIntegration.test.js +229 -0
  50. package/build/utils/pr-comment-parser.d.ts +3 -3
  51. package/build/utils/pr-comment-parser.js +6 -6
  52. package/build/utils/pr-comment-parser.test.js +3 -3
  53. package/build/utils/repoScanner.d.ts +7 -0
  54. package/build/utils/repoScanner.js +14 -6
  55. package/build/utils/repoScanner.test.js +9 -9
  56. package/build/utils/routeParsers.d.ts +32 -0
  57. package/build/utils/routeParsers.js +205 -2
  58. package/build/utils/routeParsers.test.js +279 -29
  59. package/build/utils/sourceRouteExtractor.js +174 -5
  60. package/build/utils/sourceRouteExtractor.test.js +173 -0
  61. package/build/utils/utils.d.ts +11 -0
  62. package/build/utils/utils.js +19 -0
  63. package/build/utils/utils.test.js +23 -1
  64. package/build/utils/versions.d.ts +3 -3
  65. package/build/utils/versions.js +1 -1
  66. package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
  67. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  68. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
  69. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
  70. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
  71. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  74. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  75. package/node_modules/playwright/package.json +1 -1
  76. package/package.json +3 -3
@@ -562,4 +562,177 @@ export default function App() {
562
562
  expect(routes[0].componentFile).toMatch(/src\/pages\/Cart\/index\.tsx$/);
563
563
  cleanup(repo);
564
564
  });
565
+ // ── Nested React Router routes ────────────────────────────────────────────
566
+ it("composes nested JSX <Route> child paths with their parent", () => {
567
+ // React Router v6 child paths are relative to the parent route. Without
568
+ // composition the extractor emits bare segments ("settings") that aren't
569
+ // navigable URLs.
570
+ const repo = makeTmpRepo([
571
+ {
572
+ path: "src/App.tsx",
573
+ content: `
574
+ import { Routes, Route } from 'react-router-dom';
575
+ import Dashboard from './pages/Dashboard';
576
+ import Settings from './pages/Settings';
577
+ import Users from './pages/Users';
578
+
579
+ export default function App() {
580
+ return (
581
+ <Routes>
582
+ <Route path="/dashboard" element={<Dashboard />}>
583
+ <Route path="settings" element={<Settings />} />
584
+ <Route path="users" element={<Users />} />
585
+ </Route>
586
+ </Routes>
587
+ );
588
+ }
589
+ `,
590
+ },
591
+ ]);
592
+ const routes = extractSourceRoutes(repo);
593
+ expect(routes.map((r) => r.path).sort()).toEqual([
594
+ "/dashboard",
595
+ "/dashboard/settings",
596
+ "/dashboard/users",
597
+ ]);
598
+ cleanup(repo);
599
+ });
600
+ it("composes relative child paths under a root layout route", () => {
601
+ // Canonical RR v6 shape: a path-bearing parent that itself has children,
602
+ // with relative child paths. The parent must not count its own path twice.
603
+ const repo = makeTmpRepo([
604
+ {
605
+ path: "src/App.tsx",
606
+ content: `
607
+ import { Routes, Route } from 'react-router-dom';
608
+ import Layout from './Layout';
609
+ import Dashboard from './pages/Dashboard';
610
+ import Settings from './pages/Settings';
611
+
612
+ export default function App() {
613
+ return (
614
+ <Routes>
615
+ <Route path="/" element={<Layout />}>
616
+ <Route path="dashboard" element={<Dashboard />}>
617
+ <Route path="settings" element={<Settings />} />
618
+ </Route>
619
+ </Route>
620
+ </Routes>
621
+ );
622
+ }
623
+ `,
624
+ },
625
+ ]);
626
+ const routes = extractSourceRoutes(repo);
627
+ expect(routes.find((r) => r.componentName === "Dashboard").path).toBe("/dashboard");
628
+ expect(routes.find((r) => r.componentName === "Settings").path).toBe("/dashboard/settings");
629
+ cleanup(repo);
630
+ });
631
+ it("resolves a JSX index route to its parent's path", () => {
632
+ // <Route index> has no path attribute; it renders at the parent's path.
633
+ const repo = makeTmpRepo([
634
+ {
635
+ path: "src/App.tsx",
636
+ content: `
637
+ import { Routes, Route } from 'react-router-dom';
638
+ import Dashboard from './pages/Dashboard';
639
+ import DashboardHome from './pages/DashboardHome';
640
+
641
+ export default function App() {
642
+ return (
643
+ <Routes>
644
+ <Route path="/dashboard" element={<Dashboard />}>
645
+ <Route index element={<DashboardHome />} />
646
+ </Route>
647
+ </Routes>
648
+ );
649
+ }
650
+ `,
651
+ },
652
+ ]);
653
+ const routes = extractSourceRoutes(repo);
654
+ const home = routes.find((r) => r.componentName === "DashboardHome");
655
+ expect(home).toBeDefined();
656
+ expect(home.path).toBe("/dashboard");
657
+ cleanup(repo);
658
+ });
659
+ it("extracts createBrowserRouter data-router routes with composed nested paths", () => {
660
+ // The data-router API uses object literals with `element:` (JSX) and nests
661
+ // via `children:` — neither the JSX-element path nor the Vue object path
662
+ // handled these, so the whole tree was missed (root-only fallback).
663
+ const repo = makeTmpRepo([
664
+ {
665
+ path: "src/router.tsx",
666
+ content: `
667
+ import { createBrowserRouter } from 'react-router-dom';
668
+ import Root from './Root';
669
+ import Dashboard from './pages/Dashboard';
670
+ import Settings from './pages/Settings';
671
+ import Profile from './pages/Profile';
672
+
673
+ export const router = createBrowserRouter([
674
+ {
675
+ path: '/',
676
+ element: <Root />,
677
+ children: [
678
+ { path: 'dashboard', element: <Dashboard /> },
679
+ {
680
+ path: 'settings',
681
+ element: <Settings />,
682
+ children: [
683
+ { path: 'profile', element: <Profile /> },
684
+ ],
685
+ },
686
+ ],
687
+ },
688
+ ]);
689
+ `,
690
+ },
691
+ ]);
692
+ const routes = extractSourceRoutes(repo);
693
+ expect(routes.map((r) => r.path).sort()).toEqual([
694
+ "/",
695
+ "/dashboard",
696
+ "/settings",
697
+ "/settings/profile",
698
+ ]);
699
+ expect(routes.find((r) => r.path === "/settings/profile").componentName).toBe("Profile");
700
+ cleanup(repo);
701
+ });
702
+ it("extracts createStaticRouter (SSR) routes with composed nested paths", () => {
703
+ // SSR apps build routes with createStaticRouter — same route-array shape as
704
+ // createBrowserRouter, so nested composition must work identically.
705
+ const repo = makeTmpRepo([
706
+ {
707
+ path: "src/router.tsx",
708
+ content: `
709
+ import { createStaticRouter } from 'react-router-dom/server';
710
+ import Root from './Root';
711
+ import Reports from './pages/Reports';
712
+ import Detail from './pages/Detail';
713
+
714
+ export const router = createStaticRouter([
715
+ {
716
+ path: '/',
717
+ element: <Root />,
718
+ children: [
719
+ { path: 'reports', element: <Reports />,
720
+ children: [
721
+ { path: ':id', element: <Detail /> },
722
+ ],
723
+ },
724
+ ],
725
+ },
726
+ ]);
727
+ `,
728
+ },
729
+ ]);
730
+ const routes = extractSourceRoutes(repo);
731
+ expect(routes.map((r) => r.path).sort()).toEqual([
732
+ "/",
733
+ "/reports",
734
+ "/reports/:id",
735
+ ]);
736
+ cleanup(repo);
737
+ });
565
738
  });
@@ -1,6 +1,17 @@
1
1
  import { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  export declare function readDiffFile(diffFilePath: string | undefined): string | undefined;
3
3
  export declare function toolError(message: string): CallToolResult;
4
+ /**
5
+ * Does `candidate` (an LLM- or caller-supplied name/path) identify `fullPath` (a known,
6
+ * absolute test file path)? Exact match first, then a real path-segment boundary match
7
+ * for a relative candidate (e.g. "tests/foo.py" against ".../repo/tests/foo.py") — never
8
+ * a bare substring/suffix check. Only falls back to basename equality when `candidate`
9
+ * carries no path info at all, since that's the one case where the caller genuinely gave
10
+ * us nothing more specific to disambiguate with (bare filenames can still collide across
11
+ * different directories that happen to share one — this is the caller's ambiguity, not
12
+ * something a smarter match here can resolve).
13
+ */
14
+ export declare function testFileMatches(fullPath: string, candidate: string): boolean;
4
15
  /**
5
16
  * Happy-path counterpart to `toolError`. Wraps a single text payload in the
6
17
  * MCP tool-result shape so handlers don't have to construct
@@ -18,6 +18,25 @@ export function toolError(message) {
18
18
  isError: true,
19
19
  };
20
20
  }
21
+ /**
22
+ * Does `candidate` (an LLM- or caller-supplied name/path) identify `fullPath` (a known,
23
+ * absolute test file path)? Exact match first, then a real path-segment boundary match
24
+ * for a relative candidate (e.g. "tests/foo.py" against ".../repo/tests/foo.py") — never
25
+ * a bare substring/suffix check. Only falls back to basename equality when `candidate`
26
+ * carries no path info at all, since that's the one case where the caller genuinely gave
27
+ * us nothing more specific to disambiguate with (bare filenames can still collide across
28
+ * different directories that happen to share one — this is the caller's ambiguity, not
29
+ * something a smarter match here can resolve).
30
+ */
31
+ export function testFileMatches(fullPath, candidate) {
32
+ if (fullPath === candidate)
33
+ return true;
34
+ if (path.isAbsolute(candidate))
35
+ return false;
36
+ if (candidate.includes("/"))
37
+ return fullPath.endsWith("/" + candidate);
38
+ return path.basename(fullPath) === candidate;
39
+ }
21
40
  /**
22
41
  * Happy-path counterpart to `toolError`. Wraps a single text payload in the
23
42
  * MCP tool-result shape so handlers don't have to construct
@@ -1,5 +1,27 @@
1
1
  // @ts-ignore
2
- import { validateParams, generateSkyrampHeader } from "./utils.js";
2
+ import { validateParams, generateSkyrampHeader, testFileMatches } from "./utils.js";
3
+ describe("testFileMatches", () => {
4
+ it("matches identical absolute paths", () => {
5
+ expect(testFileMatches("/repo/tests/orders_test.py", "/repo/tests/orders_test.py")).toBe(true);
6
+ });
7
+ it("does not match two different absolute paths, even with the same basename", () => {
8
+ expect(testFileMatches("/repo/frontend/tests/utils_test.py", "/repo/backend/tests/utils_test.py")).toBe(false);
9
+ });
10
+ it("matches a relative candidate against a real path-segment boundary", () => {
11
+ expect(testFileMatches("/repo/backend/tests/orders_test.py", "tests/orders_test.py")).toBe(true);
12
+ });
13
+ it("does not match a relative candidate that is only a substring, not a path-segment boundary", () => {
14
+ // "ers_test.py" is a suffix of the basename but not preceded by a path separator.
15
+ expect(testFileMatches("/repo/backend/tests/orders_test.py", "ers_test.py")).toBe(false);
16
+ });
17
+ it("falls back to basename equality only when the candidate has no path info at all", () => {
18
+ expect(testFileMatches("/repo/backend/tests/orders_test.py", "orders_test.py")).toBe(true);
19
+ });
20
+ it("bare-basename fallback is ambiguous across two files that share a basename (documented limitation)", () => {
21
+ expect(testFileMatches("/repo/frontend/tests/utils_test.py", "utils_test.py")).toBe(true);
22
+ expect(testFileMatches("/repo/backend/tests/utils_test.py", "utils_test.py")).toBe(true);
23
+ });
24
+ });
3
25
  describe("validateParams", () => {
4
26
  it("should return null for valid comma-separated key=value pairs", () => {
5
27
  const result = validateParams("foo=bar,baz=qux", "testField");
@@ -1,3 +1,3 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.30";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.30";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.30";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.32";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.32";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.32";
@@ -1,3 +1,3 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.30";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.32";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
@@ -61,9 +61,14 @@ const uploadFile = (0, import_tool.defineTool)({
61
61
  const files = params.paths ?? [];
62
62
  response.addCode(`await fileChooser.setFiles(${JSON.stringify(files)})`);
63
63
  tab.clearModalState(fileChooserModal);
64
- await tab.waitForCompletion(async () => {
65
- await fileChooserModal.fileChooser.setFiles(files);
66
- });
64
+ try {
65
+ await tab.waitForCompletion(async () => {
66
+ await fileChooserModal.fileChooser.setFiles(files);
67
+ });
68
+ } catch (e) {
69
+ tab.setModalState(fileChooserModal);
70
+ throw e;
71
+ }
67
72
  }
68
73
  });
69
74
  var files_default = [
@@ -30,7 +30,7 @@ const wait = (0, import_tool.defineTool)({
30
30
  title: "Wait for",
31
31
  description: 'Wait for text to appear, disappear, or a time to pass. REQUIRED after navigating to a folder where content was just created or renamed \u2014 file/item names update asynchronously and will not be present immediately. Always call with text: "<filename>" before attempting to hover or click a newly created item.',
32
32
  inputSchema: import_mcpBundle.z.object({
33
- time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
33
+ time: import_mcpBundle.z.number().positive().max(30).optional().describe("The time to wait, in SECONDS (not milliseconds), max 30. e.g. 3 = three seconds. Prefer text/textGone waits over a fixed time."),
34
34
  text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
35
35
  textGone: import_mcpBundle.z.string().optional().describe("The text to wait for to disappear")
36
36
  }),
@@ -54,6 +54,12 @@ const exportZipSchema = {
54
54
  }),
55
55
  type: "readOnly"
56
56
  };
57
+ function summarizeDropped(dropped) {
58
+ const counts = /* @__PURE__ */ new Map();
59
+ for (const d of dropped)
60
+ counts.set(d.toolName, (counts.get(d.toolName) ?? 0) + 1);
61
+ return [...counts.entries()].map(([tool, n]) => n > 1 ? `${tool} \xD7${n}` : tool).join(", ");
62
+ }
57
63
  function createExportZipHandler(ctx) {
58
64
  return async (params) => {
59
65
  if (!ctx.trackedActions.length) {
@@ -86,6 +92,9 @@ Requested path: ${outputZip}` }],
86
92
  await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, ctx.harPath);
87
93
  const skippedNote = skipped.length ? `
88
94
  Skipped (not exportable): ${[...new Set(skipped)].join(", ")}` : "";
95
+ const dropped = ctx.droppedActions ?? [];
96
+ const droppedNote = dropped.length ? `
97
+ Failed & dropped (not recorded): ${summarizeDropped(dropped)}` : "";
89
98
  return {
90
99
  content: [{
91
100
  type: "text",
@@ -93,7 +102,7 @@ Skipped (not exportable): ${[...new Set(skipped)].join(", ")}` : "";
93
102
  Skyramp zip written to ${outputZip}
94
103
 
95
104
  Contains:
96
- - skyramp_playwright.txt (${actionCount} actions)${skippedNote}`
105
+ - skyramp_playwright.txt (${actionCount} actions)${skippedNote}${droppedNote}`
97
106
  }]
98
107
  };
99
108
  };
@@ -74,6 +74,7 @@ class TraceRecordingBackend {
74
74
  // per-type baseline filename counter
75
75
  constructor(options) {
76
76
  this._trackedActions = [];
77
+ this._droppedActions = [];
77
78
  this._initialized = false;
78
79
  this._pageCount = 0;
79
80
  // number of popup pages seen
@@ -92,6 +93,9 @@ class TraceRecordingBackend {
92
93
  get trackedActions() {
93
94
  return this._trackedActions;
94
95
  }
96
+ get droppedActions() {
97
+ return this._droppedActions;
98
+ }
95
99
  async initialize(clientInfo) {
96
100
  const userDataDir = this._options.userDataDir || process.env.PLAYWRIGHT_USER_DATA_DIR || void 0;
97
101
  const loadExtension = this._options.loadExtension ?? splitExtensionPaths(process.env.PLAYWRIGHT_LOAD_EXTENSION);
@@ -185,12 +189,14 @@ class TraceRecordingBackend {
185
189
  this._consumeRecorderSelectors();
186
190
  const handler = (0, import_exportTool.createExportZipHandler)({
187
191
  trackedActions: this._trackedActions,
192
+ droppedActions: this._droppedActions,
188
193
  harPath: this._harPath,
189
194
  outputDir: this._outputDir
190
195
  });
191
196
  const exportResult = await handler(parsed);
192
197
  if (!exportResult.isError) {
193
198
  this._trackedActions = [];
199
+ this._droppedActions = [];
194
200
  const ra = this._browserBackend.context?.recorderActions;
195
201
  if (ra)
196
202
  ra.length = 0;
@@ -1691,8 +1697,13 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1691
1697
  };
1692
1698
  }
1693
1699
  _maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
1694
- if (result.isError)
1700
+ if (result.isError) {
1701
+ const errParsed = (0, import_response.parseResponse)(result);
1702
+ const errText = (errParsed?.error ?? errParsed?.text ?? "unknown error").replace(/\s+/g, " ").trim().slice(0, 300);
1703
+ this._droppedActions.push({ toolName, error: errText });
1704
+ traceDebug(`Dropped failed action: ${toolName} args=${JSON.stringify(args).slice(0, 200)} \u2014 ${errText} (dropped total: ${this._droppedActions.length})`);
1695
1705
  return;
1706
+ }
1696
1707
  const parsed = (0, import_response.parseResponse)(result);
1697
1708
  const code = parsed?.code ?? "";
1698
1709
  if (code || import_types.ARGS_ONLY_TOOLS.has(toolName)) {
@@ -1771,15 +1782,28 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1771
1782
  else
1772
1783
  traceDebug("Flutter placeholder clicked but semantics not confirmed active");
1773
1784
  }
1774
- /** Clean up temp directory and optionally recreate it for next session. */
1775
- _cleanupTempDir(recreate = false) {
1785
+ /**
1786
+ * Clean up temp files. When `reuse` is true (between recordings in the same
1787
+ * session), the temp directory and `_harPath` are kept STABLE and only their
1788
+ * contents are cleared; when false (session end), the directory is removed.
1789
+ *
1790
+ * SKYR-3830: `recordHar.path` is baked by value into the context factory's
1791
+ * config at initialize(). Reassigning `_harPath` to a fresh temp dir here
1792
+ * desynced it from that baked path, so every recording after the first wrote
1793
+ * its HAR to the original (now-deleted) path while export read the new
1794
+ * `_harPath` — yielding an empty HAR and zero `waitForResponse` anchors.
1795
+ * Keeping the path stable lets the next context's recordHar overwrite the
1796
+ * HAR in place on close, so subsequent exports capture network again.
1797
+ */
1798
+ _cleanupTempDir(reuse = false) {
1776
1799
  try {
1777
- import_fs.default.rmSync(this._tempDir, { recursive: true, force: true });
1778
- traceDebug(`Cleaned up temp directory: ${this._tempDir}`);
1779
- if (recreate) {
1780
- this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
1781
- this._harPath = import_path.default.join(this._tempDir, "skyramp_network.har");
1782
- traceDebug(`Created new temp directory: ${this._tempDir}`);
1800
+ if (reuse) {
1801
+ for (const entry of import_fs.default.readdirSync(this._tempDir))
1802
+ import_fs.default.rmSync(import_path.default.join(this._tempDir, entry), { recursive: true, force: true });
1803
+ traceDebug(`Cleared temp directory for reuse: ${this._tempDir}`);
1804
+ } else {
1805
+ import_fs.default.rmSync(this._tempDir, { recursive: true, force: true });
1806
+ traceDebug(`Cleaned up temp directory: ${this._tempDir}`);
1783
1807
  }
1784
1808
  } catch (e) {
1785
1809
  traceDebug(`Failed to clean up temp directory ${this._tempDir}: ${e}`);
@@ -251,7 +251,7 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
251
251
  if (textGone)
252
252
  return JSON.stringify({ name: "waitForSelector", selector: `text=${textGone}`, state: "hidden", ...base });
253
253
  if (typeof time === "number")
254
- return JSON.stringify({ name: "waitForTimeout", duration: time * 1e3, ...base });
254
+ return JSON.stringify({ name: "waitForTimeout", duration: Math.min(3e4, time * 1e3), ...base });
255
255
  return null;
256
256
  }
257
257
  if (toolName === "browser_press_key")
@@ -261,6 +261,12 @@ function trackedActionToJsonl(action, pageGuid, timestamp) {
261
261
  return null;
262
262
  return JSON.stringify({ ...args, ...base });
263
263
  }
264
+ if (toolName === "browser_evaluate") {
265
+ const fn = typeof args.function === "string" ? args.function : "";
266
+ if (!fn || args.ref && args.element)
267
+ return null;
268
+ return JSON.stringify({ name: "evaluate", expression: fn, ...base });
269
+ }
264
270
  if (!code)
265
271
  return null;
266
272
  const extracted = extractLocatorFromCode(code);
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var import_vitest = require("vitest");
3
+ var import_skyRampExport = require("./skyRampExport");
4
+ function waitForTime(time) {
5
+ return { toolName: "browser_wait_for", args: { time }, code: "", timestamp: 1e3 };
6
+ }
7
+ function jsonlFor(action) {
8
+ const line = (0, import_skyRampExport.trackedActionToJsonl)(action, "guid-1", 1e3);
9
+ return line ? JSON.parse(line) : null;
10
+ }
11
+ (0, import_vitest.describe)("browser_wait_for \u2192 waitForTimeout duration", () => {
12
+ (0, import_vitest.it)("converts a within-cap seconds value to milliseconds", () => {
13
+ const entry = jsonlFor(waitForTime(3));
14
+ (0, import_vitest.expect)(entry.name).toBe("waitForTimeout");
15
+ (0, import_vitest.expect)(entry.duration).toBe(3e3);
16
+ });
17
+ (0, import_vitest.it)("preserves fractional seconds", () => {
18
+ (0, import_vitest.expect)(jsonlFor(waitForTime(1.5)).duration).toBe(1500);
19
+ });
20
+ (0, import_vitest.it)("clamps a seconds value above the 30s live cap to 30000", () => {
21
+ (0, import_vitest.expect)(jsonlFor(waitForTime(45)).duration).toBe(3e4);
22
+ });
23
+ (0, import_vitest.it)("clamps a millisecond value mistakenly passed into the seconds param", () => {
24
+ (0, import_vitest.expect)(jsonlFor(waitForTime(1500)).duration).toBe(3e4);
25
+ (0, import_vitest.expect)(jsonlFor(waitForTime(3e3)).duration).toBe(3e4);
26
+ });
27
+ (0, import_vitest.it)("exactly at the cap is unchanged", () => {
28
+ (0, import_vitest.expect)(jsonlFor(waitForTime(30)).duration).toBe(3e4);
29
+ });
30
+ });
31
+ (0, import_vitest.describe)("browser_wait_for \u2192 text / textGone (unaffected by the clamp)", () => {
32
+ (0, import_vitest.it)("emits waitForSelector for text", () => {
33
+ const entry = jsonlFor({ toolName: "browser_wait_for", args: { text: "Report.pdf" }, code: "", timestamp: 1e3 });
34
+ (0, import_vitest.expect)(entry.name).toBe("waitForSelector");
35
+ (0, import_vitest.expect)(entry.selector).toBe("text=Report.pdf");
36
+ (0, import_vitest.expect)(entry.duration).toBeUndefined();
37
+ });
38
+ (0, import_vitest.it)("emits hidden-state waitForSelector for textGone", () => {
39
+ const entry = jsonlFor({ toolName: "browser_wait_for", args: { textGone: "Loading" }, code: "", timestamp: 1e3 });
40
+ (0, import_vitest.expect)(entry.name).toBe("waitForSelector");
41
+ (0, import_vitest.expect)(entry.selector).toBe("text=Loading");
42
+ (0, import_vitest.expect)(entry.state).toBe("hidden");
43
+ });
44
+ });
@@ -1,2 +1,2 @@
1
- import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-CYf9adZh.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.9.7"]})]}),o&&!n&&e.jsx("div",{className:"drop-target",children:e.jsx("div",{className:"title",children:"Select test to see the trace"})}),k&&e.jsx("div",{className:"drop-target",onDragLeave:()=>{v(!1)},onDrop:t=>W(t),children:e.jsx("div",{className:"title",children:"Release to analyse the Playwright Trace"})})]})},N=new x("",[]),q=({traceJson:o})=>{const[c,n]=r.useState(void 0),[l,h]=r.useState(0),u=r.useRef(null);return r.useEffect(()=>(u.current&&clearTimeout(u.current),u.current=setTimeout(async()=>{try{const p=await B(o);n(p)}catch{const p=new x("",[]);n(p)}finally{h(l+1)}},500),()=>{u.current&&clearTimeout(u.current)}),[o,l]),e.jsx(P,{isLive:!0,model:c})};async function B(o){const c=new URLSearchParams;c.set("trace",o);const l=await(await fetch(`contexts?${c.toString()}`)).json();return new x(o,l)}(async()=>{const o=new URLSearchParams(window.location.search);if(O(),window.location.protocol!=="file:"){if(o.get("isUnderTest")==="true"&&await new Promise(h=>setTimeout(h,1e3)),!navigator.serviceWorker)throw new Error(`Service workers are not supported.
1
+ import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-CYf9adZh.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.11.0"]})]}),o&&!n&&e.jsx("div",{className:"drop-target",children:e.jsx("div",{className:"title",children:"Select test to see the trace"})}),k&&e.jsx("div",{className:"drop-target",onDragLeave:()=>{v(!1)},onDrop:t=>W(t),children:e.jsx("div",{className:"title",children:"Release to analyse the Playwright Trace"})})]})},N=new x("",[]),q=({traceJson:o})=>{const[c,n]=r.useState(void 0),[l,h]=r.useState(0),u=r.useRef(null);return r.useEffect(()=>(u.current&&clearTimeout(u.current),u.current=setTimeout(async()=>{try{const p=await B(o);n(p)}catch{const p=new x("",[]);n(p)}finally{h(l+1)}},500),()=>{u.current&&clearTimeout(u.current)}),[o,l]),e.jsx(P,{isLive:!0,model:c})};async function B(o){const c=new URLSearchParams;c.set("trace",o);const l=await(await fetch(`contexts?${c.toString()}`)).json();return new x(o,l)}(async()=>{const o=new URLSearchParams(window.location.search);if(O(),window.location.protocol!=="file:"){if(o.get("isUnderTest")==="true"&&await new Promise(h=>setTimeout(h,1e3)),!navigator.serviceWorker)throw new Error(`Service workers are not supported.
2
2
  Make sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);navigator.serviceWorker.register("sw.bundle.js"),navigator.serviceWorker.controller||await new Promise(h=>{navigator.serviceWorker.oncontrollerchange=()=>h()}),setInterval(function(){fetch("ping")},1e4)}const c=o.get("trace"),l=(c==null?void 0:c.endsWith(".json"))?e.jsx(q,{traceJson:c}):e.jsx($,{});V.createRoot(document.querySelector("#root")).render(l)})();
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
8
8
  <link rel="manifest" href="./manifest.webmanifest">
9
9
  <title>Playwright Trace Viewer</title>
10
- <script type="module" crossorigin src="./index.B_7ywgmr.js"></script>
10
+ <script type="module" crossorigin src="./index.d4gkVSou.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-CYf9adZh.js">
12
12
  <link rel="stylesheet" crossorigin href="./defaultSettingsView.7ch9cixO.css">
13
13
  <link rel="stylesheet" crossorigin href="./index.BVu7tZDe.css">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-core",
3
- "version": "1.58.2-skyramp.8.10.0",
3
+ "version": "1.58.2-skyramp.8.11.0",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/playwright",
3
- "version": "1.58.2-skyramp.8.10.0",
3
+ "version": "1.58.2-skyramp.8.11.0",
4
4
  "description": "Skyramp's fork of Playwright with trace recording for UI test generation",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.10-rc.1",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -67,11 +67,11 @@
67
67
  "dependencies": {
68
68
  "@modelcontextprotocol/sdk": "^1.24.3",
69
69
  "@playwright/test": "^1.55.0",
70
- "@skyramp/skyramp": "1.3.30",
70
+ "@skyramp/skyramp": "1.3.32",
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.10.0.tgz",
74
+ "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.11.0.tgz",
75
75
  "simple-git": "^3.30.0",
76
76
  "typescript": "^5.8.3",
77
77
  "zod": "^3.25.3"