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-09-07 15:57:20 -07:00
import type { LaunchOptions , BrowserContextOptions , Page , BrowserContext , Video , APIRequestContext , Tracing } from 'playwright-core' ;
2022-05-31 15:59:36 -07:00
import type { TestType , PlaywrightTestArgs , PlaywrightTestOptions , PlaywrightWorkerArgs , PlaywrightWorkerOptions , TestInfo , VideoMode , TraceMode } from '../types/test' ;
2021-06-06 20:18:47 -07:00
import { rootTestType } from './testType' ;
2022-04-07 19:18:22 -08:00
import { createGuid , debugMode } from 'playwright-core/lib/utils' ;
import { removeFolders } from 'playwright-core/lib/utils/fileUtils' ;
2021-06-06 20:18:47 -07:00
export { expect } from './expect' ;
export const _baseTest : TestType < { } , { } > = rootTestType . test ;
2022-05-03 13:25:56 -08:00
export { addRunnerPlugin as _addRunnerPlugin } from './plugins' ;
2022-01-15 19:25:32 -08:00
import * as outOfProcess from 'playwright-core/lib/outofprocess' ;
2022-09-06 14:15:53 -07:00
import * as playwrightLibrary from 'playwright-core' ;
2022-06-27 17:46:39 -07:00
import type { TestInfoImpl } from './testInfo' ;
2021-06-15 10:06:49 -07: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-07-11 12:10:08 -08:00
_contextReuseEnabled : boolean ,
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
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
export const test = _baseTest . extend < 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-04-14 15:30:04 -07:00
playwright : [ async ( { } , use ) = > {
if ( process . env . PW_OUT_OF_PROCESS_DRIVER ) {
2022-01-27 14:32:23 -08:00
const impl = await outOfProcess . start ( {
NODE_OPTIONS : undefined // Hide driver process while debugging.
} ) ;
2022-09-06 14:15:53 -07:00
const pw = impl . playwright as any ;
pw . _setSelectors ( playwrightLibrary . selectors ) ;
await use ( pw ) ;
2022-01-15 19:25:32 -08:00
await impl . stop ( ) ;
feat: introduce experimental general-purpose grid (#8941)
This patch adds a general-purpose grid framework to parallelize
Playwright across multiple agents.
This patch adds two CLI commands to manage grid:
- `npx playwright experimental-grid-server` - to launch grid
- `npx playwrigth experimental-grid-agent` - to launch agent in a host
environment.
Grid server accepts an `--agent-factory` argument. A simple
`factory.js` might look like this:
```js
const child_process = require('child_process');
module.exports = {
name: 'My Simple Factory',
capacity: Infinity, // How many workers launch per agent
timeout: 10_000, // 10 seconds timeout to create agent
launch: ({agentId, gridURL, playwrightVersion}) => child_process.spawn(`npx`, [
'playwright'
'experimental-grid-agent',
'--grid-url', gridURL,
'--agent-id', agentId,
], {
cwd: __dirname,
shell: true,
stdio: 'inherit',
}),
};
```
With this `factory.js`, grid server could be launched like this:
```bash
npx playwright experimental-grid-server --factory=./factory.js
```
Once launched, it could be used with Playwright Test using env variable:
```bash
PW_GRID=http://localhost:3000 npx playwright test
```
2021-09-16 01:20:36 -07:00
} else {
2021-10-19 12:28:02 -04:00
await use ( require ( 'playwright-core' ) ) ;
feat: introduce experimental general-purpose grid (#8941)
This patch adds a general-purpose grid framework to parallelize
Playwright across multiple agents.
This patch adds two CLI commands to manage grid:
- `npx playwright experimental-grid-server` - to launch grid
- `npx playwrigth experimental-grid-agent` - to launch agent in a host
environment.
Grid server accepts an `--agent-factory` argument. A simple
`factory.js` might look like this:
```js
const child_process = require('child_process');
module.exports = {
name: 'My Simple Factory',
capacity: Infinity, // How many workers launch per agent
timeout: 10_000, // 10 seconds timeout to create agent
launch: ({agentId, gridURL, playwrightVersion}) => child_process.spawn(`npx`, [
'playwright'
'experimental-grid-agent',
'--grid-url', gridURL,
'--agent-id', agentId,
], {
cwd: __dirname,
shell: true,
stdio: 'inherit',
}),
};
```
With this `factory.js`, grid server could be launched like this:
```bash
npx playwright experimental-grid-server --factory=./factory.js
```
Once launched, it could be used with Playwright Test using env variable:
```bash
PW_GRID=http://localhost:3000 npx playwright test
```
2021-09-16 01:20:36 -07:00
}
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 } ] ,
connectOptions : [ process . env . PW_TEST_CONNECT_WS_ENDPOINT ? { wsEndpoint : process.env.PW_TEST_CONNECT_WS_ENDPOINT } : undefined , { scope : 'worker' , option : true } ] ,
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 ) {
dir = path . join ( workerInfo . project . outputDir , '.playwright-artifacts-' + workerInfo . workerIndex ) ;
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
2022-09-07 15:57:20 -07:00
_browserOptions : [ async ( { playwright , headless , channel , launchOptions , connectOptions } , 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 ;
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 ] ) {
2022-02-09 16:30:14 -07:00
( browserType as any ) . _defaultLaunchOptions = options ;
2022-09-07 15:57:20 -07:00
( browserType as any ) . _defaultConnectOptions = connectOptions ;
}
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 ] ) {
2022-02-09 16:30:14 -07:00
( browserType as any ) . _defaultLaunchOptions = undefined ;
2022-09-07 15:57:20 -07:00
( 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
2022-09-07 15:57:20 -07:00
browser : [ async ( { playwright , browserName } , use ) = > {
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" ` ) ;
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 } ] ,
bypassCSP : [ ( { contextOptions } , use ) = > use ( contextOptions . bypassCSP ) , { option : true } ] ,
colorScheme : [ ( { contextOptions } , use ) = > use ( contextOptions . colorScheme ) , { option : true } ] ,
deviceScaleFactor : [ ( { contextOptions } , use ) = > use ( contextOptions . deviceScaleFactor ) , { option : true } ] ,
extraHTTPHeaders : [ ( { contextOptions } , use ) = > use ( contextOptions . extraHTTPHeaders ) , { option : true } ] ,
geolocation : [ ( { contextOptions } , use ) = > use ( contextOptions . geolocation ) , { option : true } ] ,
hasTouch : [ ( { contextOptions } , use ) = > use ( contextOptions . hasTouch ) , { option : true } ] ,
httpCredentials : [ ( { contextOptions } , use ) = > use ( contextOptions . httpCredentials ) , { option : true } ] ,
ignoreHTTPSErrors : [ ( { contextOptions } , use ) = > use ( contextOptions . ignoreHTTPSErrors ) , { option : true } ] ,
isMobile : [ ( { contextOptions } , use ) = > use ( contextOptions . isMobile ) , { option : true } ] ,
javaScriptEnabled : [ ( { contextOptions } , use ) = > use ( contextOptions . javaScriptEnabled ? ? true ) , { option : true } ] ,
locale : [ ( { contextOptions } , use ) = > use ( contextOptions . locale ? ? 'en-US' ) , { option : true } ] ,
offline : [ ( { contextOptions } , use ) = > use ( contextOptions . offline ) , { option : true } ] ,
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 } ] ,
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 ;
if ( storageState !== undefined )
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-04-15 12:11:38 -07:00
_snapshotSuffix : [ process . platform , { scope : 'worker' } ] ,
2021-10-28 07:31:30 -08:00
2022-01-28 10:51:55 -08:00
_setupContextOptionsAndArtifacts : [ async ( { playwright , _snapshotSuffix , _combinedContextOptions , _browserOptions , _artifactsDir , trace , screenshot , actionTimeout , navigationTimeout } , use , testInfo ) = > {
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-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 } ;
2022-05-31 15:59:36 -07: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 ;
2021-08-09 18:09:11 -07:00
2022-01-11 17:33:41 -08:00
const createInstrumentationListener = ( context? : BrowserContext ) = > {
return {
2021-10-26 10:13:35 -08:00
onApiCallBegin : ( apiCall : string , stackTrace : ParsedStackTrace | null , 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 ,
2021-10-18 20:06:18 -08:00
forceNoParent : false
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
} ;
} ;
2022-02-09 08:54:09 -08:00
const startTracing = 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 ] ) {
await tracing . start ( { . . . traceOptions , title } ) ;
( 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 ) ;
context . setDefaultNavigationTimeout ( navigationTimeout || actionTimeout || 0 ) ;
2022-02-09 08:54:09 -08:00
await startTracing ( 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 ;
await startTracing ( 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' ) ;
2022-02-09 08:54:09 -08:00
const stopTracing = async ( tracing : Tracing ) = > {
( 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.
await page . screenshot ( { 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 ) = > {
await stopTracing ( context . tracing ) ;
2021-08-09 18:09:11 -07:00
if ( screenshot === 'on' || screenshot === 'only-on-failure' ) {
// 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 ;
await stopTracing ( tracing ) ;
} ;
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 [ ] ;
2022-01-11 17:33:41 -08:00
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-06-27 17:46:39 -07:00
if ( screenshot === 'on' || screenshot === 'only-on-failure' )
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 ) ) ;
const captureScreenshots = ( screenshot === 'on' || ( screenshot === 'only-on-failure' && testFailed ) ) ;
2021-08-09 18:09:11 -07:00
const traceAttachments : string [ ] = [ ] ;
const addTraceAttachment = ( ) = > {
2021-11-01 19:27:41 -08:00
const tracePath = testInfo . outputPath ( ` trace ${ traceAttachments . length ? '-' + traceAttachments . length : '' } .zip ` ) ;
2021-08-09 18:09:11 -07:00
traceAttachments . push ( tracePath ) ;
testInfo . attachments . push ( { name : 'trace' , path : tracePath , contentType : 'application/zip' } ) ;
return tracePath ;
} ;
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
const stopTraceChunk = async ( tracing : Tracing ) : Promise < boolean > = > {
2021-11-04 21:08:42 -07:00
// When we timeout during context.close(), we might end up with context still alive
// but artifacts being already collected. In this case, do not collect artifacts
// for the second time.
2022-02-09 08:54:09 -08:00
if ( ( tracing as any ) [ startedCollectingArtifacts ] )
return false ;
2021-08-09 18:09:11 -07:00
if ( preserveTrace )
2022-02-09 08:54:09 -08:00
await tracing . stopChunk ( { path : addTraceAttachment ( ) } ) ;
2021-10-18 21:05:59 -07:00
else if ( captureTrace )
2022-02-09 08:54:09 -08:00
await tracing . stopChunk ( ) ;
return true ;
} ;
// 5. Collect artifacts from any non-closed contexts.
await Promise . all ( leftoverContexts . map ( async context = > {
if ( ! await stopTraceChunk ( context . tracing ) )
return ;
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.
await page . screenshot ( { 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 ;
await stopTraceChunk ( tracing ) ;
} ) ) ) ;
2021-08-09 18:09:11 -07:00
// 6. Either remove or attach temporary traces and screenshots for contexts closed
// before the test has finished.
await Promise . all ( temporaryTraceFiles . map ( async file = > {
if ( preserveTrace )
await fs . promises . rename ( file , addTraceAttachment ( ) ) . catch ( ( ) = > { } ) ;
else
await fs . promises . unlink ( file ) . catch ( ( ) = > { } ) ;
} ) ) ;
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-04-13 15:13:31 -07:00
_contextFactory : [ async ( { browser , video , _artifactsDir } , use , testInfo ) = > {
2022-05-31 15:59:36 -07:00
const videoMode = normalizeVideoMode ( video ) ;
const captureVideo = shouldCaptureVideo ( videoMode , testInfo ) ;
2021-10-28 07:31:30 -08:00
const contexts = new Map < BrowserContext , { pages : Page [ ] } > ( ) ;
await use ( async options = > {
const hook = hookType ( testInfo ) ;
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
2021-08-10 09:26:36 -07:00
const prependToError = testInfo . status === 'timedOut' ?
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-07-31 14:31:17 -07:00
_contextReuseEnabled : ! ! process . env . PW_TEST_REUSE_CONTEXT ,
2022-07-12 13:30:24 -08:00
_reuseContext : async ( { video , trace , _contextReuseEnabled } , use , testInfo ) = > {
const reuse = _contextReuseEnabled && ! shouldCaptureVideo ( normalizeVideoMode ( video ) , testInfo ) && ! shouldCaptureTrace ( normalizeTraceMode ( trace ) , testInfo ) ;
2022-07-11 12:10:08 -08:00
await use ( reuse ) ;
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 } ` ;
}
2021-09-01 13:41:35 -07:00
function hookType ( testInfo : TestInfo ) : 'beforeAll' | 'afterAll' | undefined {
2022-03-17 09:36:03 -07:00
if ( ( testInfo as any ) . _timeoutManager . _runnable ? . type === 'beforeAll' )
2021-09-01 13:41:35 -07:00
return 'beforeAll' ;
2022-03-17 09:36:03 -07:00
if ( ( testInfo as any ) . _timeoutManager . _runnable ? . type === 'afterAll' )
2021-09-01 13:41:35 -07:00
return 'afterAll' ;
}
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-05-31 15:59:36 -07:00
export function normalizeVideoMode ( video : VideoMode | 'retry-with-video' | { mode : VideoMode } ) {
let videoMode = typeof video === 'string' ? video : video.mode ;
if ( videoMode === 'retry-with-video' )
videoMode = 'on-first-retry' ;
return videoMode ;
}
export function shouldCaptureVideo ( videoMode : VideoMode , testInfo : TestInfo ) {
return ( videoMode === 'on' || videoMode === 'retain-on-failure' || ( videoMode === 'on-first-retry' && testInfo . retry === 1 ) ) ;
}
export function normalizeTraceMode ( trace : TraceMode | 'retry-with-trace' | { mode : TraceMode } ) {
let traceMode = typeof trace === 'string' ? trace : trace.mode ;
if ( traceMode === 'retry-with-trace' )
traceMode = 'on-first-retry' ;
return traceMode ;
}
export function shouldCaptureTrace ( traceMode : TraceMode , testInfo : TestInfo ) {
return traceMode === 'on' || traceMode === 'retain-on-failure' || ( traceMode === 'on-first-retry' && testInfo . retry === 1 ) ;
}
2021-08-31 17:03:31 -07:00
const kTracingStarted = Symbol ( 'kTracingStarted' ) ;
2021-10-26 12:45:53 -08:00
export default test ;