feat(test-runner): add reuse context mode to share a single context between tests (#9115)

This commit is contained in:
Max Schmitt 2021-10-01 09:16:03 +02:00 committed by GitHub
parent 2cf3448b6b
commit e674d873a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 182 additions and 7 deletions

View file

@ -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'

View file

@ -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) {

View file

@ -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());
},
});

View file

@ -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);
});