Skip to content

Commit 0c80c22

Browse files
authored
feat(rpc): plumb CDPSession (#2862)
1 parent 2a86ead commit 0c80c22

File tree

12 files changed

+183
-16
lines changed

12 files changed

+183
-16
lines changed

src/chromium/crConnection.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export const CRSessionEvents = {
122122

123123
export class CRSession extends EventEmitter {
124124
_connection: CRConnection | null;
125+
_eventListener?: (method: string, params?: Object) => void;
125126
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
126127
private readonly _targetType: string;
127128
private readonly _sessionId: string;
@@ -182,7 +183,11 @@ export class CRSession extends EventEmitter {
182183
callback.resolve(object.result);
183184
} else {
184185
assert(!object.id);
185-
Promise.resolve().then(() => this.emit(object.method!, object.params));
186+
Promise.resolve().then(() => {
187+
if (this._eventListener)
188+
this._eventListener(object.method!, object.params);
189+
this.emit(object.method!, object.params);
190+
});
186191
}
187192
}
188193

src/rpc/channels.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export interface BrowserChannel extends Channel {
5252

5353
close(): Promise<void>;
5454
newContext(params: types.BrowserContextOptions): Promise<BrowserContextChannel>;
55+
56+
// Chromium-specific.
57+
newBrowserCDPSession(): Promise<CDPSessionChannel>;
5558
}
5659
export type BrowserInitializer = {};
5760

@@ -330,3 +333,14 @@ export type DownloadInitializer = {
330333
url: string,
331334
suggestedFilename: string,
332335
};
336+
337+
338+
// Chromium-specific.
339+
export interface CDPSessionChannel extends Channel {
340+
on(event: 'event', callback: (params: { method: string, params?: Object }) => void): this;
341+
on(event: 'disconnected', callback: () => void): this;
342+
343+
send(params: { method: string, params?: Object }): Promise<Object>;
344+
detach(): Promise<void>;
345+
}
346+
export type CDPSessionInitializer = {};

src/rpc/client/browser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Page } from './page';
2121
import { ChannelOwner } from './channelOwner';
2222
import { ConnectionScope } from './connection';
2323
import { Events } from '../../events';
24+
import { CDPSession } from './cdpSession';
2425

2526
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
2627
readonly _contexts = new Set<BrowserContext>();
@@ -77,4 +78,9 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
7778
this._isClosedOrClosing = true;
7879
await this._channel.close();
7980
}
81+
82+
// Chromium-specific.
83+
async newBrowserCDPSession(): Promise<CDPSession> {
84+
return CDPSession.from(await this._channel.newBrowserCDPSession());
85+
}
8086
}

src/rpc/client/cdpSession.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { CDPSessionChannel, CDPSessionInitializer } from '../channels';
18+
import { ConnectionScope } from './connection';
19+
import { ChannelOwner } from './channelOwner';
20+
import { Protocol } from '../../chromium/protocol';
21+
22+
export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitializer> {
23+
static from(cdpSession: CDPSessionChannel): CDPSession {
24+
return (cdpSession as any)._object;
25+
}
26+
27+
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;
28+
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;
29+
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;
30+
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;
31+
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;
32+
33+
constructor(scope: ConnectionScope, guid: string, initializer: CDPSessionInitializer) {
34+
super(scope, guid, initializer, true);
35+
36+
this._channel.on('event', ({ method, params }) => this.emit(method, params));
37+
this._channel.on('disconnected', () => this._scope.dispose());
38+
39+
this.on = super.on;
40+
this.addListener = super.addListener;
41+
this.off = super.removeListener;
42+
this.removeListener = super.removeListener;
43+
this.once = super.once;
44+
}
45+
46+
async send<T extends keyof Protocol.CommandParameters>(
47+
method: T,
48+
params?: Protocol.CommandParameters[T]
49+
): Promise<Protocol.CommandReturnValues[T]> {
50+
const result = await this._channel.send({ method, params });
51+
return result as Protocol.CommandReturnValues[T];
52+
}
53+
54+
async detach() {
55+
return this._channel.detach();
56+
}
57+
}

