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: npm run build
|
||||||
- run: node lib/cli/cli install --with-deps
|
- run: node lib/cli/cli install --with-deps
|
||||||
- run: npm run ttest
|
- 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 */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { Command } from 'commander';
|
import { Command, Option } from 'commander';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { Config } from './types';
|
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('--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('--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.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('-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('--forbid-only', `Fail if test.only is called (default: false)`);
|
||||||
command.option('-g, --grep <grep>', `Only run tests matching this regular expression (default: ".*")`);
|
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);
|
const overrides = overridesFromOptions(opts);
|
||||||
if (opts.headed || opts.debug)
|
if (opts.headed || opts.debug || opts.reuseContext)
|
||||||
overrides.use = { headless: false };
|
overrides.use = { headless: false };
|
||||||
if (opts.debug) {
|
if (opts.debug || opts.reuseContext) {
|
||||||
overrides.maxFailures = 1;
|
overrides.maxFailures = 1;
|
||||||
overrides.timeout = 0;
|
overrides.timeout = 0;
|
||||||
overrides.workers = 1;
|
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);
|
const loader = new Loader(defaultConfig, overrides);
|
||||||
|
|
||||||
async function loadConfig(configFile: string) {
|
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 { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType } from '../../types/types';
|
||||||
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../../types/test';
|
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../../types/test';
|
||||||
import { rootTestType } from './testType';
|
import { rootTestType } from './testType';
|
||||||
import { createGuid, removeFolders } from '../utils/utils';
|
import { assert, createGuid, removeFolders } from '../utils/utils';
|
||||||
import { GridClient } from '../grid/gridClient';
|
import { GridClient } from '../grid/gridClient';
|
||||||
|
import { Browser } from '../..';
|
||||||
export { expect } from './expect';
|
export { expect } from './expect';
|
||||||
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
export const _baseTest: TestType<{}, {}> = rootTestType.test;
|
||||||
|
|
||||||
|
|
@ -31,8 +32,83 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||||
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
type WorkerAndFileFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||||
_browserType: BrowserType;
|
_browserType: BrowserType;
|
||||||
_artifactsDir: () => string,
|
_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>({
|
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' } ],
|
||||||
|
|
@ -311,10 +387,17 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
||||||
}));
|
}));
|
||||||
}, { auto: true }],
|
}, { 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);
|
const hook = hookType(testInfo);
|
||||||
if (hook)
|
if (hook)
|
||||||
throw new Error(`"context" and "page" fixtures are not supported in ${hook}. Use browser.newContext() instead.`);
|
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;
|
let videoMode = typeof video === 'string' ? video : video.mode;
|
||||||
if (videoMode === 'retry-with-video')
|
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());
|
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.videoWidth).toBe(220);
|
||||||
expect(videoPlayer.videoHeight).toBe(110);
|
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