2021-06-03 08:07:55 -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 */
2021-10-28 07:31:30 -08:00
import { Command } from 'commander' ;
2021-09-13 15:19:40 -07:00
import fs from 'fs' ;
import path from 'path' ;
2021-06-06 17:09:53 -07:00
import type { Config } from './types' ;
2022-01-05 13:44:29 -08:00
import { Runner , builtInReporters , BuiltInReporter , kDefaultConfigFiles } from './runner' ;
2021-06-21 14:49:43 -07:00
import { stopProfiling , startProfiling } from './profiler' ;
2021-07-20 15:03:01 -05:00
import { FilePatternFilter } from './util' ;
2021-10-14 20:09:41 -08:00
import { showHTMLReport } from './reporters/html' ;
2021-10-22 15:59:52 -04:00
import { GridServer } from 'playwright-core/lib/grid/gridServer' ;
import dockerFactory from 'playwright-core/lib/grid/dockerGridFactory' ;
import { createGuid } from 'playwright-core/lib/utils/utils' ;
2022-03-01 12:56:26 -08:00
import { fileIsModule } from './loader' ;
2021-06-03 08:07:55 -07:00
const defaultTimeout = 30000 ;
2021-07-20 15:03:01 -05:00
const defaultReporter : BuiltInReporter = process . env . CI ? 'dot' : 'list' ;
2021-06-03 08:07:55 -07:00
2021-09-30 12:24:24 +02:00
export function addTestCommand ( program : Command ) {
2021-06-03 08:07:55 -07:00
const command = program . command ( 'test [test-filter...]' ) ;
command . description ( 'Run tests with Playwright Test' ) ;
command . option ( '--browser <browser>' , ` Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium") ` ) ;
command . option ( '--headed' , ` Run tests in headed browsers (default: headless) ` ) ;
2021-09-15 21:19:31 +02:00
command . option ( '--debug' , ` Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --maxFailures=1 --headed --workers=1" options ` ) ;
2022-01-05 13:44:29 -08:00
command . option ( '-c, --config <file>' , ` Configuration file, or a test directory with optional ${ kDefaultConfigFiles . map ( file = > ` " ${ file } " ` ) . join ( '/' ) } ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '--forbid-only' , ` Fail if test.only is called (default: false) ` ) ;
2022-03-01 18:12:21 -08:00
command . option ( '--fully-parallel' , ` Run all tests in parallel (default: false) ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '-g, --grep <grep>' , ` Only run tests matching this regular expression (default: ".*") ` ) ;
2021-06-18 17:56:59 -07:00
command . option ( '-gv, --grep-invert <grep>' , ` Only run tests that do not match this regular expression ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '--global-timeout <timeout>' , ` Maximum time this test suite can run in milliseconds (default: unlimited) ` ) ;
command . option ( '-j, --workers <workers>' , ` Number of concurrent workers, use 1 to run in a single worker (default: number of CPU cores / 2) ` ) ;
command . option ( '--list' , ` Collect all the tests and report them, but do not run ` ) ;
command . option ( '--max-failures <N>' , ` Stop after the first N failures ` ) ;
command . option ( '--output <dir>' , ` Folder for output artifacts (default: "test-results") ` ) ;
command . option ( '--quiet' , ` Suppress stdio ` ) ;
command . option ( '--repeat-each <N>' , ` Run each test N times (default: 1) ` ) ;
2021-07-20 15:03:01 -05:00
command . option ( '--reporter <reporter>' , ` Reporter to use, comma-separated, can be ${ builtInReporters . map ( name = > ` " ${ name } " ` ) . join ( ', ' ) } (default: " ${ defaultReporter } ") ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '--retries <retries>' , ` Maximum retry count for flaky tests, zero for no retries (default: no retries) ` ) ;
command . option ( '--shard <shard>' , ` Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5" ` ) ;
2021-09-02 09:29:55 -07:00
command . option ( '--project <project-name...>' , ` Only run tests from the specified list of projects (default: run all projects) ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '--timeout <timeout>' , ` Specify test timeout threshold in milliseconds, zero for unlimited (default: ${ defaultTimeout } ) ` ) ;
command . option ( '-u, --update-snapshots' , ` Update snapshots with actual results (default: only create missing snapshots) ` ) ;
command . option ( '-x' , ` Stop after the first failure ` ) ;
command . action ( async ( args , opts ) = > {
try {
2021-06-06 17:09:53 -07:00
await runTests ( args , opts ) ;
2021-06-03 08:07:55 -07:00
} catch ( e ) {
2021-06-23 10:30:54 -07:00
console . error ( e ) ;
2021-06-03 08:07:55 -07:00
process . exit ( 1 ) ;
}
} ) ;
2021-09-30 12:24:24 +02:00
command . addHelpText ( 'afterAll' , `
Arguments [ test - filter . . . ] :
Pass arguments to filter test files . Each argument is treated as a regular expression .
Examples :
2021-10-14 20:09:41 -08:00
$ npx playwright test my . spec . ts
$ npx playwright test -- headed
$ npx playwright test -- browser = webkit ` );
}
2022-02-01 08:08:56 -08:00
export function addListFilesCommand ( program : Command ) {
const command = program . command ( 'list-files [file-filter...]' , { hidden : true } ) ;
command . description ( 'List files with Playwright Test tests' ) ;
2022-01-21 19:11:22 -08:00
command . option ( '-c, --config <file>' , ` Configuration file, or a test directory with optional ${ kDefaultConfigFiles . map ( file = > ` " ${ file } " ` ) . join ( '/' ) } ` ) ;
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 08:08:56 -08:00
await listTestFiles ( opts ) ;
2022-01-21 19:11:22 -08:00
} catch ( e ) {
console . error ( e ) ;
process . exit ( 1 ) ;
}
} ) ;
}
2021-10-14 20:09:41 -08:00
export function addShowReportCommand ( program : Command ) {
const command = program . command ( 'show-report [report]' ) ;
command . description ( 'show HTML report' ) ;
command . action ( report = > showHTMLReport ( report ) ) ;
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 08:07:55 -07:00
}
2022-01-05 13:44:29 -08:00
async function runTests ( args : string [ ] , opts : { [ key : string ] : any } ) {
await startProfiling ( ) ;
const defaultConfig : Config = {
preserveOutput : 'always' ,
reporter : [ [ defaultReporter ] ] ,
reportSlowTests : { max : 5 , threshold : 15000 } ,
timeout : defaultTimeout ,
updateSnapshots : 'missing' ,
workers : Math.ceil ( require ( 'os' ) . cpus ( ) . length / 2 ) ,
} ;
2021-07-23 09:04:20 -07:00
if ( opts . browser ) {
const browserOpt = opts . browser . toLowerCase ( ) ;
if ( ! [ 'all' , 'chromium' , 'firefox' , 'webkit' ] . includes ( browserOpt ) )
throw new Error ( ` Unsupported browser " ${ opts . browser } ", must be one of "all", "chromium", "firefox" or "webkit" ` ) ;
const browserNames = browserOpt === 'all' ? [ 'chromium' , 'firefox' , 'webkit' ] : [ browserOpt ] ;
defaultConfig . projects = browserNames . map ( browserName = > {
return {
name : browserName ,
use : { browserName } ,
} ;
} ) ;
}
2021-06-03 08:07:55 -07:00
const overrides = overridesFromOptions ( opts ) ;
2021-10-28 07:31:30 -08:00
if ( opts . headed || opts . debug )
2021-06-03 08:07:55 -07:00
overrides . use = { headless : false } ;
2021-10-28 07:31:30 -08:00
if ( opts . debug ) {
2021-09-15 21:19:31 +02:00
overrides . maxFailures = 1 ;
overrides . timeout = 0 ;
overrides . workers = 1 ;
2021-10-01 09:16:03 +02:00
process . env . PWDEBUG = '1' ;
2021-12-01 22:48:16 +05:30
}
2021-10-01 09:16:03 +02:00
2022-01-05 13:44:29 -08:00
// When no --config option is passed, let's look for the config file in the current directory.
2022-03-01 12:56:26 -08:00
const configFileOrDirectory = opts . config ? path . resolve ( process . cwd ( ) , opts . config ) : process . cwd ( ) ;
const resolvedConfigFile = Runner . resolveConfigFile ( configFileOrDirectory ) ;
if ( restartWithExperimentalTsEsm ( resolvedConfigFile ) )
return ;
const runner = new Runner ( overrides , { defaultConfig } ) ;
const config = resolvedConfigFile ? await runner . loadConfigFromResolvedFile ( resolvedConfigFile ) : runner . loadEmptyConfig ( configFileOrDirectory ) ;
2022-01-05 13:44:29 -08:00
if ( ( 'projects' in config ) && opts . browser )
throw new Error ( ` Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead. ` ) ;
2021-09-09 14:17:18 -07:00
2022-01-05 15:49:01 -08:00
const filePatternFilter : FilePatternFilter [ ] = args . map ( arg = > {
2021-06-24 10:02:34 +02:00
const match = /^(.*):(\d+)$/ . exec ( arg ) ;
return {
re : forceRegExp ( match ? match [ 1 ] : arg ) ,
line : match ? parseInt ( match [ 2 ] , 10 ) : null ,
} ;
} ) ;
2021-09-09 14:17:18 -07:00
2021-10-19 16:10:24 -07:00
if ( process . env . PLAYWRIGHT_DOCKER )
runner . addInternalGlobalSetup ( launchDockerContainer ) ;
2022-01-05 15:49:01 -08:00
const result = await runner . runAllTests ( {
listOnly : ! ! opts . list ,
filePatternFilter ,
projectFilter : opts.project || undefined ,
} ) ;
2021-06-21 14:49:43 -07:00
await stopProfiling ( undefined ) ;
2021-11-11 16:48:08 -08:00
if ( result . status === 'interrupted' )
2021-06-03 08:07:55 -07:00
process . exit ( 130 ) ;
2021-11-11 16:48:08 -08:00
process . exit ( result . status === 'passed' ? 0 : 1 ) ;
2021-06-03 08:07:55 -07:00
}
2022-01-21 19:11:22 -08:00
2022-02-01 08:08:56 -08:00
async function listTestFiles ( opts : { [ key : string ] : any } ) {
2022-03-01 12:56:26 -08:00
const configFileOrDirectory = opts . config ? path . resolve ( process . cwd ( ) , opts . config ) : process . cwd ( ) ;
const resolvedConfigFile = Runner . resolveConfigFile ( configFileOrDirectory ) ! ;
if ( restartWithExperimentalTsEsm ( resolvedConfigFile ) )
return ;
2022-01-21 19:11:22 -08:00
const runner = new Runner ( { } , { defaultConfig : { } } ) ;
2022-03-01 12:56:26 -08:00
await runner . loadConfigFromResolvedFile ( resolvedConfigFile ) ;
const report = await runner . listTestFiles ( resolvedConfigFile , opts . project ) ;
2022-01-22 17:02:46 -08:00
process . stdout . write ( JSON . stringify ( report ) , ( ) = > {
process . exit ( 0 ) ;
} ) ;
2022-01-21 19:11:22 -08:00
}
2021-06-03 08:07:55 -07:00
function forceRegExp ( pattern : string ) : RegExp {
const match = pattern . match ( /^\/(.*)\/([gi]*)$/ ) ;
if ( match )
return new RegExp ( match [ 1 ] , match [ 2 ] ) ;
2022-02-10 07:02:37 -08:00
return new RegExp ( pattern , 'gi' ) ;
2021-06-03 08:07:55 -07:00
}
function overridesFromOptions ( options : { [ key : string ] : any } ) : Config {
const shardPair = options . shard ? options . shard . split ( '/' ) . map ( ( t : string ) = > parseInt ( t , 10 ) ) : undefined ;
return {
forbidOnly : options.forbidOnly ? true : undefined ,
2022-03-01 18:12:21 -08:00
fullyParallel : options.fullyParallel ? true : undefined ,
2022-02-01 11:51:37 -08:00
globalTimeout : options.globalTimeout ? parseInt ( options . globalTimeout , 10 ) : undefined ,
2021-06-03 08:07:55 -07:00
grep : options.grep ? forceRegExp ( options . grep ) : undefined ,
2021-06-18 17:56:59 -07:00
grepInvert : options.grepInvert ? forceRegExp ( options . grepInvert ) : undefined ,
2021-06-03 08:07:55 -07: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 ,
2021-07-20 15:03:01 -05:00
reporter : ( options . reporter && options . reporter . length ) ? options . reporter . split ( ',' ) . map ( ( r : string ) = > [ resolveReporter ( r ) ] ) : undefined ,
2021-07-27 09:13:04 -07:00
shard : shardPair ? { current : shardPair [ 0 ] , total : shardPair [ 1 ] } : undefined ,
2022-02-01 11:51:37 -08:00
timeout : options.timeout ? parseInt ( options . timeout , 10 ) : undefined ,
2021-06-03 08:07:55 -07:00
updateSnapshots : options.updateSnapshots ? 'all' as const : undefined ,
workers : options.workers ? parseInt ( options . workers , 10 ) : undefined ,
} ;
}
2021-07-20 15:03:01 -05: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 ;
return require . resolve ( id , { paths : [ process . cwd ( ) ] } ) ;
}
2021-10-19 16:10:24 -07:00
async function launchDockerContainer ( ) : Promise < ( ) = > Promise < void > > {
const gridServer = new GridServer ( dockerFactory , createGuid ( ) ) ;
await gridServer . start ( ) ;
// Start docker container in advance.
2021-11-02 13:58:26 -07:00
const { error } = await gridServer . createAgent ( ) ;
if ( error )
throw error ;
2021-10-19 16:10:24 -07:00
process . env . PW_GRID = gridServer . urlPrefix ( ) . substring ( 0 , gridServer . urlPrefix ( ) . length - 1 ) ;
return async ( ) = > await gridServer . stop ( ) ;
}
2022-03-01 12:56:26 -08:00
function restartWithExperimentalTsEsm ( configFile : string | null ) : boolean {
if ( ! configFile )
return false ;
if ( ! process . env . PW_EXPERIMENTAL_TS_ESM )
return false ;
if ( process . env . PW_EXPERIMENTAL_TS_ESM_ON )
return false ;
if ( ! configFile . endsWith ( '.ts' ) )
return false ;
if ( ! fileIsModule ( configFile ) )
return false ;
const NODE_OPTIONS = ( process . env . NODE_OPTIONS || '' ) + ` --experimental-loader= ${ require . resolve ( '@playwright/test/lib/experimentalLoader' ) } ` ;
const innerProcess = require ( 'child_process' ) . fork ( require . resolve ( 'playwright-core/cli' ) , process . argv . slice ( 2 ) , {
env : {
. . . process . env ,
NODE_OPTIONS ,
PW_EXPERIMENTAL_TS_ESM_ON : '1' ,
}
} ) ;
innerProcess . on ( 'close' , ( code : number | null ) = > {
if ( code !== 0 && code !== null )
process . exit ( code ) ;
} ) ;
return true ;
}