Skip to content

Commit

Permalink
fix: support oopif
Browse files Browse the repository at this point in the history
Out-of-process iframes are now supported in chrome.debugger

See; https://chromium-review.googlesource.com/c/chromium/src/+/5398119

Fixes: #26317
  • Loading branch information
ruifigueira committed Apr 27, 2024
1 parent 766c080 commit effbcc9
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 22 deletions.
67 changes: 50 additions & 17 deletions src/server/transport/crxTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from 'pla

type Tab = chrome.tabs.Tab;

// mimics DebuggerSession on https://chromium-review.googlesource.com/c/chromium/src/+/5398119/12/chrome/common/extensions/api/debugger.json
// TODO replace with proper type when available
type DebuggerSession = chrome.debugger.Debuggee & { sessionId?: string };

export class CrxTransport implements ConnectionTransport {
private _progress?: Progress;
private _detachedPromise?: Promise<void>;
private _targetToTab: Map<string, number>;
private _tabToTarget: Map<number, string>;
private _sessions: Map<string, number>;

onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
Expand All @@ -34,6 +39,7 @@ export class CrxTransport implements ConnectionTransport {
this._progress = progress;
this._tabToTarget = new Map();
this._targetToTab = new Map();
this._sessions = new Map();
chrome.debugger.onEvent.addListener(this._onDebuggerEvent);
chrome.debugger.onDetach.addListener(this._onRemoved);
chrome.tabs.onRemoved.addListener(this._onRemoved);
Expand All @@ -51,17 +57,38 @@ export class CrxTransport implements ConnectionTransport {
async send(message: ProtocolRequest) {
try {
const [, tabIdStr] = /crx-tab-(\d+)/.exec(message.sessionId ?? '') ?? [];
const tabId = tabIdStr ? parseInt(tabIdStr, 10) : undefined;
let debuggee: DebuggerSession;
if (tabIdStr) {
const tabId = parseInt(tabIdStr, 10);
debuggee = { tabId };
} else {
const sessionId = message.sessionId!;
const tabId = this._sessions.get(sessionId)!;
debuggee = { tabId, sessionId };
}

let result;
// chrome extensions doesn't support all CDP commands so we need to handle them
if (message.method === 'Target.setAutoAttach' && !tabId) {
if (message.method === 'Target.setAutoAttach' && !debuggee.tabId) {
// no tab to attach, just skip for now...
result = await Promise.resolve().then();
} else if (message.method === 'Target.setAutoAttach') {
// we need to exclude service workers, see https://github.com/ruifigueira/playwright-crx/issues/1
result = await this._send(message.method, { tabId, ...message.params, filter: [{ exclude: true, type: 'service_worker' }] });
} else if (message.method === 'Target.getTargetInfo' && !tabId) {
const [, versionStr] = navigator.userAgent.match(/Chrome\/([0-9]+)./) ?? [];

// we need to exclude service workers, see:
// https://github.com/ruifigueira/playwright-crx/issues/1
// https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-setAutoAttach
result = await this._send(debuggee, message.method, { ...message.params, filter: [
{ exclude: true, type: 'service_worker' },
// and these are the defaults:
// https://chromedevtools.github.io/devtools-protocol/tot/Target/#type-TargetFilter
{ exclude: true, type: 'browser' },
{ exclude: true, type: 'tab' },
// in versions prior to 126, this fallback doesn't work,
// but it is necessary for oopif frames to be discoverable in version 126 or greater
...(versionStr && parseInt(versionStr) >= 126 ? [{}] : []),
]});
} else if (message.method === 'Target.getTargetInfo' && !debuggee.tabId) {
// most likely related with https://chromium-review.googlesource.com/c/chromium/src/+/2885888
// See CRBrowser.connect
result = await Promise.resolve().then();
Expand Down Expand Up @@ -92,8 +119,7 @@ export class CrxTransport implements ConnectionTransport {
// see: https://github.com/ruifigueira/playwright-crx/issues/2
result = await Promise.resolve().then();
} else {
// @ts-ignore
result = await this._send(message.method, { tabId, ...message.params });
result = await this._send(debuggee, message.method as keyof Protocol.CommandParameters, { ...message.params });
}

this._emitMessage({
Expand All @@ -103,7 +129,6 @@ export class CrxTransport implements ConnectionTransport {
} catch (error) {
this._emitMessage({
...message,
// @ts-ignore
error,
});
}
Expand All @@ -113,10 +138,11 @@ export class CrxTransport implements ConnectionTransport {
let targetId = this._tabToTarget.get(tabId);

if (!targetId) {
await chrome.debugger.attach({ tabId }, '1.3');
const debuggee = { tabId };
await chrome.debugger.attach(debuggee, '1.3');
this._progress?.log(`<chrome debugger attached to tab ${tabId}>`);
// we don't create a new browser context, just return the current one
const { targetInfo } = await this._send('Target.getTargetInfo', { tabId });
const { targetInfo } = await this._send(debuggee, 'Target.getTargetInfo');
targetId = targetInfo.targetId;

// force browser to create a page
Expand Down Expand Up @@ -162,18 +188,18 @@ export class CrxTransport implements ConnectionTransport {
}

private async _send<T extends keyof Protocol.CommandParameters>(
debuggee: DebuggerSession,
method: T,
params?: Protocol.CommandParameters[T] & { tabId?: number }
commandParams?: Protocol.CommandParameters[T]
) {
const { tabId, ...commandParams } = params ?? {};
// eslint-disable-next-line no-console
if (!tabId) console.trace(`No tabId provided for ${method}`);
if (!debuggee.tabId) console.trace(`No tabId provided for ${method}`);

if (debugLogger.isEnabled('chromedebugger' as LogName)) {
debugLogger.log('chromedebugger' as LogName, `SEND> ${method} #${tabId}`);
debugLogger.log('chromedebugger' as LogName, `SEND> ${method} #${debuggee.tabId}`);
}

return await chrome.debugger.sendCommand({ tabId }, method, commandParams) as
return await chrome.debugger.sendCommand(debuggee, method, commandParams) as
Protocol.CommandReturnValues[T];
}

Expand All @@ -197,16 +223,23 @@ export class CrxTransport implements ConnectionTransport {
}
};

private _onDebuggerEvent = ({ tabId }: { tabId?: number }, message?: string, params?: any) => {
private _onDebuggerEvent = ({ tabId, sessionId }: DebuggerSession, message?: string, params?: any) => {
if (!tabId) return;
if (!sessionId) sessionId = this._sessionIdFor(tabId);

if (message === 'Target.attachedToTarget') {
this._sessions.set((params as Protocol.Target.attachToTargetReturnValue).sessionId, tabId);
} else if (message === 'Target.detachedFromTarget') {
this._sessions.delete((params as Protocol.Target.attachToTargetReturnValue).sessionId);
}

if (debugLogger.isEnabled(`chromedebugger` as LogName)) {
debugLogger.log('chromedebugger' as LogName, `<RECV ${message} #${tabId}`);
}

this._emitMessage({
method: message,
sessionId: this._sessionIdFor(tabId),
sessionId,
params,
});
};
Expand Down
10 changes: 10 additions & 0 deletions tests/crx/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,13 @@ test("should take screenshot", async ({ runCrxTest }) => {
expect(screenshot).not.toBeNull();
});
});

test('should report oopif frames', async ({ runCrxTest, browser }) => {
test.skip(parseInt(browser.version().match(/(\d+)\./)?.[1]!) < 126);

await runCrxTest(async ({ page, server, expect }) => {
await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(page.frames().length).toBe(2);
expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
});
});
18 changes: 14 additions & 4 deletions tests/crx/crxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@
*/

import type { Worker } from '@playwright/test';
import { test as base, chromium, mergeTests } from '@playwright/test';
import { test as base, chromium } from '@playwright/test';
import fs from 'fs';
import os from 'os';
import path from 'path';
import type { CrxApplication, BrowserContext as CrxBrowserContext, Page as CrxPage } from 'playwright-crx';
import type * as CrxTests from 'playwright-crx/test';
import { rimrafSync } from 'rimraf';
import { rimraf } from 'rimraf';

type CrxServer = {
EMPTY_PAGE: string;
PREFIX: string;
CROSS_PROCESS_PREFIX: string;
};

type CrxFixtures = {
Expand Down Expand Up @@ -70,7 +71,7 @@ export const test = base.extend<{
dirs.push(dir);
return dir;
});
rimrafSync(dirs);
await rimraf(dirs).catch(() => {});
},

context: async ({ extensionPath, createUserDataDir }, use) => {
Expand Down Expand Up @@ -109,7 +110,16 @@ export const test = base.extend<{
},

runCrxTest: async ({ extensionServiceWorker, baseURL }, use) => {
const params = { server: { PREFIX: baseURL, EMPTY_PAGE: `${baseURL}/empty.html` } };
const prefix = baseURL!;
const crossProcessUrl = new URL(prefix);
crossProcessUrl.hostname = crossProcessUrl.hostname === 'localhost' ? '127.0.0.1' : 'localhost';
const crossProcessPrefix = crossProcessUrl.toString().replace(/\/$/, '');
const server: CrxServer = {
PREFIX: prefix,
CROSS_PROCESS_PREFIX: crossProcessPrefix,
EMPTY_PAGE: `${baseURL}/empty.html`,
};
const params = { server };
use((fn) => extensionServiceWorker.evaluate(`_runTest(${fn.toString()}, ${JSON.stringify(params)})`));
},

Expand Down
6 changes: 5 additions & 1 deletion tests/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ export default defineConfig({
},
projects: [
{
name: 'chromium',
name: 'Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Chrome Canary',
use: { ...devices['Desktop Chrome'], channel: "chrome-canary" },
},
],
webServer: {
command: 'npm run serve',
Expand Down

0 comments on commit effbcc9

Please sign in to comment.