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-09-30 12:24:24 +02: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' ;
2021-07-20 15:03:01 -05:00
import { Runner , builtInReporters , BuiltInReporter } 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-09-09 14:17:18 -07:00
import { Loader } 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
const tsConfig = 'playwright.config.ts' ;
const jsConfig = 'playwright.config.js' ;
2021-07-12 11:59:58 -05:00
const mjsConfig = 'playwright.config.mjs' ;
2021-06-03 08:07:55 -07:00
const defaultConfig : Config = {
2021-06-16 16:05:30 -07:00
preserveOutput : 'always' ,
2021-06-03 22:06:59 -07:00
reporter : [ [ defaultReporter ] ] ,
2021-06-14 22:45:58 -07:00
reportSlowTests : { max : 5 , threshold : 15000 } ,
2021-06-03 08:07:55 -07:00
timeout : defaultTimeout ,
2021-06-16 07:51:54 -07:00
updateSnapshots : 'missing' ,
2021-06-03 08:07:55 -07:00
workers : Math.ceil ( require ( 'os' ) . cpus ( ) . length / 2 ) ,
} ;
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 ` ) ;
2021-06-03 08:07:55 -07:00
command . option ( '-c, --config <file>' , ` Configuration file, or a test directory with optional " ${ tsConfig } "/" ${ jsConfig } " ` ) ;
command . option ( '--forbid-only' , ` Fail if test.only is called (default: false) ` ) ;
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 :
$ test my . spec . ts
$ test -- headed
$ test -- browser = webkit ` );
2021-06-03 08:07:55 -07:00
}
2021-09-09 14:17:18 -07:00
async function createLoader ( opts : { [ key : string ] : any } ) : Promise < Loader > {
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-09-15 21:19:31 +02:00
if ( opts . headed || opts . debug )
2021-06-03 08:07:55 -07:00
overrides . use = { headless : false } ;
2021-09-15 21:19:31 +02:00
if ( opts . debug ) {
overrides . maxFailures = 1 ;
overrides . timeout = 0 ;
overrides . workers = 1 ;
process . env . PWDEBUG = '1' ;
}
2021-09-09 14:17:18 -07:00
const loader = new Loader ( defaultConfig , overrides ) ;
2021-06-03 08:07:55 -07:00
2021-07-12 11:59:58 -05:00
async function loadConfig ( configFile : string ) {
2021-06-03 08:07:55 -07:00
if ( fs . existsSync ( configFile ) ) {
if ( process . stdout . isTTY )
console . log ( ` Using config at ` + configFile ) ;
2021-09-09 14:17:18 -07:00
const loadedConfig = await loader . loadConfigFile ( configFile ) ;
2021-06-03 08:07:55 -07:00
if ( ( 'projects' in loadedConfig ) && opts . browser )
throw new Error ( ` Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead. ` ) ;
return true ;
}
return false ;
}
2021-07-12 11:59:58 -05:00
async function loadConfigFromDirectory ( directory : string ) {
const configNames = [ tsConfig , jsConfig , mjsConfig ] ;
for ( const configName of configNames ) {
if ( await loadConfig ( path . resolve ( directory , configName ) ) )
return true ;
}
return false ;
}
2021-06-03 08:07:55 -07:00
if ( opts . config ) {
const configFile = path . resolve ( process . cwd ( ) , opts . config ) ;
if ( ! fs . existsSync ( configFile ) )
throw new Error ( ` ${ opts . config } does not exist ` ) ;
if ( fs . statSync ( configFile ) . isDirectory ( ) ) {
// When passed a directory, look for a config file inside.
2021-07-12 11:59:58 -05:00
if ( ! await loadConfigFromDirectory ( configFile ) ) {
2021-06-03 08:07:55 -07:00
// If there is no config, assume this as a root testing directory.
2021-09-09 14:17:18 -07:00
loader . loadEmptyConfig ( configFile ) ;
2021-06-03 08:07:55 -07:00
}
} else {
// When passed a file, it must be a config file.
2021-07-12 11:59:58 -05:00
await loadConfig ( configFile ) ;
2021-06-03 08:07:55 -07:00
}
2021-07-12 11:59:58 -05:00
} else if ( ! await loadConfigFromDirectory ( process . cwd ( ) ) ) {
2021-06-03 08:07:55 -07:00
// No --config option, let's look for the config file in the current directory.
2021-06-08 11:02:16 -07:00
// If not, scan the world.
2021-09-09 14:17:18 -07:00
loader . loadEmptyConfig ( process . cwd ( ) ) ;
2021-06-03 08:07:55 -07:00
}
2021-09-09 14:17:18 -07:00
return loader ;
}
2021-06-03 08:07:55 -07:00
2021-09-09 14:17:18 -07:00
async function runTests ( args : string [ ] , opts : { [ key : string ] : any } ) {
await startProfiling ( ) ;
const loader = await createLoader ( opts ) ;
2021-06-24 10:02:34 +02:00
const filePatternFilters : FilePatternFilter [ ] = args . map ( arg = > {
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
const runner = new Runner ( loader ) ;
2021-06-24 10:02:34 +02:00
const result = await runner . run ( ! ! opts . list , filePatternFilters , opts . project || undefined ) ;
2021-06-21 14:49:43 -07:00
await stopProfiling ( undefined ) ;
2021-06-03 08:07:55 -07:00
if ( result === 'sigint' )
process . exit ( 130 ) ;
process . exit ( result === 'passed' ? 0 : 1 ) ;
}
function forceRegExp ( pattern : string ) : RegExp {
const match = pattern . match ( /^\/(.*)\/([gi]*)$/ ) ;
if ( match )
return new RegExp ( match [ 1 ] , match [ 2 ] ) ;
2021-06-15 17:27:52 -07:00
return new RegExp ( pattern , 'gi' ) ;
2021-06-03 08:07:55 -07:00
}
function overridesFromOptions ( options : { [ key : string ] : any } ) : Config {
const isDebuggerAttached = ! ! require ( 'inspector' ) . url ( ) ;
const shardPair = options . shard ? options . shard . split ( '/' ) . map ( ( t : string ) = > parseInt ( t , 10 ) ) : undefined ;
return {
forbidOnly : options.forbidOnly ? true : undefined ,
globalTimeout : isDebuggerAttached ? 0 : ( options . globalTimeout ? parseInt ( options . globalTimeout , 10 ) : undefined ) ,
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 ,
2021-06-03 08:07:55 -07:00
timeout : isDebuggerAttached ? 0 : ( options . timeout ? parseInt ( options . timeout , 10 ) : undefined ) ,
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 ( ) ] } ) ;
}