2021-06-03 17:07:55 +02: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 */
2022-04-19 02:50:25 +02:00
import type { Command } from 'playwright-core/lib/utilsBundle' ;
2021-09-14 00:19:40 +02:00
import fs from 'fs' ;
import path from 'path' ;
2023-01-27 02:26:47 +01:00
import { Runner } from './runner/runner' ;
2023-03-11 04:07:40 +01:00
import { stopProfiling , startProfiling } from 'playwright-core/lib/utils' ;
2023-05-04 03:45:33 +02:00
import { experimentalLoaderOption , fileIsModule , serializeError } from './util' ;
2021-10-15 06:09:41 +02:00
import { showHTMLReport } from './reporters/html' ;
2023-04-28 01:38:45 +02:00
import { createMergedReport } from './reporters/merge' ;
2023-05-12 00:41:50 +02:00
import { ConfigLoader , resolveConfigFile } from './common/configLoader' ;
2023-01-27 02:26:47 +01:00
import type { ConfigCLIOverrides } from './common/ipc' ;
2023-05-04 03:45:33 +02:00
import type { FullResult , TestError } from '../reporter' ;
2023-04-07 18:54:01 +02:00
import type { TraceMode } from '../types/test' ;
2023-04-08 02:46:47 +02:00
import { builtInReporters , defaultReporter , defaultTimeout } from './common/config' ;
2023-04-07 18:54:01 +02:00
import type { FullConfigInternal } from './common/config' ;
2023-04-25 20:19:37 +02:00
import program from 'playwright-core/lib/cli/program' ;
2023-04-28 00:16:18 +02:00
import type { ReporterDescription } from '..' ;
2023-05-04 03:45:33 +02:00
import { prepareErrorStack } from './reporters/base' ;
2022-09-10 00:25:42 +02:00
2022-09-16 00:48:12 +02:00
function addTestCommand ( program : Command ) {
2021-06-03 17:07:55 +02:00
const command = program . command ( 'test [test-filter...]' ) ;
2022-09-16 00:48:12 +02:00
command . description ( 'run tests with Playwright Test' ) ;
2023-03-21 01:12:17 +01:00
const options = testOptions . sort ( ( a , b ) = > a [ 0 ] . replace ( /-/g , '' ) . localeCompare ( b [ 0 ] . replace ( /-/g , '' ) ) ) ;
2023-03-09 17:04:02 +01:00
options . forEach ( ( [ name , description ] ) = > command . option ( name , description ) ) ;
2021-06-03 17:07:55 +02:00
command . action ( async ( args , opts ) = > {
try {
2021-06-07 02:09:53 +02:00
await runTests ( args , opts ) ;
2021-06-03 17:07:55 +02:00
} catch ( e ) {
2021-06-23 19:30:54 +02:00
console . error ( e ) ;
2021-06-03 17:07:55 +02:00
process . exit ( 1 ) ;
}
} ) ;
2021-09-30 12:24:24 +02:00
command . addHelpText ( 'afterAll' , `
Arguments [ test - filter . . . ] :
2023-01-05 22:39:39 +01:00
Pass arguments to filter test files . Each argument is treated as a regular expression . Matching is performed against the absolute file paths .
2021-09-30 12:24:24 +02:00
Examples :
2022-09-16 00:48:12 +02:00
$ npx playwright test my . spec . ts
$ npx playwright test some.spec.ts :42
$ npx playwright test -- headed
2023-03-09 17:04:02 +01:00
$ npx playwright test -- project = webkit ` );
}
2022-07-29 20:40:33 +02:00
function addListFilesCommand ( program : Command ) {
2022-02-01 17:08:56 +01:00
const command = program . command ( 'list-files [file-filter...]' , { hidden : true } ) ;
command . description ( 'List files with Playwright Test tests' ) ;
2023-05-12 00:41:50 +02:00
command . option ( '-c, --config <file>' , ` Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}" ` ) ;
2022-01-22 04:11:22 +01:00
command . option ( '--project <project-name...>' , ` Only run tests from the specified list of projects (default: list all projects) ` ) ;
command . action ( async ( args , opts ) = > {
try {
2022-02-01 17:08:56 +01:00
await listTestFiles ( opts ) ;
2022-01-22 04:11:22 +01:00
} catch ( e ) {
console . error ( e ) ;
process . exit ( 1 ) ;
}
} ) ;
}
2022-07-29 20:40:33 +02:00
function addShowReportCommand ( program : Command ) {
2021-10-15 06:09:41 +02:00
const command = program . command ( 'show-report [report]' ) ;
command . description ( 'show HTML report' ) ;
2022-11-02 23:12:48 +01:00
command . action ( ( report , options ) = > showHTMLReport ( report , options . host , + options . port ) ) ;
command . option ( '--host <host>' , 'Host to serve report on' , 'localhost' ) ;
command . option ( '--port <port>' , 'Port to serve report on' , '9323' ) ;
2021-10-15 06:09:41 +02:00
command . addHelpText ( 'afterAll' , `
Arguments [ report ] :
When specified , opens given report , otherwise opens last generated report .
Examples :
$ npx playwright show - report
$ npx playwright show - report playwright - report ` );
2021-06-03 17:07:55 +02:00
}
2023-05-12 18:26:04 +02:00
const kAttachmentModes : string [ ] = [ 'local' , 'missing' ] ;
2023-04-07 22:47:52 +02:00
function addMergeReportsCommand ( program : Command ) {
2023-04-20 00:36:17 +02:00
const command = program . command ( 'merge-reports [dir]' , { hidden : true } ) ;
2023-04-07 22:47:52 +02:00
command . description ( 'merge multiple blob reports (for sharded tests) into a single report' ) ;
command . action ( async ( dir , options ) = > {
try {
await mergeReports ( dir , options ) ;
} catch ( e ) {
console . error ( e ) ;
process . exit ( 1 ) ;
}
} ) ;
command . option ( '-c, --config <file>' , ` Configuration file. Can be used to specify additional configuration for the output report. ` ) ;
2023-04-28 00:16:18 +02:00
command . option ( '--reporter <reporter>' , ` Reporter to use, comma-separated, can be ${ builtInReporters . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } (default: " ${ defaultReporter } ") ` ) ;
2023-05-12 18:26:04 +02:00
command . option ( '--attachments <mode>' , ` Whether the attachments are available locally. Supported values are ${ kAttachmentModes . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } (default: "local") ` ) ;
2023-04-07 22:47:52 +02:00
command . addHelpText ( 'afterAll' , `
Arguments [ dir ] :
Directory containing blob reports .
Examples :
$ npx playwright merge - reports playwright - report ` );
}
2022-01-05 22:44:29 +01:00
async function runTests ( args : string [ ] , opts : { [ key : string ] : any } ) {
await startProfiling ( ) ;
// When no --config option is passed, let's look for the config file in the current directory.
2022-03-01 21:56:26 +01:00
const configFileOrDirectory = opts . config ? path . resolve ( process . cwd ( ) , opts . config ) : process . cwd ( ) ;
2023-01-26 22:20:05 +01:00
const resolvedConfigFile = resolveConfigFile ( configFileOrDirectory ) ;
2022-03-01 21:56:26 +01:00
if ( restartWithExperimentalTsEsm ( resolvedConfigFile ) )
return ;
2023-04-08 02:46:47 +02:00
const overrides = overridesFromOptions ( opts ) ;
2023-01-27 21:44:15 +01:00
const configLoader = new ConfigLoader ( overrides ) ;
2023-04-07 18:54:01 +02:00
let config : FullConfigInternal ;
2022-04-29 22:32:39 +02:00
if ( resolvedConfigFile )
2023-04-07 18:54:01 +02:00
config = await configLoader . loadConfigFile ( resolvedConfigFile , opts . deps === false ) ;
2022-04-29 22:32:39 +02:00
else
2023-04-07 18:54:01 +02:00
config = await configLoader . loadEmptyConfig ( configFileOrDirectory ) ;
config . cliArgs = args ;
config . cliGrep = opts . grep as string | undefined ;
config . cliGrepInvert = opts . grepInvert as string | undefined ;
2023-04-08 02:46:47 +02:00
config . cliListOnly = ! ! opts . list ;
2023-04-07 18:54:01 +02:00
config . cliProjectFilter = opts . project || undefined ;
2023-04-08 02:46:47 +02:00
config . cliPassWithNoTests = ! ! opts . passWithNoTests ;
2023-02-02 00:25:26 +01:00
const runner = new Runner ( config ) ;
2023-03-02 00:27:23 +01:00
let status : FullResult [ 'status' ] ;
2023-06-06 18:36:49 +02:00
if ( opts . ui || opts . uiHost || opts . uiPort )
status = await runner . uiAllTests ( { host : opts.uiHost , port : opts.uiPort ? + opts.uiPort : undefined } ) ;
2023-03-25 00:41:20 +01:00
else if ( process . env . PWTEST_WATCH )
status = await runner . watchAllTests ( ) ;
2023-03-02 00:27:23 +01:00
else
status = await runner . runAllTests ( ) ;
2023-03-11 04:07:40 +01:00
await stopProfiling ( 'runner' ) ;
2023-01-26 00:38:23 +01:00
if ( status === 'interrupted' )
2021-06-03 17:07:55 +02:00
process . exit ( 130 ) ;
2023-01-26 00:38:23 +01:00
process . exit ( status === 'passed' ? 0 : 1 ) ;
2021-06-03 17:07:55 +02:00
}
2022-02-01 17:08:56 +01:00
async function listTestFiles ( opts : { [ key : string ] : any } ) {
2022-03-25 16:44:27 +01:00
// Redefine process.stdout.write in case config decides to pollute stdio.
2023-05-04 03:45:33 +02:00
const stdoutWrite = process . stdout . write . bind ( process . stdout ) ;
2022-03-25 16:44:27 +01:00
process . stdout . write = ( ( ) = > { } ) as any ;
2023-05-04 03:45:33 +02:00
process . stderr . write = ( ( ) = > { } ) as any ;
2022-03-01 21:56:26 +01:00
const configFileOrDirectory = opts . config ? path . resolve ( process . cwd ( ) , opts . config ) : process . cwd ( ) ;
2023-01-26 22:20:05 +01:00
const resolvedConfigFile = resolveConfigFile ( configFileOrDirectory ) ! ;
2022-03-01 21:56:26 +01:00
if ( restartWithExperimentalTsEsm ( resolvedConfigFile ) )
return ;
2023-05-04 03:45:33 +02:00
try {
const configLoader = new ConfigLoader ( ) ;
const config = await configLoader . loadConfigFile ( resolvedConfigFile ) ;
const runner = new Runner ( config ) ;
const report = await runner . listTestFiles ( opts . project ) ;
stdoutWrite ( JSON . stringify ( report ) , ( ) = > {
process . exit ( 0 ) ;
} ) ;
} catch ( e ) {
const error : TestError = serializeError ( e ) ;
error . location = prepareErrorStack ( e . stack ) . location ;
stdoutWrite ( JSON . stringify ( { error } ) , ( ) = > {
process . exit ( 0 ) ;
} ) ;
}
2022-01-22 04:11:22 +01:00
}
2023-04-07 22:47:52 +02:00
async function mergeReports ( reportDir : string | undefined , opts : { [ key : string ] : any } ) {
let configFile = opts . config ;
if ( configFile ) {
configFile = path . resolve ( process . cwd ( ) , configFile ) ;
if ( ! fs . existsSync ( configFile ) )
throw new Error ( ` ${ configFile } does not exist ` ) ;
if ( ! fs . statSync ( configFile ) . isFile ( ) )
throw new Error ( ` ${ configFile } is not a file ` ) ;
}
if ( restartWithExperimentalTsEsm ( configFile ) )
return ;
const configLoader = new ConfigLoader ( ) ;
const config = await ( configFile ? configLoader . loadConfigFile ( configFile ) : configLoader . loadEmptyConfig ( process . cwd ( ) ) ) ;
2023-04-27 18:15:24 +02:00
const dir = path . resolve ( process . cwd ( ) , reportDir || '' ) ;
2023-04-26 20:48:19 +02:00
if ( ! ( await fs . promises . stat ( dir ) ) . isDirectory ( ) )
throw new Error ( 'Directory does not exist: ' + dir ) ;
2023-04-28 00:16:18 +02:00
let reporterDescriptions : ReporterDescription [ ] | undefined = resolveReporterOption ( opts . reporter ) ;
if ( ! reporterDescriptions && configFile )
reporterDescriptions = config . config . reporter ;
if ( ! reporterDescriptions )
reporterDescriptions = [ [ defaultReporter ] ] ;
2023-05-12 18:26:04 +02:00
if ( opts . attachments ) {
if ( ! kAttachmentModes . includes ( opts . attachments ) )
throw new Error ( ` Invalid --attachments value " ${ opts . attachments } ", must be one of ${ kAttachmentModes . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } . ` ) ;
}
const resolveAttachmentPaths = opts . attachments !== 'missing' ;
await createMergedReport ( config , dir , reporterDescriptions ! , resolveAttachmentPaths ) ;
2023-04-07 22:47:52 +02:00
}
2022-04-29 22:32:39 +02:00
function overridesFromOptions ( options : { [ key : string ] : any } ) : ConfigCLIOverrides {
2021-06-03 17:07:55 +02:00
const shardPair = options . shard ? options . shard . split ( '/' ) . map ( ( t : string ) = > parseInt ( t , 10 ) ) : undefined ;
2023-04-08 02:46:47 +02:00
const overrides : ConfigCLIOverrides = {
2021-06-03 17:07:55 +02:00
forbidOnly : options.forbidOnly ? true : undefined ,
2022-03-02 03:12:21 +01:00
fullyParallel : options.fullyParallel ? true : undefined ,
2022-02-01 20:51:37 +01:00
globalTimeout : options.globalTimeout ? parseInt ( options . globalTimeout , 10 ) : undefined ,
2021-06-03 17:07:55 +02:00
maxFailures : options.x ? 1 : ( options . maxFailures ? parseInt ( options . maxFailures , 10 ) : undefined ) ,
outputDir : options.output ? path . resolve ( process . cwd ( ) , options . output ) : undefined ,
quiet : options.quiet ? options.quiet : undefined ,
repeatEach : options.repeatEach ? parseInt ( options . repeatEach , 10 ) : undefined ,
retries : options.retries ? parseInt ( options . retries , 10 ) : undefined ,
2023-04-28 00:16:18 +02:00
reporter : resolveReporterOption ( options . reporter ) ,
2021-07-27 18:13:04 +02:00
shard : shardPair ? { current : shardPair [ 0 ] , total : shardPair [ 1 ] } : undefined ,
2022-02-01 20:51:37 +01:00
timeout : options.timeout ? parseInt ( options . timeout , 10 ) : undefined ,
2022-09-01 14:34:36 +02:00
ignoreSnapshots : options.ignoreSnapshots ? ! ! options.ignoreSnapshots : undefined ,
2021-06-03 17:07:55 +02:00
updateSnapshots : options.updateSnapshots ? 'all' as const : undefined ,
2022-09-21 20:17:36 +02:00
workers : options.workers ,
2021-06-03 17:07:55 +02:00
} ;
2023-04-08 02:46:47 +02:00
if ( options . browser ) {
const browserOpt = options . browser . toLowerCase ( ) ;
if ( ! [ 'all' , 'chromium' , 'firefox' , 'webkit' ] . includes ( browserOpt ) )
throw new Error ( ` Unsupported browser " ${ options . browser } ", must be one of "all", "chromium", "firefox" or "webkit" ` ) ;
const browserNames = browserOpt === 'all' ? [ 'chromium' , 'firefox' , 'webkit' ] : [ browserOpt ] ;
overrides . projects = browserNames . map ( browserName = > {
return {
name : browserName ,
use : { browserName } ,
} ;
} ) ;
}
if ( options . headed || options . debug )
overrides . use = { headless : false } ;
if ( ! options . ui && options . debug ) {
overrides . maxFailures = 1 ;
overrides . timeout = 0 ;
overrides . workers = 1 ;
process . env . PWDEBUG = '1' ;
}
if ( ! options . ui && options . trace ) {
if ( ! kTraceModes . includes ( options . trace ) )
throw new Error ( ` Unsupported trace mode " ${ options . trace } ", must be one of ${ kTraceModes . map ( mode = > ` " ${ mode } " ` ) . join ( ', ' ) } ` ) ;
overrides . use = overrides . use || { } ;
overrides . use . trace = options . trace ;
}
return overrides ;
2021-06-03 17:07:55 +02:00
}
2021-07-20 22:03:01 +02:00
2023-04-28 00:16:18 +02:00
function resolveReporterOption ( reporter? : string ) : ReporterDescription [ ] | undefined {
if ( ! reporter || ! reporter . length )
return undefined ;
return reporter . split ( ',' ) . map ( ( r : string ) = > [ resolveReporter ( r ) ] ) ;
}
2021-07-20 22:03:01 +02:00
function resolveReporter ( id : string ) {
if ( builtInReporters . includes ( id as any ) )
return id ;
const localPath = path . resolve ( process . cwd ( ) , id ) ;
if ( fs . existsSync ( localPath ) )
return localPath ;
2022-08-18 20:12:33 +02:00
return require . resolve ( id , { paths : [ process . cwd ( ) ] } ) ;
2021-07-20 22:03:01 +02:00
}
2021-10-20 01:10:24 +02:00
2022-03-01 21:56:26 +01:00
function restartWithExperimentalTsEsm ( configFile : string | null ) : boolean {
2022-03-25 16:44:42 +01:00
const nodeVersion = + process . versions . node . split ( '.' ) [ 0 ] ;
// New experimental loader is only supported on Node 16+.
if ( nodeVersion < 16 )
return false ;
2022-03-01 21:56:26 +01:00
if ( ! configFile )
return false ;
2022-03-10 23:48:33 +01:00
if ( process . env . PW_DISABLE_TS_ESM )
2022-03-01 21:56:26 +01:00
return false ;
2022-03-10 23:48:33 +01:00
if ( process . env . PW_TS_ESM_ON )
2022-03-01 21:56:26 +01:00
return false ;
if ( ! fileIsModule ( configFile ) )
return false ;
2022-08-23 19:22:05 +02:00
const NODE_OPTIONS = ( process . env . NODE_OPTIONS || '' ) + experimentalLoaderOption ( ) ;
2023-04-25 20:19:37 +02:00
const innerProcess = require ( 'child_process' ) . fork ( require . resolve ( './cli' ) , process . argv . slice ( 2 ) , {
2022-03-01 21:56:26 +01:00
env : {
. . . process . env ,
NODE_OPTIONS ,
2022-03-10 23:48:33 +01:00
PW_TS_ESM_ON : '1' ,
2022-03-01 21:56:26 +01:00
}
} ) ;
innerProcess . on ( 'close' , ( code : number | null ) = > {
if ( code !== 0 && code !== null )
process . exit ( code ) ;
} ) ;
return true ;
}
2022-08-05 18:20:39 +02:00
2023-03-31 22:04:24 +02:00
const kTraceModes : TraceMode [ ] = [ 'on' , 'off' , 'on-first-retry' , 'on-all-retries' , 'retain-on-failure' ] ;
2023-03-09 17:04:02 +01:00
2023-03-21 01:12:17 +01:00
const testOptions : [ string , string ] [ ] = [
2023-03-09 17:04:02 +01:00
[ '--browser <browser>' , ` Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium") ` ] ,
2023-05-12 00:41:50 +02:00
[ '-c, --config <file>' , ` Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}" ` ] ,
2023-03-09 17:04:02 +01:00
[ '--debug' , ` Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options ` ] ,
[ '--forbid-only' , ` Fail if test.only is called (default: false) ` ] ,
2023-03-21 01:12:17 +01:00
[ '--fully-parallel' , ` Run all tests in parallel (default: false) ` ] ,
2023-03-09 17:04:02 +01:00
[ '--global-timeout <timeout>' , ` Maximum time this test suite can run in milliseconds (default: unlimited) ` ] ,
[ '-g, --grep <grep>' , ` Only run tests matching this regular expression (default: ".*") ` ] ,
[ '-gv, --grep-invert <grep>' , ` Only run tests that do not match this regular expression ` ] ,
2023-03-21 01:12:17 +01:00
[ '--headed' , ` Run tests in headed browsers (default: headless) ` ] ,
[ '--ignore-snapshots' , ` Ignore screenshot and snapshot expectations ` ] ,
2023-03-09 17:04:02 +01:00
[ '--list' , ` Collect all the tests and report them, but do not run ` ] ,
2023-03-21 01:12:17 +01:00
[ '--max-failures <N>' , ` Stop after the first N failures ` ] ,
[ '--no-deps' , 'Do not run project dependencies' ] ,
[ '--output <dir>' , ` Folder for output artifacts (default: "test-results") ` ] ,
2023-03-09 17:04:02 +01:00
[ '--pass-with-no-tests' , ` Makes test run succeed even if no tests were found ` ] ,
2023-03-21 01:12:17 +01:00
[ '--project <project-name...>' , ` Only run tests from the specified list of projects (default: run all projects) ` ] ,
[ '--quiet' , ` Suppress stdio ` ] ,
2023-03-09 17:04:02 +01:00
[ '--repeat-each <N>' , ` Run each test N times (default: 1) ` ] ,
2023-04-08 02:46:47 +02:00
[ '--reporter <reporter>' , ` Reporter to use, comma-separated, can be ${ builtInReporters . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } (default: " ${ defaultReporter } ") ` ] ,
2023-03-09 17:04:02 +01:00
[ '--retries <retries>' , ` Maximum retry count for flaky tests, zero for no retries (default: no retries) ` ] ,
[ '--shard <shard>' , ` Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5" ` ] ,
2023-03-21 01:12:17 +01:00
[ '--timeout <timeout>' , ` Specify test timeout threshold in milliseconds, zero for unlimited (default: ${ defaultTimeout } ) ` ] ,
2023-03-09 17:04:02 +01:00
[ '--trace <mode>' , ` Force tracing mode, can be ${ kTraceModes . map ( mode = > ` " ${ mode } " ` ) . join ( ', ' ) } ` ] ,
2023-03-09 22:03:01 +01:00
[ '--ui' , ` Run tests in interactive UI mode ` ] ,
2023-06-06 18:36:49 +02:00
[ '--ui-host <host>' , 'Host to serve UI on; specifying this option opens UI in a browser tab' ] ,
[ '--ui-port <port>' , 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab' ] ,
2023-03-09 17:04:02 +01:00
[ '-u, --update-snapshots' , ` Update snapshots with actual results (default: only create missing snapshots) ` ] ,
2023-03-21 01:12:17 +01:00
[ '-j, --workers <workers>' , ` Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%) ` ] ,
[ '-x' , ` Stop after the first failure ` ] ,
2023-03-09 17:04:02 +01:00
] ;
2023-04-25 20:19:37 +02:00
addTestCommand ( program ) ;
addShowReportCommand ( program ) ;
addListFilesCommand ( program ) ;
addMergeReportsCommand ( program ) ;
program . parse ( process . argv ) ;