mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: add an ExtensionTransport (#12382)
This commit is contained in:
parent
0af4664b8a
commit
5772210af3
32
package-lock.json
generated
32
package-lock.json
generated
@ -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,
|
||||
|
@ -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",
|
||||
|
183
packages/puppeteer-core/src/cdp/ExtensionTransport.test.ts
Normal file
183
packages/puppeteer-core/src/cdp/ExtensionTransport.test.ts
Normal 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',
|
||||
{},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
182
packages/puppeteer-core/src/cdp/ExtensionTransport.ts
Normal file
182
packages/puppeteer-core/src/cdp/ExtensionTransport.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user