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:
Dmitry Gozman 2021-08-09 18:09:11 -07:00 committed by GitHub
parent 8eac1e96d3
commit 3bf3318350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 711 additions and 205 deletions

View File

@ -23,12 +23,14 @@ import { BrowserContextOptions } from './types';
import { isSafeCloseError } from '../utils/errors'; import { isSafeCloseError } from '../utils/errors';
import * as api from '../../types/types'; import * as api from '../../types/types';
import { CDPSession } from './cdpSession'; import { CDPSession } from './cdpSession';
import type { BrowserType } from './browserType';
export class Browser extends ChannelOwner<channels.BrowserChannel, channels.BrowserInitializer> implements api.Browser { export class Browser extends ChannelOwner<channels.BrowserChannel, channels.BrowserInitializer> implements api.Browser {
readonly _contexts = new Set<BrowserContext>(); readonly _contexts = new Set<BrowserContext>();
private _isConnected = true; private _isConnected = true;
private _closedPromise: Promise<void>; private _closedPromise: Promise<void>;
_remoteType: 'owns-connection' | 'uses-connection' | null = null; _remoteType: 'owns-connection' | 'uses-connection' | null = null;
private _browserType!: BrowserType;
readonly _name: string; readonly _name: string;
static from(browser: channels.BrowserChannel): Browser { 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)); 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> { async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
return this._wrapApiCall(async (channel: channels.BrowserChannel) => { return this._wrapApiCall(async (channel: channels.BrowserChannel) => {
options = { ...this._browserType._defaultContextOptions, ...options };
const contextOptions = await prepareBrowserContextParams(options); const contextOptions = await prepareBrowserContextParams(options);
const context = BrowserContext.from((await channel.newContext(contextOptions)).context); const context = BrowserContext.from((await channel.newContext(contextOptions)).context);
context._options = contextOptions; context._options = contextOptions;
this._contexts.add(context); this._contexts.add(context);
context._logger = options.logger || this._logger; context._logger = options.logger || this._logger;
context._setBrowserType(this._browserType);
await this._browserType._onDidCreateContext?.(context);
return context; return context;
}); });
} }

View File

