Skip to content
Open
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
25 changes: 25 additions & 0 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ import type * as channels from '../channels';

export type WindowBounds = { top?: number, left?: number, width?: number, height?: number };

// Browsers disallow these WebUI hosts in off-the-record profiles and redirect them to the original
// profile, which crashes when the profile was created over CDP. Edge allows most of them in InPrivate.
// See https://github.com/microsoft/playwright/issues/41935.
const kCrashingWebUIHosts = {
chromium: new Set(['apps', 'extensions', 'help', 'history', 'password-manager', 'settings']),
edge: new Set(['history']),
};

// Chromium canonicalizes WebUI urls as standard ones, so "VIEW-SOURCE:Chrome:Settings" ends up
// being "chrome://settings/".
function webUIHost(url: string): string {
const match = /^(?:view-source:)?(?:chrome|edge):\/*([^/?#]+)/i.exec(url);
const authority = match ? `http://${match[1]}` : '';
return URL.canParse(authority) ? new URL(authority).hostname : '';
}

export class CRPage implements PageDelegate {
readonly utilityWorldName: string;
readonly _mainFrameSession: FrameSession;
Expand Down Expand Up @@ -152,9 +168,18 @@ export class CRPage implements PageDelegate {
}

async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
this._assertNavigationDoesNotCrashBrowser(url);
return this._sessionForFrame(frame)._navigate(frame, url, referrer);
}

private _assertNavigationDoesNotCrashBrowser(url: string) {
if (this._browserContext.isPersistentContext())
return;
const isEdge = this._browserContext._browser.userAgent().includes('Edg/');
if ((isEdge ? kCrashingWebUIHosts.edge : kCrashingWebUIHosts.chromium).has(webUIHost(url)))
throw new Error(`Cannot navigate to "${url}": this page is not available in an isolated browser context, and opening it crashes the browser. Use browserType.launchPersistentContext() instead.`);
}

async updateExtraHTTPHeaders(): Promise<void> {
const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders,
Expand Down
51 changes: 51 additions & 0 deletions tests/library/chromium/chromium.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import { contextTest as test, expect } from '../../config/browserTest';
import { playwrightTest } from '../../config/browserTest';

import type { Page } from 'playwright-core';

test('should create a worker from a service worker', async ({ page, server }) => {
const [worker] = await Promise.all([
page.context().waitForEvent('serviceworker'),
Expand Down Expand Up @@ -747,6 +749,55 @@ test('should capture console.log from ServiceWorker start', async ({ context, pa
expect(consoleMessage.type()).toBe('log');
});

test.describe('WebUI navigation', () => {
const isEdge = (channel: string | undefined) => !!channel?.startsWith('msedge');
const gotoError = (page: Page, url: string) => page.goto(url).then(() => '', e => e.message);

test('should refuse WebUI pages that crash the browser in an isolated context', async ({ browser, page, channel }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41935' });
test.skip(isEdge(channel), 'Edge only disallows chrome://history');

const hosts = ['apps', 'extensions', 'help', 'history', 'password-manager', 'settings'];
for (const url of [...hosts.map(host => `chrome://${host}`), 'chrome://extensions/', 'chrome://settings/help', 'chrome://SETTINGS', 'chrome:settings', 'chrome:///settings', 'view-source:chrome://settings'])
expect(await gotoError(page, url)).toContain(`Cannot navigate to "${url}"`);
// The refusal only matters because it keeps the browser process alive.
expect(browser.isConnected()).toBe(true);
expect(await page.evaluate(() => 1 + 1)).toBe(2);
});

test('should refuse WebUI pages that crash Edge in an isolated context', async ({ browser, page, channel }) => {
test.skip(!isEdge(channel), 'Edge has its own list of pages disallowed in InPrivate');

for (const url of ['edge://history', 'chrome://history', 'view-source:edge://history'])
expect(await gotoError(page, url)).toContain(`Cannot navigate to "${url}"`);
// Edge does allow these in InPrivate, so they must not be refused.
for (const url of ['edge://settings', 'edge://extensions'])
expect(await gotoError(page, url)).not.toContain('Cannot navigate to');
expect(browser.isConnected()).toBe(true);
});

test('should navigate to WebUI pages that work in an isolated context', async ({ page, headless }) => {
test.skip(headless, 'WebUI pages are not available in headless');

const response = await page.goto('chrome://version');
expect(response.status()).toBe(200);
});

test('should navigate to any WebUI page in a persistent context', async ({ browserType, createUserDataDir, headless }) => {
test.skip(headless, 'WebUI pages are not available in headless');

const context = await browserType.launchPersistentContext(await createUserDataDir());
try {
const page = await context.newPage();
// Committing the navigation is all this asserts - waiting for the WebUI to load is slow and beside the point.
const response = await page.goto('chrome://extensions', { waitUntil: 'commit' });
expect(response.status()).toBe(200);
} finally {
await context.close();
}
});
});

test('should fire dialogclosed event when dialog is closed out of band', async ({ page }) => {
// Establish the CDP session up front: creating one while a dialog is blocking the page hangs.
const client = await page.context().newCDPSession(page);
Expand Down