chore: add an ExtensionTransport (#12382)

This commit is contained in:
Alex Rudenko 2024-05-03 20:21:08 +02:00 committed by GitHub
parent 0af4664b8a
commit 5772210af3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 399 additions and 0 deletions

32
package-lock.json generated
View File

@ -12266,6 +12266,7 @@
"ws": "8.17.0"
},
"devDependencies": {
"@types/chrome": "0.0.267",
"@types/debug": "4.1.12",
"@types/node": "18.17.15",
"@types/ws": "8.5.10",
@ -12277,6 +12278,37 @@
"node": ">=18"
}
},
"packages/puppeteer-core/node_modules/@types/chrome": {
"version": "0.0.267",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.267.tgz",
"integrity": "sha512-vnCWPpYjazSPRMNmybRH+0q4f738F+Pbbls4ZPFsPr9/4TTNJyK1OLZDpSnghnEWb4stfmIUtq/GegnlfD4sPA==",
"dev": true,
"dependencies": {
"@types/filesystem": "*",
"@types/har-format": "*"
}
},
"packages/puppeteer-core/node_modules/@types/chrome/node_modules/@types/filesystem": {
"version": "0.0.36",
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
"dev": true,
"dependencies": {
"@types/filewriter": "*"
}
},
"packages/puppeteer-core/node_modules/@types/chrome/node_modules/@types/filesystem/node_modules/@types/filewriter": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==",
"dev": true
},
"packages/puppeteer-core/node_modules/@types/chrome/node_modules/@types/har-format": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz",
"integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==",
"dev": true
},
"packages/puppeteer-core/node_modules/@types/node": {
"version": "18.17.15",
"dev": true,

View File

@ -128,6 +128,7 @@
"devDependencies": {
"@types/debug": "4.1.12",
"@types/node": "18.17.15",
"@types/chrome": "0.0.267",
"@types/ws": "8.5.10",
"mitt": "3.0.1",
"parsel-js": "1.1.2",

View File

@ -0,0 +1,183 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import {afterEach, describe, it} from 'node:test';
import expect from 'expect';
import sinon from 'sinon';
import {ExtensionTransport} from './ExtensionTransport.js';
type EventListenerFunction = (
source: chrome.debugger.Debuggee,
method: string,
params?: object | undefined
) => void;
describe('ExtensionTransport', function () {
afterEach(() => {
sinon.restore();
});
function mockChrome() {
const fakeAttach = sinon.fake.resolves<
[chrome.debugger.Debuggee, string],
Promise<void>
>(undefined);
const fakeDetach = sinon.fake.resolves<
[chrome.debugger.Debuggee],
Promise<void>
>(undefined);
const fakeSendCommand = sinon.fake.resolves<
[chrome.debugger.Debuggee, string, object | undefined],
Promise<object>
>({});
const fakeGetTargets = sinon.fake.resolves<
[],
Promise<chrome.debugger.TargetInfo[]>
>([]);
const onEvent: EventListenerFunction[] = [];
sinon.define(globalThis, 'chrome', {
debugger: {
attach: fakeAttach,
detach: fakeDetach,
sendCommand: fakeSendCommand,
getTargets: fakeGetTargets,
onEvent: {
addListener: (cb: EventListenerFunction) => {
onEvent.push(cb);
},
removeListener: (cb: EventListenerFunction) => {
const idx = onEvent.indexOf(cb);
if (idx !== -1) {
onEvent.splice(idx, 1);
}
},
},
},
});
return {
fakeAttach,
fakeDetach,
fakeSendCommand,
fakeGetTargets,
onEvent,
};
}
describe('connectTab', function () {
it('should attach using tabId', async () => {
const {fakeAttach, onEvent} = mockChrome();
const transport = await ExtensionTransport.connectTab(1);
expect(transport).toBeInstanceOf(ExtensionTransport);
expect(fakeAttach.calledOnceWith({tabId: 1}, '1.3')).toBeTruthy();
expect(onEvent).toHaveLength(1);
});
it('should detach', async () => {
const {onEvent} = mockChrome();
const transport = await ExtensionTransport.connectTab(1);
transport.close();
expect(onEvent).toHaveLength(0);
});
});
describe('send', function () {
async function testTranportResponse(command: object): Promise<string[]> {
const {fakeSendCommand} = mockChrome();
const transport = await ExtensionTransport.connectTab(1);
const onmessageFake = sinon.fake();
transport.onmessage = onmessageFake;
transport.send(JSON.stringify(command));
expect(fakeSendCommand.notCalled).toBeTruthy();
return onmessageFake.getCalls().map(call => {
return call.args[0];
});
}
it('provides a dummy response to Browser.getVersion', async () => {
expect(
await testTranportResponse({
id: 1,
method: 'Browser.getVersion',
})
).toStrictEqual([
'{"id":1,"method":"Browser.getVersion","result":{"protocolVersion":"1.3","product":"chrome","revision":"unknown","userAgent":"chrome","jsVersion":"unknown"}}',
]);
});
it('provides a dummy response to Target.getBrowserContexts', async () => {
expect(
await testTranportResponse({
id: 1,
method: 'Target.getBrowserContexts',
})
).toStrictEqual([
'{"id":1,"method":"Target.getBrowserContexts","result":{"browserContextIds":[]}}',
]);
});
it('provides a dummy response and events to Target.setDiscoverTargets', async () => {
expect(
await testTranportResponse({
id: 1,
method: 'Target.setDiscoverTargets',
})
).toStrictEqual([
'{"method":"Target.targetCreated","params":{"targetInfo":{"targetId":"tabTargetId","type":"tab","title":"tab","url":"about:blank","attached":false,"canAccessOpener":false}}}',
'{"method":"Target.targetCreated","params":{"targetInfo":{"targetId":"pageTargetId","type":"page","title":"page","url":"about:blank","attached":false,"canAccessOpener":false}}}',
'{"id":1,"method":"Target.setDiscoverTargets","result":{}}',
]);
});
it('attaches to a dummy tab target on Target.setAutoAttach', async () => {
expect(
await testTranportResponse({
id: 1,
method: 'Target.setAutoAttach',
})
).toStrictEqual([
'{"method":"Target.attachedToTarget","params":{"targetInfo":{"targetId":"tabTargetId","type":"tab","title":"tab","url":"about:blank","attached":false,"canAccessOpener":false},"sessionId":"tabTargetSessionId"}}',
'{"id":1,"method":"Target.setAutoAttach","result":{}}',
]);
});
it('attaches to a dummy page target on Target.setAutoAttach', async () => {
expect(
await testTranportResponse({
id: 1,
method: 'Target.setAutoAttach',
sessionId: 'tabTargetSessionId',
})
).toStrictEqual([
'{"method":"Target.attachedToTarget","params":{"targetInfo":{"targetId":"pageTargetId","type":"page","title":"page","url":"about:blank","attached":false,"canAccessOpener":false},"sessionId":"pageTargetSessionId"}}',
'{"id":1,"sessionId":"tabTargetSessionId","method":"Target.setAutoAttach","result":{}}',
]);
});
it('rewrites session id for pageTargetSessionId commands', async () => {
const {fakeSendCommand} = mockChrome();
const transport = await ExtensionTransport.connectTab(1);
transport.send(
JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {},
sessionId: 'pageTargetSessionId',
})
);
expect(fakeSendCommand.calledOnce).toBeTruthy();
expect(fakeSendCommand.lastCall.args).toStrictEqual([
{
tabId: 1,
sessionId: undefined,
},
'Runtime.evaluate',
{},
]);
});
});
});

