Skip to content

Commit d8bedd8

Browse files
authored
chore: explicitly type SerializedArgument, fix rpc dispatchEvent (#2988)
We now have types for SerializedValue/SerializedArgument. This will allow us to avoid double parse/serialize for evaluation arguments/results. Drive-by: typing exposed a bug in ElementHandle.dispatchEvent().
1 parent 070a257 commit d8bedd8

11 files changed

+127
-110
lines changed

src/common/utilityScriptSerializers.ts

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17+
export type SerializedValue =
18+
undefined | boolean | number | string |
19+
{ v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
20+
{ d: string } |
21+
{ r: [string, string] } |
22+
{ a: SerializedValue[] } |
23+
{ o: { [key: string]: SerializedValue } } |
24+
{ h: number };
25+
1726
function isRegExp(obj: any): obj is RegExp {
1827
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
1928
}
@@ -26,44 +35,48 @@ function isError(obj: any): obj is Error {
2635
return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error');
2736
}
2837

29-
export function parseEvaluationResultValue(value: any, handles: any[] = []): any {
38+
export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any {
3039
if (value === undefined)
3140
return undefined;
3241
if (typeof value === 'object') {
33-
if (value.v === 'undefined')
34-
return undefined;
35-
if (value.v === null)
36-
return null;
37-
if (value.v === 'NaN')
38-
return NaN;
39-
if (value.v === 'Infinity')
40-
return Infinity;
41-
if (value.v === '-Infinity')
42-
return -Infinity;
43-
if (value.v === '-0')
44-
return -0;
45-
if (value.d)
42+
if ('v' in value) {
43+
if (value.v === 'undefined')
44+
return undefined;
45+
if (value.v === 'null')
46+
return null;
47+
if (value.v === 'NaN')
48+
return NaN;
49+
if (value.v === 'Infinity')
50+
return Infinity;
51+
if (value.v === '-Infinity')
52+
return -Infinity;
53+
if (value.v === '-0')
54+
return -0;
55+
}
56+
if ('d' in value)
4657
return new Date(value.d);
47-
if (value.r)
58+
if ('r' in value)
4859
return new RegExp(value.r[0], value.r[1]);
49-
if (value.a)
60+
if ('a' in value)
5061
return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
51-
if (value.o) {
62+
if ('o' in value) {
63+
const result: any = {};
5264
for (const name of Object.keys(value.o))
53-
value.o[name] = parseEvaluationResultValue(value.o[name], handles);
54-
return value.o;
65+
result[name] = parseEvaluationResultValue(value.o[name], handles);
66+
return result;
5567
}
56-
if (typeof value.h === 'number')
68+
if ('h' in value)
5769
return handles[value.h];
5870
}
5971
return value;
6072
}
6173

62-
export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }): any {
74+
export type HandleOrValue = { h: number } | { fallThrough: any };
75+
export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => HandleOrValue): SerializedValue {
6376
return serialize(value, jsHandleSerializer, new Set());
6477
}
6578

66-
function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }, visited: Set<any>): any {
79+
function serialize(value: any, jsHandleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
6780
const result = jsHandleSerializer(value);
6881
if ('fallThrough' in result)
6982
value = result.fallThrough;
@@ -77,7 +90,7 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
7790
if (Object.is(value, undefined))
7891
return { v: 'undefined' };
7992
if (Object.is(value, null))
80-
return { v: null };
93+
return { v: 'null' };
8194
if (Object.is(value, NaN))
8295
return { v: 'NaN' };
8396
if (Object.is(value, Infinity))
@@ -86,7 +99,12 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
8699
return { v: '-Infinity' };
87100
if (Object.is(value, -0))
88101
return { v: '-0' };
89-
if (isPrimitiveValue(value))
102+
103+
if (typeof value === 'boolean')
104+
return value;
105+
if (typeof value === 'number')
106+
return value;
107+
if (typeof value === 'string')
90108
return value;
91109

92110
if (isError(value)) {
@@ -130,14 +148,3 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
130148
return { o: result };
131149
}
132150
}
133-
134-
export function isPrimitiveValue(value: any): boolean {
135-
switch (typeof value) {
136-
case 'boolean':
137-
case 'number':
138-
case 'string':
139-
return true;
140-
default:
141-
return false;
142-
}
143-
}

src/javascript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu
196196
return handles.length - 1;
197197
};
198198

