chore(rpc): serialize rpc into actual wire string (#2740)

This commit is contained in:
Pavel Feldman 2020-06-27 11:10:07 -07:00 committed by GitHub
parent 3e33523ee3
commit 4e94bdabfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 198 additions and 129 deletions

View file

@ -14,10 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs';
import * as mime from 'mime';
import * as path from 'path';
import * as util from 'util';
import * as frames from './frames'; import * as frames from './frames';
import { assert, helper, assertMaxArguments } from './helper'; import { assert, helper, assertMaxArguments } from './helper';
import InjectedScript from './injected/injectedScript'; import InjectedScript from './injected/injectedScript';
@ -30,6 +26,7 @@ import * as types from './types';
import { Progress } from './progress'; import { Progress } from './progress';
import DebugScript from './debug/injected/debugScript'; import DebugScript from './debug/injected/debugScript';
import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
import { normalizeFilePayloads } from './rpc/serializers';
export class FrameExecutionContext extends js.ExecutionContext { export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame; readonly frame: frames.Frame;
@ -504,25 +501,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, {})); }, {}));
if (typeof multiple === 'string') if (typeof multiple === 'string')
return multiple; return multiple;
let ff: string[] | types.FilePayload[]; const filePayloads = await normalizeFilePayloads(files);
if (!Array.isArray(files)) assert(multiple || filePayloads.length <= 1, 'Non-multiple file input can only accept single file!');
ff = [ files ] as string[] | types.FilePayload[];
else
ff = files;
assert(multiple || ff.length <= 1, 'Non-multiple file input can only accept single file!');
const filePayloads: types.FilePayload[] = [];
for (const item of ff) {
if (typeof item === 'string') {
const file: types.FilePayload = {
name: path.basename(item),
mimeType: mime.getType(item) || 'application/octet-stream',
buffer: await util.promisify(fs.readFile)(item)
};
filePayloads.push(file);
} else {
filePayloads.push(item);
}
}
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads); await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads);

View file

@ -362,21 +362,6 @@ export function logPolitely(toBeLogged: string) {
console.log(toBeLogged); // eslint-disable-line no-console console.log(toBeLogged); // eslint-disable-line no-console
} }
export function serializeError(e: any): types.Error {
if (e instanceof Error)
return { message: e.message, stack: e.stack };
return { value: e };
}
export function parseError(error: types.Error): any {
if (error.message !== undefined) {
const e = new Error(error.message);
e.stack = error.stack;
return e;
}
return error.value;
}
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']); const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
export const helper = Helper; export const helper = Helper;

View file

