2021-06-06 17:09:53 -07:00
/ * *
* Copyright Microsoft Corporation . All rights reserved .
*
* 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 .
* /
import fs from 'fs' ;
import path from 'path' ;
import rimraf from 'rimraf' ;
2021-11-23 09:30:53 -08:00
import * as mime from 'mime' ;
2021-06-06 17:09:53 -07:00
import util from 'util' ;
2021-08-28 07:19:45 -07:00
import colors from 'colors/safe' ;
2021-06-06 17:09:53 -07:00
import { EventEmitter } from 'events' ;
2021-12-18 09:32:41 -08:00
import { monotonicTime , serializeError , sanitizeForFilePath , getContainedPath , addSuffixToFilePath , prependToTestError , trimLongString , formatLocation } from './util' ;
2021-08-02 17:17:20 -07:00
import { TestBeginPayload , TestEndPayload , RunPayload , TestEntry , DonePayload , WorkerInitParams , StepBeginPayload , StepEndPayload } from './ipc' ;
2021-06-06 17:09:53 -07:00
import { setCurrentTestInfo } from './globals' ;
import { Loader } from './loader' ;
2021-07-19 14:54:18 -07:00
import { Modifier , Suite , TestCase } from './test' ;
2021-12-15 10:39:49 -08:00
import { Annotations , TestCaseType , TestError , TestInfo , TestInfoImpl , TestStepInternal , WorkerInfo } from './types' ;
2021-06-06 17:09:53 -07:00
import { ProjectImpl } from './project' ;
2021-11-15 13:17:26 -08:00
import { FixtureRunner } from './fixtures' ;
2021-10-22 15:59:52 -04:00
import { DeadlineRunner , raceAgainstDeadline } from 'playwright-core/lib/utils/async' ;
2022-01-13 10:38:47 -08:00
import { calculateSha1 } from 'playwright-core/lib/utils/utils' ;
2021-06-06 17:09:53 -07:00
const removeFolderAsync = util . promisify ( rimraf ) ;
2021-12-15 10:39:49 -08:00
type TestData = { testId : string , testInfo : TestInfoImpl , type : TestCaseType } ;
2021-09-27 15:58:26 -07:00
2021-06-06 17:09:53 -07:00
export class WorkerRunner extends EventEmitter {
private _params : WorkerInitParams ;
private _loader ! : Loader ;
private _project ! : ProjectImpl ;
private _workerInfo ! : WorkerInfo ;
private _projectNamePathSegment = '' ;
private _uniqueProjectNamePathSegment = '' ;
private _fixtureRunner : FixtureRunner ;
2021-09-27 21:38:19 -07:00
private _failedTest : TestData | undefined ;
2021-07-07 12:04:43 -07:00
private _fatalError : TestError | undefined ;
2021-06-06 17:09:53 -07:00
private _entries = new Map < string , TestEntry > ( ) ;
2021-07-28 15:43:37 -07:00
private _isStopped = false ;
2021-08-10 10:54:05 -07:00
private _runFinished = Promise . resolve ( ) ;
private _currentDeadlineRunner : DeadlineRunner < any > | undefined ;
2021-09-27 15:58:26 -07:00
_currentTest : TestData | null = null ;
2021-06-06 17:09:53 -07:00
constructor ( params : WorkerInitParams ) {
super ( ) ;
this . _params = params ;
this . _fixtureRunner = new FixtureRunner ( ) ;
}
2021-08-10 10:54:05 -07:00
stop ( ) : Promise < void > {
if ( ! this . _isStopped ) {
this . _isStopped = true ;
// Interrupt current action.
2021-08-31 14:44:08 -07:00
this . _currentDeadlineRunner ? . interrupt ( ) ;
2021-08-10 10:54:05 -07:00
// TODO: mark test as 'interrupted' instead.
if ( this . _currentTest && this . _currentTest . testInfo . status === 'passed' )
this . _currentTest . testInfo . status = 'skipped' ;
2021-07-30 13:12:49 -07:00
}
2021-08-10 10:54:05 -07:00
return this . _runFinished ;
2021-06-06 17:09:53 -07:00
}
async cleanup() {
2021-07-07 12:04:43 -07:00
// We have to load the project to get the right deadline below.
2021-07-12 11:59:58 -05:00
await this . _loadIfNeeded ( ) ;
2021-09-28 10:56:50 -07:00
await this . _teardownScopes ( ) ;
if ( this . _fatalError )
this . emit ( 'teardownError' , { error : this._fatalError } ) ;
}
private async _teardownScopes() {
2021-06-06 17:09:53 -07:00
// TODO: separate timeout for teardown?
const result = await raceAgainstDeadline ( ( async ( ) = > {
await this . _fixtureRunner . teardownScope ( 'test' ) ;
await this . _fixtureRunner . teardownScope ( 'worker' ) ;
} ) ( ) , this . _deadline ( ) ) ;
2021-08-31 10:50:30 -07:00
if ( result . timedOut && ! this . _fatalError )
this . _fatalError = { message : colors.red ( ` Timeout of ${ this . _project . config . timeout } ms exceeded while shutting down environment ` ) } ;
2021-06-06 17:09:53 -07:00
}
unhandledError ( error : Error | any ) {
2021-10-29 13:36:12 -07:00
// Usually, we do not differentiate between errors in the control flow
// and unhandled errors - both lead to the test failing. This is good for regular tests,
// so that you can, e.g. expect() from inside an event handler. The test fails,
// and we restart the worker.
//
// However, for tests marked with test.fail(), this is a problem. Unhandled error
// could come either from the user test code (legit failure), or from a fixture or
// a test runner. In the latter case, the worker state could be messed up,
// and continuing to run tests in the same worker is problematic. Therefore,
// we turn this into a fatal error and restart the worker anyway.
if ( this . _currentTest && this . _currentTest . type === 'test' && this . _currentTest . testInfo . expectedStatus !== 'failed' ) {
2021-08-10 10:54:05 -07:00
if ( ! this . _currentTest . testInfo . error ) {
this . _currentTest . testInfo . status = 'failed' ;
this . _currentTest . testInfo . error = serializeError ( error ) ;
}
2021-06-06 17:09:53 -07:00
} else {
// No current test - fatal error.
2021-08-10 10:54:05 -07:00
if ( ! this . _fatalError )
this . _fatalError = serializeError ( error ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-06-06 17:09:53 -07:00
}
private _deadline() {
2021-08-31 14:44:08 -07:00
return this . _project . config . timeout ? monotonicTime ( ) + this . _project.config.timeout : 0 ;
2021-06-06 17:09:53 -07:00
}
2021-07-12 11:59:58 -05:00
private async _loadIfNeeded() {
2021-06-06 17:09:53 -07:00
if ( this . _loader )
return ;
2021-07-12 11:59:58 -05:00
this . _loader = await Loader . deserialize ( this . _params . loader ) ;
2021-06-06 17:09:53 -07:00
this . _project = this . _loader . projects ( ) [ this . _params . projectIndex ] ;
this . _projectNamePathSegment = sanitizeForFilePath ( this . _project . config . name ) ;
const sameName = this . _loader . projects ( ) . filter ( project = > project . config . name === this . _project . config . name ) ;
if ( sameName . length > 1 )
this . _uniqueProjectNamePathSegment = this . _project . config . name + ( sameName . indexOf ( this . _project ) + 1 ) ;
else
this . _uniqueProjectNamePathSegment = this . _project . config . name ;
this . _uniqueProjectNamePathSegment = sanitizeForFilePath ( this . _uniqueProjectNamePathSegment ) ;
this . _workerInfo = {
workerIndex : this._params.workerIndex ,
2021-11-01 10:37:34 -07:00
parallelIndex : this._params.parallelIndex ,
2021-06-06 17:09:53 -07:00
project : this._project.config ,
config : this._loader.fullConfig ( ) ,
} ;
}
async run ( runPayload : RunPayload ) {
2021-08-29 11:21:06 -07:00
let runFinishedCallback = ( ) = > { } ;
this . _runFinished = new Promise ( f = > runFinishedCallback = f ) ;
2021-08-10 10:54:05 -07:00
try {
this . _entries = new Map ( runPayload . entries . map ( e = > [ e . testId , e ] ) ) ;
await this . _loadIfNeeded ( ) ;
const fileSuite = await this . _loader . loadTestFile ( runPayload . file ) ;
const suite = this . _project . cloneFileSuite ( fileSuite , this . _params . repeatEachIndex , test = > {
if ( ! this . _entries . has ( test . _id ) )
return false ;
return true ;
} ) ;
2021-11-15 13:17:26 -08:00
if ( suite ) {
const firstPool = suite . allTests ( ) [ 0 ] . _pool ! ;
this . _fixtureRunner . setPool ( firstPool ) ;
2021-08-10 10:54:05 -07:00
await this . _runSuite ( suite , [ ] ) ;
}
2021-09-28 10:56:50 -07:00
if ( this . _failedTest )
await this . _teardownScopes ( ) ;
2021-08-10 10:54:05 -07:00
} catch ( e ) {
// In theory, we should run above code without any errors.
// However, in the case we screwed up, or loadTestFile failed in the worker
// but not in the runner, let's do a fatal error.
this . unhandledError ( e ) ;
} finally {
2021-09-28 10:56:50 -07:00
if ( this . _failedTest ) {
// Now that we did run all hooks and teared down scopes, we can
// report the failure, possibly with any error details revealed by teardown.
2021-12-15 10:39:49 -08:00
this . emit ( 'testEnd' , buildTestEndPayload ( this . _failedTest ) ) ;
2021-09-28 10:56:50 -07:00
}
2021-06-06 17:09:53 -07:00
this . _reportDone ( ) ;
2021-08-29 11:21:06 -07:00
runFinishedCallback ( ) ;
2021-06-06 17:09:53 -07:00
}
}
2021-07-02 15:49:05 -07:00
private async _runSuite ( suite : Suite , annotations : Annotations ) {
2021-08-10 10:54:05 -07:00
// When stopped, do not run a suite. But if we have started running the suite with hooks,
// always finish the hooks.
2021-06-06 17:09:53 -07:00
if ( this . _isStopped )
return ;
2021-07-02 15:49:05 -07:00
annotations = annotations . concat ( suite . _annotations ) ;
for ( const beforeAllModifier of suite . _modifiers ) {
if ( ! this . _fixtureRunner . dependsOnWorkerFixturesOnly ( beforeAllModifier . fn , beforeAllModifier . location ) )
continue ;
// TODO: separate timeout for beforeAll modifiers?
2022-01-10 20:25:56 -08:00
const result = await raceAgainstDeadline ( this . _fixtureRunner . resolveParametersAndRunFunction ( beforeAllModifier . fn , this . _workerInfo , undefined ) , this . _deadline ( ) ) ;
2021-07-02 15:49:05 -07:00
if ( result . timedOut ) {
2021-08-23 09:21:40 -07:00
if ( ! this . _fatalError )
2021-12-18 09:32:41 -08:00
this . _fatalError = serializeError ( new Error ( ` Timeout of ${ this . _project . config . timeout } ms exceeded while running ${ beforeAllModifier . type } modifier \ n at ${ formatLocation ( beforeAllModifier . location ) } ` ) ) ;
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-12-22 09:59:58 -08:00
} else if ( ! ! result . result ) {
2021-07-02 15:49:05 -07:00
annotations . push ( { type : beforeAllModifier . type , description : beforeAllModifier.description } ) ;
2021-12-22 09:59:58 -08:00
}
2021-07-02 15:49:05 -07:00
}
2021-12-15 10:39:49 -08:00
for ( const hook of suite . hooks ) {
2021-08-09 13:26:33 -07:00
if ( hook . _type !== 'beforeAll' )
2021-06-06 17:09:53 -07:00
continue ;
2021-08-09 13:26:33 -07:00
const firstTest = suite . allTests ( ) [ 0 ] ;
await this . _runTestOrAllHook ( hook , annotations , this . _entries . get ( firstTest . _id ) ? . retry || 0 ) ;
2021-06-06 17:09:53 -07:00
}
for ( const entry of suite . _entries ) {
2021-08-09 13:26:33 -07:00
if ( entry instanceof Suite ) {
2021-07-02 15:49:05 -07:00
await this . _runSuite ( entry , annotations ) ;
2021-08-09 13:26:33 -07:00
} else {
const runEntry = this . _entries . get ( entry . _id ) ;
2021-08-10 10:54:05 -07:00
if ( runEntry && ! this . _isStopped )
2021-08-09 13:26:33 -07:00
await this . _runTestOrAllHook ( entry , annotations , runEntry . retry ) ;
}
2021-06-06 17:09:53 -07:00
}
2021-12-15 10:39:49 -08:00
for ( const hook of suite . hooks ) {
2021-08-09 13:26:33 -07:00
if ( hook . _type !== 'afterAll' )
2021-06-06 17:09:53 -07:00
continue ;
2021-08-09 13:26:33 -07:00
await this . _runTestOrAllHook ( hook , annotations , 0 ) ;
2021-06-06 17:09:53 -07:00
}
}
2021-08-09 13:26:33 -07:00
private async _runTestOrAllHook ( test : TestCase , annotations : Annotations , retry : number ) {
2021-06-06 17:09:53 -07:00
const startTime = monotonicTime ( ) ;
2021-07-18 17:40:59 -07:00
const startWallTime = Date . now ( ) ;
2021-06-06 17:09:53 -07:00
let deadlineRunner : DeadlineRunner < any > | undefined ;
const testId = test . _id ;
const baseOutputDir = ( ( ) = > {
2021-07-15 22:02:10 -07:00
const relativeTestFilePath = path . relative ( this . _project . config . testDir , test . _requireFile . replace ( /\.(spec|test)\.(js|ts|mjs)$/ , '' ) ) ;
2021-06-06 17:09:53 -07:00
const sanitizedRelativePath = relativeTestFilePath . replace ( process . platform === 'win32' ? new RegExp ( '\\\\' , 'g' ) : new RegExp ( '/' , 'g' ) , '-' ) ;
2021-09-01 13:41:35 -07:00
const fullTitleWithoutSpec = test . titlePath ( ) . slice ( 1 ) . join ( ' ' ) + ( test . _type === 'test' ? '' : '-worker' + this . _params . workerIndex ) ;
2021-12-13 13:56:03 -05:00
let testOutputDir = sanitizedRelativePath + '-' + sanitizeForFilePath ( trimLongString ( fullTitleWithoutSpec ) ) ;
2021-06-06 17:09:53 -07:00
if ( this . _uniqueProjectNamePathSegment )
testOutputDir += '-' + this . _uniqueProjectNamePathSegment ;
2021-08-09 13:26:33 -07:00
if ( retry )
testOutputDir += '-retry' + retry ;
2021-06-06 17:09:53 -07:00
if ( this . _params . repeatEachIndex )
testOutputDir += '-repeat' + this . _params . repeatEachIndex ;
return path . join ( this . _project . config . outputDir , testOutputDir ) ;
} ) ( ) ;
2021-11-02 10:02:49 -05:00
const snapshotDir = ( ( ) = > {
const relativeTestFilePath = path . relative ( this . _project . config . testDir , test . _requireFile ) ;
return path . join ( this . _project . config . snapshotDir , relativeTestFilePath + '-snapshots' ) ;
} ) ( ) ;
2021-08-02 17:17:20 -07:00
let lastStepId = 0 ;
2021-07-30 13:12:49 -07:00
const testInfo : TestInfoImpl = {
2021-08-09 13:26:33 -07:00
workerIndex : this._params.workerIndex ,
2021-11-01 10:37:34 -07:00
parallelIndex : this._params.parallelIndex ,
2021-08-09 13:26:33 -07:00
project : this._project.config ,
config : this._loader.fullConfig ( ) ,
2021-07-15 22:02:10 -07:00
title : test.title ,
2021-11-01 20:23:35 -08:00
titlePath : test.titlePath ( ) ,
2021-07-16 12:40:33 -07:00
file : test.location.file ,
line : test.location.line ,
column : test.location.column ,
2021-07-15 22:02:10 -07:00
fn : test.fn ,
2021-06-06 17:09:53 -07:00
repeatEachIndex : this._params.repeatEachIndex ,
2021-08-09 13:26:33 -07:00
retry ,
2021-07-29 14:33:37 -07:00
expectedStatus : test.expectedStatus ,
2021-06-06 17:09:53 -07:00
annotations : [ ] ,
2021-07-16 13:48:37 -07:00
attachments : [ ] ,
2022-01-05 16:39:33 -08:00
attach : async ( name : string , options : { path? : string , body? : string | Buffer , contentType? : string } = { } ) = > {
if ( ( options . path !== undefined ? 1 : 0 ) + ( options . body !== undefined ? 1 : 0 ) !== 1 )
throw new Error ( ` Exactly one of "path" and "body" must be specified ` ) ;
2022-01-24 16:06:36 -07:00
if ( options . path !== undefined ) {
2022-01-13 10:38:47 -08:00
const hash = calculateSha1 ( options . path ) ;
2022-01-05 16:39:33 -08:00
const dest = testInfo . outputPath ( 'attachments' , hash + path . extname ( options . path ) ) ;
2021-11-23 09:30:53 -08:00
await fs . promises . mkdir ( path . dirname ( dest ) , { recursive : true } ) ;
2022-01-05 16:39:33 -08:00
await fs . promises . copyFile ( options . path , dest ) ;
const contentType = options . contentType ? ? ( mime . getType ( path . basename ( options . path ) ) || 'application/octet-stream' ) ;
testInfo . attachments . push ( { name , contentType , path : dest } ) ;
} else {
const contentType = options . contentType ? ? ( typeof options . body === 'string' ? 'text/plain' : 'application/octet-stream' ) ;
testInfo . attachments . push ( { name , contentType , body : typeof options . body === 'string' ? Buffer . from ( options . body ) : options . body } ) ;
2021-11-23 09:30:53 -08:00
}
} ,
2021-06-06 17:09:53 -07:00
duration : 0 ,
status : 'passed' ,
stdout : [ ] ,
stderr : [ ] ,
timeout : this._project.config.timeout ,
snapshotSuffix : '' ,
outputDir : baseOutputDir ,
2021-11-02 10:02:49 -05:00
snapshotDir ,
2021-06-06 17:09:53 -07:00
outputPath : ( . . . pathSegments : string [ ] ) : string = > {
fs . mkdirSync ( baseOutputDir , { recursive : true } ) ;
2021-10-01 11:15:44 -05:00
const joinedPath = path . join ( . . . pathSegments ) ;
const outputPath = getContainedPath ( baseOutputDir , joinedPath ) ;
if ( outputPath ) return outputPath ;
throw new Error ( ` The outputPath is not allowed outside of the parent directory. Please fix the defined path. \ n \ n \ toutputPath: ${ joinedPath } ` ) ;
2021-06-06 17:09:53 -07:00
} ,
2021-10-01 11:15:44 -05:00
snapshotPath : ( . . . pathSegments : string [ ] ) : string = > {
2021-06-06 17:09:53 -07:00
let suffix = '' ;
if ( this . _projectNamePathSegment )
suffix += '-' + this . _projectNamePathSegment ;
if ( testInfo . snapshotSuffix )
suffix += '-' + testInfo . snapshotSuffix ;
2021-10-01 11:15:44 -05:00
const subPath = addSuffixToFilePath ( path . join ( . . . pathSegments ) , suffix ) ;
2021-11-02 10:02:49 -05:00
const snapshotPath = getContainedPath ( snapshotDir , subPath ) ;
2021-10-01 11:15:44 -05:00
if ( snapshotPath ) return snapshotPath ;
throw new Error ( ` The snapshotPath is not allowed outside of the parent directory. Please fix the defined path. \ n \ n \ tsnapshotPath: ${ subPath } ` ) ;
2021-06-06 17:09:53 -07:00
} ,
skip : ( . . . args : [ arg? : any , description? : string ] ) = > modifier ( testInfo , 'skip' , args ) ,
fixme : ( . . . args : [ arg? : any , description? : string ] ) = > modifier ( testInfo , 'fixme' , args ) ,
fail : ( . . . args : [ arg? : any , description? : string ] ) = > modifier ( testInfo , 'fail' , args ) ,
slow : ( . . . args : [ arg? : any , description? : string ] ) = > modifier ( testInfo , 'slow' , args ) ,
setTimeout : ( timeout : number ) = > {
2021-12-17 15:17:48 -08:00
if ( ! testInfo . timeout )
return ; // Zero timeout means some debug mode - do not set a timeout.
2021-06-06 17:09:53 -07:00
testInfo . timeout = timeout ;
if ( deadlineRunner )
2021-08-31 14:44:08 -07:00
deadlineRunner . updateDeadline ( deadline ( ) ) ;
2021-06-06 17:09:53 -07:00
} ,
2021-09-16 15:51:27 -07:00
_addStep : data = > {
const stepId = ` ${ data . category } @ ${ data . title } @ ${ ++ lastStepId } ` ;
2021-09-03 13:08:17 -07:00
let callbackHandled = false ;
const step : TestStepInternal = {
2021-09-16 15:51:27 -07:00
. . . data ,
2021-09-03 13:08:17 -07:00
complete : ( error? : Error | TestError ) = > {
if ( callbackHandled )
return ;
callbackHandled = true ;
if ( error instanceof Error )
error = serializeError ( error ) ;
const payload : StepEndPayload = {
testId ,
stepId ,
wallTime : Date.now ( ) ,
error ,
} ;
2021-12-15 10:39:49 -08:00
this . emit ( 'stepEnd' , payload ) ;
2021-09-03 13:08:17 -07:00
}
} ;
2021-10-18 21:14:01 -08:00
const hasLocation = data . location && ! data . location . file . includes ( '@playwright' ) ;
// Sanitize location that comes from user land, it might have extra properties.
const location = data . location && hasLocation ? { file : data.location.file , line : data.location.line , column : data.location.column } : undefined ;
2021-09-03 13:08:17 -07:00
const payload : StepBeginPayload = {
2021-08-02 17:17:20 -07:00
testId ,
stepId ,
2021-09-16 15:51:27 -07:00
. . . data ,
2021-10-18 20:06:18 -08:00
location ,
2021-08-31 16:34:52 -07:00
wallTime : Date.now ( ) ,
2021-08-02 17:17:20 -07:00
} ;
2021-12-15 10:39:49 -08:00
this . emit ( 'stepBegin' , payload ) ;
2021-09-03 13:08:17 -07:00
return step ;
2021-08-02 17:17:20 -07:00
} ,
2021-06-06 17:09:53 -07:00
} ;
2021-06-29 13:33:13 -07:00
// Inherit test.setTimeout() from parent suites.
2021-10-19 08:38:04 -07:00
for ( let suite : Suite | undefined = test . parent ; suite ; suite = suite . parent ) {
2021-06-29 13:33:13 -07:00
if ( suite . _timeout !== undefined ) {
testInfo . setTimeout ( suite . _timeout ) ;
break ;
}
}
2021-07-02 15:49:05 -07:00
// Process annotations defined on parent suites.
for ( const annotation of annotations ) {
testInfo . annotations . push ( annotation ) ;
switch ( annotation . type ) {
case 'fixme' :
case 'skip' :
testInfo . expectedStatus = 'skipped' ;
break ;
case 'fail' :
if ( testInfo . expectedStatus !== 'skipped' )
testInfo . expectedStatus = 'failed' ;
break ;
case 'slow' :
testInfo . setTimeout ( testInfo . timeout * 3 ) ;
break ;
}
}
2021-09-27 21:38:19 -07:00
const testData : TestData = { testInfo , testId , type : test . _type } ;
this . _currentTest = testData ;
2021-08-10 10:54:05 -07:00
setCurrentTestInfo ( testInfo ) ;
2021-06-06 17:09:53 -07:00
const deadline = ( ) = > {
2021-08-31 14:44:08 -07:00
return testInfo . timeout ? startTime + testInfo.timeout : 0 ;
2021-06-06 17:09:53 -07:00
} ;
2021-12-15 10:39:49 -08:00
this . emit ( 'testBegin' , buildTestBeginPayload ( testData , startWallTime ) ) ;
2021-06-06 17:09:53 -07:00
if ( testInfo . expectedStatus === 'skipped' ) {
testInfo . status = 'skipped' ;
2021-12-15 10:39:49 -08:00
this . emit ( 'testEnd' , buildTestEndPayload ( testData ) ) ;
2021-06-06 17:09:53 -07:00
return ;
}
// Update the fixture pool - it may differ between tests, but only in test-scoped fixtures.
2021-07-15 22:02:10 -07:00
this . _fixtureRunner . setPool ( test . _pool ! ) ;
2021-06-06 17:09:53 -07:00
2021-08-10 10:54:05 -07:00
this . _currentDeadlineRunner = deadlineRunner = new DeadlineRunner ( this . _runTestWithBeforeHooks ( test , testInfo ) , deadline ( ) ) ;
2021-06-06 17:09:53 -07:00
const result = await deadlineRunner . result ;
// Do not overwrite test failure upon hook timeout.
if ( result . timedOut && testInfo . status === 'passed' )
testInfo . status = 'timedOut' ;
2021-11-10 16:02:27 -08:00
testInfo . duration = monotonicTime ( ) - startTime ;
2021-08-10 10:54:05 -07:00
if ( ! result . timedOut ) {
this . _currentDeadlineRunner = deadlineRunner = new DeadlineRunner ( this . _runAfterHooks ( test , testInfo ) , deadline ( ) ) ;
2021-08-31 14:44:08 -07:00
deadlineRunner . updateDeadline ( deadline ( ) ) ;
2021-08-10 10:54:05 -07:00
const hooksResult = await deadlineRunner . result ;
// Do not overwrite test failure upon hook timeout.
if ( hooksResult . timedOut && testInfo . status === 'passed' )
testInfo . status = 'timedOut' ;
} else {
// A timed-out test gets a full additional timeout to run after hooks.
const newDeadline = this . _deadline ( ) ;
this . _currentDeadlineRunner = deadlineRunner = new DeadlineRunner ( this . _runAfterHooks ( test , testInfo ) , newDeadline ) ;
await deadlineRunner . result ;
2021-06-06 17:09:53 -07:00
}
testInfo . duration = monotonicTime ( ) - startTime ;
2021-09-28 10:56:50 -07:00
this . _currentDeadlineRunner = undefined ;
2021-08-10 10:54:05 -07:00
this . _currentTest = null ;
setCurrentTestInfo ( null ) ;
2021-07-28 15:43:37 -07:00
2021-09-28 10:56:50 -07:00
const isFailure = testInfo . status !== 'skipped' && testInfo . status !== testInfo . expectedStatus ;
2021-08-25 12:19:50 -07:00
if ( isFailure ) {
2021-12-15 10:39:49 -08:00
// Delay reporting testEnd result until after teardownScopes is done.
this . _failedTest = testData ;
if ( test . _type !== 'test' ) {
// beforeAll/afterAll hook failure skips any remaining tests in the worker.
2021-11-18 14:36:55 -08:00
if ( ! this . _fatalError )
2021-08-28 07:19:45 -07:00
this . _fatalError = testInfo . error ;
2021-11-18 14:36:55 -08:00
// Keep any error we have, and add "timeout" message.
if ( testInfo . status === 'timedOut' )
2021-12-18 09:32:41 -08:00
this . _fatalError = prependToTestError ( this . _fatalError , colors . red ( ` Timeout of ${ testInfo . timeout } ms exceeded in ${ test . _type } hook. \ n ` ) , test . location ) ;
2021-08-28 07:19:45 -07:00
}
2021-08-10 10:54:05 -07:00
this . stop ( ) ;
2021-12-15 10:39:49 -08:00
} else {
this . emit ( 'testEnd' , buildTestEndPayload ( testData ) ) ;
2021-06-06 17:09:53 -07:00
}
2021-09-28 10:56:50 -07:00
const preserveOutput = this . _loader . fullConfig ( ) . preserveOutput === 'always' ||
( this . _loader . fullConfig ( ) . preserveOutput === 'failures-only' && isFailure ) ;
if ( ! preserveOutput )
await removeFolderAsync ( testInfo . outputDir ) . catch ( e = > { } ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-09 13:26:33 -07:00
private async _runBeforeHooks ( test : TestCase , testInfo : TestInfoImpl ) {
2022-01-10 20:25:56 -08:00
const beforeEachModifiers : Modifier [ ] = [ ] ;
for ( let s : Suite | undefined = test . parent ; s ; s = s . parent ) {
const modifiers = s . _modifiers . filter ( modifier = > ! this . _fixtureRunner . dependsOnWorkerFixturesOnly ( modifier . fn , modifier . location ) ) ;
beforeEachModifiers . push ( . . . modifiers . reverse ( ) ) ;
}
beforeEachModifiers . reverse ( ) ;
for ( const modifier of beforeEachModifiers ) {
const result = await this . _fixtureRunner . resolveParametersAndRunFunction ( modifier . fn , this . _workerInfo , testInfo ) ;
testInfo [ modifier . type ] ( ! ! result , modifier . description ! ) ;
2021-06-06 17:09:53 -07:00
}
2022-01-10 20:25:56 -08:00
await this . _runHooks ( test . parent ! , 'beforeEach' , testInfo ) ;
2021-08-09 13:26:33 -07:00
}
private async _runTestWithBeforeHooks ( test : TestCase , testInfo : TestInfoImpl ) {
2021-09-16 15:51:27 -07:00
const step = testInfo . _addStep ( {
category : 'hook' ,
title : 'Before Hooks' ,
canHaveChildren : true ,
forceNoParent : true
} ) ;
2022-01-10 20:25:56 -08:00
let error1 : TestError | undefined ;
2021-08-09 13:26:33 -07:00
if ( test . _type === 'test' )
2022-01-10 20:25:56 -08:00
error1 = await this . _runFn ( ( ) = > this . _runBeforeHooks ( test , testInfo ) , testInfo , 'allowSkips' ) ;
2021-06-06 17:09:53 -07:00
// Do not run the test when beforeEach hook fails.
2021-08-12 07:58:00 -07:00
if ( testInfo . status === 'failed' || testInfo . status === 'skipped' ) {
2022-01-10 20:25:56 -08:00
step . complete ( error1 ) ;
2021-06-06 17:09:53 -07:00
return ;
2021-08-12 07:58:00 -07:00
}
2021-06-06 17:09:53 -07:00
2022-01-10 20:25:56 -08:00
const error2 = await this . _runFn ( async ( ) = > {
const params = await this . _fixtureRunner . resolveParametersForFunction ( test . fn , this . _workerInfo , testInfo ) ;
step . complete ( ) ; // Report fixture hooks step as completed.
const fn = test . fn ; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
await fn ( params , testInfo ) ;
} , testInfo , 'allowSkips' ) ;
step . complete ( error2 ) ; // Second complete is a no-op.
2021-06-06 17:09:53 -07:00
}
2021-08-02 17:17:20 -07:00
private async _runAfterHooks ( test : TestCase , testInfo : TestInfoImpl ) {
2022-01-10 20:25:56 -08:00
const step = testInfo . _addStep ( {
category : 'hook' ,
title : 'After Hooks' ,
canHaveChildren : true ,
forceNoParent : true
} ) ;
let teardownError1 : TestError | undefined ;
if ( test . _type === 'test' )
teardownError1 = await this . _runFn ( ( ) = > this . _runHooks ( test . parent ! , 'afterEach' , testInfo ) , testInfo , 'disallowSkips' ) ;
// Continue teardown even after the failure.
const teardownError2 = await this . _runFn ( ( ) = > this . _fixtureRunner . teardownScope ( 'test' ) , testInfo , 'disallowSkips' ) ;
step . complete ( teardownError1 || teardownError2 ) ;
2021-06-06 17:09:53 -07:00
}
private async _runHooks ( suite : Suite , type : 'beforeEach' | 'afterEach' , testInfo : TestInfo ) {
const all = [ ] ;
for ( let s : Suite | undefined = suite ; s ; s = s . parent ) {
2021-08-09 13:26:33 -07:00
const funcs = s . _eachHooks . filter ( e = > e . type === type ) . map ( e = > e . fn ) ;
2021-06-06 17:09:53 -07:00
all . push ( . . . funcs . reverse ( ) ) ;
}
if ( type === 'beforeEach' )
all . reverse ( ) ;
let error : Error | undefined ;
for ( const hook of all ) {
try {
2022-01-10 20:25:56 -08:00
await this . _fixtureRunner . resolveParametersAndRunFunction ( hook , this . _workerInfo , testInfo ) ;
2021-06-06 17:09:53 -07:00
} catch ( e ) {
// Always run all the hooks, and capture the first error.
error = error || e ;
}
}
if ( error )
throw error ;
}
2022-01-10 20:25:56 -08:00
private async _runFn ( fn : Function , testInfo : TestInfoImpl , skips : 'allowSkips' | 'disallowSkips' ) : Promise < TestError | undefined > {
try {
await fn ( ) ;
} catch ( error ) {
if ( skips === 'allowSkips' && error instanceof SkipError ) {
if ( testInfo . status === 'passed' )
testInfo . status = 'skipped' ;
} else {
const serialized = serializeError ( error ) ;
// Do not overwrite any previous error and error status.
// Some (but not all) scenarios include:
// - expect() that fails after uncaught exception.
// - fail after the timeout, e.g. due to fixture teardown.
if ( testInfo . status === 'passed' )
testInfo . status = 'failed' ;
if ( ! ( 'error' in testInfo ) )
testInfo . error = serialized ;
return serialized ;
}
}
}
2021-06-06 17:09:53 -07:00
private _reportDone() {
2021-09-27 21:38:19 -07:00
const donePayload : DonePayload = { fatalError : this._fatalError } ;
2021-06-06 17:09:53 -07:00
this . emit ( 'done' , donePayload ) ;
2021-08-31 10:50:30 -07:00
this . _fatalError = undefined ;
2021-09-27 21:38:19 -07:00
this . _failedTest = undefined ;
2021-06-06 17:09:53 -07:00
}
}
2021-12-15 10:39:49 -08:00
function buildTestBeginPayload ( testData : TestData , startWallTime : number ) : TestBeginPayload {
2021-06-06 17:09:53 -07:00
return {
2021-12-15 10:39:49 -08:00
testId : testData.testId ,
2021-07-18 17:40:59 -07:00
startWallTime ,
2021-06-06 17:09:53 -07:00
} ;
}
2021-12-15 10:39:49 -08:00
function buildTestEndPayload ( testData : TestData ) : TestEndPayload {
const { testId , testInfo } = testData ;
2021-06-06 17:09:53 -07:00
return {
testId ,
duration : testInfo.duration ,
status : testInfo.status ! ,
error : testInfo.error ,
expectedStatus : testInfo.expectedStatus ,
annotations : testInfo.annotations ,
timeout : testInfo.timeout ,
2021-07-16 13:48:37 -07:00
attachments : testInfo.attachments.map ( a = > ( {
name : a.name ,
contentType : a.contentType ,
path : a.path ,
body : a.body?.toString ( 'base64' )
} ) )
2021-06-06 17:09:53 -07:00
} ;
}
function modifier ( testInfo : TestInfo , type : 'skip' | 'fail' | 'fixme' | 'slow' , modifierArgs : [ arg? : any , description? : string ] ) {
2021-07-29 14:33:37 -07:00
if ( typeof modifierArgs [ 1 ] === 'function' ) {
throw new Error ( [
'It looks like you are calling test.skip() inside the test and pass a callback.' ,
'Pass a condition instead and optional description instead:' ,
` test('my test', async ({ page, isMobile }) => { ` ,
` test.skip(isMobile, 'This test is not applicable on mobile'); ` ,
` }); ` ,
] . join ( '\n' ) ) ;
}
2021-06-06 17:09:53 -07:00
if ( modifierArgs . length >= 1 && ! modifierArgs [ 0 ] )
return ;
const description = modifierArgs [ 1 ] ;
testInfo . annotations . push ( { type , description } ) ;
if ( type === 'slow' ) {
testInfo . setTimeout ( testInfo . timeout * 3 ) ;
} else if ( type === 'skip' || type === 'fixme' ) {
testInfo . expectedStatus = 'skipped' ;
throw new SkipError ( 'Test is skipped: ' + ( description || '' ) ) ;
} else if ( type === 'fail' ) {
if ( testInfo . expectedStatus !== 'skipped' )
testInfo . expectedStatus = 'failed' ;
}
}
class SkipError extends Error {
}