feat(rpc): plumb CDPSession (#2862)

This commit is contained in:
Dmitry Gozman 2020-07-07 18:47:00 -07:00 committed by GitHub
parent 2a86ead0ac
commit 0c80c22716
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 16 deletions

View file

@ -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);
});
}
}

View file

@ -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 = {};

View file

@ -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());
}
}

View 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();
}
}

View file

@ -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;

View file

@ -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());
}
}

View 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();
}
}

View file

@ -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;
}
}

View file

@ -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();
});
});

View file

@ -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)));
}
}

View file

@ -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();

View file

@ -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');