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().
This commit is contained in:
Dmitry Gozman 2020-07-17 09:53:13 -07:00 committed by GitHub
parent 070a257600
commit d8bedd851d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 131 additions and 114 deletions

View file

@ -14,6 +14,15 @@
* limitations under the License. * 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 { function isRegExp(obj: any): obj is RegExp {
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
} }
@ -26,44 +35,48 @@ function isError(obj: any): obj is Error {
return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === '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) if (value === undefined)
return undefined; return undefined;
if (typeof value === 'object') { if (typeof value === 'object') {
if (value.v === 'undefined') if ('v' in value) {
return undefined; if (value.v === 'undefined')
if (value.v === null) return undefined;
return null; if (value.v === 'null')
if (value.v === 'NaN') return null;
return NaN; if (value.v === 'NaN')
if (value.v === 'Infinity') return NaN;
return Infinity; if (value.v === 'Infinity')
if (value.v === '-Infinity') return Infinity;
return -Infinity; if (value.v === '-Infinity')
if (value.v === '-0') return -Infinity;
return -0; if (value.v === '-0')
if (value.d) return -0;
return new Date(value.d);
if (value.r)
return new RegExp(value.r[0], value.r[1]);
if (value.a)
return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
if (value.o) {
for (const name of Object.keys(value.o))
value.o[name] = parseEvaluationResultValue(value.o[name], handles);
return value.o;
} }
if (typeof value.h === 'number') if ('d' in value)
return new Date(value.d);
if ('r' in value)
return new RegExp(value.r[0], value.r[1]);
if ('a' in value)
return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
if ('o' in value) {
const result: any = {};
for (const name of Object.keys(value.o))
result[name] = parseEvaluationResultValue(value.o[name], handles);
return result;
}
if ('h' in value)
return handles[value.h]; return handles[value.h];
} }
return value; 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()); 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); const result = jsHandleSerializer(value);
if ('fallThrough' in result) if ('fallThrough' in result)
value = result.fallThrough; value = result.fallThrough;
@ -77,7 +90,7 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
if (Object.is(value, undefined)) if (Object.is(value, undefined))
return { v: 'undefined' }; return { v: 'undefined' };
if (Object.is(value, null)) if (Object.is(value, null))
return { v: null }; return { v: 'null' };
if (Object.is(value, NaN)) if (Object.is(value, NaN))
return { v: 'NaN' }; return { v: 'NaN' };
if (Object.is(value, Infinity)) if (Object.is(value, Infinity))
@ -86,7 +99,12 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
return { v: '-Infinity' }; return { v: '-Infinity' };
if (Object.is(value, -0)) if (Object.is(value, -0))
return { v: '-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; return value;
if (isError(value)) { if (isError(value)) {
@ -130,14 +148,3 @@ function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough
return { o: result }; return { o: result };
} }
} }
export function isPrimitiveValue(value: any): boolean {
switch (typeof value) {
case 'boolean':
case 'number':
case 'string':
return true;
default:
return false;
}
}

View file

@ -196,7 +196,7 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu
return handles.length - 1; 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 instanceof JSHandle) {
if (!handle._objectId) if (!handle._objectId)
return { fallThrough: handle._value }; return { fallThrough: handle._value };

View file

@ -16,8 +16,11 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as types from '../types'; import * as types from '../types';
import { SerializedValue } from '../common/utilityScriptSerializers';
export type Binary = string; export type Binary = string;
export type SerializedArgument = { value: SerializedValue, handles: Channel[] };
export type BrowserContextOptions = { export type BrowserContextOptions = {
viewport?: types.Size | null, viewport?: types.Size | null,
ignoreHTTPSErrors?: boolean, ignoreHTTPSErrors?: boolean,
@ -215,17 +218,17 @@ export interface FrameChannel extends Channel {
on(event: 'loadstate', callback: (params: { add?: types.LifecycleEvent, remove?: types.LifecycleEvent }) => void): this; on(event: 'loadstate', callback: (params: { add?: types.LifecycleEvent, remove?: types.LifecycleEvent }) => void): this;
on(event: 'navigated', callback: (params: FrameNavigatedEvent) => void): this; on(event: 'navigated', callback: (params: FrameNavigatedEvent) => void): this;
evalOnSelector(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: any}): Promise<{ value: any }>; evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
addScriptTag(params: { url?: string, content?: string, type?: string }): Promise<{ element: ElementHandleChannel }>; addScriptTag(params: { url?: string, content?: string, type?: string }): Promise<{ element: ElementHandleChannel }>;
addStyleTag(params: { url?: string, content?: 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>; 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>; click(params: { selector: string, force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
content(): Promise<{ value: string }>; content(): Promise<{ value: string }>;
dblclick(params: { selector: string, force?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>; 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>; dispatchEvent(params: { selector: string, type: string, eventInit: SerializedArgument } & types.TimeoutOptions): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ value: any }>; evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>; evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
fill(params: { selector: string, value: string } & types.NavigatingActionWaitOptions): Promise<void>; fill(params: { selector: string, value: string } & types.NavigatingActionWaitOptions): Promise<void>;
focus(params: { selector: string } & types.TimeoutOptions): Promise<void>; focus(params: { selector: string } & types.TimeoutOptions): Promise<void>;
frameElement(): Promise<{ element: ElementHandleChannel }>; frameElement(): Promise<{ element: ElementHandleChannel }>;
@ -244,7 +247,7 @@ export interface FrameChannel extends Channel {
title(): Promise<{ value: string }>; title(): Promise<{ value: string }>;
type(params: { selector: string, text: string, delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>; 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>; 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 }>; waitForSelector(params: { selector: string } & types.WaitForElementOptions): Promise<{ element: ElementHandleChannel | null }>;
} }
export type FrameInitializer = { export type FrameInitializer = {
@ -256,8 +259,8 @@ export type FrameInitializer = {
export interface WorkerChannel extends Channel { export interface WorkerChannel extends Channel {
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>; evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>; evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
} }
export type WorkerInitializer = { export type WorkerInitializer = {
url: string, url: string,
@ -268,11 +271,11 @@ export interface JSHandleChannel extends Channel {
on(event: 'previewUpdated', callback: (params: { preview: string }) => void): this; on(event: 'previewUpdated', callback: (params: { preview: string }) => void): this;
dispose(): Promise<void>; dispose(): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>; evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<{ handle: JSHandleChannel }>; evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
getPropertyList(): Promise<{ properties: { name: string, value: JSHandleChannel}[] }>; getPropertyList(): Promise<{ properties: { name: string, value: JSHandleChannel}[] }>;
getProperty(params: { name: string }): Promise<{ handle: JSHandleChannel }>; getProperty(params: { name: string }): Promise<{ handle: JSHandleChannel }>;
jsonValue(): Promise<{ value: any }>; jsonValue(): Promise<{ value: SerializedValue }>;
} }
export type JSHandleInitializer = { export type JSHandleInitializer = {
preview: string, preview: string,
@ -280,14 +283,14 @@ export type JSHandleInitializer = {
export interface ElementHandleChannel extends JSHandleChannel { export interface ElementHandleChannel extends JSHandleChannel {
evalOnSelector(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: any }): Promise<{ value: any }>; evalOnSelectorAll(params: { selector: string; expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
boundingBox(): Promise<{ value: types.Rect | null }>; boundingBox(): Promise<{ value: types.Rect | null }>;
check(params: { force?: boolean } & { noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>; check(params: { force?: boolean } & { noWaitAfter?: boolean } & types.TimeoutOptions): Promise<void>;
click(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>; click(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions): Promise<void>;
contentFrame(): Promise<{ frame: FrameChannel | null }>; contentFrame(): Promise<{ frame: FrameChannel | null }>;
dblclick(params: { force?: boolean, noWaitAfter?: boolean } & types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions): Promise<void>; 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>; fill(params: { value: string } & types.NavigatingActionWaitOptions): Promise<void>;
focus(): Promise<void>; focus(): Promise<void>;
getAttribute(params: { name: string }): Promise<{ value: string | null }>; getAttribute(params: { name: string }): Promise<{ value: string | null }>;
@ -359,11 +362,12 @@ export type ConsoleMessageInitializer = {
export interface BindingCallChannel extends Channel { export interface BindingCallChannel extends Channel {
reject(params: { error: types.Error }): void; reject(params: { error: types.Error }): void;
resolve(params: { result: any }): void; resolve(params: { result: SerializedArgument }): void;
} }
export type BindingCallInitializer = { export type BindingCallInitializer = {
frame: FrameChannel, frame: FrameChannel,
name: string, name: string,
// TODO: migrate this to SerializedArgument.
args: any[] args: any[]
}; };
@ -443,9 +447,9 @@ export interface ElectronApplicationChannel extends Channel {
on(event: 'close', callback: () => void): this; on(event: 'close', callback: () => void): this;
on(event: 'window', callback: (params: { page: PageChannel, browserWindow: JSHandleChannel }) => void): this; on(event: 'window', callback: (params: { page: PageChannel, browserWindow: JSHandleChannel }) => void): this;
newBrowserWindow(params: { arg: any }): Promise<{ page: PageChannel }>; newBrowserWindow(params: { arg: SerializedArgument }): Promise<{ page: PageChannel }>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }>; evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ handle: JSHandleChannel }>; evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }>;
close(): Promise<void>; close(): Promise<void>;
} }
export type ElectronApplicationInitializer = { export type ElectronApplicationInitializer = {

View file

@ -80,7 +80,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
async dispatchEvent(type: string, eventInit: Object = {}) { async dispatchEvent(type: string, eventInit: Object = {}) {
return this._wrapApiCall('elementHandle.dispatchEvent', async () => { return this._wrapApiCall('elementHandle.dispatchEvent', async () => {
await this._elementChannel.dispatchEvent({ type, eventInit }); await this._elementChannel.dispatchEvent({ type, eventInit: serializeArgument(eventInit) });
}); });
} }

View file

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { JSHandleChannel, JSHandleInitializer, SerializedArgument, Channel } from '../channels';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner'; 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 NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);
type Unboxed<Arg> = type Unboxed<Arg> =
@ -95,20 +95,22 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
} }
} }
export function serializeArgument(arg: any): any { // This function takes care of converting all JSHandles to their channels,
const guids: { guid: string }[] = []; // so that generic channel serializer converts them to guids.
const pushHandle = (guid: string): number => { export function serializeArgument(arg: any): SerializedArgument {
guids.push({ guid }); const handles: Channel[] = [];
return guids.length - 1; const pushHandle = (channel: Channel): number => {
handles.push(channel);
return handles.length - 1;
}; };
const value = serializeAsCallArgument(arg, value => { const value = serializeAsCallArgument(arg, value => {
if (value instanceof ChannelOwner) if (value instanceof JSHandle)
return { h: pushHandle(value._guid) }; return { h: pushHandle(value._channel) };
return { fallThrough: value }; return { fallThrough: value };
}); });
return { value, guids }; return { value, handles };
} }
export function parseResult(arg: any): any { export function parseResult(arg: SerializedValue): any {
return parseEvaluationResultValue(arg, []); return parseEvaluationResultValue(arg, []);
} }

View file

@ -16,12 +16,13 @@
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron'; 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 { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserContextBase } from '../../browserContext'; import { BrowserContextBase } from '../../browserContext';
import { PageDispatcher } from './pageDispatcher'; import { PageDispatcher } from './pageDispatcher';
import { parseArgument } from './jsHandleDispatcher'; import { parseArgument } from './jsHandleDispatcher';
import { createHandle } from './elementHandlerDispatcher'; import { createHandle } from './elementHandlerDispatcher';
import { SerializedValue } from '../../common/utilityScriptSerializers';
export class ElectronDispatcher extends Dispatcher<Electron, ElectronInitializer> implements ElectronChannel { export class ElectronDispatcher extends Dispatcher<Electron, ElectronInitializer> implements ElectronChannel {
constructor(scope: DispatcherScope, electron: Electron) { constructor(scope: DispatcherScope, electron: Electron) {
@ -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)); const page = await this._object.newBrowserWindow(parseArgument(params.arg));
return { page: lookupDispatcher<PageChannel>(page) }; 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!; const handle = this._object._nodeElectronHandle!;
return { value: await handle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg)) }; 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 handle = this._object._nodeElectronHandle!;
const result = await handle._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); const result = await handle._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
return { handle: createHandle(this._scope, result) }; return { handle: createHandle(this._scope, result) };

View file

@ -17,10 +17,11 @@
import { ElementHandle } from '../../dom'; import { ElementHandle } from '../../dom';
import * as js from '../../javascript'; import * as js from '../../javascript';
import * as types from '../../types'; import * as types from '../../types';
import { ElementHandleChannel, FrameChannel, Binary } from '../channels'; import { ElementHandleChannel, FrameChannel, Binary, SerializedArgument } from '../channels';
import { DispatcherScope, lookupNullableDispatcher } from './dispatcher'; import { DispatcherScope, lookupNullableDispatcher } from './dispatcher';
import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { SerializedValue } from '../../common/utilityScriptSerializers';
export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher {
return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle);
@ -64,8 +65,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
return { value: await this._elementHandle.innerHTML() }; return { value: await this._elementHandle.innerHTML() };
} }
async dispatchEvent(params: { type: string, eventInit: Object }) { async dispatchEvent(params: { type: string, eventInit: SerializedArgument }) {
await this._elementHandle.dispatchEvent(params.type, params.eventInit); await this._elementHandle.dispatchEvent(params.type, parseArgument(params.eventInit));
} }
async scrollIntoViewIfNeeded(params: types.TimeoutOptions) { async scrollIntoViewIfNeeded(params: types.TimeoutOptions) {
@ -138,11 +139,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme
return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) }; return { elements: elements.map(e => new ElementHandleDispatcher(this._scope, e)) };
} }
async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
} }

View file

@ -16,11 +16,12 @@
import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent, kNavigationEvent, NavigationEvent } from '../../frames'; import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent, kNavigationEvent, NavigationEvent } from '../../frames';
import * as types from '../../types'; import * as types from '../../types';
import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel } from '../channels'; import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel, SerializedArgument } from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { convertSelectOptionValues, ElementHandleDispatcher, createHandle, convertInputFiles } from './elementHandlerDispatcher'; import { convertSelectOptionValues, ElementHandleDispatcher, createHandle, convertInputFiles } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers'; import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers';
import { SerializedValue } from '../../common/utilityScriptSerializers';
export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> implements FrameChannel { export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> implements FrameChannel {
private _frame: Frame; private _frame: Frame;
@ -64,11 +65,11 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return { element: new ElementHandleDispatcher(this._scope, await this._frame.frameElement()) }; return { element: new ElementHandleDispatcher(this._scope, await this._frame.frameElement()) };
} }
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._frame._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._frame._evaluateExpression(params.expression, params.isFunction, 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 }> {
return { handle: createHandle(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; return { handle: createHandle(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) };
} }
@ -76,15 +77,15 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(params.selector, params)) }; return { element: ElementHandleDispatcher.createNullable(this._scope, await this._frame.waitForSelector(params.selector, params)) };
} }
async dispatchEvent(params: { selector: string, type: string, eventInit: any } & types.TimeoutOptions): Promise<void> { async dispatchEvent(params: { selector: string, type: string, eventInit: SerializedArgument } & types.TimeoutOptions): Promise<void> {
return this._frame.dispatchEvent(params.selector, params.type, parseArgument(params.eventInit), params); return this._frame.dispatchEvent(params.selector, params.type, parseArgument(params.eventInit), params);
} }
async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evalOnSelector(params: { selector: string, expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evalOnSelectorAll(params: { selector: string, expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
@ -173,7 +174,7 @@ export class FrameDispatcher extends Dispatcher<Frame, FrameInitializer> impleme
await this._frame.uncheck(params.selector, params); await this._frame.uncheck(params.selector, params);
} }
async waitForFunction(params: { expression: string, isFunction: boolean, arg: any } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }> { async waitForFunction(params: { expression: string, isFunction: boolean, arg: SerializedArgument } & types.WaitForFunctionOptions): Promise<{ handle: JSHandleChannel }> {
return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params)) }; return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params)) };
} }

