mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-02 08:07:34 +03:00
chore: rework wait task to accept arbitrary task on dom world (#126)
This commit is contained in:
parent
99f9b11be8
commit
e124d44a55
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
/node_modules/
|
||||
/test/output-chromium
|
||||
/test/output-firefox
|
||||
/test/output-webkit
|
||||
/test/test-user-data-dir*
|
||||
/.local-chromium/
|
||||
/.local-browser/
|
||||
|
@ -1828,7 +1828,7 @@ await element.setInputFiles('/tmp/myfile.pdf');
|
||||
#### page.waitForFunction(pageFunction[, options[, ...args]])
|
||||
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||
- `options` <[Object]> Optional waiting parameters
|
||||
- `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values:
|
||||
- `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values:
|
||||
- `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes.
|
||||
- `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
|
||||
@ -2864,7 +2864,7 @@ await page.waitFor(selector => !!document.querySelector(selector), {}, selector)
|
||||
#### frame.waitForFunction(pageFunction[, options[, ...args]])
|
||||
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||
- `options` <[Object]> Optional waiting parameters
|
||||
- `polling` <[string]|[number]> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values:
|
||||
- `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values:
|
||||
- `raf` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes.
|
||||
- `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
|
||||
|
@ -579,10 +579,7 @@ export class Page extends EventEmitter {
|
||||
return this.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
waitForFunction(pageFunction: Function, options: {
|
||||
polling?: string | number;
|
||||
timeout?: number; } = {},
|
||||
...args: any[]): Promise<js.JSHandle> {
|
||||
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
|
||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
}
|
||||
|
76
src/dom.ts
76
src/dom.ts
@ -10,7 +10,6 @@ import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
||||
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||
import { assert, helper } from './helper';
|
||||
import Injected from './injected/injected';
|
||||
import { WaitTaskParams } from './waitTask';
|
||||
|
||||
export interface DOMWorldDelegate {
|
||||
keyboard: input.Keyboard;
|
||||
@ -307,36 +306,57 @@ function selectorToString(selector: Selector): string {
|
||||
return `:scope >> ${selector.selector}`;
|
||||
}
|
||||
|
||||
export type Task = (domWorld: DOMWorld) => Promise<js.JSHandle>;
|
||||
|
||||
export type Polling = 'raf' | 'mutation' | number;
|
||||
export type WaitForFunctionOptions = { polling?: Polling, timeout?: number };
|
||||
|
||||
export function waitForFunctionTask(pageFunction: Function | string, options: WaitForFunctionOptions, ...args: any[]) {
|
||||
const { polling = 'raf' } = options;
|
||||
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);
|
||||
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)';
|
||||
|
||||
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, predicateBody: string, polling: Polling, timeout: number, ...args) => {
|
||||
const predicate = new Function('...args', predicateBody);
|
||||
if (polling === 'raf')
|
||||
return injected.pollRaf(predicate, timeout, ...args);
|
||||
if (polling === 'mutation')
|
||||
return injected.pollMutation(predicate, timeout, ...args);
|
||||
return injected.pollInterval(polling, predicate, timeout, ...args);
|
||||
}, await domWorld.injected(), predicateBody, polling, options.timeout, ...args);
|
||||
}
|
||||
|
||||
export type WaitForSelectorOptions = { visible?: boolean, hidden?: boolean, timeout?: number };
|
||||
|
||||
export function waitForSelectorTask(selector: string, options: WaitForSelectorOptions): WaitTaskParams {
|
||||
const { visible: waitForVisible = false, hidden: waitForHidden = false, timeout } = options;
|
||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||
const title = `selector "${selector}"${waitForHidden ? ' to be hidden' : ''}`;
|
||||
const params: WaitTaskParams = {
|
||||
predicateBody: predicate,
|
||||
title,
|
||||
polling,
|
||||
timeout,
|
||||
args: [normalizeSelector(selector), waitForVisible, waitForHidden],
|
||||
passInjected: true
|
||||
};
|
||||
return params;
|
||||
export function waitForSelectorTask(selector: string, options: WaitForSelectorOptions): Task {
|
||||
const { visible: waitForVisible = false, hidden: waitForHidden = false } = options;
|
||||
selector = normalizeSelector(selector);
|
||||
|
||||
function predicate(injected: Injected, selector: string, waitForVisible: boolean, waitForHidden: boolean): (Node | boolean) | null {
|
||||
const element = injected.querySelector(selector, document);
|
||||
if (!element)
|
||||
return waitForHidden;
|
||||
if (!waitForVisible && !waitForHidden)
|
||||
return element;
|
||||
const style = window.getComputedStyle(element);
|
||||
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
||||
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
|
||||
return success ? element : null;
|
||||
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, selector: string, waitForVisible: boolean, waitForHidden: boolean, timeout: number) => {
|
||||
if (waitForVisible || waitForHidden)
|
||||
return injected.pollRaf(predicate, timeout);
|
||||
return injected.pollMutation(predicate, timeout);
|
||||
|
||||
function hasVisibleBoundingBox(): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||
function predicate(): Element | boolean {
|
||||
const element = injected.querySelector(selector, document);
|
||||
if (!element)
|
||||
return waitForHidden;
|
||||
if (!waitForVisible && !waitForHidden)
|
||||
return element;
|
||||
const style = window.getComputedStyle(element);
|
||||
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
||||
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
|
||||
return success ? element : false;
|
||||
|
||||
function hasVisibleBoundingBox(): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, await domWorld.injected(), selector, waitForVisible, waitForHidden, options.timeout);
|
||||
}
|
||||
|
@ -493,7 +493,7 @@ export class Page extends EventEmitter {
|
||||
return this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
||||
}
|
||||
|
||||
waitForFunction(pageFunction: Function | string, options: { polling?: string | number; timeout?: number; } | undefined = {}, ...args): Promise<js.JSHandle> {
|
||||
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args): Promise<js.JSHandle> {
|
||||
return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
|
||||
|
132
src/frames.ts
132
src/frames.ts
@ -22,8 +22,8 @@ import * as dom from './dom';
|
||||
import * as network from './network';
|
||||
import { helper, assert } from './helper';
|
||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
||||
import { WaitTaskParams, WaitTask } from './waitTask';
|
||||
import { TimeoutSettings } from './TimeoutSettings';
|
||||
import { TimeoutError } from './Errors';
|
||||
|
||||
const readFileAsync = helper.promisify(fs.readFile);
|
||||
|
||||
@ -32,7 +32,7 @@ type World = {
|
||||
contextPromise: Promise<js.ExecutionContext>;
|
||||
contextResolveCallback: (c: js.ExecutionContext) => void;
|
||||
context: js.ExecutionContext | null;
|
||||
waitTasks: Set<WaitTask>;
|
||||
rerunnableTasks: Set<RerunnableTask>;
|
||||
};
|
||||
|
||||
export type NavigateOptions = {
|
||||
@ -65,8 +65,8 @@ export class Frame {
|
||||
this._timeoutSettings = timeoutSettings;
|
||||
this._parentFrame = parentFrame;
|
||||
|
||||
this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, waitTasks: new Set() });
|
||||
this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, waitTasks: new Set() });
|
||||
this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
||||
this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
||||
this._setContext('main', null);
|
||||
this._setContext('utility', null);
|
||||
|
||||
@ -386,8 +386,9 @@ export class Frame {
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options: dom.WaitForSelectorOptions = {}): Promise<dom.ElementHandle | null> {
|
||||
const params = dom.waitForSelectorTask(selector, { timeout: this._timeoutSettings.timeout(), ...options });
|
||||
const handle = await this._scheduleWaitTask(params, 'utility');
|
||||
const task = dom.waitForSelectorTask(selector, { timeout: this._timeoutSettings.timeout(), ...options });
|
||||
const title = `selector "${selector}"${options.hidden ? ' to be hidden' : ''}`;
|
||||
const handle = await this._scheduleRerunnableTask(task, 'utility', options.timeout, title);
|
||||
if (!handle.asElement()) {
|
||||
await handle.dispose();
|
||||
return null;
|
||||
@ -404,22 +405,10 @@ export class Frame {
|
||||
return this.waitForSelector('xpath=' + xpath, options);
|
||||
}
|
||||
|
||||
waitForFunction(
|
||||
pageFunction: Function | string,
|
||||
options: { polling?: string | number; timeout?: number; } = {},
|
||||
...args): Promise<js.JSHandle> {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this._timeoutSettings.timeout(),
|
||||
} = options;
|
||||
const params: WaitTaskParams = {
|
||||
predicateBody: pageFunction,
|
||||
title: 'function',
|
||||
polling,
|
||||
timeout,
|
||||
args
|
||||
};
|
||||
return this._scheduleWaitTask(params, 'main');
|
||||
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
|
||||
options = { timeout: this._timeoutSettings.timeout(), ...options };
|
||||
const task = dom.waitForFunctionTask(pageFunction, options, ...args);
|
||||
return this._scheduleRerunnableTask(task, 'main', options.timeout);
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
@ -435,30 +424,31 @@ export class Frame {
|
||||
_detach() {
|
||||
this._detached = true;
|
||||
for (const world of this._worlds.values()) {
|
||||
for (const waitTask of world.waitTasks)
|
||||
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||
for (const rerunnableTask of world.rerunnableTasks)
|
||||
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||
}
|
||||
if (this._parentFrame)
|
||||
this._parentFrame._childFrames.delete(this);
|
||||
this._parentFrame = null;
|
||||
}
|
||||
|
||||
private _scheduleWaitTask(params: WaitTaskParams, worldType: WorldType): Promise<js.JSHandle> {
|
||||
private _scheduleRerunnableTask(task: dom.Task, worldType: WorldType, timeout?: number, title?: string): Promise<js.JSHandle> {
|
||||
const world = this._worlds.get(worldType);
|
||||
const task = new WaitTask(params, () => world.waitTasks.delete(task));
|
||||
world.waitTasks.add(task);
|
||||
const rerunnableTask = new RerunnableTask(world, task, timeout, title);
|
||||
world.rerunnableTasks.add(rerunnableTask);
|
||||
if (world.context)
|
||||
task.rerun(world.context);
|
||||
return task.promise;
|
||||
rerunnableTask.rerun(world.context._domWorld);
|
||||
return rerunnableTask.promise;
|
||||
}
|
||||
|
||||
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
|
||||
const world = this._worlds.get(worldType);
|
||||
world.context = context;
|
||||
if (context) {
|
||||
assert(context._domWorld, 'Frame context must have a dom world');
|
||||
world.contextResolveCallback.call(null, context);
|
||||
for (const waitTask of world.waitTasks)
|
||||
waitTask.rerun(context);
|
||||
for (const rerunnableTask of world.rerunnableTasks)
|
||||
rerunnableTask.rerun(context._domWorld);
|
||||
} else {
|
||||
world.contextPromise = new Promise(fulfill => {
|
||||
world.contextResolveCallback = fulfill;
|
||||
@ -483,3 +473,83 @@ export class Frame {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RerunnableTask {
|
||||
readonly promise: Promise<js.JSHandle>;
|
||||
private _world: World;
|
||||
private _task: dom.Task;
|
||||
private _runCount: number;
|
||||
private _resolve: (result: js.JSHandle) => void;
|
||||
private _reject: (reason: Error) => void;
|
||||
private _timeoutTimer: NodeJS.Timer;
|
||||
private _terminated: boolean;
|
||||
|
||||
constructor(world: World, task: dom.Task, timeout?: number, title?: string) {
|
||||
this._world = world;
|
||||
this._task = task;
|
||||
this._runCount = 0;
|
||||
this.promise = new Promise<js.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 (timeout) {
|
||||
const timeoutError = new TimeoutError(`waiting for ${title || 'function'} failed: timeout ${timeout}ms exceeded`);
|
||||
this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
|
||||
}
|
||||
}
|
||||
|
||||
terminate(error: Error) {
|
||||
this._terminated = true;
|
||||
this._reject(error);
|
||||
this._doCleanup();
|
||||
}
|
||||
|
||||
async rerun(domWorld: dom.DOMWorld) {
|
||||
const runCount = ++this._runCount;
|
||||
let success: js.JSHandle | null = null;
|
||||
let error = null;
|
||||
try {
|
||||
success = await this._task(domWorld);
|
||||
} 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 domWorld.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._world.rerunnableTasks.delete(this);
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,82 @@ class Injected {
|
||||
append();
|
||||
return result;
|
||||
}
|
||||
|
||||
pollMutation(predicate: Function, timeout: number, ...args: any[]): Promise<any> {
|
||||
let timedOut = false;
|
||||
if (timeout)
|
||||
setTimeout(() => timedOut = true, timeout);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
pollRaf(predicate: Function, timeout: number, ...args: any[]): Promise<any> {
|
||||
let timedOut = false;
|
||||
if (timeout)
|
||||
setTimeout(() => timedOut = true, timeout);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pollInterval(pollInterval: number, predicate: Function, timeout: number, ...args: any[]): Promise<any> {
|
||||
let timedOut = false;
|
||||
if (timeout)
|
||||
setTimeout(() => timedOut = true, timeout);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Injected;
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
import * as frames from './frames';
|
||||
import { assert } from './helper';
|
||||
import { NetworkManager } from './chromium/NetworkManager';
|
||||
|
||||
export type NetworkCookie = {
|
||||
name: string,
|
||||
@ -50,11 +49,11 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]): Network
|
||||
|
||||
export function rewriteCookies(cookies: SetNetworkCookieParam[]): SetNetworkCookieParam[] {
|
||||
return cookies.map(c => {
|
||||
assert(c.name, "Cookie should have a name");
|
||||
assert(c.value, "Cookie should have a value");
|
||||
assert(c.url || (c.domain && c.path), "Cookie should have a url or a domain/path pair");
|
||||
assert(!(c.url && c.domain), "Cookie should have either url or domain");
|
||||
assert(!(c.url && c.path), "Cookie should have either url or domain");
|
||||
assert(c.name, 'Cookie should have a name');
|
||||
assert(c.value, 'Cookie should have a value');
|
||||
assert(c.url || (c.domain && c.path), 'Cookie should have a url or a domain/path pair');
|
||||
assert(!(c.url && c.domain), 'Cookie should have either url or domain');
|
||||
assert(!(c.url && c.path), 'Cookie should have either url or domain');
|
||||
const copy = {...c};
|
||||
if (copy.url) {
|
||||
assert(copy.url !== 'about:blank', `Blank page can not have cookie "${c.name}"`);
|
||||
|
187
src/waitTask.ts
187
src/waitTask.ts
@ -1,187 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { assert, helper } from './helper';
|
||||
import * as js from './javascript';
|
||||
import { TimeoutError } from './Errors';
|
||||
import Injected from './injected/injected';
|
||||
|
||||
export type WaitTaskParams = {
|
||||
// TODO: ensure types.
|
||||
predicateBody: Function | string;
|
||||
title: string;
|
||||
polling: string | number;
|
||||
timeout: number;
|
||||
args: any[];
|
||||
passInjected?: boolean;
|
||||
};
|
||||
|
||||
export class WaitTask {
|
||||
readonly promise: Promise<js.JSHandle>;
|
||||
private _cleanup: () => void;
|
||||
private _params: WaitTaskParams & { predicateBody: string };
|
||||
private _runCount: number;
|
||||
private _resolve: (result: js.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<js.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: js.ExecutionContext) {
|
||||
const runCount = ++this._runCount;
|
||||
let success: js.JSHandle | null = null;
|
||||
let error = null;
|
||||
try {
|
||||
assert(context._domWorld, 'Wait task requires a dom world');
|
||||
success = await context.evaluateHandle(waitForPredicatePageFunction, await context._domWorld.injected(), this._params.predicateBody, this._params.polling, this._params.timeout, !!this._params.passInjected, ...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(injected: Injected, predicateBody: string, polling: string | number, timeout: number, passInjected: boolean, ...args): Promise<any> {
|
||||
if (passInjected)
|
||||
args = [injected, ...args];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -510,10 +510,7 @@ export class Page extends EventEmitter {
|
||||
return this.mainFrame().waitForXPath(xpath, options);
|
||||
}
|
||||
|
||||
waitForFunction(pageFunction: Function, options: {
|
||||
polling?: string | number;
|
||||
timeout?: number; } = {},
|
||||
...args: any[]): Promise<js.JSHandle> {
|
||||
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
|
||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ function checkSources(sources) {
|
||||
properties = type.getProperties().map(property => serializeSymbol(property, nextCircular));
|
||||
return new Documentation.Type('Object', properties);
|
||||
}
|
||||
if (type.isUnion() && (typeName.includes('|') || type.types.every(type => type.isStringLiteral()))) {
|
||||
if (type.isUnion() && (typeName.includes('|') || type.types.every(type => type.isStringLiteral() || type.intrinsicName === 'number'))) {
|
||||
const types = type.types.map(type => serializeType(type, circular));
|
||||
const name = types.map(type => type.name).join('|');
|
||||
const properties = [].concat(...types.map(type => type.properties));
|
||||
|
@ -56,8 +56,10 @@ class MDOutline {
|
||||
const type = findType(str);
|
||||
const properties = [];
|
||||
const comment = str.substring(str.indexOf('<') + type.length + 2).trim();
|
||||
// Strings have enum values instead of properties
|
||||
if (type !== 'string' && type !== 'string|number' && type !== 'string|Array<string>' && type !== 'Array<string>') {
|
||||
const hasNonEnumProperties = type.split('|').some(part => {
|
||||
return part !== 'string' && part !== 'number' && part !== 'Array<string>' && !(part[0] === '"' && part[part.length - 1] === '"');
|
||||
});
|
||||
if (hasNonEnumProperties) {
|
||||
for (const childElement of element.querySelectorAll(':scope > ul > li')) {
|
||||
const property = parseProperty(childElement);
|
||||
property.required = property.comment.includes('***required***');
|
||||
|
Loading…
Reference in New Issue
Block a user