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.
package/lib/cli/program.js
CHANGED
|
@@ -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