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';
|
2021-10-28 07:31:30 -08:00
|
|
|
|
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType, Video } from 'playwright-core';
|
2021-10-14 05:55:08 -04:00
|
|
|
|
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
|
2021-06-06 20:18:47 -07:00
|
|
|
|
import { rootTestType } from './testType';
|
2021-10-28 07:31:30 -08:00
|
|
|
|
import { createGuid, removeFolders } from 'playwright-core/lib/utils/utils';
|
2021-10-22 15:59:52 -04:00
|
|
|
|
import { GridClient } from 'playwright-core/lib/grid/gridClient';
|
2021-10-11 10:52:17 -04:00
|
|
|
|
import { Browser } from 'playwright-core';
|
2021-11-18 14:36:55 -08:00
|
|
|
|
import { prependToTestError } from './util';
|
2021-06-06 20:18:47 -07:00
|
|
|
|
export { expect } from './expect';
|
|
|
|
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
2021-06-15 10:06:49 -07:00
|
|
|
|
|
2021-08-09 18:09:11 -07:00
|
|
|
|
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
|
|
|
|
_combinedContextOptions: BrowserContextOptions,
|
|
|
|
|
_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-08-09 18:09:11 -07:00
|
|
|
|
_browserType: BrowserType;
|
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>({
|
|
|
|
|
defaultBrowserType: [ 'chromium', { scope: 'worker', option: true } ],
|
|
|
|
|
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true } ],
|
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
|
|
|
|
playwright: [async ({}, use, workerInfo) => {
|
|
|
|
|
if (process.env.PW_GRID) {
|
|
|
|
|
const gridClient = await GridClient.connect(process.env.PW_GRID);
|
|
|
|
|
await use(gridClient.playwright() as any);
|
|
|
|
|
await gridClient.close();
|
|
|
|
|
} 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
|
|
|
|
}
|
|
|
|
|
}, { scope: 'worker' } ],
|
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
|
|
|
|
headless: [ undefined, { scope: 'worker', option: true } ],
|
|
|
|
|
channel: [ undefined, { scope: 'worker', option: true } ],
|
|
|
|
|
launchOptions: [ {}, { 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]);
|
|
|
|
|
}, { scope: 'worker' }],
|
2021-06-03 08:07:55 -07:00
|
|
|
|
|
2021-10-26 12:45:53 -08:00
|
|
|
|
_browserOptions: [browserOptionsWorkerFixture, { scope: 'worker' }],
|
|
|
|
|
_browserType: [browserTypeWorkerFixture, { scope: 'worker' }],
|
|
|
|
|
browser: [browserWorkerFixture, { scope: 'worker' } ],
|
2021-06-03 08:07:55 -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
|
|
|
|
acceptDownloads: [ undefined, { option: true } ],
|
|
|
|
|
bypassCSP: [ undefined, { option: true } ],
|
|
|
|
|
colorScheme: [ undefined, { option: true } ],
|
|
|
|
|
deviceScaleFactor: [ undefined, { option: true } ],
|
|
|
|
|
extraHTTPHeaders: [ undefined, { option: true } ],
|
|
|
|
|
geolocation: [ undefined, { option: true } ],
|
|
|
|
|
hasTouch: [ undefined, { option: true } ],
|
|
|
|
|
httpCredentials: [ undefined, { option: true } ],
|
|
|
|
|
ignoreHTTPSErrors: [ undefined, { option: true } ],
|
|
|
|
|
isMobile: [ undefined, { option: true } ],
|
|
|
|
|
javaScriptEnabled: [ undefined, { option: true } ],
|
|
|
|
|
locale: [ undefined, { option: true } ],
|
|
|
|
|
offline: [ undefined, { option: true } ],
|
|
|
|
|
permissions: [ undefined, { option: true } ],
|
|
|
|
|
proxy: [ undefined, { option: true } ],
|
|
|
|
|
storageState: [ undefined, { option: true } ],
|
|
|
|
|
timezoneId: [ undefined, { option: true } ],
|
|
|
|
|
userAgent: [ undefined, { option: true } ],
|
|
|
|
|
viewport: [ undefined, { option: true } ],
|
|
|
|
|
actionTimeout: [ undefined, { option: true } ],
|
|
|
|
|
navigationTimeout: [ undefined, { option: true } ],
|
|
|
|
|
baseURL: [ async ({ }, use) => {
|
2021-07-07 20:19:42 +02:00
|
|
|
|
await use(process.env.PLAYWRIGHT_TEST_BASE_URL);
|
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
|
|
|
|
}, { 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,
|
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;
|
2021-08-09 18:09:11 -07:00
|
|
|
|
await use({
|
|
|
|
|
...contextOptions,
|
|
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-11-02 13:58:26 -07:00
|
|
|
|
_snapshotSuffix: [process.env.PLAYWRIGHT_DOCKER ? 'docker' : process.platform, { scope: 'worker' }],
|
2021-10-28 07:31:30 -08:00
|
|
|
|
|
|
|
|
|
_setupContextOptionsAndArtifacts: [async ({ _snapshotSuffix, _browserType, _combinedContextOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => {
|
|
|
|
|
testInfo.snapshotSuffix = _snapshotSuffix;
|
2021-08-09 18:09:11 -07:00
|
|
|
|
if (process.env.PWDEBUG)
|
|
|
|
|
testInfo.setTimeout(0);
|
|
|
|
|
|
2021-11-08 15:39:58 -08:00
|
|
|
|
let traceMode = typeof trace === 'string' ? trace : trace.mode;
|
|
|
|
|
if (traceMode as any === 'retry-with-trace')
|
|
|
|
|
traceMode = 'on-first-retry';
|
|
|
|
|
const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true };
|
|
|
|
|
const traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined };
|
|
|
|
|
|
|
|
|
|
const captureTrace = (traceMode === 'on' || traceMode === 'retain-on-failure' || (traceMode === 'on-first-retry' && testInfo.retry === 1));
|
2021-08-09 18:09:11 -07:00
|
|
|
|
const temporaryTraceFiles: string[] = [];
|
|
|
|
|
const temporaryScreenshots: string[] = [];
|
2021-11-18 14:36:55 -08:00
|
|
|
|
const createdContexts = new Set<BrowserContext>();
|
2021-08-09 18:09:11 -07:00
|
|
|
|
|
|
|
|
|
const onDidCreateContext = async (context: BrowserContext) => {
|
2021-11-18 14:36:55 -08:00
|
|
|
|
createdContexts.add(context);
|
2021-08-09 18:09:11 -07:00
|
|
|
|
context.setDefaultTimeout(actionTimeout || 0);
|
|
|
|
|
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
|
2021-08-31 17:03:31 -07:00
|
|
|
|
if (captureTrace) {
|
2021-11-01 20:23:35 -08:00
|
|
|
|
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
2021-08-31 17:03:31 -07:00
|
|
|
|
if (!(context.tracing as any)[kTracingStarted]) {
|
2021-11-08 15:39:58 -08:00
|
|
|
|
await context.tracing.start({ ...traceOptions, title });
|
2021-08-31 17:03:31 -07:00
|
|
|
|
(context.tracing as any)[kTracingStarted] = true;
|
2021-10-18 21:05:59 -07:00
|
|
|
|
} else {
|
2021-11-01 20:23:35 -08:00
|
|
|
|
await context.tracing.startChunk({ title });
|
2021-08-31 17:03:31 -07:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2021-10-18 21:05:59 -07:00
|
|
|
|
(context.tracing as any)[kTracingStarted] = false;
|
2021-08-16 16:46:35 -07:00
|
|
|
|
await context.tracing.stop();
|
2021-08-31 17:03:31 -07:00
|
|
|
|
}
|
2021-10-26 10:13:35 -08:00
|
|
|
|
(context as any)._instrumentation.addListener({
|
|
|
|
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, userData: any) => {
|
2021-09-27 09:19:59 -07:00
|
|
|
|
if (apiCall.startsWith('expect.'))
|
|
|
|
|
return { userObject: null };
|
2021-10-18 20:06:18 -08:00
|
|
|
|
const testInfoImpl = testInfo as any;
|
|
|
|
|
const step = testInfoImpl._addStep({
|
|
|
|
|
location: stackTrace?.frames[0],
|
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;
|
2021-09-15 11:34:23 -07:00
|
|
|
|
step?.complete(error);
|
2021-08-09 18:09:11 -07:00
|
|
|
|
},
|
2021-10-26 10:13:35 -08:00
|
|
|
|
});
|
2021-08-09 18:09:11 -07:00
|
|
|
|
};
|
|
|
|
|
|
2021-11-04 21:08:42 -07:00
|
|
|
|
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
|
|
|
|
|
2021-08-09 18:09:11 -07:00
|
|
|
|
const onWillCloseContext = async (context: BrowserContext) => {
|
2021-11-04 21:08:42 -07:00
|
|
|
|
(context 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);
|
2021-08-31 17:03:31 -07:00
|
|
|
|
await context.tracing.stopChunk({ path: tracePath });
|
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.
|
|
|
|
|
await Promise.all(context.pages().map(async page => {
|
|
|
|
|
const screenshotPath = path.join(_artifactsDir(), createGuid() + '.png');
|
|
|
|
|
temporaryScreenshots.push(screenshotPath);
|
|
|
|
|
await page.screenshot({ timeout: 5000, path: screenshotPath }).catch(() => {});
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1. Setup instrumentation and process existing contexts.
|
|
|
|
|
(_browserType as any)._onDidCreateContext = onDidCreateContext;
|
|
|
|
|
(_browserType as any)._onWillCloseContext = onWillCloseContext;
|
|
|
|
|
(_browserType as any)._defaultContextOptions = _combinedContextOptions;
|
|
|
|
|
const existingContexts = Array.from((_browserType as any)._contexts) as BrowserContext[];
|
|
|
|
|
await Promise.all(existingContexts.map(onDidCreateContext));
|
|
|
|
|
|
|
|
|
|
// 2. Run the test.
|
|
|
|
|
await use();
|
|
|
|
|
|
|
|
|
|
// 3. Determine whether we need the artifacts.
|
|
|
|
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
2021-09-01 13:41:35 -07:00
|
|
|
|
const isHook = !!hookType(testInfo);
|
2021-11-08 15:39:58 -08:00
|
|
|
|
const preserveTrace = captureTrace && !isHook && (traceMode === 'on' || (testFailed && traceMode === 'retain-on-failure') || (traceMode === 'on-first-retry' && testInfo.retry === 1));
|
2021-08-09 18:09:11 -07:00
|
|
|
|
const captureScreenshots = !isHook && (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed));
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
const leftoverContexts = Array.from((_browserType as any)._contexts) as BrowserContext[];
|
2021-08-31 16:34:52 -07:00
|
|
|
|
(_browserType as any)._onDidCreateContext = undefined;
|
2021-08-09 18:09:11 -07:00
|
|
|
|
(_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());
|
2021-08-09 18:09:11 -07:00
|
|
|
|
|
|
|
|
|
// 5. Collect artifacts from any non-closed contexts.
|
|
|
|
|
await Promise.all(leftoverContexts.map(async context => {
|
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.
|
|
|
|
|
if ((context as any)[startedCollectingArtifacts])
|
|
|
|
|
return;
|
|
|
|
|
|
2021-08-09 18:09:11 -07:00
|
|
|
|
if (preserveTrace)
|
2021-08-31 17:03:31 -07:00
|
|
|
|
await context.tracing.stopChunk({ path: addTraceAttachment() });
|
2021-10-18 21:05:59 -07:00
|
|
|
|
else if (captureTrace)
|
|
|
|
|
await context.tracing.stopChunk();
|
2021-08-09 18:09:11 -07:00
|
|
|
|
if (captureScreenshots)
|
|
|
|
|
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 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(() => {});
|
|
|
|
|
}));
|
2021-11-18 14:36:55 -08:00
|
|
|
|
|
|
|
|
|
// 7. Cleanup created contexts when we know it's safe - this will produce nice error message.
|
|
|
|
|
if (hookType(testInfo) === 'beforeAll' && testInfo.status === 'timedOut') {
|
|
|
|
|
const anyContext = leftoverContexts[0];
|
|
|
|
|
const pendingCalls = anyContext ? formatPendingCalls((anyContext as any)._connection.pendingProtocolCalls()) : '';
|
|
|
|
|
await Promise.all(leftoverContexts.filter(c => createdContexts.has(c)).map(c => c.close()));
|
|
|
|
|
testInfo.error = prependToTestError(testInfo.error, pendingCalls);
|
|
|
|
|
}
|
2021-08-09 18:09:11 -07:00
|
|
|
|
}, { auto: true }],
|
|
|
|
|
|
2021-10-28 07:31:30 -08:00
|
|
|
|
_contextFactory: async ({ browser, video, _artifactsDir }, use, testInfo) => {
|
2021-08-09 18:09:11 -07:00
|
|
|
|
let videoMode = typeof video === 'string' ? video : video.mode;
|
|
|
|
|
if (videoMode === 'retry-with-video')
|
|
|
|
|
videoMode = 'on-first-retry';
|
|
|
|
|
|
|
|
|
|
const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
2021-10-28 07:31:30 -08:00
|
|
|
|
const contexts = new Map<BrowserContext, { pages: Page[] }>();
|
|
|
|
|
|
|
|
|
|
await use(async options => {
|
|
|
|
|
const hook = hookType(testInfo);
|
|
|
|
|
if (hook)
|
|
|
|
|
throw new Error(`"context" and "page" fixtures are not supported in ${hook}. Use browser.newContext() instead.`);
|
|
|
|
|
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()) : '';
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
const videoPath = await v.path();
|
|
|
|
|
const savedPath = testInfo.outputPath(path.basename(videoPath));
|
|
|
|
|
await v.saveAs(savedPath);
|
|
|
|
|
testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// Silent catch empty videos.
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
2021-11-18 14:36:55 -08:00
|
|
|
|
testInfo.error = prependToTestError(testInfo.error, prependToError);
|
2021-10-28 07:31:30 -08:00
|
|
|
|
},
|
2021-06-03 08:07:55 -07:00
|
|
|
|
|
2021-10-28 07:31:30 -08:00
|
|
|
|
context: async ({ _contextFactory }, use) => {
|
|
|
|
|
await use(await _contextFactory());
|
2021-06-03 08:07:55 -07:00
|
|
|
|
},
|
|
|
|
|
|
2021-10-28 07:31:30 -08:00
|
|
|
|
page: async ({ context }, use) => {
|
2021-06-03 08:07:55 -07:00
|
|
|
|
await use(await context.newPage());
|
|
|
|
|
},
|
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-10-26 12:45:53 -08:00
|
|
|
|
export async function browserOptionsWorkerFixture(
|
|
|
|
|
{
|
|
|
|
|
headless,
|
|
|
|
|
channel,
|
|
|
|
|
launchOptions
|
|
|
|
|
}: {
|
|
|
|
|
headless: boolean | undefined,
|
|
|
|
|
channel: string | undefined,
|
|
|
|
|
launchOptions: LaunchOptions
|
|
|
|
|
}, use: (options: LaunchOptions) => Promise<void>) {
|
|
|
|
|
const options: LaunchOptions = {
|
|
|
|
|
handleSIGINT: false,
|
|
|
|
|
timeout: 0,
|
|
|
|
|
...launchOptions,
|
|
|
|
|
};
|
|
|
|
|
if (headless !== undefined)
|
|
|
|
|
options.headless = headless;
|
|
|
|
|
if (channel !== undefined)
|
|
|
|
|
options.channel = channel;
|
|
|
|
|
await use(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function browserTypeWorkerFixture(
|
|
|
|
|
{
|
|
|
|
|
playwright,
|
|
|
|
|
browserName,
|
|
|
|
|
_browserOptions
|
|
|
|
|
}: {
|
|
|
|
|
playwright: any,
|
|
|
|
|
browserName: string,
|
|
|
|
|
_browserOptions: LaunchOptions
|
|
|
|
|
}, use: (browserType: BrowserType) => Promise<void>) {
|
|
|
|
|
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
|
|
|
|
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
|
|
|
|
const browserType = playwright[browserName];
|
|
|
|
|
(browserType as any)._defaultLaunchOptions = _browserOptions;
|
|
|
|
|
await use(browserType);
|
|
|
|
|
(browserType as any)._defaultLaunchOptions = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function browserWorkerFixture(
|
|
|
|
|
{ _browserType }: { _browserType: BrowserType },
|
|
|
|
|
use: (browser: Browser) => Promise<void>) {
|
|
|
|
|
const browser = await _browserType.launch();
|
|
|
|
|
await use(browser);
|
|
|
|
|
await browser.close();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-17 15:09:38 -07:00
|
|
|
|
|
2021-08-31 16:34:52 -07:00
|
|
|
|
function formatPendingCalls(calls: ParsedStackTrace[]) {
|
2021-06-17 15:09:38 -07:00
|
|
|
|
if (!calls.length)
|
|
|
|
|
return '';
|
|
|
|
|
return 'Pending operations:\n' + calls.map(call => {
|
2021-08-31 16:34:52 -07:00
|
|
|
|
const frame = call.frames && call.frames[0] ? formatStackFrame(call.frames[0]) : '<unknown>';
|
2021-06-17 15:09:38 -07:00
|
|
|
|
return ` - ${call.apiName} at ${frame}\n`;
|
|
|
|
|
}).join('') + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
if (testInfo.title.startsWith('beforeAll'))
|
|
|
|
|
return 'beforeAll';
|
|
|
|
|
if (testInfo.title.startsWith('afterAll'))
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
const kTracingStarted = Symbol('kTracingStarted');
|
2021-10-26 12:45:53 -08:00
|
|
|
|
|
|
|
|
|
export default test;
|