Implement Frame.evaluate
This patch implements Frame.evaluate method. References #4.
This commit is contained in:
parent
4b0b3b5ff5
commit
3d90ea38a9
@ -37,6 +37,7 @@
|
|||||||
- [dialog.message()](#dialogmessage)
|
- [dialog.message()](#dialogmessage)
|
||||||
+ [class: Frame](#class-frame)
|
+ [class: Frame](#class-frame)
|
||||||
- [frame.childFrames()](#framechildframes)
|
- [frame.childFrames()](#framechildframes)
|
||||||
|
- [frame.evaluate(fun, args)](#frameevaluatefun-args)
|
||||||
- [frame.isDetached()](#frameisdetached)
|
- [frame.isDetached()](#frameisdetached)
|
||||||
- [frame.isMainFrame()](#frameismainframe)
|
- [frame.isMainFrame()](#frameismainframe)
|
||||||
- [frame.name()](#framename)
|
- [frame.name()](#framename)
|
||||||
@ -239,6 +240,12 @@ Pages could be closed by `page.close()` method.
|
|||||||
|
|
||||||
#### frame.childFrames()
|
#### frame.childFrames()
|
||||||
|
|
||||||
|
#### frame.evaluate(fun, args)
|
||||||
|
|
||||||
|
- `fun` <[function]> Function to be evaluated in browser context
|
||||||
|
- `args` <[Array]<[string]>> Arguments to pass to `fun`
|
||||||
|
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
|
||||||
|
|
||||||
#### frame.isDetached()
|
#### frame.isDetached()
|
||||||
|
|
||||||
#### frame.isMainFrame()
|
#### frame.isMainFrame()
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let EventEmitter = require('events');
|
let EventEmitter = require('events');
|
||||||
|
let helper = require('./helper');
|
||||||
|
|
||||||
class FrameManager extends EventEmitter {
|
class FrameManager extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
@ -37,9 +38,14 @@ class FrameManager extends EventEmitter {
|
|||||||
this._frames = new Map();
|
this._frames = new Map();
|
||||||
this._mainFrame = this._addFramesRecursively(null, frameTree);
|
this._mainFrame = this._addFramesRecursively(null, frameTree);
|
||||||
|
|
||||||
this._client.on('Page.frameAttached', event => this._frameAttached(event.frameId, event.parentFrameId));
|
/** @type {!Map<string, string>} */
|
||||||
this._client.on('Page.frameNavigated', event => this._frameNavigated(event.frame));
|
this._frameIdToExecutionContextId = new Map();
|
||||||
this._client.on('Page.frameDetached', event => this._frameDetached(event.frameId));
|
|
||||||
|
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
|
||||||
|
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
|
||||||
|
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
|
||||||
|
this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
|
||||||
|
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +67,7 @@ class FrameManager extends EventEmitter {
|
|||||||
* @param {?string} parentFrameId
|
* @param {?string} parentFrameId
|
||||||
* @return {?Frame}
|
* @return {?Frame}
|
||||||
*/
|
*/
|
||||||
_frameAttached(frameId, parentFrameId) {
|
_onFrameAttached(frameId, parentFrameId) {
|
||||||
if (this._frames.has(frameId))
|
if (this._frames.has(frameId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -71,7 +77,7 @@ class FrameManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let parentFrame = this._frames.get(parentFrameId);
|
let parentFrame = this._frames.get(parentFrameId);
|
||||||
let frame = new Frame(parentFrame, frameId, null);
|
let frame = new Frame(this, parentFrame, frameId, null);
|
||||||
this._frames.set(frame._id, frame);
|
this._frames.set(frame._id, frame);
|
||||||
this.emit(FrameManager.Events.FrameAttached, frame);
|
this.emit(FrameManager.Events.FrameAttached, frame);
|
||||||
}
|
}
|
||||||
@ -79,7 +85,7 @@ class FrameManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @param {!Object} framePayload
|
* @param {!Object} framePayload
|
||||||
*/
|
*/
|
||||||
_frameNavigated(framePayload) {
|
_onFrameNavigated(framePayload) {
|
||||||
let frame = this._frames.get(framePayload.id);
|
let frame = this._frames.get(framePayload.id);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
// Simulate missed "frameAttached" for a main frame navigation to the new backend process.
|
// Simulate missed "frameAttached" for a main frame navigation to the new backend process.
|
||||||
@ -92,12 +98,21 @@ class FrameManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @param {string} frameId
|
* @param {string} frameId
|
||||||
*/
|
*/
|
||||||
_frameDetached(frameId) {
|
_onFrameDetached(frameId) {
|
||||||
let frame = this._frames.get(frameId);
|
let frame = this._frames.get(frameId);
|
||||||
if (frame)
|
if (frame)
|
||||||
this._removeFramesRecursively(frame);
|
this._removeFramesRecursively(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onExecutionContextCreated(context) {
|
||||||
|
if (context.auxData && context.auxData.isDefault && context.auxData.frameId)
|
||||||
|
this._frameIdToExecutionContextId.set(context.auxData.frameId, context.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onExecutionContextDestroyed(executionContextId) {
|
||||||
|
this._frameIdToExecutionContextId.delete(executionContextId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {!Frame} frame
|
* @param {!Frame} frame
|
||||||
* @param {string} newFrameId
|
* @param {string} newFrameId
|
||||||
@ -121,7 +136,7 @@ class FrameManager extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
_addFramesRecursively(parentFrame, frameTreePayload) {
|
_addFramesRecursively(parentFrame, frameTreePayload) {
|
||||||
let framePayload = frameTreePayload.frame;
|
let framePayload = frameTreePayload.frame;
|
||||||
let frame = new Frame(parentFrame, framePayload.id, framePayload);
|
let frame = new Frame(this, parentFrame, framePayload.id, framePayload);
|
||||||
this._frames.set(frame._id, frame);
|
this._frames.set(frame._id, frame);
|
||||||
|
|
||||||
for (let i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i)
|
for (let i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i)
|
||||||
@ -139,6 +154,28 @@ class FrameManager extends EventEmitter {
|
|||||||
this._frames.delete(frame._id);
|
this._frames.delete(frame._id);
|
||||||
this.emit(FrameManager.Events.FrameDetached, frame);
|
this.emit(FrameManager.Events.FrameDetached, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Frame} frame
|
||||||
|
* @param {function()} fun
|
||||||
|
* @param {!Array<*>} args
|
||||||
|
* @return {!Promise<(!Object|undefined)>}
|
||||||
|
*/
|
||||||
|
async _evaluateOnFrame(frame, fun, ...args) {
|
||||||
|
let contextId = undefined;
|
||||||
|
if (!frame.isMainFrame()) {
|
||||||
|
contextId = this._frameIdToExecutionContextId.get(frame._id);
|
||||||
|
console.assert(contextId, 'Frame does not have default context to evaluate in!');
|
||||||
|
}
|
||||||
|
let syncExpression = helper.evaluationString(fun, ...args);
|
||||||
|
let expression = `Promise.resolve(${syncExpression})`;
|
||||||
|
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: true, awaitPromise: true });
|
||||||
|
if (exceptionDetails) {
|
||||||
|
let message = await helper.getExceptionMessage(this._client, exceptionDetails);
|
||||||
|
throw new Error('Evaluation failed: ' + message);
|
||||||
|
}
|
||||||
|
return remoteObject.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
@ -153,11 +190,13 @@ FrameManager.Events = {
|
|||||||
*/
|
*/
|
||||||
class Frame {
|
class Frame {
|
||||||
/**
|
/**
|
||||||
|
* @param {!FrameManager} frameManager
|
||||||
* @param {?Frame} parentFrame
|
* @param {?Frame} parentFrame
|
||||||
* @param {string} frameId
|
* @param {string} frameId
|
||||||
* @param {?Object} payload
|
* @param {?Object} payload
|
||||||
*/
|
*/
|
||||||
constructor(parentFrame, frameId, payload) {
|
constructor(frameManager, parentFrame, frameId, payload) {
|
||||||
|
this._frameManager = frameManager;
|
||||||
this._parentFrame = parentFrame;
|
this._parentFrame = parentFrame;
|
||||||
this._url = '';
|
this._url = '';
|
||||||
this._id = frameId;
|
this._id = frameId;
|
||||||
@ -169,6 +208,15 @@ class Frame {
|
|||||||
this._parentFrame._childFrames.add(this);
|
this._parentFrame._childFrames.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {function()} fun
|
||||||
|
* @param {!Array<*>} args
|
||||||
|
* @return {!Promise<(!Object|undefined)>}
|
||||||
|
*/
|
||||||
|
async evaluate(fun, ...args) {
|
||||||
|
return this._frameManager._evaluateOnFrame(this, fun, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
57
lib/Page.js
57
lib/Page.js
@ -21,6 +21,7 @@ let Request = require('./Request');
|
|||||||
let Navigator = require('./Navigator');
|
let Navigator = require('./Navigator');
|
||||||
let Dialog = require('./Dialog');
|
let Dialog = require('./Dialog');
|
||||||
let FrameManager = require('./FrameManager');
|
let FrameManager = require('./FrameManager');
|
||||||
|
let helper = require('./helper');
|
||||||
|
|
||||||
class Page extends EventEmitter {
|
class Page extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
@ -34,7 +35,7 @@ class Page extends EventEmitter {
|
|||||||
client.send('Runtime.enable', {}),
|
client.send('Runtime.enable', {}),
|
||||||
client.send('Security.enable', {}),
|
client.send('Security.enable', {}),
|
||||||
]);
|
]);
|
||||||
let expression = Page._evaluationString(() => window.devicePixelRatio);
|
let expression = helper.evaluationString(() => window.devicePixelRatio);
|
||||||
let {result:{value: screenDPI}} = await client.send('Runtime.evaluate', { expression, returnByValue: true });
|
let {result:{value: screenDPI}} = await client.send('Runtime.evaluate', { expression, returnByValue: true });
|
||||||
let frameManager = await FrameManager.create(client);
|
let frameManager = await FrameManager.create(client);
|
||||||
let page = new Page(client, frameManager, screenDPI);
|
let page = new Page(client, frameManager, screenDPI);
|
||||||
@ -144,7 +145,7 @@ class Page extends EventEmitter {
|
|||||||
throw new Error(`Failed to set in-page callback with name ${name}: window['${name}'] already exists!`);
|
throw new Error(`Failed to set in-page callback with name ${name}: window['${name}'] already exists!`);
|
||||||
this._inPageCallbacks[name] = callback;
|
this._inPageCallbacks[name] = callback;
|
||||||
|
|
||||||
let expression = Page._evaluationString(inPageCallback, name);
|
let expression = helper.evaluationString(inPageCallback, name);
|
||||||
await this._client.send('Page.addScriptToEvaluateOnLoad', { scriptSource: expression });
|
await this._client.send('Page.addScriptToEvaluateOnLoad', { scriptSource: expression });
|
||||||
await this._client.send('Runtime.evaluate', { expression, returnByValue: true });
|
await this._client.send('Runtime.evaluate', { expression, returnByValue: true });
|
||||||
|
|
||||||
@ -205,7 +206,7 @@ class Page extends EventEmitter {
|
|||||||
* @param {!Object} exceptionDetails
|
* @param {!Object} exceptionDetails
|
||||||
*/
|
*/
|
||||||
async _handleException(exceptionDetails) {
|
async _handleException(exceptionDetails) {
|
||||||
let message = await this._getExceptionMessage(exceptionDetails);
|
let message = await helper.getExceptionMessage(this._client, exceptionDetails);
|
||||||
this.emit(Page.Events.Error, new Error(message));
|
this.emit(Page.Events.Error, new Error(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +214,7 @@ class Page extends EventEmitter {
|
|||||||
if (event.type === 'debug' && event.args.length && event.args[0].value === 'driver:InPageCallback') {
|
if (event.type === 'debug' && event.args.length && event.args[0].value === 'driver:InPageCallback') {
|
||||||
let {name, seq, args} = JSON.parse(event.args[1].value);
|
let {name, seq, args} = JSON.parse(event.args[1].value);
|
||||||
let result = await this._inPageCallbacks[name](...args);
|
let result = await this._inPageCallbacks[name](...args);
|
||||||
let expression = Page._evaluationString(deliverResult, name, seq, result);
|
let expression = helper.evaluationString(deliverResult, name, seq, result);
|
||||||
this._client.send('Runtime.evaluate', { expression });
|
this._client.send('Runtime.evaluate', { expression });
|
||||||
|
|
||||||
function deliverResult(name, seq, result) {
|
function deliverResult(name, seq, result) {
|
||||||
@ -307,42 +308,7 @@ class Page extends EventEmitter {
|
|||||||
* @return {!Promise<(!Object|undefined)>}
|
* @return {!Promise<(!Object|undefined)>}
|
||||||
*/
|
*/
|
||||||
async evaluate(fun, ...args) {
|
async evaluate(fun, ...args) {
|
||||||
let syncExpression = Page._evaluationString(fun, ...args);
|
return this._frameManager.mainFrame().evaluate(fun, ...args);
|
||||||
let expression = `Promise.resolve(${syncExpression})`;
|
|
||||||
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
|
||||||
if (exceptionDetails) {
|
|
||||||
let message = await this._getExceptionMessage(exceptionDetails);
|
|
||||||
throw new Error('Evaluation failed: ' + message);
|
|
||||||
}
|
|
||||||
return remoteObject.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {!Object} exceptionDetails
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
async _getExceptionMessage(exceptionDetails) {
|
|
||||||
let message = '';
|
|
||||||
let exception = exceptionDetails.exception;
|
|
||||||
if (exception) {
|
|
||||||
let response = await this._client.send('Runtime.callFunctionOn', {
|
|
||||||
objectId: exception.objectId,
|
|
||||||
functionDeclaration: 'function() { return this.message; }',
|
|
||||||
returnByValue: true,
|
|
||||||
});
|
|
||||||
message = response.result.value;
|
|
||||||
} else {
|
|
||||||
message = exceptionDetails.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exceptionDetails.stackTrace) {
|
|
||||||
for (let callframe of exceptionDetails.stackTrace.callFrames) {
|
|
||||||
let location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
|
|
||||||
let functionName = callframe.functionName || '<anonymous>';
|
|
||||||
message += `\n at ${functionName} (${location})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -351,19 +317,10 @@ class Page extends EventEmitter {
|
|||||||
* @return {!Promise}
|
* @return {!Promise}
|
||||||
*/
|
*/
|
||||||
async evaluateOnInitialized(fun, ...args) {
|
async evaluateOnInitialized(fun, ...args) {
|
||||||
let scriptSource = Page._evaluationString(fun, ...args);
|
let scriptSource = helper.evaluationString(fun, ...args);
|
||||||
await this._client.send('Page.addScriptToEvaluateOnLoad', { scriptSource });
|
await this._client.send('Page.addScriptToEvaluateOnLoad', { scriptSource });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {function()} fun
|
|
||||||
* @param {!Array<*>} args
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
static _evaluationString(fun, ...args) {
|
|
||||||
return `(${fun})(${args.map(x => JSON.stringify(x)).join(',')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {!Object=} options
|
* @param {!Object=} options
|
||||||
* @return {!Promise<!Buffer>}
|
* @return {!Promise<!Buffer>}
|
||||||
|
57
lib/helper.js
Normal file
57
lib/helper.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Helper {
|
||||||
|
/**
|
||||||
|
* @param {function()} fun
|
||||||
|
* @param {!Array<*>} args
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static evaluationString(fun, ...args) {
|
||||||
|
return `(${fun})(${args.map(x => JSON.stringify(x)).join(',')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Connection} client
|
||||||
|
* @param {!Object} exceptionDetails
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static async getExceptionMessage(client, exceptionDetails) {
|
||||||
|
let message = '';
|
||||||
|
let exception = exceptionDetails.exception;
|
||||||
|
if (exception) {
|
||||||
|
let response = await client.send('Runtime.callFunctionOn', {
|
||||||
|
objectId: exception.objectId,
|
||||||
|
functionDeclaration: 'function() { return this.message; }',
|
||||||
|
returnByValue: true,
|
||||||
|
});
|
||||||
|
message = response.result.value;
|
||||||
|
} else {
|
||||||
|
message = exceptionDetails.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exceptionDetails.stackTrace) {
|
||||||
|
for (let callframe of exceptionDetails.stackTrace.callFrames) {
|
||||||
|
let location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber;
|
||||||
|
let functionName = callframe.functionName || '<anonymous>';
|
||||||
|
message += `\n at ${functionName} (${location})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Helper;
|
15
test/test.js
15
test/test.js
@ -81,6 +81,21 @@ describe('Puppeteer', function() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Frame.evaluate', function() {
|
||||||
|
let FrameUtils = require('./frame-utils');
|
||||||
|
it('should have different execution contexts', SX(async function() {
|
||||||
|
await page.navigate(EMPTY_PAGE);
|
||||||
|
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
|
||||||
|
expect(page.frames().length).toBe(2);
|
||||||
|
let frame1 = page.frames()[0];
|
||||||
|
let frame2 = page.frames()[1];
|
||||||
|
await frame1.evaluate(() => window.FOO = 'foo');
|
||||||
|
await frame2.evaluate(() => window.FOO = 'bar');
|
||||||
|
expect(await frame1.evaluate(() => window.FOO)).toBe('foo');
|
||||||
|
expect(await frame2.evaluate(() => window.FOO)).toBe('bar');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
it('Page Events: ConsoleMessage', SX(async function() {
|
it('Page Events: ConsoleMessage', SX(async function() {
|
||||||
let msgs = [];
|
let msgs = [];
|
||||||
page.on('consolemessage', msg => msgs.push(msg));
|
page.on('consolemessage', msg => msgs.push(msg));
|
||||||
|
Loading…
Reference in New Issue
Block a user