src/rpc/client/connection.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { Dialog } from './dialog';
3030
import { Download } from './download';
3131
import { parseError } from '../serializers';
3232
import { BrowserServer } from './browserServer';
33+
import { CDPSession } from './cdpSession';
3334

3435
export class Connection {
3536
readonly _objects = new Map<string, ChannelOwner<any, any>>();
@@ -192,6 +193,10 @@ export class ConnectionScope {
192193
case 'browserType':
193194
result = new BrowserType(this, guid, initializer);
194195
break;
196+
case 'cdpSession':
197+
// Chromium-specific.
198+
result = new CDPSession(this, guid, initializer);
199+
break;
195200
case 'context':
196201
result = new BrowserContext(this, guid, initializer);
197202
break;

src/rpc/server/browserDispatcher.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { Browser, BrowserBase } from '../../browser';
1818
import { BrowserContextBase } from '../../browserContext';
1919
import { Events } from '../../events';
2020
import * as types from '../../types';
21-
import { BrowserChannel, BrowserContextChannel, BrowserInitializer } from '../channels';
21+
import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel } from '../channels';
2222
import { BrowserContextDispatcher } from './browserContextDispatcher';
23+
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
2324
import { Dispatcher, DispatcherScope } from './dispatcher';
25+
import { CRBrowser } from '../../chromium/crBrowser';
2426

2527
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
2628
constructor(scope: DispatcherScope, browser: BrowserBase) {
@@ -38,4 +40,10 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
3840
async close(): Promise<void> {
3941
await this._object.close();
4042
}
43+
44+
// Chromium-specific.
45+
async newBrowserCDPSession(): Promise<CDPSessionChannel> {
46+
const crBrowser = this._object as CRBrowser;
47+
return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession());
48+
}
4149
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { CRSession, CRSessionEvents } from '../../chromium/crConnection';
18+
import { CDPSessionChannel, CDPSessionInitializer } from '../channels';
19+
import { Dispatcher, DispatcherScope } from './dispatcher';
20+
21+
export class CDPSessionDispatcher extends Dispatcher<CRSession, CDPSessionInitializer> implements CDPSessionChannel {
22+
constructor(scope: DispatcherScope, crSession: CRSession) {
23+
super(scope, crSession, 'cdpSession', {}, true);
24+
crSession._eventListener = (method, params) => this._dispatchEvent('event', { method, params });
25+
crSession.on(CRSessionEvents.Disconnected, () => {
26+
this._dispatchEvent('disconnected');
27+
this._scope.dispose();
28+
});
29+
}
30+
31+
async send(params: { method: string, params?: Object }): Promise<Object> {
32+
return this._object.send(params.method as any, params.params);
33+
}
34+
35+
async detach(): Promise<void> {
36+
return this._object.detach();
37+
}
38+
}

test/channels.spec.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
* limitations under the License.
1616
*/
1717

18-
const path = require('path');
19-
const util = require('util');
20-
const vm = require('vm');
2118
const { FFOX, CHROMIUM, WEBKIT, WIN, CHANNEL } = require('./utils').testOptions(browserType);
2219

2320
describe.skip(!CHANNEL)('Channels', function() {
2421
it('should work', async({browser}) => {
2522
expect(!!browser._channel).toBeTruthy();
2623
});
24+
2725
it('should scope context handles', async({browser, server}) => {
2826
const GOLDEN_PRECONDITION = {
2927
objects: [ 'chromium', 'browser' ],
@@ -50,7 +48,31 @@ describe.skip(!CHANNEL)('Channels', function() {
5048
await expectScopeState(browser, GOLDEN_PRECONDITION);
5149
});
5250

53-
it('should browser handles', async({browserType, defaultBrowserOptions}) => {
51+
it('should scope CDPSession handles', async({browserType, browser, server}) => {
52+
const GOLDEN_PRECONDITION = {
53+
objects: [ 'chromium', 'browser' ],
54+
scopes: [
55+
{ _guid: '', objects: [ 'chromium', 'browser' ] },
56+
{ _guid: 'browser', objects: [] }
57+
]
58+
};
59+
await expectScopeState(browserType, GOLDEN_PRECONDITION);
60+
61+
const session = await browser.newBrowserCDPSession();
62+
await expectScopeState(browserType, {
63+
objects: [ 'chromium', 'browser', 'cdpSession' ],
64+
scopes: [
65+
{ _guid: '', objects: [ 'chromium', 'browser' ] },
66+
{ _guid: 'browser', objects: ['cdpSession'] },
67+
{ _guid: 'cdpSession', objects: [] },
68+
]
69+
});
70+
71+
await session.detach();
72+
await expectScopeState(browserType, GOLDEN_PRECONDITION);
73+
});
74+
75+
it('should scope browser handles', async({browserType, defaultBrowserOptions}) => {
5476
const GOLDEN_PRECONDITION = {
5577
objects: [ 'chromium', 'browser' ],
5678
scopes: [
@@ -96,4 +118,4 @@ function trimGuids(object) {
96118
if (typeof object === 'string')
97119
return object ? object.match(/[^@]+/)[0] : '';
98120
return object;
99-
}
121+
}

test/chromium/session.spec.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,18 @@ describe.skip(CHANNEL)('ChromiumBrowserContext.createSession', function() {
9595
await context.close();
9696
});
9797
});
98-
describe.skip(CHANNEL)('ChromiumBrowser.newBrowserCDPSession', function() {
98+
describe('ChromiumBrowser.newBrowserCDPSession', function() {
9999
it('should work', async function({page, browser, server}) {
100100
const session = await browser.newBrowserCDPSession();
101+
101102
const version = await session.send('Browser.getVersion');
102103
expect(version.userAgent).toBeTruthy();
104+
105+
let gotEvent = false;
106+
session.on('Target.targetCreated', () => gotEvent = true);
107+
await session.send('Target.setDiscoverTargets', { discover: true });
108+
expect(gotEvent).toBe(true);
109+
103110
await session.detach();
104111
});
105112
});

utils/doclint/Source.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ const writeFileAsync = util.promisify(fs.writeFile);
2525

2626
const PROJECT_DIR = path.join(__dirname, '..', '..');
2727

28-
async function recursiveReadDir(dirPath) {
28+
async function recursiveReadDir(dirPath, exclude) {
2929
const files = [];
30+
if (exclude.includes(dirPath))
31+
return files;
3032
for (const file of await readdirAsync(dirPath)) {
3133
const fullPath = path.join(dirPath, file);
3234
if ((await statAsync(fullPath)).isDirectory())
33-
files.push(...await recursiveReadDir(fullPath))
35+
files.push(...await recursiveReadDir(fullPath, exclude))
3436
else
3537
files.push(fullPath);
3638
}
@@ -100,7 +102,7 @@ class Source {
100102
async save() {
101103
await writeFileAsync(this.filePath(), this.text());
102104
}
103-
105+
104106
async saveAs(path) {
105107
await writeFileAsync(path, this.text());
106108
}
@@ -118,11 +120,12 @@ class Source {
118120
/**
119121
* @param {string} dirPath
120122
* @param {string=} extension
123+
* @param {Array<string>=} exclude
121124
* @return {!Promise<!Array<!Source>>}
122125
*/
123-
static async readdir(dirPath, extension = '') {
126+
static async readdir(dirPath, extension = '', exclude = []) {
124127
extension = extension.toLowerCase();
125-
const filePaths = (await recursiveReadDir(dirPath)).filter(fileName => fileName.toLowerCase().endsWith(extension));
128+
const filePaths = (await recursiveReadDir(dirPath, exclude)).filter(fileName => fileName.toLowerCase().endsWith(extension));
126129
return Promise.all(filePaths.map(filePath => Source.readFile(filePath)));
127130
}
128131
}

0 commit comments

Comments
 (0)