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