chore: follow up to address evaluation review comments (#2380)

This commit is contained in:
Pavel Feldman 2020-05-27 22:19:05 -07:00 committed by GitHub
parent 46508c6b87
commit 6620008dcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 74 additions and 55 deletions

View file

@ -95,7 +95,7 @@ class CRAXNode implements accessibility.AXNode {
} }
async _findElement(element: dom.ElementHandle): Promise<CRAXNode | null> { async _findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
const objectId = element._objectId!; const objectId = element._objectId;
const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', { objectId }); const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', { objectId });
const needle = this.find(node => node._payload.backendDOMNodeId === backendNodeId); const needle = this.find(node => node._payload.backendDOMNodeId === backendNodeId);
return needle || null; return needle || null;

View file

@ -21,7 +21,7 @@ import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as js from '../javascript'; import * as js from '../javascript';
import * as debugSupport from '../debug/debugSupport'; import * as debugSupport from '../debug/debugSupport';
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject'; import { parseEvaluationResultValue } from '../utilityScriptSerializers';
export class CRExecutionContext implements js.ExecutionContextDelegate { export class CRExecutionContext implements js.ExecutionContextDelegate {
_client: CRSession; _client: CRSession;
@ -32,14 +32,14 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
this._contextId = contextPayload.id; this._contextId = contextPayload.id;
} }
async rawEvaluate(expression: string): Promise<RemoteObject> { async rawEvaluate(expression: string): Promise<string> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression), expression: debugSupport.ensureSourceUrl(expression),
contextId: this._contextId, contextId: this._contextId,
}).catch(rewriteError); }).catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return remoteObject; return remoteObject.objectId!;
} }
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
@ -100,6 +100,10 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
async releaseHandle(handle: js.JSHandle): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId) if (!handle._objectId)
return; return;
@ -129,3 +133,9 @@ function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
error.message += ' Are you passing a nested JSHandle?'; error.message += ' Are you passing a nested JSHandle?';
throw error; throw error;
} }
function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
const value = remoteObject.value;
const unserializableValue = remoteObject.unserializableValue;
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
}

View file

@ -65,9 +65,9 @@ export class FrameExecutionContext extends js.ExecutionContext {
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { noWaitAfter: true }); }, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { noWaitAfter: true });
} }
createHandle(remoteObject: any): js.JSHandle { createHandle(remoteObject: js.RemoteObject): js.JSHandle {
if (this.frame._page._delegate.isElementHandle(remoteObject)) if (this.frame._page._delegate.isElementHandle(remoteObject))
return new ElementHandle(this, remoteObject); return new ElementHandle(this, remoteObject.objectId!);
return super.createHandle(remoteObject); return super.createHandle(remoteObject);
} }
@ -81,7 +81,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
${custom.join(',\n')} ${custom.join(',\n')}
]) ])
`; `;
this._injectedPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object)); this._injectedPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId));
} }
return this._injectedPromise; return this._injectedPromise;
} }
@ -90,9 +90,11 @@ export class FrameExecutionContext extends js.ExecutionContext {
export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> { export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
readonly _context: FrameExecutionContext; readonly _context: FrameExecutionContext;
readonly _page: Page; readonly _page: Page;
readonly _objectId: string;
constructor(context: FrameExecutionContext, remoteObject: any) { constructor(context: FrameExecutionContext, objectId: string) {
super(context, remoteObject); super(context, 'node', objectId);
this._objectId = objectId;
this._context = context; this._context = context;
this._page = context.frame._page; this._page = context.frame._page;
} }

View file

@ -20,7 +20,7 @@ import * as js from '../javascript';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as debugSupport from '../debug/debugSupport'; import * as debugSupport from '../debug/debugSupport';
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject'; import { parseEvaluationResultValue } from '../utilityScriptSerializers';
export class FFExecutionContext implements js.ExecutionContextDelegate { export class FFExecutionContext implements js.ExecutionContextDelegate {
_session: FFSession; _session: FFSession;
@ -31,14 +31,14 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
this._executionContextId = executionContextId; this._executionContextId = executionContextId;
} }
async rawEvaluate(expression: string): Promise<RemoteObject> { async rawEvaluate(expression: string): Promise<string> {
const payload = await this._session.send('Runtime.evaluate', { const payload = await this._session.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression), expression: debugSupport.ensureSourceUrl(expression),
returnByValue: false, returnByValue: false,
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
}).catch(rewriteError); }).catch(rewriteError);
checkException(payload.exceptionDetails); checkException(payload.exceptionDetails);
return payload.result!; return payload.result!.objectId!;
} }
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
@ -97,6 +97,10 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
async releaseHandle(handle: js.JSHandle): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId) if (!handle._objectId)
return; return;
@ -135,3 +139,9 @@ function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Pro
error.message += ' Are you passing a nested JSHandle?'; error.message += ' Are you passing a nested JSHandle?';
throw error; throw error;
} }
function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
const value = remoteObject.value;
const unserializableValue = remoteObject.unserializableValue;
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
}

