feat(test-runner): add reuse context mode to share a single context between tests (#9115)
This commit is contained in:
parent
2cf3448b6b
commit
e674d873a3
3
.github/workflows/tests_primary.yml
vendored
3
.github/workflows/tests_primary.yml
vendored
|
|
@ -66,3 +66,6 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: node lib/cli/cli install --with-deps
|
||||
- run: npm run ttest
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- run: xvfb-run npm run ttest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { Command, Option } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { Config } from './types';
|
||||
|
|
@ -45,6 +45,7 @@ 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: ".*")`);
|
||||
|
|
@ -96,14 +97,18 @@ async function createLoader(opts: { [key: string]: any }): Promise<Loader> {
|
|||
}
|
||||
|
||||
const overrides = overridesFromOptions(opts);
|
||||
if (opts.headed || opts.debug)
|
||||
if (opts.headed || opts.debug || opts.reuseContext)
|
||||
overrides.use = { headless: false };
|
||||
if (opts.debug) {
|
||||
if (opts.debug || opts.reuseContext) {
|
||||
overrides.maxFailures = 1;
|
||||
overrides.timeout = 0;
|
||||
overrides.workers = 1;
|
||||
process.env.PWDEBUG = '1';
|
||||
}
|
||||
if (opts.debug)
|
||||
process.env.PWDEBUG = '1';
|
||||
if (opts.reuseContext)
|
||||
process.env.PWTEST_REUSE_CONTEXT = '1';
|
||||
|
||||
const loader = new Loader(defaultConfig, overrides);
|
||||
|
||||
async function loadConfig(configFile: string) {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import * as path from 'path';
|
|||
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType } from '../../types/types';
|
||||
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../../types/test';
|
||||
import { rootTestType } from './testType';
|
||||
import { createGuid, removeFolders } from '../utils/utils';
|
||||
import { assert, createGuid, removeFolders } from '../utils/utils';
|
||||
import { GridClient } from '../grid/gridClient';
|
||||
import { Browser } from '../..';
|
||||
export { expect } from './expect';
|
||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||
|
||||
|
|
@ -31,8 +32,83 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
|||
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
_browserType: BrowserType;
|
||||
_artifactsDir: () => string,
|
||||
_reuseBrowerContext: ReuseBrowerContextStorage,
|
||||
};
|
||||
|
||||
class ReuseBrowerContextStorage {
|
||||
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;
|
||||
this._uniqueOrigins.add(new URL(frame.url()).origin);
|
||||
}));
|
||||
return this._browserContext;
|
||||
}
|
||||
|
||||
async _refurbishExistingContext(newContextOptions: BrowserContextOptions): Promise<BrowserContext> {
|
||||
assert(this._browserContext);
|
||||
const pages = this._browserContext.pages();
|
||||
const page = pages[0];
|
||||
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 Promise.all(pages.slice(1).map(page => page.close()));
|
||||
await page.goto('about:blank');
|
||||
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);
|
||||
if (
|
||||
(
|
||||
this._options.viewport?.width !== newOptions.viewport?.width ||
|
||||
this._options.viewport?.height !== newOptions.viewport?.height
|
||||
) &&
|
||||
(newOptions.viewport?.height && newOptions.viewport?.width)
|
||||
)
|
||||
await page.setViewportSize({ width: newOptions.viewport?.width, height: newOptions.viewport?.height });
|
||||
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' } ],
|
||||
|
|
@ -311,10 +387,17 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||
}));
|
||||
}, { auto: true }],
|
||||
|
||||
context: async ({ browser, video, _artifactsDir }, use, testInfo) => {
|
||||
_reuseBrowerContext: [new ReuseBrowerContextStorage(), { scope: 'worker' }],
|
||||
|
||||
context: async ({ browser, video, _artifactsDir, _reuseBrowerContext, _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 (_reuseBrowerContext.isEnabled()) {
|
||||
const context = await _reuseBrowerContext.obtainContext(browser, _combinedContextOptions);
|
||||
await use(context);
|
||||
return;
|
||||
}
|
||||
|
||||
let videoMode = typeof video === 'string' ? video : video.mode;
|
||||
if (videoMode === 'retry-with-video')
|
||||
|
|
@ -366,7 +449,11 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||
}
|
||||
},
|
||||
|
||||
page: async ({ context }, use) => {
|
||||
page: async ({ context, _reuseBrowerContext }, use) => {
|
||||
if (_reuseBrowerContext.isEnabled()) {
|
||||
await use(await _reuseBrowerContext.obtainPage());
|
||||
return;
|
||||
}
|
||||
await use(await context.newPage());
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -456,3 +456,83 @@ 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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue