feat(rpc): plumb CDPSession (#2862)
This commit is contained in:
parent
2a86ead0ac
commit
0c80c22716
|
|
@ -122,6 +122,7 @@ export const CRSessionEvents = {
|
|||
|
||||
export class CRSession extends EventEmitter {
|
||||
_connection: CRConnection | null;
|
||||
_eventListener?: (method: string, params?: Object) => void;
|
||||
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
|
||||
private readonly _targetType: string;
|
||||
private readonly _sessionId: string;
|
||||
|
|
@ -182,7 +183,11 @@ export class CRSession extends EventEmitter {
|
|||
callback.resolve(object.result);
|
||||
} else {
|
||||
assert(!object.id);
|
||||
Promise.resolve().then(() => this.emit(object.method!, object.params));
|
||||
Promise.resolve().then(() => {
|
||||
if (this._eventListener)
|
||||
this._eventListener(object.method!, object.params);
|
||||
this.emit(object.method!, object.params);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export interface BrowserChannel extends Channel {
|
|||
|
||||
close(): Promise<void>;
|
||||
newContext(params: types.BrowserContextOptions): Promise<BrowserContextChannel>;
|
||||
|
||||
// Chromium-specific.
|
||||
newBrowserCDPSession(): Promise<CDPSessionChannel>;
|
||||
}
|
||||
export type BrowserInitializer = {};
|
||||
|
||||
|
|
@ -330,3 +333,14 @@ export type DownloadInitializer = {
|
|||
url: string,
|
||||
suggestedFilename: string,
|
||||
};
|
||||
|
||||
|
||||
// Chromium-specific.
|
||||
export interface CDPSessionChannel extends Channel {
|
||||
on(event: 'event', callback: (params: { method: string, params?: Object }) => void): this;
|
||||
on(event: 'disconnected', callback: () => void): this;
|
||||
|
||||
send(params: { method: string, params?: Object }): Promise<Object>;
|
||||
detach(): Promise<void>;
|
||||
}
|
||||
export type CDPSessionInitializer = {};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Page } from './page';
|
|||
import { ChannelOwner } from './channelOwner';
|
||||
import { ConnectionScope } from './connection';
|
||||
import { Events } from '../../events';
|
||||
import { CDPSession } from './cdpSession';
|
||||
|
||||
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
||||
readonly _contexts = new Set<BrowserContext>();
|
||||
|
|
@ -77,4 +78,9 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
|
|||
this._isClosedOrClosing = true;
|
||||
await this._channel.close();
|
||||
}
|
||||
|
||||
// Chromium-specific.
|
||||
async newBrowserCDPSession(): Promise<CDPSession> {
|
||||
return CDPSession.from(await this._channel.newBrowserCDPSession());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
src/rpc/client/cdpSession.ts
Normal file
57
src/rpc/client/cdpSession.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CDPSessionChannel, CDPSessionInitializer } from '../channels';
|
||||
import { ConnectionScope } from './connection';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Protocol } from '../../chromium/protocol';
|
||||
|
||||
export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitializer> {
|
||||
static from(cdpSession: CDPSessionChannel): CDPSession {
|
||||
return (cdpSession as any)._object;
|
||||
}
|
||||
|
||||
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
|
||||
constructor(scope: ConnectionScope, guid: string, initializer: CDPSessionInitializer) {
|
||||
super(scope, guid, initializer, true);
|
||||
|
||||
this._channel.on('event', ({ method, params }) => this.emit(method, params));
|
||||
this._channel.on('disconnected', () => this._scope.dispose());
|
||||
|
||||
this.on = super.on;
|
||||
this.addListener = super.addListener;
|
||||
this.off = super.removeListener;
|
||||
this.removeListener = super.removeListener;
|
||||
this.once = super.once;
|
||||
}
|
||||
|
||||
async send<T extends keyof Protocol.CommandParameters>(
|
||||
method: T,
|
||||
params?: Protocol.CommandParameters[T]
|
||||
): Promise<Protocol.CommandReturnValues[T]> {
|
||||
const result = await this._channel.send({ method, params });
|
||||
return result as Protocol.CommandReturnValues[T];
|
||||
}
|
||||
|
||||
async detach() {
|
||||
return this._channel.detach();
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import { Dialog } from './dialog';
|
|||
import { Download } from './download';
|
||||
import { parseError } from '../serializers';
|
||||
import { BrowserServer } from './browserServer';
|
||||
import { CDPSession } from './cdpSession';
|
||||
|
||||
export class Connection {
|
||||
readonly _objects = new Map<string, ChannelOwner<any, any>>();
|
||||
|
|
@ -192,6 +193,10 @@ export class ConnectionScope {
|
|||
case 'browserType':
|
||||
result = new BrowserType(this, guid, initializer);
|
||||
break;
|
||||
case 'cdpSession':
|
||||
// Chromium-specific.
|
||||
result = new CDPSession(this, guid, initializer);
|
||||
break;
|
||||
case 'context':
|
||||
result = new BrowserContext(this, guid, initializer);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import { Browser, BrowserBase } from '../../browser';
|
|||
import { BrowserContextBase } from '../../browserContext';
|
||||
import { Events } from '../../events';
|
||||
import * as types from '../../types';
|
||||
import { BrowserChannel, BrowserContextChannel, BrowserInitializer } from '../channels';
|
||||
import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel } from '../channels';
|
||||
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
import { CRBrowser } from '../../chromium/crBrowser';
|
||||
|
||||
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
|
||||
constructor(scope: DispatcherScope, browser: BrowserBase) {
|
||||
|
|
@ -38,4 +40,10 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
|
|||
async close(): Promise<void> {
|
||||
await this._object.close();
|
||||
}
|
||||
|
||||
// Chromium-specific.
|
||||
async newBrowserCDPSession(): Promise<CDPSessionChannel> {
|
||||
const crBrowser = this._object as CRBrowser;
|
||||
return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
src/rpc/server/cdpSessionDispatcher.ts
Normal file
38
src/rpc/server/cdpSessionDispatcher.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CRSession, CRSessionEvents } from '../../chromium/crConnection';
|
||||
import { CDPSessionChannel, CDPSessionInitializer } from '../channels';
|
||||
import { Dispatcher, DispatcherScope } from './dispatcher';
|
||||
|
||||
export class CDPSessionDispatcher extends Dispatcher<CRSession, CDPSessionInitializer> implements CDPSessionChannel {
|
||||
constructor(scope: DispatcherScope, crSession: CRSession) {
|
||||
super(scope, crSession, 'cdpSession', {}, true);
|
||||
crSession._eventListener = (method, params) => this._dispatchEvent('event', { method, params });
|
||||
crSession.on(CRSessionEvents.Disconnected, () => {
|
||||
this._dispatchEvent('disconnected');
|
||||
this._scope.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
async send(params: { method: string, params?: Object }): Promise<Object> {
|
||||
return this._object.send(params.method as any, params.params);
|
||||
}
|
||||
|
||||
async detach(): Promise<void> {
|
||||
return this._object.detach();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const vm = require('vm');
|
||||
const { FFOX, CHROMIUM, WEBKIT, WIN, CHANNEL } = require('./utils').testOptions(browserType);
|
||||
|
||||
describe.skip(!CHANNEL)('Channels', function() {
|
||||
it('should work', async({browser}) => {
|
||||
expect(!!browser._channel).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should scope context handles', async({browser, server}) => {
|
||||
const GOLDEN_PRECONDITION = {
|
||||
objects: [ 'chromium', 'browser' ],
|
||||
|
|
@ -50,7 +48,31 @@ describe.skip(!CHANNEL)('Channels', function() {
|
|||
await expectScopeState(browser, GOLDEN_PRECONDITION);
|
||||
});
|
||||
|
||||
it('should browser handles', async({browserType, defaultBrowserOptions}) => {
|
||||
it('should scope CDPSession handles', async({browserType, browser, server}) => {
|
||||
const GOLDEN_PRECONDITION = {
|
||||
objects: [ 'chromium', 'browser' ],
|
||||
scopes: [
|
||||
{ _guid: '', objects: [ 'chromium', 'browser' ] },
|
||||
{ _guid: 'browser', objects: [] }
|
||||
]
|
||||
};
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
|
||||
const session = await browser.newBrowserCDPSession();
|
||||
await expectScopeState(browserType, {
|
||||
objects: [ 'chromium', 'browser', 'cdpSession' ],
|
||||
scopes: [
|
||||
{ _guid: '', objects: [ 'chromium', 'browser' ] },
|
||||
{ _guid: 'browser', objects: ['cdpSession'] },
|
||||
{ _guid: 'cdpSession', objects: [] },
|
||||
]
|
||||
});
|
||||
|
||||
await session.detach();
|
||||
await expectScopeState(browserType, GOLDEN_PRECONDITION);
|
||||
});
|
||||
|
||||
it('should scope browser handles', async({browserType, defaultBrowserOptions}) => {
|
||||
const GOLDEN_PRECONDITION = {
|
||||
objects: [ 'chromium', 'browser' ],
|
||||
scopes: [
|
||||
|
|
@ -96,4 +118,4 @@ function trimGuids(object) {
|
|||
if (typeof object === 'string')
|
||||
return object ? object.match(/[^@]+/)[0] : '';
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,11 +95,18 @@ describe.skip(CHANNEL)('ChromiumBrowserContext.createSession', function() {
|
|||
await context.close();
|
||||
});
|
||||
});
|
||||
describe.skip(CHANNEL)('ChromiumBrowser.newBrowserCDPSession', function() {
|
||||
describe('ChromiumBrowser.newBrowserCDPSession', function() {
|
||||
it('should work', async function({page, browser, server}) {
|
||||
const session = await browser.newBrowserCDPSession();
|
||||
|
||||
const version = await session.send('Browser.getVersion');
|
||||
expect(version.userAgent).toBeTruthy();
|
||||
|
||||
let gotEvent = false;
|
||||
session.on('Target.targetCreated', () => gotEvent = true);
|
||||
await session.send('Target.setDiscoverTargets', { discover: true });
|
||||
expect(gotEvent).toBe(true);
|
||||
|
||||
await session.detach();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ const writeFileAsync = util.promisify(fs.writeFile);
|
|||
|
||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
async function recursiveReadDir(dirPath) {
|
||||
async function recursiveReadDir(dirPath, exclude) {
|
||||
const files = [];
|
||||
if (exclude.includes(dirPath))
|
||||
return files;
|
||||
for (const file of await readdirAsync(dirPath)) {
|
||||
const fullPath = path.join(dirPath, file);
|
||||
if ((await statAsync(fullPath)).isDirectory())
|
||||
files.push(...await recursiveReadDir(fullPath))
|
||||
files.push(...await recursiveReadDir(fullPath, exclude))
|
||||
else
|
||||
files.push(fullPath);
|
||||
}
|
||||
|
|
@ -100,7 +102,7 @@ class Source {
|
|||
async save() {
|
||||
await writeFileAsync(this.filePath(), this.text());
|
||||
}
|
||||
|
||||
|
||||
async saveAs(path) {
|
||||
await writeFileAsync(path, this.text());
|
||||
}
|
||||
|
|
@ -118,11 +120,12 @@ class Source {
|
|||
/**
|
||||
* @param {string} dirPath
|
||||
* @param {string=} extension
|
||||
* @param {Array<string>=} exclude
|
||||
* @return {!Promise<!Array<!Source>>}
|
||||
*/
|
||||
static async readdir(dirPath, extension = '') {
|
||||
static async readdir(dirPath, extension = '', exclude = []) {
|
||||
extension = extension.toLowerCase();
|
||||
const filePaths = (await recursiveReadDir(dirPath)).filter(fileName => fileName.toLowerCase().endsWith(extension));
|
||||
const filePaths = (await recursiveReadDir(dirPath, exclude)).filter(fileName => fileName.toLowerCase().endsWith(extension));
|
||||
return Promise.all(filePaths.map(filePath => Source.readFile(filePath)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ async function run() {
|
|||
const browser = await playwright.chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const checkPublicAPI = require('./check_public_api');
|
||||
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
|
||||
const rpcDir = path.join(PROJECT_DIR, 'src', 'rpc');
|
||||
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src'), '', [rpcDir]);
|
||||
messages.push(...await checkPublicAPI(page, [api], jsSources));
|
||||
await browser.close();
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ let documentation;
|
|||
const api = await Source.readFile(path.join(PROJECT_DIR, 'docs', 'api.md'));
|
||||
const {documentation: mdDocumentation} = await require('../doclint/check_public_api/MDBuilder')(page, [api]);
|
||||
await browser.close();
|
||||
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
|
||||
const rpcDir = path.join(PROJECT_DIR, 'src', 'rpc');
|
||||
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src'), '', [rpcDir]);
|
||||
const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources);
|
||||
documentation = mergeDocumentation(mdDocumentation, jsDocumentation);
|
||||
const handledClasses = new Set();
|
||||
|
|
@ -408,7 +409,7 @@ function mergeClasses(mdClass, jsClass) {
|
|||
}
|
||||
|
||||
function generateDevicesTypes() {
|
||||
const namedDevices =
|
||||
const namedDevices =
|
||||
Object.keys(devices)
|
||||
.map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`)
|
||||
.join('\n');
|
||||
|
|
|
|||
Loading…
Reference in a new issue