mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(test runner): make sure options, trace and screenshot apply to all contexts (#8083)
- Uses some auto fixtures to set default options and instrumentation on BrowserType. - Moves screenshot, trace and video to worker-scoped fixtures. - Throws in page/context when used from beforeAll/afterAll. - Plumbs around BrowserType to be accessible from Browser and BrowserContext.
This commit is contained in:
parent
8eac1e96d3
commit
3bf3318350
@ -23,12 +23,14 @@ import { BrowserContextOptions } from './types';
|
||||
import { isSafeCloseError } from '../utils/errors';
|
||||
import * as api from '../../types/types';
|
||||
import { CDPSession } from './cdpSession';
|
||||
import type { BrowserType } from './browserType';
|
||||
|
||||
export class Browser extends ChannelOwner<channels.BrowserChannel, channels.BrowserInitializer> implements api.Browser {
|
||||
readonly _contexts = new Set<BrowserContext>();
|
||||
private _isConnected = true;
|
||||
private _closedPromise: Promise<void>;
|
||||
_remoteType: 'owns-connection' | 'uses-connection' | null = null;
|
||||
private _browserType!: BrowserType;
|
||||
readonly _name: string;
|
||||
|
||||
static from(browser: channels.BrowserChannel): Browser {
|
||||
@ -46,13 +48,22 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
|
||||
this._closedPromise = new Promise(f => this.once(Events.Browser.Disconnected, f));
|
||||
}
|
||||
|
||||
_setBrowserType(browserType: BrowserType) {
|
||||
this._browserType = browserType;
|
||||
for (const context of this._contexts)
|
||||
context._setBrowserType(browserType);
|
||||
}
|
||||
|
||||
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
return this._wrapApiCall(async (channel: channels.BrowserChannel) => {
|
||||
options = { ...this._browserType._defaultContextOptions, ...options };
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const context = BrowserContext.from((await channel.newContext(contextOptions)).context);
|
||||
context._options = contextOptions;
|
||||
this._contexts.add(context);
|
||||
context._logger = options.logger || this._logger;
|
||||
context._setBrowserType(this._browserType);
|
||||
await this._browserType._onDidCreateContext?.(context);
|
||||
return context;
|
||||
});
|
||||
}
|
||||
|
||||
@ -33,11 +33,13 @@ import * as api from '../../types/types';
|
||||
import * as structs from '../../types/structs';
|
||||
import { CDPSession } from './cdpSession';
|
||||
import { Tracing } from './tracing';
|
||||
import type { BrowserType } from './browserType';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
|
||||
_pages = new Set<Page>();
|
||||
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
|
||||
readonly _browser: Browser | null = null;
|
||||
private _browserType: BrowserType | undefined;
|
||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||
_timeoutSettings = new TimeoutSettings();
|
||||
_ownerPage: Page | undefined;
|
||||
@ -89,6 +91,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
||||
}
|
||||
|
||||
_setBrowserType(browserType: BrowserType) {
|
||||
this._browserType = browserType;
|
||||
browserType._contexts.add(this);
|
||||
}
|
||||
|
||||
private _onPage(page: Page): void {
|
||||
this._pages.add(page);
|
||||
this.emit(Events.BrowserContext.Page, page);
|
||||
@ -311,12 +318,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||
_onClose() {
|
||||
if (this._browser)
|
||||
this._browser._contexts.delete(this);
|
||||
this._browserType?._contexts?.delete(this);
|
||||
this.emit(Events.BrowserContext.Close, this);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
await this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||
await this._browserType?._onWillCloseContext?.(this);
|
||||
await channel.close();
|
||||
await this._closedPromise;
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@ import * as channels from '../protocol/channels';
|
||||
import { Browser } from './browser';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types';
|
||||
import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions, BrowserContextOptions } from './types';
|
||||
import WebSocket from 'ws';
|
||||
import { Connection } from './connection';
|
||||
import { Events } from './events';
|
||||
@ -45,6 +45,13 @@ export interface BrowserServer extends api.BrowserServer {
|
||||
export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, channels.BrowserTypeInitializer> implements api.BrowserType {
|
||||
private _timeoutSettings = new TimeoutSettings();
|
||||
_serverLauncher?: BrowserServerLauncher;
|
||||
_contexts = new Set<BrowserContext>();
|
||||
|
||||
// Instrumentation.
|
||||
_defaultContextOptions: BrowserContextOptions = {};
|
||||
_defaultLaunchOptions: LaunchOptions = {};
|
||||
_onDidCreateContext?: (context: BrowserContext) => Promise<void>;
|
||||
_onWillCloseContext?: (context: BrowserContext) => Promise<void>;
|
||||
|
||||
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
||||
return (browserType as any)._object;
|
||||
@ -69,6 +76,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = { ...this._defaultLaunchOptions, ...options };
|
||||
const launchOptions: channels.BrowserTypeLaunchParams = {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
@ -77,6 +85,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
};
|
||||
const browser = Browser.from((await channel.launch(launchOptions)).browser);
|
||||
browser._logger = logger;
|
||||
browser._setBrowserType(this);
|
||||
return browser;
|
||||
}, logger);
|
||||
}
|
||||
@ -90,6 +99,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
|
||||
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options };
|
||||
const contextParams = await prepareBrowserContextParams(options);
|
||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||
...contextParams,
|
||||
@ -103,6 +113,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
const context = BrowserContext.from(result.context);
|
||||
context._options = contextParams;
|
||||
context._logger = options.logger;
|
||||
context._setBrowserType(this);
|
||||
await this._onDidCreateContext?.(context);
|
||||
return context;
|
||||
}, options.logger);
|
||||
}
|
||||
@ -181,6 +193,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
const browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
||||
browser._logger = logger;
|
||||
browser._remoteType = 'owns-connection';
|
||||
browser._setBrowserType((playwright as any)[browser._name]);
|
||||
const closeListener = () => {
|
||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||
for (const context of browser.contexts()) {
|
||||
@ -252,6 +265,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
||||
browser._contexts.add(BrowserContext.from(result.defaultContext));
|
||||
browser._remoteType = 'uses-connection';
|
||||
browser._logger = logger;
|
||||
browser._setBrowserType(this);
|
||||
return browser;
|
||||
}, logger);
|
||||
}
|
||||
|
||||
@ -47,7 +47,6 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||
if (this._parent) {
|
||||
this._parent._objects.set(guid, this);
|
||||
this._logger = this._parent._logger;
|
||||
this._csi = this._parent._csi;
|
||||
}
|
||||
|
||||
this._channel = this._createChannel(new EventEmitter(), null);
|
||||
@ -95,10 +94,15 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||
const stackTrace = captureStackTrace();
|
||||
const { apiName, frameTexts } = stackTrace;
|
||||
const channel = this._createChannel({}, stackTrace);
|
||||
|
||||
let ancestorWithCSI: ChannelOwner<any> = this;
|
||||
while (!ancestorWithCSI._csi && ancestorWithCSI._parent)
|
||||
ancestorWithCSI = ancestorWithCSI._parent;
|
||||
let csiCallback: ((e?: Error) => void) | undefined;
|
||||
|
||||
try {
|
||||
logApiCall(logger, `=> ${apiName} started`);
|
||||
csiCallback = this._csi?.onApiCall(apiName);
|
||||
csiCallback = ancestorWithCSI._csi?.onApiCall(apiName);
|
||||
const result = await func(channel as any, stackTrace);
|
||||
csiCallback?.();
|
||||
logApiCall(logger, `<= ${apiName} succeeded`);
|
||||
|
||||
@ -487,9 +487,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||
async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) {
|
||||
try {
|
||||
await this._wrapApiCall(async (channel: channels.PageChannel) => {
|
||||
await channel.close(options);
|
||||
if (this._ownedContext)
|
||||
await this._ownedContext.close();
|
||||
else
|
||||
await channel.close(options);
|
||||
});
|
||||
} catch (e) {
|
||||
if (isSafeCloseError(e))
|
||||
|
||||
@ -16,27 +16,51 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext } from '../../types/types';
|
||||
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType } from '../../types/types';
|
||||
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test';
|
||||
import { rootTestType } from './testType';
|
||||
import { createGuid, removeFolders } from '../utils/utils';
|
||||
export { expect } from './expect';
|
||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||
|
||||
const artifactsFolder = path.join(os.tmpdir(), 'pwt-' + createGuid());
|
||||
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||
_combinedContextOptions: BrowserContextOptions,
|
||||
_setupContextOptionsAndArtifacts: void;
|
||||
};
|
||||
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
_browserType: BrowserType;
|
||||
_artifactsDir: () => string,
|
||||
};
|
||||
|
||||
export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>({
|
||||
export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
||||
defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
|
||||
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
|
||||
playwright: [ require('../inprocess'), { scope: 'worker' } ],
|
||||
headless: [ undefined, { scope: 'worker' } ],
|
||||
channel: [ undefined, { scope: 'worker' } ],
|
||||
launchOptions: [ {}, { scope: 'worker' } ],
|
||||
screenshot: [ 'off', { scope: 'worker' } ],
|
||||
video: [ 'off', { scope: 'worker' } ],
|
||||
trace: [ 'off', { scope: 'worker' } ],
|
||||
|
||||
browser: [ async ({ playwright, browserName, headless, channel, launchOptions }, use) => {
|
||||
_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' }],
|
||||
|
||||
_browserType: [async ({ playwright, browserName, headless, channel, launchOptions }, use) => {
|
||||
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
|
||||
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
||||
const browserType = playwright[browserName];
|
||||
|
||||
const options: LaunchOptions = {
|
||||
handleSIGINT: false,
|
||||
timeout: 0,
|
||||
@ -46,15 +70,18 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
options.headless = headless;
|
||||
if (channel !== undefined)
|
||||
options.channel = channel;
|
||||
const browser = await playwright[browserName].launch(options);
|
||||
|
||||
(browserType as any)._defaultLaunchOptions = options;
|
||||
await use(browserType);
|
||||
(browserType as any)._defaultLaunchOptions = undefined;
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
browser: [ async ({ _browserType }, use) => {
|
||||
const browser = await _browserType.launch();
|
||||
await use(browser);
|
||||
await browser.close();
|
||||
await removeFolders([artifactsFolder]);
|
||||
}, { scope: 'worker' } ],
|
||||
|
||||
screenshot: 'off',
|
||||
video: 'off',
|
||||
trace: 'off',
|
||||
acceptDownloads: undefined,
|
||||
bypassCSP: undefined,
|
||||
colorScheme: undefined,
|
||||
@ -81,11 +108,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
},
|
||||
contextOptions: {},
|
||||
|
||||
createContext: async ({
|
||||
browser,
|
||||
screenshot,
|
||||
trace,
|
||||
video,
|
||||
_combinedContextOptions: async ({
|
||||
acceptDownloads,
|
||||
bypassCSP,
|
||||
colorScheme,
|
||||
@ -107,22 +130,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
userAgent,
|
||||
baseURL,
|
||||
contextOptions,
|
||||
actionTimeout,
|
||||
navigationTimeout
|
||||
}, use, testInfo) => {
|
||||
testInfo.snapshotSuffix = process.platform;
|
||||
if (process.env.PWDEBUG)
|
||||
testInfo.setTimeout(0);
|
||||
|
||||
let videoMode = typeof video === 'string' ? video : video.mode;
|
||||
if (videoMode === 'retry-with-video')
|
||||
videoMode = 'on-first-retry';
|
||||
if (trace === 'retry-with-trace')
|
||||
trace = 'on-first-retry';
|
||||
|
||||
const captureVideo = (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
||||
const captureTrace = (trace === 'on' || trace === 'retain-on-failure' || (trace === 'on-first-retry' && testInfo.retry === 1));
|
||||
|
||||
}, use) => {
|
||||
const options: BrowserContextOptions = {};
|
||||
if (acceptDownloads !== undefined)
|
||||
options.acceptDownloads = acceptDownloads;
|
||||
@ -164,6 +172,124 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
options.viewport = viewport;
|
||||
if (baseURL !== undefined)
|
||||
options.baseURL = baseURL;
|
||||
await use({
|
||||
...contextOptions,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
|
||||
_setupContextOptionsAndArtifacts: [async ({ _browserType, _combinedContextOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => {
|
||||
testInfo.snapshotSuffix = process.platform;
|
||||
if (process.env.PWDEBUG)
|
||||
testInfo.setTimeout(0);
|
||||
|
||||
if (trace === 'retry-with-trace')
|
||||
trace = 'on-first-retry';
|
||||
const captureTrace = (trace === 'on' || trace === 'retain-on-failure' || (trace === 'on-first-retry' && testInfo.retry === 1));
|
||||
const temporaryTraceFiles: string[] = [];
|
||||
const temporaryScreenshots: string[] = [];
|
||||
|
||||
const onDidCreateContext = async (context: BrowserContext) => {
|
||||
context.setDefaultTimeout(actionTimeout || 0);
|
||||
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
|
||||
if (captureTrace)
|
||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
(context as any)._csi = {
|
||||
onApiCall: (name: string) => {
|
||||
return (testInfo as any)._addStep('pw:api', name);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const onWillCloseContext = async (context: BrowserContext) => {
|
||||
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);
|
||||
await (context.tracing as any)._export({ path: tracePath });
|
||||
}
|
||||
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.
|
||||
const oldOnDidCreateContext = (_browserType as any)._onDidCreateContext;
|
||||
(_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;
|
||||
const isHook = testInfo.title === 'beforeAll' || testInfo.title === 'afterAll';
|
||||
const preserveTrace = captureTrace && !isHook && (trace === 'on' || (testFailed && trace === 'retain-on-failure') || (trace === 'on-first-retry' && testInfo.retry === 1));
|
||||
const captureScreenshots = !isHook && (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed));
|
||||
|
||||
const traceAttachments: string[] = [];
|
||||
const addTraceAttachment = () => {
|
||||
const tracePath = testInfo.outputPath(`trace${traceAttachments.length ? '-' + traceAttachments.length : ''}.zip`);
|
||||
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[];
|
||||
(_browserType as any)._onDidCreateContext = oldOnDidCreateContext;
|
||||
(_browserType as any)._onWillCloseContext = undefined;
|
||||
(_browserType as any)._defaultContextOptions = undefined;
|
||||
leftoverContexts.forEach(context => (context as any)._csi = undefined);
|
||||
|
||||
// 5. Collect artifacts from any non-closed contexts.
|
||||
await Promise.all(leftoverContexts.map(async context => {
|
||||
if (preserveTrace)
|
||||
await (context.tracing as any)._export({ path: addTraceAttachment() });
|
||||
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(() => {});
|
||||
}));
|
||||
}, { auto: true }],
|
||||
|
||||
createContext: async ({ browser, video, _artifactsDir }, use, testInfo) => {
|
||||
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));
|
||||
|
||||
const allContexts: BrowserContext[] = [];
|
||||
const allPages: Page[] = [];
|
||||
@ -171,64 +297,21 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
await use(async (additionalOptions = {}) => {
|
||||
let recordVideoDir: string | null = null;
|
||||
const recordVideoSize = typeof video === 'string' ? undefined : video.size;
|
||||
if (captureVideo) {
|
||||
await fs.promises.mkdir(artifactsFolder, { recursive: true });
|
||||
recordVideoDir = artifactsFolder;
|
||||
}
|
||||
if (captureVideo)
|
||||
recordVideoDir = _artifactsDir();
|
||||
|
||||
const combinedOptions: BrowserContextOptions = {
|
||||
recordVideo: recordVideoDir ? { dir: recordVideoDir, size: recordVideoSize } : undefined,
|
||||
...contextOptions,
|
||||
...options,
|
||||
...additionalOptions,
|
||||
};
|
||||
const context = await browser.newContext(combinedOptions);
|
||||
(context as any)._csi = {
|
||||
onApiCall: (name: string) => {
|
||||
return (testInfo as any)._addStep('pw:api', name);
|
||||
},
|
||||
};
|
||||
context.setDefaultTimeout(actionTimeout || 0);
|
||||
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
|
||||
context.on('page', page => allPages.push(page));
|
||||
|
||||
if (captureTrace) {
|
||||
const name = path.relative(testInfo.project.outputDir, testInfo.outputDir).replace(/[\/\\]/g, '-');
|
||||
const suffix = allContexts.length ? '-' + allContexts.length : '';
|
||||
await context.tracing.start({ name: name + suffix, screenshots: true, snapshots: true });
|
||||
}
|
||||
|
||||
allContexts.push(context);
|
||||
return context;
|
||||
});
|
||||
|
||||
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
||||
|
||||
await Promise.all(allContexts.map(async (context, contextIndex) => {
|
||||
const preserveTrace = captureTrace && (trace === 'on' || (testFailed && trace === 'retain-on-failure') || (trace === 'on-first-retry' && testInfo.retry === 1));
|
||||
if (preserveTrace) {
|
||||
const suffix = contextIndex ? '-' + contextIndex : '';
|
||||
const tracePath = testInfo.outputPath(`trace${suffix}.zip`);
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
||||
} else if (captureTrace) {
|
||||
await context.tracing.stop();
|
||||
}
|
||||
}));
|
||||
|
||||
const captureScreenshots = (screenshot === 'on' || (screenshot === 'only-on-failure' && testFailed));
|
||||
if (captureScreenshots) {
|
||||
await Promise.all(allPages.map(async (page, index) => {
|
||||
const screenshotPath = testInfo.outputPath(`test-${testFailed ? 'failed' : 'finished'}-${index + 1}.png`);
|
||||
try {
|
||||
await page.screenshot({ timeout: 5000, path: screenshotPath });
|
||||
testInfo.attachments.push({ name: 'screenshot', path: screenshotPath, contentType: 'image/png' });
|
||||
} catch {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const prependToError = (testInfo.status === 'timedOut' && allContexts.length) ?
|
||||
const prependToError = (testInfo.status === 'timedOut' && allContexts.length) ?
|
||||
formatPendingCalls((allContexts[0] as any)._connection.pendingProtocolCalls()) : '';
|
||||
await Promise.all(allContexts.map(context => context.close()));
|
||||
if (prependToError) {
|
||||
@ -241,6 +324,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
await Promise.all(allPages.map(async page => {
|
||||
@ -259,7 +343,9 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
|
||||
}
|
||||
},
|
||||
|
||||
context: async ({ createContext }, use) => {
|
||||
context: async ({ createContext }, use, testInfo) => {
|
||||
if (testInfo.title === 'beforeAll' || testInfo.title === 'afterAll')
|
||||
throw new Error(`"context" and "page" fixtures are not suppoted in ${testInfo.title}. Use browser.newContext() instead.`);
|
||||
await use(await createContext());
|
||||
},
|
||||
|
||||
|
||||
@ -19,11 +19,23 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect, stripAscii } from './playwright-test-fixtures';
|
||||
|
||||
const files = {
|
||||
'helper.ts': `
|
||||
export const test = pwt.test.extend({
|
||||
auto: [ async ({}, run, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await run();
|
||||
}, { auto: true } ]
|
||||
});
|
||||
`
|
||||
};
|
||||
|
||||
test('should support golden', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -34,6 +46,7 @@ test('should support golden', async ({runInlineTest}) => {
|
||||
|
||||
test('should fail on wrong golden', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Line1
|
||||
Line2
|
||||
Line3
|
||||
@ -42,7 +55,7 @@ Line5
|
||||
Line6
|
||||
Line7`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
const data = [];
|
||||
data.push('Line1');
|
||||
@ -67,9 +80,10 @@ Line7`,
|
||||
|
||||
test('should write detailed failure result to an output folder', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world updated').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -89,9 +103,10 @@ test('should write detailed failure result to an output folder', async ({runInli
|
||||
|
||||
test("doesn\'t create comparison artifacts in an output folder for passed negated snapshot matcher", async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world updated').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -110,9 +125,10 @@ test("doesn\'t create comparison artifacts in an output folder for passed negate
|
||||
|
||||
test('should pass on different snapshots with negate matcher', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world updated').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -124,9 +140,10 @@ test('should pass on different snapshots with negate matcher', async ({runInline
|
||||
|
||||
test('should fail on same snapshots with negate matcher', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -140,8 +157,9 @@ test('should fail on same snapshots with negate matcher', async ({runInlineTest}
|
||||
|
||||
test('should write missing expectations locally', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -157,8 +175,9 @@ test('should write missing expectations locally', async ({runInlineTest}, testIn
|
||||
|
||||
test('shouldn\'t write missing expectations locally for negated matcher', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -175,9 +194,10 @@ test('should update snapshot with the update-snapshots flag', async ({runInlineT
|
||||
const EXPECTED_SNAPSHOT = 'Hello world';
|
||||
const ACTUAL_SNAPSHOT = 'Hello world updated';
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -195,9 +215,10 @@ test('shouldn\'t update snapshot with the update-snapshots flag for negated matc
|
||||
const EXPECTED_SNAPSHOT = 'Hello world';
|
||||
const ACTUAL_SNAPSHOT = 'Hello world updated';
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('${ACTUAL_SNAPSHOT}').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -213,8 +234,9 @@ test('shouldn\'t update snapshot with the update-snapshots flag for negated matc
|
||||
test('should silently write missing expectations locally with the update-snapshots flag', async ({runInlineTest}, testInfo) => {
|
||||
const ACTUAL_SNAPSHOT = 'Hello world new';
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -230,8 +252,9 @@ test('should silently write missing expectations locally with the update-snapsho
|
||||
|
||||
test('should silently write missing expectations locally with the update-snapshots flag for negated matcher', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').not.toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -246,11 +269,12 @@ test('should silently write missing expectations locally with the update-snapsho
|
||||
|
||||
test('should match multiple snapshots', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot1.txt': `Snapshot1`,
|
||||
'a.spec.js-snapshots/snapshot2.txt': `Snapshot2`,
|
||||
'a.spec.js-snapshots/snapshot3.txt': `Snapshot3`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Snapshot1').toMatchSnapshot('snapshot1.txt');
|
||||
expect('Snapshot2').toMatchSnapshot('snapshot2.txt');
|
||||
@ -263,6 +287,7 @@ test('should match multiple snapshots', async ({runInlineTest}) => {
|
||||
|
||||
test('should match snapshots from multiple projects', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
import * as path from 'path';
|
||||
module.exports = { projects: [
|
||||
@ -271,14 +296,14 @@ test('should match snapshots from multiple projects', async ({runInlineTest}) =>
|
||||
]};
|
||||
`,
|
||||
'p1/a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('../helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Snapshot1').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
`,
|
||||
'p1/a.spec.js-snapshots/snapshot.txt': `Snapshot1`,
|
||||
'p2/a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('../helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Snapshot2').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
@ -290,9 +315,10 @@ test('should match snapshots from multiple projects', async ({runInlineTest}) =>
|
||||
|
||||
test('should use provided name', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/provided.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot('provided.txt');
|
||||
});
|
||||
@ -303,8 +329,9 @@ test('should use provided name', async ({runInlineTest}) => {
|
||||
|
||||
test('should throw without a name', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot();
|
||||
});
|
||||
@ -316,9 +343,10 @@ test('should throw without a name', async ({runInlineTest}) => {
|
||||
|
||||
test('should use provided name via options', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/provided.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot({ name: 'provided.txt' });
|
||||
});
|
||||
@ -329,9 +357,10 @@ test('should use provided name via options', async ({runInlineTest}) => {
|
||||
|
||||
test('should compare binary', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.dat': Buffer.from([1,2,3,4]),
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from([1,2,3,4])).toMatchSnapshot('snapshot.dat');
|
||||
});
|
||||
@ -342,10 +371,11 @@ test('should compare binary', async ({runInlineTest}) => {
|
||||
|
||||
test('should compare PNG images', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png':
|
||||
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
@ -356,10 +386,11 @@ test('should compare PNG images', async ({runInlineTest}) => {
|
||||
|
||||
test('should compare different PNG images', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png':
|
||||
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
@ -384,10 +415,11 @@ test('should respect threshold', async ({runInlineTest}) => {
|
||||
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
|
||||
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': expected,
|
||||
'a.spec.js-snapshots/snapshot2.png': expected,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 });
|
||||
expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png', { threshold: 0.2 });
|
||||
@ -403,6 +435,7 @@ test('should respect project threshold', async ({runInlineTest}) => {
|
||||
const expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
|
||||
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toMatchSnapshot: { threshold: 0.2 } } },
|
||||
@ -411,7 +444,7 @@ test('should respect project threshold', async ({runInlineTest}) => {
|
||||
'a.spec.js-snapshots/snapshot.png': expected,
|
||||
'a.spec.js-snapshots/snapshot2.png': expected,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 });
|
||||
expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png');
|
||||
@ -425,9 +458,10 @@ test('should respect project threshold', async ({runInlineTest}) => {
|
||||
|
||||
test('should sanitize snapshot name', async ({runInlineTest}) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/-snapshot-.txt': `Hello world`,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot('../../snapshot!.txt');
|
||||
});
|
||||
@ -438,8 +472,9 @@ test('should sanitize snapshot name', async ({runInlineTest}) => {
|
||||
|
||||
test('should write missing expectations with sanitized snapshot name', async ({runInlineTest}, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = pwt;
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('Hello world').toMatchSnapshot('../../snapshot!.txt');
|
||||
});
|
||||
|
||||
280
tests/playwright-test/playwright.artifacts.spec.ts
Normal file
280
tests/playwright-test/playwright.artifacts.spec.ts
Normal file
@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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 { test, expect } from './playwright-test-fixtures';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function listFiles(dir: string): string[] {
|
||||
const result: string[] = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const entry of entries) {
|
||||
result.push(entry.name);
|
||||
if (entry.isDirectory())
|
||||
result.push(...listFiles(path.join(dir, entry.name)).map(x => ' ' + x));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const testFiles = {
|
||||
'artifacts.spec.ts': `
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
const { test } = pwt;
|
||||
|
||||
test.describe('shared', () => {
|
||||
let page;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage({});
|
||||
await page.setContent('<button>Click me</button><button>And me</button>');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('shared passing', async ({ }) => {
|
||||
await page.click('text=Click me');
|
||||
});
|
||||
|
||||
test('shared failing', async ({ }) => {
|
||||
await page.click('text=And me');
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('passing', async ({ page }) => {
|
||||
await page.setContent('I am the page');
|
||||
});
|
||||
|
||||
test('two contexts', async ({ page, createContext }) => {
|
||||
await page.setContent('I am the page');
|
||||
|
||||
const context2 = await createContext();
|
||||
const page2 = await context2.newPage();
|
||||
await page2.setContent('I am the page');
|
||||
});
|
||||
|
||||
test('failing', async ({ page }) => {
|
||||
await page.setContent('I am the page');
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
test('two contexts failing', async ({ page, createContext }) => {
|
||||
await page.setContent('I am the page');
|
||||
|
||||
const context2 = await createContext();
|
||||
const page2 = await context2.newPage();
|
||||
await page2.setContent('I am the page');
|
||||
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
test('own context passing', async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent('<button>Click me</button><button>And me</button>');
|
||||
await page.click('text=Click me');
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('own context failing', async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent('<button>Click me</button><button>And me</button>');
|
||||
await page.click('text=Click me');
|
||||
await page.close();
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
|
||||
const testPersistent = test.extend({
|
||||
page: async ({ playwright, browserName }, use) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'user-data-dir-'));
|
||||
const context = await playwright[browserName].launchPersistentContext(dir);
|
||||
await use(context.pages()[0]);
|
||||
await context.close();
|
||||
rimraf.sync(dir);
|
||||
},
|
||||
});
|
||||
|
||||
testPersistent('persistent passing', async ({ page }) => {
|
||||
await page.setContent('<button>Click me</button><button>And me</button>');
|
||||
});
|
||||
|
||||
testPersistent('persistent failing', async ({ page }) => {
|
||||
await page.setContent('<button>Click me</button><button>And me</button>');
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
};
|
||||
|
||||
test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...testFiles,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { screenshot: 'on' } };
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(5);
|
||||
expect(result.failed).toBe(5);
|
||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'artifacts-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-own-context-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-own-context-passing',
|
||||
' test-finished-1.png',
|
||||
'artifacts-passing',
|
||||
' test-finished-1.png',
|
||||
'artifacts-persistent-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-persistent-passing',
|
||||
' test-finished-1.png',
|
||||
'artifacts-shared-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-shared-passing',
|
||||
' test-finished-1.png',
|
||||
'artifacts-two-contexts',
|
||||
' test-finished-1.png',
|
||||
' test-finished-2.png',
|
||||
'artifacts-two-contexts-failing',
|
||||
' test-failed-1.png',
|
||||
' test-failed-2.png',
|
||||
'report.json',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should work with screenshot: only-on-failure', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...testFiles,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { screenshot: 'only-on-failure' } };
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(5);
|
||||
expect(result.failed).toBe(5);
|
||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'artifacts-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-own-context-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-persistent-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-shared-failing',
|
||||
' test-failed-1.png',
|
||||
'artifacts-two-contexts-failing',
|
||||
' test-failed-1.png',
|
||||
' test-failed-2.png',
|
||||
'report.json',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should work with trace: on', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...testFiles,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { trace: 'on' } };
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(5);
|
||||
expect(result.failed).toBe(5);
|
||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'artifacts-failing',
|
||||
' trace.zip',
|
||||
'artifacts-own-context-failing',
|
||||
' trace.zip',
|
||||
'artifacts-own-context-passing',
|
||||
' trace.zip',
|
||||
'artifacts-passing',
|
||||
' trace.zip',
|
||||
'artifacts-persistent-failing',
|
||||
' trace.zip',
|
||||
'artifacts-persistent-passing',
|
||||
' trace.zip',
|
||||
'artifacts-shared-failing',
|
||||
' trace.zip',
|
||||
'artifacts-shared-passing',
|
||||
' trace.zip',
|
||||
'artifacts-two-contexts',
|
||||
' trace-1.zip',
|
||||
' trace.zip',
|
||||
'artifacts-two-contexts-failing',
|
||||
' trace-1.zip',
|
||||
' trace.zip',
|
||||
'report.json',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should work with trace: retain-on-failure', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...testFiles,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { trace: 'retain-on-failure' } };
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(5);
|
||||
expect(result.failed).toBe(5);
|
||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'artifacts-failing',
|
||||
' trace.zip',
|
||||
'artifacts-own-context-failing',
|
||||
' trace.zip',
|
||||
'artifacts-persistent-failing',
|
||||
' trace.zip',
|
||||
'artifacts-shared-failing',
|
||||
' trace.zip',
|
||||
'artifacts-two-contexts-failing',
|
||||
' trace-1.zip',
|
||||
' trace.zip',
|
||||
'report.json',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should work with trace: on-first-retry', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...testFiles,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { trace: 'on-first-retry' } };
|
||||
`,
|
||||
}, { workers: 1, retries: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(5);
|
||||
expect(result.failed).toBe(5);
|
||||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'artifacts-failing-retry1',
|
||||
' trace.zip',
|
||||
'artifacts-own-context-failing-retry1',
|
||||
' trace.zip',
|
||||
'artifacts-persistent-failing-retry1',
|
||||
' trace.zip',
|
||||
'artifacts-shared-failing-retry1',
|
||||
' trace.zip',
|
||||
'artifacts-two-contexts-failing-retry1',
|
||||
' trace-1.zip',
|
||||
' trace.zip',
|
||||
'report.json',
|
||||
]);
|
||||
});
|
||||
@ -161,6 +161,65 @@ test('should override use:browserName with --browser', async ({ runInlineTest })
|
||||
]);
|
||||
});
|
||||
|
||||
test('should respect context options in various contexts', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { viewport: { width: 500, height: 500 } } };
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
const { test } = pwt;
|
||||
test.use({ locale: 'fr-CH' });
|
||||
|
||||
let context;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
context = await browser.newContext();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('shared context', async ({}) => {
|
||||
const page = await context.newPage();
|
||||
expect(page.viewportSize()).toEqual({ width: 500, height: 500 });
|
||||
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
|
||||
});
|
||||
|
||||
test('own context', async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
expect(page.viewportSize()).toEqual({ width: 500, height: 500 });
|
||||
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('default context', async ({ page }) => {
|
||||
expect(page.viewportSize()).toEqual({ width: 500, height: 500 });
|
||||
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
|
||||
});
|
||||
|
||||
test('persistent context', async ({ playwright, browserName }) => {
|
||||
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'user-data-dir-'));
|
||||
const context = await playwright[browserName].launchPersistentContext(dir);
|
||||
const page = context.pages()[0];
|
||||
|
||||
expect(page.viewportSize()).toEqual({ width: 500, height: 500 });
|
||||
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
|
||||
|
||||
await context.close();
|
||||
rimraf.sync(dir);
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(4);
|
||||
});
|
||||
|
||||
test('should report error and pending operations on timeout', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
@ -185,6 +244,22 @@ test('should report error and pending operations on timeout', async ({ runInline
|
||||
expect(stripAscii(result.output)).toContain(`10 | page.textContent('text=More missing'),`);
|
||||
});
|
||||
|
||||
test('should throw when using page in beforeAll', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test.beforeAll(async ({ page }) => {
|
||||
});
|
||||
test('ok', async ({ page }) => {
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain(`Error: "context" and "page" fixtures are not suppoted in beforeAll. Use browser.newContext() instead.`);
|
||||
});
|
||||
|
||||
test('should report click error on sigint', async ({ runInlineTest }) => {
|
||||
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||
|
||||
@ -208,37 +283,6 @@ test('should report click error on sigint', async ({ runInlineTest }) => {
|
||||
expect(stripAscii(result.output)).toContain(`8 | const promise = page.click('text=Missing');`);
|
||||
});
|
||||
|
||||
test('should work with screenshot: only-on-failure', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { screenshot: 'only-on-failure' }, name: 'chromium' };
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('pass', async ({ page }) => {
|
||||
await page.setContent('<div>PASS</div>');
|
||||
test.expect(1 + 1).toBe(2);
|
||||
});
|
||||
test('fail', async ({ page }) => {
|
||||
await page.setContent('<div>FAIL</div>');
|
||||
const page2 = await page.context().newPage();
|
||||
await page2.setContent('<div>FAIL</div>');
|
||||
test.expect(1 + 1).toBe(1);
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const screenshotPass = testInfo.outputPath('test-results', 'a-pass-chromium', 'test-failed-1.png');
|
||||
const screenshotFail1 = testInfo.outputPath('test-results', 'a-fail-chromium', 'test-failed-1.png');
|
||||
const screenshotFail2 = testInfo.outputPath('test-results', 'a-fail-chromium', 'test-failed-2.png');
|
||||
expect(fs.existsSync(screenshotPass)).toBe(false);
|
||||
expect(fs.existsSync(screenshotFail1)).toBe(true);
|
||||
expect(fs.existsSync(screenshotFail2)).toBe(true);
|
||||
});
|
||||
|
||||
test('should work with video: retain-on-failure', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
@ -339,36 +383,3 @@ test('should work with video size', async ({ runInlineTest }, testInfo) => {
|
||||
expect(videoPlayer.videoWidth).toBe(220);
|
||||
expect(videoPlayer.videoHeight).toBe(110);
|
||||
});
|
||||
|
||||
test('should work with multiple contexts and trace: on', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { trace: 'on' } };
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('pass', async ({ page, createContext }) => {
|
||||
await page.setContent('<div>PASS</div>');
|
||||
|
||||
const context1 = await createContext();
|
||||
const page1 = await context1.newPage();
|
||||
await page1.setContent('<div>PASS</div>');
|
||||
|
||||
const context2 = await createContext({ locale: 'en-US' });
|
||||
const page2 = await context2.newPage();
|
||||
await page2.setContent('<div>PASS</div>');
|
||||
|
||||
test.expect(1 + 1).toBe(2);
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const traceDefault = testInfo.outputPath('test-results', 'a-pass', 'trace.zip');
|
||||
const trace1 = testInfo.outputPath('test-results', 'a-pass', 'trace-1.zip');
|
||||
const trace2 = testInfo.outputPath('test-results', 'a-pass', 'trace-2.zip');
|
||||
expect(fs.existsSync(traceDefault)).toBe(true);
|
||||
expect(fs.existsSync(trace1)).toBe(true);
|
||||
expect(fs.existsSync(trace2)).toBe(true);
|
||||
});
|
||||
|
||||
@ -114,7 +114,6 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
test('should work without a file extension', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'reporter.ts': smallReporterJS,
|
||||
@ -162,6 +161,9 @@ test('should load reporter from node_modules', async ({ runInlineTest }) => {
|
||||
test('should report expect steps', async ({ runInlineTest }) => {
|
||||
const expectReporterJS = `
|
||||
class Reporter {
|
||||
onStdOut(chunk) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
onStepBegin(test, result, step) {
|
||||
const copy = { ...step, startTime: undefined, duration: undefined };
|
||||
console.log('%%%% begin', JSON.stringify(copy));
|
||||
@ -232,6 +234,15 @@ test('should report expect steps', async ({ runInlineTest }) => {
|
||||
test('should report api steps', async ({ runInlineTest }) => {
|
||||
const expectReporterJS = `
|
||||
class Reporter {
|
||||
onStdOut(chunk) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
onTestBegin(test) {
|
||||
console.log('%%%% test begin ' + test.title);
|
||||
}
|
||||
onTestEnd(test) {
|
||||
console.log('%%%% test end ' + test.title);
|
||||
}
|
||||
onStepBegin(test, result, step) {
|
||||
const copy = { ...step, startTime: undefined, duration: undefined };
|
||||
console.log('%%%% begin', JSON.stringify(copy));
|
||||
@ -259,11 +270,31 @@ test('should report api steps', async ({ runInlineTest }) => {
|
||||
await page.setContent('<button></button>');
|
||||
await page.click('button');
|
||||
});
|
||||
|
||||
test.describe('suite', () => {
|
||||
let myPage;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
myPage = await browser.newPage();
|
||||
await myPage.setContent('<button></button>');
|
||||
});
|
||||
|
||||
test('pass1', async () => {
|
||||
await myPage.click('button');
|
||||
});
|
||||
test('pass2', async () => {
|
||||
await myPage.click('button');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await myPage.close();
|
||||
});
|
||||
});
|
||||
`
|
||||
}, { reporter: '', workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([
|
||||
`%%%% test begin pass`,
|
||||
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||
@ -276,6 +307,23 @@ test('should report api steps', async ({ runInlineTest }) => {
|
||||
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
||||
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
||||
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||
`%%%% test end pass`,
|
||||
`%%%% test begin pass1`,
|
||||
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`,
|
||||
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`,
|
||||
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||
`%%%% test end pass1`,
|
||||
`%%%% test begin pass2`,
|
||||
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
|
||||
`%% begin {\"title\":\"page.click\",\"category\":\"pw:api\"}`,
|
||||
`%% end {\"title\":\"page.click\",\"category\":\"pw:api\"}`,
|
||||
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||
`%%%% test end pass2`,
|
||||
]);
|
||||
});
|
||||
|
||||
@ -283,6 +331,9 @@ test('should report api steps', async ({ runInlineTest }) => {
|
||||
test('should report api step failure', async ({ runInlineTest }) => {
|
||||
const expectReporterJS = `
|
||||
class Reporter {
|
||||
onStdOut(chunk) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
onStepBegin(test, result, step) {
|
||||
const copy = { ...step, startTime: undefined, duration: undefined };
|
||||
console.log('%%%% begin', JSON.stringify(copy));
|
||||
@ -333,6 +384,9 @@ test('should report api step failure', async ({ runInlineTest }) => {
|
||||
test('should report test.step', async ({ runInlineTest }) => {
|
||||
const expectReporterJS = `
|
||||
class Reporter {
|
||||
onStdOut(chunk) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
onStepBegin(test, result, step) {
|
||||
const copy = { ...step, startTime: undefined, duration: undefined };
|
||||
console.log('%%%% begin', JSON.stringify(copy));
|
||||
|
||||
@ -68,7 +68,7 @@ test('should retry timeout', async ({ runInlineTest }) => {
|
||||
await new Promise(f => setTimeout(f, 10000));
|
||||
});
|
||||
`
|
||||
}, { timeout: 100, retries: 2 });
|
||||
}, { timeout: 1000, retries: 2 });
|
||||
expect(exitCode).toBe(1);
|
||||
expect(passed).toBe(0);
|
||||
expect(failed).toBe(1);
|
||||
|
||||
@ -88,6 +88,7 @@ test('should include the project name', async ({ runInlineTest }) => {
|
||||
'helper.ts': `
|
||||
export const test = pwt.test.extend({
|
||||
auto: [ async ({}, run, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await run();
|
||||
}, { auto: true } ]
|
||||
});
|
||||
|
||||
58
types/test.d.ts
vendored
58
types/test.d.ts
vendored
@ -2297,6 +2297,35 @@ export interface PlaywrightWorkerOptions {
|
||||
* [fixtures.channel](https://playwright.dev/docs/api/class-fixtures#fixtures-channel) take priority over this.
|
||||
*/
|
||||
launchOptions: LaunchOptions;
|
||||
/**
|
||||
* Whether to automatically capture a screenshot after each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not capture screenshots.
|
||||
* - `'on'`: Capture screenshot after each test.
|
||||
* - `'only-on-failure'`: Capture screenshot after each test failure.
|
||||
*
|
||||
* Learn more about [automatic screenshots](https://playwright.dev/docs/test-configuration#automatic-screenshots).
|
||||
*/
|
||||
screenshot: 'off' | 'on' | 'only-on-failure';
|
||||
/**
|
||||
* Whether to record a trace for each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not record a trace.
|
||||
* - `'on'`: Record a trace for each test.
|
||||
* - `'retain-on-failure'`: Record a trace for each test, but remove it from successful test runs.
|
||||
* - `'on-first-retry'`: Record a trace only when retrying a test for the first time.
|
||||
*
|
||||
* Learn more about [recording trace](https://playwright.dev/docs/test-configuration#record-test-trace).
|
||||
*/
|
||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
|
||||
/**
|
||||
* Whether to record video for each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not record video.
|
||||
* - `'on'`: Record video for each test.
|
||||
* - `'retain-on-failure'`: Record video for each test, but remove all videos from successful test runs.
|
||||
* - `'on-first-retry'`: Record video only when retrying a test for the first time.
|
||||
*
|
||||
* Learn more about [recording video](https://playwright.dev/docs/test-configuration#record-video).
|
||||
*/
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
}
|
||||
|
||||
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-video';
|
||||
@ -2390,35 +2419,6 @@ export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' |
|
||||
*
|
||||
*/
|
||||
export interface PlaywrightTestOptions {
|
||||
/**
|
||||
* Whether to automatically capture a screenshot after each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not capture screenshots.
|
||||
* - `'on'`: Capture screenshot after each test.
|
||||
* - `'only-on-failure'`: Capture screenshot after each test failure.
|
||||
*
|
||||
* Learn more about [automatic screenshots](https://playwright.dev/docs/test-configuration#automatic-screenshots).
|
||||
*/
|
||||
screenshot: 'off' | 'on' | 'only-on-failure';
|
||||
/**
|
||||
* Whether to record a trace for each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not record a trace.
|
||||
* - `'on'`: Record a trace for each test.
|
||||
* - `'retain-on-failure'`: Record a trace for each test, but remove it from successful test runs.
|
||||
* - `'on-first-retry'`: Record a trace only when retrying a test for the first time.
|
||||
*
|
||||
* Learn more about [recording trace](https://playwright.dev/docs/test-configuration#record-test-trace).
|
||||
*/
|
||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
|
||||
/**
|
||||
* Whether to record video for each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not record video.
|
||||
* - `'on'`: Record video for each test.
|
||||
* - `'retain-on-failure'`: Record video for each test, but remove all videos from successful test runs.
|
||||
* - `'on-first-retry'`: Record video only when retrying a test for the first time.
|
||||
*
|
||||
* Learn more about [recording video](https://playwright.dev/docs/test-configuration#record-video).
|
||||
*/
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
/**
|
||||
* Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
|
||||
*/
|
||||
|
||||
6
utils/generate_types/overrides-test.d.ts
vendored
6
utils/generate_types/overrides-test.d.ts
vendored
@ -284,14 +284,14 @@ export interface PlaywrightWorkerOptions {
|
||||
headless: boolean | undefined;
|
||||
channel: BrowserChannel | undefined;
|
||||
launchOptions: LaunchOptions;
|
||||
screenshot: 'off' | 'on' | 'only-on-failure';
|
||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
}
|
||||
|
||||
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-video';
|
||||
|
||||
export interface PlaywrightTestOptions {
|
||||
screenshot: 'off' | 'on' | 'only-on-failure';
|
||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
acceptDownloads: boolean | undefined;
|
||||
bypassCSP: boolean | undefined;
|
||||
colorScheme: ColorScheme | undefined;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user