feat(rpc): introduce protocol.pdl (#3054)

We now generate channels.ts from the protocol definition. There are still some shortcomings,
like union types - these will be addressed in follow ups.
This commit is contained in:
Dmitry Gozman 2020-07-20 17:38:06 -07:00 committed by GitHub
parent 726f636b5c
commit 5848ed8f41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 3420 additions and 494 deletions

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,6 @@ export class Accessibility {
async snapshot(options: { interestingOnly?: boolean; root?: ElementHandle } = {}): Promise<types.SerializedAXNode | null> {
const root = options.root ? options.root._elementChannel : undefined;
const result = await this._channel.accessibilitySnapshot({ interestingOnly: options.interestingOnly, root });
return result.rootAXNode;
return result.rootAXNode || null;
}
}

View file

@ -15,7 +15,7 @@
*/
import * as types from '../../types';
import { BrowserChannel, BrowserInitializer, BrowserContextOptions } from '../channels';
import { BrowserChannel, BrowserInitializer, BrowserNewContextParams } from '../channels';
import { BrowserContext } from './browserContext';
import { Page } from './page';
import { ChannelOwner } from './channelOwner';
@ -55,7 +55,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browser.newContext', async () => {
const contextOptions: BrowserContextOptions = {
const contextOptions: BrowserNewContextParams = {
...options,
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
};

View file

@ -141,7 +141,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
return this._wrapApiCall('browserContext.setGeolocation', async () => {
await this._channel.setGeolocation({ geolocation });
await this._channel.setGeolocation({ geolocation: geolocation || undefined });
});
}
@ -159,7 +159,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
return this._wrapApiCall('browserContext.setHTTPCredentials', async () => {
await this._channel.setHTTPCredentials({ httpCredentials });
await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || undefined });
});
}

View file

@ -15,7 +15,7 @@
*/
import * as types from '../../types';
import { BrowserTypeChannel, BrowserTypeInitializer, LaunchPersistentContextOptions, LaunchOptions, LaunchServerOptions } from '../channels';
import { BrowserTypeChannel, BrowserTypeInitializer, BrowserTypeLaunchParams, BrowserTypeLaunchServerParams, BrowserTypeLaunchPersistentContextParams } from '../channels';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
@ -45,7 +45,7 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launch', async () => {
const launchOptions: LaunchOptions = {
const launchOptions: BrowserTypeLaunchParams = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
@ -61,7 +61,7 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launchServer', async () => {
const launchServerOptions: LaunchServerOptions = {
const launchServerOptions: BrowserTypeLaunchServerParams = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
@ -75,7 +75,7 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launchPersistentContext', async () => {
const persistentOptions: LaunchPersistentContextOptions = {
const persistentOptions: BrowserTypeLaunchPersistentContextParams = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),

View file

@ -37,11 +37,11 @@ export class Download extends ChannelOwner<DownloadChannel, DownloadInitializer>
}
async path(): Promise<string | null> {
return (await this._channel.path()).value;
return (await this._channel.path()).value || null;
}
async failure(): Promise<string | null> {
return (await this._channel.failure()).error;
return (await this._channel.failure()).error || null;
}
async createReadStream(): Promise<Readable | null> {

View file

@ -15,17 +15,18 @@
*/
import * as types from '../../types';
import { ElectronChannel, ElectronInitializer, ElectronLaunchOptions, ElectronApplicationChannel, ElectronApplicationInitializer } from '../channels';
import { ElectronChannel, ElectronInitializer, ElectronApplicationChannel, ElectronApplicationInitializer, ElectronLaunchParams } from '../channels';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Page } from './page';
import { serializeArgument, FuncOn, parseResult, SmartHandle, JSHandle } from './jsHandle';
import { ElectronEvents } from '../../server/electron';
import { ElectronEvents, ElectronLaunchOptionsBase } from '../../server/electron';
import { TimeoutSettings } from '../../timeoutSettings';
import { Waiter } from './waiter';
import { TimeoutError } from '../../errors';
import { Events } from '../../events';
import { LoggerSink } from '../../loggerSink';
import { envObjectToArray } from '../serializers';
export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer> {
static from(electron: ElectronChannel): Electron {
@ -36,11 +37,16 @@ export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer>
super(parent, type, guid, initializer, true);
}
async launch(executablePath: string, options: ElectronLaunchOptions & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
async launch(executablePath: string, options: ElectronLaunchOptionsBase & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('electron.launch', async () => {
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
const params: ElectronLaunchParams = {
...options,
env: options.env ? envObjectToArray(options.env) : undefined,
executablePath,
};
return ElectronApplication.from((await this._channel.launch(params)).electronApplication);
}, logger);
}
}

View file

@ -29,7 +29,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
return (handle as any)._object;
}
static fromNullable(handle: ElementHandleChannel | null): ElementHandle | null {
static fromNullable(handle: ElementHandleChannel | undefined): ElementHandle | null {
return handle ? ElementHandle.from(handle) : null;
}
@ -56,13 +56,15 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
async getAttribute(name: string): Promise<string | null> {
return this._wrapApiCall('elementHandle.getAttribute', async () => {
return (await this._elementChannel.getAttribute({ name })).value;
const value = (await this._elementChannel.getAttribute({ name })).value;
return value === undefined ? null : value;
});
}
async textContent(): Promise<string | null> {
return this._wrapApiCall('elementHandle.textContent', async () => {
return (await this._elementChannel.textContent()).value;
const value = (await this._elementChannel.textContent()).value;
return value === undefined ? null : value;
});
}
@ -165,7 +167,8 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
async boundingBox(): Promise<types.Rect | null> {
return this._wrapApiCall('elementHandle.boundingBox', async () => {
return (await this._elementChannel.boundingBox()).value;
const value = (await this._elementChannel.boundingBox()).value;
return value === undefined ? null : value;
});
}

View file

@ -53,7 +53,7 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
return (frame as any)._object;
}
static fromNullable(frame: FrameChannel | null): Frame | null {
static fromNullable(frame: FrameChannel | undefined): Frame | null {
return frame ? Frame.from(frame) : null;
}
@ -132,7 +132,7 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
});
}
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request || null) : null;
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
const response = request ? await waiter.waitForPromise(request._finalRequest().response()) : null;
waiter.dispose();
return response;
@ -306,7 +306,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
return this._wrapApiCall(this._apiName('textContent'), async () => {
return (await this._channel.textContent({ selector, ...options })).value;
const value = (await this._channel.textContent({ selector, ...options })).value;
return value === undefined ? null : value;
});
}
@ -324,7 +325,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
return this._wrapApiCall(this._apiName('getAttribute'), async () => {
return (await this._channel.getAttribute({ selector, name, ...options })).value;
const value = (await this._channel.getAttribute({ selector, name, ...options })).value;
return value === undefined ? null : value;
});
}

View file

@ -14,10 +14,10 @@
* limitations under the License.
*/
import { JSHandleChannel, JSHandleInitializer, SerializedArgument, Channel } from '../channels';
import { JSHandleChannel, JSHandleInitializer, SerializedArgument, SerializedValue, Channel } from '../channels';
import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner';
import { serializeAsCallArgument, parseEvaluationResultValue, SerializedValue } from '../../common/utilityScriptSerializers';
import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers';
type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
type Unboxed<Arg> =
@ -42,10 +42,6 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
return (handle as any)._object;
}
static fromNullable(handle: JSHandleChannel | null): JSHandle | null {
return handle ? JSHandle.from(handle) : null;
}
constructor(parent: ChannelOwner, type: string, guid: string, initializer: JSHandleInitializer) {
super(parent, type, guid, initializer);
this._preview = this._initializer.preview;
@ -112,5 +108,5 @@ export function serializeArgument(arg: any): SerializedArgument {
}
export function parseResult(arg: SerializedValue): any {
return parseEvaluationResultValue(arg, []);
return parseEvaluationResultValue(arg as any, []);
}

View file

@ -19,7 +19,7 @@ import * as types from '../../types';
import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Frame } from './frame';
import { normalizeFulfillParameters, headersArrayToObject, normalizeContinueOverrides } from '../serializers';
import { normalizeFulfillParameters, headersArrayToObject, normalizeContinueOverrides, parseError } from '../serializers';
export type NetworkCookie = {
name: string,
@ -54,7 +54,7 @@ export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
return (request as any)._object;
}
static fromNullable(request: RequestChannel | null): Request | null {
static fromNullable(request: RequestChannel | undefined): Request | null {
return request ? Request.from(request) : null;
}
@ -79,7 +79,7 @@ export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
}
postData(): string | null {
return this._initializer.postData;
return this._initializer.postData || null;
}
postDataJSON(): Object | null {
@ -174,7 +174,7 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
return (response as any)._object;
}
static fromNullable(response: ResponseChannel | null): Response | null {
static fromNullable(response: ResponseChannel | undefined): Response | null {
return response ? Response.from(response) : null;
}
@ -204,7 +204,10 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
}
async finished(): Promise<Error | null> {
return (await this._channel.finished()).error;
const result = await this._channel.finished();
if (result.error)
return parseError(result.error);
return null;
}
async body(): Promise<Buffer> {

View file

@ -20,8 +20,8 @@ import { Events } from '../../events';
import { assert, assertMaxArguments, helper, Listener } from '../../helper';
import { TimeoutSettings } from '../../timeoutSettings';
import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, PageChannel, PageInitializer, PDFOptions } from '../channels';
import { parseError, serializeError, headersObjectToArray } from '../serializers';
import { BindingCallChannel, BindingCallInitializer, PageChannel, PageInitializer, PagePdfParams } from '../channels';
import { parseError, headersObjectToArray, serializeError } from '../serializers';
import { Accessibility } from './accessibility';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
@ -69,7 +69,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
return (page as any)._object;
}
static fromNullable(page: PageChannel | null): Page | null {
static fromNullable(page: PageChannel | undefined): Page | null {
return page ? Page.from(page) : null;
}
@ -86,7 +86,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
this._mainFrame = Frame.from(initializer.mainFrame);
this._mainFrame._page = this;
this._frames.add(this._mainFrame);
this._viewportSize = initializer.viewportSize;
this._viewportSize = initializer.viewportSize || null;
this._closed = initializer.isClosed;
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
@ -115,8 +115,8 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
}
}
private _onRequestFailed(request: Request, failureText: string | null) {
request._failureText = failureText;
private _onRequestFailed(request: Request, failureText: string | undefined) {
request._failureText = failureText || null;
this.emit(Events.Page.RequestFailed, request);
}
@ -518,7 +518,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
async _pdf(options: types.PDFOptions = {}): Promise<Buffer> {
const path = options.path;
const transportOptions: PDFOptions = { ...options } as PDFOptions;
const transportOptions: PagePdfParams = { ...options } as PagePdfParams;
if (path)
delete (transportOptions as any).path;
if (transportOptions.margin)

1483
src/rpc/protocol.pdl Normal file

File diff suppressed because it is too large Load diff

View file

@ -21,24 +21,25 @@ import * as util from 'util';
import { TimeoutError } from '../errors';
import * as types from '../types';
import { helper, assert } from '../helper';
import { SerializedError } from './channels';
import { serializeAsCallArgument, parseEvaluationResultValue } from '../common/utilityScriptSerializers';
export function serializeError(e: any): types.Error {
export function serializeError(e: any): SerializedError {
if (helper.isError(e))
return { message: e.message, stack: e.stack, name: e.name };
return { value: e };
return { error: { message: e.message, stack: e.stack, name: e.name } };
return { value: serializeAsCallArgument(e, value => ({ fallThrough: value })) };
}
export function parseError(error: types.Error): any {
if (error.message === undefined)
return error.value;
if (error.name === 'TimeoutError') {
const e = new TimeoutError(error.message);
e.stack = error.stack;
export function parseError(error: SerializedError): Error {
if (!error.error)
return parseEvaluationResultValue(error.value as any, []);
if (error.error.name === 'TimeoutError') {
const e = new TimeoutError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
const e = new Error(error.message);
e.stack = error.stack;
const e = new Error(error.error.message);
e.stack = error.error.stack || '';
return e;
}

View file

@ -19,7 +19,7 @@ import { BrowserContextBase, BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
import { PageChannel, BrowserContextChannel, BrowserContextInitializer, CDPSessionChannel } from '../channels';
import { PageChannel, BrowserContextChannel, BrowserContextInitializer, CDPSessionChannel, BrowserContextSetGeolocationParams, BrowserContextSetHTTPCredentialsParams } from '../channels';
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../../chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
@ -91,8 +91,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
await this._context.clearPermissions();
}
async setGeolocation(params: { geolocation: types.Geolocation | null }): Promise<void> {
await this._context.setGeolocation(params.geolocation);
async setGeolocation(params: BrowserContextSetGeolocationParams): Promise<void> {
await this._context.setGeolocation(params.geolocation || null);
}
async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise<void> {
@ -103,8 +103,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
await this._context.setOffline(params.offline);
}
async setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise<void> {
await this._context.setHTTPCredentials(params.httpCredentials);
async setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams): Promise<void> {
await this._context.setHTTPCredentials(params.httpCredentials || null);
}
async addInitScript(params: { source: string }): Promise<void> {

View file

@ -17,7 +17,7 @@
import { Browser, BrowserBase } from '../../browser';
import { BrowserContextBase } from '../../browserContext';
import { Events } from '../../events';
import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel, Binary, BrowserContextOptions } from '../channels';
import { BrowserChannel, BrowserContextChannel, BrowserInitializer, CDPSessionChannel, Binary, BrowserNewContextParams } from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
@ -34,7 +34,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
});
}
async newContext(params: BrowserContextOptions): Promise<{ context: BrowserContextChannel }> {
async newContext(params: BrowserNewContextParams): Promise<{ context: BrowserContextChannel }> {
const options = {
...params,
extraHTTPHeaders: params.extraHTTPHeaders ? headersArrayToObject(params.extraHTTPHeaders) : undefined,

View file

@ -18,7 +18,7 @@ import { BrowserBase } from '../../browser';
import { BrowserTypeBase, BrowserType } from '../../server/browserType';
import * as types from '../../types';
import { BrowserDispatcher } from './browserDispatcher';
import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel, BrowserTypeInitializer, BrowserServerChannel, LaunchPersistentContextOptions, LaunchOptions, LaunchServerOptions } from '../channels';
import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel, BrowserTypeInitializer, BrowserServerChannel, BrowserTypeLaunchParams, BrowserTypeLaunchPersistentContextParams, BrowserTypeLaunchServerParams } from '../channels';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { BrowserContextBase } from '../../browserContext';
import { BrowserContextDispatcher } from './browserContextDispatcher';
@ -33,7 +33,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
}, true, browserType.name());
}
async launch(params: LaunchOptions): Promise<{ browser: BrowserChannel }> {
async launch(params: BrowserTypeLaunchParams): Promise<{ browser: BrowserChannel }> {
const options = {
...params,
ignoreDefaultArgs: params.ignoreAllDefaultArgs ? true : params.ignoreDefaultArgs,
@ -43,7 +43,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
return { browser: new BrowserDispatcher(this._scope, browser as BrowserBase) };
}
async launchPersistentContext(params: LaunchPersistentContextOptions): Promise<{ context: BrowserContextChannel }> {
async launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams): Promise<{ context: BrowserContextChannel }> {
const options = {
...params,
ignoreDefaultArgs: params.ignoreAllDefaultArgs ? true : params.ignoreDefaultArgs,
@ -54,7 +54,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
return { context: new BrowserContextDispatcher(this._scope, browserContext as BrowserContextBase) };
}
async launchServer(params: LaunchServerOptions): Promise<{ server: BrowserServerChannel }> {
async launchServer(params: BrowserTypeLaunchServerParams): Promise<{ server: BrowserServerChannel }> {
const options = {
...params,
ignoreDefaultArgs: params.ignoreAllDefaultArgs ? true : params.ignoreDefaultArgs,

View file

@ -31,8 +31,8 @@ export function existingDispatcher<DispatcherType>(object: any): DispatcherType
return object[dispatcherSymbol];
}
export function lookupNullableDispatcher<DispatcherType>(object: any | null): DispatcherType | null {
return object ? lookupDispatcher(object) : null;
export function lookupNullableDispatcher<DispatcherType>(object: any | null): DispatcherType | undefined {
return object ? lookupDispatcher(object) : undefined;
}
export class Dispatcher<Type, Initializer> extends EventEmitter implements Channel {

View file

@ -27,20 +27,22 @@ export class DownloadDispatcher extends Dispatcher<Download, DownloadInitializer
});
}
async path(): Promise<{ value: string | null }> {
return { value: await this._object.path() };
async path(): Promise<{ value?: string }> {
const path = await this._object.path();
return { value: path || undefined };
}
async stream(): Promise<{ stream: StreamChannel | null }> {
async stream(): Promise<{ stream?: StreamChannel }> {
const stream = await this._object.createReadStream();
if (!stream)
return { stream: null };
return {};
await new Promise(f => stream.on('readable', f));
return { stream: new StreamDispatcher(this._scope, stream) };
}
async failure(): Promise<{ error: string | null }> {
return { error: await this._object.failure() };
async failure(): Promise<{ error?: string }> {
const error = await this._object.failure();
return { error: error || undefined };
}
async delete(): Promise<void> {

View file

@ -16,21 +16,26 @@
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron';
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions, SerializedArgument } from '../channels';
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, SerializedArgument, ElectronLaunchParams } from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserContextBase } from '../../browserContext';
import { PageDispatcher } from './pageDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { createHandle } from './elementHandlerDispatcher';
import { SerializedValue } from '../../common/utilityScriptSerializers';
import { envArrayToObject } from '../serializers';
export class ElectronDispatcher extends Dispatcher<Electron, ElectronInitializer> implements ElectronChannel {
constructor(scope: DispatcherScope, electron: Electron) {
super(scope, electron, 'electron', {}, true);
}
async launch(params: { executablePath: string } & ElectronLaunchOptions): Promise<{ electronApplication: ElectronApplicationChannel }> {
const electronApplication = await this._object.launch(params.executablePath, params);
async launch(params: ElectronLaunchParams): Promise<{ electronApplication: ElectronApplicationChannel }> {
const options = {
...params,
env: params.env ? envArrayToObject(params.env) : undefined,
};
const electronApplication = await this._object.launch(params.executablePath, options);
return { electronApplication: new ElectronApplicationDispatcher(this._scope, electronApplication) };
}
}

View file

@ -30,9 +30,9 @@ export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHan
export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel {
readonly _elementHandle: ElementHandle;
static createNullable(scope: DispatcherScope, handle: ElementHandle | null): ElementHandleDispatcher | null {
static createNullable(scope: DispatcherScope, handle: ElementHandle | null): ElementHandleDispatcher | undefined {
if (!handle)
return null;
return undefined;
return new ElementHandleDispatcher(scope, handle);
}
@ -41,20 +41,22 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
this._elementHandle = elementHandle;
}
async ownerFrame(): Promise<{ frame: FrameChannel | null }> {
async ownerFrame(): Promise<{ frame?: FrameChannel }> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.ownerFrame()) };
}
async contentFrame(): Promise<{ frame: FrameChannel | null }> {
async contentFrame(): Promise<{ frame?: FrameChannel }> {
return { frame: lookupNullableDispatcher<FrameDispatcher>(await this._elementHandle.contentFrame()) };
}
async getAttribute(params: { name: string }): Promise<{ value: string | null }> {
return { value: await this._elementHandle.getAttribute(params.name) };
async getAttribute(params: { name: string }): Promise<{ value?: string }> {
const value = await this._elementHandle.getAttribute(params.name);
return { value: value === null ? undefined : value };
}
async textContent(): Promise<{ value: string | null }> {
return { value: await this._elementHandle.textContent() };
async textContent(): Promise<{ value?: string }> {
const value = await this._elementHandle.textContent();
return { value: value === null ? undefined : value };
}
async innerText(): Promise<{ value: string }> {
@ -121,17 +123,18 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
await this._elementHandle.uncheck(params);
}
async boundingBox(): Promise<{ value: types.Rect | null }> {
return { value: await this._elementHandle.boundingBox() };
async boundingBox(): Promise<{ value?: types.Rect }> {
const value = await this._elementHandle.boundingBox();
return { value: value || undefined };
}
async screenshot(params: types.ElementScreenshotOptions): Promise<{ binary: Binary }> {
return { binary: (await this._elementHandle.screenshot(params)).toString('base64') };
}
async querySelector(params: { selector: string }): Promise<{ element: ElementHandleChannel | null }> {
async querySelector(params: { selector: string }): Promise<{ element?: ElementHandleChannel }> {
const handle = await this._elementHandle.$(params.selector);
return { element: handle ? new ElementHandleDispatcher(this._scope, handle) : null };
return { element: handle ? new ElementHandleDispatcher(this._scope, handle) : undefined };
}
async querySelectorAll(params: { selector: string }): Promise<{ elements: ElementHandleChannel[] }> {

View file

@ -53,11 +53,11 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
});
}
async goto(params: { url: string } & types.GotoOptions): Promise<{ response: ResponseChannel | null }> {
async goto(params: { url: string } & types.GotoOptions): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(params.url, params)) };
}
async waitForNavigation(params: types.WaitForNavigationOptions): Promise<{ response: ResponseChannel | null }> {
async waitForNavigation(params: types.WaitForNavigationOptions): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.waitForNavigation(params)) };
}
@ -73,7 +73,7 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return { handle: createHandle(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) };
}
async waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element: ElementHandleChannel | null }> {
async waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element?: ElementHandleChannel }> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(params.selector, params)) };
}
@ -89,7 +89,7 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
}
async querySelector(params: { selector: string }): Promise<{ element: ElementHandleChannel | null }> {
async querySelector(params: { selector: string }): Promise<{ element?: ElementHandleChannel }> {
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.$(params.selector)) };
}
@ -130,8 +130,9 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
await this._frame.focus(params.selector, params);
}
async textContent(params: { selector: string } & types.TimeoutOptions): Promise<{ value: string | null }> {
return { value: await this._frame.textContent(params.selector, params) };
async textContent(params: { selector: string } & types.TimeoutOptions): Promise<{ value?: string }> {
const value = await this._frame.textContent(params.selector, params);
return { value: value === null ? undefined : value };
}
async innerText(params: { selector: string } & types.TimeoutOptions): Promise<{ value: string }> {
@ -142,8 +143,9 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return { value: await this._frame.innerHTML(params.selector, params) };
}
async getAttribute(params: { selector: string, name: string } & types.TimeoutOptions): Promise<{ value: string | null }> {
return { value: await this._frame.getAttribute(params.selector, params.name, params) };
async getAttribute(params: { selector: string, name: string } & types.TimeoutOptions): Promise<{ value?: string }> {
const value = await this._frame.getAttribute(params.selector, params.name, params);
return { value: value === null ? undefined : value };
}
async hover(params: { selector: string } & types.PointerActionOptions & types.TimeoutOptions & { force?: boolean }): Promise<void> {

View file

@ -63,7 +63,7 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
// Generic channel parser converts guids to JSHandleDispatchers,
// and this function takes care of coverting them into underlying JSHandles.
export function parseArgument(arg: SerializedArgument): any {
return parseEvaluationResultValue(arg.value, arg.handles.map(arg => (arg as JSHandleDispatcher)._object));
return parseEvaluationResultValue(arg.value as any, arg.handles.map(arg => (arg as JSHandleDispatcher)._object));
}
export function serializeResult(arg: any): SerializedValue {

View file

@ -15,10 +15,10 @@
*/
import { Request, Response, Route } from '../../network';
import { RequestChannel, ResponseChannel, RouteChannel, ResponseInitializer, RequestInitializer, RouteInitializer, Binary } from '../channels';
import { RequestChannel, ResponseChannel, RouteChannel, ResponseInitializer, RequestInitializer, RouteInitializer, Binary, SerializedError } from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { headersObjectToArray, headersArrayToObject } from '../serializers';
import { headersObjectToArray, headersArrayToObject, serializeError } from '../serializers';
import * as types from '../../types';
export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> implements RequestChannel {
@ -28,24 +28,25 @@ export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> i
return result || new RequestDispatcher(scope, request);
}
static fromNullable(scope: DispatcherScope, request: Request | null): RequestDispatcher | null {
return request ? RequestDispatcher.from(scope, request) : null;
static fromNullable(scope: DispatcherScope, request: Request | null): RequestDispatcher | undefined {
return request ? RequestDispatcher.from(scope, request) : undefined;
}
private constructor(scope: DispatcherScope, request: Request) {
const postData = request.postData();
super(scope, request, 'request', {
frame: FrameDispatcher.from(scope, request.frame()),
url: request.url(),
resourceType: request.resourceType(),
method: request.method(),
postData: request.postData(),
postData: postData === null ? undefined : postData,
headers: headersObjectToArray(request.headers()),
isNavigationRequest: request.isNavigationRequest(),
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
});
}
async response(): Promise<{ response: ResponseChannel | null }> {
async response(): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._object.response()) };
}
}
@ -63,8 +64,9 @@ export class ResponseDispatcher extends Dispatcher<Response, ResponseInitializer
});
}
async finished(): Promise<{ error: Error | null }> {
return { error: await this._object.finished() };
async finished(): Promise<{ error?: SerializedError }> {
const error = await this._object.finished();
return { error: error ? serializeError(error) : undefined };
}
async body(): Promise<{ binary: Binary }> {

View file

@ -20,7 +20,7 @@ import { Frame } from '../../frames';
import { Request } from '../../network';
import { Page, Worker } from '../../page';
import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, PDFOptions, SerializedArgument } from '../channels';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, SerializedArgument, PagePdfParams, SerializedError } from '../channels';
import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher';
import { parseError, serializeError, headersArrayToObject } from '../serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
@ -42,7 +42,7 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
// If we split pageCreated and pageReady, there should be no main frame during pageCreated.
super(scope, page, 'page', {
mainFrame: FrameDispatcher.from(scope, page.mainFrame()),
viewportSize: page.viewportSize(),
viewportSize: page.viewportSize() || undefined,
isClosed: page.isClosed()
});
this._page = page;
@ -79,7 +79,7 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
this._page.setDefaultTimeout(params.timeout);
}
async opener(): Promise<{ page: PageChannel | null }> {
async opener(): Promise<{ page?: PageChannel }> {
return { page: lookupNullableDispatcher<PageDispatcher>(await this._page.opener()) };
}
@ -95,15 +95,15 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
await this._page.setExtraHTTPHeaders(headersArrayToObject(params.headers));
}
async reload(params: types.NavigateOptions): Promise<{ response: ResponseChannel | null }> {
async reload(params: types.NavigateOptions): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(params)) };
}
async goBack(params: types.NavigateOptions): Promise<{ response: ResponseChannel | null }> {
async goBack(params: types.NavigateOptions): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(params)) };
}
async goForward(params: types.NavigateOptions): Promise<{ response: ResponseChannel | null }> {
async goForward(params: types.NavigateOptions): Promise<{ response?: ResponseChannel }> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(params)) };
}
@ -180,15 +180,15 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
await this._page.mouse.click(params.x, params.y, params);
}
async accessibilitySnapshot(params: { interestingOnly?: boolean, root?: ElementHandleChannel }): Promise<{ rootAXNode: types.SerializedAXNode | null }> {
async accessibilitySnapshot(params: { interestingOnly?: boolean, root?: ElementHandleChannel }): Promise<{ rootAXNode?: types.SerializedAXNode }> {
const rootAXNode = await this._page.accessibility.snapshot({
interestingOnly: params.interestingOnly,
root: params.root ? (params.root as ElementHandleDispatcher)._elementHandle : undefined
});
return { rootAXNode };
return { rootAXNode: rootAXNode || undefined };
}
async pdf(params: PDFOptions): Promise<{ pdf: Binary }> {
async pdf(params: PagePdfParams): Promise<{ pdf: Binary }> {
if (!this._page.pdf)
throw new Error('PDF generation is only supported for Headless Chromium');
const buffer = await this._page.pdf(params);
@ -263,11 +263,11 @@ export class BindingCallDispatcher extends Dispatcher<{}, BindingCallInitializer
return this._promise;
}
resolve(params: { result: SerializedArgument }) {
async resolve(params: { result: SerializedArgument }) {
this._resolve!(parseArgument(params.result));
}
reject(params: { error: types.Error }) {
async reject(params: { error: SerializedError }) {
this._reject!(parseError(params.error));
}
}

View file

@ -34,15 +34,14 @@ import { EventEmitter } from 'events';
import { helper } from '../helper';
import { LoggerSink } from '../loggerSink';
type ElectronLaunchOptions = {
export type ElectronLaunchOptionsBase = {
args?: string[],
cwd?: string,
env?: {[key: string]: string|number|boolean},
env?: types.Env,
handleSIGINT?: boolean,
handleSIGTERM?: boolean,
handleSIGHUP?: boolean,
timeout?: number,
logger?: LoggerSink,
};
export const ElectronEvents = {
@ -165,7 +164,7 @@ export class ElectronApplication extends EventEmitter {
}
export class Electron {
async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise<ElectronApplication> {
async launch(executablePath: string, options: ElectronLaunchOptionsBase & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
const {
args = [],
env = process.env,

View file

@ -348,8 +348,7 @@ export type ConsoleMessageLocation = {
};
export type Error = {
message?: string,
name?: string,
message: string,
name: string,
stack?: string,
value?: any
};

217
utils/generate_channels.js Executable file
View file

@ -0,0 +1,217 @@
#!/usr/bin/env node
/**
* 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.
*/
const fs = require('fs');
const path = require('path');
const channels = new Set();
function tokenize(source) {
const lines = source.split('\n').filter(line => {
const trimmed = line.trim();
return !!trimmed && trimmed[0] != '#';
});
const stack = [{ indent: -1, list: [], words: '' }];
for (const line of lines) {
const indent = line.length - line.trimLeft().length;
const o = { indent, list: [], words: line.split(' ').filter(word => !!word) };
let current = stack[stack.length - 1];
while (indent <= current.indent) {
stack.pop();
current = stack[stack.length - 1];
}
current.list.push(o);
stack.push(o);
}
return stack[0].list;
}
function raise(item) {
throw new Error(item.words.join(' '));
}
function titleCase(name) {
return name[0].toUpperCase() + name.substring(1);
}
function inlineType(type, item, indent) {
const array = type.endsWith('[]');
if (array)
type = type.substring(0, type.length - 2);
let inner = '';
if (type === 'enum') {
const literals = item.list.map(literal => {
if (literal.words.length > 1 || literal.list.length)
raise(literal);
return literal.words[0];
});
inner = literals.map(literal => `'${literal}'`).join(' | ');
if (array)
inner = `(${inner})`;
} else if (['string', 'boolean', 'number', 'undefined'].includes(type)) {
inner = type;
} else if (type === 'object') {
inner = `{\n${properties(item, indent + ' ')}\n${indent}}`;
} else if (type === 'binary') {
inner = 'Binary';
} else if (type === 'Error') {
inner = 'SerializedError';
} else if (channels.has(type)) {
inner = type + 'Channel';
} else {
inner = type;
}
return inner + (array ? '[]' : '');
}
function properties(item, indent) {
const result = [];
for (const prop of item.list) {
if (prop.words.length !== 2)
raise(prop);
let name = prop.words[0];
if (!name.endsWith(':'))
raise(item);
name = name.substring(0, name.length - 1);
const optional = name.endsWith('?');
if (optional)
name = name.substring(0, name.length - 1);
result.push(`${indent}${name}${optional ? '?' : ''}: ${inlineType(prop.words[1], prop, indent)},`);
}
return result.join('\n');
}
function objectType(name, item, indent) {
if (!item.list.length)
return `export type ${name} = {};`;
return `export type ${name} = {\n${properties(item, indent)}\n};`
}
const result = [
`/**
* 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.
*/
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import { EventEmitter } from 'events';
export type Binary = string;
export interface Channel extends EventEmitter {
}
`];
const pdl = fs.readFileSync(path.join(__dirname, '..', 'src', 'rpc', 'protocol.pdl'), 'utf-8');
const list = tokenize(pdl);
for (const item of list) {
if (item.words[0] === 'interface')
channels.add(item.words[1]);
}
for (const item of list) {
if (item.words[0] === 'union') {
if (item.words.length !== 2)
raise(item);
result.push(`export type ${item.words[1]} = ${item.list.map(clause => {
if (clause.words.length !== 1)
raise(clause);
return inlineType(clause.words[0], clause, ' ');
}).join(' | ')};`);
} else if (item.words[0] === 'type') {
if (item.words.length !== 2)
raise(item);
result.push(`export type ${item.words[1]} = {`);
result.push(properties(item, ' '));
result.push(`};`);
} else if (item.words[0] === 'interface') {
const channelName = item.words[1];
result.push(`// ----------- ${channelName} -----------`);
const init = item.list.find(i => i.words[0] === 'initializer');
if (init && init.words.length > 1)
raise(init);
result.push(objectType(channelName + 'Initializer', init || { list: [] }, ' '));
let extendsName = 'Channel';
if (item.words.length === 4 && item.words[2] === 'extends')
extendsName = item.words[3] + 'Channel';
else if (item.words.length !== 2)
raise(item);
result.push(`export interface ${channelName}Channel extends ${extendsName} {`);
const types = new Map();
for (const method of item.list) {
if (method === init)
continue;
if (method.words[0] === 'command') {
if (method.words.length !== 2)
raise(method);
const methodName = method.words[1];
const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(methodName)}Params`;
types.set(paramsName, parameters || { list: [] });
const returns = method.list.find(i => i.words[0] === 'returns');
const resultName = `${channelName}${titleCase(methodName)}Result`;
types.set(resultName, returns);
result.push(` ${methodName}(params${parameters ? '' : '?'}: ${paramsName}): Promise<${resultName}>;`);
} else if (method.words[0] === 'event') {
if (method.words.length !== 2)
raise(method);
const eventName = method.words[1];
const parameters = method.list.find(i => i.words[0] === 'parameters');
const paramsName = `${channelName}${titleCase(eventName)}Event`;
types.set(paramsName, parameters || { list: [] });
result.push(` on(event: '${eventName}', callback: (params: ${paramsName}) => void): this;`);
} else {
raise(method);
}
}
result.push(`}`);
for (const [name, item] of types) {
if (!item)
result.push(`export type ${name} = void;`);
else
result.push(objectType(name, item, ' '));
}
} else {
raise(item);
}
result.push(``);
}
fs.writeFileSync(path.join(__dirname, '..', 'src', 'rpc', 'channels.ts'), result.join('\n'), 'utf-8');