@ -101,7 +101,7 @@ export interface PageChannel extends Channel {
goForward(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>; goForward(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
opener(): Promise<PageChannel | null>; opener(): Promise<PageChannel | null>;
reload(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>; reload(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
screenshot(params: { options?: types.ScreenshotOptions }): Promise<Buffer>; screenshot(params: { options?: types.ScreenshotOptions }): Promise<string>;
setExtraHTTPHeaders(params: { headers: types.Headers }): Promise<void>; setExtraHTTPHeaders(params: { headers: types.Headers }): Promise<void>;
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
setViewportSize(params: { viewportSize: types.Size }): Promise<void>; setViewportSize(params: { viewportSize: types.Size }): Promise<void>;
@ -151,7 +151,7 @@ export interface FrameChannel extends Channel {
querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>; querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>;
selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise<string[]>; selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise<string[]>;
setContent(params: { html: string, options: types.NavigateOptions }): Promise<void>; setContent(params: { html: string, options: types.NavigateOptions }): Promise<void>;
setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise<void>; setInputFiles(params: { selector: string, files: { name: string, mimeType: string, buffer: string }[], options: types.NavigatingActionWaitOptions }): Promise<void>;
textContent(params: { selector: string, options: types.TimeoutOptions }): Promise<string | null>; textContent(params: { selector: string, options: types.TimeoutOptions }): Promise<string | null>;
title(): Promise<string>; title(): Promise<string>;
type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>; type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
@ -199,7 +199,7 @@ export interface ElementHandleChannel extends JSHandleChannel {
press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>; press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
querySelector(params: { selector: string }): Promise<ElementHandleChannel | null>; querySelector(params: { selector: string }): Promise<ElementHandleChannel | null>;
querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>; querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>;
screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<Buffer>; screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<string>;
scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise<void>; scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise<void>;
selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise<string[]>; selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise<string[]>;
selectText(params: { options?: types.TimeoutOptions }): Promise<void>; selectText(params: { options?: types.TimeoutOptions }): Promise<void>;

View file

@ -38,7 +38,8 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
super(connection, channel, initializer); super(connection, channel, initializer);
} }
async newContext(options?: types.BrowserContextOptions): Promise<BrowserContext> { async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
delete (options as any).logger;
const context = BrowserContext.from(await this._channel.newContext({ options })); const context = BrowserContext.from(await this._channel.newContext({ options }));
this._contexts.add(context); this._contexts.add(context);
context._browser = this; context._browser = this;
@ -49,7 +50,8 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
return [...this._contexts]; return [...this._contexts];
} }
async newPage(options?: types.BrowserContextOptions): Promise<Page> { async newPage(options: types.BrowserContextOptions = {}): Promise<Page> {
delete (options as any).logger;
const context = await this.newContext(options); const context = await this.newContext(options);
const page = await context.newPage(); const page = await context.newPage();
page._ownedContext = context; page._ownedContext = context;

View file

@ -34,15 +34,18 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
return this._initializer.name; return this._initializer.name;
} }
async launch(options?: types.LaunchOptions): Promise<Browser> { async launch(options: types.LaunchOptions = {}): Promise<Browser> {
delete (options as any).logger;
return Browser.from(await this._channel.launch({ options })); return Browser.from(await this._channel.launch({ options }));
} }
async launchPersistentContext(userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: types.LaunchOptions & types.BrowserContextOptions = {}): Promise<BrowserContext> {
delete (options as any).logger;
return BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, options })); return BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, options }));
} }
async connect(options: types.ConnectOptions): Promise<Browser> { async connect(options: types.ConnectOptions): Promise<Browser> {
delete (options as any).logger;
return Browser.from(await this._channel.connect({ options })); return Browser.from(await this._channel.connect({ options }));
} }
} }

View file

