chore: reuse WaitTask between browsers (#67)
This commit is contained in:
parent
cb72348dfc
commit
cc9b58878b
|
|
@ -17,15 +17,15 @@
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { TimeoutError } from '../Errors';
|
|
||||||
import { ExecutionContext } from './ExecutionContext';
|
import { ExecutionContext } from './ExecutionContext';
|
||||||
import { Frame } from './Frame';
|
import { Frame } from './Frame';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
import { assert, helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
import { ElementHandle, JSHandle } from './JSHandle';
|
import { ElementHandle, JSHandle } from './JSHandle';
|
||||||
import { LifecycleWatcher } from './LifecycleWatcher';
|
import { LifecycleWatcher } from './LifecycleWatcher';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from '../input';
|
import { WaitTask, WaitTaskParams } from '../waitTask';
|
||||||
|
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
||||||
export class DOMWorld {
|
export class DOMWorld {
|
||||||
|
|
@ -36,7 +36,7 @@ export class DOMWorld {
|
||||||
private _contextPromise: Promise<ExecutionContext>;
|
private _contextPromise: Promise<ExecutionContext>;
|
||||||
private _contextResolveCallback: ((c: ExecutionContext) => void) | null;
|
private _contextResolveCallback: ((c: ExecutionContext) => void) | null;
|
||||||
private _context: ExecutionContext | null;
|
private _context: ExecutionContext | null;
|
||||||
_waitTasks = new Set<WaitTask>();
|
_waitTasks = new Set<WaitTask<JSHandle>>();
|
||||||
private _detached = false;
|
private _detached = false;
|
||||||
|
|
||||||
constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) {
|
constructor(frameManager: FrameManager, frame: Frame, timeoutSettings: TimeoutSettings) {
|
||||||
|
|
@ -366,177 +366,3 @@ export class DOMWorld {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WaitTaskParams = {
|
|
||||||
predicateBody: Function | string;
|
|
||||||
title: string;
|
|
||||||
polling: string | number;
|
|
||||||
timeout: number;
|
|
||||||
args: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
class WaitTask {
|
|
||||||
readonly promise: Promise<JSHandle>;
|
|
||||||
private _cleanup: () => void;
|
|
||||||
private _params: WaitTaskParams & { predicateBody: string };
|
|
||||||
private _runCount: number;
|
|
||||||
private _resolve: (result: JSHandle) => void;
|
|
||||||
private _reject: (reason: Error) => void;
|
|
||||||
private _timeoutTimer: NodeJS.Timer;
|
|
||||||
private _terminated: boolean;
|
|
||||||
|
|
||||||
constructor(params: WaitTaskParams, cleanup: () => void) {
|
|
||||||
if (helper.isString(params.polling))
|
|
||||||
assert(params.polling === 'raf' || params.polling === 'mutation', 'Unknown polling option: ' + params.polling);
|
|
||||||
else if (helper.isNumber(params.polling))
|
|
||||||
assert(params.polling > 0, 'Cannot poll with non-positive interval: ' + params.polling);
|
|
||||||
else
|
|
||||||
throw new Error('Unknown polling options: ' + params.polling);
|
|
||||||
|
|
||||||
this._params = {
|
|
||||||
...params,
|
|
||||||
predicateBody: helper.isString(params.predicateBody) ? 'return (' + params.predicateBody + ')' : 'return (' + params.predicateBody + ')(...args)'
|
|
||||||
};
|
|
||||||
this._cleanup = cleanup;
|
|
||||||
this._runCount = 0;
|
|
||||||
this.promise = new Promise<JSHandle>((resolve, reject) => {
|
|
||||||
this._resolve = resolve;
|
|
||||||
this._reject = reject;
|
|
||||||
});
|
|
||||||
// Since page navigation requires us to re-install the pageScript, we should track
|
|
||||||
// timeout on our end.
|
|
||||||
if (params.timeout) {
|
|
||||||
const timeoutError = new TimeoutError(`waiting for ${params.title} failed: timeout ${params.timeout}ms exceeded`);
|
|
||||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), params.timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
terminate(error: Error) {
|
|
||||||
this._terminated = true;
|
|
||||||
this._reject(error);
|
|
||||||
this._doCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerun(context: ExecutionContext) {
|
|
||||||
const runCount = ++this._runCount;
|
|
||||||
let success: JSHandle | null = null;
|
|
||||||
let error = null;
|
|
||||||
try {
|
|
||||||
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._terminated || runCount !== this._runCount) {
|
|
||||||
if (success)
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
||||||
// If execution context has been already destroyed, `context.evaluate` will
|
|
||||||
// throw an error - ignore this predicate run altogether.
|
|
||||||
if (!error && await context.evaluate(s => !s, success).catch(e => true)) {
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the page is navigated, the promise is rejected.
|
|
||||||
// We will try again in the new execution context.
|
|
||||||
if (error && error.message.includes('Execution context was destroyed'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// We could have tried to evaluate in a context which was already
|
|
||||||
// destroyed.
|
|
||||||
if (error && error.message.includes('Cannot find context with specified id'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
this._reject(error);
|
|
||||||
else
|
|
||||||
this._resolve(success);
|
|
||||||
|
|
||||||
this._doCleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
_doCleanup() {
|
|
||||||
clearTimeout(this._timeoutTimer);
|
|
||||||
this._cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPredicatePageFunction(predicateBody: string, polling: string | number, timeout: number, ...args): Promise<any> {
|
|
||||||
const predicate = new Function('...args', predicateBody);
|
|
||||||
let timedOut = false;
|
|
||||||
if (timeout)
|
|
||||||
setTimeout(() => timedOut = true, timeout);
|
|
||||||
if (polling === 'raf')
|
|
||||||
return await pollRaf();
|
|
||||||
if (polling === 'mutation')
|
|
||||||
return await pollMutation();
|
|
||||||
if (typeof polling === 'number')
|
|
||||||
return await pollInterval(polling);
|
|
||||||
|
|
||||||
function pollMutation(): Promise<any> {
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
return Promise.resolve(success);
|
|
||||||
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
const observer = new MutationObserver(mutations => {
|
|
||||||
if (timedOut) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill();
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollRaf(): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onRaf();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onRaf() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(onRaf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollInterval(pollInterval: number): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onTimeout();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onTimeout() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
setTimeout(onTimeout, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {helper, assert} from '../helper';
|
|
||||||
import {TimeoutError} from '../Errors';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import {ElementHandle, JSHandle} from './JSHandle';
|
import {ElementHandle, JSHandle} from './JSHandle';
|
||||||
import { ExecutionContext } from './ExecutionContext';
|
import { ExecutionContext } from './ExecutionContext';
|
||||||
|
import { WaitTaskParams, WaitTask } from '../waitTask';
|
||||||
|
|
||||||
const readFileAsync = util.promisify(fs.readFile);
|
const readFileAsync = util.promisify(fs.readFile);
|
||||||
|
|
||||||
|
|
@ -31,7 +30,8 @@ export class DOMWorld {
|
||||||
_documentPromise: any;
|
_documentPromise: any;
|
||||||
_contextPromise: any;
|
_contextPromise: any;
|
||||||
_contextResolveCallback: any;
|
_contextResolveCallback: any;
|
||||||
_waitTasks: Set<WaitTask>;
|
private _context: ExecutionContext | null;
|
||||||
|
_waitTasks: Set<WaitTask<JSHandle>>;
|
||||||
_detached: boolean;
|
_detached: boolean;
|
||||||
constructor(frame, timeoutSettings) {
|
constructor(frame, timeoutSettings) {
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
|
|
@ -50,12 +50,13 @@ export class DOMWorld {
|
||||||
return this._frame;
|
return this._frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setContext(context) {
|
_setContext(context: ExecutionContext) {
|
||||||
|
this._context = context;
|
||||||
if (context) {
|
if (context) {
|
||||||
this._contextResolveCallback.call(null, context);
|
this._contextResolveCallback.call(null, context);
|
||||||
this._contextResolveCallback = null;
|
this._contextResolveCallback = null;
|
||||||
for (const waitTask of this._waitTasks)
|
for (const waitTask of this._waitTasks)
|
||||||
waitTask.rerun();
|
waitTask.rerun(context);
|
||||||
} else {
|
} else {
|
||||||
this._documentPromise = null;
|
this._documentPromise = null;
|
||||||
this._contextPromise = new Promise(fulfill => {
|
this._contextPromise = new Promise(fulfill => {
|
||||||
|
|
@ -250,7 +251,14 @@ export class DOMWorld {
|
||||||
polling = 'raf',
|
polling = 'raf',
|
||||||
timeout = this._timeoutSettings.timeout(),
|
timeout = this._timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
const params: WaitTaskParams = {
|
||||||
|
predicateBody: pageFunction,
|
||||||
|
title: 'function',
|
||||||
|
polling,
|
||||||
|
timeout,
|
||||||
|
args
|
||||||
|
};
|
||||||
|
return this._scheduleWaitTask(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
|
|
@ -265,8 +273,14 @@ export class DOMWorld {
|
||||||
} = options;
|
} = options;
|
||||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||||
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
||||||
const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
|
const params: WaitTaskParams = {
|
||||||
const handle = await waitTask.promise;
|
predicateBody: predicate,
|
||||||
|
title,
|
||||||
|
polling,
|
||||||
|
timeout,
|
||||||
|
args: [selectorOrXPath, isXPath, waitForVisible, waitForHidden]
|
||||||
|
};
|
||||||
|
const handle = await this._scheduleWaitTask(params);
|
||||||
if (!handle.asElement()) {
|
if (!handle.asElement()) {
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -294,174 +308,12 @@ export class DOMWorld {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class WaitTask {
|
private _scheduleWaitTask(params: WaitTaskParams): Promise<JSHandle> {
|
||||||
promise: any;
|
const task = new WaitTask(params, () => this._waitTasks.delete(task));
|
||||||
_domWorld: any;
|
this._waitTasks.add(task);
|
||||||
_polling: any;
|
if (this._context)
|
||||||
_timeout: any;
|
task.rerun(this._context);
|
||||||
_predicateBody: string;
|
return task.promise;
|
||||||
_args: any[];
|
|
||||||
_runCount: number;
|
|
||||||
_resolve: (value?: unknown) => void;
|
|
||||||
_reject: (reason?: any) => void;
|
|
||||||
_timeoutTimer: NodeJS.Timer;
|
|
||||||
_terminated: boolean;
|
|
||||||
_runningTask: any;
|
|
||||||
constructor(domWorld: DOMWorld, predicateBody: Function | string, title, polling: string | number, timeout: number, ...args: Array<any>) {
|
|
||||||
if (helper.isString(polling))
|
|
||||||
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
|
||||||
else if (helper.isNumber(polling))
|
|
||||||
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
|
||||||
else
|
|
||||||
throw new Error('Unknown polling options: ' + polling);
|
|
||||||
|
|
||||||
this._domWorld = domWorld;
|
|
||||||
this._polling = polling;
|
|
||||||
this._timeout = timeout;
|
|
||||||
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
|
||||||
this._args = args;
|
|
||||||
this._runCount = 0;
|
|
||||||
domWorld._waitTasks.add(this);
|
|
||||||
this.promise = new Promise((resolve, reject) => {
|
|
||||||
this._resolve = resolve;
|
|
||||||
this._reject = reject;
|
|
||||||
});
|
|
||||||
// Since page navigation requires us to re-install the pageScript, we should track
|
|
||||||
// timeout on our end.
|
|
||||||
if (timeout) {
|
|
||||||
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
|
|
||||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
|
||||||
}
|
|
||||||
this.rerun();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminate(error: Error) {
|
|
||||||
this._terminated = true;
|
|
||||||
this._reject(error);
|
|
||||||
this._cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerun() {
|
|
||||||
const runCount = ++this._runCount;
|
|
||||||
let success: JSHandle | null = null;
|
|
||||||
let error = null;
|
|
||||||
try {
|
|
||||||
success = await this._domWorld.evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._terminated || runCount !== this._runCount) {
|
|
||||||
if (success)
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
||||||
// If the frame's execution context has already changed, `frame.evaluate` will
|
|
||||||
// throw an error - ignore this predicate run altogether.
|
|
||||||
if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) {
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the page is navigated, the promise is rejected.
|
|
||||||
// Try again right away.
|
|
||||||
if (error && error.message.includes('Execution context was destroyed')) {
|
|
||||||
this.rerun();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
this._reject(error);
|
|
||||||
else
|
|
||||||
this._resolve(success);
|
|
||||||
|
|
||||||
this._cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanup() {
|
|
||||||
clearTimeout(this._timeoutTimer);
|
|
||||||
this._domWorld._waitTasks.delete(this);
|
|
||||||
this._runningTask = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args): Promise<any> {
|
|
||||||
const predicate = new Function('...args', predicateBody);
|
|
||||||
let timedOut = false;
|
|
||||||
if (timeout)
|
|
||||||
setTimeout(() => timedOut = true, timeout);
|
|
||||||
if (polling === 'raf')
|
|
||||||
return await pollRaf();
|
|
||||||
if (polling === 'mutation')
|
|
||||||
return await pollMutation();
|
|
||||||
if (typeof polling === 'number')
|
|
||||||
return await pollInterval(polling);
|
|
||||||
|
|
||||||
function pollMutation(): Promise<any> {
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
return Promise.resolve(success);
|
|
||||||
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
const observer = new MutationObserver(mutations => {
|
|
||||||
if (timedOut) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill();
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollRaf(): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onRaf();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onRaf() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(onRaf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollInterval(pollInterval: number): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onTimeout();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onTimeout() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
setTimeout(onTimeout, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
182
src/waitTask.ts
Normal file
182
src/waitTask.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
import { assert, helper } from './helper';
|
||||||
|
import * as types from './types';
|
||||||
|
import { TimeoutError } from './Errors';
|
||||||
|
|
||||||
|
export type WaitTaskParams = {
|
||||||
|
// TODO: ensure types.
|
||||||
|
predicateBody: Function | string;
|
||||||
|
title: string;
|
||||||
|
polling: string | number;
|
||||||
|
timeout: number;
|
||||||
|
args: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WaitTask<Handle extends types.Handle> {
|
||||||
|
readonly promise: Promise<Handle>;
|
||||||
|
private _cleanup: () => void;
|
||||||
|
private _params: WaitTaskParams & { predicateBody: string };
|
||||||
|
private _runCount: number;
|
||||||
|
private _resolve: (result: Handle) => void;
|
||||||
|
private _reject: (reason: Error) => void;
|
||||||
|
private _timeoutTimer: NodeJS.Timer;
|
||||||
|
private _terminated: boolean;
|
||||||
|
|
||||||
|
constructor(params: WaitTaskParams, cleanup: () => void) {
|
||||||
|
if (helper.isString(params.polling))
|
||||||
|
assert(params.polling === 'raf' || params.polling === 'mutation', 'Unknown polling option: ' + params.polling);
|
||||||
|
else if (helper.isNumber(params.polling))
|
||||||
|
assert(params.polling > 0, 'Cannot poll with non-positive interval: ' + params.polling);
|
||||||
|
else
|
||||||
|
throw new Error('Unknown polling options: ' + params.polling);
|
||||||
|
|
||||||
|
this._params = {
|
||||||
|
...params,
|
||||||
|
predicateBody: helper.isString(params.predicateBody) ? 'return (' + params.predicateBody + ')' : 'return (' + params.predicateBody + ')(...args)'
|
||||||
|
};
|
||||||
|
this._cleanup = cleanup;
|
||||||
|
this._runCount = 0;
|
||||||
|
this.promise = new Promise<Handle>((resolve, reject) => {
|
||||||
|
this._resolve = resolve;
|
||||||
|
this._reject = reject;
|
||||||
|
});
|
||||||
|
// Since page navigation requires us to re-install the pageScript, we should track
|
||||||
|
// timeout on our end.
|
||||||
|
if (params.timeout) {
|
||||||
|
const timeoutError = new TimeoutError(`waiting for ${params.title} failed: timeout ${params.timeout}ms exceeded`);
|
||||||
|
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), params.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate(error: Error) {
|
||||||
|
this._terminated = true;
|
||||||
|
this._reject(error);
|
||||||
|
this._doCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
async rerun(context: types.EvaluationContext<Handle>) {
|
||||||
|
const runCount = ++this._runCount;
|
||||||
|
let success: Handle | null = null;
|
||||||
|
let error = null;
|
||||||
|
try {
|
||||||
|
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._terminated || runCount !== this._runCount) {
|
||||||
|
if (success)
|
||||||
|
await success.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
||||||
|
// If execution context has been already destroyed, `context.evaluate` will
|
||||||
|
// throw an error - ignore this predicate run altogether.
|
||||||
|
if (!error && await context.evaluate(s => !s, success).catch(e => true)) {
|
||||||
|
await success.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the page is navigated, the promise is rejected.
|
||||||
|
// We will try again in the new execution context.
|
||||||
|
if (error && error.message.includes('Execution context was destroyed'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We could have tried to evaluate in a context which was already
|
||||||
|
// destroyed.
|
||||||
|
if (error && error.message.includes('Cannot find context with specified id'))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
this._reject(error);
|
||||||
|
else
|
||||||
|
this._resolve(success);
|
||||||
|
|
||||||
|
this._doCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_doCleanup() {
|
||||||
|
clearTimeout(this._timeoutTimer);
|
||||||
|
this._cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForPredicatePageFunction(predicateBody: string, polling: string | number, timeout: number, ...args): Promise<any> {
|
||||||
|
const predicate = new Function('...args', predicateBody);
|
||||||
|
let timedOut = false;
|
||||||
|
if (timeout)
|
||||||
|
setTimeout(() => timedOut = true, timeout);
|
||||||
|
if (polling === 'raf')
|
||||||
|
return await pollRaf();
|
||||||
|
if (polling === 'mutation')
|
||||||
|
return await pollMutation();
|
||||||
|
if (typeof polling === 'number')
|
||||||
|
return await pollInterval(polling);
|
||||||
|
|
||||||
|
function pollMutation(): Promise<any> {
|
||||||
|
const success = predicate.apply(null, args);
|
||||||
|
if (success)
|
||||||
|
return Promise.resolve(success);
|
||||||
|
|
||||||
|
let fulfill;
|
||||||
|
const result = new Promise(x => fulfill = x);
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
if (timedOut) {
|
||||||
|
observer.disconnect();
|
||||||
|
fulfill();
|
||||||
|
}
|
||||||
|
const success = predicate.apply(null, args);
|
||||||
|
if (success) {
|
||||||
|
observer.disconnect();
|
||||||
|
fulfill(success);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollRaf(): Promise<any> {
|
||||||
|
let fulfill;
|
||||||
|
const result = new Promise(x => fulfill = x);
|
||||||
|
onRaf();
|
||||||
|
return result;
|
||||||
|
|
||||||
|
function onRaf() {
|
||||||
|
if (timedOut) {
|
||||||
|
fulfill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const success = predicate.apply(null, args);
|
||||||
|
if (success)
|
||||||
|
fulfill(success);
|
||||||
|
else
|
||||||
|
requestAnimationFrame(onRaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollInterval(pollInterval: number): Promise<any> {
|
||||||
|
let fulfill;
|
||||||
|
const result = new Promise(x => fulfill = x);
|
||||||
|
onTimeout();
|
||||||
|
return result;
|
||||||
|
|
||||||
|
function onTimeout() {
|
||||||
|
if (timedOut) {
|
||||||
|
fulfill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const success = predicate.apply(null, args);
|
||||||
|
if (success)
|
||||||
|
fulfill(success);
|
||||||
|
else
|
||||||
|
setTimeout(onTimeout, pollInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,8 @@ import { NetworkManager, NetworkManagerEvents, Request, Response } from './Netwo
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { MultiClickOptions, ClickOptions, SelectOption } from '../input';
|
import { MultiClickOptions, ClickOptions, SelectOption } from '../input';
|
||||||
|
import { WaitTask, WaitTaskParams } from '../waitTask';
|
||||||
|
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
||||||
export const FrameManagerEvents = {
|
export const FrameManagerEvents = {
|
||||||
|
|
@ -238,8 +240,8 @@ export class Frame {
|
||||||
_detached: boolean;
|
_detached: boolean;
|
||||||
_loaderId: string;
|
_loaderId: string;
|
||||||
_lifecycleEvents: Set<string>;
|
_lifecycleEvents: Set<string>;
|
||||||
_waitTasks: Set<WaitTask>;
|
_waitTasks: Set<WaitTask<JSHandle>>;
|
||||||
_executionContext: ExecutionContext;
|
_executionContext: ExecutionContext | null;
|
||||||
_contextPromise: Promise<ExecutionContext>;
|
_contextPromise: Promise<ExecutionContext>;
|
||||||
_contextResolveCallback: (arg: ExecutionContext) => void;
|
_contextResolveCallback: (arg: ExecutionContext) => void;
|
||||||
_childFrames: Set<Frame>;
|
_childFrames: Set<Frame>;
|
||||||
|
|
@ -300,7 +302,14 @@ export class Frame {
|
||||||
polling = 'raf',
|
polling = 'raf',
|
||||||
timeout = this._frameManager._timeoutSettings.timeout(),
|
timeout = this._frameManager._timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
|
const params: WaitTaskParams = {
|
||||||
|
predicateBody: pageFunction,
|
||||||
|
title: 'function',
|
||||||
|
polling,
|
||||||
|
timeout,
|
||||||
|
args
|
||||||
|
};
|
||||||
|
return this._scheduleWaitTask(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executionContext(): Promise<ExecutionContext> {
|
async executionContext(): Promise<ExecutionContext> {
|
||||||
|
|
@ -606,8 +615,14 @@ export class Frame {
|
||||||
} = options;
|
} = options;
|
||||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||||
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
|
||||||
const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
|
const params: WaitTaskParams = {
|
||||||
const handle = await waitTask.promise;
|
predicateBody: predicate,
|
||||||
|
title,
|
||||||
|
polling,
|
||||||
|
timeout,
|
||||||
|
args: [selectorOrXPath, isXPath, waitForVisible, waitForHidden]
|
||||||
|
};
|
||||||
|
const handle = await this._scheduleWaitTask(params);
|
||||||
if (!handle.asElement()) {
|
if (!handle.asElement()) {
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -676,7 +691,7 @@ export class Frame {
|
||||||
this._contextResolveCallback.call(null, context);
|
this._contextResolveCallback.call(null, context);
|
||||||
this._contextResolveCallback = null;
|
this._contextResolveCallback = null;
|
||||||
for (const waitTask of this._waitTasks)
|
for (const waitTask of this._waitTasks)
|
||||||
waitTask.rerun();
|
waitTask.rerun(context);
|
||||||
} else {
|
} else {
|
||||||
this._documentPromise = null;
|
this._documentPromise = null;
|
||||||
this._contextPromise = new Promise(fulfill => {
|
this._contextPromise = new Promise(fulfill => {
|
||||||
|
|
@ -684,6 +699,14 @@ export class Frame {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _scheduleWaitTask(params: WaitTaskParams): Promise<JSHandle> {
|
||||||
|
const task = new WaitTask(params, () => this._waitTasks.delete(task));
|
||||||
|
this._waitTasks.add(task);
|
||||||
|
if (this._executionContext)
|
||||||
|
task.rerun(this._executionContext);
|
||||||
|
return task.promise;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -773,181 +796,3 @@ class NextNavigationWatchdog {
|
||||||
clearTimeout(this._timeoutId);
|
clearTimeout(this._timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WaitTask {
|
|
||||||
promise: Promise<any>;
|
|
||||||
_domWorld: any;
|
|
||||||
_polling: string | number;
|
|
||||||
_timeout: number;
|
|
||||||
_predicateBody: string;
|
|
||||||
_args: any[];
|
|
||||||
_runCount: number;
|
|
||||||
_resolve: (value?: unknown) => void;
|
|
||||||
_reject: (reason?: any) => void;
|
|
||||||
_timeoutTimer: NodeJS.Timer;
|
|
||||||
_terminated: boolean;
|
|
||||||
_runningTask: any;
|
|
||||||
constructor(domWorld: Frame, predicateBody: Function | string, title, polling: string | number, timeout: number, ...args: Array<any>) {
|
|
||||||
if (helper.isString(polling))
|
|
||||||
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
|
|
||||||
else if (helper.isNumber(polling))
|
|
||||||
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
|
|
||||||
else
|
|
||||||
throw new Error('Unknown polling options: ' + polling);
|
|
||||||
|
|
||||||
this._domWorld = domWorld;
|
|
||||||
this._polling = polling;
|
|
||||||
this._timeout = timeout;
|
|
||||||
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
|
|
||||||
this._args = args;
|
|
||||||
this._runCount = 0;
|
|
||||||
domWorld._waitTasks.add(this);
|
|
||||||
this.promise = new Promise((resolve, reject) => {
|
|
||||||
this._resolve = resolve;
|
|
||||||
this._reject = reject;
|
|
||||||
});
|
|
||||||
// Since page navigation requires us to re-install the pageScript, we should track
|
|
||||||
// timeout on our end.
|
|
||||||
if (timeout) {
|
|
||||||
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
|
|
||||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
|
||||||
}
|
|
||||||
this.rerun();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminate(error: Error) {
|
|
||||||
this._terminated = true;
|
|
||||||
this._reject(error);
|
|
||||||
this._cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerun() {
|
|
||||||
const runCount = ++this._runCount;
|
|
||||||
/** @type {?JSHandle} */
|
|
||||||
let success: JSHandle | null = null;
|
|
||||||
let error = null;
|
|
||||||
try {
|
|
||||||
success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._terminated || runCount !== this._runCount) {
|
|
||||||
if (success)
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
||||||
// If the frame's execution context has already changed, `frame.evaluate` will
|
|
||||||
// throw an error - ignore this predicate run altogether.
|
|
||||||
if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) {
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the page is navigated, the promise is rejected.
|
|
||||||
// We will try again in the new execution context.
|
|
||||||
if (error && error.message.includes('Execution context was destroyed'))
|
|
||||||
return;
|
|
||||||
// In WebKit cross-process navigation leads to targetDestroyed (followed
|
|
||||||
// by targetCreated).
|
|
||||||
if (error && error.message.includes('Target closed'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// We could have tried to evaluate in a context which was already
|
|
||||||
// destroyed.
|
|
||||||
if (error && error.message.includes('Cannot find context with specified id'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
this._reject(error);
|
|
||||||
else
|
|
||||||
this._resolve(success);
|
|
||||||
|
|
||||||
this._cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanup() {
|
|
||||||
clearTimeout(this._timeoutTimer);
|
|
||||||
this._domWorld._waitTasks.delete(this);
|
|
||||||
this._runningTask = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPredicatePageFunction(predicateBody: string, polling: string, timeout: number, ...args): Promise<any> {
|
|
||||||
const predicate = new Function('...args', predicateBody);
|
|
||||||
let timedOut = false;
|
|
||||||
if (timeout)
|
|
||||||
setTimeout(() => timedOut = true, timeout);
|
|
||||||
if (polling === 'raf')
|
|
||||||
return await pollRaf();
|
|
||||||
if (polling === 'mutation')
|
|
||||||
return await pollMutation();
|
|
||||||
if (typeof polling === 'number')
|
|
||||||
return await pollInterval(polling);
|
|
||||||
|
|
||||||
function pollMutation(): Promise<any> {
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
return Promise.resolve(success);
|
|
||||||
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
const observer = new MutationObserver(mutations => {
|
|
||||||
if (timedOut) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill();
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollRaf(): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onRaf();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onRaf() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
requestAnimationFrame(onRaf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollInterval(pollInterval: number): Promise<any> {
|
|
||||||
let fulfill;
|
|
||||||
const result = new Promise(x => fulfill = x);
|
|
||||||
onTimeout();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
function onTimeout() {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicate.apply(null, args);
|
|
||||||
if (success)
|
|
||||||
fulfill(success);
|
|
||||||
else
|
|
||||||
setTimeout(onTimeout, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue