From 4e94bdabfd82fb41807e6beb7dc35560d9675899 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 27 Jun 2020 11:10:07 -0700 Subject: [PATCH] chore(rpc): serialize rpc into actual wire string (#2740) --- src/dom.ts | 26 +-------- src/helper.ts | 15 ----- src/rpc/channels.ts | 6 +- src/rpc/client/browser.ts | 6 +- src/rpc/client/browserType.ts | 7 ++- src/rpc/client/elementHandle.ts | 8 +-- src/rpc/client/frame.ts | 16 +++--- src/rpc/client/jsHandle.ts | 35 ++++++------ src/rpc/client/page.ts | 5 +- src/rpc/connection.ts | 33 +++++++---- src/rpc/dispatcher.ts | 19 +++++-- src/rpc/serializers.ts | 64 ++++++++++++++++++++++ src/rpc/server/elementHandlerDispatcher.ts | 12 ++-- src/rpc/server/frameDispatcher.ts | 38 ++++--------- src/rpc/server/jsHandleDispatcher.ts | 30 +++++++++- src/rpc/server/pageDispatcher.ts | 6 +- src/types.ts | 1 + 17 files changed, 198 insertions(+), 129 deletions(-) create mode 100644 src/rpc/serializers.ts diff --git a/src/dom.ts b/src/dom.ts index 69f1cc8412..e037b18090 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -14,10 +14,6 @@ * 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 { assert, helper, assertMaxArguments } from './helper'; import InjectedScript from './injected/injectedScript'; @@ -30,6 +26,7 @@ import * as types from './types'; import { Progress } from './progress'; import DebugScript from './debug/injected/debugScript'; import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; +import { normalizeFilePayloads } from './rpc/serializers'; export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; @@ -504,25 +501,8 @@ export class ElementHandle extends js.JSHandle { }, {})); if (typeof multiple === 'string') return multiple; - let ff: string[] | types.FilePayload[]; - if (!Array.isArray(files)) - 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); - } - } + const filePayloads = await normalizeFilePayloads(files); + assert(multiple || filePayloads.length <= 1, 'Non-multiple file input can only accept single file!'); await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { progress.throwIfAborted(); // Avoid action that has side-effects. await this._page._delegate.setInputFiles(this as any as ElementHandle, filePayloads); diff --git a/src/helper.ts b/src/helper.ts index 8e64f16a31..9d3d1f871f 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -362,21 +362,6 @@ export function logPolitely(toBeLogged: string) { 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(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']); export const helper = Helper; diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 185cdcf653..1ae4b09aba 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -101,7 +101,7 @@ export interface PageChannel extends Channel { goForward(params: { options?: types.NavigateOptions }): Promise; opener(): Promise; reload(params: { options?: types.NavigateOptions }): Promise; - screenshot(params: { options?: types.ScreenshotOptions }): Promise; + screenshot(params: { options?: types.ScreenshotOptions }): Promise; setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; setViewportSize(params: { viewportSize: types.Size }): Promise; @@ -151,7 +151,7 @@ export interface FrameChannel extends Channel { querySelectorAll(params: { selector: string }): Promise; selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise; setContent(params: { html: string, options: types.NavigateOptions }): Promise; - setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise; + setInputFiles(params: { selector: string, files: { name: string, mimeType: string, buffer: string }[], options: types.NavigatingActionWaitOptions }): Promise; textContent(params: { selector: string, options: types.TimeoutOptions }): Promise; title(): Promise; type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; @@ -199,7 +199,7 @@ export interface ElementHandleChannel extends JSHandleChannel { press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; querySelector(params: { selector: string }): Promise; querySelectorAll(params: { selector: string }): Promise; - screenshot(params: { options?: types.ElementScreenshotOptions }): Promise; + screenshot(params: { options?: types.ElementScreenshotOptions }): Promise; scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise; selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise; selectText(params: { options?: types.TimeoutOptions }): Promise; diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index d16e32316f..3b7994fa74 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -38,7 +38,8 @@ export class Browser extends ChannelOwner { super(connection, channel, initializer); } - async newContext(options?: types.BrowserContextOptions): Promise { + async newContext(options: types.BrowserContextOptions = {}): Promise { + delete (options as any).logger; const context = BrowserContext.from(await this._channel.newContext({ options })); this._contexts.add(context); context._browser = this; @@ -49,7 +50,8 @@ export class Browser extends ChannelOwner { return [...this._contexts]; } - async newPage(options?: types.BrowserContextOptions): Promise { + async newPage(options: types.BrowserContextOptions = {}): Promise { + delete (options as any).logger; const context = await this.newContext(options); const page = await context.newPage(); page._ownedContext = context; diff --git a/src/rpc/client/browserType.ts b/src/rpc/client/browserType.ts index 329c2aa3a6..69ae15befb 100644 --- a/src/rpc/client/browserType.ts +++ b/src/rpc/client/browserType.ts @@ -34,15 +34,18 @@ export class BrowserType extends ChannelOwner { + async launch(options: types.LaunchOptions = {}): Promise { + delete (options as any).logger; return Browser.from(await this._channel.launch({ options })); } - async launchPersistentContext(userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions): Promise { + async launchPersistentContext(userDataDir: string, options: types.LaunchOptions & types.BrowserContextOptions = {}): Promise { + delete (options as any).logger; return BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, options })); } async connect(options: types.ConnectOptions): Promise { + delete (options as any).logger; return Browser.from(await this._channel.connect({ options })); } } diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts index 903de0e856..f4a7b50257 100644 --- a/src/rpc/client/elementHandle.ts +++ b/src/rpc/client/elementHandle.ts @@ -17,7 +17,7 @@ import * as types from '../../types'; import { ElementHandleChannel, JSHandleInitializer } from '../channels'; import { Frame } from './frame'; -import { FuncOn, JSHandle, convertArg } from './jsHandle'; +import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle'; import { Connection } from '../connection'; export class ElementHandle extends JSHandle { @@ -125,7 +125,7 @@ export class ElementHandle extends JSHandle { } async screenshot(options?: types.ElementScreenshotOptions): Promise { - return await this._elementChannel.screenshot({ options }); + return Buffer.from(await this._elementChannel.screenshot({ options }), 'base64'); } async $(selector: string): Promise | null> { @@ -139,13 +139,13 @@ export class ElementHandle extends JSHandle { async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; async $eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { - 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(selector: string, pageFunction: FuncOn, arg: Arg): Promise; async $$eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { - 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) })); } } diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts index 167c79040c..e7d5fc5ca1 100644 --- a/src/rpc/client/frame.ts +++ b/src/rpc/client/frame.ts @@ -21,11 +21,12 @@ import { FrameChannel, FrameInitializer } from '../channels'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; 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 { Response } from './network'; import { Page } from './page'; import { Connection } from '../connection'; +import { normalizeFilePayloads } from '../serializers'; export type GotoOptions = types.NavigateOptions & { referer?: string, @@ -78,14 +79,14 @@ export class Frame extends ChannelOwner { async evaluateHandle(pageFunction: Func1, arg?: any): Promise>; async evaluateHandle(pageFunction: Func1, arg: Arg): Promise> { assertMaxArguments(arguments.length, 2); - return JSHandle.from(await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) })) as SmartHandle; + return JSHandle.from(await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) })) as SmartHandle; } async evaluate(pageFunction: Func1, arg: Arg): Promise; async evaluate(pageFunction: Func1, arg?: any): Promise; async evaluate(pageFunction: Func1, arg: Arg): Promise { 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 | null> { @@ -104,14 +105,14 @@ export class Frame extends ChannelOwner { async $eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { 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(selector: string, pageFunction: FuncOn, arg: Arg): Promise; async $$eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { 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[]> { @@ -196,7 +197,8 @@ export class Frame extends ChannelOwner { } async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { - 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 = {}) { @@ -222,7 +224,7 @@ export class Frame extends ChannelOwner { async waitForFunction(pageFunction: Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: Func1, arg?: any, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: Func1, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise> { - return JSHandle.from(await this._channel.waitForFunction({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg, options })) as SmartHandle; + return JSHandle.from(await this._channel.waitForFunction({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), options })) as SmartHandle; } async title(): Promise { diff --git a/src/rpc/client/jsHandle.ts b/src/rpc/client/jsHandle.ts index 2a1384459a..84db879be0 100644 --- a/src/rpc/client/jsHandle.ts +++ b/src/rpc/client/jsHandle.ts @@ -18,6 +18,7 @@ import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { ElementHandle } from './elementHandle'; import { ChannelOwner } from './channelOwner'; import { Connection } from '../connection'; +import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers'; type NoHandles = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); type Unboxed = @@ -51,13 +52,13 @@ export class JSHandle extends ChannelOwner(pageFunction: FuncOn, arg: Arg): Promise; async evaluate(pageFunction: FuncOn, arg?: any): Promise; async evaluate(pageFunction: FuncOn, arg: Arg): Promise { - 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(pageFunction: FuncOn, arg: Arg): Promise>; async evaluateHandle(pageFunction: FuncOn, arg?: any): Promise>; async evaluateHandle(pageFunction: FuncOn, arg: Arg): Promise> { - 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; } @@ -97,18 +98,20 @@ export class JSHandle extends ChannelOwner convertArg(item)); - if (arg instanceof ChannelOwner) - return arg._channel; - if (typeof arg === 'object') { - const result: any = {}; - for (const key of Object.keys(arg)) - result[key] = convertArg(arg[key]); - return result; - } - return arg; +export function serializeArgument(arg: any): any { + const guids: { guid: string }[] = []; + const pushHandle = (guid: string): number => { + guids.push({ guid }); + return guids.length - 1; + }; + const value = serializeAsCallArgument(arg, value => { + if (value instanceof ChannelOwner) + return { h: pushHandle(value._channel._guid) }; + return { fallThrough: value }; + }); + return { value, guids }; +} + +export function parseResult(arg: any): any { + return parseEvaluationResultValue(arg, []); } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 00cd4ce13f..bf4a88cf5c 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -17,7 +17,7 @@ import { EventEmitter } 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 { PageChannel, BindingCallChannel, Channel, PageInitializer, BindingCallInitializer } from '../channels'; import { BrowserContext } from './browserContext'; @@ -34,6 +34,7 @@ import { Dialog } from './dialog'; import { Download } from './download'; import { TimeoutError } from '../../errors'; import { TimeoutSettings } from '../../timeoutSettings'; +import { parseError, serializeError } from '../serializers'; export class Page extends ChannelOwner { readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; @@ -365,7 +366,7 @@ export class Page extends ChannelOwner { } async screenshot(options?: types.ScreenshotOptions): Promise { - return await this._channel.screenshot({ options }); + return Buffer.from(await this._channel.screenshot({ options }), 'base64'); } async title(): Promise { diff --git a/src/rpc/connection.ts b/src/rpc/connection.ts index cab2bb2187..613c82e528 100644 --- a/src/rpc/connection.ts +++ b/src/rpc/connection.ts @@ -29,11 +29,14 @@ import { Channel } from './channels'; import { ConsoleMessage } from './client/consoleMessage'; import { Dialog } from './client/dialog'; import { Download } from './client/download'; +import { parseError } from './serializers'; export class Connection { private _channels = new Map(); private _waitingForObject = new Map(); - sendMessageToServerTransport = (message: any): Promise => Promise.resolve(); + sendMessageToServerTransport = (message: string): void => {}; + private _lastId = 0; + private _callbacks = new Map void, reject: (a: Error) => void }>(); constructor() {} @@ -103,23 +106,33 @@ export class Connection { return new Promise(f => this._waitingForObject.set(guid, f)); } - async sendMessageToServer(message: { guid: string, method: string, params: any }) { - const converted = {...message, params: this._replaceChannelsWithGuids(message.params)}; + async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise { + const id = ++this._lastId; + const converted = { id, ...message, params: this._replaceChannelsWithGuids(message.params) }; debug('pw:channel:command')(converted); - const response = await this.sendMessageToServerTransport(converted); - debug('pw:channel:response')(response); - return this._replaceGuidsWithChannels(response); + this.sendMessageToServerTransport(JSON.stringify(converted)); + return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject })); } - dispatchMessageFromServer(message: { guid: string, method: string, params: any }) { - debug('pw:channel:event')(message); - const { guid, method, params } = message; + dispatchMessageFromServer(message: string) { + const parsedMessage = JSON.parse(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__') { this._createRemoteObject(params.type, guid, params.initializer); return; } - const channel = this._channels.get(guid)!; channel.emit(method, this._replaceGuidsWithChannels(params)); } diff --git a/src/rpc/dispatcher.ts b/src/rpc/dispatcher.ts index 8aad1880e2..547cc28cb5 100644 --- a/src/rpc/dispatcher.ts +++ b/src/rpc/dispatcher.ts @@ -17,6 +17,7 @@ import { EventEmitter } from 'events'; import { helper } from '../helper'; import { Channel } from './channels'; +import { serializeError } from './serializers'; export class Dispatcher extends EventEmitter implements Channel { readonly _guid: string; @@ -43,16 +44,22 @@ export class Dispatcher extends EventEmitter implements Chann export class DispatcherScope { readonly dispatchers = new Map>(); readonly dispatcherSymbol = Symbol('dispatcher'); - sendMessageToClientTransport = (message: any) => {}; + sendMessageToClientTransport = (message: string) => {}; async sendMessageToClient(guid: string, method: string, params: any): Promise { - this.sendMessageToClientTransport({ guid, method, params: this._replaceDispatchersWithGuids(params) }); + this.sendMessageToClientTransport(JSON.stringify({ guid, method, params: this._replaceDispatchersWithGuids(params) })); } - async dispatchMessageFromClient(message: any): Promise { - const dispatcher = this.dispatchers.get(message.guid)!; - const value = await (dispatcher as any)[message.method](this._replaceGuidsWithDispatchers(message.params)); - return this._replaceDispatchersWithGuids(value); + async dispatchMessageFromClient(message: string) { + const parsedMessage = JSON.parse(message); + const { id, guid, method, params } = parsedMessage; + 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 { diff --git a/src/rpc/serializers.ts b/src/rpc/serializers.ts new file mode 100644 index 0000000000..aa5daf4aec --- /dev/null +++ b/src/rpc/serializers.ts @@ -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 { + 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; +} diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts index f6fb3a2d9e..ad5423e951 100644 --- a/src/rpc/server/elementHandlerDispatcher.ts +++ b/src/rpc/server/elementHandlerDispatcher.ts @@ -19,8 +19,8 @@ import * as js from '../../javascript'; import * as types from '../../types'; import { ElementHandleChannel, FrameChannel } from '../channels'; import { DispatcherScope } from '../dispatcher'; -import { convertArg, FrameDispatcher } from './frameDispatcher'; -import { JSHandleDispatcher } from './jsHandleDispatcher'; +import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; +import { FrameDispatcher } from './frameDispatcher'; export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel { readonly _elementHandle: ElementHandle; @@ -140,8 +140,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme return await this._elementHandle.boundingBox(); } - async screenshot(params: { options?: types.ElementScreenshotOptions }): Promise { - return await this._elementHandle.screenshot(params.options); + async screenshot(params: { options?: types.ElementScreenshotOptions }): Promise { + return (await this._elementHandle.screenshot(params.options)).toString('base64'); } async querySelector(params: { selector: string }): Promise { @@ -154,11 +154,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme } async $evalExpression(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { - 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 { - 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))); } } diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts index a659c06c78..fb72249f56 100644 --- a/src/rpc/server/frameDispatcher.ts +++ b/src/rpc/server/frameDispatcher.ts @@ -16,10 +16,10 @@ import { Frame } from '../../frames'; 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 { ElementHandleDispatcher, convertSelectOptionValues } from './elementHandlerDispatcher'; -import { JSHandleDispatcher } from './jsHandleDispatcher'; +import { convertSelectOptionValues, ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { ResponseDispatcher } from './networkDispatchers'; export class FrameDispatcher extends Dispatcher implements FrameChannel { @@ -63,15 +63,15 @@ export class FrameDispatcher extends Dispatcher impleme } async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise { - 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 { - 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 { - 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 { @@ -79,11 +79,11 @@ export class FrameDispatcher extends Dispatcher impleme } async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { - 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 { - 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 { @@ -151,8 +151,8 @@ export class FrameDispatcher extends Dispatcher impleme 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 { - await this._frame.setInputFiles(params.selector, params.files, params.options); + async setInputFiles(params: { selector: string, files: { name: string, mimeType: string, buffer: string }[], options: types.NavigatingActionWaitOptions }): Promise { + 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 { @@ -172,26 +172,10 @@ export class FrameDispatcher extends Dispatcher impleme } async waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise { - 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 { 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; -} diff --git a/src/rpc/server/jsHandleDispatcher.ts b/src/rpc/server/jsHandleDispatcher.ts index 204a708149..18c9cea193 100644 --- a/src/rpc/server/jsHandleDispatcher.ts +++ b/src/rpc/server/jsHandleDispatcher.ts @@ -17,8 +17,8 @@ import * as js from '../../javascript'; import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { Dispatcher, DispatcherScope } from '../dispatcher'; -import { convertArg } from './frameDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { parseEvaluationResultValue, serializeAsCallArgument } from '../../common/utilityScriptSerializers'; export class JSHandleDispatcher extends Dispatcher implements JSHandleChannel { @@ -29,11 +29,11 @@ export class JSHandleDispatcher extends Dispatcher { - 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 { - 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); } @@ -53,3 +53,27 @@ export class JSHandleDispatcher extends Dispatcher ({ 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; +} diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index e0cefe6f48..4de203576c 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -17,12 +17,12 @@ import { BrowserContext } from '../../browserContext'; import { Events } from '../../events'; import { Frame } from '../../frames'; -import { parseError, serializeError } from '../../helper'; import { Request } from '../../network'; import { Page } from '../../page'; import * as types from '../../types'; import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel } from '../channels'; import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { parseError, serializeError } from '../serializers'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { DialogDispatcher } from './dialogDispatcher'; import { DownloadDispatcher } from './downloadDispatcher'; @@ -129,8 +129,8 @@ export class PageDispatcher extends Dispatcher implements }); } - async screenshot(params: { options?: types.ScreenshotOptions }): Promise { - return await this._page.screenshot(params.options); + async screenshot(params: { options?: types.ScreenshotOptions }): Promise { + return (await this._page.screenshot(params.options)).toString('base64'); } async close(params: { options?: { runBeforeUnload?: boolean } }): Promise { diff --git a/src/types.ts b/src/types.ts index 97b2e558b5..5445dac061 100644 --- a/src/types.ts +++ b/src/types.ts @@ -308,6 +308,7 @@ export type ConsoleMessageLocation = { export type Error = { message?: string, + name?: string, stack?: string, value?: any };