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-08-02 19:06:47 +00:00
|
|
|
const fs = require('fs');
|
|
|
|
const path = require('path');
|
|
|
|
const EventEmitter = require('events');
|
|
|
|
const 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
|
|
|
|
* @param {!Object} frameTree
|
2017-07-22 03:29:31 +00:00
|
|
|
* @param {!Mouse} mouse
|
2017-06-21 20:58:49 +00:00
|
|
|
*/
|
2017-07-28 23:52:38 +00:00
|
|
|
constructor(client, mouse) {
|
2017-06-21 20:51:06 +00:00
|
|
|
super();
|
|
|
|
this._client = client;
|
2017-07-22 03:29:31 +00:00
|
|
|
this._mouse = mouse;
|
2017-06-21 20:51:06 +00:00
|
|
|
/** @type {!Map<string, !Frame>} */
|
|
|
|
this._frames = new Map();
|
|
|
|
|
2017-06-27 19:40:46 +00:00
|
|
|
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;
|
2017-07-25 21:30:04 +00:00
|
|
|
console.assert(parentFrameId);
|
2017-06-22 20:38:10 +00:00
|
|
|
let parentFrame = this._frames.get(parentFrameId);
|
2017-07-25 21:58:10 +00:00
|
|
|
let frame = new Frame(this._client, this._mouse, parentFrame, frameId);
|
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-07-28 23:52:38 +00:00
|
|
|
const isMainFrame = !framePayload.parentId;
|
|
|
|
let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id);
|
|
|
|
console.assert(isMainFrame || frame, 'We either navigate top level or have old version of the navigated frame');
|
|
|
|
|
|
|
|
// Detach all child frames first.
|
|
|
|
if (frame) {
|
|
|
|
for (let child of frame.childFrames())
|
|
|
|
this._removeFramesRecursively(child);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update or create main frame.
|
|
|
|
if (isMainFrame) {
|
|
|
|
if (frame) {
|
|
|
|
// Update frame id to retain frame identity on cross-process navigation.
|
|
|
|
this._frames.delete(frame._id, frame);
|
|
|
|
frame._id = framePayload.id;
|
|
|
|
} else {
|
|
|
|
// Initial main frame navigation.
|
|
|
|
frame = new Frame(this._client, this._mouse, null, framePayload.id);
|
|
|
|
}
|
|
|
|
this._frames.set(framePayload.id, frame);
|
|
|
|
this._mainFrame = frame;
|
2017-06-21 20:36:04 +00:00
|
|
|
}
|
2017-07-28 23:52:38 +00:00
|
|
|
|
|
|
|
// Update frame payload.
|
|
|
|
frame._navigated(framePayload);
|
|
|
|
|
|
|
|
this.emit(FrameManager.Events.FrameNavigated, frame);
|
2017-06-21 20:51:06 +00:00
|
|
|
}
|
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) {
|
2017-07-25 21:30:04 +00:00
|
|
|
const frameId = context.auxData && context.auxData.isDefault ? context.auxData.frameId : null;
|
|
|
|
const frame = this._frames.get(frameId);
|
|
|
|
if (!frame)
|
|
|
|
return;
|
|
|
|
frame._defaultContextId = context.id;
|
2017-07-28 23:52:38 +00:00
|
|
|
for (let waitTask of frame._waitTasks)
|
|
|
|
waitTask.rerun();
|
2017-06-21 20:51:06 +00:00
|
|
|
}
|
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);
|
|
|
|
this.emit(FrameManager.Events.FrameDetached, frame);
|
|
|
|
}
|
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-07-25 21:30:04 +00:00
|
|
|
* @param {!Connection} client
|
|
|
|
* @param {!Mouse} mouse
|
2017-06-21 20:58:49 +00:00
|
|
|
* @param {?Frame} parentFrame
|
|
|
|
* @param {string} frameId
|
|
|
|
*/
|
2017-07-25 21:58:10 +00:00
|
|
|
constructor(client, mouse, parentFrame, frameId) {
|
2017-07-25 21:30:04 +00:00
|
|
|
this._client = client;
|
|
|
|
this._mouse = mouse;
|
2017-06-21 20:51:06 +00:00
|
|
|
this._parentFrame = parentFrame;
|
|
|
|
this._url = '';
|
|
|
|
this._id = frameId;
|
2017-07-25 21:30:04 +00:00
|
|
|
this._defaultContextId = '<not-initialized>';
|
2017-07-24 16:58:51 +00:00
|
|
|
/** @type {!Set<!WaitTask>} */
|
|
|
|
this._waitTasks = new Set();
|
2017-07-20 02:04:51 +00:00
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/** @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-27 19:23:41 +00:00
|
|
|
* @param {function()|string} 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-27 19:23:41 +00:00
|
|
|
let expression = helper.evaluationString(pageFunction, ...args);
|
2017-07-25 21:30:04 +00:00
|
|
|
const contextId = this._defaultContextId;
|
2017-07-27 19:23:41 +00:00
|
|
|
let { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false});
|
2017-07-25 21:30:04 +00:00
|
|
|
if (exceptionDetails)
|
|
|
|
throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails));
|
|
|
|
return await helper.serializeRemoteObject(this._client, remoteObject);
|
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
|
|
|
isDetached() {
|
|
|
|
return this._detached;
|
|
|
|
}
|
2017-06-21 20:36:04 +00:00
|
|
|
|
2017-07-25 18:37:46 +00:00
|
|
|
/**
|
|
|
|
* @param {string} filePath
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async injectFile(filePath) {
|
|
|
|
let contents = await new Promise((resolve, reject) => {
|
|
|
|
fs.readFile(filePath, 'utf8', (err, data) => {
|
|
|
|
if (err) return reject(err);
|
|
|
|
resolve(data);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
contents += `//# sourceURL=` + filePath.replace(/\n/g,'');
|
2017-07-27 19:23:41 +00:00
|
|
|
return this.evaluate(contents);
|
2017-07-25 18:37:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} url
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async addScriptTag(url) {
|
|
|
|
return this.evaluate(addScriptTag, url);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} url
|
|
|
|
*/
|
|
|
|
function addScriptTag(url) {
|
|
|
|
let script = document.createElement('script');
|
|
|
|
script.src = url;
|
|
|
|
let promise = new Promise(x => script.onload = x);
|
|
|
|
document.head.appendChild(script);
|
|
|
|
return promise;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-21 19:41:49 +00:00
|
|
|
/**
|
2017-07-28 00:09:28 +00:00
|
|
|
* @param {(string|number|function())} selectorOrTimeout
|
2017-07-21 19:41:49 +00:00
|
|
|
* @param {!Object=} options
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
2017-07-28 00:09:28 +00:00
|
|
|
waitFor(selectorOrFunctionOrTimeout, options = {}) {
|
|
|
|
if (helper.isString(selectorOrFunctionOrTimeout))
|
|
|
|
return this.waitForSelector(selectorOrFunctionOrTimeout, options);
|
|
|
|
if (helper.isNumber(selectorOrFunctionOrTimeout))
|
|
|
|
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
|
|
|
|
if (typeof selectorOrFunctionOrTimeout === 'function')
|
|
|
|
return this.waitForFunction(selectorOrFunctionOrTimeout, options);
|
|
|
|
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
2017-07-21 19:41:49 +00:00
|
|
|
}
|
|
|
|
|
2017-07-07 22:39:02 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
2017-07-21 07:58:38 +00:00
|
|
|
* @param {!Object=} options
|
2017-07-20 02:04:51 +00:00
|
|
|
* @return {!Promise}
|
2017-07-07 22:39:02 +00:00
|
|
|
*/
|
2017-07-21 19:41:49 +00:00
|
|
|
waitForSelector(selector, options = {}) {
|
2017-07-21 18:46:57 +00:00
|
|
|
const timeout = options.timeout || 30000;
|
2017-07-24 16:58:51 +00:00
|
|
|
const waitForVisible = !!options.visible;
|
2017-07-27 22:17:43 +00:00
|
|
|
const polling = waitForVisible ? 'raf' : 'mutation';
|
|
|
|
return this.waitForFunction(predicate, {timeout, polling}, selector, waitForVisible);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {boolean} waitForVisible
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
function predicate(selector, waitForVisible) {
|
|
|
|
const node = document.querySelector(selector);
|
|
|
|
if (!node)
|
|
|
|
return false;
|
|
|
|
if (!waitForVisible)
|
|
|
|
return true;
|
|
|
|
const style = window.getComputedStyle(node);
|
|
|
|
return style && style.display !== 'none' && style.visibility !== 'hidden';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {function()} pageFunction
|
|
|
|
* @param {!Object=} options
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
waitForFunction(pageFunction, options = {}, ...args) {
|
|
|
|
const timeout = options.timeout || 30000;
|
|
|
|
const polling = options.polling || 'raf';
|
|
|
|
const predicateCode = 'return ' + helper.evaluationString(pageFunction, ...args);
|
|
|
|
return new WaitTask(this, predicateCode, polling, timeout).promise;
|
2017-07-07 22:39:02 +00:00
|
|
|
}
|
|
|
|
|
2017-07-28 07:06:57 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {!Array<string>} filePaths
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async uploadFile(selector, ...filePaths) {
|
|
|
|
let expression = helper.evaluationString(selector => document.querySelector(selector), selector);
|
|
|
|
const {result} = await this._client.send('Runtime.evaluate', { expression });
|
|
|
|
if (!result)
|
|
|
|
return;
|
|
|
|
const objectId = result.objectId;
|
|
|
|
filePaths = filePaths.map(filePath => path.resolve(filePath));
|
|
|
|
return this._client.send('DOM.setFileInputFiles', { objectId, files: filePaths });
|
|
|
|
}
|
|
|
|
|
2017-07-25 18:37:46 +00:00
|
|
|
/**
|
|
|
|
* @return {!Promise<string>}
|
|
|
|
*/
|
|
|
|
async title() {
|
|
|
|
return this.evaluate(() => document.title);
|
|
|
|
}
|
|
|
|
|
2017-07-22 03:29:31 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
2017-07-25 23:05:23 +00:00
|
|
|
* @return {!Promise<{x: number, y: number}>}
|
2017-07-22 03:29:31 +00:00
|
|
|
*/
|
2017-07-25 23:05:23 +00:00
|
|
|
async _centerOfElement(selector) {
|
2017-07-22 03:29:31 +00:00
|
|
|
let center = await this.evaluate(selector => {
|
|
|
|
let element = document.querySelector(selector);
|
|
|
|
if (!element)
|
|
|
|
return null;
|
|
|
|
element.scrollIntoViewIfNeeded();
|
|
|
|
let rect = element.getBoundingClientRect();
|
|
|
|
return {
|
2017-07-24 23:14:32 +00:00
|
|
|
x: (Math.max(rect.left, 0) + Math.min(rect.right, window.innerWidth)) / 2,
|
|
|
|
y: (Math.max(rect.top, 0) + Math.min(rect.bottom, window.innerHeight)) / 2
|
2017-07-22 03:29:31 +00:00
|
|
|
};
|
|
|
|
}, selector);
|
|
|
|
if (!center)
|
|
|
|
throw new Error('No node found for selector: ' + selector);
|
2017-07-25 23:05:23 +00:00
|
|
|
return center;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async hover(selector) {
|
|
|
|
let {x, y} = await this._centerOfElement(selector);
|
|
|
|
await this._mouse.move(x, y);
|
2017-07-22 03:29:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @param {!Object=} options
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async click(selector, options) {
|
2017-07-25 23:05:23 +00:00
|
|
|
let {x, y} = await this._centerOfElement(selector);
|
|
|
|
await this._mouse.click(x, y, options);
|
2017-07-22 03:29:31 +00:00
|
|
|
}
|
|
|
|
|
2017-07-25 18:37:46 +00:00
|
|
|
/**
|
|
|
|
* @param {string} selector
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
async focus(selector) {
|
|
|
|
let success = await this.evaluate(selector => {
|
|
|
|
let node = document.querySelector(selector);
|
|
|
|
if (!node)
|
|
|
|
return false;
|
|
|
|
node.focus();
|
|
|
|
return true;
|
|
|
|
}, selector);
|
|
|
|
if (!success)
|
|
|
|
throw new Error('No node found for selector: ' + selector);
|
|
|
|
}
|
|
|
|
|
2017-06-21 20:51:06 +00:00
|
|
|
/**
|
2017-07-25 21:58:10 +00:00
|
|
|
* @param {!Object} framePayload
|
2017-06-21 20:58:49 +00:00
|
|
|
*/
|
2017-07-25 21:58:10 +00:00
|
|
|
_navigated(framePayload) {
|
2017-06-21 20:51:06 +00:00
|
|
|
this._name = framePayload.name;
|
|
|
|
this._url = framePayload.url;
|
|
|
|
}
|
|
|
|
|
|
|
|
_detach() {
|
2017-07-24 16:58:51 +00:00
|
|
|
for (let waitTask of this._waitTasks)
|
|
|
|
waitTask.terminate(new Error('waitForSelector failed: frame got detached.'));
|
2017-06-21 20:51:06 +00:00
|
|
|
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
|
|
|
|
2017-07-24 16:58:51 +00:00
|
|
|
class WaitTask {
|
2017-07-20 02:04:51 +00:00
|
|
|
/**
|
2017-07-24 16:58:51 +00:00
|
|
|
* @param {!Frame} frame
|
2017-07-27 22:17:43 +00:00
|
|
|
* @param {string} predicateBody
|
|
|
|
* @param {string} polling
|
2017-07-24 16:58:51 +00:00
|
|
|
* @param {number} timeout
|
2017-07-20 02:04:51 +00:00
|
|
|
*/
|
2017-07-27 22:17:43 +00:00
|
|
|
constructor(frame, predicateBody, polling, timeout) {
|
|
|
|
if (helper.isString(polling))
|
|
|
|
console.assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
|
|
|
else if (helper.isNumber(polling))
|
|
|
|
console.assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
|
|
|
else
|
|
|
|
throw new Error('Unknown polling options: ' + polling);
|
|
|
|
|
2017-07-24 16:58:51 +00:00
|
|
|
this._frame = frame;
|
2017-07-27 22:17:43 +00:00
|
|
|
this._pageScript = helper.evaluationString(waitForPredicatePageFunction, predicateBody, polling, timeout);
|
2017-07-25 22:57:31 +00:00
|
|
|
this._runCount = 0;
|
2017-07-25 21:58:10 +00:00
|
|
|
frame._waitTasks.add(this);
|
2017-07-20 02:04:51 +00:00
|
|
|
this.promise = new Promise((resolve, reject) => {
|
|
|
|
this._resolve = resolve;
|
|
|
|
this._reject = reject;
|
|
|
|
});
|
2017-07-24 16:58:51 +00:00
|
|
|
// Since page navigation requires us to re-install the pageScript, we should track
|
|
|
|
// timeout on our end.
|
|
|
|
this._timeoutTimer = setTimeout(() => this.terminate(new Error(`waiting failed: timeout ${timeout}ms exceeded`)), timeout);
|
2017-07-25 21:58:10 +00:00
|
|
|
this.rerun();
|
2017-07-20 02:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {!Error} error
|
|
|
|
*/
|
|
|
|
terminate(error) {
|
2017-07-25 22:57:31 +00:00
|
|
|
this._terminated = true;
|
2017-07-20 02:04:51 +00:00
|
|
|
this._reject(error);
|
2017-07-24 16:58:51 +00:00
|
|
|
this._cleanup();
|
2017-07-20 02:04:51 +00:00
|
|
|
}
|
|
|
|
|
2017-07-25 22:57:31 +00:00
|
|
|
async rerun() {
|
|
|
|
const runCount = ++this._runCount;
|
|
|
|
let success = false;
|
|
|
|
let error = null;
|
|
|
|
try {
|
2017-07-27 19:23:41 +00:00
|
|
|
success = await this._frame.evaluate(this._pageScript);
|
2017-07-25 22:57:31 +00:00
|
|
|
} catch (e) {
|
|
|
|
error = e;
|
2017-07-24 16:58:51 +00:00
|
|
|
}
|
2017-07-25 22:57:31 +00:00
|
|
|
|
|
|
|
if (this._terminated || runCount !== this._runCount)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
|
|
if (!success && !error)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (error)
|
|
|
|
this._reject(error);
|
|
|
|
else
|
|
|
|
this._resolve();
|
|
|
|
|
|
|
|
this._cleanup();
|
2017-07-24 16:58:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_cleanup() {
|
|
|
|
clearTimeout(this._timeoutTimer);
|
2017-07-25 21:58:10 +00:00
|
|
|
this._frame._waitTasks.delete(this);
|
2017-07-24 16:58:51 +00:00
|
|
|
this._runningTask = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-07-27 22:17:43 +00:00
|
|
|
* @param {string} predicateBody
|
|
|
|
* @param {string} polling
|
2017-07-24 16:58:51 +00:00
|
|
|
* @param {number} timeout
|
|
|
|
* @return {!Promise<boolean>}
|
|
|
|
*/
|
2017-07-27 22:17:43 +00:00
|
|
|
async function waitForPredicatePageFunction(predicateBody, polling, timeout) {
|
|
|
|
const predicate = new Function(predicateBody);
|
2017-07-25 22:57:31 +00:00
|
|
|
let timedOut = false;
|
|
|
|
setTimeout(() => timedOut = true, timeout);
|
2017-07-27 22:17:43 +00:00
|
|
|
if (polling === 'raf')
|
|
|
|
await pollRaf();
|
|
|
|
else if (polling === 'mutation')
|
|
|
|
await pollMutation();
|
|
|
|
else if (typeof polling === 'number')
|
|
|
|
await pollInterval(polling);
|
2017-07-25 22:57:31 +00:00
|
|
|
return !timedOut;
|
2017-07-24 16:58:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return {!Promise<!Element>}
|
|
|
|
*/
|
2017-07-27 22:17:43 +00:00
|
|
|
function pollMutation() {
|
|
|
|
if (predicate())
|
2017-07-25 22:57:31 +00:00
|
|
|
return Promise.resolve();
|
2017-07-24 16:58:51 +00:00
|
|
|
|
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
const observer = new MutationObserver(mutations => {
|
2017-07-27 22:17:43 +00:00
|
|
|
if (timedOut || predicate()) {
|
2017-07-24 16:58:51 +00:00
|
|
|
observer.disconnect();
|
2017-07-25 22:57:31 +00:00
|
|
|
fulfill();
|
2017-07-24 16:58:51 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
observer.observe(document, {
|
|
|
|
childList: true,
|
|
|
|
subtree: true
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-07-27 22:17:43 +00:00
|
|
|
* @return {!Promise}
|
2017-07-24 16:58:51 +00:00
|
|
|
*/
|
2017-07-27 22:17:43 +00:00
|
|
|
function pollRaf() {
|
2017-07-24 16:58:51 +00:00
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
onRaf();
|
|
|
|
return result;
|
|
|
|
|
2017-07-25 22:57:31 +00:00
|
|
|
function onRaf() {
|
2017-07-27 22:17:43 +00:00
|
|
|
if (timedOut || predicate())
|
2017-07-25 22:57:31 +00:00
|
|
|
fulfill();
|
2017-07-27 22:17:43 +00:00
|
|
|
else
|
2017-07-24 16:58:51 +00:00
|
|
|
requestAnimationFrame(onRaf);
|
2017-07-27 22:17:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {number} pollInterval
|
|
|
|
* @return {!Promise}
|
|
|
|
*/
|
|
|
|
function pollInterval(pollInterval) {
|
|
|
|
let fulfill;
|
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
onTimeout();
|
|
|
|
return result;
|
|
|
|
|
|
|
|
function onTimeout() {
|
|
|
|
if (timedOut || predicate())
|
|
|
|
fulfill();
|
|
|
|
else
|
|
|
|
setTimeout(onTimeout, pollInterval);
|
2017-07-20 02:04:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-17 21:27:51 +00:00
|
|
|
module.exports = FrameManager;
|