View file

@ -15,9 +15,9 @@
*/ */
import * as js from '../../javascript'; import * as js from '../../javascript';
import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { JSHandleChannel, JSHandleInitializer, SerializedArgument } from '../channels';
import { Dispatcher, DispatcherScope } from './dispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher';
import { parseEvaluationResultValue, serializeAsCallArgument } from '../../common/utilityScriptSerializers'; import { parseEvaluationResultValue, serializeAsCallArgument, SerializedValue } from '../../common/utilityScriptSerializers';
import { createHandle } from './elementHandlerDispatcher'; import { createHandle } from './elementHandlerDispatcher';
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
jsHandle._setPreviewCallback(preview => this._dispatchEvent('previewUpdated', { preview })); jsHandle._setPreviewCallback(preview => this._dispatchEvent('previewUpdated', { preview }));
} }
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<{ value: any }> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; return { value: serializeResult(await this._object._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 jsHandle = await this._object._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); const jsHandle = await this._object._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg));
return { handle: createHandle(this._scope, jsHandle) }; return { handle: createHandle(this._scope, jsHandle) };
} }
@ -51,7 +51,7 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
return { properties }; return { properties };
} }
async jsonValue(): Promise<{ value: any }> { async jsonValue(): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._object.jsonValue()) }; return { value: serializeResult(await this._object.jsonValue()) };
} }
@ -60,26 +60,12 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, JSHandleInitiali
} }
} }
export function parseArgument(arg: { value: any, guids: JSHandleDispatcher[] }): any { // Generic channel parser converts guids to JSHandleDispatchers,
return parseEvaluationResultValue(arg.value, convertDispatchersToObjects(arg.guids)); // and this function takes care of coverting them into underlying JSHandles.
export function parseArgument(arg: SerializedArgument): any {
return parseEvaluationResultValue(arg.value, arg.handles.map(arg => (arg as JSHandleDispatcher)._object));
} }
export function serializeResult(arg: any): any { export function serializeResult(arg: any): SerializedValue {
return serializeAsCallArgument(arg, value => ({ fallThrough: value })); 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

@ -20,7 +20,7 @@ import { Frame } from '../../frames';
import { Request } from '../../network'; import { Request } from '../../network';
import { Page, Worker } from '../../page'; import { Page, Worker } from '../../page';
import * as types from '../../types'; import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, PDFOptions } from '../channels'; import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, PDFOptions, SerializedArgument } from '../channels';
import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher';
import { parseError, serializeError, headersArrayToObject } from '../serializers'; import { parseError, serializeError, headersArrayToObject } from '../serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
@ -32,6 +32,7 @@ import { serializeResult, parseArgument } from './jsHandleDispatcher';
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { FileChooser } from '../../fileChooser'; import { FileChooser } from '../../fileChooser';
import { CRCoverage } from '../../chromium/crCoverage'; import { CRCoverage } from '../../chromium/crCoverage';
import { SerializedValue } from '../../common/utilityScriptSerializers';
export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements PageChannel { export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements PageChannel {
private _page: Page; private _page: Page;
@ -232,11 +233,11 @@ export class WorkerDispatcher extends Dispatcher<Worker, WorkerInitializer> impl
worker.on(Events.Worker.Close, () => this._dispatchEvent('close')); worker.on(Events.Worker.Close, () => this._dispatchEvent('close'));
} }
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<{ value: any }> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any, isPage?: boolean }): Promise<{ handle: JSHandleChannel }> { async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }> {
return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) };
} }
} }
@ -262,7 +263,7 @@ export class BindingCallDispatcher extends Dispatcher<{}, BindingCallInitializer
return this._promise; return this._promise;
} }
resolve(params: { result: any }) { resolve(params: { result: SerializedArgument }) {
this._resolve!(parseArgument(params.result)); this._resolve!(parseArgument(params.result));
} }

View file

@ -132,6 +132,20 @@ describe('Page.dispatchEvent(drag)', function() {
}); });
}); });
describe('ElementHandle.dispatchEvent(drag)', function() {
it.fail(WEBKIT)('should dispatch drag drop events', async({page, server}) => {
await page.goto(server.PREFIX + '/drag-n-drop.html');
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
const source = await page.$('#source');
await source.dispatchEvent('dragstart', { dataTransfer });
const target = await page.$('#target');
await target.dispatchEvent('drop', { dataTransfer });
expect(await page.evaluate(() => {
return source.parentElement === target;
})).toBeTruthy();
});
});
describe('ElementHandle.dispatchEvent(click)', function() { describe('ElementHandle.dispatchEvent(click)', function() {
it('should dispatch click event', async({page, server}) => { it('should dispatch click event', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');