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' ;
2023-03-31 08:57:07 -07:00
import type { APIRequestContext , BrowserContext , Browser , BrowserContextOptions , LaunchOptions , Page , Tracing , Video } from 'playwright-core' ;
2022-11-02 20:22:35 -07:00
import * as playwrightLibrary from 'playwright-core' ;
2023-03-16 18:17:07 -07:00
import { createGuid , debugMode , addInternalStackPrefix , mergeTraceFiles , saveTraceFile , removeFolders } 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' ;
2023-04-07 09:54:01 -07:00
import { type ContextReuseMode } from './common/config' ;
2023-03-14 15:58:55 -07:00
import { artifactsFolderName } from './isomorphic/folders' ;
2023-04-28 08:57:43 -07:00
import type { ClientInstrumentation , ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation' ;
import type { ParsedStackTrace } from '../../playwright-core/src/utils/stackTrace' ;
2023-05-01 13:53:15 -07:00
import { currentTestInfo , setCurrentTestInstrumentation } from './common/globals' ;
2023-01-26 17:26:47 -08:00
export { expect } from './matchers/expect' ;
2023-03-17 11:50:44 -07:00
export { store as _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 ,
2023-04-28 16:54:52 -07:00
_setupContextOptions : 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 ;
2023-05-01 13:53:15 -07:00
_setupArtifacts : void ;
2021-10-28 07:31:30 -08:00
_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-16 07:03:33 -07:00
( browserType as any ) . _defaultLaunchOptions = options ;
( browserType as any ) . _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-16 07:03:33 -07:00
( browserType as any ) . _defaultLaunchOptions = undefined ;
( browserType as any ) . _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
2023-04-28 16:54:52 -07:00
_setupContextOptions : [ async ( { playwright , _snapshotSuffix , _combinedContextOptions , _artifactsDir , 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 ) ;
2023-04-28 16:54:52 -07:00
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
( browserType as any ) . _defaultContextOptions = _combinedContextOptions ;
( browserType as any ) . _defaultContextTimeout = actionTimeout || 0 ;
( browserType as any ) . _defaultContextNavigationTimeout = navigationTimeout || 0 ;
}
( playwright . request as any ) . _defaultContextOptions = { . . . _combinedContextOptions } ;
( playwright . request as any ) . _defaultContextOptions . tracesDir = path . join ( _artifactsDir ( ) , 'traces' ) ;
await use ( ) ;
( playwright . request as any ) . _defaultContextOptions = undefined ;
for ( const browserType of [ playwright . chromium , playwright . firefox , playwright . webkit ] ) {
( browserType as any ) . _defaultContextOptions = undefined ;
( browserType as any ) . _defaultContextTimeout = undefined ;
( browserType as any ) . _defaultContextNavigationTimeout = undefined ;
}
} , { auto : 'all-hooks-included' , _title : 'context configuration' } as any ] ,
2021-08-09 18:09:11 -07:00
2023-05-01 13:53:15 -07:00
_setupArtifacts : [ async ( { playwright , _artifactsDir , trace , screenshot } , use ) = > {
let artifactsRecorder : ArtifactsRecorder | undefined ;
2021-08-09 18:09:11 -07:00
2023-04-28 08:57:43 -07:00
const csiListener : ClientInstrumentationListener = {
onApiCallBegin : ( apiCall : string , stackTrace : ParsedStackTrace | null , wallTime : number , userData : any ) = > {
2023-05-01 13:53:15 -07:00
const testInfo = currentTestInfo ( ) ;
if ( ! testInfo || apiCall . startsWith ( 'expect.' ) || apiCall . includes ( 'setTestIdAttribute' ) )
2023-04-28 08:57:43 -07:00
return { userObject : null } ;
2023-05-01 13:53:15 -07:00
const step = testInfo . _addStep ( {
2023-04-28 08:57:43 -07:00
location : stackTrace?.frames [ 0 ] as any ,
category : 'pw:api' ,
title : apiCall ,
wallTime ,
2023-05-03 16:04:20 -07:00
laxParent : true ,
2023-04-28 08:57:43 -07:00
} ) ;
userData . userObject = step ;
} ,
onApiCallEnd : ( userData : any , error? : Error ) = > {
const step = userData . userObject ;
step ? . complete ( { error } ) ;
} ,
onWillPause : ( ) = > {
2023-05-01 13:53:15 -07:00
currentTestInfo ( ) ? . setTimeout ( 0 ) ;
2023-04-28 08:57:43 -07:00
} ,
onDidCreateBrowserContext : async ( context : BrowserContext ) = > {
2023-05-01 13:53:15 -07:00
await artifactsRecorder ? . didCreateBrowserContext ( context ) ;
const testInfo = currentTestInfo ( ) ;
if ( testInfo )
attachConnectedHeaderIfNeeded ( testInfo , context . browser ( ) ) ;
2023-04-28 08:57:43 -07:00
} ,
onDidCreateRequestContext : async ( context : APIRequestContext ) = > {
2023-05-01 13:53:15 -07:00
await artifactsRecorder ? . didCreateRequestContext ( context ) ;
2023-04-28 08:57:43 -07:00
} ,
onWillCloseBrowserContext : async ( context : BrowserContext ) = > {
2023-05-01 13:53:15 -07:00
await artifactsRecorder ? . willCloseBrowserContext ( context ) ;
2023-04-28 08:57:43 -07:00
} ,
onWillCloseRequestContext : async ( context : APIRequestContext ) = > {
2023-05-01 13:53:15 -07:00
await artifactsRecorder ? . willCloseRequestContext ( context ) ;
2023-04-28 08:57:43 -07:00
} ,
2022-01-11 17:33:41 -08:00
} ;
2023-05-01 13:53:15 -07:00
const willStartTest = async ( testInfo : TestInfoImpl ) = > {
artifactsRecorder = new ArtifactsRecorder ( playwright , _artifactsDir ( ) , trace , screenshot ) ;
await artifactsRecorder . willStartTest ( testInfo ) ;
} ;
const didFinishTestFunction = async ( testInfo : TestInfoImpl ) = > {
await artifactsRecorder ? . didFinishTestFunction ( ) ;
} ;
const didFinishTest = async ( testInfo : TestInfoImpl ) = > {
await artifactsRecorder ? . didFinishTest ( ) ;
artifactsRecorder = undefined ;
} ;
// 1. Setup instrumentation.
const clientInstrumentation = ( playwright as any ) . _instrumentation as ClientInstrumentation ;
clientInstrumentation . addListener ( csiListener ) ;
setCurrentTestInstrumentation ( { willStartTest , didFinishTestFunction , didFinishTest } ) ;
// 2. Setup for the first test in the worker.
{
const firstTestInfo = currentTestInfo ( ) ;
if ( firstTestInfo )
await willStartTest ( firstTestInfo ) ;
}
2021-08-09 18:09:11 -07:00
// 2. Run the test.
await use ( ) ;
2023-05-01 13:53:15 -07:00
// 3. Teardown for the last test in the worker.
{
const lastTestInfo = currentTestInfo ( ) ;
if ( lastTestInfo )
await didFinishTest ( lastTestInfo ) ;
}
// 4. Cleanup instrumentation.
setCurrentTestInstrumentation ( undefined ) ;
clientInstrumentation . removeListener ( csiListener ) ;
} , { scope : 'worker' , auto : 'all-hooks-included' , _title : 'trace recording' } 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 = > {
2023-04-18 09:02:33 -07:00
( context as any ) [ kStartedContextTearDown ] = true ;
2021-10-28 07:31:30 -08:00
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 ) = > {
2023-03-31 08:57:07 -07:00
attachConnectedHeaderIfNeeded ( testInfo , browser ) ;
2022-07-12 13:30:24 -08:00
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 ) ;
2023-03-16 14:32:12 -07:00
( context as any ) [ kIsReusedContext ] = true ;
2022-07-11 12:10:08 -08:00
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
2023-03-16 07:03:33 -07:00
request : async ( { playwright } , use ) = > {
const request = await playwright . request . newContext ( ) ;
2021-10-06 09:09:27 -08:00
await use ( request ) ;
2023-04-18 09:02:33 -07:00
( request as any ) [ kStartedContextTearDown ] = true ;
2021-10-06 09:09:27 -08:00
await request . dispose ( ) ;
2023-03-16 07:03:33 -07:00
} ,
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 ,
} ;
2023-04-29 14:34:45 -07:00
type ScreenshotOption = PlaywrightWorkerOptions [ 'screenshot' ] | undefined ;
type TraceOption = PlaywrightWorkerOptions [ 'trace' ] | undefined ;
type Playwright = PlaywrightWorkerArgs [ 'playwright' ] ;
2023-03-29 13:35:31 -07:00
function normalizeVideoMode ( video : VideoMode | 'retry-with-video' | { mode : VideoMode } | undefined ) : VideoMode {
2022-10-11 09:10:43 -08:00
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 ) ) ;
}
2023-04-29 14:34:45 -07:00
function normalizeTraceMode ( trace : TraceOption ) : TraceMode {
2022-10-11 09:10:43 -08:00
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 ) {
2023-03-31 22:04:24 +02:00
return traceMode === 'on' || traceMode === 'retain-on-failure' || ( traceMode === 'on-first-retry' && testInfo . retry === 1 ) || ( traceMode === 'on-all-retries' && testInfo . retry > 0 ) ;
2022-05-31 15:59:36 -07:00
}
2023-04-29 14:34:45 -07:00
function normalizeScreenshotMode ( screenshot : ScreenshotOption ) : ScreenshotMode {
2022-12-21 10:16:36 -08:00
if ( ! screenshot )
return 'off' ;
return typeof screenshot === 'string' ? screenshot : screenshot.mode ;
}
2023-03-31 08:57:07 -07:00
function attachConnectedHeaderIfNeeded ( testInfo : TestInfo , browser : Browser | null ) {
const connectHeaders : { name : string , value : string } [ ] | undefined = ( browser as any ) ? . _connectHeaders ;
if ( ! connectHeaders )
return ;
for ( const header of connectHeaders ) {
if ( header . name !== 'x-playwright-attachment' )
continue ;
const [ name , value ] = header . value . split ( '=' ) ;
if ( ! name || ! value )
continue ;
if ( testInfo . attachments . some ( attachment = > attachment . name === name ) )
continue ;
testInfo . attachments . push ( { name , contentType : 'text/plain' , body : Buffer.from ( value ) } ) ;
}
}
2021-08-31 17:03:31 -07:00
const kTracingStarted = Symbol ( 'kTracingStarted' ) ;
2023-03-16 14:32:12 -07:00
const kIsReusedContext = Symbol ( 'kReusedContext' ) ;
2023-04-18 09:02:33 -07:00
const kStartedContextTearDown = Symbol ( 'kStartedContextTearDown' ) ;
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 ,
} ;
}
2023-04-29 14:34:45 -07:00
class ArtifactsRecorder {
private _testInfo ! : TestInfoImpl ;
private _playwright : Playwright ;
private _artifactsDir : string ;
private _screenshotMode : ScreenshotMode ;
private _traceMode : TraceMode ;
private _captureTrace = false ;
private _screenshotOptions : { mode : ScreenshotMode } & Pick < playwrightLibrary.PageScreenshotOptions , 'fullPage' | 'omitBackground' > | undefined ;
private _traceOptions : { screenshots : boolean , snapshots : boolean , sources : boolean , mode? : TraceMode } ;
private _temporaryTraceFiles : string [ ] = [ ] ;
private _temporaryScreenshots : string [ ] = [ ] ;
private _reusedContexts = new Set < BrowserContext > ( ) ;
private _traceOrdinal = 0 ;
private _screenshottedSymbol : symbol ;
private _startedCollectingArtifacts : symbol ;
constructor ( playwright : Playwright , artifactsDir : string , trace : TraceOption , screenshot : ScreenshotOption ) {
this . _playwright = playwright ;
this . _artifactsDir = artifactsDir ;
this . _screenshotMode = normalizeScreenshotMode ( screenshot ) ;
this . _screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot ;
this . _traceMode = normalizeTraceMode ( trace ) ;
const defaultTraceOptions = { screenshots : true , snapshots : true , sources : true } ;
this . _traceOptions = typeof trace === 'string' ? defaultTraceOptions : { . . . defaultTraceOptions , . . . trace , mode : undefined } ;
this . _screenshottedSymbol = Symbol ( 'screenshotted' ) ;
this . _startedCollectingArtifacts = Symbol ( 'startedCollectingArtifacts' ) ;
}
2023-05-01 13:53:15 -07:00
async willStartTest ( testInfo : TestInfoImpl ) {
2023-04-29 14:34:45 -07:00
this . _testInfo = testInfo ;
this . _captureTrace = shouldCaptureTrace ( this . _traceMode , testInfo ) && ! process . env . PW_TEST_DISABLE_TRACING ;
// Process existing contexts.
for ( const browserType of [ this . _playwright . chromium , this . _playwright . firefox , this . _playwright . webkit ] ) {
const promises : ( Promise < void > | undefined ) [ ] = [ ] ;
const existingContexts = Array . from ( ( browserType as any ) . _contexts ) as BrowserContext [ ] ;
for ( const context of existingContexts ) {
if ( ( context as any ) [ kIsReusedContext ] )
this . _reusedContexts . add ( context ) ;
else
promises . push ( this . didCreateBrowserContext ( context ) ) ;
}
await Promise . all ( promises ) ;
}
{
const existingApiRequests : APIRequestContext [ ] = Array . from ( ( this . _playwright . request as any ) . _contexts as Set < APIRequestContext > ) ;
await Promise . all ( existingApiRequests . map ( c = > this . didCreateRequestContext ( c ) ) ) ;
}
}
async didCreateBrowserContext ( context : BrowserContext ) {
await this . _startTraceChunkOnContextCreation ( context . tracing ) ;
}
async willCloseBrowserContext ( context : BrowserContext ) {
// 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 ( this . _reusedContexts . has ( context ) )
return ;
await this . _stopTracing ( context . tracing , ( context as any ) [ kStartedContextTearDown ] ) ;
if ( this . _screenshotMode === 'on' || this . _screenshotMode === 'only-on-failure' ) {
// Capture screenshot for now. We'll know whether we have to preserve them
// after the test finishes.
await Promise . all ( context . pages ( ) . map ( page = > this . _screenshotPage ( page ) ) ) ;
}
}
async didCreateRequestContext ( context : APIRequestContext ) {
const tracing = ( context as any ) . _tracing as Tracing ;
await this . _startTraceChunkOnContextCreation ( tracing ) ;
}
async willCloseRequestContext ( context : APIRequestContext ) {
const tracing = ( context as any ) . _tracing as Tracing ;
await this . _stopTracing ( tracing , ( context as any ) [ kStartedContextTearDown ] ) ;
}
2023-05-01 13:53:15 -07:00
async didFinishTestFunction() {
if ( this . _testInfo . _isFailure ( ) && ( this . _screenshotMode === 'on' || this . _screenshotMode === 'only-on-failure' ) )
await this . _screenshotOnTestFailure ( ) ;
}
async didFinishTest() {
2023-04-29 14:34:45 -07:00
const captureScreenshots = this . _screenshotMode === 'on' || ( this . _screenshotMode === 'only-on-failure' && this . _testInfo . status !== this . _testInfo . expectedStatus ) ;
const leftoverContexts : BrowserContext [ ] = [ ] ;
for ( const browserType of [ this . _playwright . chromium , this . _playwright . firefox , this . _playwright . webkit ] )
leftoverContexts . push ( . . . ( browserType as any ) . _contexts ) ;
const leftoverApiRequests : APIRequestContext [ ] = Array . from ( ( this . _playwright . request as any ) . _contexts as Set < APIRequestContext > ) ;
// Collect traces/screenshots for remaining contexts.
await Promise . all ( leftoverContexts . map ( async context = > {
await this . _stopTracing ( context . tracing , true ) ;
if ( captureScreenshots ) {
await Promise . all ( context . pages ( ) . map ( async page = > {
if ( ( page as any ) [ this . _screenshottedSymbol ] )
return ;
// 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.
await page . screenshot ( { . . . this . _screenshotOptions , timeout : 5000 , path : this._addScreenshotAttachment ( ) , caret : 'initial' } ) . catch ( ( ) = > { } ) ;
} ) ) ;
}
} ) . concat ( leftoverApiRequests . map ( async context = > {
const tracing = ( context as any ) . _tracing as Tracing ;
await this . _stopTracing ( tracing , true ) ;
} ) ) ) ;
// Collect test trace.
if ( this . _preserveTrace ( ) ) {
const events = this . _testInfo . _traceEvents ;
if ( events . length ) {
const tracePath = path . join ( this . _artifactsDir , createGuid ( ) + '.zip' ) ;
this . _temporaryTraceFiles . push ( tracePath ) ;
await saveTraceFile ( tracePath , events , this . _traceOptions . sources ) ;
}
}
// Either remove or attach temporary traces and screenshots for contexts closed
// before the test has finished.
if ( this . _preserveTrace ( ) && this . _temporaryTraceFiles . length ) {
const tracePath = this . _testInfo . outputPath ( ` trace.zip ` ) ;
await mergeTraceFiles ( tracePath , this . _temporaryTraceFiles ) ;
this . _testInfo . attachments . push ( { name : 'trace' , path : tracePath , contentType : 'application/zip' } ) ;
}
await Promise . all ( this . _temporaryScreenshots . map ( async file = > {
if ( captureScreenshots )
await fs . promises . rename ( file , this . _addScreenshotAttachment ( ) ) . catch ( ( ) = > { } ) ;
else
await fs . promises . unlink ( file ) . catch ( ( ) = > { } ) ;
} ) ) ;
}
private _addScreenshotAttachment() {
const testFailed = this . _testInfo . status !== this . _testInfo . expectedStatus ;
const index = this . _testInfo . attachments . filter ( a = > a . name === 'screenshot' ) . length + 1 ;
const screenshotPath = this . _testInfo . outputPath ( ` test- ${ testFailed ? 'failed' : 'finished' } - ${ index } .png ` ) ;
this . _testInfo . attachments . push ( { name : 'screenshot' , path : screenshotPath , contentType : 'image/png' } ) ;
return screenshotPath ;
}
private async _screenshotPage ( page : Page ) {
if ( ( page as any ) [ this . _screenshottedSymbol ] )
return ;
( page as any ) [ this . _screenshottedSymbol ] = true ;
const screenshotPath = path . join ( this . _artifactsDir , createGuid ( ) + '.png' ) ;
this . _temporaryScreenshots . push ( screenshotPath ) ;
// 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.
await page . screenshot ( { . . . this . _screenshotOptions , timeout : 5000 , path : screenshotPath , caret : 'initial' } ) . catch ( ( ) = > { } ) ;
}
private async _screenshotOnTestFailure() {
const contexts : BrowserContext [ ] = [ ] ;
for ( const browserType of [ this . _playwright . chromium , this . _playwright . firefox , this . _playwright . webkit ] )
contexts . push ( . . . ( browserType as any ) . _contexts ) ;
await Promise . all ( contexts . map ( ctx = > Promise . all ( ctx . pages ( ) . map ( page = > this . _screenshotPage ( page ) ) ) ) ) ;
}
private async _startTraceChunkOnContextCreation ( tracing : Tracing ) {
if ( this . _captureTrace ) {
const title = [ path . relative ( this . _testInfo . project . testDir , this . _testInfo . file ) + ':' + this . _testInfo . line , . . . this . _testInfo . titlePath . slice ( 1 ) ] . join ( ' › ' ) ;
const ordinalSuffix = this . _traceOrdinal ? ` - ${ this . _traceOrdinal } ` : '' ;
++ this . _traceOrdinal ;
const retrySuffix = this . _testInfo . retry ? ` - ${ this . _testInfo . retry } ` : '' ;
const name = ` ${ this . _testInfo . testId } ${ retrySuffix } ${ ordinalSuffix } ` ;
if ( ! ( tracing as any ) [ kTracingStarted ] ) {
await tracing . start ( { . . . this . _traceOptions , title , name } ) ;
( tracing as any ) [ kTracingStarted ] = true ;
} else {
await tracing . startChunk ( { title , name } ) ;
}
} else {
if ( ( tracing as any ) [ kTracingStarted ] ) {
( tracing as any ) [ kTracingStarted ] = false ;
await tracing . stop ( ) ;
}
}
}
private _preserveTrace() {
const testFailed = this . _testInfo . status !== this . _testInfo . expectedStatus ;
return this . _captureTrace && ( this . _traceMode === 'on' || ( testFailed && this . _traceMode === 'retain-on-failure' ) || ( this . _traceMode === 'on-first-retry' && this . _testInfo . retry === 1 ) || ( this . _traceMode === 'on-all-retries' && this . _testInfo . retry > 0 ) ) ;
}
private async _stopTracing ( tracing : Tracing , contextTearDownStarted : boolean ) {
if ( ( tracing as any ) [ this . _startedCollectingArtifacts ] )
return ;
( tracing as any ) [ this . _startedCollectingArtifacts ] = true ;
if ( this . _captureTrace ) {
let tracePath ;
// Create a trace file if we know that:
// - it is's going to be used due to the config setting and the test status or
// - we are inside a test or afterEach and the user manually closed the context.
if ( this . _preserveTrace ( ) || ! contextTearDownStarted ) {
tracePath = path . join ( this . _artifactsDir , createGuid ( ) + '.zip' ) ;
this . _temporaryTraceFiles . push ( tracePath ) ;
}
await tracing . stopChunk ( { path : tracePath } ) ;
}
}
}
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 ;