playwright-codegen-pro-core 1.0.5 → 1.0.6

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.
@@ -57,6 +57,7 @@ class BrowserBackend {
57
57
  specFile: import_path.default.join(cwd, "tests", "mcp-session.spec.ts"),
58
58
  secrets: this._config.secrets
59
59
  });
60
+ this._context.mcpRecorder = this._recorder;
60
61
  }
61
62
  async dispose() {
62
63
  await this._recorder?.dispose().catch((e) => (0, import_utilsBundle.debug)("pw:tools:error")(e));
@@ -28,7 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var mcpSessionRecorder_exports = {};
30
30
  __export(mcpSessionRecorder_exports, {
31
- McpSessionRecorder: () => McpSessionRecorder
31
+ McpSessionRecorder: () => McpSessionRecorder,
32
+ slugify: () => slugify
32
33
  });
33
34
  module.exports = __toCommonJS(mcpSessionRecorder_exports);
34
35
  var import_fs = __toESM(require("fs"));
@@ -41,6 +42,9 @@ const MAX_URL_LEN = 140;
41
42
  function truncUrl(url) {
42
43
  return url.length > MAX_URL_LEN ? url.slice(0, MAX_URL_LEN) + "\u2026" : url;
43
44
  }
45
+ function slugify(name) {
46
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "mcp-session";
47
+ }
44
48
  class McpSessionRecorder {
45
49
  constructor(context, options) {
46
50
  this._actions = [];
@@ -48,10 +52,36 @@ class McpSessionRecorder {
48
52
  this._sessionStart = Date.now();
49
53
  this._disposed = false;
50
54
  this._options = options;
55
+ this._defaultScenario = options.scenarioName;
56
+ this._defaultSpecFile = options.specFile;
51
57
  this._sessionFile = import_path.default.join(options.cwd, ".playwright-session.md");
52
58
  this._network = new import_mcpNetworkCapture.McpNetworkCapture(context, () => this._current, () => this._scheduleWrite());
53
59
  this._network.start();
54
60
  }
61
+ /**
62
+ * Begin a fresh recording. Clears accumulated actions so the next flow becomes its own
63
+ * test. With a name, the draft is written to tests/<slug>.spec.ts (so back-to-back flows
64
+ * each get their own file); without a name, resets to the default mcp-session.spec.ts.
65
+ * Returns the relative path of the spec the new flow will be written to.
66
+ */
67
+ reset(name) {
68
+ this._actions = [];
69
+ this._current = null;
70
+ this._sessionStart = Date.now();
71
+ if (name) {
72
+ this._options.scenarioName = name;
73
+ this._options.specFile = import_path.default.join(this._options.cwd, "tests", `${slugify(name)}.spec.ts`);
74
+ } else {
75
+ this._options.scenarioName = this._defaultScenario;
76
+ this._options.specFile = this._defaultSpecFile;
77
+ }
78
+ if (this._writeTimer) {
79
+ clearTimeout(this._writeTimer);
80
+ this._writeTimer = void 0;
81
+ }
82
+ this._writeFiles();
83
+ return import_path.default.relative(this._options.cwd, this._options.specFile);
84
+ }
55
85
  /** Called before a tool runs, so network events attribute to it. */
56
86
  beginAction(toolName) {
57
87
  this._current = { toolName, startTime: Date.now(), events: [] };
@@ -208,5 +238,6 @@ ${body || " // No actions recorded yet."}
208
238
  }
209
239
  // Annotate the CommonJS export names for ESM import in node:
210
240
  0 && (module.exports = {
211
- McpSessionRecorder
241
+ McpSessionRecorder,
242
+ slugify
212
243
  });
@@ -71,4 +71,25 @@ const recorderGetSession = (0, import_tool.defineTool)({
71
71
  response.addTextResult("No recorder session found. Run `npx playwright codegen --ai-codegen`, record actions, and the session will be available here automatically.");
72
72
  }
73
73
  });
74
- var recorder_default = [recorderGetSession];
74
+ const recorderReset = (0, import_tool.defineTool)({
75
+ capability: "core",
76
+ schema: {
77
+ name: "recorder_reset",
78
+ title: "Start a new recording",
79
+ description: "Start a fresh recording, clearing the accumulated actions so the next flow becomes its own clean test. Call this between independent flows when testing or documenting a site back-to-back. With a `name`, the draft test is written to tests/<name>.spec.ts and the test is named accordingly (each flow gets its own file); without a name, it resets to the default tests/mcp-session.spec.ts. The previous flow's spec file is left in place.",
80
+ inputSchema: import_mcpBundle.z.object({
81
+ name: import_mcpBundle.z.string().optional().describe('Name for the new flow, e.g. "login" or "checkout". Becomes the test name and spec filename tests/<name>.spec.ts.')
82
+ }),
83
+ type: "readOnly"
84
+ },
85
+ handle: async (context, params, response) => {
86
+ const recorder = context.mcpRecorder;
87
+ if (!recorder) {
88
+ response.addTextResult("Live recording is not active for this server.");
89
+ return;
90
+ }
91
+ const specFile = recorder.reset(params.name);
92
+ response.addTextResult(params.name ? `Started new recording "${params.name}". Subsequent actions are recorded into ${specFile} (and .playwright-session.md).` : `Recording reset. Subsequent actions are recorded into ${specFile} (and .playwright-session.md).`);
93
+ }
94
+ });
95
+ var recorder_default = [recorderGetSession, recorderReset];
@@ -51,6 +51,8 @@ Recommended workflow to "write a test for X":
51
51
  2. Call \`recorder_get_session\` to read back the accumulated session.
52
52
  3. Produce a polished final test from it, following the "Best Practices" in the returned prompt (web-first assertions, role-based locators, waitForResponse around API-triggering clicks, afterEach cleanup for created data, no hard waits). Save it to the target file named in the session.
53
53
 
54
+ Testing or documenting multiple flows back-to-back: call \`recorder_reset\` between flows so each becomes its own clean test rather than piling into one. Pass a name (e.g. \`recorder_reset({ name: "login" })\`) to write that flow's draft to \`tests/login.spec.ts\`. Typical loop: drive flow A \u2192 recorder_get_session \u2192 recorder_reset({ name: "checkout" }) \u2192 drive flow B \u2192 recorder_get_session \u2192 ...
55
+
54
56
  \`recorder_get_session\` also reads sessions from a separate \`playwright-codegen-pro codegen <url>\` run (\`.playwright-session.md\` live, or \`.playwright-prompt.md\` after the user clicks "Generate Test"). Secrets are redacted in all cases.`;
55
57
  function decorateMCPCommand(command) {
56
58
  command.option("--allowed-hosts <hosts...>", "comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.", import_config.commaSeparatedList).option("--allowed-origins <origins>", "semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all.\nImportant: *does not* serve as a security boundary and *does not* affect redirects. ", import_config.semicolonSeparatedList).option("--allow-unrestricted-file-access", "allow access to files outside of the workspace roots. Also allows unrestricted access to file:// URLs. By default access to file system is restricted to workspace root directories (or cwd if no roots are configured) only, and navigation to file:// URLs is blocked.").option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.\nImportant: *does not* serve as a security boundary and *does not* affect redirects.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf, devtools.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--cdp-timeout <timeout>", "timeout in milliseconds for connecting to CDP endpoint, defaults to 30000ms", import_config.numberParser).option("--codegen <lang>", 'specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".', import_config.enumParser.bind(null, "--codegen", ["none", "typescript"])).option("--config <path>", "path to the configuration file.").option("--console-level <level>", 'level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.', import_config.enumParser.bind(null, "--console-level", ["error", "warning", "info", "debug"])).option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--init-page <path...>", "path to TypeScript file to evaluate on Playwright page object").option("--init-script <path...>", "path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".', import_config.enumParser.bind(null, "--image-responses", ["allow", "omit"])).option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--output-mode <mode>", 'whether to save snapshots, console messages, network logs to a file or to the standard output. Can be "file" or "stdout". Default is "stdout".', import_config.enumParser.bind(null, "--output-mode", ["file", "stdout"])).option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--sandbox", "enable the sandbox for all process types that are normally not sandboxed.").option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--snapshot-mode <mode>", 'when taking snapshots for responses, specifies the mode to use. Can be "full" or "none". Default is "full".').option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--test-id-attribute <attribute>", 'specify the attribute to use for test ids, defaults to "data-testid"').option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280x720"', import_config.resolutionParser.bind(null, "--viewport-size")).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
@@ -0,0 +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-ConrJv9G.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 workbench-loader-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.0.5"]})]}),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
+ 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.CzAVbF3D.js"></script>
10
+ <script type="module" crossorigin src="./index.hhnlQ6BY.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-ConrJv9G.js">
12
12
  <link rel="stylesheet" crossorigin href="./defaultSettingsView.B4dS75f0.css">
13
13
  <link rel="stylesheet" crossorigin href="./index.CzXZzn5A.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-codegen-pro-core",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Core engine for playwright-codegen-pro — AI-powered Playwright codegen with network capture and MCP integration",
5
5
  "repository": {
6
6
  "type": "git",