chore: make WaitTask not depend on DOMWorld (#66)

This opens up opportunity for reuse.
This commit is contained in:
Dmitry Gozman 2019-11-25 16:55:03 -08:00 committed by GitHub
parent d4d0654666
commit 72b252e5e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -35,6 +35,7 @@ export class DOMWorld {
private _documentPromise: Promise<ElementHandle> | null = null; private _documentPromise: Promise<ElementHandle> | null = null;
private _contextPromise: Promise<ExecutionContext>; private _contextPromise: Promise<ExecutionContext>;
private _contextResolveCallback: ((c: ExecutionContext) => void) | null; private _contextResolveCallback: ((c: ExecutionContext) => void) | null;
private _context: ExecutionContext | null;
_waitTasks = new Set<WaitTask>(); _waitTasks = new Set<WaitTask>();
private _detached = false; private _detached = false;
@ -51,11 +52,12 @@ export class DOMWorld {
} }
_setContext(context: ExecutionContext | null) { _setContext(context: ExecutionContext | null) {
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 => {
@ -295,7 +297,22 @@ 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);
}
private _scheduleWaitTask(params: WaitTaskParams): Promise<JSHandle> {
const task = new WaitTask(params, () => this._waitTasks.delete(task));
this._waitTasks.add(task);
if (this._context)
task.rerun(this._context);
return task.promise;
} }
async title(): Promise<string> { async title(): Promise<string> {
@ -313,8 +330,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;
@ -343,60 +366,62 @@ export class DOMWorld {
} }
} }
type WaitTaskParams = {
predicateBody: Function | string;
title: string;
polling: string | number;
timeout: number;
args: any[];
};
class WaitTask { class WaitTask {
promise: Promise<JSHandle>; readonly promise: Promise<JSHandle>;
_domWorld: DOMWorld; private _cleanup: () => void;
_polling: string | number; private _params: WaitTaskParams & { predicateBody: string };
_timeout: number; private _runCount: number;
_predicateBody: string; private _resolve: (result: JSHandle) => void;
_args: any[]; private _reject: (reason: Error) => void;
_runCount: number; private _timeoutTimer: NodeJS.Timer;
_resolve: (result: JSHandle) => void; private _terminated: boolean;
_reject: (reason: Error) => void;
_timeoutTimer: NodeJS.Timer;
_terminated: boolean;
_runningTask: any;
constructor(domWorld: DOMWorld, predicateBody: Function | string, title, polling: string | number, timeout: number, ...args: any[]) { constructor(params: WaitTaskParams, cleanup: () => void) {
if (helper.isString(polling)) if (helper.isString(params.polling))
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); assert(params.polling === 'raf' || params.polling === 'mutation', 'Unknown polling option: ' + params.polling);
else if (helper.isNumber(polling)) else if (helper.isNumber(params.polling))
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); assert(params.polling > 0, 'Cannot poll with non-positive interval: ' + params.polling);
else else
throw new Error('Unknown polling options: ' + polling); throw new Error('Unknown polling options: ' + params.polling);
this._domWorld = domWorld; this._params = {
this._polling = polling; ...params,
this._timeout = timeout; predicateBody: helper.isString(params.predicateBody) ? 'return (' + params.predicateBody + ')' : 'return (' + params.predicateBody + ')(...args)'
this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)'; };
this._args = args; this._cleanup = cleanup;
this._runCount = 0; this._runCount = 0;
domWorld._waitTasks.add(this);
this.promise = new Promise<JSHandle>((resolve, reject) => { this.promise = new Promise<JSHandle>((resolve, reject) => {
this._resolve = resolve; this._resolve = resolve;
this._reject = reject; this._reject = reject;
}); });
// Since page navigation requires us to re-install the pageScript, we should track // Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end. // timeout on our end.
if (timeout) { if (params.timeout) {
const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`); const timeoutError = new TimeoutError(`waiting for ${params.title} failed: timeout ${params.timeout}ms exceeded`);
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout); this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), params.timeout);
} }
this.rerun();
} }
terminate(error: Error) { terminate(error: Error) {
this._terminated = true; this._terminated = true;
this._reject(error); this._reject(error);
this._cleanup(); this._doCleanup();
} }
async rerun() { async rerun(context: ExecutionContext) {
const runCount = ++this._runCount; const runCount = ++this._runCount;
let success: JSHandle | null = null; let success: JSHandle | null = null;
let error = null; let error = null;
try { try {
success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args); success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
} catch (e) { } catch (e) {
error = e; error = e;
} }
@ -408,9 +433,9 @@ class WaitTask {
} }
// Ignore timeouts in pageScript - we track timeouts ourselves. // Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will // If execution context has been already destroyed, `context.evaluate` will
// throw an error - ignore this predicate run altogether. // throw an error - ignore this predicate run altogether.
if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) { if (!error && await context.evaluate(s => !s, success).catch(e => true)) {
await success.dispose(); await success.dispose();
return; return;
} }
@ -430,13 +455,12 @@ class WaitTask {
else else
this._resolve(success); this._resolve(success);
this._cleanup(); this._doCleanup();
} }
_cleanup() { _doCleanup() {
clearTimeout(this._timeoutTimer); clearTimeout(this._timeoutTimer);
this._domWorld._waitTasks.delete(this); this._cleanup();
this._runningTask = null;
} }
} }