test: migrate to upstream fixtures (#9835)

This commit is contained in:
Pavel Feldman 2021-10-28 07:31:30 -08:00 коммит произвёл GitHub
Родитель 9af5aaabbb
Коммит 2e4722d460
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 231 добавлений и 485 удалений

Просмотреть файл

@ -997,17 +997,17 @@ export type BrowserContextNewPageResult = {
page: PageChannel,
};
export type BrowserContextSetDefaultNavigationTimeoutNoReplyParams = {
timeout: number,
timeout?: number,
};
export type BrowserContextSetDefaultNavigationTimeoutNoReplyOptions = {
timeout?: number,
};
export type BrowserContextSetDefaultNavigationTimeoutNoReplyResult = void;
export type BrowserContextSetDefaultTimeoutNoReplyParams = {
timeout: number,
timeout?: number,
};
export type BrowserContextSetDefaultTimeoutNoReplyOptions = {
timeout?: number,
};
export type BrowserContextSetDefaultTimeoutNoReplyResult = void;
export type BrowserContextSetExtraHTTPHeadersParams = {
@ -1253,17 +1253,17 @@ export type PageWorkerEvent = {
worker: WorkerChannel,
};
export type PageSetDefaultNavigationTimeoutNoReplyParams = {
timeout: number,
timeout?: number,
};
export type PageSetDefaultNavigationTimeoutNoReplyOptions = {
timeout?: number,
};
export type PageSetDefaultNavigationTimeoutNoReplyResult = void;
export type PageSetDefaultTimeoutNoReplyParams = {
timeout: number,
timeout?: number,
};
export type PageSetDefaultTimeoutNoReplyOptions = {
timeout?: number,
};
export type PageSetDefaultTimeoutNoReplyResult = void;
export type PageSetFileChooserInterceptedNoReplyParams = {

Просмотреть файл

@ -733,11 +733,11 @@ BrowserContext:
setDefaultNavigationTimeoutNoReply:
parameters:
timeout: number
timeout: number?
setDefaultTimeoutNoReply:
parameters:
timeout: number
timeout: number?
setExtraHTTPHeaders:
parameters:
@ -897,11 +897,11 @@ Page:
setDefaultNavigationTimeoutNoReply:
parameters:
timeout: number
timeout: number?
setDefaultTimeoutNoReply:
parameters:
timeout: number
timeout: number?
setFileChooserInterceptedNoReply:
parameters:

Просмотреть файл

@ -455,10 +455,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
});
scheme.BrowserContextNewPageParams = tOptional(tObject({}));
scheme.BrowserContextSetDefaultNavigationTimeoutNoReplyParams = tObject({
timeout: tNumber,
timeout: tOptional(tNumber),
});
scheme.BrowserContextSetDefaultTimeoutNoReplyParams = tObject({
timeout: tNumber,
timeout: tOptional(tNumber),
});
scheme.BrowserContextSetExtraHTTPHeadersParams = tObject({
headers: tArray(tType('NameValue')),
@ -511,10 +511,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.BrowserContextTracingStopParams = tOptional(tObject({}));
scheme.BrowserContextHarExportParams = tOptional(tObject({}));
scheme.PageSetDefaultNavigationTimeoutNoReplyParams = tObject({
timeout: tNumber,
timeout: tOptional(tNumber),
});
scheme.PageSetDefaultTimeoutNoReplyParams = tObject({
timeout: tNumber,
timeout: tOptional(tNumber),
});
scheme.PageSetFileChooserInterceptedNoReplyParams = tObject({
intercepted: tBoolean,

Просмотреть файл

@ -204,11 +204,11 @@ export abstract class BrowserContext extends SdkObject {
await this._doClearPermissions();
}
setDefaultNavigationTimeout(timeout: number) {
setDefaultNavigationTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
setDefaultTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultTimeout(timeout);
}

Просмотреть файл

@ -260,11 +260,11 @@ export class Page extends SdkObject {
return this._frameManager.frames();
}
setDefaultNavigationTimeout(timeout: number) {
setDefaultNavigationTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
setDefaultTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultTimeout(timeout);
}

Просмотреть файл

@ -218,7 +218,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
const zipArtifact = skipCompress ? null : await this._exportZip(entries, state).catch(() => null);
return { artifact: zipArtifact, entries };
});
}) || { artifact: null, entries: [] };
}
private async _exportZip(entries: NameValue[], state: RecordingState): Promise<Artifact | null> {
@ -360,11 +360,13 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
});
}
private async _appendTraceOperation<T>(cb: () => Promise<T>): Promise<T> {
private async _appendTraceOperation<T>(cb: () => Promise<T>): Promise<T | undefined> {
// This method serializes all writes to the trace.
let error: Error | undefined;
let result: T | undefined;
this._writeChain = this._writeChain.then(async () => {
if (!this._context._browser.isConnected())
return;
try {
result = await cb();
} catch (e) {
@ -374,7 +376,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
await this._writeChain;
if (error)
throw error;
return result!;
return result;
}
}

Просмотреть файл

@ -22,27 +22,27 @@ const TIMEOUT = debugMode() ? 0 : DEFAULT_TIMEOUT;
export class TimeoutSettings {
private _parent: TimeoutSettings | undefined;
private _defaultTimeout: number | null = null;
private _defaultNavigationTimeout: number | null = null;
private _defaultTimeout: number | undefined;
private _defaultNavigationTimeout: number | undefined;
constructor(parent?: TimeoutSettings) {
this._parent = parent;
}
setDefaultTimeout(timeout: number) {
setDefaultTimeout(timeout: number | undefined) {
this._defaultTimeout = timeout;
}
setDefaultNavigationTimeout(timeout: number) {
setDefaultNavigationTimeout(timeout: number | undefined) {
this._defaultNavigationTimeout = timeout;
}
navigationTimeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
return options.timeout;
if (this._defaultNavigationTimeout !== null)
if (this._defaultNavigationTimeout !== undefined)
return this._defaultNavigationTimeout;
if (this._defaultTimeout !== null)
if (this._defaultTimeout !== undefined)
return this._defaultTimeout;
if (this._parent)
return this._parent.navigationTimeout(options);
@ -52,7 +52,7 @@ export class TimeoutSettings {
timeout(options: { timeout?: number }): number {
if (typeof options.timeout === 'number')
return options.timeout;
if (this._defaultTimeout !== null)
if (this._defaultTimeout !== undefined)
return this._defaultTimeout;
if (this._parent)
return this._parent.timeout(options);

Просмотреть файл

@ -16,7 +16,7 @@
/* eslint-disable no-console */
import { Command, Option } from 'commander';
import { Command } from 'commander';
import fs from 'fs';
import path from 'path';
import type { Config } from './types';
@ -49,7 +49,6 @@ export function addTestCommand(program: Command) {
command.option('--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`);
command.option('--headed', `Run tests in headed browsers (default: headless)`);
command.option('--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --maxFailures=1 --headed --workers=1" options`);
command.addOption(new Option('--reuse-context').hideHelp());
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "${tsConfig}"/"${jsConfig}"`);
command.option('--forbid-only', `Fail if test.only is called (default: false)`);
command.option('-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`);
@ -114,17 +113,15 @@ async function createLoader(opts: { [key: string]: any }): Promise<Loader> {
}
const overrides = overridesFromOptions(opts);
if (opts.headed || opts.debug || opts.reuseContext)
if (opts.headed || opts.debug)
overrides.use = { headless: false };
if (opts.debug || opts.reuseContext) {
if (opts.debug) {
overrides.maxFailures = 1;
overrides.timeout = 0;
overrides.workers = 1;
}
if (opts.debug)
process.env.PWDEBUG = '1';
if (opts.reuseContext)
process.env.PWTEST_REUSE_CONTEXT = '1';
const loader = new Loader(defaultConfig, overrides);

Просмотреть файл

@ -16,10 +16,10 @@
import * as fs from 'fs';
import * as path from 'path';
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType } from 'playwright-core';
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType, Video } from 'playwright-core';
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
import { rootTestType } from './testType';
import { assert, createGuid, removeFolders } from 'playwright-core/lib/utils/utils';
import { createGuid, removeFolders } from 'playwright-core/lib/utils/utils';
import { GridClient } from 'playwright-core/lib/grid/gridClient';
import { Browser } from 'playwright-core';
export { expect } from './expect';
@ -28,97 +28,15 @@ export const _baseTest: TestType<{}, {}> = rootTestType.test;
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions,
_setupContextOptionsAndArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
};
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
_browserType: BrowserType;
_browserOptions: LaunchOptions;
_artifactsDir: () => string,
_reuseBrowserContext: ReuseBrowserContextStorage,
_artifactsDir: () => string;
_snapshotSuffix: string;
};
export class ReuseBrowserContextStorage {
private _browserContext?: BrowserContext;
private _uniqueOrigins = new Set<string>();
private _options?: BrowserContextOptions;
private _pauseNavigationEventCollection = false;
isEnabled(): boolean {
return !!process.env.PWTEST_REUSE_CONTEXT;
}
async obtainContext(browser: Browser, newContextOptions: BrowserContextOptions): Promise<BrowserContext> {
if (!this._browserContext)
return await this._createNewContext(browser);
return await this._refurbishExistingContext(newContextOptions);
}
private async _createNewContext(browser: Browser): Promise<BrowserContext> {
this._browserContext = await browser.newContext();
this._options = (this._browserContext as any)._options;
this._browserContext.on('page', page => {
page.on('framenavigated', frame => {
if (this._pauseNavigationEventCollection)
return;
const origin = new URL(frame.url()).origin;
if (origin !== 'null') // 'chrome-error://chromewebdata/'
this._uniqueOrigins.add(origin);
});
page.on('crash', () => {
this._browserContext?.close().then(() => {});
this._browserContext = undefined;
});
});
return this._browserContext;
}
async _refurbishExistingContext(newContextOptions: BrowserContextOptions): Promise<BrowserContext> {
assert(this._browserContext);
const page = this._browserContext.pages().length > 0 ? this._browserContext.pages()[0] : await this._browserContext.newPage();
this._pauseNavigationEventCollection = true;
try {
const initialOrigin = new URL(page.url()).origin;
await page.route('**/*', route => route.fulfill({ body: `<html></html>`, contentType: 'text/html' }));
while (this._uniqueOrigins.size > 0) {
const nextOrigin = this._uniqueOrigins.has(initialOrigin) ? initialOrigin : this._uniqueOrigins.values().next().value;
this._uniqueOrigins.delete(nextOrigin);
await page.goto(nextOrigin);
await page.evaluate(() => window.localStorage.clear());
await page.evaluate(() => window.sessionStorage.clear());
}
await page.unroute('**/*');
await page.goto('about:blank');
await Promise.all(this._browserContext.pages().slice(1).map(page => page.close()));
await this._browserContext.clearCookies();
await this._applyNewContextOptions(page, newContextOptions);
} finally {
this._pauseNavigationEventCollection = false;
}
return this._browserContext;
}
private async _applyNewContextOptions(page: Page, newOptions: BrowserContextOptions) {
assert(this._options);
const currentViewport = page.viewportSize();
const newViewport = newOptions.viewport === undefined ? { width: 1280, height: 720 } : newOptions.viewport;
if (
(
currentViewport?.width !== newViewport?.width ||
currentViewport?.height !== newViewport?.height
) &&
(newViewport?.height && newViewport?.width)
)
await page.setViewportSize(newViewport);
this._options = newOptions;
}
async obtainPage(): Promise<Page> {
assert(this._browserContext);
if (this._browserContext.pages().length === 0)
return await this._browserContext.newPage();
return this._browserContext.pages()[0];
}
}
export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
defaultBrowserType: [ 'chromium', { scope: 'worker' } ],
browserName: [ ({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker' } ],
@ -251,8 +169,10 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
});
},
_setupContextOptionsAndArtifacts: [async ({ _browserType, _combinedContextOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => {
testInfo.snapshotSuffix = process.platform;
_snapshotSuffix: [process.platform, { scope: 'worker' }],
_setupContextOptionsAndArtifacts: [async ({ _snapshotSuffix, _browserType, _combinedContextOptions, _artifactsDir, trace, screenshot, actionTimeout, navigationTimeout }, use, testInfo) => {
testInfo.snapshotSuffix = _snapshotSuffix;
if (process.env.PWDEBUG)
testInfo.setTimeout(0);
@ -381,39 +301,55 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
}));
}, { auto: true }],
_reuseBrowserContext: [new ReuseBrowserContextStorage(), { scope: 'worker' }],
context: async ({ browser, video, _artifactsDir, _reuseBrowserContext, _combinedContextOptions }, use, testInfo) => {
const hook = hookType(testInfo);
if (hook)
throw new Error(`"context" and "page" fixtures are not supported in ${hook}. Use browser.newContext() instead.`);
if (_reuseBrowserContext.isEnabled()) {
const context = await _reuseBrowserContext.obtainContext(browser, _combinedContextOptions);
await use(context);
return;
}
_contextFactory: 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 videoOptions: BrowserContextOptions = captureVideo ? {
recordVideo: {
dir: _artifactsDir(),
size: typeof video === 'string' ? undefined : video.size,
}
} : {};
const context = await browser.newContext(videoOptions);
const contexts = new Map<BrowserContext, { pages: Page[] }>();
const allPages: Page[] = [];
context.on('page', page => allPages.push(page));
await use(context);
await use(async options => {
const hook = hookType(testInfo);
if (hook)
throw new Error(`"context" and "page" fixtures are not supported in ${hook}. Use browser.newContext() instead.`);
const videoOptions: BrowserContextOptions = captureVideo ? {
recordVideo: {
dir: _artifactsDir(),
size: typeof video === 'string' ? undefined : video.size,
}
} : {};
const context = await browser.newContext({ ...videoOptions, ...options });
const contextData: { pages: Page[] } = { pages: [] };
contexts.set(context, contextData);
context.on('page', page => contextData.pages.push(page));
return context;
});
const prependToError = testInfo.status === 'timedOut' ?
formatPendingCalls((context as any)._connection.pendingProtocolCalls()) : '';
await context.close();
formatPendingCalls((browser as any)._connection.pendingProtocolCalls()) : '';
await Promise.all([...contexts.keys()].map(async context => {
await context.close();
const testFailed = testInfo.status !== testInfo.expectedStatus;
const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
if (preserveVideo) {
const { pages } = contexts.get(context)!;
const videos = pages.map(p => p.video()).filter(Boolean) as Video[];
await Promise.all(videos.map(async v => {
try {
const videoPath = await v.path();
const savedPath = testInfo.outputPath(path.basename(videoPath));
await v.saveAs(savedPath);
testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
} catch (e) {
// Silent catch empty videos.
}
}));
}
}));
if (prependToError) {
if (!testInfo.error) {
testInfo.error = { value: prependToError };
@ -423,31 +359,13 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
testInfo.error.stack = prependToError + testInfo.error.stack;
}
}
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 => {
const v = page.video();
if (!v)
return;
try {
const videoPath = await v.path();
const savedPath = testInfo.outputPath(path.basename(videoPath));
await v.saveAs(savedPath);
testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
} catch (e) {
// Silent catch empty videos.
}
}));
}
},
page: async ({ context, _reuseBrowserContext }, use) => {
if (_reuseBrowserContext.isEnabled()) {
await use(await _reuseBrowserContext.obtainPage());
return;
}
context: async ({ _contextFactory }, use) => {
await use(await _contextFactory());
},
page: async ({ context }, use) => {
await use(await context.newPage());
},

Просмотреть файл

@ -14,28 +14,27 @@
* limitations under the License.
*/
import { baseTest } from '../config/baseTest';
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
import type { AndroidDevice, BrowserContext } from 'playwright-core';
import type { Fixtures, PlaywrightWorkerOptions } from '@playwright/test';
import { PageTestFixtures } from '../page/pageTest';
import { TestModeWorkerFixtures } from '../config/testModeFixtures';
import { browserTest } from '../config/browserTest';
export { expect } from '@playwright/test';
type AndroidWorkerFixtures = {
type AndroidWorkerFixtures = PageWorkerFixtures & {
androidDevice: AndroidDevice;
androidContext: BrowserContext;
};
export const androidFixtures: Fixtures<PageTestFixtures, AndroidWorkerFixtures & { androidContext: BrowserContext }, {}, PlaywrightWorkerOptions & TestModeWorkerFixtures> = {
androidDevice: [ async ({ playwright }, run) => {
export const androidTest = baseTest.extend<PageTestFixtures, AndroidWorkerFixtures>({
androidDevice: [async ({ playwright }, run) => {
const device = (await playwright._android.devices())[0];
await device.shell('am force-stop org.chromium.webview_shell');
await device.shell('am force-stop com.android.chrome');
device.setDefaultTimeout(90000);
await run(device);
await device.close();
}, { scope: 'worker' } ],
}, { scope: 'worker' }],
browserVersion: async ({ androidDevice }, run) => {
browserVersion: [async ({ androidDevice }, run) => {
const browserVersion = (await androidDevice.shell('dumpsys package com.android.chrome'))
.toString('utf8')
.split('\n')
@ -43,21 +42,21 @@ export const androidFixtures: Fixtures<PageTestFixtures, AndroidWorkerFixtures &
.trim()
.split('=')[1];
await run(browserVersion);
},
}, { scope: 'worker' }],
browserMajorVersion: async ({ browserVersion }, run) => {
browserMajorVersion: [async ({ browserVersion }, run) => {
await run(Number(browserVersion.split('.')[0]));
},
}, { scope: 'worker' }],
isAndroid: true,
isElectron: false,
isAndroid: [true, { scope: 'worker' }],
isElectron: [false, { scope: 'worker' }],
androidContext: [ async ({ androidDevice }, run) => {
androidContext: [async ({ androidDevice }, run) => {
const context = await androidDevice.launchBrowser();
const [ page ] = context.pages();
const [page] = context.pages();
await page.goto('data:text/html,Default page');
await run(context);
}, { scope: 'worker' } ],
}, { scope: 'worker' }],
page: async ({ androidContext }, run) => {
// Retain default page, otherwise Clank will re-create it.
@ -66,6 +65,4 @@ export const androidFixtures: Fixtures<PageTestFixtures, AndroidWorkerFixtures &
const page = await androidContext.newPage();
await run(page);
},
};
export const androidTest = browserTest.extend<PageTestFixtures, AndroidWorkerFixtures>(androidFixtures as any);
});

Просмотреть файл

@ -134,10 +134,7 @@ it('close() should abort waitForEvent', async ({ browser }) => {
it('close() should be callable twice', async ({ browser }) => {
const context = await browser.newContext();
await Promise.all([
context.close(),
context.close(),
]);
await context.close();
await context.close();
});

Просмотреть файл

@ -42,9 +42,8 @@ it.describe('device', () => {
await context.close();
});
it('should scroll to click', async ({ browser, server, contextOptions }) => {
it('should scroll to click', async ({ browser, server }) => {
const context = await browser.newContext({
...contextOptions,
viewport: {
width: 400,
height: 400,
@ -60,7 +59,7 @@ it.describe('device', () => {
await context.close();
});
it('should scroll twice when emulated', async ({ server, contextFactory, playwright }) => {
it('should scroll twice when emulated', async ({ contextFactory, playwright }) => {
const device = playwright.devices['iPhone 6'];
const context = await contextFactory(device);
const page = await context.newPage();
@ -105,7 +104,7 @@ it.describe('device', () => {
await context.close();
});
it('should emulate viewport and screen size', async ({ server, contextFactory, playwright }) => {
it('should emulate viewport and screen size', async ({ contextFactory, playwright }) => {
const device = playwright.devices['iPhone 12'];
const context = await contextFactory(device);
const page = await context.newPage();
@ -124,7 +123,7 @@ it.describe('device', () => {
await context.close();
});
it('should emulate viewport without screen size', async ({ server, contextFactory, playwright }) => {
it('should emulate viewport without screen size', async ({ contextFactory, playwright }) => {
const device = playwright.devices['iPhone 6'];
const context = await contextFactory(device);
const page = await context.newPage();

Просмотреть файл

@ -47,7 +47,7 @@ it('should throw for missing global proxy on Chromium Windows', async ({ browser
}
});
it('should work when passing the proxy only on the context level', async ({ browserName, platform, browserType, contextOptions, server, proxyServer }) => {
it('should work when passing the proxy only on the context level', async ({ browserName, platform, browserType, server, proxyServer }) => {
// Currently an upstream bug in the network stack of Chromium which leads that
// the wrong proxy gets used in the BrowserContext.
it.fixme(browserName === 'chromium' && platform === 'win32');
@ -59,7 +59,6 @@ it('should work when passing the proxy only on the context level', async ({ brow
proxy: undefined,
});
const context = await browser.newContext({
...contextOptions,
proxy: { server: `localhost:${proxyServer.PORT}` }
});

Просмотреть файл

@ -16,10 +16,9 @@
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import * as path from 'path';
import { test as pageTest } from '../page/pageTest';
import { androidFixtures } from '../android/androidTest';
import { ServerWorkerOptions } from './serverFixtures';
import { playwrightFixtures } from './browserTest';
process.env.PWPAGE_IMPL = 'android';
const outputDir = path.join(__dirname, '..', '..', 'test-results');
const testDir = path.join(__dirname, '..');
@ -65,7 +64,6 @@ config.projects.push({
browserName: 'chromium',
},
testDir: path.join(testDir, 'page'),
define: { test: pageTest, fixtures: { ...playwrightFixtures, ...androidFixtures } },
metadata,
});

Просмотреть файл

@ -14,16 +14,24 @@
* limitations under the License.
*/
import { _baseTest } from '@playwright/test';
import { test } from '@playwright/test';
import { commonFixtures, CommonFixtures } from './commonFixtures';
import { serverFixtures, ServerFixtures, ServerWorkerOptions } from './serverFixtures';
import { coverageFixtures, CoverageWorkerOptions } from './coverageFixtures';
import { platformFixtures, PlatformWorkerFixtures } from './platformFixtures';
import { testModeFixtures, TestModeWorkerFixtures } from './testModeFixtures';
export const baseTest = _baseTest
.extend<{}, CoverageWorkerOptions>(coverageFixtures)
export type BaseTestWorkerFixtures = {
_snapshotSuffix: string;
};
export const baseTest = test
.extend<{}, CoverageWorkerOptions>(coverageFixtures as any)
.extend<{}, PlatformWorkerFixtures>(platformFixtures)
.extend<{}, TestModeWorkerFixtures>(testModeFixtures)
.extend<{}, TestModeWorkerFixtures>(testModeFixtures as any)
.extend<CommonFixtures>(commonFixtures)
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures as any);
.extend<ServerFixtures, ServerWorkerOptions>(serverFixtures as any)
.extend<{}, BaseTestWorkerFixtures>({
_snapshotSuffix: ['', { scope: 'worker' }],
});

Просмотреть файл

@ -14,70 +14,49 @@
* limitations under the License.
*/
import type { Fixtures, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions, Page } from 'playwright-core';
import { removeFolders } from 'playwright-core/lib/utils/utils';
import { browserOptionsWorkerFixture, browserTypeWorkerFixture, browserWorkerFixture, ReuseBrowserContextStorage } from '../../packages/playwright-test/lib/index';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { RemoteServer, RemoteServerOptions } from './remoteServer';
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
import * as path from 'path';
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
import { removeFolders } from 'playwright-core/lib/utils/utils';
import { baseTest } from './baseTest';
import { CommonFixtures } from './commonFixtures';
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
import { DefaultTestMode, DriverTestMode, ServiceTestMode } from './testMode';
import { TestModeWorkerFixtures } from './testModeFixtures';
import { RemoteServer, RemoteServerOptions } from './remoteServer';
export type PlaywrightWorkerFixtures = {
playwright: typeof import('playwright-core');
_browserType: BrowserType;
_browserOptions: LaunchOptions;
browserType: BrowserType;
browser: Browser;
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
browserVersion: string;
_reuseBrowserContext: ReuseBrowserContextStorage;
toImpl: (rpcObject: any) => any;
browserMajorVersion: number;
browserType: BrowserType;
isAndroid: boolean;
isElectron: boolean;
};
type PlaywrightTestFixtures = {
type BrowserTestTestFixtures = PageTestFixtures & {
createUserDataDir: () => Promise<string>;
launchPersistent: (options?: Parameters<BrowserType['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>;
startRemoteServer: (options?: RemoteServerOptions) => Promise<RemoteServer>;
contextOptions: BrowserContextOptions;
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
context: BrowserContext;
page: Page;
};
export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTestFixtures, PlaywrightWorkerOptions & PlaywrightWorkerFixtures, CommonFixtures, TestModeWorkerFixtures> = {
hasTouch: undefined,
playwright: [ async ({ mode }, run) => {
const testMode = {
default: new DefaultTestMode(),
service: new ServiceTestMode(),
driver: new DriverTestMode(),
}[mode];
require('playwright-core/lib/utils/utils').setUnderTest();
const playwright = await testMode.setup();
await run(playwright);
await testMode.teardown();
}, { scope: 'worker' } ],
toImpl: [ async ({ playwright }, run) => run((playwright as any)._toImpl), { scope: 'worker' } ],
_browserType: [browserTypeWorkerFixture, { scope: 'worker' } ],
_browserOptions: [browserOptionsWorkerFixture, { scope: 'worker' } ],
launchOptions: [ {}, { scope: 'worker' } ],
browserType: [async ({ _browserType }, use) => use(_browserType), { scope: 'worker' } ],
browser: [browserWorkerFixture, { scope: 'worker' } ],
const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>({
browserVersion: [async ({ browser }, run) => {
await run(browser.version());
}, { scope: 'worker' } ],
_reuseBrowserContext: [new ReuseBrowserContextStorage(), { scope: 'worker' }],
browserType: [async ({ _browserType }: any, run) => {
await run(_browserType);
}, { scope: 'worker' } ],
browserMajorVersion: [async ({ browserVersion }, run) => {
await run(Number(browserVersion.split('.')[0]));
}, { scope: 'worker' } ],
isAndroid: [false, { scope: 'worker' } ],
isElectron: [false, { scope: 'worker' } ],
contextFactory: async ({ _contextFactory }: any, run) => {
await run(_contextFactory);
},
createUserDataDir: async ({}, run) => {
const dirs: string[] = [];
@ -99,7 +78,7 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
let persistentContext: BrowserContext | undefined;
await run(async options => {
if (persistentContext)
throw new Error('can only launch one persitent context');
throw new Error('can only launch one persistent context');
const userDataDir = await createUserDataDir();
persistentContext = await browserType.launchPersistentContext(userDataDir, { ...options });
const page = persistentContext.pages()[0];
@ -121,90 +100,8 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
if (remoteServer)
await remoteServer.close();
},
});
contextOptions: async ({ video, hasTouch }, run, testInfo) => {
const debugName = path.relative(testInfo.project.outputDir, testInfo.outputDir).replace(/[\/\\]/g, '-');
const contextOptions = {
recordVideo: video === 'on' ? { dir: testInfo.outputPath('') } : undefined,
_debugName: debugName,
hasTouch,
} as BrowserContextOptions;
await run(contextOptions);
},
contextFactory: async ({ browser, contextOptions, trace }, run, testInfo) => {
const contexts = new Map<BrowserContext, { closed: boolean }>();
await run(async options => {
const context = await browser.newContext({ ...contextOptions, ...options });
contexts.set(context, { closed: false });
context.on('close', () => contexts.get(context).closed = true);
if (trace === 'on')
await context.tracing.start({ screenshots: true, snapshots: true, sources: true } as any);
(context as any)._instrumentation.addListener({
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, userData: any) => {
if (apiCall.startsWith('expect.'))
return { userObject: null };
const testInfoImpl = testInfo as any;
const step = testInfoImpl._addStep({
location: stackTrace?.frames[0],
category: 'pw:api',
title: apiCall,
canHaveChildren: false,
forceNoParent: false
});
userData.userObject = step;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete(error);
},
});
return context;
});
await Promise.all([...contexts.keys()].map(async context => {
const videos = context.pages().map(p => p.video()).filter(Boolean);
if (trace === 'on' && !contexts.get(context)!.closed) {
const tracePath = testInfo.outputPath('trace.zip');
await context.tracing.stop({ path: tracePath });
testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
}
await context.close();
for (const v of videos) {
const videoPath = await v.path().catch(() => null);
if (!videoPath)
continue;
const savedPath = testInfo.outputPath(path.basename(videoPath));
await v.saveAs(savedPath);
testInfo.attachments.push({ name: 'video', path: savedPath, contentType: 'video/webm' });
}
}));
},
context: async ({ contextFactory, browser, _reuseBrowserContext, contextOptions }, run) => {
if (_reuseBrowserContext.isEnabled()) {
const context = await _reuseBrowserContext.obtainContext(browser, contextOptions);
await run(context);
return;
}
await run(await contextFactory());
},
page: async ({ context, _reuseBrowserContext }, run) => {
if (_reuseBrowserContext.isEnabled()) {
await run(await _reuseBrowserContext.obtainPage());
return;
}
await run(await context.newPage());
},
browserName: [ 'chromium' , { scope: 'worker' } ],
headless: [ undefined, { scope: 'worker' } ],
channel: [ undefined, { scope: 'worker' } ],
video: [ 'off', { scope: 'worker' } ],
trace: [ 'off', { scope: 'worker' } ],
};
const test = baseTest.extend<PlaywrightTestOptions & PlaywrightTestFixtures, PlaywrightWorkerOptions & PlaywrightWorkerFixtures>(playwrightFixtures);
export const playwrightTest = test;
export const browserTest = test;
export const contextTest = test;

Просмотреть файл

@ -16,8 +16,6 @@
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import * as path from 'path';
import { playwrightFixtures } from './browserTest';
import { test as pageTest } from '../page/pageTest';
import { TestModeWorkerFixtures } from './testModeFixtures';
import { CoverageWorkerOptions } from './coverageFixtures';
@ -32,15 +30,6 @@ const getExecutablePath = (browserName: BrowserName) => {
return process.env.WKPATH;
};
const pageFixtures = {
...playwrightFixtures,
browserMajorVersion: async ({ browserVersion }, run) => {
await run(Number(browserVersion.split('.')[0]));
},
isAndroid: false,
isElectron: false,
};
const mode = (process.env.PWTEST_MODE || 'default') as ('default' | 'driver' | 'service');
const headed = !!process.env.HEADFUL;
const channel = process.env.PWTEST_CHANNEL as any;
@ -95,7 +84,6 @@ for (const browserName of browserNames) {
trace: trace ? 'on' : undefined,
coverageName: browserName,
},
define: { test: pageTest, fixtures: pageFixtures },
metadata: {
platform: process.platform,
docker: !!process.env.INSIDE_DOCKER,

Просмотреть файл

@ -16,11 +16,10 @@
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import * as path from 'path';
import { electronFixtures } from '../electron/electronTest';
import { test as pageTest } from '../page/pageTest';
import { playwrightFixtures } from './browserTest';
import { CoverageWorkerOptions } from './coverageFixtures';
process.env.PWPAGE_IMPL = 'electron';
const outputDir = path.join(__dirname, '..', '..', 'test-results');
const testDir = path.join(__dirname, '..');
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions> = {
@ -65,7 +64,6 @@ config.projects.push({
coverageName: 'electron',
},
testDir: path.join(testDir, 'page'),
define: { test: pageTest, fixtures: { ...playwrightFixtures, ...electronFixtures } },
metadata,
});

Просмотреть файл

@ -14,12 +14,10 @@
* limitations under the License.
*/
import { ElectronApplication, Page } from 'playwright-core';
import type { Fixtures, PlaywrightWorkerOptions } from '@playwright/test';
import { baseTest } from '../config/baseTest';
import * as path from 'path';
import { PageTestFixtures } from '../page/pageTest';
import { TestModeWorkerFixtures } from '../config/testModeFixtures';
import { browserTest } from '../config/browserTest';
import { ElectronApplication, Page } from 'playwright-core';
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
export { expect } from '@playwright/test';
type ElectronTestFixtures = PageTestFixtures & {
@ -28,11 +26,12 @@ type ElectronTestFixtures = PageTestFixtures & {
};
const electronVersion = require('electron/package.json').version;
export const electronFixtures: Fixtures<ElectronTestFixtures, {}, {}, PlaywrightWorkerOptions & TestModeWorkerFixtures> = {
browserVersion: electronVersion,
browserMajorVersion: Number(electronVersion.split('.')[0]),
isAndroid: false,
isElectron: true,
export const electronTest = baseTest.extend<ElectronTestFixtures, PageWorkerFixtures>({
browserVersion: [electronVersion, { scope: 'worker' }],
browserMajorVersion: [Number(electronVersion.split('.')[0]), { scope: 'worker' }],
isAndroid: [false, { scope: 'worker' }],
isElectron: [true, { scope: 'worker' }],
electronApp: async ({ playwright }, run) => {
// This env prevents 'Electron Security Policy' console message.
@ -70,6 +69,4 @@ export const electronFixtures: Fixtures<ElectronTestFixtures, {}, {}, Playwright
page: async ({ newWindow }, run) => {
await run(await newWindow());
},
};
export const electronTest = browserTest.extend<ElectronTestFixtures>(electronFixtures as any);
});

Просмотреть файл

@ -168,8 +168,8 @@ it('should set playwright as user-agent', async ({ playwright, server }) => {
expect(serverRequest.headers['user-agent']).toBe('Playwright/' + getPlaywrightVersion());
});
it('should be able to construct with context options', async ({ playwright, server, contextOptions }) => {
const request = await playwright.request.newContext(contextOptions);
it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
const request = await playwright.request.newContext((browserType as any)._defaultContextOptions);
const response = await request.get(server.EMPTY_PAGE);
expect(response.ok()).toBeTruthy();
});

Просмотреть файл

@ -244,7 +244,8 @@ it('should work when page calls history API in beforeunload', async ({ page, ser
expect(response.status()).toBe(200);
});
it('should fail when navigating to bad url', async ({ page, browserName }) => {
it('should fail when navigating to bad url', async ({ mode, page, browserName }) => {
it.fixme(mode === 'service', 'baseURL is inherited from webServer in config');
let error = null;
await page.goto('asdfasdf').catch(e => error = e);
if (browserName === 'chromium' || browserName === 'webkit')
@ -347,6 +348,8 @@ it('should fail when exceeding default maximum timeout', async ({ page, server,
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
let error = null;
// Undo what harness did.
page.context().setDefaultNavigationTimeout(undefined);
page.context().setDefaultTimeout(2);
page.setDefaultTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
@ -361,6 +364,8 @@ it('should fail when exceeding browser context timeout', async ({ page, server,
// Hang for request to the empty.html
server.setRoute('/empty.html', (req, res) => { });
let error = null;
// Undo what harness did.
page.context().setDefaultNavigationTimeout(undefined);
page.context().setDefaultTimeout(2);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
expect(error.message).toContain('page.goto: Timeout 2ms exceeded.');

Просмотреть файл

@ -14,26 +14,21 @@
* limitations under the License.
*/
import { baseTest } from '../config/baseTest';
import type { Page, ViewportSize } from 'playwright-core';
import { VideoMode } from '@playwright/test';
import { TestType } from '@playwright/test';
import { PlatformWorkerFixtures } from '../config/platformFixtures';
import { TestModeWorkerFixtures } from '../config/testModeFixtures';
import { androidTest } from '../android/androidTest';
import { browserTest } from '../config/browserTest';
import { electronTest } from '../electron/electronTest';
import { PageTestFixtures, PageWorkerFixtures } from './pageTestApi';
import { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
export { expect } from '@playwright/test';
// Page test does not guarantee an isolated context, just a new page (because Android).
export type PageTestFixtures = {
browserVersion: string;
browserMajorVersion: number;
page: Page;
isAndroid: boolean;
isElectron: boolean;
};
let impl: TestType<PageTestFixtures & ServerFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & ServerWorkerOptions> = browserTest;
export type PageWorkerFixtures = {
headless: boolean,
channel: string,
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
video: VideoMode | { mode: VideoMode, size: ViewportSize };
browserName: 'chromium' | 'firefox' | 'webkit',
};
if (process.env.PWPAGE_IMPL === 'android')
impl = androidTest;
if (process.env.PWPAGE_IMPL === 'electron')
impl = electronTest;
export const test = baseTest.declare<PageTestFixtures, PageWorkerFixtures>();
export const test = impl;

36
tests/page/pageTestApi.ts Normal file
Просмотреть файл

@ -0,0 +1,36 @@
/**
* 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 type { Page, ViewportSize } from 'playwright-core';
import { VideoMode } from '@playwright/test';
export { expect } from '@playwright/test';
// Page test does not guarantee an isolated context, just a new page (because Android).
export type PageTestFixtures = {
page: Page;
};
export type PageWorkerFixtures = {
headless: boolean;
channel: string;
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | /** deprecated */ 'retry-with-trace';
video: VideoMode | { mode: VideoMode, size: ViewportSize };
browserName: 'chromium' | 'firefox' | 'webkit';
browserVersion: string;
browserMajorVersion: number;
isAndroid: boolean;
isElectron: boolean;
};

Просмотреть файл

@ -456,83 +456,3 @@ test('should work with video size', async ({ runInlineTest }, testInfo) => {
expect(videoPlayer.videoWidth).toBe(220);
expect(videoPlayer.videoHeight).toBe(110);
});
test('should be able to re-use the context when debug mode is used', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test.use({
colorScheme: 'light',
viewport: {
width: 1920,
height: 1080,
},
})
const host1 = 'http://host1.com/foobar';
test.beforeEach(async({page, context}) => {
context.route(host1, route => route.fulfill({body: '<html></html>', contentType: 'text/html'}, {times: 1}));
console.log(page._guid + '|');
console.log(context._guid + '|');
})
test('initial setup', async ({ page }) => {
await page.goto(host1);
expect(await page.evaluate(() => window.localStorage.getItem('foobar'))).toBe(null);
await page.evaluate(() => window.localStorage.setItem('foobar', 'bar'));
expect(page.viewportSize()).toStrictEqual({
width: 1920,
height: 1080,
});
});
test('second run after persistent data has changed', async ({ page }) => {
await page.goto(host1);
expect(await page.evaluate(() => window.localStorage.getItem('foobar'))).toBe(null);
await page.evaluate(() => window.localStorage.setItem('foobar', 'bar'));
expect(page.viewportSize()).toStrictEqual({
width: 1920,
height: 1080,
});
});
test.describe('inside a describe block', () => {
test.use({
colorScheme: 'dark',
viewport: {
width: 1000,
height: 500,
},
});
test('using different options', async ({ page }) => {
await page.goto(host1);
expect(await page.evaluate(() => window.localStorage.getItem('foobar'))).toBe(null);
expect(page.viewportSize()).toStrictEqual({
width: 1000,
height: 500,
});
});
});
test('after the describe block', async ({ page }) => {
await page.goto(host1);
expect(await page.evaluate(() => window.localStorage.getItem('foobar'))).toBe(null);
expect(page.viewportSize()).toStrictEqual({
width: 1920,
height: 1080,
});
});
`
}, { '--reuse-context': true });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(4);
const pageIds = result.output.match(/page@(.*)\|/g);
const browserContextIds = result.output.match(/browser-context@(.*)\|/g);
expect(pageIds.length).toBe(4);
expect(new Set(pageIds).size).toBe(1);
expect(browserContextIds.length).toBe(4);
expect(new Set(browserContextIds).size).toBe(1);
});

Просмотреть файл

@ -530,12 +530,11 @@ it.describe('screencast', () => {
}
});
it('should emulate an iphone', async ({ contextFactory, playwright, contextOptions, browserName }, testInfo) => {
it('should emulate an iphone', async ({ contextFactory, playwright, browserName }, testInfo) => {
it.skip(browserName === 'firefox', 'isMobile is not supported in Firefox');
const device = playwright.devices['iPhone 6'];
const context = await contextFactory({
...contextOptions,
...device,
recordVideo: {
dir: testInfo.outputPath(''),
@ -552,11 +551,10 @@ it.describe('screencast', () => {
expect(videoPlayer.videoHeight).toBe(666);
});
it('should throw on browser close', async ({ browserType, contextOptions }, testInfo) => {
it('should throw on browser close', async ({ browserType }, testInfo) => {
const size = { width: 320, height: 240 };
const browser = await browserType.launch();
const context = await browser.newContext({
...contextOptions,
recordVideo: {
dir: testInfo.outputPath(''),
size,
@ -573,12 +571,11 @@ it.describe('screencast', () => {
expect(saveResult.message).toContain('browser has been closed');
});
it('should throw if browser dies', async ({ browserType, contextOptions }, testInfo) => {
it('should throw if browser dies', async ({ browserType }, testInfo) => {
const size = { width: 320, height: 240 };
const browser = await browserType.launch();
const context = await browser.newContext({
...contextOptions,
recordVideo: {
dir: testInfo.outputPath(''),
size,
@ -595,13 +592,12 @@ it.describe('screencast', () => {
expect(saveResult.message).toContain('rowser has been closed');
});
it('should wait for video to finish if page was closed', async ({ browserType, contextOptions }, testInfo) => {
it('should wait for video to finish if page was closed', async ({ browserType }, testInfo) => {
const size = { width: 320, height: 240 };
const browser = await browserType.launch();
const videoDir = testInfo.outputPath('');
const context = await browser.newContext({
...contextOptions,
recordVideo: {
dir: videoDir,
size,
@ -622,7 +618,7 @@ it.describe('screencast', () => {
expect(videoPlayer.videoHeight).toBe(240);
});
it('should not create video for internal pages', async ({ browser, browserName, contextOptions, server }, testInfo) => {
it('should not create video for internal pages', async ({ browser, server }, testInfo) => {
it.fixme(true, 'https://github.com/microsoft/playwright/issues/6743');
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', 'name=value');
@ -631,7 +627,6 @@ it.describe('screencast', () => {
const videoDir = testInfo.outputPath('');
const context = await browser.newContext({
...contextOptions,
recordVideo: {
dir: videoDir
}