Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/tests_primary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
13 changes: 9 additions & 4 deletions src/test/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: ".*")`);
Expand Down Expand Up @@ -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';
Comment thread
mxschmitt marked this conversation as resolved.

const loader = new Loader(defaultConfig, overrides);

async function loadConfig(configFile: string) {
Expand Down
93 changes: 90 additions & 3 deletions src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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' } ],
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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());
},
});
Expand Down
80 changes: 80 additions & 0 deletions tests/playwright-test/playwright.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});