playwright-codegen-pro-core 1.0.1 → 1.0.3

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.
@@ -44,6 +44,7 @@ var import_utils2 = require("../utils");
44
44
  var import_ascii = require("../server/utils/ascii");
45
45
  var import_utilsBundle = require("../utilsBundle");
46
46
  var import_program = require("../tools/cli-client/program");
47
+ var import_program2 = require("../tools/mcp/program");
47
48
  var import_utilsBundle2 = require("../utilsBundle");
48
49
  const packageJSON = require("../../package.json");
49
50
  import_utilsBundle.program.version("Version " + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME));
@@ -299,11 +300,12 @@ Examples:
299
300
  $ show-trace
300
301
  $ show-trace https://example.com/trace.zip`);
301
302
  (0, import_traceCli.addTraceCommands)(import_utilsBundle.program, logErrorAndExit);
303
+ (0, import_program2.decorateMCPCommand)(import_utilsBundle.program.command("mcp").description("Start MCP server for AI tool integration"));
302
304
  import_utilsBundle.program.command("cli", { hidden: true }).allowExcessArguments(true).allowUnknownOption(true).action(async (options) => {
303
305
  process.argv.splice(process.argv.indexOf("cli"), 1);
304
306
  (0, import_program.program)();
305
307
  });
306
- async function launchContext(options, extraOptions) {
308
+ async function launchContext(options, extraOptions, extraContextOptions = {}) {
307
309
  validateOptions(options);
308
310
  const browserType = lookupBrowserType(options);
309
311
  const launchOptions = extraOptions;
@@ -312,7 +314,7 @@ async function launchContext(options, extraOptions) {
312
314
  launchOptions.handleSIGINT = false;
313
315
  const contextOptions = (
314
316
  // Copy the device descriptor since we have to compare and modify the options.
315
- options.device ? { ...playwright.devices[options.device] } : {}
317
+ options.device ? { ...playwright.devices[options.device], ...extraContextOptions } : { ...extraContextOptions }
316
318
  );
317
319
  if (!extraOptions.headless)
318
320
  contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1;
@@ -436,10 +438,17 @@ async function open(options, url) {
436
438
  async function codegen(options, url) {
437
439
  const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
438
440
  const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
441
+ const videoDir = import_path.default.join(process.cwd(), ".playwright-session-video");
442
+ try {
443
+ import_fs.default.rmSync(videoDir, { recursive: true, force: true });
444
+ } catch {
445
+ }
439
446
  const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
440
447
  headless: !!process.env.PWTEST_CLI_HEADLESS,
441
448
  executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
442
449
  tracesDir
450
+ }, {
451
+ recordVideo: { dir: videoDir, size: { width: 1280, height: 720 } }
443
452
  });
444
453
  const donePromise = new import_utils.ManualPromise();
445
454
  maybeSetupTestHooks(browser, closeBrowser, donePromise);
@@ -60,6 +60,9 @@ class RecorderApp {
60
60
  this._inspectedContext = null;
61
61
  this._scenarioName = "my scenario";
62
62
  this._throttledSessionFile = null;
63
+ this._screenshotDir = null;
64
+ this._actionScreenshots = /* @__PURE__ */ new Map();
65
+ this._screenshotCounter = 0;
63
66
  this._page = page;
64
67
  this._recorder = recorder;
65
68
  this._frontend = createRecorderFrontend(page);
@@ -73,8 +76,18 @@ class RecorderApp {
73
76
  };
74
77
  this._aiCodegen = !!params.aiCodegen;
75
78
  this._throttledOutputFile = params.outputFile ? new import_throttledFile.ThrottledFile(params.outputFile) : null;
76
- if (this._aiCodegen)
79
+ if (this._aiCodegen) {
77
80
  this._throttledSessionFile = new import_throttledFile.ThrottledFile(import_path.default.join(process.cwd(), ".playwright-session.md"));
81
+ this._screenshotDir = import_path.default.join(process.cwd(), ".playwright-session-screenshots");
82
+ try {
83
+ import_fs.default.rmSync(this._screenshotDir, { recursive: true, force: true });
84
+ } catch {
85
+ }
86
+ try {
87
+ import_fs.default.mkdirSync(this._screenshotDir, { recursive: true });
88
+ } catch {
89
+ }
90
+ }
78
91
  this._primaryGeneratorId = process.env.TEST_INSPECTOR_LANGUAGE || params.language || determinePrimaryGeneratorId(params.sdkLanguage);
79
92
  this._selectedGeneratorId = this._primaryGeneratorId;
80
93
  for (const languageGenerator of (0, import_languages.languageSet)()) {
@@ -113,6 +126,7 @@ class RecorderApp {
113
126
  this._networkCapture?.dispose();
114
127
  this._networkCapture = null;
115
128
  this._recorder.close();
129
+ void this._finalizeVideo();
116
130
  inspectedContext.close({ reason: "Recorder window closed" }).catch(() => {
117
131
  });
118
132
  this._page.browserContext.close({ reason: "Recorder window closed" }).catch(() => {
@@ -265,6 +279,7 @@ class RecorderApp {
265
279
  this._throttledSessionFile?.flush();
266
280
  this._networkCapture?.dispose();
267
281
  this._networkCapture = null;
282
+ void this._finalizeVideo();
268
283
  this._page.browserContext.close({ reason: "Recorder window closed" }).catch(() => {
269
284
  });
270
285
  });
@@ -300,6 +315,27 @@ class RecorderApp {
300
315
  this._actions.push(action);
301
316
  this._networkCapture?.onActionAdded(action);
302
317
  this._updateActions("reveal");
318
+ void this._captureScreenshotForAction(action);
319
+ }
320
+ async _captureScreenshotForAction(action) {
321
+ if (!this._screenshotDir || !this._inspectedContext)
322
+ return;
323
+ const page = findPageByGuid(this._inspectedContext, action.frame.pageGuid);
324
+ if (!page)
325
+ return;
326
+ const index = ++this._screenshotCounter;
327
+ const filename = `${String(index).padStart(3, "0")}-${action.action.name}.png`;
328
+ const filepath = import_path.default.join(this._screenshotDir, filename);
329
+ try {
330
+ const controller = new import_progress.ProgressController();
331
+ await controller.run(async (progress) => {
332
+ const buffer = await page.screenshot(progress, { type: "png", fullPage: false });
333
+ await import_fs.default.promises.writeFile(filepath, buffer);
334
+ });
335
+ this._actionScreenshots.set(action, filepath);
336
+ this._updateActions();
337
+ } catch {
338
+ }
303
339
  }
304
340
  _onSignalAdded(signal) {
305
341
  const lastAction = this._actions.findLast((a) => a.frame.pageGuid === signal.frame.pageGuid);
@@ -364,7 +400,9 @@ class RecorderApp {
364
400
  scenarioName: this._scenarioName,
365
401
  outputFile: "tests/" + this._scenarioName.replace(/\s+/g, "-") + ".spec.ts",
366
402
  mode: "clipboard",
367
- pageHasWebSockets: false
403
+ pageHasWebSockets: false,
404
+ screenshots: this._actionScreenshots,
405
+ videoPath: this._aiCodegen ? ".playwright-session.webm" : void 0
368
406
  });
369
407
  this._throttledSessionFile.setContent(prompt);
370
408
  }
@@ -399,7 +437,9 @@ class RecorderApp {
399
437
  scenarioName,
400
438
  outputFile,
401
439
  mode: "clipboard",
402
- pageHasWebSockets: false
440
+ pageHasWebSockets: false,
441
+ screenshots: this._actionScreenshots,
442
+ videoPath: this._aiCodegen ? ".playwright-session.webm" : void 0
403
443
  });
404
444
  const promptFilePath = import_path.default.join(process.cwd(), ".playwright-prompt.md");
405
445
  await import_fs.default.promises.writeFile(promptFilePath, prompt, "utf-8");
@@ -414,6 +454,26 @@ class RecorderApp {
414
454
  this._emitGenerationStatus({ status: "error", message: String(error?.message ?? error), progress: 0 });
415
455
  }
416
456
  }
457
+ async _finalizeVideo() {
458
+ if (!this._aiCodegen)
459
+ return;
460
+ const videoDir = import_path.default.join(process.cwd(), ".playwright-session-video");
461
+ const finalPath = import_path.default.join(process.cwd(), ".playwright-session.webm");
462
+ for (let attempt = 0; attempt < 10; attempt++) {
463
+ try {
464
+ const files = await import_fs.default.promises.readdir(videoDir);
465
+ const webmFile = files.find((f) => f.endsWith(".webm"));
466
+ if (webmFile) {
467
+ await import_fs.default.promises.rename(import_path.default.join(videoDir, webmFile), finalPath);
468
+ await import_fs.default.promises.rm(videoDir, { recursive: true, force: true }).catch(() => {
469
+ });
470
+ return;
471
+ }
472
+ } catch {
473
+ }
474
+ await new Promise((resolve) => setTimeout(resolve, 200));
475
+ }
476
+ }
417
477
  async _runGeneration(scenarioName, outputFile) {
418
478
  if (!process.env.PW_AI_ENDPOINT) {
419
479
  await this._exportPrompt(scenarioName, outputFile);
@@ -55,6 +55,10 @@ function buildPrompt(session, options) {
55
55
  const a = ctx.action;
56
56
  const location = a.selector ? `${a.selector}` : a.url ?? "";
57
57
  sessionSection += `- ${a.name}: ${location} at t=${ctx.startTime}ms
58
+ `;
59
+ const screenshot = options.screenshots?.get(ctx);
60
+ if (screenshot)
61
+ sessionSection += ` Screenshot: ${import_path.default.relative(process.cwd(), screenshot)}
58
62
  `;
59
63
  const direct = (ctx.networkEvents ?? []).filter((e) => e.bucket === "direct");
60
64
  const pageLoad = (ctx.networkEvents ?? []).filter((e) => e.bucket === "pageLoad");
@@ -92,13 +96,25 @@ function buildPrompt(session, options) {
92
96
  cleanupSection = "## Data Created During Session\nNo data was created. No cleanup needed.\n";
93
97
  }
94
98
  const wsSection = hasWebSockets ? "## WebSocket Note\nThis page uses WebSocket connections. Assert on UI state changes that reflect server pushes rather than trying to intercept WS frames.\n\n" : "";
99
+ const screenshotCount = options.screenshots?.size ?? 0;
100
+ let visualSection = "";
101
+ if (screenshotCount > 0 || options.videoPath) {
102
+ visualSection = "## Visual Context\n";
103
+ if (screenshotCount > 0)
104
+ visualSection += `Per-action screenshots: ${screenshotCount} file(s) under .playwright-session-screenshots/ \u2014 referenced inline below. Read them as visual context for each step.
105
+ `;
106
+ if (options.videoPath)
107
+ visualSection += `Full session video: ${options.videoPath} (finalized when the recorder window is closed).
108
+ `;
109
+ visualSection += "\n";
110
+ }
95
111
  return `# Playwright Test Generation Request
96
112
 
97
113
  ## Scenario
98
114
  Name: ${options.scenarioName}
99
115
  Target file: ${relativeOutput}
100
116
 
101
- ## Recorded Session
117
+ ${visualSection}## Recorded Session
102
118
  ${sessionSection.trimEnd()}
103
119
 
104
120
  ${cleanupSection}
@@ -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.2"]})]}),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.EVGp-u_4.js"></script>
10
+ <script type="module" crossorigin src="./index.CNL5E5VV.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.1",
3
+ "version": "1.0.3",
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",