@@ -28,6 +28,8 @@ import type * as playwright from '../../../types/test';
28
28
import type { Config , ToolCapability } from '../config' ;
29
29
import type { ClientInfo } from '../sdk/server' ;
30
30
31
+ type ViewportSize = { width : number ; height : number } ;
32
+
31
33
export type CLIOptions = {
32
34
allowedOrigins ?: string [ ] ;
33
35
blockedOrigins ?: string [ ] ;
@@ -53,14 +55,15 @@ export type CLIOptions = {
53
55
proxyServer ?: string ;
54
56
saveSession ?: boolean ;
55
57
saveTrace ?: boolean ;
58
+ saveVideo ?: ViewportSize ;
56
59
secrets ?: Record < string , string > ;
57
60
sharedBrowserContext ?: boolean ;
58
61
storageState ?: string ;
59
62
timeoutAction ?: number ;
60
63
timeoutNavigation ?: number ;
61
64
userAgent ?: string ;
62
65
userDataDir ?: string ;
63
- viewportSize ?: string ;
66
+ viewportSize ?: ViewportSize ;
64
67
} ;
65
68
66
69
export const defaultConfig : FullConfig = {
@@ -127,6 +130,8 @@ async function validateConfig(config: FullConfig): Promise<void> {
127
130
throw new Error ( `Init script file does not exist: ${ script } ` ) ;
128
131
}
129
132
}
133
+ if ( config . sharedBrowserContext && config . saveVideo )
134
+ throw new Error ( 'saveVideo is not supported when sharedBrowserContext is true' ) ;
130
135
}
131
136
132
137
export function configFromCLIOptions ( cliOptions : CLIOptions ) : Config {
@@ -183,16 +188,8 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
183
188
if ( cliOptions . userAgent )
184
189
contextOptions . userAgent = cliOptions . userAgent ;
185
190
186
- if ( cliOptions . viewportSize ) {
187
- try {
188
- const [ width , height ] = cliOptions . viewportSize . split ( ',' ) . map ( n => + n ) ;
189
- if ( isNaN ( width ) || isNaN ( height ) )
190
- throw new Error ( 'bad values' ) ;
191
- contextOptions . viewport = { width, height } ;
192
- } catch ( e ) {
193
- throw new Error ( 'Invalid viewport size format: use "width,height", for example --viewport-size="800,600"' ) ;
194
- }
195
- }
191
+ if ( cliOptions . viewportSize )
192
+ contextOptions . viewport = cliOptions . viewportSize ;
196
193
197
194
if ( cliOptions . ignoreHttpsErrors )
198
195
contextOptions . ignoreHTTPSErrors = true ;
@@ -203,6 +200,14 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
203
200
if ( cliOptions . grantPermissions )
204
201
contextOptions . permissions = cliOptions . grantPermissions ;
205
202
203
+ if ( cliOptions . saveVideo ) {
204
+ contextOptions . recordVideo = {
205
+ // Videos are moved to output directory on saveAs.
206
+ dir : tmpDir ( ) ,
207
+ size : cliOptions . saveVideo ,
208
+ } ;
209
+ }
210
+
206
211
const result : Config = {
207
212
browser : {
208
213
browserName,
@@ -225,6 +230,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
225
230
} ,
226
231
saveSession : cliOptions . saveSession ,
227
232
saveTrace : cliOptions . saveTrace ,
233
+ saveVideo : cliOptions . saveVideo ,
228
234
secrets : cliOptions . secrets ,
229
235
sharedBrowserContext : cliOptions . sharedBrowserContext ,
230
236
outputDir : cliOptions . outputDir ,
@@ -266,13 +272,14 @@ function configFromEnv(): Config {
266
272
options . proxyBypass = envToString ( process . env . PLAYWRIGHT_MCP_PROXY_BYPASS ) ;
267
273
options . proxyServer = envToString ( process . env . PLAYWRIGHT_MCP_PROXY_SERVER ) ;
268
274
options . saveTrace = envToBoolean ( process . env . PLAYWRIGHT_MCP_SAVE_TRACE ) ;
275
+ options . saveVideo = resolutionParser ( '--save-video' , process . env . PLAYWRIGHT_MCP_SAVE_VIDEO ) ;
269
276
options . secrets = dotenvFileLoader ( process . env . PLAYWRIGHT_MCP_SECRETS_FILE ) ;
270
277
options . storageState = envToString ( process . env . PLAYWRIGHT_MCP_STORAGE_STATE ) ;
271
278
options . timeoutAction = numberParser ( process . env . PLAYWRIGHT_MCP_TIMEOUT_ACTION ) ;
272
279
options . timeoutNavigation = numberParser ( process . env . PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION ) ;
273
280
options . userAgent = envToString ( process . env . PLAYWRIGHT_MCP_USER_AGENT ) ;
274
281
options . userDataDir = envToString ( process . env . PLAYWRIGHT_MCP_USER_DATA_DIR ) ;
275
- options . viewportSize = envToString ( process . env . PLAYWRIGHT_MCP_VIEWPORT_SIZE ) ;
282
+ options . viewportSize = resolutionParser ( '--viewport-size' , process . env . PLAYWRIGHT_MCP_VIEWPORT_SIZE ) ;
276
283
return configFromCLIOptions ( options ) ;
277
284
}
278
285
@@ -287,27 +294,35 @@ async function loadConfig(configFile: string | undefined): Promise<Config> {
287
294
}
288
295
}
289
296
290
- export async function outputFile ( config : FullConfig , clientInfo : ClientInfo , fileName : string , options : { origin : 'code' | 'llm' | 'web' } ) : Promise < string > {
297
+ function tmpDir ( ) : string {
298
+ return path . join ( process . env . PW_TMPDIR_FOR_TEST ?? os . tmpdir ( ) , 'playwright-mcp-output' ) ;
299
+ }
300
+
301
+ export function outputDir ( config : FullConfig , clientInfo : ClientInfo ) : string {
291
302
const rootPath = firstRootPath ( clientInfo ) ;
292
- const outputDir = config . outputDir
303
+ return config . outputDir
293
304
?? ( rootPath ? path . join ( rootPath , '.playwright-mcp' ) : undefined )
294
- ?? path . join ( process . env . PW_TMPDIR_FOR_TEST ?? os . tmpdir ( ) , 'playwright-mcp-output' , String ( clientInfo . timestamp ) ) ;
305
+ ?? path . join ( tmpDir ( ) , String ( clientInfo . timestamp ) ) ;
306
+ }
307
+
308
+ export async function outputFile ( config : FullConfig , clientInfo : ClientInfo , fileName : string , options : { origin : 'code' | 'llm' | 'web' } ) : Promise < string > {
309
+ const dir = outputDir ( config , clientInfo ) ;
295
310
296
311
// Trust code.
297
312
if ( options . origin === 'code' )
298
- return path . resolve ( outputDir , fileName ) ;
313
+ return path . resolve ( dir , fileName ) ;
299
314
300
315
// Trust llm to use valid characters in file names.
301
316
if ( options . origin === 'llm' ) {
302
317
fileName = fileName . split ( '\\' ) . join ( '/' ) ;
303
- const resolvedFile = path . resolve ( outputDir , fileName ) ;
304
- if ( ! resolvedFile . startsWith ( path . resolve ( outputDir ) + path . sep ) )
318
+ const resolvedFile = path . resolve ( dir , fileName ) ;
319
+ if ( ! resolvedFile . startsWith ( path . resolve ( dir ) + path . sep ) )
305
320
throw new Error ( `Resolved file path for ${ fileName } is outside of the output directory` ) ;
306
321
return resolvedFile ;
307
322
}
308
323
309
324
// Do not trust web, at all.
310
- return path . join ( outputDir , sanitizeForFilePath ( fileName ) ) ;
325
+ return path . join ( dir , sanitizeForFilePath ( fileName ) ) ;
311
326
}
312
327
313
328
function pickDefined < T extends object > ( obj : T | undefined ) : Partial < T > {
@@ -379,6 +394,27 @@ export function numberParser(value: string | undefined): number | undefined {
379
394
return + value ;
380
395
}
381
396
397
+ export function resolutionParser ( name : string , value : string | undefined ) : ViewportSize | undefined {
398
+ if ( ! value )
399
+ return undefined ;
400
+ if ( value . includes ( 'x' ) ) {
401
+ const [ width , height ] = value . split ( 'x' ) . map ( v => + v ) ;
402
+ if ( isNaN ( width ) || isNaN ( height ) || width <= 0 || height <= 0 )
403
+ throw new Error ( `Invalid resolution format: use ${ name } ="800x600"` ) ;
404
+ return { width, height } ;
405
+ }
406
+
407
+ // Legacy format
408
+ if ( value . includes ( ',' ) ) {
409
+ const [ width , height ] = value . split ( ',' ) . map ( v => + v ) ;
410
+ if ( isNaN ( width ) || isNaN ( height ) || width <= 0 || height <= 0 )
411
+ throw new Error ( `Invalid resolution format: use ${ name } ="800x600"` ) ;
412
+ return { width, height } ;
413
+ }
414
+
415
+ throw new Error ( `Invalid resolution format: use ${ name } ="800x600"` ) ;
416
+ }
417
+
382
418
export function headerParser ( arg : string | undefined , previous ?: Record < string , string > ) : Record < string , string > {
383
419
if ( ! arg )
384
420
return previous || { } ;
0 commit comments