199-
args = args.map(arg => serializeAsCallArgument(arg, (handle: any): { h?: number, fallThrough?: any } => {
199+
args = args.map(arg => serializeAsCallArgument(arg, handle => {
200200
if (handle instanceof JSHandle) {
201201
if (!handle._objectId)
202202
return { fallThrough: handle._value };

src/rpc/channels.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
import { EventEmitter } from 'events';
1818
import * as types from '../types';
19+
import { SerializedValue } from '../common/utilityScriptSerializers';
1920

2021
export type Binary = string;
22+
export type SerializedArgument = { value: SerializedValue, handles: Channel[] };
23+
2124
export type BrowserContextOptions = {
2225
viewport?: types.Size | null,
2326
ignoreHTTPSErrors?: boolean,
@@ -215,17 +218,17 @@ export interface FrameChannel extends Channel {
215218
on(event: 'loadstate', callback: (params: { add?: types.LifecycleEvent, remove?: types.LifecycleEvent }) => void): this;
216219
on(event: 'navigated', callback: (params: FrameNavigatedEvent) => void): this;
217220

218-
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
219-
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
221+
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
222+
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
220223
addScriptTag(params: { url?: string, content?: string, type?: string }): Promise<{ element: ElementHandleChannel }>;
221224
addStyleTag(params: { url?: string, content?: string }): Promise<{ element: ElementHandleChannel }>;
222225
check(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
223226
click(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
224227
content(): Promise<{ value: string }>;
225228
dblclick(params: { selector: string, force?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>;
226-
dispatchEvent(params: { selector: string, type: string, eventInit: any } & types.TimeoutOptions): Promise<void>;
227-
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>;
228-
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>;
229+
dispatchEvent(params: { selector: string, type: string, eventInit: SerializedArgument } & types.TimeoutOptions): Promise<void>;
230+
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
231+
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
229232
fill(params: { selector: string, value: string } & types.NavigatingActionWaitOptions): Promise<void>;
230233
focus(params: { selector: string } & types.TimeoutOptions): Promise<void>;
231234
frameElement(): Promise<{ element: ElementHandleChannel }>;
@@ -244,7 +247,7 @@ export interface FrameChannel extends Channel {
244247
title(): Promise<{ value: string }>;
245248
type(params: { selector: string, text: string, delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
246249
uncheck(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
247-
waitForFunction(params: { expression: string, isFunction: boolean, arg: any } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }>;
250+
waitForFunction(params: { expression: string, isFunction: boolean, arg: SerializedArgument } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }>;
248251
waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element: ElementHandleChannel | null }>;
249252
}
250253
export type FrameInitializer = {
@@ -256,8 +259,8 @@ export type FrameInitializer = {
256259

257260

258261
export interface WorkerChannel extends Channel {
259-
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
260-
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>;
262+
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
263+
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
261264
}
262265
export type WorkerInitializer = {
263266
url: string,
@@ -268,26 +271,26 @@ export interface JSHandleChannel extends Channel {
268271
on(event: 'previewUpdated', callback: (params: { preview: string }) => void): this;
269272

270273
dispose(): Promise<void>;
271-
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
272-
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>;
274+
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
275+
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
273276
getPropertyList(): Promise<{ properties: { name: string, value: JSHandleChannel}[] }>;
274277
getProperty(params: { name: string }): Promise<{ handle: JSHandleChannel }>;
275-
jsonValue(): Promise<{ value: any }>;
278+
jsonValue(): Promise<{ value: SerializedValue }>;
276279
}
277280
export type JSHandleInitializer = {
278281
preview: string,
279282
};
280283

281284

282285
export interface ElementHandleChannel extends JSHandleChannel {
283-
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
284-
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
286+
evalOnSelector(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
287+
evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
285288
boundingBox(): Promise<{ value: types.Rect | null }>;
286289
check(params: { force?: boolean } & { noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
287290
click(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
288291
contentFrame(): Promise<{ frame: FrameChannel | null }>;
289292
dblclick(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>;
290-
dispatchEvent(params: { type: string, eventInit: any }): Promise<void>;
293+
dispatchEvent(params: { type: string, eventInit: SerializedArgument }): Promise<void>;
291294
fill(params: { value: string } & types.NavigatingActionWaitOptions): Promise<void>;
292295
focus(): Promise<void>;
293296
getAttribute(params: { name: string }): Promise<{ value: string | null }>;
@@ -359,11 +362,12 @@ export type ConsoleMessageInitializer = {
359362

360363
export interface BindingCallChannel extends Channel {
361364
reject(params: { error: types.Error }): void;
362-
resolve(params: { result: any }): void;
365+
resolve(params: { result: SerializedArgument }): void;
363366
}
364367
export type BindingCallInitializer = {
365368
frame: FrameChannel,
366369
name: string,
370+
// TODO: migrate this to SerializedArgument.
367371
args: any[]
368372
};
369373

@@ -443,9 +447,9 @@ export interface ElectronApplicationChannel extends Channel {
443447
on(event: 'close', callback: () => void): this;
444448
on(event: 'window', callback: (params: { page: PageChannel, browserWindow: JSHandleChannel }) => void): this;
445449

446-
newBrowserWindow(params: { arg: any }): Promise<{ page: PageChannel }>;
447-
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>;
448-
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>;
450+
newBrowserWindow(params: { arg: SerializedArgument }): Promise<{ page: PageChannel }>;
451+
evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
452+
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
449453
close(): Promise<void>;
450454
}
451455
export type ElectronApplicationInitializer = {

src/rpc/client/elementHandle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
8080

8181
async dispatchEvent(type: string, eventInit: Object = {}) {
8282
return this._wrapApiCall('elementHandle.dispatchEvent', async () => {
83-
await this._elementChannel.dispatchEvent({ type, eventInit });
83+
await this._elementChannel.dispatchEvent({ type, eventInit: serializeArgument(eventInit) });
8484
});
8585
}
8686

src/rpc/client/jsHandle.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { JSHandleChannel, JSHandleInitializer } from '../channels';
17+
import { JSHandleChannel, JSHandleInitializer, SerializedArgument, Channel } from '../channels';
1818
import { ElementHandle } from './elementHandle';
1919
import { ChannelOwner } from './channelOwner';
20-
import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers';
20+
import { serializeAsCallArgument, parseEvaluationResultValue, SerializedValue } from '../../common/utilityScriptSerializers';
2121

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

98-
export function serializeArgument(arg: any): any {
99-
const guids: { guid: string }[] = [];
100-
const pushHandle = (guid: string): number => {
101-
guids.push({ guid });
102-
return guids.length - 1;
98+
// This function takes care of converting all JSHandles to their channels,
99+
// so that generic channel serializer converts them to guids.
100+
export function serializeArgument(arg: any): SerializedArgument {
101+
const handles: Channel[] = [];
102+
const pushHandle = (channel: Channel): number => {
103+
handles.push(channel);
104+
return handles.length - 1;
103105
};
104106
const value = serializeAsCallArgument(arg, value => {
105-
if (value instanceof ChannelOwner)
106-
return { h: pushHandle(value._guid) };
107+
if (value instanceof JSHandle)
108+
return { h: pushHandle(value._channel) };
107109
return { fallThrough: value };
108110
});
109-
return { value, guids };
111+
return { value, handles };
110112
}
111113

112-
export function parseResult(arg: any): any {
114+
export function parseResult(arg: SerializedValue): any {
113115
return parseEvaluationResultValue(arg, []);
114116
}

src/rpc/server/electronDispatcher.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
1818
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron';
19-
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions } from '../channels';
19+
import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, ElectronLaunchOptions, SerializedArgument } from '../channels';
2020
import { BrowserContextDispatcher } from './browserContextDispatcher';
2121
import { BrowserContextBase } from '../../browserContext';
2222
import { PageDispatcher } from './pageDispatcher';
2323
import { parseArgument } from './jsHandleDispatcher';
2424
import { createHandle } from './elementHandlerDispatcher';
25+
import { SerializedValue } from '../../common/utilityScriptSerializers';
2526

2627
export class ElectronDispatcher extends Dispatcher<Electron, ElectronInitializer> implements ElectronChannel {
2728
constructor(scope: DispatcherScope, electron: Electron) {
@@ -49,17 +50,17 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
4950
});
5051
}
5152

52-
async newBrowserWindow(params: { arg: any }): Promise<{ page: PageChannel }> {
53+
async newBrowserWindow(params: { arg: SerializedArgument }): Promise<{ page: PageChannel }> {
5354
const page = await this._object.newBrowserWindow(parseArgument(params.arg));
5455
return { page: lookupDispatcher<PageChannel>(page) };
5556
}
5657

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

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

0 commit comments

Comments
 (0)