View File

@ -0,0 +1,182 @@
/**
* @license
* Copyright 2024 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
const tabTargetInfo = {
targetId: 'tabTargetId',
type: 'tab',
title: 'tab',
url: 'about:blank',
attached: false,
canAccessOpener: false,
};
const pageTargetInfo = {
targetId: 'pageTargetId',
type: 'page',
title: 'page',
url: 'about:blank',
attached: false,
canAccessOpener: false,
};
/**
* Experimental ExtensionTransport allows establishing a connection via
* chrome.debugger API if Puppeteer runs in an extension. Since Chrome
* DevTools Protocol is restricted for extensions, the transport
* implements missing commands and events.
*
* @experimental
* @internal
*/
export class ExtensionTransport implements ConnectionTransport {
static async connectTab(tabId: number): Promise<ExtensionTransport> {
await chrome.debugger.attach({tabId}, '1.3');
return new ExtensionTransport(tabId);
}
onmessage?: (message: string) => void;
onclose?: () => void;
#tabId: number;
/**
* @internal
*/
constructor(tabId: number) {
this.#tabId = tabId;
chrome.debugger.onEvent.addListener(this.#debuggerEventHandler);
}
#debuggerEventHandler = (
source: chrome.debugger.Debuggee,
method: string,
params?: object | undefined
): void => {
if (source.tabId !== this.#tabId) {
return;
}
this.#dispatchResponse({
// @ts-expect-error sessionId is not in stable yet.
sessionId: source.sessionId ?? 'pageTargetSessionId',
method: method,
params: params,
});
};
#dispatchResponse(message: object): void {
this.onmessage?.(JSON.stringify(message));
}
send(message: string): void {
const parsed = JSON.parse(message);
switch (parsed.method) {
case 'Browser.getVersion': {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {
protocolVersion: '1.3',
product: 'chrome',
revision: 'unknown',
userAgent: 'chrome',
jsVersion: 'unknown',
},
});
return;
}
case 'Target.getBrowserContexts': {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {
browserContextIds: [],
},
});
return;
}
case 'Target.setDiscoverTargets': {
this.#dispatchResponse({
method: 'Target.targetCreated',
params: {
targetInfo: tabTargetInfo,
},
});
this.#dispatchResponse({
method: 'Target.targetCreated',
params: {
targetInfo: pageTargetInfo,
},
});
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {},
});
return;
}
case 'Target.setAutoAttach': {
if (parsed.sessionId === 'tabTargetSessionId') {
this.#dispatchResponse({
method: 'Target.attachedToTarget',
params: {
targetInfo: pageTargetInfo,
sessionId: 'pageTargetSessionId',
},
});
} else if (!parsed.sessionId) {
this.#dispatchResponse({
method: 'Target.attachedToTarget',
params: {
targetInfo: tabTargetInfo,
sessionId: 'tabTargetSessionId',
},
});
}
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId,
method: parsed.method,
result: {},
});
return;
}
}
if (parsed.sessionId === 'pageTargetSessionId') {
delete parsed.sessionId;
}
chrome.debugger
.sendCommand(
// @ts-expect-error sessionId is not in stable yet.
{tabId: this.#tabId, sessionId: parsed.sessionId},
parsed.method,
parsed.params
)
.then(response => {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
method: parsed.method,
result: response,
});
})
.catch(err => {
this.#dispatchResponse({
id: parsed.id,
sessionId: parsed.sessionId ?? 'pageTargetSessionId',
method: parsed.method,
error: err,
});
});
}
close(): void {
chrome.debugger.onEvent.removeListener(this.#debuggerEventHandler);
}
}

View File

@ -19,6 +19,7 @@ export * from './Dialog.js';
export * from './ElementHandle.js';
export * from './EmulationManager.js';
export * from './ExecutionContext.js';
export * from './ExtensionTransport.js';
export * from './FirefoxTargetManager.js';
export * from './Frame.js';
export * from './FrameManager.js';