playwright-codegen-pro-core 1.0.2 → 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.
@@ -305,7 +305,7 @@ import_utilsBundle.program.command("cli", { hidden: true }).allowExcessArguments
305
305
  process.argv.splice(process.argv.indexOf("cli"), 1);
306
306
  (0, import_program.program)();
307
307
  });
308
- async function launchContext(options, extraOptions) {
308
+ async function launchContext(options, extraOptions, extraContextOptions = {}) {
309
309
  validateOptions(options);
310
310
  const browserType = lookupBrowserType(options);
311
311
  const launchOptions = extraOptions;
@@ -314,7 +314,7 @@ async function launchContext(options, extraOptions) {
314
314
  launchOptions.handleSIGINT = false;
315
315
  const contextOptions = (
316
316
  // Copy the device descriptor since we have to compare and modify the options.
317
- options.device ? { ...playwright.devices[options.device] } : {}
317
+ options.device ? { ...playwright.devices[options.device], ...extraContextOptions } : { ...extraContextOptions }
318
318
  );
319
319
  if (!extraOptions.headless)
320
320
  contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1;
@@ -438,10 +438,17 @@ async function open(options, url) {
438
438
  async function codegen(options, url) {
439
439
  const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
440
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
+ }
441
446
  const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
442
447
  headless: !!process.env.PWTEST_CLI_HEADLESS,
443
448
  executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
444
449
  tracesDir
450
+ }, {
451
+ recordVideo: { dir: videoDir, size: { width: 1280, height: 720 } }
445
452
  });
446
453
  const donePromise = new import_utils.ManualPromise();
447
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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-codegen-pro-core",
3
- "version": "1.0.2",
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",