@ -33,11 +33,13 @@ import * as api from '../../types/types';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
import { CDPSession } from './cdpSession'; import { CDPSession } from './cdpSession';
import { Tracing } from './tracing'; import { Tracing } from './tracing';
import type { BrowserType } from './browserType';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = []; private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
readonly _browser: Browser | null = null; readonly _browser: Browser | null = null;
private _browserType: BrowserType | undefined;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>(); readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
_timeoutSettings = new TimeoutSettings(); _timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined; _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)); 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 { private _onPage(page: Page): void {
this._pages.add(page); this._pages.add(page);
this.emit(Events.BrowserContext.Page, page); this.emit(Events.BrowserContext.Page, page);
@ -311,12 +318,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_onClose() { _onClose() {
if (this._browser) if (this._browser)
this._browser._contexts.delete(this); this._browser._contexts.delete(this);
this._browserType?._contexts?.delete(this);
this.emit(Events.BrowserContext.Close, this); this.emit(Events.BrowserContext.Close, this);
} }
async close(): Promise<void> { async close(): Promise<void> {
try { try {
await this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await this._browserType?._onWillCloseContext?.(this);
await channel.close(); await channel.close();
await this._closedPromise; await this._closedPromise;
}); });

View File

@ -18,7 +18,7 @@ import * as channels from '../protocol/channels';
import { Browser } from './browser'; import { Browser } from './browser';
import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types'; import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions, BrowserContextOptions } from './types';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { Connection } from './connection'; import { Connection } from './connection';
import { Events } from './events'; 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 { export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, channels.BrowserTypeInitializer> implements api.BrowserType {
private _timeoutSettings = new TimeoutSettings(); private _timeoutSettings = new TimeoutSettings();
_serverLauncher?: BrowserServerLauncher; _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 { static from(browserType: channels.BrowserTypeChannel): BrowserType {
return (browserType as any)._object; return (browserType as any)._object;
@ -69,6 +76,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { 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).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.'); assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = { ...this._defaultLaunchOptions, ...options };
const launchOptions: channels.BrowserTypeLaunchParams = { const launchOptions: channels.BrowserTypeLaunchParams = {
...options, ...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, 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); const browser = Browser.from((await channel.launch(launchOptions)).browser);
browser._logger = logger; browser._logger = logger;
browser._setBrowserType(this);
return browser; return browser;
}, logger); }, logger);
} }
@ -90,6 +99,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); 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 contextParams = await prepareBrowserContextParams(options);
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
...contextParams, ...contextParams,
@ -103,6 +113,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
const context = BrowserContext.from(result.context); const context = BrowserContext.from(result.context);
context._options = contextParams; context._options = contextParams;
context._logger = options.logger; context._logger = options.logger;
context._setBrowserType(this);
await this._onDidCreateContext?.(context);
return context; return context;
}, options.logger); }, options.logger);
} }
@ -181,6 +193,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
const browser = Browser.from(playwright._initializer.preLaunchedBrowser!); const browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._logger = logger; browser._logger = logger;
browser._remoteType = 'owns-connection'; browser._remoteType = 'owns-connection';
browser._setBrowserType((playwright as any)[browser._name]);
const closeListener = () => { const closeListener = () => {
// Emulate all pages, contexts and the browser closing upon disconnect. // Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser.contexts()) { 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._contexts.add(BrowserContext.from(result.defaultContext));
browser._remoteType = 'uses-connection'; browser._remoteType = 'uses-connection';
browser._logger = logger; browser._logger = logger;
browser._setBrowserType(this);
return browser; return browser;
}, logger); }, logger);
} }

View File

