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 .
* /
import * as fs from 'fs' ;
2021-06-08 11:22:07 -07:00
import * as path from 'path' ;
2022-11-02 20:22:35 -07:00
import type { APIRequestContext , BrowserContext , BrowserContextOptions , LaunchOptions , Page , Tracing , Video } from 'playwright-core' ;
2023-03-13 16:50:51 -07:00
import type { BrowserType as BrowserTypeImpl } from 'playwright-core/lib/client/browserType' ;
2022-11-02 20:22:35 -07:00
import * as playwrightLibrary from 'playwright-core' ;
2023-02-28 13:26:23 -08:00
import { createGuid , debugMode , removeFolders , addInternalStackPrefix , mergeTraceFiles , saveTraceFile } from 'playwright-core/lib/utils' ;
2022-12-21 10:16:36 -08:00
import type { Fixtures , PlaywrightTestArgs , PlaywrightTestOptions , PlaywrightWorkerArgs , PlaywrightWorkerOptions , ScreenshotMode , TestInfo , TestType , TraceMode , VideoMode } from '../types/test' ;
2023-02-09 19:22:17 -08:00
import type { TestInfoImpl } from './worker/testInfo' ;
2023-01-26 17:26:47 -08:00
import { rootTestType } from './common/testType' ;
import { type ContextReuseMode } from './common/types' ;
2023-03-14 15:58:55 -07:00
import { artifactsFolderName } from './isomorphic/folders' ;
2023-01-26 17:26:47 -08:00
export { expect } from './matchers/expect' ;
2023-02-16 16:48:28 -08:00
export { store } from './store' ;
2022-11-02 20:22:35 -07:00
export const _baseTest : TestType < { } , { } > = rootTestType . test ;
2021-06-15 10:06:49 -07:00
2023-02-21 14:15:11 -08:00
addInternalStackPrefix ( path . dirname ( require . resolve ( '../package.json' ) ) ) ;
2023-01-17 22:38:30 +01:00
2022-01-06 09:29:05 -08:00
if ( ( process as any ) [ '__pw_initiator__' ] ) {
const originalStackTraceLimit = Error . stackTraceLimit ;
Error . stackTraceLimit = 200 ;
try {
throw new Error ( 'Requiring @playwright/test second time, \nFirst:\n' + ( process as any ) [ '__pw_initiator__' ] + '\n\nSecond: ' ) ;
} finally {
Error . stackTraceLimit = originalStackTraceLimit ;
}
} else {
( process as any ) [ '__pw_initiator__' ] = new Error ( ) . stack ;
}
2021-08-09 18:09:11 -07:00
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions : BrowserContextOptions ,
2022-12-28 16:58:34 -08:00
_contextReuseMode : ContextReuseMode ,
2022-07-12 13:30:24 -08:00
_reuseContext : boolean ,
2021-08-09 18:09:11 -07:00
_setupContextOptionsAndArtifacts : void ;
2021-10-28 07:31:30 -08:00
_contextFactory : ( options? : BrowserContextOptions ) = > Promise < BrowserContext > ;
2021-08-09 18:09:11 -07:00
} ;
feat(test runner): replace declare/define with "options" (#10293)
1. Fixtures defined in test.extend() can now have `{ option: true }` configuration that makes them overridable in the config. Options support all other properties of fixtures - value/function, scope, auto.
```
const test = base.extend<MyOptions>({
foo: ['default', { option: true }],
});
```
2. test.declare() and project.define are removed.
3. project.use applies overrides to default option values and nothing else. Any test.extend() and test.use() calls take priority over config options.
Required user changes: if someone used to define fixture options with test.extend(), overriding them in config will stop working. The solution is to add `{ option: true }`.
```
// Old code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: 123,
myFixture: ({ myOption }, use) => use(2 * myOption),
});
// New code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: [123, { option: true }],
myFixture: ({ myOption }, use) => use(2 * myOption),
});
```
2021-11-18 15:45:52 -08:00
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
2021-10-26 12:45:53 -08:00
_browserOptions : LaunchOptions ;
2021-10-28 07:31:30 -08:00
_artifactsDir : ( ) = > string ;
_snapshotSuffix : string ;
2021-08-09 18:09:11 -07:00
} ;
2021-06-15 10:06:49 -07:00
2022-12-05 18:15:01 -08:00
const playwrightFixtures : Fixtures < TestFixtures , WorkerFixtures > = ( {
2022-08-18 20:12:33 +02:00
defaultBrowserType : [ 'chromium' , { scope : 'worker' , option : true } ] ,
browserName : [ ( { defaultBrowserType } , use ) = > use ( defaultBrowserType ) , { scope : 'worker' , option : true } ] ,
2022-12-13 16:04:44 -08:00
playwright : [ async ( { } , use ) = > {
await use ( require ( 'playwright-core' ) ) ;
2022-08-18 20:12:33 +02:00
} , { scope : 'worker' } ] ,
headless : [ ( { launchOptions } , use ) = > use ( launchOptions . headless ? ? true ) , { scope : 'worker' , option : true } ] ,
channel : [ ( { launchOptions } , use ) = > use ( launchOptions . channel ) , { scope : 'worker' , option : true } ] ,
launchOptions : [ { } , { scope : 'worker' , option : true } ] ,
2022-10-20 21:30:37 -04:00
connectOptions : [ ( { } , use ) = > {
2023-03-13 16:50:51 -07:00
// Usually, when connect options are specified (e.g, in the config or in the environment),
// all launch() calls are turned into connect() calls.
// However, when running in "reuse browser" mode and connecting to the reusable server,
// only the default "browser" fixture should turn into reused browser.
use ( process . env . PW_TEST_REUSE_CONTEXT ? undefined : connectOptionsFromEnv ( ) ) ;
2022-10-20 21:30:37 -04:00
} , { scope : 'worker' , option : true } ] ,
2022-08-18 20:12:33 +02:00
screenshot : [ 'off' , { scope : 'worker' , option : true } ] ,
video : [ 'off' , { scope : 'worker' , option : true } ] ,
trace : [ 'off' , { scope : 'worker' , option : true } ] ,
2021-08-09 18:09:11 -07:00
_artifactsDir : [ async ( { } , use , workerInfo ) = > {
let dir : string | undefined ;
await use ( ( ) = > {
if ( ! dir ) {
2023-03-14 15:58:55 -07:00
dir = path . join ( workerInfo . project . outputDir , artifactsFolderName ( workerInfo . workerIndex ) ) ;
2021-08-09 18:09:11 -07:00
fs . mkdirSync ( dir , { recursive : true } ) ;
}
return dir ;
} ) ;
if ( dir )
await removeFolders ( [ dir ] ) ;
2022-06-30 17:05:08 -07:00
} , { scope : 'worker' , _title : 'playwright configuration' } as any ] ,
2021-06-03 08:07:55 -07:00
2023-03-14 15:58:55 -07:00
_browserOptions : [ async ( { playwright , headless , channel , launchOptions , connectOptions , _artifactsDir } , use ) = > {
2022-01-28 10:51:55 -08:00
const options : LaunchOptions = {
handleSIGINT : false ,
2022-04-13 15:13:31 -07:00
timeout : 0 ,
2022-01-28 10:51:55 -08:00
. . . launchOptions ,
} ;
if ( headless !== undefined )
options . headless = headless ;
if ( channel !== undefined )
options . channel = channel ;
2023-03-14 15:58:55 -07:00
options . tracesDir = path . join ( _artifactsDir ( ) , 'traces' ) ;
2022-02-09 16:30:14 -07:00
2022-09-07 15:57:20 -07:00
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
2023-03-13 16:50:51 -07:00
( browserType as BrowserTypeImpl ) . _defaultLaunchOptions = options ;
( browserType as BrowserTypeImpl ) . _defaultConnectOptions = connectOptions ;
2022-09-07 15:57:20 -07:00
}
2022-01-28 10:51:55 -08:00
await use ( options ) ;
2022-09-07 15:57:20 -07:00
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
2023-03-13 16:50:51 -07:00
( browserType as BrowserTypeImpl ) . _defaultLaunchOptions = undefined ;
( browserType as BrowserTypeImpl ) . _defaultConnectOptions = undefined ;
2022-02-08 20:45:42 -08:00
}
2022-09-07 15:57:20 -07:00
} , { scope : 'worker' , auto : true } ] ,
2022-02-08 20:45:42 -08:00
2023-03-13 16:50:51 -07:00
browser : [ async ( { playwright , browserName , _browserOptions } , use , testInfo ) = > {
2022-04-13 15:13:31 -07:00
if ( ! [ 'chromium' , 'firefox' , 'webkit' ] . includes ( browserName ) )
throw new Error ( ` Unexpected browserName " ${ browserName } ", must be one of "chromium", "firefox" or "webkit" ` ) ;
2023-03-13 16:50:51 -07:00
// Support for "reuse browser" mode.
const connectOptions = connectOptionsFromEnv ( ) ;
if ( connectOptions && process . env . PW_TEST_REUSE_CONTEXT ) {
const browser = await playwright [ browserName ] . connect ( {
. . . connectOptions ,
headers : {
'x-playwright-reuse-context' : '1' ,
'x-playwright-launch-options' : JSON . stringify ( _browserOptions ) ,
. . . connectOptions . headers ,
} ,
} ) ;
await use ( browser ) ;
await browser . close ( ) ;
return ;
}
2022-01-28 10:51:55 -08:00
const browser = await playwright [ browserName ] . launch ( ) ;
await use ( browser ) ;
await browser . close ( ) ;
2022-08-18 20:12:33 +02:00
} , { scope : 'worker' , timeout : 0 } ] ,
2021-06-03 08:07:55 -07:00
2022-08-18 20:12:33 +02:00
acceptDownloads : [ ( { contextOptions } , use ) = > use ( contextOptions . acceptDownloads ? ? true ) , { option : true } ] ,
2023-02-02 13:11:50 -08:00
bypassCSP : [ ( { contextOptions } , use ) = > use ( contextOptions . bypassCSP ? ? false ) , { option : true } ] ,
colorScheme : [ ( { contextOptions } , use ) = > use ( contextOptions . colorScheme === undefined ? 'light' : contextOptions . colorScheme ) , { option : true } ] ,
2022-08-18 20:12:33 +02:00
deviceScaleFactor : [ ( { contextOptions } , use ) = > use ( contextOptions . deviceScaleFactor ) , { option : true } ] ,
extraHTTPHeaders : [ ( { contextOptions } , use ) = > use ( contextOptions . extraHTTPHeaders ) , { option : true } ] ,
geolocation : [ ( { contextOptions } , use ) = > use ( contextOptions . geolocation ) , { option : true } ] ,
2023-02-02 13:11:50 -08:00
hasTouch : [ ( { contextOptions } , use ) = > use ( contextOptions . hasTouch ? ? false ) , { option : true } ] ,
2022-08-18 20:12:33 +02:00
httpCredentials : [ ( { contextOptions } , use ) = > use ( contextOptions . httpCredentials ) , { option : true } ] ,
2023-02-02 13:11:50 -08:00
ignoreHTTPSErrors : [ ( { contextOptions } , use ) = > use ( contextOptions . ignoreHTTPSErrors ? ? false ) , { option : true } ] ,
isMobile : [ ( { contextOptions } , use ) = > use ( contextOptions . isMobile ? ? false ) , { option : true } ] ,
2022-08-18 20:12:33 +02:00
javaScriptEnabled : [ ( { contextOptions } , use ) = > use ( contextOptions . javaScriptEnabled ? ? true ) , { option : true } ] ,
locale : [ ( { contextOptions } , use ) = > use ( contextOptions . locale ? ? 'en-US' ) , { option : true } ] ,
2023-02-02 13:11:50 -08:00
offline : [ ( { contextOptions } , use ) = > use ( contextOptions . offline ? ? false ) , { option : true } ] ,
2022-08-18 20:12:33 +02:00
permissions : [ ( { contextOptions } , use ) = > use ( contextOptions . permissions ) , { option : true } ] ,
proxy : [ ( { contextOptions } , use ) = > use ( contextOptions . proxy ) , { option : true } ] ,
storageState : [ ( { contextOptions } , use ) = > use ( contextOptions . storageState ) , { option : true } ] ,
timezoneId : [ ( { contextOptions } , use ) = > use ( contextOptions . timezoneId ) , { option : true } ] ,
userAgent : [ ( { contextOptions } , use ) = > use ( contextOptions . userAgent ) , { option : true } ] ,
2022-07-01 09:20:13 -08:00
viewport : [ ( { contextOptions } , use ) = > use ( contextOptions . viewport === undefined ? { width : 1280 , height : 720 } : contextOptions . viewport ) , { option : true } ] ,
2022-08-18 20:12:33 +02:00
actionTimeout : [ 0 , { option : true } ] ,
2022-09-27 20:06:07 -08:00
testIdAttribute : [ 'data-testid' , { option : true } ] ,
2022-08-18 20:12:33 +02:00
navigationTimeout : [ 0 , { option : true } ] ,
baseURL : [ async ( { } , use ) = > {
2021-07-07 20:19:42 +02:00
await use ( process . env . PLAYWRIGHT_TEST_BASE_URL ) ;
2022-08-18 20:12:33 +02:00
} , { option : true } ] ,
serviceWorkers : [ ( { contextOptions } , use ) = > use ( contextOptions . serviceWorkers ? ? 'allow' ) , { option : true } ] ,
contextOptions : [ { } , { option : true } ] ,
2021-06-03 08:07:55 -07:00
2021-08-09 18:09:11 -07:00
_combinedContextOptions : async ( {
2021-07-29 21:03:50 -07:00
acceptDownloads ,
bypassCSP ,
colorScheme ,
deviceScaleFactor ,
extraHTTPHeaders ,
hasTouch ,
geolocation ,
httpCredentials ,
ignoreHTTPSErrors ,
isMobile ,
javaScriptEnabled ,
locale ,
offline ,
permissions ,
proxy ,
storageState ,
viewport ,
timezoneId ,
userAgent ,
baseURL ,
contextOptions ,
2022-06-08 18:27:51 -04:00
serviceWorkers ,
2021-08-09 18:09:11 -07:00
} , use ) = > {
2021-07-29 14:03:58 -07:00
const options : BrowserContextOptions = { } ;
2021-06-03 08:07:55 -07:00
if ( acceptDownloads !== undefined )
options . acceptDownloads = acceptDownloads ;
if ( bypassCSP !== undefined )
options . bypassCSP = bypassCSP ;
if ( colorScheme !== undefined )
options . colorScheme = colorScheme ;
if ( deviceScaleFactor !== undefined )
options . deviceScaleFactor = deviceScaleFactor ;
if ( extraHTTPHeaders !== undefined )
options . extraHTTPHeaders = extraHTTPHeaders ;
if ( geolocation !== undefined )
options . geolocation = geolocation ;
if ( hasTouch !== undefined )
options . hasTouch = hasTouch ;
if ( httpCredentials !== undefined )
options . httpCredentials = httpCredentials ;
if ( ignoreHTTPSErrors !== undefined )
options . ignoreHTTPSErrors = ignoreHTTPSErrors ;
if ( isMobile !== undefined )
options . isMobile = isMobile ;
if ( javaScriptEnabled !== undefined )
options . javaScriptEnabled = javaScriptEnabled ;
if ( locale !== undefined )
options . locale = locale ;
if ( offline !== undefined )
options . offline = offline ;
if ( permissions !== undefined )
options . permissions = permissions ;
if ( proxy !== undefined )
options . proxy = proxy ;
2023-01-18 12:56:03 -08:00
if ( storageState !== undefined )
2021-06-03 08:07:55 -07:00
options . storageState = storageState ;
if ( timezoneId !== undefined )
options . timezoneId = timezoneId ;
if ( userAgent !== undefined )
options . userAgent = userAgent ;
if ( viewport !== undefined )
options . viewport = viewport ;
2021-07-07 20:19:42 +02:00
if ( baseURL !== undefined )
options . baseURL = baseURL ;
2022-06-08 18:27:51 -04:00
if ( serviceWorkers !== undefined )
options . serviceWorkers = serviceWorkers ;
2021-08-09 18:09:11 -07:00
await use ( {
. . . contextOptions ,
. . . options ,
} ) ;
} ,
2022-09-13 15:49:04 -07:00
_snapshotSuffix : [ process . platform , { scope : 'worker' } ] ,
2021-10-28 07:31:30 -08:00
2022-12-28 16:58:34 -08:00
_setupContextOptionsAndArtifacts : [ async ( { playwright , _snapshotSuffix , _combinedContextOptions , _reuseContext , _artifactsDir , trace , screenshot , actionTimeout , navigationTimeout , testIdAttribute } , use , testInfo ) = > {
2022-09-27 20:06:07 -08:00
if ( testIdAttribute )
playwrightLibrary . selectors . setTestIdAttribute ( testIdAttribute ) ;
2021-10-28 07:31:30 -08:00
testInfo . snapshotSuffix = _snapshotSuffix ;
2022-01-25 09:40:24 -07:00
if ( debugMode ( ) )
2021-08-09 18:09:11 -07:00
testInfo . setTimeout ( 0 ) ;
2022-12-21 10:16:36 -08:00
const screenshotMode = normalizeScreenshotMode ( screenshot ) ;
const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot ;
2022-05-31 15:59:36 -07:00
const traceMode = normalizeTraceMode ( trace ) ;
2021-11-08 15:39:58 -08:00
const defaultTraceOptions = { screenshots : true , snapshots : true , sources : true } ;
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { . . . defaultTraceOptions , . . . trace , mode : undefined } ;
2023-01-05 14:50:47 -08:00
const captureTrace = shouldCaptureTrace ( traceMode , testInfo ) ;
2021-08-09 18:09:11 -07:00
const temporaryTraceFiles : string [ ] = [ ] ;
const temporaryScreenshots : string [ ] = [ ] ;
2022-06-27 17:46:39 -07:00
const testInfoImpl = testInfo as TestInfoImpl ;
2023-01-05 14:50:47 -08:00
const reusedContexts = new Set < BrowserContext > ( ) ;
2023-03-14 15:58:55 -07:00
let traceOrdinal = 0 ;
2021-08-09 18:09:11 -07:00
2022-01-11 17:33:41 -08:00
const createInstrumentationListener = ( context? : BrowserContext ) = > {
return {
2023-02-23 14:37:53 -08:00
onApiCallBegin : ( apiCall : string , stackTrace : ParsedStackTrace | null , wallTime : number , userData : any ) = > {
2021-09-27 09:19:59 -07:00
if ( apiCall . startsWith ( 'expect.' ) )
return { userObject : null } ;
2021-12-16 18:32:46 -08:00
if ( apiCall === 'page.pause' ) {
2021-12-15 11:12:52 -08:00
testInfo . setTimeout ( 0 ) ;
2022-01-11 17:33:41 -08:00
context ? . setDefaultNavigationTimeout ( 0 ) ;
context ? . setDefaultTimeout ( 0 ) ;
2021-12-16 18:32:46 -08:00
}
2021-10-18 20:06:18 -08:00
const step = testInfoImpl . _addStep ( {
2022-06-27 17:46:39 -07:00
location : stackTrace?.frames [ 0 ] as any ,
2021-09-16 15:51:27 -07:00
category : 'pw:api' ,
title : apiCall ,
canHaveChildren : false ,
2023-02-23 14:37:53 -08:00
forceNoParent : false ,
wallTime ,
2021-09-16 15:51:27 -07:00
} ) ;
2021-10-26 10:13:35 -08:00
userData . userObject = step ;
2021-09-15 11:34:23 -07:00
} ,
2021-10-26 10:13:35 -08:00
onApiCallEnd : ( userData : any , error? : Error ) = > {
const step = userData . userObject ;
2022-03-30 20:52:00 -08:00
step ? . complete ( { error } ) ;
2021-08-09 18:09:11 -07:00
} ,
2022-01-11 17:33:41 -08:00
} ;
} ;
2023-01-05 14:50:47 -08:00
const startTraceChunkOnContextCreation = async ( tracing : Tracing ) = > {
2022-01-11 17:33:41 -08:00
if ( captureTrace ) {
const title = [ path . relative ( testInfo . project . testDir , testInfo . file ) + ':' + testInfo . line , . . . testInfo . titlePath . slice ( 1 ) ] . join ( ' › ' ) ;
2022-02-09 08:54:09 -08:00
if ( ! ( tracing as any ) [ kTracingStarted ] ) {
2023-03-14 15:58:55 -07:00
const ordinalSuffix = traceOrdinal ? ` - ${ traceOrdinal } ` : '' ;
++ traceOrdinal ;
const retrySuffix = testInfo . retry ? ` - ${ testInfo . retry } ` : '' ;
const name = ` ${ testInfo . testId } ${ retrySuffix } ${ ordinalSuffix } ` ;
await tracing . start ( { . . . traceOptions , title , name } ) ;
2022-02-09 08:54:09 -08:00
( tracing as any ) [ kTracingStarted ] = true ;
2022-01-11 17:33:41 -08:00
} else {
2022-02-09 08:54:09 -08:00
await tracing . startChunk ( { title } ) ;
2022-01-11 17:33:41 -08:00
}
} else {
2022-07-11 12:10:08 -08:00
if ( ( tracing as any ) [ kTracingStarted ] ) {
( tracing as any ) [ kTracingStarted ] = false ;
await tracing . stop ( ) ;
}
2022-01-11 17:33:41 -08:00
}
2022-02-09 08:54:09 -08:00
} ;
const onDidCreateBrowserContext = async ( context : BrowserContext ) = > {
2022-03-01 13:43:38 -08:00
context . setDefaultTimeout ( actionTimeout || 0 ) ;
2023-02-02 07:36:16 -08:00
context . setDefaultNavigationTimeout ( navigationTimeout || 0 ) ;
2023-01-05 14:50:47 -08:00
await startTraceChunkOnContextCreation ( context . tracing ) ;
2022-01-11 17:33:41 -08:00
const listener = createInstrumentationListener ( context ) ;
( context as any ) . _instrumentation . addListener ( listener ) ;
( context . request as any ) . _instrumentation . addListener ( listener ) ;
} ;
const onDidCreateRequestContext = async ( context : APIRequestContext ) = > {
2022-02-09 08:54:09 -08:00
const tracing = ( context as any ) . _tracing as Tracing ;
2023-01-05 14:50:47 -08:00
await startTraceChunkOnContextCreation ( tracing ) ;
2022-01-11 17:33:41 -08:00
( context as any ) . _instrumentation . addListener ( createInstrumentationListener ( ) ) ;
2021-08-09 18:09:11 -07:00
} ;
2021-11-04 21:08:42 -07:00
const startedCollectingArtifacts = Symbol ( 'startedCollectingArtifacts' ) ;
2023-02-25 21:21:09 -08:00
const stopTracing = async ( tracing : Tracing ) = > {
if ( ( tracing as any ) [ startedCollectingArtifacts ] )
return ;
2022-02-09 08:54:09 -08:00
( tracing as any ) [ startedCollectingArtifacts ] = true ;
2021-08-09 18:09:11 -07:00
if ( captureTrace ) {
// Export trace for now. We'll know whether we have to preserve it
// after the test finishes.
const tracePath = path . join ( _artifactsDir ( ) , createGuid ( ) + '.zip' ) ;
temporaryTraceFiles . push ( tracePath ) ;
2022-02-09 08:54:09 -08:00
await tracing . stopChunk ( { path : tracePath } ) ;
2021-08-09 18:09:11 -07:00
}
2022-02-09 08:54:09 -08:00
} ;
2022-06-27 17:46:39 -07:00
const screenshottedSymbol = Symbol ( 'screenshotted' ) ;
const screenshotPage = async ( page : Page ) = > {
if ( ( page as any ) [ screenshottedSymbol ] )
return ;
( page as any ) [ screenshottedSymbol ] = true ;
const screenshotPath = path . join ( _artifactsDir ( ) , createGuid ( ) + '.png' ) ;
temporaryScreenshots . push ( screenshotPath ) ;
2022-08-09 14:03:07 -07:00
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
// and let the page modify itself from the problematic state it had at the moment of failure.
2022-12-07 16:45:33 +00:00
await page . screenshot ( { . . . screenshotOptions , timeout : 5000 , path : screenshotPath , caret : 'initial' } ) . catch ( ( ) = > { } ) ;
2022-06-27 17:46:39 -07:00
} ;
const screenshotOnTestFailure = async ( ) = > {
const contexts : BrowserContext [ ] = [ ] ;
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] )
contexts . push ( . . . ( browserType as any ) . _contexts ) ;
await Promise . all ( contexts . map ( ctx = > Promise . all ( ctx . pages ( ) . map ( screenshotPage ) ) ) ) ;
} ;
2022-02-09 08:54:09 -08:00
const onWillCloseContext = async ( context : BrowserContext ) = > {
2023-01-05 14:50:47 -08:00
// When reusing context, we get all previous contexts closed at the start of next test.
// Do not record empty traces and useless screenshots for them.
if ( reusedContexts . has ( context ) )
return ;
2023-02-25 21:21:09 -08:00
await stopTracing ( context . tracing ) ;
2022-12-07 16:45:33 +00:00
if ( screenshotMode === 'on' || screenshotMode === 'only-on-failure' ) {
2021-08-09 18:09:11 -07:00
// Capture screenshot for now. We'll know whether we have to preserve them
// after the test finishes.
2022-06-27 17:46:39 -07:00
await Promise . all ( context . pages ( ) . map ( screenshotPage ) ) ;
2021-08-09 18:09:11 -07:00
}
} ;
2022-02-09 08:54:09 -08:00
const onWillCloseRequestContext = async ( context : APIRequestContext ) = > {
const tracing = ( context as any ) . _tracing as Tracing ;
2023-02-25 21:21:09 -08:00
await stopTracing ( tracing ) ;
2022-02-09 08:54:09 -08:00
} ;
2021-08-09 18:09:11 -07:00
// 1. Setup instrumentation and process existing contexts.
2021-11-30 19:59:35 +01:00
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
2022-01-11 17:33:41 -08:00
( browserType as any ) . _onDidCreateContext = onDidCreateBrowserContext ;
2021-11-29 12:21:15 -05:00
( browserType as any ) . _onWillCloseContext = onWillCloseContext ;
( browserType as any ) . _defaultContextOptions = _combinedContextOptions ;
const existingContexts = Array . from ( ( browserType as any ) . _contexts ) as BrowserContext [ ] ;
2023-01-05 14:50:47 -08:00
if ( _reuseContext )
existingContexts . forEach ( c = > reusedContexts . add ( c ) ) ;
else
await Promise . all ( existingContexts . map ( onDidCreateBrowserContext ) ) ;
2021-11-29 12:21:15 -05:00
}
2022-02-09 08:54:09 -08:00
{
( playwright . request as any ) . _onDidCreateContext = onDidCreateRequestContext ;
( playwright . request as any ) . _onWillCloseContext = onWillCloseRequestContext ;
const existingApiRequests : APIRequestContext [ ] = Array . from ( ( playwright . request as any ) . _contexts as Set < APIRequestContext > ) ;
await Promise . all ( existingApiRequests . map ( onDidCreateRequestContext ) ) ;
}
2022-12-07 16:45:33 +00:00
if ( screenshotMode === 'on' || screenshotMode === 'only-on-failure' )
2022-06-27 17:46:39 -07:00
testInfoImpl . _onTestFailureImmediateCallbacks . set ( screenshotOnTestFailure , 'Screenshot on failure' ) ;
2021-08-09 18:09:11 -07:00
// 2. Run the test.
await use ( ) ;
// 3. Determine whether we need the artifacts.
const testFailed = testInfo . status !== testInfo . expectedStatus ;
2021-12-15 16:06:10 -08:00
const preserveTrace = captureTrace && ( traceMode === 'on' || ( testFailed && traceMode === 'retain-on-failure' ) || ( traceMode === 'on-first-retry' && testInfo . retry === 1 ) ) ;
2022-12-07 16:45:33 +00:00
const captureScreenshots = screenshotMode === 'on' || ( screenshotMode === 'only-on-failure' && testFailed ) ;
2021-08-09 18:09:11 -07:00
const screenshotAttachments : string [ ] = [ ] ;
const addScreenshotAttachment = ( ) = > {
const screenshotPath = testInfo . outputPath ( ` test- ${ testFailed ? 'failed' : 'finished' } - ${ screenshotAttachments . length + 1 } .png ` ) ;
screenshotAttachments . push ( screenshotPath ) ;
testInfo . attachments . push ( { name : 'screenshot' , path : screenshotPath , contentType : 'image/png' } ) ;
return screenshotPath ;
} ;
// 4. Cleanup instrumentation.
2021-11-29 12:21:15 -05:00
const leftoverContexts : BrowserContext [ ] = [ ] ;
2021-11-30 19:59:35 +01:00
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
2021-11-29 12:21:15 -05:00
leftoverContexts . push ( . . . ( browserType as any ) . _contexts ) ;
( browserType as any ) . _onDidCreateContext = undefined ;
( browserType as any ) . _onWillCloseContext = undefined ;
( browserType as any ) . _defaultContextOptions = undefined ;
}
2021-10-26 10:13:35 -08:00
leftoverContexts . forEach ( context = > ( context as any ) . _instrumentation . removeAllListeners ( ) ) ;
2022-01-11 17:33:41 -08:00
for ( const context of ( playwright . request as any ) . _contexts )
context . _instrumentation . removeAllListeners ( ) ;
2022-02-09 08:54:09 -08:00
const leftoverApiRequests : APIRequestContext [ ] = Array . from ( ( playwright . request as any ) . _contexts as Set < APIRequestContext > ) ;
( playwright . request as any ) . _onDidCreateContext = undefined ;
( playwright . request as any ) . _onWillCloseContext = undefined ;
2022-06-27 17:46:39 -07:00
testInfoImpl . _onTestFailureImmediateCallbacks . delete ( screenshotOnTestFailure ) ;
2021-08-09 18:09:11 -07:00
2022-02-09 08:54:09 -08:00
// 5. Collect artifacts from any non-closed contexts.
await Promise . all ( leftoverContexts . map ( async context = > {
2023-02-25 21:21:09 -08:00
await stopTracing ( context . tracing ) ;
2022-06-27 17:46:39 -07:00
if ( captureScreenshots ) {
await Promise . all ( context . pages ( ) . map ( async page = > {
if ( ( page as any ) [ screenshottedSymbol ] )
return ;
2022-08-09 14:03:07 -07:00
// Pass caret=initial to avoid any evaluations that might slow down the screenshot
// and let the page modify itself from the problematic state it had at the moment of failure.
2022-12-07 16:45:33 +00:00
await page . screenshot ( { . . . screenshotOptions , timeout : 5000 , path : addScreenshotAttachment ( ) , caret : 'initial' } ) . catch ( ( ) = > { } ) ;
2022-06-27 17:46:39 -07:00
} ) ) ;
}
2022-02-09 08:54:09 -08:00
} ) . concat ( leftoverApiRequests . map ( async context = > {
const tracing = ( context as any ) . _tracing as Tracing ;
2023-02-25 21:21:09 -08:00
await stopTracing ( tracing ) ;
2022-02-09 08:54:09 -08:00
} ) ) ) ;
2021-08-09 18:09:11 -07:00
2023-02-28 13:26:23 -08:00
// 6. Save test trace.
if ( preserveTrace ) {
const events = ( testInfo as any ) . _traceEvents ;
if ( events . length ) {
const tracePath = path . join ( _artifactsDir ( ) , createGuid ( ) + '.zip' ) ;
temporaryTraceFiles . push ( tracePath ) ;
await saveTraceFile ( tracePath , events , traceOptions . sources ) ;
}
}
// 7. Either remove or attach temporary traces and screenshots for contexts closed
2021-08-09 18:09:11 -07:00
// before the test has finished.
2023-02-27 22:31:47 -08:00
if ( preserveTrace && temporaryTraceFiles . length ) {
const tracePath = testInfo . outputPath ( ` trace.zip ` ) ;
await mergeTraceFiles ( tracePath , temporaryTraceFiles ) ;
testInfo . attachments . push ( { name : 'trace' , path : tracePath , contentType : 'application/zip' } ) ;
}
2021-08-09 18:09:11 -07:00
await Promise . all ( temporaryScreenshots . map ( async file = > {
if ( captureScreenshots )
await fs . promises . rename ( file , addScreenshotAttachment ( ) ) . catch ( ( ) = > { } ) ;
else
await fs . promises . unlink ( file ) . catch ( ( ) = > { } ) ;
} ) ) ;
2022-06-30 17:05:08 -07:00
} , { auto : 'all-hooks-included' , _title : 'playwright configuration' } as any ] ,
2021-08-09 18:09:11 -07:00
2022-12-28 16:58:34 -08:00
_contextFactory : [ async ( { browser , video , _artifactsDir , _reuseContext } , use , testInfo ) = > {
2023-01-05 14:50:47 -08:00
const testInfoImpl = testInfo as TestInfoImpl ;
2022-05-31 15:59:36 -07:00
const videoMode = normalizeVideoMode ( video ) ;
2022-12-28 16:58:34 -08:00
const captureVideo = shouldCaptureVideo ( videoMode , testInfo ) && ! _reuseContext ;
2021-10-28 07:31:30 -08:00
const contexts = new Map < BrowserContext , { pages : Page [ ] } > ( ) ;
await use ( async options = > {
2023-01-05 14:50:47 -08:00
const hook = hookType ( testInfoImpl ) ;
2022-07-28 23:07:28 +02:00
if ( hook ) {
throw new Error ( [
` "context" and "page" fixtures are not supported in " ${ hook } " since they are created on a per-test basis. ` ,
` If you would like to reuse a single page between tests, create context manually with browser.newContext(). See https://aka.ms/playwright/reuse-page for details. ` ,
` If you would like to configure your page before each test, do that in beforeEach hook instead. ` ,
] . join ( '\n' ) ) ;
}
2021-10-28 07:31:30 -08:00
const videoOptions : BrowserContextOptions = captureVideo ? {
recordVideo : {
dir : _artifactsDir ( ) ,
size : typeof video === 'string' ? undefined : video . size ,
}
} : { } ;
const context = await browser . newContext ( { . . . videoOptions , . . . options } ) ;
const contextData : { pages : Page [ ] } = { pages : [ ] } ;
contexts . set ( context , contextData ) ;
context . on ( 'page' , page = > contextData . pages . push ( page ) ) ;
return context ;
} ) ;
2021-06-03 08:07:55 -07:00
2023-01-05 14:50:47 -08:00
const prependToError = testInfoImpl . _didTimeout ?
2021-10-28 07:31:30 -08:00
formatPendingCalls ( ( browser as any ) . _connection . pendingProtocolCalls ( ) ) : '' ;
2022-01-26 07:43:07 -08:00
let counter = 0 ;
2021-10-28 07:31:30 -08:00
await Promise . all ( [ . . . contexts . keys ( ) ] . map ( async context = > {
await context . close ( ) ;
const testFailed = testInfo . status !== testInfo . expectedStatus ;
const preserveVideo = captureVideo && ( videoMode === 'on' || ( testFailed && videoMode === 'retain-on-failure' ) || ( videoMode === 'on-first-retry' && testInfo . retry === 1 ) ) ;
if ( preserveVideo ) {
const { pages } = contexts . get ( context ) ! ;
const videos = pages . map ( p = > p . video ( ) ) . filter ( Boolean ) as Video [ ] ;
await Promise . all ( videos . map ( async v = > {
try {
2022-01-26 07:43:07 -08:00
const savedPath = testInfo . outputPath ( ` video ${ counter ? '-' + counter : '' } .webm ` ) ;
++ counter ;
2021-10-28 07:31:30 -08:00
await v . saveAs ( savedPath ) ;
testInfo . attachments . push ( { name : 'video' , path : savedPath , contentType : 'video/webm' } ) ;
} catch ( e ) {
// Silent catch empty videos.
}
} ) ) ;
}
} ) ) ;
2022-02-02 19:33:51 -07:00
if ( prependToError )
2022-02-23 12:32:12 -08:00
testInfo . errors . push ( { message : prependToError } ) ;
2022-04-13 15:13:31 -07:00
} , { scope : 'test' , _title : 'context' } as any ] ,
2021-06-03 08:07:55 -07:00
2022-12-28 16:58:34 -08:00
_contextReuseMode : process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : ( process . env . PW_TEST_REUSE_CONTEXT ? 'force' : 'none' ) ,
2022-07-12 13:30:24 -08:00
2023-02-09 08:49:51 -08:00
_reuseContext : [ async ( { video , _contextReuseMode } , use , testInfo ) = > {
2023-01-05 14:50:47 -08:00
const reuse = _contextReuseMode === 'force' || ( _contextReuseMode === 'when-possible' && ! shouldCaptureVideo ( normalizeVideoMode ( video ) , testInfo ) ) ;
2022-07-11 12:10:08 -08:00
await use ( reuse ) ;
2023-02-09 08:49:51 -08:00
} , { scope : 'test' , _title : 'context' } as any ] ,
2021-06-03 08:07:55 -07:00
2022-07-12 13:30:24 -08:00
context : async ( { playwright , browser , _reuseContext , _contextFactory } , use , testInfo ) = > {
if ( ! _reuseContext ) {
2022-07-11 12:10:08 -08:00
await use ( await _contextFactory ( ) ) ;
return ;
}
const defaultContextOptions = ( playwright . chromium as any ) . _defaultContextOptions as BrowserContextOptions ;
const context = await ( browser as any ) . _newContextForReuse ( defaultContextOptions ) ;
await use ( context ) ;
} ,
2022-07-12 13:30:24 -08:00
page : async ( { context , _reuseContext } , use ) = > {
if ( ! _reuseContext ) {
2022-07-11 12:10:08 -08:00
await use ( await context . newPage ( ) ) ;
return ;
}
// First time we are reusing the context, we should create the page.
let [ page ] = context . pages ( ) ;
if ( ! page )
page = await context . newPage ( ) ;
await use ( page ) ;
2021-06-03 08:07:55 -07:00
} ,
2021-10-06 09:09:27 -08:00
request : async ( { playwright , _combinedContextOptions } , use ) = > {
const request = await playwright . request . newContext ( _combinedContextOptions ) ;
await use ( request ) ;
await request . dispose ( ) ;
}
2021-06-03 08:07:55 -07:00
} ) ;
2021-10-06 09:09:27 -08:00
2021-06-17 15:09:38 -07:00
2021-08-31 16:34:52 -07:00
function formatPendingCalls ( calls : ParsedStackTrace [ ] ) {
2022-02-07 17:11:36 -08:00
calls = calls . filter ( call = > ! ! call . apiName ) ;
2021-06-17 15:09:38 -07:00
if ( ! calls . length )
return '' ;
return 'Pending operations:\n' + calls . map ( call = > {
2022-02-07 17:11:36 -08:00
const frame = call . frames && call . frames [ 0 ] ? ' at ' + formatStackFrame ( call . frames [ 0 ] ) : '' ;
return ` - ${ call . apiName } ${ frame } \ n ` ;
2022-02-23 12:32:12 -08:00
} ) . join ( '' ) ;
2021-06-17 15:09:38 -07:00
}
2021-07-19 12:20:24 -05:00
function formatStackFrame ( frame : StackFrame ) {
const file = path . relative ( process . cwd ( ) , frame . file ) || path . basename ( frame . file ) ;
2021-06-17 15:09:38 -07:00
return ` ${ file } : ${ frame . line || 1 } : ${ frame . column || 1 } ` ;
}
2023-01-05 14:50:47 -08:00
function hookType ( testInfo : TestInfoImpl ) : 'beforeAll' | 'afterAll' | undefined {
const type = testInfo . _timeoutManager . currentRunnableType ( ) ;
if ( type === 'beforeAll' || type === 'afterAll' )
return type ;
2021-09-01 13:41:35 -07:00
}
2021-06-17 15:09:38 -07:00
type StackFrame = {
file : string ,
line? : number ,
column? : number ,
function ? : string ,
} ;
2021-08-31 16:34:52 -07:00
type ParsedStackTrace = {
frames : StackFrame [ ] ;
frameTexts : string [ ] ;
apiName : string ;
2021-06-17 15:09:38 -07:00
} ;
2021-08-31 17:03:31 -07:00
2022-10-11 09:10:43 -08:00
export function normalizeVideoMode ( video : VideoMode | 'retry-with-video' | { mode : VideoMode } | undefined ) : VideoMode {
if ( ! video )
return 'off' ;
2022-05-31 15:59:36 -07:00
let videoMode = typeof video === 'string' ? video : video.mode ;
if ( videoMode === 'retry-with-video' )
videoMode = 'on-first-retry' ;
return videoMode ;
}
2022-12-28 16:58:34 -08:00
function shouldCaptureVideo ( videoMode : VideoMode , testInfo : TestInfo ) {
2022-05-31 15:59:36 -07:00
return ( videoMode === 'on' || videoMode === 'retain-on-failure' || ( videoMode === 'on-first-retry' && testInfo . retry === 1 ) ) ;
}
2022-10-11 09:10:43 -08:00
export function normalizeTraceMode ( trace : TraceMode | 'retry-with-trace' | { mode : TraceMode } | undefined ) : TraceMode {
if ( ! trace )
return 'off' ;
2022-05-31 15:59:36 -07:00
let traceMode = typeof trace === 'string' ? trace : trace.mode ;
if ( traceMode === 'retry-with-trace' )
traceMode = 'on-first-retry' ;
return traceMode ;
}
2022-12-28 16:58:34 -08:00
function shouldCaptureTrace ( traceMode : TraceMode , testInfo : TestInfo ) {
2022-05-31 15:59:36 -07:00
return traceMode === 'on' || traceMode === 'retain-on-failure' || ( traceMode === 'on-first-retry' && testInfo . retry === 1 ) ;
}
2022-12-21 10:16:36 -08:00
function normalizeScreenshotMode ( screenshot : PlaywrightWorkerOptions [ 'screenshot' ] | undefined ) : ScreenshotMode {
if ( ! screenshot )
return 'off' ;
return typeof screenshot === 'string' ? screenshot : screenshot.mode ;
}
2021-08-31 17:03:31 -07:00
const kTracingStarted = Symbol ( 'kTracingStarted' ) ;
2021-10-26 12:45:53 -08:00
2023-03-13 16:50:51 -07:00
function connectOptionsFromEnv() {
const wsEndpoint = process . env . PW_TEST_CONNECT_WS_ENDPOINT ;
if ( ! wsEndpoint )
return undefined ;
const headers = process . env . PW_TEST_CONNECT_HEADERS ? JSON . parse ( process . env . PW_TEST_CONNECT_HEADERS ) : undefined ;
return {
wsEndpoint ,
headers ,
_exposeNetwork : process.env.PW_TEST_CONNECT_EXPOSE_NETWORK ,
} ;
}
2022-12-05 18:15:01 -08:00
export const test = _baseTest . extend < TestFixtures , WorkerFixtures > ( playwrightFixtures ) ;
2021-10-26 12:45:53 -08:00
export default test ;