fix(domworld): fix missing binding for waittasks (#6562)

This commit is contained in:
Johan Bay 2020-11-03 11:39:31 +01:00 committed by GitHub
parent 659193a4f5
commit 67da1cf866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 131 additions and 88 deletions

View File

@ -18,7 +18,7 @@ import { InternalQueryHandler } from './QueryHandler.js';
import { ElementHandle, JSHandle } from './JSHandle.js'; import { ElementHandle, JSHandle } from './JSHandle.js';
import { Protocol } from 'devtools-protocol'; import { Protocol } from 'devtools-protocol';
import { CDPSession } from './Connection.js'; import { CDPSession } from './Connection.js';
import { DOMWorld, WaitForSelectorOptions } from './DOMWorld.js'; import { DOMWorld, PageBinding, WaitForSelectorOptions } from './DOMWorld.js';
async function queryAXTree( async function queryAXTree(
client: CDPSession, client: CDPSession,
@ -87,12 +87,20 @@ const waitFor = async (
domWorld: DOMWorld, domWorld: DOMWorld,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
) => { ): Promise<ElementHandle<Element>> => {
await addHandlerToWorld(domWorld); const binding: PageBinding = {
name: 'ariaQuerySelector',
pptrFunction: async (selector: string) => {
const document = await domWorld._document();
const element = await queryOne(document, selector);
return element;
},
};
return domWorld.waitForSelectorInPage( return domWorld.waitForSelectorInPage(
(_: Element, selector: string) => globalThis.ariaQuerySelector(selector), (_: Element, selector: string) => globalThis.ariaQuerySelector(selector),
selector, selector,
options options,
binding
); );
}; };
@ -121,14 +129,6 @@ const queryAllArray = async (
return jsHandle; return jsHandle;
}; };
async function addHandlerToWorld(domWorld: DOMWorld) {
await domWorld.addBinding('ariaQuerySelector', async (selector: string) => {
const document = await domWorld._document();
const element = await queryOne(document, selector);
return element;
});
}
/** /**
* @internal * @internal
*/ */

View File

@ -59,6 +59,14 @@ export interface WaitForSelectorOptions {
timeout?: number; timeout?: number;
} }
/**
* @internal
*/
export interface PageBinding {
name: string;
pptrFunction: Function;
}
/** /**
* @internal * @internal
*/ */
@ -73,14 +81,19 @@ export class DOMWorld {
private _detached = false; private _detached = false;
/** /**
* internal * @internal
*/ */
_waitTasks = new Set<WaitTask>(); _waitTasks = new Set<WaitTask>();
// Contains mapping from functions that should be bound to Puppeteer functions. /**
private _boundFunctions = new Map<string, Function>(); * @internal
* Contains mapping from functions that should be bound to Puppeteer functions.
*/
_boundFunctions = new Map<string, Function>();
// Set of bindings that have been registered in the current context. // Set of bindings that have been registered in the current context.
private _ctxBindings = new Set<string>(); private _ctxBindings = new Set<string>();
private static bindingIdentifier = (name: string, contextId: number) =>
`${name}_${contextId}`;
constructor( constructor(
frameManager: FrameManager, frameManager: FrameManager,
@ -104,10 +117,6 @@ export class DOMWorld {
if (context) { if (context) {
this._contextResolveCallback.call(null, context); this._contextResolveCallback.call(null, context);
this._contextResolveCallback = null; this._contextResolveCallback = null;
this._ctxBindings.clear();
for (const name of this._boundFunctions.keys()) {
await this.addBindingToContext(name);
}
for (const waitTask of this._waitTasks) waitTask.rerun(); for (const waitTask of this._waitTasks) waitTask.rerun();
} else { } else {
this._documentPromise = null; this._documentPromise = null;
@ -482,19 +491,27 @@ export class DOMWorld {
/** /**
* @internal * @internal
*/ */
async addBindingToContext(name: string) { async addBindingToContext(
context: ExecutionContext,
name: string
): Promise<void> {
// Previous operation added the binding so we are done. // Previous operation added the binding so we are done.
if (this._ctxBindings.has(name)) return; if (
this._ctxBindings.has(
DOMWorld.bindingIdentifier(name, context._contextId)
)
) {
return;
}
// Wait for other operation to finish // Wait for other operation to finish
if (this._settingUpBinding) { if (this._settingUpBinding) {
await this._settingUpBinding; await this._settingUpBinding;
return this.addBindingToContext(name); return this.addBindingToContext(context, name);
} }
const bind = async (name: string) => { const bind = async (name: string) => {
const expression = helper.pageBindingInitString('internal', name); const expression = helper.pageBindingInitString('internal', name);
try { try {
const context = await this.executionContext();
await context._client.send('Runtime.addBinding', { await context._client.send('Runtime.addBinding', {
name, name,
executionContextId: context._contextId, executionContextId: context._contextId,
@ -511,14 +528,15 @@ export class DOMWorld {
'Cannot find context with specified id' 'Cannot find context with specified id'
); );
if (ctxDestroyed || ctxNotFound) { if (ctxDestroyed || ctxNotFound) {
// Retry adding the binding in the next context return;
await bind(name);
} else { } else {
debugError(error); debugError(error);
return; return;
} }
} }
this._ctxBindings.add(name); this._ctxBindings.add(
DOMWorld.bindingIdentifier(name, context._contextId)
);
}; };
this._settingUpBinding = bind(name); this._settingUpBinding = bind(name);
@ -526,18 +544,12 @@ export class DOMWorld {
this._settingUpBinding = null; this._settingUpBinding = null;
} }
/**
* @internal
*/
async addBinding(name: string, puppeteerFunction: Function): Promise<void> {
this._boundFunctions.set(name, puppeteerFunction);
await this.addBindingToContext(name);
}
private async _onBindingCalled( private async _onBindingCalled(
event: Protocol.Runtime.BindingCalledEvent event: Protocol.Runtime.BindingCalledEvent
): Promise<void> { ): Promise<void> {
let payload: { type: string; name: string; seq: number; args: unknown[] }; let payload: { type: string; name: string; seq: number; args: unknown[] };
if (!this._hasContext()) return;
const context = await this.executionContext();
try { try {
payload = JSON.parse(event.payload); payload = JSON.parse(event.payload);
} catch { } catch {
@ -546,9 +558,13 @@ export class DOMWorld {
return; return;
} }
const { type, name, seq, args } = payload; const { type, name, seq, args } = payload;
if (type !== 'internal' || !this._ctxBindings.has(name)) return; if (
if (!this._hasContext()) return; type !== 'internal' ||
const context = await this.executionContext(); !this._ctxBindings.has(
DOMWorld.bindingIdentifier(name, context._contextId)
)
)
return;
if (context._contextId !== event.executionContextId) return; if (context._contextId !== event.executionContextId) return;
try { try {
const result = await this._boundFunctions.get(name)(...args); const result = await this._boundFunctions.get(name)(...args);
@ -574,7 +590,8 @@ export class DOMWorld {
async waitForSelectorInPage( async waitForSelectorInPage(
queryOne: Function, queryOne: Function,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions,
binding?: PageBinding
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const { const {
visible: waitForVisible = false, visible: waitForVisible = false,
@ -595,16 +612,16 @@ export class DOMWorld {
: document.querySelector(selector); : document.querySelector(selector);
return checkWaitForOptions(node, waitForVisible, waitForHidden); return checkWaitForOptions(node, waitForVisible, waitForHidden);
} }
const waitTask = new WaitTask( const waitTaskOptions: WaitTaskOptions = {
this, domWorld: this,
helper.makePredicateString(predicate, queryOne), predicateBody: helper.makePredicateString(predicate, queryOne),
title, title,
polling, polling,
timeout, timeout,
selector, args: [selector, waitForVisible, waitForHidden],
waitForVisible, binding,
waitForHidden };
); const waitTask = new WaitTask(waitTaskOptions);
const jsHandle = await waitTask.promise; const jsHandle = await waitTask.promise;
const elementHandle = jsHandle.asElement(); const elementHandle = jsHandle.asElement();
if (!elementHandle) { if (!elementHandle) {
@ -639,16 +656,15 @@ export class DOMWorld {
).singleNodeValue; ).singleNodeValue;
return checkWaitForOptions(node, waitForVisible, waitForHidden); return checkWaitForOptions(node, waitForVisible, waitForHidden);
} }
const waitTask = new WaitTask( const waitTaskOptions: WaitTaskOptions = {
this, domWorld: this,
helper.makePredicateString(predicate), predicateBody: helper.makePredicateString(predicate),
title, title,
polling, polling,
timeout, timeout,
xpath, args: [xpath, waitForVisible, waitForHidden],
waitForVisible, };
waitForHidden const waitTask = new WaitTask(waitTaskOptions);
);
const jsHandle = await waitTask.promise; const jsHandle = await waitTask.promise;
const elementHandle = jsHandle.asElement(); const elementHandle = jsHandle.asElement();
if (!elementHandle) { if (!elementHandle) {
@ -667,14 +683,16 @@ export class DOMWorld {
polling = 'raf', polling = 'raf',
timeout = this._timeoutSettings.timeout(), timeout = this._timeoutSettings.timeout(),
} = options; } = options;
return new WaitTask( const waitTaskOptions: WaitTaskOptions = {
this, domWorld: this,
pageFunction, predicateBody: pageFunction,
'function', title: 'function',
polling, polling,
timeout, timeout,
...args args,
).promise; };
const waitTask = new WaitTask(waitTaskOptions);
return waitTask.promise;
} }
async title(): Promise<string> { async title(): Promise<string> {
@ -682,6 +700,19 @@ export class DOMWorld {
} }
} }
/**
* @internal
*/
export interface WaitTaskOptions {
domWorld: DOMWorld;
predicateBody: Function | string;
title: string;
polling: string | number;
timeout: number;
binding?: PageBinding;
args: SerializableOrJSHandle[];
}
/** /**
* @internal * @internal
*/ */
@ -691,6 +722,7 @@ export class WaitTask {
_timeout: number; _timeout: number;
_predicateBody: string; _predicateBody: string;
_args: SerializableOrJSHandle[]; _args: SerializableOrJSHandle[];
_binding: PageBinding;
_runCount = 0; _runCount = 0;
promise: Promise<JSHandle>; promise: Promise<JSHandle>;
_resolve: (x: JSHandle) => void; _resolve: (x: JSHandle) => void;
@ -698,48 +730,51 @@ export class WaitTask {
_timeoutTimer?: NodeJS.Timeout; _timeoutTimer?: NodeJS.Timeout;
_terminated = false; _terminated = false;
constructor( constructor(options: WaitTaskOptions) {
domWorld: DOMWorld, if (helper.isString(options.polling))
predicateBody: Function | string,
title: string,
polling: string | number,
timeout: number,
...args: SerializableOrJSHandle[]
) {
if (helper.isString(polling))
assert( assert(
polling === 'raf' || polling === 'mutation', options.polling === 'raf' || options.polling === 'mutation',
'Unknown polling option: ' + polling 'Unknown polling option: ' + options.polling
); );
else if (helper.isNumber(polling)) else if (helper.isNumber(options.polling))
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); assert(
else throw new Error('Unknown polling options: ' + polling); options.polling > 0,
'Cannot poll with non-positive interval: ' + options.polling
);
else throw new Error('Unknown polling options: ' + options.polling);
function getPredicateBody(predicateBody: Function | string) { function getPredicateBody(predicateBody: Function | string) {
if (helper.isString(predicateBody)) return `return (${predicateBody});`; if (helper.isString(predicateBody)) return `return (${predicateBody});`;
return `return (${predicateBody})(...args);`; return `return (${predicateBody})(...args);`;
} }
this._domWorld = domWorld; this._domWorld = options.domWorld;
this._polling = polling; this._polling = options.polling;
this._timeout = timeout; this._timeout = options.timeout;
this._predicateBody = getPredicateBody(predicateBody); this._predicateBody = getPredicateBody(options.predicateBody);
this._args = args; this._args = options.args;
this._binding = options.binding;
this._runCount = 0; this._runCount = 0;
domWorld._waitTasks.add(this); this._domWorld._waitTasks.add(this);
if (this._binding) {
this._domWorld._boundFunctions.set(
this._binding.name,
this._binding.pptrFunction
);
}
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 (options.timeout) {
const timeoutError = new TimeoutError( const timeoutError = new TimeoutError(
`waiting for ${title} failed: timeout ${timeout}ms exceeded` `waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
); );
this._timeoutTimer = setTimeout( this._timeoutTimer = setTimeout(
() => this.terminate(timeoutError), () => this.terminate(timeoutError),
timeout options.timeout
); );
} }
this.rerun(); this.rerun();
@ -753,11 +788,16 @@ export class WaitTask {
async rerun(): Promise<void> { async rerun(): Promise<void> {
const runCount = ++this._runCount; const runCount = ++this._runCount;
/** @type {?JSHandle} */ let success: JSHandle = null;
let success = null; let error: Error = null;
let error = null; const context = await this._domWorld.executionContext();
if (this._terminated || runCount !== this._runCount) return;
if (this._binding) {
await this._domWorld.addBindingToContext(context, this._binding.name);
}
if (this._terminated || runCount !== this._runCount) return;
try { try {
success = await (await this._domWorld.executionContext()).evaluateHandle( success = await context.evaluateHandle(
waitForPredicatePageFunction, waitForPredicatePageFunction,
this._predicateBody, this._predicateBody,
this._polling, this._polling,
@ -783,10 +823,13 @@ export class WaitTask {
await success.dispose(); await success.dispose();
return; return;
} }
// When frame is detached the task should have been terminated by the DOMWorld.
// This can fail if we were adding this task while the frame was detached,
// so we terminate here instead.
if (error) { if (error) {
if (error.message.includes('TypeError: binding is not a function')) {
return this.rerun();
}
// When frame is detached the task should have been terminated by the DOMWorld.
// This can fail if we were adding this task while the frame was detached,
// so we terminate here instead.
if ( if (
error.message.includes( error.message.includes(
'Execution context is not available in detached frame' 'Execution context is not available in detached frame'