2017-06-17 21:27:51 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2017-06-22 20:38:10 +00:00
|
|
|
let EventEmitter = require('events');
|
2017-06-27 19:40:46 +00:00
|
|
|
let helper = require('./helper');
|
2017-06-17 21:27:51 +00:00
|
|
|
|
|
|
|
class FrameManager extends EventEmitter {
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {!Connection} client
|
2017-06-29 06:09:28 +00:00
|
|
|
* @return {!Promise<!FrameManager>}
|
2017-06-21 20:58:49 +00:00
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
static async create(client) {
|
2017-06-22 20:38:10 +00:00
|
|
|
let mainFramePayload = await client.send('Page.getResourceTree');
|
2017-06-21 20:51:06 +00:00
|
|
|
return new FrameManager(client, mainFramePayload.frameTree);
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {!Connection} client
|
|
|
|
* @param {!Object} frameTree
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
constructor(client, frameTree) {
|
|
|
|
super();
|
|
|
|
this._client = client;
|
|
|
|
/** @type {!Map<string, !Frame>} */
|
|
|
|
this._frames = new Map();
|
|
|
|
this._mainFrame = this._addFramesRecursively(null, frameTree);
|
|
|
|
|
2017-06-27 19:40:46 +00:00
|
|
|
/** @type {!Map<string, string>} */
|
|
|
|
this._frameIdToExecutionContextId = new Map();
|
|
|
|
|
|
|
|
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));
|
2017-06-21 20:51:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {!Frame}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
mainFrame() {
|
|
|
|
return this._mainFrame;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {!Array<!Frame>}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
frames() {
|
|
|
|
return Array.from(this._frames.values());
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {string} frameId
|
|
|
|
* @param {?string} parentFrameId
|
|
|
|
* @return {?Frame}
|
|
|
|
*/
|
2017-06-27 19:40:46 +00:00
|
|
|
_onFrameAttached(frameId, parentFrameId) {
|
2017-06-21 20:51:06 +00:00
|
|
|
if (this._frames.has(frameId))
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!parentFrameId) {
|
|
|
|
// Navigation to the new backend process.
|
|
|
|
this._navigateFrame(this._mainFrame, frameId, null);
|
|
|
|
return;
|
|
|
|
}
|
2017-06-22 20:38:10 +00:00
|
|
|
let parentFrame = this._frames.get(parentFrameId);
|
2017-06-27 19:40:46 +00:00
|
|
|
let frame = new Frame(this, parentFrame, frameId, null);
|
2017-06-21 20:51:06 +00:00
|
|
|
this._frames.set(frame._id, frame);
|
|
|
|
this.emit(FrameManager.Events.FrameAttached, frame);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {!Object} framePayload
|
|
|
|
*/
|
2017-06-27 19:40:46 +00:00
|
|
|
_onFrameNavigated(framePayload) {
|
2017-06-22 20:38:10 +00:00
|
|
|
let frame = this._frames.get(framePayload.id);
|
2017-06-21 20:51:06 +00:00
|
|
|
if (!frame) {
|
|
|
|
// Simulate missed "frameAttached" for a main frame navigation to the new backend process.
|
|
|
|
console.assert(!framePayload.parentId, 'Main frame shouldn\'t have parent frame id.');
|
|
|
|
frame = this._mainFrame;
|
2017-06-21 20:36:04 +00:00
|
|
|
}
|
2017-06-21 20:51:06 +00:00
|
|
|
this._navigateFrame(frame, framePayload.id, framePayload);
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {string} frameId
|
|
|
|
*/
|
2017-06-27 19:40:46 +00:00
|
|
|
_onFrameDetached(frameId) {
|
2017-06-22 20:38:10 +00:00
|
|
|
let frame = this._frames.get(frameId);
|
2017-06-21 20:51:06 +00:00
|
|
|
if (frame)
|
|
|
|
this._removeFramesRecursively(frame);
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-27 19:40:46 +00:00
|
|
|
_onExecutionContextCreated(context) {
|
|
|
|
if (context.auxData && context.auxData.isDefault && context.auxData.frameId)
|
|
|
|
this._frameIdToExecutionContextId.set(context.auxData.frameId, context.id);
|
|
|
|
}
|
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {!Frame} frame
|
|
|
|
* @param {string} newFrameId
|
|
|
|
* @param {?Object} newFramePayload
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
_navigateFrame(frame, newFrameId, newFramePayload) {
|
|
|
|
// Detach all child frames first.
|
2017-06-22 20:38:10 +00:00
|
|
|
for (let child of frame.childFrames())
|
2017-06-21 20:51:06 +00:00
|
|
|
this._removeFramesRecursively(child);
|
|
|
|
this._frames.delete(frame._id, frame);
|
2017-07-12 04:52:09 +00:00
|
|
|
this._frameIdToExecutionContextId.delete(frame._id);
|
2017-06-21 20:51:06 +00:00
|
|
|
frame._id = newFrameId;
|
|
|
|
frame._adoptPayload(newFramePayload);
|
|
|
|
this._frames.set(newFrameId, frame);
|
|
|
|
this.emit(FrameManager.Events.FrameNavigated, frame);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {?Frame} parentFrame
|
|
|
|
* @param {!Object} frameTreePayload
|
|
|
|
* @return {!Frame}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
_addFramesRecursively(parentFrame, frameTreePayload) {
|
2017-06-22 20:38:10 +00:00
|
|
|
let framePayload = frameTreePayload.frame;
|
2017-06-27 19:40:46 +00:00
|
|
|
let frame = new Frame(this, parentFrame, framePayload.id, framePayload);
|
2017-06-21 20:51:06 +00:00
|
|
|
this._frames.set(frame._id, frame);
|
|
|
|
|
2017-06-22 20:38:10 +00:00
|
|
|
for (let i = 0; frameTreePayload.childFrames && i < frameTreePayload.childFrames.length; ++i)
|
2017-06-21 20:51:06 +00:00
|
|
|
this._addFramesRecursively(frame, frameTreePayload.childFrames[i]);
|
|
|
|
return frame;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {!Frame} frame
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
_removeFramesRecursively(frame) {
|
2017-06-22 20:38:10 +00:00
|
|
|
for (let child of frame.childFrames())
|
2017-06-21 20:51:06 +00:00
|
|
|
this._removeFramesRecursively(child);
|
|
|
|
frame._detach();
|
|
|
|
this._frames.delete(frame._id);
|
2017-07-12 04:52:09 +00:00
|
|
|
this._frameIdToExecutionContextId.delete(frame._id);
|
2017-06-21 20:51:06 +00:00
|
|
|
this.emit(FrameManager.Events.FrameDetached, frame);
|
|
|
|
}
|
2017-06-27 19:40:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {!Frame} frame
|
2017-07-18 01:56:56 +00:00
|
|
|
* @param {string} expression
|
2017-06-27 19:40:46 +00:00
|
|
|
* @return {!Promise<(!Object|undefined)>}
|
|
|
|
*/
|
2017-07-18 01:56:56 +00:00
|
|
|
async _evaluateOnFrame(frame, expression) {
|
2017-06-27 19:40:46 +00:00
|
|
|
let contextId = undefined;
|
|
|
|
if (!frame.isMainFrame()) {
|
|
|
|
contextId = this._frameIdToExecutionContextId.get(frame._id);
|
|
|
|
console.assert(contextId, 'Frame does not have default context to evaluate in!');
|
|
|
|
}
|
2017-07-18 01:56:56 +00:00
|
|
|
expression = `Promise.resolve(${expression})`;
|
2017-07-06 17:41:01 +00:00
|
|
|
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true });
|
2017-06-27 19:40:46 +00:00
|
|
|
if (exceptionDetails) {
|
|
|
|
let message = await helper.getExceptionMessage(this._client, exceptionDetails);
|
|
|
|
throw new Error('Evaluation failed: ' + message);
|
|
|
|
}
|
2017-07-17 08:57:37 +00:00
|
|
|
return await helper.serializeRemoteObject(this._client, remoteObject);
|
2017-06-27 19:40:46 +00:00
|
|
|
}
|
2017-07-07 22:39:02 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {!Frame} frame
|
|
|
|
* @return {!Promise<undefined>}
|
|
|
|
*/
|
|
|
|
async _waitForInFrame(selector, frame) {
|
2017-07-18 19:16:19 +00:00
|
|
|
|
|
|
|
function code(selector) {
|
|
|
|
if (document.querySelector(selector))
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
|
|
let callback;
|
|
|
|
const result = new Promise(fulfill => callback = fulfill);
|
|
|
|
|
|
|
|
const mo = new MutationObserver((mutations, observer) => {
|
2017-07-18 09:10:02 +00:00
|
|
|
if (document.querySelector(selector)) {
|
2017-07-18 19:16:19 +00:00
|
|
|
observer.disconnect();
|
|
|
|
callback();
|
2017-07-18 09:10:02 +00:00
|
|
|
return;
|
2017-07-07 22:39:02 +00:00
|
|
|
}
|
|
|
|
});
|
2017-07-18 19:16:19 +00:00
|
|
|
mo.observe(document.documentElement, {
|
|
|
|
childList: true,
|
|
|
|
subtree: true
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-07-14 22:55:38 +00:00
|
|
|
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 { exceptionDetails } = await this._client.send('Runtime.evaluate', {
|
2017-07-07 22:39:02 +00:00
|
|
|
expression: helper.evaluationString(code, selector),
|
2017-07-14 22:55:38 +00:00
|
|
|
contextId,
|
2017-07-07 22:39:02 +00:00
|
|
|
awaitPromise: true,
|
2017-07-14 22:55:38 +00:00
|
|
|
returnByValue: false,
|
2017-07-07 22:39:02 +00:00
|
|
|
});
|
2017-07-14 22:55:38 +00:00
|
|
|
if (exceptionDetails) {
|
|
|
|
let message = await helper.getExceptionMessage(this._client, exceptionDetails);
|
|
|
|
throw new Error('Evaluation failed: ' + message);
|
|
|
|
}
|
2017-07-07 22:39:02 +00:00
|
|
|
}
|
2017-06-17 21:27:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @enum {string} */
|
|
|
|
FrameManager.Events = {
|
2017-06-21 20:51:06 +00:00
|
|
|
FrameAttached: 'frameattached',
|
|
|
|
FrameNavigated: 'framenavigated',
|
|
|
|
FrameDetached: 'framedetached'
|
2017-06-17 21:27:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @unrestricted
|
|
|
|
*/
|
|
|
|
class Frame {
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-27 19:40:46 +00:00
|
|
|
* @param {!FrameManager} frameManager
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {?Frame} parentFrame
|
|
|
|
* @param {string} frameId
|
|
|
|
* @param {?Object} payload
|
|
|
|
*/
|
2017-06-27 19:40:46 +00:00
|
|
|
constructor(frameManager, parentFrame, frameId, payload) {
|
|
|
|
this._frameManager = frameManager;
|
2017-06-21 20:51:06 +00:00
|
|
|
this._parentFrame = parentFrame;
|
|
|
|
this._url = '';
|
|
|
|
this._id = frameId;
|
|
|
|
this._adoptPayload(payload);
|
|
|
|
|
|
|
|
/** @type {!Set<!Frame>} */
|
|
|
|
this._childFrames = new Set();
|
|
|
|
if (this._parentFrame)
|
|
|
|
this._parentFrame._childFrames.add(this);
|
|
|
|
}
|
|
|
|
|
2017-06-27 19:40:46 +00:00
|
|
|
/**
|
2017-07-14 20:51:22 +00:00
|
|
|
* @param {function()} pageFunction
|
2017-06-27 19:40:46 +00:00
|
|
|
* @param {!Array<*>} args
|
|
|
|
* @return {!Promise<(!Object|undefined)>}
|
|
|
|
*/
|
2017-07-14 20:51:22 +00:00
|
|
|
async evaluate(pageFunction, ...args) {
|
2017-07-18 01:56:56 +00:00
|
|
|
return this._frameManager._evaluateOnFrame(this, helper.evaluationString(pageFunction, ...args));
|
2017-06-27 19:40:46 +00:00
|
|
|
}
|
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {string}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
name() {
|
|
|
|
return this._name || '';
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {string}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
url() {
|
|
|
|
return this._url;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {?Frame}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
parentFrame() {
|
|
|
|
return this._parentFrame;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {!Array.<!Frame>}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
childFrames() {
|
|
|
|
return Array.from(this._childFrames);
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {boolean}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
isMainFrame() {
|
|
|
|
return !this._detached && !this._parentFrame;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @return {boolean}
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
isDetached() {
|
|
|
|
return this._detached;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-07-07 22:39:02 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @return {!Promise<undefined>}
|
|
|
|
*/
|
|
|
|
async waitFor(selector) {
|
|
|
|
await this._frameManager._waitForInFrame(selector, this);
|
|
|
|
}
|
|
|
|
|
2017-07-18 01:56:56 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {function(!Element):T} pageFunction
|
|
|
|
* @param {!Array<*>} args
|
|
|
|
* @return {!Promise<?T>}
|
|
|
|
*/
|
|
|
|
async $(selector, pageFunction, ...args) {
|
|
|
|
let argsString = ['node'].concat(args.map(x => JSON.stringify(x))).join(',');
|
|
|
|
let expression = `(()=>{
|
|
|
|
let node = document.querySelector(${JSON.stringify(selector)});
|
|
|
|
if (!node)
|
|
|
|
return null;
|
|
|
|
return (${pageFunction})(${argsString});
|
|
|
|
})()`;
|
|
|
|
return this._frameManager._evaluateOnFrame(this, expression);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {function(!Element):T} pageFunction
|
|
|
|
* @param {!Array<*>} args
|
|
|
|
* @return {!Promise<!Array<T>>}
|
|
|
|
*/
|
|
|
|
async $$(selector, pageFunction, ...args) {
|
|
|
|
let argsString = ['node, index'].concat(args.map(x => JSON.stringify(x))).join(',');
|
|
|
|
let expression = `(()=>{
|
|
|
|
let nodes = document.querySelectorAll(${JSON.stringify(selector)});
|
|
|
|
return Array.prototype.map.call(nodes, (node, index) => (${pageFunction})(${argsString}));
|
|
|
|
})()`;
|
|
|
|
return this._frameManager._evaluateOnFrame(this, expression);
|
|
|
|
}
|
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {?Object} framePayload
|
|
|
|
*/
|
2017-06-21 20:51:06 +00:00
|
|
|
_adoptPayload(framePayload) {
|
|
|
|
framePayload = framePayload || {
|
|
|
|
name: '',
|
|
|
|
url: '',
|
|
|
|
};
|
|
|
|
this._name = framePayload.name;
|
|
|
|
this._url = framePayload.url;
|
|
|
|
}
|
|
|
|
|
|
|
|
_detach() {
|
|
|
|
this._detached = true;
|
|
|
|
if (this._parentFrame)
|
|
|
|
this._parentFrame._childFrames.delete(this);
|
|
|
|
this._parentFrame = null;
|
|
|
|
}
|
2017-06-17 21:27:51 +00:00
|
|
|
}
|
2017-07-19 03:53:00 +00:00
|
|
|
helper.tracePublicAPI(Frame);
|
2017-06-17 21:27:51 +00:00
|
|
|
|
|
|
|
module.exports = FrameManager;
|