diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index ed05aee9b4..6ac2ae5348 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -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 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); + }); } } diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 46dfe7cf2b..67a7d6b49d 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -52,6 +52,9 @@ export interface BrowserChannel extends Channel { close(): Promise; newContext(params: types.BrowserContextOptions): Promise; + + // Chromium-specific. + newBrowserCDPSession(): Promise; } 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; + detach(): Promise; +} +export type CDPSessionInitializer = {}; diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index 2cfe9f4b92..99e1667079 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -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 { readonly _contexts = new Set(); @@ -77,4 +78,9 @@ export class Browser extends ChannelOwner { this._isClosedOrClosing = true; await this._channel.close(); } + + // Chromium-specific. + async newBrowserCDPSession(): Promise { + return CDPSession.from(await this._channel.newBrowserCDPSession()); + } } diff --git a/src/rpc/client/cdpSession.ts b/src/rpc/client/cdpSession.ts new file mode 100644 index 0000000000..a813b590d0 --- /dev/null +++ b/src/rpc/client/cdpSession.ts @@ -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 { + static from(cdpSession: CDPSessionChannel): CDPSession { + return (cdpSession as any)._object; + } + + on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + once: (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( + method: T, + params?: Protocol.CommandParameters[T] + ): Promise { + const result = await this._channel.send({ method, params }); + return result as Protocol.CommandReturnValues[T]; + } + + async detach() { + return this._channel.detach(); + } +} diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index dc0598ec0a..77c15651d9 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -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>(); @@ -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; diff --git a/src/rpc/server/browserDispatcher.ts b/src/rpc/server/browserDispatcher.ts index 826a77f2f4..c19b203dff 100644 --- a/src/rpc/server/browserDispatcher.ts +++ b/src/rpc/server/browserDispatcher.ts @@ -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 implements BrowserChannel { constructor(scope: DispatcherScope, browser: BrowserBase) { @@ -38,4 +40,10 @@ export class BrowserDispatcher extends Dispatcher i async close(): Promise { await this._object.close(); } + + // Chromium-specific. + async newBrowserCDPSession(): Promise { + const crBrowser = this._object as CRBrowser; + return new CDPSessionDispatcher(this._scope, await crBrowser.newBrowserCDPSession()); + } } diff --git a/src/rpc/server/cdpSessionDispatcher.ts b/src/rpc/server/cdpSessionDispatcher.ts new file mode 100644 index 0000000000..26e3ff7d57 --- /dev/null +++ b/src/rpc/server/cdpSessionDispatcher.ts @@ -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 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 { + return this._object.send(params.method as any, params.params); + } + + async detach(): Promise { + return this._object.detach(); + } +} diff --git a/test/channels.spec.js b/test/channels.spec.js index c12f025b9d..ed96cedddc 100644 --- a/test/channels.spec.js +++ b/test/channels.spec.js @@ -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; -} \ No newline at end of file +} diff --git a/test/chromium/session.spec.js b/test/chromium/session.spec.js index eb7c2cefbb..c347f05c50 100644 --- a/test/chromium/session.spec.js +++ b/test/chromium/session.spec.js @@ -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(); }); }); diff --git a/utils/doclint/Source.js b/utils/doclint/Source.js index 8656803840..ccc6df3097 100644 --- a/utils/doclint/Source.js +++ b/utils/doclint/Source.js @@ -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=} exclude * @return {!Promise>} */ - 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))); } } diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 4b8bdf3408..559c5085b9 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -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(); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 4b3d04d2d9..ee42112b6d 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -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');