2023-04-25 11:19:37 -07:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
/* eslint-disable no-console */
import fs from 'fs' ;
import os from 'os' ;
import path from 'path' ;
import type { Command } from '../utilsBundle' ;
2024-08-23 10:19:36 -07:00
import { program , dotenv } from '../utilsBundle' ;
2024-01-23 15:22:09 -08:00
export { program } from '../utilsBundle' ;
2023-04-25 11:19:37 -07:00
import { runDriver , runServer , printApiJson , launchBrowserServer } from './driver' ;
2024-03-18 09:50:11 -07:00
import { runTraceInBrowser , runTraceViewerApp } from '../server/trace/viewer/traceViewer' ;
import type { TraceViewerServerOptions } from '../server/trace/viewer/traceViewer' ;
2023-04-25 11:19:37 -07:00
import * as playwright from '../..' ;
import type { BrowserContext } from '../client/browserContext' ;
import type { Browser } from '../client/browser' ;
import type { Page } from '../client/page' ;
import type { BrowserType } from '../client/browserType' ;
import type { BrowserContextOptions , LaunchOptions } from '../client/types' ;
2023-08-17 17:53:08 +02:00
import { wrapInASCIIBox , isLikelyNpxGlobal , assert , gracefullyProcessExitDoNotHang , getPackageManagerExecCommand } from '../utils' ;
2023-04-25 11:19:37 -07:00
import type { Executable } from '../server' ;
import { registry , writeDockerVersion } from '../server' ;
2023-10-17 21:34:02 -07:00
import { isTargetClosedError } from '../client/errors' ;
2023-04-25 11:19:37 -07:00
const packageJSON = require ( '../../package.json' ) ;
program
. version ( 'Version ' + ( process . env . PW_CLI_DISPLAY_VERSION || packageJSON . version ) )
. name ( buildBasePlaywrightCLICommand ( process . env . PW_LANG_NAME ) ) ;
program
. command ( 'mark-docker-image [dockerImageNameTemplate]' , { hidden : true } )
. description ( 'mark docker image' )
. allowUnknownOption ( true )
. action ( function ( dockerImageNameTemplate ) {
assert ( dockerImageNameTemplate , 'dockerImageNameTemplate is required' ) ;
writeDockerVersion ( dockerImageNameTemplate ) . catch ( logErrorAndExit ) ;
} ) ;
commandWithOpenOptions ( 'open [url]' , 'open page in browser specified via -b, --browser' , [ ] )
. action ( function ( url , options ) {
open ( options , url , codegenId ( ) ) . catch ( logErrorAndExit ) ;
} )
. addHelpText ( 'afterAll' , `
Examples :
$ open
$ open - b webkit https : //example.com`);
commandWithOpenOptions ( 'codegen [url]' , 'open page and generate code for user actions' ,
[
[ '-o, --output <file name>' , 'saves the generated script to a file' ] ,
2024-02-20 20:08:53 +01:00
[ '--target <language>' , ` language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit ` , codegenId ( ) ] ,
2023-04-25 11:19:37 -07:00
[ '--save-trace <filename>' , 'record a trace for the session and save it to a file' ] ,
2023-04-29 12:04:33 -07:00
[ '--test-id-attribute <attributeName>' , 'use the specified attribute to generate data test ID selectors' ] ,
2023-04-25 11:19:37 -07:00
] ) . action ( function ( url , options ) {
2023-04-29 12:04:33 -07:00
codegen ( options , url ) . catch ( logErrorAndExit ) ;
2023-04-25 11:19:37 -07:00
} ) . addHelpText ( 'afterAll' , `
Examples :
$ codegen
$ codegen -- target = python
$ codegen - b webkit https : //example.com`);
function suggestedBrowsersToInstall() {
return registry . executables ( ) . filter ( e = > e . installType !== 'none' && e . type !== 'tool' ) . map ( e = > e . name ) . join ( ', ' ) ;
}
2024-11-14 12:20:44 +00:00
function defaultBrowsersToInstall ( options : { noShell? : boolean , onlyShell? : boolean } ) : Executable [ ] {
let executables = registry . defaultExecutables ( ) ;
if ( options . noShell )
executables = executables . filter ( e = > e . name !== 'chromium-headless-shell' ) ;
if ( options . onlyShell )
executables = executables . filter ( e = > e . name !== 'chromium' ) ;
return executables ;
}
function checkBrowsersToInstall ( args : string [ ] , options : { noShell? : boolean , onlyShell? : boolean } ) : Executable [ ] {
if ( options . noShell && options . onlyShell )
throw new Error ( ` Only one of --no-shell and --only-shell can be specified ` ) ;
2023-04-25 11:19:37 -07:00
const faultyArguments : string [ ] = [ ] ;
const executables : Executable [ ] = [ ] ;
2024-11-14 12:20:44 +00:00
const handleArgument = ( arg : string ) = > {
2023-04-25 11:19:37 -07:00
const executable = registry . findExecutable ( arg ) ;
if ( ! executable || executable . installType === 'none' )
faultyArguments . push ( arg ) ;
else
executables . push ( executable ) ;
2024-11-14 12:20:44 +00:00
if ( executable ? . browserName === 'chromium' )
executables . push ( registry . findExecutable ( 'ffmpeg' ) ! ) ;
} ;
for ( const arg of args ) {
if ( arg === 'chromium' ) {
if ( ! options . onlyShell )
handleArgument ( 'chromium' ) ;
if ( ! options . noShell )
handleArgument ( 'chromium-headless-shell' ) ;
} else {
handleArgument ( arg ) ;
}
2023-04-25 11:19:37 -07:00
}
2024-11-14 12:20:44 +00:00
2023-07-24 08:29:29 -07:00
if ( faultyArguments . length )
throw new Error ( ` Invalid installation targets: ${ faultyArguments . map ( name = > ` ' ${ name } ' ` ) . join ( ', ' ) } . Expecting one of: ${ suggestedBrowsersToInstall ( ) } ` ) ;
2023-04-25 11:19:37 -07:00
return executables ;
}
program
. command ( 'install [browser...]' )
. description ( 'ensure browsers necessary for this version of Playwright are installed' )
. option ( '--with-deps' , 'install system dependencies for browsers' )
. option ( '--dry-run' , 'do not execute installation, only print information' )
. option ( '--force' , 'force reinstall of stable browser channels' )
2024-11-14 12:20:44 +00:00
. option ( '--only-shell' , 'only install headless shell when installing chromium' )
. option ( '--no-shell' , 'do not install chromium headless shell' )
. action ( async function ( args : string [ ] , options : { withDeps? : boolean , force? : boolean , dryRun? : boolean , shell? : boolean , noShell? : boolean , onlyShell? : boolean } ) {
// For '--no-shell' option, commander sets `shell: false` instead.
if ( options . shell === false )
options . noShell = true ;
2023-04-25 11:19:37 -07:00
if ( isLikelyNpxGlobal ( ) ) {
console . error ( wrapInASCIIBox ( [
` WARNING: It looks like you are running 'npx playwright install' without first ` ,
` installing your project's dependencies. ` ,
` ` ,
` To avoid unexpected behavior, please install your dependencies first, and ` ,
` then run Playwright's install command: ` ,
` ` ,
` npm install ` ,
` npx playwright install ` ,
` ` ,
` If your project does not yet depend on Playwright, first install the ` ,
` applicable npm package (most commonly @playwright/test), and ` ,
` then run Playwright's install command to download the browsers: ` ,
` ` ,
` npm install @playwright/test ` ,
` npx playwright install ` ,
` ` ,
] . join ( '\n' ) , 1 ) ) ;
}
try {
const hasNoArguments = ! args . length ;
2024-11-14 12:20:44 +00:00
const executables = hasNoArguments ? defaultBrowsersToInstall ( options ) : checkBrowsersToInstall ( args , options ) ;
2023-04-25 11:19:37 -07:00
if ( options . withDeps )
await registry . installDeps ( executables , ! ! options . dryRun ) ;
if ( options . dryRun ) {
for ( const executable of executables ) {
const version = executable . browserVersion ? ` version ` + executable . browserVersion : '' ;
console . log ( ` browser: ${ executable . name } ${ version ? ' ' + version : '' } ` ) ;
console . log ( ` Install location: ${ executable . directory ? ? '<system>' } ` ) ;
if ( executable . downloadURLs ? . length ) {
const [ url , . . . fallbacks ] = executable . downloadURLs ;
console . log ( ` Download url: ${ url } ` ) ;
for ( let i = 0 ; i < fallbacks . length ; ++ i )
console . log ( ` Download fallback ${ i + 1 } : ${ fallbacks [ i ] } ` ) ;
}
console . log ( ` ` ) ;
}
} else {
const forceReinstall = hasNoArguments ? false : ! ! options . force ;
await registry . install ( executables , forceReinstall ) ;
2024-01-25 20:55:53 +01:00
await registry . validateHostRequirementsForExecutablesIfNeeded ( executables , process . env . PW_LANG_NAME || 'javascript' ) . catch ( ( e : Error ) = > {
e . name = 'Playwright Host validation warning' ;
console . error ( e ) ;
} ) ;
2023-04-25 11:19:37 -07:00
}
} catch ( e ) {
console . log ( ` Failed to install browsers \ n ${ e } ` ) ;
2023-07-24 08:29:29 -07:00
gracefullyProcessExitDoNotHang ( 1 ) ;
2023-04-25 11:19:37 -07:00
}
} ) . addHelpText ( 'afterAll' , `
Examples :
- $ install
Install default browsers .
- $ install chrome firefox
Install custom browsers , supports $ { suggestedBrowsersToInstall ( ) } . ` );
2023-06-05 18:50:21 +02:00
program
. command ( 'uninstall' )
2023-06-06 22:09:15 +02:00
. description ( 'Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.' )
. option ( '--all' , 'Removes all browsers used by any Playwright installation from the system.' )
2023-06-05 18:50:21 +02:00
. action ( async ( options : { all? : boolean } ) = > {
2023-08-24 07:33:32 -07:00
delete process . env . PLAYWRIGHT_SKIP_BROWSER_GC ;
2023-06-05 18:50:21 +02:00
await registry . uninstall ( ! ! options . all ) . then ( ( { numberOfBrowsersLeft } ) = > {
if ( ! options . all && numberOfBrowsersLeft > 0 ) {
console . log ( 'Successfully uninstalled Playwright browsers for the current Playwright installation.' ) ;
2023-06-06 22:09:15 +02:00
console . log ( ` There are still ${ numberOfBrowsersLeft } browsers left, used by other Playwright installations. \ nTo uninstall Playwright browsers for all installations, re-run with --all flag. ` ) ;
2023-06-05 18:50:21 +02:00
}
} ) . catch ( logErrorAndExit ) ;
} ) ;
2023-04-25 11:19:37 -07:00
program
. command ( 'install-deps [browser...]' )
. description ( 'install dependencies necessary to run browsers (will ask for sudo permissions)' )
. option ( '--dry-run' , 'Do not execute installation commands, only print them' )
. action ( async function ( args : string [ ] , options : { dryRun? : boolean } ) {
try {
if ( ! args . length )
2024-11-14 12:20:44 +00:00
await registry . installDeps ( defaultBrowsersToInstall ( { } ) , ! ! options . dryRun ) ;
2023-04-25 11:19:37 -07:00
else
2024-11-14 12:20:44 +00:00
await registry . installDeps ( checkBrowsersToInstall ( args , { } ) , ! ! options . dryRun ) ;
2023-04-25 11:19:37 -07:00
} catch ( e ) {
console . log ( ` Failed to install browser dependencies \ n ${ e } ` ) ;
2023-07-24 08:29:29 -07:00
gracefullyProcessExitDoNotHang ( 1 ) ;
2023-04-25 11:19:37 -07:00
}
} ) . addHelpText ( 'afterAll' , `
Examples :
- $ install - deps
Install dependencies for default browsers .
- $ install - deps chrome firefox
Install dependencies for specific browsers , supports $ { suggestedBrowsersToInstall ( ) } . ` );
const browsers = [
{ alias : 'cr' , name : 'Chromium' , type : 'chromium' } ,
{ alias : 'ff' , name : 'Firefox' , type : 'firefox' } ,
{ alias : 'wk' , name : 'WebKit' , type : 'webkit' } ,
] ;
for ( const { alias , name , type } of browsers ) {
commandWithOpenOptions ( ` ${ alias } [url] ` , ` open page in ${ name } ` , [ ] )
. action ( function ( url , options ) {
open ( { . . . options , browser : type } , url , options . target ) . catch ( logErrorAndExit ) ;
} ) . addHelpText ( 'afterAll' , `
Examples :
$ $ { alias } https : //example.com`);
}
commandWithOpenOptions ( 'screenshot <url> <filename>' , 'capture a page screenshot' ,
[
[ '--wait-for-selector <selector>' , 'wait for selector before taking a screenshot' ] ,
[ '--wait-for-timeout <timeout>' , 'wait for timeout in milliseconds before taking a screenshot' ] ,
[ '--full-page' , 'whether to take a full page screenshot (entire scrollable area)' ] ,
] ) . action ( function ( url , filename , command ) {
screenshot ( command , command , url , filename ) . catch ( logErrorAndExit ) ;
} ) . addHelpText ( 'afterAll' , `
Examples :
$ screenshot - b webkit https : //example.com example.png`);
commandWithOpenOptions ( 'pdf <url> <filename>' , 'save page as pdf' ,
[
[ '--wait-for-selector <selector>' , 'wait for given selector before saving as pdf' ] ,
[ '--wait-for-timeout <timeout>' , 'wait for given timeout in milliseconds before saving as pdf' ] ,
] ) . action ( function ( url , filename , options ) {
pdf ( options , options , url , filename ) . catch ( logErrorAndExit ) ;
} ) . addHelpText ( 'afterAll' , `
Examples :
$ pdf https : //example.com example.pdf`);
program
. command ( 'run-driver' , { hidden : true } )
. action ( function ( options ) {
runDriver ( ) ;
} ) ;
program
. command ( 'run-server' , { hidden : true } )
. option ( '--port <port>' , 'Server port' )
2023-12-19 23:19:46 +01:00
. option ( '--host <host>' , 'Server host' )
2023-04-25 11:19:37 -07:00
. option ( '--path <path>' , 'Endpoint Path' , '/' )
. option ( '--max-clients <maxClients>' , 'Maximum clients' )
2023-05-23 10:56:37 -07:00
. option ( '--mode <mode>' , 'Server mode, either "default" or "extension"' )
2023-04-25 11:19:37 -07:00
. action ( function ( options ) {
runServer ( {
port : options.port ? + options.port : undefined ,
2023-12-19 23:19:46 +01:00
host : options.host ,
2023-04-25 11:19:37 -07:00
path : options.path ,
maxConnections : options.maxClients ? + options.maxClients : Infinity ,
2023-05-23 10:56:37 -07:00
extension : options.mode === 'extension' || ! ! process . env . PW_EXTENSION_MODE ,
2023-04-25 11:19:37 -07:00
} ) . catch ( logErrorAndExit ) ;
} ) ;
program
. command ( 'print-api-json' , { hidden : true } )
. action ( function ( options ) {
printApiJson ( ) ;
} ) ;
program
. command ( 'launch-server' , { hidden : true } )
. requiredOption ( '--browser <browserName>' , 'Browser name, one of "chromium", "firefox" or "webkit"' )
. option ( '--config <path-to-config-file>' , 'JSON file with launchServer options' )
. action ( function ( options ) {
launchBrowserServer ( options . browser , options . config ) ;
} ) ;
program
. command ( 'show-trace [trace...]' )
. option ( '-b, --browser <browserType>' , 'browser to use, one of cr, chromium, ff, firefox, wk, webkit' , 'chromium' )
2023-06-06 09:36:49 -07:00
. option ( '-h, --host <host>' , 'Host to serve trace on; specifying this option opens trace in a browser tab' )
. option ( '-p, --port <port>' , 'Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab' )
2023-05-25 15:38:47 -07:00
. option ( '--stdin' , 'Accept trace URLs over stdin to update the viewer' )
2023-04-25 11:19:37 -07:00
. description ( 'show trace viewer' )
. action ( function ( traces , options ) {
if ( options . browser === 'cr' )
options . browser = 'chromium' ;
if ( options . browser === 'ff' )
options . browser = 'firefox' ;
if ( options . browser === 'wk' )
options . browser = 'webkit' ;
2024-03-18 09:50:11 -07:00
const openOptions : TraceViewerServerOptions = {
2023-06-06 09:36:49 -07:00
host : options.host ,
port : + options . port ,
isServer : ! ! options . stdin ,
} ;
2024-03-18 09:50:11 -07:00
if ( options . port !== undefined || options . host !== undefined )
runTraceInBrowser ( traces , openOptions ) . catch ( logErrorAndExit ) ;
else
runTraceViewerApp ( traces , options . browser , openOptions , true ) . catch ( logErrorAndExit ) ;
2023-04-25 11:19:37 -07:00
} ) . addHelpText ( 'afterAll' , `
Examples :
2024-09-17 15:32:30 +02:00
$ show - trace https : //example.com/trace.zip`);
2023-04-25 11:19:37 -07:00
type Options = {
browser : string ;
channel? : string ;
colorScheme? : string ;
device? : string ;
geolocation? : string ;
ignoreHttpsErrors? : boolean ;
lang? : string ;
loadStorage? : string ;
proxyServer? : string ;
proxyBypass? : string ;
blockServiceWorkers? : boolean ;
saveHar? : string ;
saveHarGlob? : string ;
saveStorage? : string ;
saveTrace? : string ;
timeout : string ;
timezone? : string ;
viewportSize? : string ;
userAgent? : string ;
} ;
type CaptureOptions = {
waitForSelector? : string ;
waitForTimeout? : string ;
fullPage : boolean ;
} ;
2024-09-12 13:39:44 -07:00
async function launchContext ( options : Options , extraOptions : LaunchOptions ) : Promise < { browser : Browser , browserName : string , launchOptions : LaunchOptions , contextOptions : BrowserContextOptions , context : BrowserContext } > {
2023-04-25 11:19:37 -07:00
validateOptions ( options ) ;
const browserType = lookupBrowserType ( options ) ;
2024-09-12 13:39:44 -07:00
const launchOptions : LaunchOptions = extraOptions ;
2023-04-25 11:19:37 -07:00
if ( options . channel )
launchOptions . channel = options . channel as any ;
launchOptions . handleSIGINT = false ;
const contextOptions : BrowserContextOptions =
// Copy the device descriptor since we have to compare and modify the options.
options . device ? { . . . playwright . devices [ options . device ] } : { } ;
// In headful mode, use host device scale factor for things to look nice.
// In headless, keep things the way it works in Playwright by default.
// Assume high-dpi on MacOS. TODO: this is not perfect.
2024-09-12 13:39:44 -07:00
if ( ! extraOptions . headless )
2023-04-25 11:19:37 -07:00
contextOptions . deviceScaleFactor = os . platform ( ) === 'darwin' ? 2 : 1 ;
// Work around the WebKit GTK scrolling issue.
if ( browserType . name ( ) === 'webkit' && process . platform === 'linux' ) {
delete contextOptions . hasTouch ;
delete contextOptions . isMobile ;
}
if ( contextOptions . isMobile && browserType . name ( ) === 'firefox' )
contextOptions . isMobile = undefined ;
if ( options . blockServiceWorkers )
contextOptions . serviceWorkers = 'block' ;
// Proxy
if ( options . proxyServer ) {
launchOptions . proxy = {
server : options.proxyServer
} ;
if ( options . proxyBypass )
launchOptions . proxy . bypass = options . proxyBypass ;
}
const browser = await browserType . launch ( launchOptions ) ;
if ( process . env . PWTEST_CLI_IS_UNDER_TEST ) {
( process as any ) . _didSetSourcesForTest = ( text : string ) = > {
process . stdout . write ( '\n-------------8<-------------\n' ) ;
process . stdout . write ( text ) ;
process . stdout . write ( '\n-------------8<-------------\n' ) ;
const autoExitCondition = process . env . PWTEST_CLI_AUTO_EXIT_WHEN ;
if ( autoExitCondition && text . includes ( autoExitCondition ) )
2024-09-20 13:08:33 -07:00
closeBrowser ( ) ;
2023-04-25 11:19:37 -07:00
} ;
// Make sure we exit abnormally when browser crashes.
const logs : string [ ] = [ ] ;
require ( 'playwright-core/lib/utilsBundle' ) . debug . log = ( . . . args : any [ ] ) = > {
const line = require ( 'util' ) . format ( . . . args ) + '\n' ;
logs . push ( line ) ;
process . stderr . write ( line ) ;
} ;
browser . on ( 'disconnected' , ( ) = > {
const hasCrashLine = logs . some ( line = > line . includes ( 'process did exit:' ) && ! line . includes ( 'process did exit: exitCode=0, signal=null' ) ) ;
if ( hasCrashLine ) {
process . stderr . write ( 'Detected browser crash.\n' ) ;
2023-07-24 08:29:29 -07:00
gracefullyProcessExitDoNotHang ( 1 ) ;
2023-04-25 11:19:37 -07:00
}
} ) ;
}
// Viewport size
if ( options . viewportSize ) {
try {
2024-11-29 14:13:53 +00:00
const [ width , height ] = options . viewportSize . split ( ',' ) . map ( n = > + n ) ;
if ( isNaN ( width ) || isNaN ( height ) )
throw new Error ( 'bad values' ) ;
2023-04-25 11:19:37 -07:00
contextOptions . viewport = { width , height } ;
} catch ( e ) {
2024-11-29 14:13:53 +00:00
throw new Error ( 'Invalid viewport size format: use "width,height", for example --viewport-size="800,600"' ) ;
2023-04-25 11:19:37 -07:00
}
}
// Geolocation
if ( options . geolocation ) {
try {
const [ latitude , longitude ] = options . geolocation . split ( ',' ) . map ( n = > parseFloat ( n . trim ( ) ) ) ;
contextOptions . geolocation = {
latitude ,
longitude
} ;
} catch ( e ) {
2023-07-24 08:29:29 -07:00
throw new Error ( 'Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"' ) ;
2023-04-25 11:19:37 -07:00
}
contextOptions . permissions = [ 'geolocation' ] ;
}
// User agent
if ( options . userAgent )
contextOptions . userAgent = options . userAgent ;
// Lang
if ( options . lang )
contextOptions . locale = options . lang ;
// Color scheme
if ( options . colorScheme )
contextOptions . colorScheme = options . colorScheme as 'dark' | 'light' ;
// Timezone
if ( options . timezone )
contextOptions . timezoneId = options . timezone ;
// Storage
if ( options . loadStorage )
contextOptions . storageState = options . loadStorage ;
if ( options . ignoreHttpsErrors )
contextOptions . ignoreHTTPSErrors = true ;
// HAR
if ( options . saveHar ) {
contextOptions . recordHar = { path : path.resolve ( process . cwd ( ) , options . saveHar ) , mode : 'minimal' } ;
if ( options . saveHarGlob )
contextOptions . recordHar . urlFilter = options . saveHarGlob ;
contextOptions . serviceWorkers = 'block' ;
}
// Close app when the last window closes.
const context = await browser . newContext ( contextOptions ) ;
let closingBrowser = false ;
async function closeBrowser() {
// We can come here multiple times. For example, saving storage creates
// a temporary page and we call closeBrowser again when that page closes.
if ( closingBrowser )
return ;
closingBrowser = true ;
if ( options . saveTrace )
await context . tracing . stop ( { path : options.saveTrace } ) ;
if ( options . saveStorage )
await context . storageState ( { path : options.saveStorage } ) . catch ( e = > null ) ;
if ( options . saveHar )
await context . close ( ) ;
await browser . close ( ) ;
}
context . on ( 'page' , page = > {
page . on ( 'dialog' , ( ) = > { } ) ; // Prevent dialogs from being automatically dismissed.
page . on ( 'close' , ( ) = > {
const hasPage = browser . contexts ( ) . some ( context = > context . pages ( ) . length > 0 ) ;
if ( hasPage )
return ;
// Avoid the error when the last page is closed because the browser has been closed.
2024-09-20 13:08:33 -07:00
closeBrowser ( ) . catch ( ( ) = > { } ) ;
2023-04-25 11:19:37 -07:00
} ) ;
} ) ;
process . on ( 'SIGINT' , async ( ) = > {
await closeBrowser ( ) ;
2023-07-24 08:29:29 -07:00
gracefullyProcessExitDoNotHang ( 130 ) ;
2023-04-25 11:19:37 -07:00
} ) ;
const timeout = options . timeout ? parseInt ( options . timeout , 10 ) : 0 ;
context . setDefaultTimeout ( timeout ) ;
context . setDefaultNavigationTimeout ( timeout ) ;
if ( options . saveTrace )
await context . tracing . start ( { screenshots : true , snapshots : true } ) ;
// Omit options that we add automatically for presentation purpose.
delete launchOptions . headless ;
delete launchOptions . executablePath ;
delete launchOptions . handleSIGINT ;
delete contextOptions . deviceScaleFactor ;
return { browser , browserName : browserType.name ( ) , context , contextOptions , launchOptions } ;
}
async function openPage ( context : BrowserContext , url : string | undefined ) : Promise < Page > {
const page = await context . newPage ( ) ;
if ( url ) {
if ( fs . existsSync ( url ) )
url = 'file://' + path . resolve ( url ) ;
else if ( ! url . startsWith ( 'http' ) && ! url . startsWith ( 'file://' ) && ! url . startsWith ( 'about:' ) && ! url . startsWith ( 'data:' ) )
url = 'http://' + url ;
await page . goto ( url ) . catch ( error = > {
2023-10-12 11:05:34 -07:00
if ( process . env . PWTEST_CLI_AUTO_EXIT_WHEN && isTargetClosedError ( error ) ) {
2023-04-25 11:19:37 -07:00
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
// in a stray navigation aborted error. We should ignore it.
} else {
throw error ;
}
} ) ;
}
return page ;
}
async function open ( options : Options , url : string | undefined , language : string ) {
2024-09-12 13:39:44 -07:00
const { context , launchOptions , contextOptions } = await launchContext ( options , { headless : ! ! process . env . PWTEST_CLI_HEADLESS , executablePath : process.env.PWTEST_CLI_EXECUTABLE_PATH } ) ;
2023-04-25 11:19:37 -07:00
await context . _enableRecorder ( {
language ,
launchOptions ,
contextOptions ,
device : options.device ,
saveStorage : options.saveStorage ,
2024-10-24 19:23:19 +02:00
handleSIGINT : false ,
2023-04-25 11:19:37 -07:00
} ) ;
await openPage ( context , url ) ;
}
2023-04-29 12:04:33 -07:00
async function codegen ( options : Options & { target : string , output? : string , testIdAttribute? : string } , url : string | undefined ) {
const { target : language , output : outputFile , testIdAttribute : testIdAttributeName } = options ;
2024-09-20 16:56:05 -07:00
const tracesDir = path . join ( os . tmpdir ( ) , ` playwright-recorder-trace- ${ Date . now ( ) } ` ) ;
2024-09-12 13:39:44 -07:00
const { context , launchOptions , contextOptions } = await launchContext ( options , {
headless : ! ! process . env . PWTEST_CLI_HEADLESS ,
executablePath : process.env.PWTEST_CLI_EXECUTABLE_PATH ,
tracesDir ,
} ) ;
2024-08-23 10:19:36 -07:00
dotenv . config ( { path : 'playwright.env' } ) ;
2023-04-25 11:19:37 -07:00
await context . _enableRecorder ( {
language ,
launchOptions ,
contextOptions ,
device : options.device ,
saveStorage : options.saveStorage ,
mode : 'recording' ,
2024-09-20 15:25:49 -07:00
codegenMode : process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions' ,
2023-04-29 12:04:33 -07:00
testIdAttributeName ,
2023-04-25 11:19:37 -07:00
outputFile : outputFile ? path . resolve ( outputFile ) : undefined ,
2024-10-24 19:23:19 +02:00
handleSIGINT : false ,
2023-04-25 11:19:37 -07:00
} ) ;
await openPage ( context , url ) ;
}
async function waitForPage ( page : Page , captureOptions : CaptureOptions ) {
if ( captureOptions . waitForSelector ) {
console . log ( ` Waiting for selector ${ captureOptions . waitForSelector } ... ` ) ;
await page . waitForSelector ( captureOptions . waitForSelector ) ;
}
if ( captureOptions . waitForTimeout ) {
console . log ( ` Waiting for timeout ${ captureOptions . waitForTimeout } ... ` ) ;
await page . waitForTimeout ( parseInt ( captureOptions . waitForTimeout , 10 ) ) ;
}
}
async function screenshot ( options : Options , captureOptions : CaptureOptions , url : string , path : string ) {
2024-09-12 13:39:44 -07:00
const { context } = await launchContext ( options , { headless : true } ) ;
2023-04-25 11:19:37 -07:00
console . log ( 'Navigating to ' + url ) ;
const page = await openPage ( context , url ) ;
await waitForPage ( page , captureOptions ) ;
console . log ( 'Capturing screenshot into ' + path ) ;
await page . screenshot ( { path , fullPage : ! ! captureOptions . fullPage } ) ;
// launchContext takes care of closing the browser.
await page . close ( ) ;
}
async function pdf ( options : Options , captureOptions : CaptureOptions , url : string , path : string ) {
2023-07-24 08:29:29 -07:00
if ( options . browser !== 'chromium' )
throw new Error ( 'PDF creation is only working with Chromium' ) ;
2024-09-12 13:39:44 -07:00
const { context } = await launchContext ( { . . . options , browser : 'chromium' } , { headless : true } ) ;
2023-04-25 11:19:37 -07:00
console . log ( 'Navigating to ' + url ) ;
const page = await openPage ( context , url ) ;
await waitForPage ( page , captureOptions ) ;
console . log ( 'Saving as pdf into ' + path ) ;
await page . pdf ! ( { path } ) ;
// launchContext takes care of closing the browser.
await page . close ( ) ;
}
function lookupBrowserType ( options : Options ) : BrowserType {
let name = options . browser ;
if ( options . device ) {
const device = playwright . devices [ options . device ] ;
name = device . defaultBrowserType ;
}
let browserType : any ;
switch ( name ) {
case 'chromium' : browserType = playwright . chromium ; break ;
case 'webkit' : browserType = playwright . webkit ; break ;
case 'firefox' : browserType = playwright . firefox ; break ;
case 'cr' : browserType = playwright . chromium ; break ;
case 'wk' : browserType = playwright . webkit ; break ;
case 'ff' : browserType = playwright . firefox ; break ;
}
if ( browserType )
return browserType ;
program . help ( ) ;
}
function validateOptions ( options : Options ) {
if ( options . device && ! ( options . device in playwright . devices ) ) {
2023-07-24 08:29:29 -07:00
const lines = [ ` Device descriptor not found: ' ${ options . device } ', available devices are: ` ] ;
2023-04-25 11:19:37 -07:00
for ( const name in playwright . devices )
2023-07-24 08:29:29 -07:00
lines . push ( ` " ${ name } " ` ) ;
throw new Error ( lines . join ( '\n' ) ) ;
2023-04-25 11:19:37 -07:00
}
2023-07-24 08:29:29 -07:00
if ( options . colorScheme && ! [ 'light' , 'dark' ] . includes ( options . colorScheme ) )
throw new Error ( 'Invalid color scheme, should be one of "light", "dark"' ) ;
2023-04-25 11:19:37 -07:00
}
function logErrorAndExit ( e : Error ) {
2023-07-24 08:29:29 -07:00
if ( process . env . PWDEBUGIMPL )
console . error ( e ) ;
else
console . error ( e . name + ': ' + e . message ) ;
gracefullyProcessExitDoNotHang ( 1 ) ;
2023-04-25 11:19:37 -07:00
}
function codegenId ( ) : string {
return process . env . PW_LANG_NAME || 'playwright-test' ;
}
function commandWithOpenOptions ( command : string , description : string , options : any [ ] [ ] ) : Command {
let result = program . command ( command ) . description ( description ) ;
for ( const option of options )
result = result . option ( option [ 0 ] , . . . option . slice ( 1 ) ) ;
return result
. option ( '-b, --browser <browserType>' , 'browser to use, one of cr, chromium, ff, firefox, wk, webkit' , 'chromium' )
. option ( '--block-service-workers' , 'block service workers' )
. option ( '--channel <channel>' , 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc' )
. option ( '--color-scheme <scheme>' , 'emulate preferred color scheme, "light" or "dark"' )
. option ( '--device <deviceName>' , 'emulate device, for example "iPhone 11"' )
. option ( '--geolocation <coordinates>' , 'specify geolocation coordinates, for example "37.819722,-122.478611"' )
. option ( '--ignore-https-errors' , 'ignore https errors' )
. option ( '--load-storage <filename>' , 'load context storage state from the file, previously saved with --save-storage' )
. option ( '--lang <language>' , 'specify language / locale, for example "en-GB"' )
. option ( '--proxy-server <proxy>' , 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"' )
. option ( '--proxy-bypass <bypass>' , 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"' )
. option ( '--save-har <filename>' , 'save HAR file with all network activity at the end' )
. option ( '--save-har-glob <glob pattern>' , 'filter entries in the HAR by matching url against this glob pattern' )
. option ( '--save-storage <filename>' , 'save context storage state at the end, for later use with --load-storage' )
. option ( '--timezone <time zone>' , 'time zone to emulate, for example "Europe/Rome"' )
. option ( '--timeout <timeout>' , 'timeout for Playwright actions in milliseconds, no timeout by default' )
. option ( '--user-agent <ua string>' , 'specify user agent string' )
. option ( '--viewport-size <size>' , 'specify browser viewport size in pixels, for example "1280, 720"' ) ;
}
function buildBasePlaywrightCLICommand ( cliTargetLang : string | undefined ) : string {
switch ( cliTargetLang ) {
case 'python' :
return ` playwright ` ;
case 'java' :
return ` mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="...options.." ` ;
case 'csharp' :
return ` pwsh bin/Debug/netX/playwright.ps1 ` ;
2023-08-17 17:53:08 +02:00
default : {
const packageManagerCommand = getPackageManagerExecCommand ( ) ;
return ` ${ packageManagerCommand } playwright ` ;
}
2023-04-25 11:19:37 -07:00
}
}