View file

@ -373,7 +373,7 @@ export class FFPage implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> { async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { contentFrameId } = await this._session.send('Page.describeNode', { const { contentFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId!, objectId: handle._objectId,
}); });
if (!contentFrameId) if (!contentFrameId)
return null; return null;
@ -383,7 +383,7 @@ export class FFPage implements PageDelegate {
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> { async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
const { ownerFrameId } = await this._session.send('Page.describeNode', { const { ownerFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId!, objectId: handle._objectId
}); });
return ownerFrameId || null; return ownerFrameId || null;
} }
@ -414,7 +414,7 @@ export class FFPage implements PageDelegate {
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> { async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
return await this._session.send('Page.scrollIntoViewIfNeeded', { return await this._session.send('Page.scrollIntoViewIfNeeded', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId!, objectId: handle._objectId,
rect, rect,
}).then(() => 'success' as const).catch(e => { }).then(() => 'success' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document')) if (e instanceof Error && e.message.includes('Node is detached from document'))
@ -433,7 +433,7 @@ export class FFPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> { async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', { const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId!, objectId: handle._objectId,
}).catch(logError(this._page)); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
@ -452,7 +452,7 @@ export class FFPage implements PageDelegate {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> { async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('Page.adoptNode', { const result = await this._session.send('Page.adoptNode', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: handle._objectId!, objectId: handle._objectId,
executionContextId: (to._delegate as FFExecutionContext)._executionContextId executionContextId: (to._delegate as FFExecutionContext)._executionContextId
}); });
if (!result.remoteObject) if (!result.remoteObject)

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { serializeAsCallArgument, parseEvaluationResultValue } from '../remoteObject'; import { serializeAsCallArgument, parseEvaluationResultValue } from '../utilityScriptSerializers';
export default class UtilityScript { export default class UtilityScript {
evaluate(returnByValue: boolean, expression: string) { evaluate(returnByValue: boolean, expression: string) {

View file

@ -19,12 +19,18 @@ import * as dom from './dom';
import * as utilityScriptSource from './generated/utilityScriptSource'; import * as utilityScriptSource from './generated/utilityScriptSource';
import { InnerLogger } from './logger'; import { InnerLogger } from './logger';
import * as debugSupport from './debug/debugSupport'; import * as debugSupport from './debug/debugSupport';
import { RemoteObject, serializeAsCallArgument } from './remoteObject'; import { serializeAsCallArgument } from './utilityScriptSerializers';
export type RemoteObject = {
objectId?: string,
value?: any
};
export interface ExecutionContextDelegate { export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>; evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
rawEvaluate(pageFunction: string): Promise<RemoteObject>; rawEvaluate(pageFunction: string): Promise<string>;
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>; getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
releaseHandle(handle: JSHandle): Promise<void>; releaseHandle(handle: JSHandle): Promise<void>;
handleJSONValue<T>(handle: JSHandle<T>): Promise<T>; handleJSONValue<T>(handle: JSHandle<T>): Promise<T>;
} }
@ -62,13 +68,13 @@ export class ExecutionContext {
utilityScript(): Promise<JSHandle> { utilityScript(): Promise<JSHandle> {
if (!this._utilityScriptPromise) { if (!this._utilityScriptPromise) {
const source = `new (${utilityScriptSource.source})()`; const source = `new (${utilityScriptSource.source})()`;
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object)); this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new JSHandle(this, 'object', objectId));
} }
return this._utilityScriptPromise; return this._utilityScriptPromise;
} }
createHandle(remoteObject: RemoteObject): JSHandle { createHandle(remoteObject: RemoteObject): JSHandle {
return new JSHandle(this, remoteObject); return this._delegate.createHandle(this, remoteObject);
} }
} }
@ -79,14 +85,11 @@ export class JSHandle<T = any> {
readonly _value: any; readonly _value: any;
private _type: string; private _type: string;
constructor(context: ExecutionContext, remoteObject: RemoteObject) { constructor(context: ExecutionContext, type: string, objectId?: string, value?: any) {
this._context = context; this._context = context;
this._objectId = remoteObject.objectId; this._objectId = objectId;
// Remote objects for primitive (or unserializable) objects carry value. this._value = value;
this._value = potentiallyUnserializableValue(remoteObject); this._type = type;
// WebKit does not have a 'promise' type.
const isPromise = remoteObject.className === 'Promise';
this._type = isPromise ? 'promise' : remoteObject.subtype || remoteObject.type || 'object';
} }
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>; async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
@ -207,13 +210,7 @@ export async function prepareFunctionCall(
return { functionText, values: [ args.length, ...args ], handles: resultHandles, dispose }; return { functionText, values: [ args.length, ...args ], handles: resultHandles, dispose };
} }
function potentiallyUnserializableValue(remoteObject: RemoteObject): any { export function parseUnserializableValue(unserializableValue: string): any {
const value = remoteObject.value;
let unserializableValue = remoteObject.unserializableValue;
if (remoteObject.type === 'number' && value === null)
unserializableValue = remoteObject.description;
if (!unserializableValue)
return value;
if (unserializableValue === 'NaN') if (unserializableValue === 'NaN')
return NaN; return NaN;
if (unserializableValue === 'Infinity') if (unserializableValue === 'Infinity')
@ -222,5 +219,4 @@ function potentiallyUnserializableValue(remoteObject: RemoteObject): any {
return -Infinity; return -Infinity;
if (unserializableValue === '-0') if (unserializableValue === '-0')
return -0; return -0;
return undefined;
} }

View file

@ -16,16 +16,6 @@
// This file can't have dependencies, it is a part of the utility script. // This file can't have dependencies, it is a part of the utility script.
export type RemoteObject = {
type?: string,
subtype?: string,
className?: string,
objectId?: string,
value?: any,
unserializableValue?: string
description?: string
};
export function parseEvaluationResultValue(value: any, handles: any[] = []): any { export function parseEvaluationResultValue(value: any, handles: any[] = []): any {
// { type: 'undefined' } does not even have value. // { type: 'undefined' } does not even have value.
if (value === 'undefined') if (value === 'undefined')

View file

@ -20,7 +20,7 @@ import { helper } from '../helper';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as js from '../javascript'; import * as js from '../javascript';
import * as debugSupport from '../debug/debugSupport'; import * as debugSupport from '../debug/debugSupport';
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject'; import { parseEvaluationResultValue } from '../utilityScriptSerializers';
export class WKExecutionContext implements js.ExecutionContextDelegate { export class WKExecutionContext implements js.ExecutionContextDelegate {
private readonly _session: WKSession; private readonly _session: WKSession;
@ -40,7 +40,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
this._contextDestroyedCallback(); this._contextDestroyedCallback();
} }
async rawEvaluate(expression: string): Promise<RemoteObject> { async rawEvaluate(expression: string): Promise<string> {
const contextId = this._contextId; const contextId = this._contextId;
const response = await this._session.send('Runtime.evaluate', { const response = await this._session.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression), expression: debugSupport.ensureSourceUrl(expression),
@ -49,7 +49,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
}); });
if (response.wasThrown) if (response.wasThrown)
throw new Error('Evaluation failed: ' + response.result.description); throw new Error('Evaluation failed: ' + response.result.description);
return response.result; return response.result.objectId!;
} }
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
@ -154,6 +154,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
const isPromise = remoteObject.className === 'Promise';
return new js.JSHandle(context, isPromise ? 'promise' : remoteObject.subtype || remoteObject.type, remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
async releaseHandle(handle: js.JSHandle): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId) if (!handle._objectId)
return; return;
@ -183,3 +188,9 @@ const contextDestroyedResult = {
description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.' description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.'
} as Protocol.Runtime.RemoteObject } as Protocol.Runtime.RemoteObject
}; };
function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
const value = remoteObject.value;
const unserializableValue = remoteObject.type === 'number' && value === null ? remoteObject.description : undefined;
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
}

