Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: explicitly type SerializedArgument, fix rpc dispatchEvent #2988

Merged
merged 1 commit into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 42 additions & 35 deletions src/common/utilityScriptSerializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
* limitations under the License.
*/

export type SerializedValue =
undefined | boolean | number | string |
{ v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
{ d: string } |
{ r: [string, string] } |
{ a: SerializedValue[] } |
{ o: { [key: string]: SerializedValue } } |
{ h: number };

function isRegExp(obj: any): obj is RegExp {
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
}
Expand All @@ -26,44 +35,48 @@ function isError(obj: any): obj is Error {
return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error');
}

export function parseEvaluationResultValue(value: any, handles: any[] = []): any {
export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any {
if (value === undefined)
return undefined;
if (typeof value === 'object') {
if (value.v === 'undefined')
return undefined;
if (value.v === null)
return null;
if (value.v === 'NaN')
return NaN;
if (value.v === 'Infinity')
return Infinity;
if (value.v === '-Infinity')
return -Infinity;
if (value.v === '-0')
return -0;
if (value.d)
if ('v' in value) {
if (value.v === 'undefined')
return undefined;
if (value.v === 'null')
return null;
if (value.v === 'NaN')
return NaN;
if (value.v === 'Infinity')
return Infinity;
if (value.v === '-Infinity')
return -Infinity;
if (value.v === '-0')
return -0;
}
if ('d' in value)
return new Date(value.d);
if (value.r)
if ('r' in value)
return new RegExp(value.r[0], value.r[1]);
if (value.a)
if ('a' in value)
return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
if (value.o) {
if ('o' in value) {
const result: any = {};
for (const name of Object.keys(value.o))
value.o[name] = parseEvaluationResultValue(value.o[name], handles);
return value.o;
result[name] = parseEvaluationResultValue(value.o[name], handles);
return result;
}
if (typeof value.h === 'number')
if ('h' in value)
return handles[value.h];
}
return value;
}

export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }): any {
export type HandleOrValue = { h: number } | { fallThrough: any };
export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => HandleOrValue): SerializedValue {
return serialize(value, jsHandleSerializer, new Set());
}

function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }, visited: Set<any>): any {
function serialize(value: any, jsHandleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
const result = jsHandleSerializer(value);
if ('fallThrough' in result)
value = result.fallThrough;
Expand All @@ -77,7 +90,7 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
if (Object.is(value, undefined))
return { v: 'undefined' };
if (Object.is(value, null))
return { v: null };
return { v: 'null' };
if (Object.is(value, NaN))
return { v: 'NaN' };
if (Object.is(value, Infinity))
Expand All @@ -86,7 +99,12 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
return { v: '-Infinity' };
if (Object.is(value, -0))
return { v: '-0' };
if (isPrimitiveValue(value))

if (typeof value === 'boolean')
return value;
if (typeof value === 'number')
return value;
if (typeof value === 'string')
return value;

if (isError(value)) {
Expand Down Expand Up @@ -130,14 +148,3 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
return { o: result };
}
}

export function isPrimitiveValue(value: any): boolean {
switch (typeof value) {
case 'boolean':
case 'number':
case 'string':
return true;
default:
return false;
}
}
2 changes: 1 addition & 1 deletion src/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu
return handles.length - 1;
};

args = args.map(arg => serializeAsCallArgument(arg, (handle: any): { h?: number, fallThrough?: any } => {
args = args.map(arg => serializeAsCallArgument(arg, handle => {
if (handle instanceof JSHandle) {
if (!handle._objectId)
return { fallThrough: handle._value };
Expand Down
40 changes: 22 additions & 18 deletions src/rpc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

import { EventEmitter } from 'events';
import * as types from '../types';
import { SerializedValue } from '../common/utilityScriptSerializers';

export type Binary = string;
export type SerializedArgument = { value: SerializedValue, handles: Channel[] };

export type BrowserContextOptions = {
viewport?: types.Size | null,
ignoreHTTPSErrors?: boolean,
Expand Down Expand Up @@ -198,17 +201,17 @@ export interface FrameChannel extends Channel {
on(event: 'loadstate', callback: (params: { add?: types.LifecycleEvent, remove?: types.LifecycleEvent }) => void): this;
on(event: 'navigated', callback: (params: FrameNavigatedEvent) => void): this;

evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
addScriptTag(params: { url?: string, content?: string, type?: string }): Promise<{ element: ElementHandleChannel }>;
addStyleTag(params: { url?: string, content?: string }): Promise<{ element: ElementHandleChannel }>;
check(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
click(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
content(): Promise<{ value: string }>;
dblclick(params: { selector: string, force?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>;
dispatchEvent(params: { selector: string, type: string, eventInit: any } & types.TimeoutOptions): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>;
dispatchEvent(params: { selector: string, type: string, eventInit: SerializedArgument } & types.TimeoutOptions): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
fill(params: { selector: string, value: string } & types.NavigatingActionWaitOptions): Promise<void>;
focus(params: { selector: string } & types.TimeoutOptions): Promise<void>;
frameElement(): Promise<{ element: ElementHandleChannel }>;
Expand All @@ -227,7 +230,7 @@ export interface FrameChannel extends Channel {
title(): Promise<{ value: string }>;
type(params: { selector: string, text: string, delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
uncheck(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
waitForFunction(params: { expression: string, isFunction: boolean, arg: any } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }>;
waitForFunction(params: { expression: string, isFunction: boolean, arg: SerializedArgument } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }>;
waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element: ElementHandleChannel | null }>;
}
export type FrameInitializer = {
Expand All @@ -239,8 +242,8 @@ export type FrameInitializer = {


export interface WorkerChannel extends Channel {
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
}
export type WorkerInitializer = {
url: string,
Expand All @@ -251,26 +254,26 @@ export interface JSHandleChannel extends Channel {
on(event: 'previewUpdated', callback: (params: { preview: string }) => void): this;

dispose(): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
getPropertyList(): Promise<{ properties: { name: string, value: JSHandleChannel}[] }>;
getProperty(params: { name: string }): Promise<{ handle: JSHandleChannel }>;
jsonValue(): Promise<{ value: any }>;
jsonValue(): Promise<{ value: SerializedValue }>;
}
export type JSHandleInitializer = {
preview: string,
};


export interface ElementHandleChannel extends JSHandleChannel {
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
boundingBox(): Promise<{ value: types.Rect | null }>;
check(params: { force?: boolean } & { noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
click(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
contentFrame(): Promise<{ frame: FrameChannel | null }>;
dblclick(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>;
dispatchEvent(params: { type: string, eventInit: any }): Promise<void>;
dispatchEvent(params: { type: string, eventInit: SerializedArgument }): Promise<void>;
fill(params: { value: string } & types.NavigatingActionWaitOptions): Promise<void>;
focus(): Promise<void>;
getAttribute(params: { name: string }): Promise<{ value: string | null }>;
Expand Down Expand Up @@ -342,11 +345,12 @@ export type ConsoleMessageInitializer = {

export interface BindingCallChannel extends Channel {
reject(params: { error: types.Error }): void;
resolve(params: { result: any }): void;
resolve(params: { result: SerializedArgument }): void;
}
export type BindingCallInitializer = {
frame: FrameChannel,
name: string,
// TODO: migrate this to SerializedArgument.
args: any[]
};

Expand Down Expand Up @@ -426,9 +430,9 @@ export interface ElectronApplicationChannel extends Channel {
on(event: 'close', callback: () => void): this;
on(event: 'window', callback: (params: { page: PageChannel, browserWindow: JSHandleChannel }) => void): this;

newBrowserWindow(params: { arg: any }): Promise<{ page: PageChannel }>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>;
newBrowserWindow(params: { arg: SerializedArgument }): Promise<{ page: PageChannel }>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
close(): Promise<void>;
}
export type ElectronApplicationInitializer = {
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {

async dispatchEvent(type: string, eventInit: Object = {}) {
return this._wrapApiCall('elementHandle.dispatchEvent', async () => {
await this._elementChannel.dispatchEvent({ type, eventInit });
await this._elementChannel.dispatchEvent({ type, eventInit: serializeArgument(eventInit) });
});
}

Expand Down
24 changes: 13 additions & 11 deletions src/rpc/client/jsHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
* limitations under the License.
*/

import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { JSHandleChannel, JSHandleInitializer, SerializedArgument, Channel } from '../channels';
import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner';
import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers';
import { serializeAsCallArgument, parseEvaluationResultValue, SerializedValue } from '../../common/utilityScriptSerializers';

type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
type Unboxed<Arg> =
Expand Down Expand Up @@ -95,20 +95,22 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
}
}

export function serializeArgument(arg: any): any {
const guids: { guid: string }[] = [];
const pushHandle = (guid: string): number => {
guids.push({ guid });
return guids.length - 1;
// This function takes care of converting all JSHandles to their channels,
// so that generic channel serializer converts them to guids.
export function serializeArgument(arg: any): SerializedArgument {
const handles: Channel[] = [];
const pushHandle = (channel: Channel): number => {
handles.push(channel);
return handles.length - 1;
};
const value = serializeAsCallArgument(arg, value => {
if (value instanceof ChannelOwner)
return { h: pushHandle(value._guid) };
if (value instanceof JSHandle)
return { h: pushHandle(value._channel) };
return { fallThrough: value };
});
return { value, guids };
return { value, handles };
}

export function parseResult(arg: any): any {
export function parseResult(arg: SerializedValue): any {
return parseEvaluationResultValue(arg, []);
}
9 changes: 5 additions & 4 deletions src/rpc/server/electronDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron';
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions } from '../channels';
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions, SerializedArgument } from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserContextBase } from '../../browserContext';
import { PageDispatcher } from './pageDispatcher';
import { parseArgument } from './jsHandleDispatcher';
import { createHandle } from './elementHandlerDispatcher';
import { SerializedValue } from '../../common/utilityScriptSerializers';

export class ElectronDispatcher extends Dispatcher<Electron, ElectronInitializer> implements ElectronChannel {
constructor(scope: DispatcherScope, electron: Electron) {
Expand Down Expand Up @@ -49,17 +50,17 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
});
}

async newBrowserWindow(params: { arg: any }): Promise<{ page: PageChannel }> {
async newBrowserWindow(params: { arg: SerializedArgument }): Promise<{ page: PageChannel }> {
const page = await this._object.newBrowserWindow(parseArgument(params.arg));
return { page: lookupDispatcher<PageChannel>(page) };
}

async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> {
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
const handle = this._object._nodeElectronHandle!;
return { value: await handle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg)) };
}

async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }> {
async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }> {
const handle = this._object._nodeElectronHandle!;
const result = await handle._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
return { handle: createHandle(this._scope, result) };
Expand Down
Loading