@ -17,7 +17,7 @@
import * as types from '../../types'; import * as types from '../../types';
import { ElementHandleChannel, JSHandleInitializer } from '../channels'; import { ElementHandleChannel, JSHandleInitializer } from '../channels';
import { Frame } from './frame'; import { Frame } from './frame';
import { FuncOn, JSHandle, convertArg } from './jsHandle'; import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle';
import { Connection } from '../connection'; import { Connection } from '../connection';
export class ElementHandle<T extends Node = Node> extends JSHandle<T> { export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
@ -125,7 +125,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
} }
async screenshot(options?: types.ElementScreenshotOptions): Promise<Buffer> { async screenshot(options?: types.ElementScreenshotOptions): Promise<Buffer> {
return await this._elementChannel.screenshot({ options }); return Buffer.from(await this._elementChannel.screenshot({ options }), 'base64');
} }
async $(selector: string): Promise<ElementHandle<Element> | null> { async $(selector: string): Promise<ElementHandle<Element> | null> {
@ -139,13 +139,13 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R>; async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>; async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> { async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
return await this._elementChannel.$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return parseResult(await this._elementChannel.$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
} }
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>; async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>; async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>;
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> { async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
return await this._elementChannel.$$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return parseResult(await this._elementChannel.$$evalExpression({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
} }
} }

View file

@ -21,11 +21,12 @@ import { FrameChannel, FrameInitializer } from '../channels';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { ElementHandle, convertSelectOptionValues } from './elementHandle'; import { ElementHandle, convertSelectOptionValues } from './elementHandle';
import { JSHandle, Func1, FuncOn, SmartHandle, convertArg } from './jsHandle'; import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
import * as network from './network'; import * as network from './network';
import { Response } from './network'; import { Response } from './network';
import { Page } from './page'; import { Page } from './page';
import { Connection } from '../connection'; import { Connection } from '../connection';
import { normalizeFilePayloads } from '../serializers';
export type GotoOptions = types.NavigateOptions & { export type GotoOptions = types.NavigateOptions & {
referer?: string, referer?: string,
@ -78,14 +79,14 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async evaluateHandle<R>(pageFunction: Func1<void, R>, arg?: any): Promise<SmartHandle<R>>; async evaluateHandle<R>(pageFunction: Func1<void, R>, arg?: any): Promise<SmartHandle<R>>;
async evaluateHandle<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<SmartHandle<R>> { async evaluateHandle<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<SmartHandle<R>> {
assertMaxArguments(arguments.length, 2); assertMaxArguments(arguments.length, 2);
return JSHandle.from(await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) })) as SmartHandle<R>; return JSHandle.from(await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) })) as SmartHandle<R>;
} }
async evaluate<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<R>; async evaluate<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<R>;
async evaluate<R>(pageFunction: Func1<void, R>, arg?: any): Promise<R>; async evaluate<R>(pageFunction: Func1<void, R>, arg?: any): Promise<R>;
async evaluate<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<R> { async evaluate<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 2); assertMaxArguments(arguments.length, 2);
return await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return parseResult(await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
} }
async $(selector: string): Promise<ElementHandle<Element> | null> { async $(selector: string): Promise<ElementHandle<Element> | null> {
@ -104,14 +105,14 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>; async $eval<R>(selector: string, pageFunction: FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> { async $eval<R, Arg>(selector: string, pageFunction: FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return await this._channel.$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return await this._channel.$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
} }
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>; async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>; async $$eval<R>(selector: string, pageFunction: FuncOn<Element[], void, R>, arg?: any): Promise<R>;
async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> { async $$eval<R, Arg>(selector: string, pageFunction: FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return await this._channel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return await this._channel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
} }
async $$(selector: string): Promise<ElementHandle<Element>[]> { async $$(selector: string): Promise<ElementHandle<Element>[]> {
@ -196,7 +197,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
} }
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> { async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
await this._channel.setInputFiles({ selector, files, options }); const filePayloads = await normalizeFilePayloads(files);
await this._channel.setInputFiles({ selector, files: filePayloads.map(f => ({ name: f.name, mimeType: f.mimeType, buffer: f.buffer.toString('base64') })), options });
} }
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
@ -222,7 +224,7 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>; async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>;
async waitForFunction<R>(pageFunction: Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>; async waitForFunction<R>(pageFunction: Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<SmartHandle<R>>;
async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> { async waitForFunction<R, Arg>(pageFunction: Func1<Arg, R>, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise<SmartHandle<R>> {
return JSHandle.from(await this._channel.waitForFunction({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg, options })) as SmartHandle<R>; return JSHandle.from(await this._channel.waitForFunction({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), options })) as SmartHandle<R>;
} }
async title(): Promise<string> { async title(): Promise<string> {

View file

@ -18,6 +18,7 @@ import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection'; import { Connection } from '../connection';
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 NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
type Unboxed<Arg> = type Unboxed<Arg> =
@ -51,13 +52,13 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R>; async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
async evaluate<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<R>; async evaluate<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<R>;
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R> { async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<R> {
return await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); return parseResult(await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }));
} }
async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>>; async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>>;
async evaluateHandle<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<SmartHandle<R>>; async evaluateHandle<R>(pageFunction: FuncOn<T, void, R>, arg?: any): Promise<SmartHandle<R>>;
async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>> { async evaluateHandle<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg: Arg): Promise<SmartHandle<R>> {
const handleChannel = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); const handleChannel = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
return JSHandle.from(handleChannel) as SmartHandle<R>; return JSHandle.from(handleChannel) as SmartHandle<R>;
} }
@ -97,18 +98,20 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
} }
} }
export function convertArg(arg: any): any { export function serializeArgument(arg: any): any {
if (arg === null) const guids: { guid: string }[] = [];
return null; const pushHandle = (guid: string): number => {
if (Array.isArray(arg)) guids.push({ guid });
return arg.map(item => convertArg(item)); return guids.length - 1;
if (arg instanceof ChannelOwner) };
return arg._channel; const value = serializeAsCallArgument(arg, value => {
if (typeof arg === 'object') { if (value instanceof ChannelOwner)
const result: any = {}; return { h: pushHandle(value._channel._guid) };
for (const key of Object.keys(arg)) return { fallThrough: value };
result[key] = convertArg(arg[key]); });
return result; return { value, guids };
} }
return arg;
export function parseResult(arg: any): any {
return parseEvaluationResultValue(arg, []);
} }