View file

@ -710,7 +710,7 @@ export class WKPage implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> { async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const nodeInfo = await this._session.send('DOM.describeNode', { const nodeInfo = await this._session.send('DOM.describeNode', {
objectId: handle._objectId! objectId: handle._objectId
}); });
if (!nodeInfo.contentFrameId) if (!nodeInfo.contentFrameId)
return null; return null;
@ -751,7 +751,7 @@ export class WKPage implements PageDelegate {
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> { async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
return await this._session.send('DOM.scrollIntoViewIfNeeded', { return await this._session.send('DOM.scrollIntoViewIfNeeded', {
objectId: handle._objectId!, objectId: handle._objectId,
rect, rect,
}).then(() => 'success' as const).catch(e => { }).then(() => 'success' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node does not have a layout object')) if (e instanceof Error && e.message.includes('Node does not have a layout object'))
@ -771,7 +771,7 @@ export class WKPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> { async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('DOM.getContentQuads', { const result = await this._session.send('DOM.getContentQuads', {
objectId: handle._objectId! objectId: handle._objectId
}).catch(logError(this._page)); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
@ -788,13 +788,13 @@ export class WKPage implements PageDelegate {
} }
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> { async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
const objectId = handle._objectId!; const objectId = handle._objectId;
await this._session.send('DOM.setInputFiles', { objectId, files: dom.toFileTransferPayload(files) }); await this._session.send('DOM.setInputFiles', { objectId, files: dom.toFileTransferPayload(files) });
} }
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> { async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('DOM.resolveNode', { const result = await this._session.send('DOM.resolveNode', {
objectId: handle._objectId!, objectId: handle._objectId,
executionContextId: (to._delegate as WKExecutionContext)._contextId executionContextId: (to._delegate as WKExecutionContext)._contextId
}).catch(logError(this._page)); }).catch(logError(this._page));
if (!result || result.object.subtype === 'null') if (!result || result.object.subtype === 'null')