@ -47,7 +47,6 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
if (this._parent) { if (this._parent) {
this._parent._objects.set(guid, this); this._parent._objects.set(guid, this);
this._logger = this._parent._logger; this._logger = this._parent._logger;
this._csi = this._parent._csi;
} }
this._channel = this._createChannel(new EventEmitter(), null); 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 stackTrace = captureStackTrace();
const { apiName, frameTexts } = stackTrace; const { apiName, frameTexts } = stackTrace;
const channel = this._createChannel({}, 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; let csiCallback: ((e?: Error) => void) | undefined;
try { try {
logApiCall(logger, `=> ${apiName} started`); logApiCall(logger, `=> ${apiName} started`);
csiCallback = this._csi?.onApiCall(apiName); csiCallback = ancestorWithCSI._csi?.onApiCall(apiName);
const result = await func(channel as any, stackTrace); const result = await func(channel as any, stackTrace);
csiCallback?.(); csiCallback?.();
logApiCall(logger, `<= ${apiName} succeeded`); logApiCall(logger, `<= ${apiName} succeeded`);

View File

@ -487,9 +487,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) { async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) {
try { try {
await this._wrapApiCall(async (channel: channels.PageChannel) => { await this._wrapApiCall(async (channel: channels.PageChannel) => {
await channel.close(options);
if (this._ownedContext) if (this._ownedContext)
await this._ownedContext.close(); await this._ownedContext.close();
else
await channel.close(options);
}); });
} catch (e) { } catch (e) {
if (isSafeCloseError(e)) if (isSafeCloseError(e))

View File

@ -16,27 +16,51 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType } from '../../types/types';
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext } from '../../types/types';
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test'; import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '../../types/test';
import { rootTestType } from './testType'; import { rootTestType } from './testType';
import { createGuid, removeFolders } from '../utils/utils'; import { createGuid, removeFolders } from '../utils/utils';
export { expect } from './expect'; export { expect } from './expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test; 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' } ], defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ], browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
playwright: [ require('../inprocess'), { scope: 'worker' } ], playwright: [ require('../inprocess'), { scope: 'worker' } ],
headless: [ undefined, { scope: 'worker' } ], headless: [ undefined, { scope: 'worker' } ],
channel: [ undefined, { scope: 'worker' } ], channel: [ undefined, { scope: 'worker' } ],
launchOptions: [ {}, { 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)) if (!['chromium', 'firefox', 'webkit'].includes(browserName))
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
const browserType = playwright[browserName];
const options: LaunchOptions = { const options: LaunchOptions = {
handleSIGINT: false, handleSIGINT: false,
timeout: 0, timeout: 0,
@ -46,15 +70,18 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
options.headless = headless; options.headless = headless;
if (channel !== undefined) if (channel !== undefined)
options.channel = channel; 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 use(browser);
await browser.close(); await browser.close();
await removeFolders([artifactsFolder]);
}, { scope: 'worker' } ], }, { scope: 'worker' } ],
screenshot: 'off',
video: 'off',
trace: 'off',
acceptDownloads: undefined, acceptDownloads: undefined,
bypassCSP: undefined, bypassCSP: undefined,
colorScheme: undefined, colorScheme: undefined,
@ -81,11 +108,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
}, },
contextOptions: {}, contextOptions: {},
createContext: async ({ _combinedContextOptions: async ({
browser,
screenshot,
trace,
video,
acceptDownloads, acceptDownloads,
bypassCSP, bypassCSP,
colorScheme, colorScheme,
@ -107,22 +130,7 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
userAgent, userAgent,
baseURL, baseURL,
contextOptions, contextOptions,
actionTimeout, }, use) => {
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));
const options: BrowserContextOptions = {}; const options: BrowserContextOptions = {};
if (acceptDownloads !== undefined) if (acceptDownloads !== undefined)
options.acceptDownloads = acceptDownloads; options.acceptDownloads = acceptDownloads;
@ -164,6 +172,124 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
options.viewport = viewport; options.viewport = viewport;
if (baseURL !== undefined) if (baseURL !== undefined)
options.baseURL = baseURL; 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 allContexts: BrowserContext[] = [];
const allPages: Page[] = []; const allPages: Page[] = [];
@ -171,63 +297,20 @@ export const test = _baseTest.extend<PlaywrightTestArgs & PlaywrightTestOptions,
await use(async (additionalOptions = {}) => { await use(async (additionalOptions = {}) => {
let recordVideoDir: string | null = null; let recordVideoDir: string | null = null;
const recordVideoSize = typeof video === 'string' ? undefined : video.size; const recordVideoSize = typeof video === 'string' ? undefined : video.size;
if (captureVideo) { if (captureVideo)
await fs.promises.mkdir(artifactsFolder, { recursive: true }); recordVideoDir = _artifactsDir();
recordVideoDir = artifactsFolder;
}
const combinedOptions: BrowserContextOptions = { const combinedOptions: BrowserContextOptions = {
recordVideo: recordVideoDir ? { dir: recordVideoDir, size: recordVideoSize } : undefined, recordVideo: recordVideoDir ? { dir: recordVideoDir, size: recordVideoSize } : undefined,
...contextOptions,
...options,
...additionalOptions, ...additionalOptions,
}; };
const context = await browser.newContext(combinedOptions); 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)); 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); allContexts.push(context);
return 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()) : ''; formatPendingCalls((allContexts[0] as any)._connection.pendingProtocolCalls()) : '';
await Promise.all(allContexts.map(context => context.close())); await Promise.all(allContexts.map(context => context.close()));
@ -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)); const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
if (preserveVideo) { if (preserveVideo) {
await Promise.all(allPages.map(async page => { 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()); await use(await createContext());
}, },

View File

@ -19,11 +19,23 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { test, expect, stripAscii } from './playwright-test-fixtures'; 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}) => { test('should support golden', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`, 'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt'); expect('Hello world').toMatchSnapshot('snapshot.txt');
}); });
@ -34,6 +46,7 @@ test('should support golden', async ({runInlineTest}) => {
test('should fail on wrong golden', async ({runInlineTest}) => { test('should fail on wrong golden', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Line1 'a.spec.js-snapshots/snapshot.txt': `Line1
Line2 Line2
Line3 Line3
@ -42,7 +55,7 @@ Line5
Line6 Line6
Line7`, Line7`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
const data = []; const data = [];
data.push('Line1'); data.push('Line1');
@ -67,9 +80,10 @@ Line7`,
test('should write detailed failure result to an output folder', async ({runInlineTest}, testInfo) => { test('should write detailed failure result to an output folder', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`, 'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world updated').toMatchSnapshot('snapshot.txt'); 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) => { test("doesn\'t create comparison artifacts in an output folder for passed negated snapshot matcher", async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`, 'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world updated').not.toMatchSnapshot('snapshot.txt'); 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}) => { test('should pass on different snapshots with negate matcher', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`, 'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world updated').not.toMatchSnapshot('snapshot.txt'); 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}) => { test('should fail on same snapshots with negate matcher', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': `Hello world`, 'a.spec.js-snapshots/snapshot.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt'); 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) => { test('should write missing expectations locally', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('snapshot.txt'); 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) => { test('shouldn\'t write missing expectations locally for negated matcher', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt'); 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 EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated'; const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT, 'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt'); 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 EXPECTED_SNAPSHOT = 'Hello world';
const ACTUAL_SNAPSHOT = 'Hello world updated'; const ACTUAL_SNAPSHOT = 'Hello world updated';
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT, 'a.spec.js-snapshots/snapshot.txt': EXPECTED_SNAPSHOT,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').not.toMatchSnapshot('snapshot.txt'); 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) => { test('should silently write missing expectations locally with the update-snapshots flag', async ({runInlineTest}, testInfo) => {
const ACTUAL_SNAPSHOT = 'Hello world new'; const ACTUAL_SNAPSHOT = 'Hello world new';
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('${ACTUAL_SNAPSHOT}').toMatchSnapshot('snapshot.txt'); 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) => { test('should silently write missing expectations locally with the update-snapshots flag for negated matcher', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').not.toMatchSnapshot('snapshot.txt'); 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}) => { test('should match multiple snapshots', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot1.txt': `Snapshot1`, 'a.spec.js-snapshots/snapshot1.txt': `Snapshot1`,
'a.spec.js-snapshots/snapshot2.txt': `Snapshot2`, 'a.spec.js-snapshots/snapshot2.txt': `Snapshot2`,
'a.spec.js-snapshots/snapshot3.txt': `Snapshot3`, 'a.spec.js-snapshots/snapshot3.txt': `Snapshot3`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Snapshot1').toMatchSnapshot('snapshot1.txt'); expect('Snapshot1').toMatchSnapshot('snapshot1.txt');
expect('Snapshot2').toMatchSnapshot('snapshot2.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}) => { test('should match snapshots from multiple projects', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'playwright.config.ts': ` 'playwright.config.ts': `
import * as path from 'path'; import * as path from 'path';
module.exports = { projects: [ module.exports = { projects: [
@ -271,14 +296,14 @@ test('should match snapshots from multiple projects', async ({runInlineTest}) =>
]}; ]};
`, `,
'p1/a.spec.js': ` 'p1/a.spec.js': `
const { test } = pwt; const { test } = require('../helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Snapshot1').toMatchSnapshot('snapshot.txt'); expect('Snapshot1').toMatchSnapshot('snapshot.txt');
}); });
`, `,
'p1/a.spec.js-snapshots/snapshot.txt': `Snapshot1`, 'p1/a.spec.js-snapshots/snapshot.txt': `Snapshot1`,
'p2/a.spec.js': ` 'p2/a.spec.js': `
const { test } = pwt; const { test } = require('../helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Snapshot2').toMatchSnapshot('snapshot.txt'); 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}) => { test('should use provided name', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/provided.txt': `Hello world`, 'a.spec.js-snapshots/provided.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('provided.txt'); 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}) => { test('should throw without a name', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot(); 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}) => { test('should use provided name via options', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/provided.txt': `Hello world`, 'a.spec.js-snapshots/provided.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot({ name: 'provided.txt' }); 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}) => { test('should compare binary', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.dat': Buffer.from([1,2,3,4]), 'a.spec.js-snapshots/snapshot.dat': Buffer.from([1,2,3,4]),
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect(Buffer.from([1,2,3,4])).toMatchSnapshot('snapshot.dat'); 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}) => { test('should compare PNG images', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': 'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'), Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64')).toMatchSnapshot('snapshot.png'); 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) => { test('should compare different PNG images', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': 'a.spec.js-snapshots/snapshot.png':
Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'), Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==', 'base64'),
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png'); 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 expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png')); const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/snapshot.png': expected, 'a.spec.js-snapshots/snapshot.png': expected,
'a.spec.js-snapshots/snapshot2.png': expected, 'a.spec.js-snapshots/snapshot2.png': expected,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 }); 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 }); 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 expected = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-expected.png'));
const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png')); const actual = fs.readFileSync(path.join(__dirname, 'assets/screenshot-canvas-actual.png'));
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { projects: [ module.exports = { projects: [
{ expect: { toMatchSnapshot: { threshold: 0.2 } } }, { 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/snapshot.png': expected,
'a.spec.js-snapshots/snapshot2.png': expected, 'a.spec.js-snapshots/snapshot2.png': expected,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 }); expect(Buffer.from('${actual.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', { threshold: 0.3 });
expect(Buffer.from('${actual.toString('base64')}', 'base64')).not.toMatchSnapshot('snapshot.png'); 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}) => { test('should sanitize snapshot name', async ({runInlineTest}) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js-snapshots/-snapshot-.txt': `Hello world`, 'a.spec.js-snapshots/-snapshot-.txt': `Hello world`,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('../../snapshot!.txt'); 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) => { test('should write missing expectations with sanitized snapshot name', async ({runInlineTest}, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
...files,
'a.spec.js': ` 'a.spec.js': `
const { test } = pwt; const { test } = require('./helper');
test('is a test', ({}) => { test('is a test', ({}) => {
expect('Hello world').toMatchSnapshot('../../snapshot!.txt'); expect('Hello world').toMatchSnapshot('../../snapshot!.txt');
}); });

View 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',
]);
});