View file

@ -17,7 +17,7 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Events } from '../../events'; import { Events } from '../../events';
import { assert, assertMaxArguments, helper, Listener, serializeError, parseError } from '../../helper'; import { assert, assertMaxArguments, helper, Listener } from '../../helper';
import * as types from '../../types'; import * as types from '../../types';
import { PageChannel, BindingCallChannel, Channel, PageInitializer, BindingCallInitializer } from '../channels'; import { PageChannel, BindingCallChannel, Channel, PageInitializer, BindingCallInitializer } from '../channels';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
@ -34,6 +34,7 @@ import { Dialog } from './dialog';
import { Download } from './download'; import { Download } from './download';
import { TimeoutError } from '../../errors'; import { TimeoutError } from '../../errors';
import { TimeoutSettings } from '../../timeoutSettings'; import { TimeoutSettings } from '../../timeoutSettings';
import { parseError, serializeError } from '../serializers';
export class Page extends ChannelOwner<PageChannel, PageInitializer> { export class Page extends ChannelOwner<PageChannel, PageInitializer> {
readonly pdf: ((options?: types.PDFOptions) => Promise<Buffer>) | undefined; readonly pdf: ((options?: types.PDFOptions) => Promise<Buffer>) | undefined;
@ -365,7 +366,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
} }
async screenshot(options?: types.ScreenshotOptions): Promise<Buffer> { async screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
return await this._channel.screenshot({ options }); return Buffer.from(await this._channel.screenshot({ options }), 'base64');
} }
async title(): Promise<string> { async title(): Promise<string> {

View file

@ -29,11 +29,14 @@ import { Channel } from './channels';
import { ConsoleMessage } from './client/consoleMessage'; import { ConsoleMessage } from './client/consoleMessage';
import { Dialog } from './client/dialog'; import { Dialog } from './client/dialog';
import { Download } from './client/download'; import { Download } from './client/download';
import { parseError } from './serializers';
export class Connection { export class Connection {
private _channels = new Map<string, Channel>(); private _channels = new Map<string, Channel>();
private _waitingForObject = new Map<string, any>(); private _waitingForObject = new Map<string, any>();
sendMessageToServerTransport = (message: any): Promise<any> => Promise.resolve(); sendMessageToServerTransport = (message: string): void => {};
private _lastId = 0;
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void }>();
constructor() {} constructor() {}
@ -103,23 +106,33 @@ export class Connection {
return new Promise(f => this._waitingForObject.set(guid, f)); return new Promise(f => this._waitingForObject.set(guid, f));
} }
async sendMessageToServer(message: { guid: string, method: string, params: any }) { async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise<any> {
const converted = {...message, params: this._replaceChannelsWithGuids(message.params)}; const id = ++this._lastId;
const converted = { id, ...message, params: this._replaceChannelsWithGuids(message.params) };
debug('pw:channel:command')(converted); debug('pw:channel:command')(converted);
const response = await this.sendMessageToServerTransport(converted); this.sendMessageToServerTransport(JSON.stringify(converted));
debug('pw:channel:response')(response); return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
return this._replaceGuidsWithChannels(response);
} }
dispatchMessageFromServer(message: { guid: string, method: string, params: any }) { dispatchMessageFromServer(message: string) {
debug('pw:channel:event')(message); const parsedMessage = JSON.parse(message);
const { guid, method, params } = message; const { id, guid, method, params, result, error } = parsedMessage;
if (id) {
debug('pw:channel:response')(parsedMessage);
const callback = this._callbacks.get(id)!;
this._callbacks.delete(id);
if (error)
callback.reject(parseError(error));
else
callback.resolve(this._replaceGuidsWithChannels(result));
return;
}
debug('pw:channel:event')(parsedMessage);
if (method === '__create__') { if (method === '__create__') {
this._createRemoteObject(params.type, guid, params.initializer); this._createRemoteObject(params.type, guid, params.initializer);
return; return;
} }
const channel = this._channels.get(guid)!; const channel = this._channels.get(guid)!;
channel.emit(method, this._replaceGuidsWithChannels(params)); channel.emit(method, this._replaceGuidsWithChannels(params));
} }

View file

@ -17,6 +17,7 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { helper } from '../helper'; import { helper } from '../helper';
import { Channel } from './channels'; import { Channel } from './channels';
import { serializeError } from './serializers';
export class Dispatcher<Type, Initializer> extends EventEmitter implements Channel { export class Dispatcher<Type, Initializer> extends EventEmitter implements Channel {
readonly _guid: string; readonly _guid: string;
@ -43,16 +44,22 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements Chann
export class DispatcherScope { export class DispatcherScope {
readonly dispatchers = new Map<string, Dispatcher<any, any>>(); readonly dispatchers = new Map<string, Dispatcher<any, any>>();
readonly dispatcherSymbol = Symbol('dispatcher'); readonly dispatcherSymbol = Symbol('dispatcher');
sendMessageToClientTransport = (message: any) => {}; sendMessageToClientTransport = (message: string) => {};
async sendMessageToClient(guid: string, method: string, params: any): Promise<any> { async sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
this.sendMessageToClientTransport({ guid, method, params: this._replaceDispatchersWithGuids(params) }); this.sendMessageToClientTransport(JSON.stringify({ guid, method, params: this._replaceDispatchersWithGuids(params) }));
} }
async dispatchMessageFromClient(message: any): Promise<any> { async dispatchMessageFromClient(message: string) {
const dispatcher = this.dispatchers.get(message.guid)!; const parsedMessage = JSON.parse(message);
const value = await (dispatcher as any)[message.method](this._replaceGuidsWithDispatchers(message.params)); const { id, guid, method, params } = parsedMessage;
return this._replaceDispatchersWithGuids(value); const dispatcher = this.dispatchers.get(guid)!;
try {
const result = await (dispatcher as any)[method](this._replaceGuidsWithDispatchers(params));
this.sendMessageToClientTransport(JSON.stringify({ id, result: this._replaceDispatchersWithGuids(result) }));
} catch (e) {
this.sendMessageToClientTransport(JSON.stringify({ id, error: serializeError(e) }));
}
} }
private _replaceDispatchersWithGuids(payload: any): any { private _replaceDispatchersWithGuids(payload: any): any {

64
src/rpc/serializers.ts Normal file
View file

@ -0,0 +1,64 @@
/**
* 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 * as fs from 'fs';
import * as mime from 'mime';
import * as path from 'path';
import * as util from 'util';
import { TimeoutError } from '../errors';
import * as types from '../types';
export function serializeError(e: any): types.Error {
if (e instanceof Error)
return { message: e.message, stack: e.stack, name: e.name };
return { value: e };
}
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;
return e;
}
const e = new Error(error.message);
e.stack = error.stack;
return e;
}
export async function normalizeFilePayloads(files: string | types.FilePayload | string[] | types.FilePayload[]): Promise<types.FilePayload[]> {
let ff: string[] | types.FilePayload[];
if (!Array.isArray(files))
ff = [ files ] as string[] | types.FilePayload[];
else
ff = files;
const filePayloads: types.FilePayload[] = [];
for (const item of ff) {
if (typeof item === 'string') {
const file: types.FilePayload = {
name: path.basename(item),
mimeType: mime.getType(item) || 'application/octet-stream',
buffer: await util.promisify(fs.readFile)(item)
};
filePayloads.push(file);
} else {
filePayloads.push(item);
}
}
return filePayloads;
}

View file

@ -19,8 +19,8 @@ import * as js from '../../javascript';
import * as types from '../../types'; import * as types from '../../types';
import { ElementHandleChannel, FrameChannel } from '../channels'; import { ElementHandleChannel, FrameChannel } from '../channels';
import { DispatcherScope } from '../dispatcher'; import { DispatcherScope } from '../dispatcher';
import { convertArg, FrameDispatcher } from './frameDispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import { JSHandleDispatcher } from './jsHandleDispatcher'; import { FrameDispatcher } from './frameDispatcher';
export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel { export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel {
readonly _elementHandle: ElementHandle; readonly _elementHandle: ElementHandle;
@ -140,8 +140,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
return await this._elementHandle.boundingBox(); return await this._elementHandle.boundingBox();
} }
async screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<Buffer> { async screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<string> {
return await this._elementHandle.screenshot(params.options); return (await this._elementHandle.screenshot(params.options)).toString('base64');
} }
async querySelector(params: { selector: string }): Promise<ElementHandleChannel | null> { async querySelector(params: { selector: string }): Promise<ElementHandleChannel | null> {
@ -154,11 +154,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
} }
async $evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> { async $evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); return serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
} }
async $$evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> { async $$evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); return serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
} }
} }

View file

@ -16,10 +16,10 @@
import { Frame } from '../../frames'; import { Frame } from '../../frames';
import * as types from '../../types'; import * as types from '../../types';
import { ElementHandleChannel, FrameChannel, JSHandleChannel, ResponseChannel, FrameInitializer } from '../channels'; import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher'; import { Dispatcher, DispatcherScope } from '../dispatcher';
import { ElementHandleDispatcher, convertSelectOptionValues } from './elementHandlerDispatcher'; import { convertSelectOptionValues, ElementHandleDispatcher } from './elementHandlerDispatcher';
import { JSHandleDispatcher } from './jsHandleDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher } from './networkDispatchers'; import { ResponseDispatcher } from './networkDispatchers';
export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> implements FrameChannel { export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> implements FrameChannel {
@ -63,15 +63,15 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
} }
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._frame._evaluateExpression(params.expression, params.isFunction, convertArg(this._scope, params.arg)); return serializeResult(await this._frame._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg)));
} }
async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel> { async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel> {
return ElementHandleDispatcher.fromElement(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, convertArg(this._scope, params.arg))); return ElementHandleDispatcher.fromElement(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg)));
} }
async waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise<ElementHandleChannel | null> { async waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise<ElementHandleChannel | null> {
return ElementHandleDispatcher.fromNullableElement(this._scope, await this._frame.waitForSelector(params.selector)); return ElementHandleDispatcher.fromNullableElement(this._scope, await this._frame.waitForSelector(params.selector, params.options));
} }
async dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise<void> { async dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise<void> {
@ -79,11 +79,11 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
} }
async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> { async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._frame._$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); return serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
} }
async $$eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> { async $$eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); return serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg)));
} }
async querySelector(params: { selector: string }): Promise<ElementHandleChannel | null> { async querySelector(params: { selector: string }): Promise<ElementHandleChannel | null> {
@ -151,8 +151,8 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return this._frame.selectOption(params.selector, convertSelectOptionValues(params.values), params.options); return this._frame.selectOption(params.selector, convertSelectOptionValues(params.values), params.options);
} }
async setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise<void> { async setInputFiles(params: { selector: string, files: { name: string, mimeType: string, buffer: string }[], options: types.NavigatingActionWaitOptions }): Promise<void> {
await this._frame.setInputFiles(params.selector, params.files, params.options); await this._frame.setInputFiles(params.selector, params.files.map(f => ({ name: f.name, mimeType: f.mimeType, buffer: Buffer.from(f.buffer, 'base64') })), params.options);
} }
async type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void> { async type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void> {
@ -172,26 +172,10 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
} }
async waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise<JSHandleChannel> { async waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise<JSHandleChannel> {
return ElementHandleDispatcher.from(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, convertArg(this._scope, params.arg), params.options)); return ElementHandleDispatcher.from(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params.options));
} }
async title(): Promise<string> { async title(): Promise<string> {
return await this._frame.title(); return await this._frame.title();
} }
} }
export function convertArg(scope: DispatcherScope, arg: any): any {
if (arg === null)
return null;
if (Array.isArray(arg))
return arg.map(item => convertArg(scope, item));
if (arg instanceof JSHandleDispatcher)
return arg._object;
if (typeof arg === 'object') {
const result: any = {};
for (const key of Object.keys(arg))
result[key] = convertArg(scope, arg[key]);
return result;
}
return arg;
}

View file

@ -17,8 +17,8 @@
import * as js from '../../javascript'; import * as js from '../../javascript';
import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher'; import { Dispatcher, DispatcherScope } from '../dispatcher';
import { convertArg } from './frameDispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { parseEvaluationResultValue, serializeAsCallArgument } from '../../common/utilityScriptSerializers';
export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitializer> implements JSHandleChannel { export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitializer> implements JSHandleChannel {
@ -29,11 +29,11 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
} }
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any> {
return this._object._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, convertArg(this._scope, params.arg)); return this._object._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg));
} }
async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel> { async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel> {
const jsHandle = await this._object._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, convertArg(this._scope, params.arg)); const jsHandle = await this._object._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
return ElementHandleDispatcher.from(this._scope, jsHandle); return ElementHandleDispatcher.from(this._scope, jsHandle);
} }
@ -53,3 +53,27 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
await this._object.dispose(); await this._object.dispose();
} }
} }
export function parseArgument(arg: { value: any, guids: JSHandleDispatcher[] }): any {
return convertDispatchersToObjects(parseEvaluationResultValue(arg.value, arg.guids));
}
export function serializeResult(arg: any): any {
return serializeAsCallArgument(arg, value => ({ fallThrough: value }));
}
function convertDispatchersToObjects(arg: any): any {
if (arg === null)
return null;
if (Array.isArray(arg))
return arg.map(item => convertDispatchersToObjects(item));
if (arg instanceof JSHandleDispatcher)
return arg._object;
if (typeof arg === 'object') {
const result: any = {};
for (const key of Object.keys(arg))
result[key] = convertDispatchersToObjects(arg[key]);
return result;
}
return arg;
}

View file

@ -17,12 +17,12 @@
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { Events } from '../../events'; import { Events } from '../../events';
import { Frame } from '../../frames'; import { Frame } from '../../frames';
import { parseError, serializeError } from '../../helper';
import { Request } from '../../network'; import { Request } from '../../network';
import { Page } from '../../page'; import { Page } from '../../page';
import * as types from '../../types'; import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel } from '../channels'; import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher'; import { Dispatcher, DispatcherScope } from '../dispatcher';
import { parseError, serializeError } from '../serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher'; import { DialogDispatcher } from './dialogDispatcher';
import { DownloadDispatcher } from './downloadDispatcher'; import { DownloadDispatcher } from './downloadDispatcher';
@ -129,8 +129,8 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
}); });
} }
async screenshot(params: { options?: types.ScreenshotOptions }): Promise<Buffer> { async screenshot(params: { options?: types.ScreenshotOptions }): Promise<string> {
return await this._page.screenshot(params.options); return (await this._page.screenshot(params.options)).toString('base64');
} }
async close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void> { async close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void> {

View file

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