View File

@ -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) => { test('should report error and pending operations on timeout', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'a.test.ts': ` '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'),`); 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('should report click error on sigint', async ({ runInlineTest }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows'); 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');`); 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) => { test('should work with video: retain-on-failure', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
@ -339,36 +383,3 @@ test('should work with video size', async ({ runInlineTest }, testInfo) => {
expect(videoPlayer.videoWidth).toBe(220); expect(videoPlayer.videoWidth).toBe(220);
expect(videoPlayer.videoHeight).toBe(110); 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);
});

View File

@ -114,7 +114,6 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
]); ]);
}); });
test('should work without a file extension', async ({ runInlineTest }) => { test('should work without a file extension', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'reporter.ts': smallReporterJS, 'reporter.ts': smallReporterJS,
@ -162,6 +161,9 @@ test('should load reporter from node_modules', async ({ runInlineTest }) => {
test('should report expect steps', async ({ runInlineTest }) => { test('should report expect steps', async ({ runInlineTest }) => {
const expectReporterJS = ` const expectReporterJS = `
class Reporter { class Reporter {
onStdOut(chunk) {
process.stdout.write(chunk);
}
onStepBegin(test, result, step) { onStepBegin(test, result, step) {
const copy = { ...step, startTime: undefined, duration: undefined }; const copy = { ...step, startTime: undefined, duration: undefined };
console.log('%%%% begin', JSON.stringify(copy)); console.log('%%%% begin', JSON.stringify(copy));
@ -232,6 +234,15 @@ test('should report expect steps', async ({ runInlineTest }) => {
test('should report api steps', async ({ runInlineTest }) => { test('should report api steps', async ({ runInlineTest }) => {
const expectReporterJS = ` const expectReporterJS = `
class Reporter { 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) { onStepBegin(test, result, step) {
const copy = { ...step, startTime: undefined, duration: undefined }; const copy = { ...step, startTime: undefined, duration: undefined };
console.log('%%%% begin', JSON.stringify(copy)); console.log('%%%% begin', JSON.stringify(copy));
@ -259,11 +270,31 @@ test('should report api steps', async ({ runInlineTest }) => {
await page.setContent('<button></button>'); await page.setContent('<button></button>');
await page.click('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 }); }, { reporter: '', workers: 1 });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([ expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([
`%%%% test begin pass`,
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`, `%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`, `%% 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\"}`, `%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`, `%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\"}`, `%% 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 }) => { test('should report api step failure', async ({ runInlineTest }) => {
const expectReporterJS = ` const expectReporterJS = `
class Reporter { class Reporter {
onStdOut(chunk) {
process.stdout.write(chunk);
}
onStepBegin(test, result, step) { onStepBegin(test, result, step) {
const copy = { ...step, startTime: undefined, duration: undefined }; const copy = { ...step, startTime: undefined, duration: undefined };
console.log('%%%% begin', JSON.stringify(copy)); 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 }) => { test('should report test.step', async ({ runInlineTest }) => {
const expectReporterJS = ` const expectReporterJS = `
class Reporter { class Reporter {
onStdOut(chunk) {
process.stdout.write(chunk);
}
onStepBegin(test, result, step) { onStepBegin(test, result, step) {
const copy = { ...step, startTime: undefined, duration: undefined }; const copy = { ...step, startTime: undefined, duration: undefined };
console.log('%%%% begin', JSON.stringify(copy)); console.log('%%%% begin', JSON.stringify(copy));

View File

@ -68,7 +68,7 @@ test('should retry timeout', async ({ runInlineTest }) => {
await new Promise(f => setTimeout(f, 10000)); await new Promise(f => setTimeout(f, 10000));
}); });
` `
}, { timeout: 100, retries: 2 }); }, { timeout: 1000, retries: 2 });
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
expect(passed).toBe(0); expect(passed).toBe(0);
expect(failed).toBe(1); expect(failed).toBe(1);

View File

@ -88,6 +88,7 @@ test('should include the project name', async ({ runInlineTest }) => {
'helper.ts': ` 'helper.ts': `
export const test = pwt.test.extend({ export const test = pwt.test.extend({
auto: [ async ({}, run, testInfo) => { auto: [ async ({}, run, testInfo) => {
testInfo.snapshotSuffix = '';
await run(); await run();
}, { auto: true } ] }, { auto: true } ]
}); });

58
types/test.d.ts vendored
View File

@ -2297,6 +2297,35 @@ export interface PlaywrightWorkerOptions {
* [fixtures.channel](https://playwright.dev/docs/api/class-fixtures#fixtures-channel) take priority over this. * [fixtures.channel](https://playwright.dev/docs/api/class-fixtures#fixtures-channel) take priority over this.
*/ */
launchOptions: LaunchOptions; 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'; 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 { 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. * Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
*/ */

View File

@ -284,14 +284,14 @@ export interface PlaywrightWorkerOptions {
headless: boolean | undefined; headless: boolean | undefined;
channel: BrowserChannel | undefined; channel: BrowserChannel | undefined;
launchOptions: LaunchOptions; 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 type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-video';
export interface PlaywrightTestOptions { 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; acceptDownloads: boolean | undefined;
bypassCSP: boolean | undefined; bypassCSP: boolean | undefined;
colorScheme: ColorScheme | undefined; colorScheme: ColorScheme | undefined;