mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-04 17:35:48 +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/
|
/node_modules/
|
||||||
/test/output-chromium
|
/test/output-chromium
|
||||||
/test/output-firefox
|
/test/output-firefox
|
||||||
|
/test/output-webkit
|
||||||
/test/test-user-data-dir*
|
/test/test-user-data-dir*
|
||||||
/.local-chromium/
|
/.local-chromium/
|
||||||
/.local-browser/
|
/.local-browser/
|
||||||
|
@ -1828,7 +1828,7 @@ await element.setInputFiles('/tmp/myfile.pdf');
|
|||||||
#### page.waitForFunction(pageFunction[, options[, ...args]])
|
#### page.waitForFunction(pageFunction[, options[, ...args]])
|
||||||
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||||
- `options` <[Object]> Optional waiting parameters
|
- `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.
|
- `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.
|
- `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.
|
- `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]])
|
#### frame.waitForFunction(pageFunction[, options[, ...args]])
|
||||||
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
|
||||||
- `options` <[Object]> Optional waiting parameters
|
- `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.
|
- `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.
|
- `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.
|
- `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);
|
return this.mainFrame().waitForXPath(xpath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForFunction(pageFunction: Function, options: {
|
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
|
||||||
polling?: string | number;
|
|
||||||
timeout?: number; } = {},
|
|
||||||
...args: any[]): Promise<js.JSHandle> {
|
|
||||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
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 * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||||
import { assert, helper } from './helper';
|
import { assert, helper } from './helper';
|
||||||
import Injected from './injected/injected';
|
import Injected from './injected/injected';
|
||||||
import { WaitTaskParams } from './waitTask';
|
|
||||||
|
|
||||||
export interface DOMWorldDelegate {
|
export interface DOMWorldDelegate {
|
||||||
keyboard: input.Keyboard;
|
keyboard: input.Keyboard;
|
||||||
@ -307,36 +306,57 @@ function selectorToString(selector: Selector): string {
|
|||||||
return `:scope >> ${selector.selector}`;
|
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 type WaitForSelectorOptions = { visible?: boolean, hidden?: boolean, timeout?: number };
|
||||||
|
|
||||||
export function waitForSelectorTask(selector: string, options: WaitForSelectorOptions): WaitTaskParams {
|
export function waitForSelectorTask(selector: string, options: WaitForSelectorOptions): Task {
|
||||||
const { visible: waitForVisible = false, hidden: waitForHidden = false, timeout } = options;
|
const { visible: waitForVisible = false, hidden: waitForHidden = false } = options;
|
||||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
selector = normalizeSelector(selector);
|
||||||
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;
|
|
||||||
|
|
||||||
function predicate(injected: Injected, selector: string, waitForVisible: boolean, waitForHidden: boolean): (Node | boolean) | null {
|
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, selector: string, waitForVisible: boolean, waitForHidden: boolean, timeout: number) => {
|
||||||
const element = injected.querySelector(selector, document);
|
if (waitForVisible || waitForHidden)
|
||||||
if (!element)
|
return injected.pollRaf(predicate, timeout);
|
||||||
return waitForHidden;
|
return injected.pollMutation(predicate, timeout);
|
||||||
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;
|
|
||||||
|
|
||||||
function hasVisibleBoundingBox(): boolean {
|
function predicate(): Element | boolean {
|
||||||
const rect = element.getBoundingClientRect();
|
const element = injected.querySelector(selector, document);
|
||||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
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);
|
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);
|
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 * as network from './network';
|
||||||
import { helper, assert } from './helper';
|
import { helper, assert } from './helper';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
||||||
import { WaitTaskParams, WaitTask } from './waitTask';
|
|
||||||
import { TimeoutSettings } from './TimeoutSettings';
|
import { TimeoutSettings } from './TimeoutSettings';
|
||||||
|
import { TimeoutError } from './Errors';
|
||||||
|
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ type World = {
|
|||||||
contextPromise: Promise<js.ExecutionContext>;
|
contextPromise: Promise<js.ExecutionContext>;
|
||||||
contextResolveCallback: (c: js.ExecutionContext) => void;
|
contextResolveCallback: (c: js.ExecutionContext) => void;
|
||||||
context: js.ExecutionContext | null;
|
context: js.ExecutionContext | null;
|
||||||
waitTasks: Set<WaitTask>;
|
rerunnableTasks: Set<RerunnableTask>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavigateOptions = {
|
export type NavigateOptions = {
|
||||||
@ -65,8 +65,8 @@ export class Frame {
|
|||||||
this._timeoutSettings = timeoutSettings;
|
this._timeoutSettings = timeoutSettings;
|
||||||
this._parentFrame = parentFrame;
|
this._parentFrame = parentFrame;
|
||||||
|
|
||||||
this._worlds.set('main', { 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, waitTasks: new Set() });
|
this._worlds.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
||||||
this._setContext('main', null);
|
this._setContext('main', null);
|
||||||
this._setContext('utility', null);
|
this._setContext('utility', null);
|
||||||
|
|
||||||
@ -386,8 +386,9 @@ export class Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForSelector(selector: string, options: dom.WaitForSelectorOptions = {}): Promise<dom.ElementHandle | null> {
|
async waitForSelector(selector: string, options: dom.WaitForSelectorOptions = {}): Promise<dom.ElementHandle | null> {
|
||||||
const params = dom.waitForSelectorTask(selector, { timeout: this._timeoutSettings.timeout(), ...options });
|
const task = dom.waitForSelectorTask(selector, { timeout: this._timeoutSettings.timeout(), ...options });
|
||||||
const handle = await this._scheduleWaitTask(params, 'utility');
|
const title = `selector "${selector}"${options.hidden ? ' to be hidden' : ''}`;
|
||||||
|
const handle = await this._scheduleRerunnableTask(task, 'utility', options.timeout, title);
|
||||||
if (!handle.asElement()) {
|
if (!handle.asElement()) {
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
return null;
|
return null;
|
||||||
@ -404,22 +405,10 @@ export class Frame {
|
|||||||
return this.waitForSelector('xpath=' + xpath, options);
|
return this.waitForSelector('xpath=' + xpath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForFunction(
|
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
|
||||||
pageFunction: Function | string,
|
options = { timeout: this._timeoutSettings.timeout(), ...options };
|
||||||
options: { polling?: string | number; timeout?: number; } = {},
|
const task = dom.waitForFunctionTask(pageFunction, options, ...args);
|
||||||
...args): Promise<js.JSHandle> {
|
return this._scheduleRerunnableTask(task, 'main', options.timeout);
|
||||||
const {
|
|
||||||
polling = 'raf',
|
|
||||||
timeout = this._timeoutSettings.timeout(),
|
|
||||||
} = options;
|
|
||||||
const params: WaitTaskParams = {
|
|
||||||
predicateBody: pageFunction,
|
|
||||||
title: 'function',
|
|
||||||
polling,
|
|
||||||
timeout,
|
|
||||||
args
|
|
||||||
};
|
|
||||||
return this._scheduleWaitTask(params, 'main');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
@ -435,30 +424,31 @@ export class Frame {
|
|||||||
_detach() {
|
_detach() {
|
||||||
this._detached = true;
|
this._detached = true;
|
||||||
for (const world of this._worlds.values()) {
|
for (const world of this._worlds.values()) {
|
||||||
for (const waitTask of world.waitTasks)
|
for (const rerunnableTask of world.rerunnableTasks)
|
||||||
waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||||
}
|
}
|
||||||
if (this._parentFrame)
|
if (this._parentFrame)
|
||||||
this._parentFrame._childFrames.delete(this);
|
this._parentFrame._childFrames.delete(this);
|
||||||
this._parentFrame = null;
|
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 world = this._worlds.get(worldType);
|
||||||
const task = new WaitTask(params, () => world.waitTasks.delete(task));
|
const rerunnableTask = new RerunnableTask(world, task, timeout, title);
|
||||||
world.waitTasks.add(task);
|
world.rerunnableTasks.add(rerunnableTask);
|
||||||
if (world.context)
|
if (world.context)
|
||||||
task.rerun(world.context);
|
rerunnableTask.rerun(world.context._domWorld);
|
||||||
return task.promise;
|
return rerunnableTask.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
|
private _setContext(worldType: WorldType, context: js.ExecutionContext | null) {
|
||||||
const world = this._worlds.get(worldType);
|
const world = this._worlds.get(worldType);
|
||||||
world.context = context;
|
world.context = context;
|
||||||
if (context) {
|
if (context) {
|
||||||
|
assert(context._domWorld, 'Frame context must have a dom world');
|
||||||
world.contextResolveCallback.call(null, context);
|
world.contextResolveCallback.call(null, context);
|
||||||
for (const waitTask of world.waitTasks)
|
for (const rerunnableTask of world.rerunnableTasks)
|
||||||
waitTask.rerun(context);
|
rerunnableTask.rerun(context._domWorld);
|
||||||
} else {
|
} else {
|
||||||
world.contextPromise = new Promise(fulfill => {
|
world.contextPromise = new Promise(fulfill => {
|
||||||
world.contextResolveCallback = 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();
|
append();
|
||||||
return result;
|
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;
|
export default Injected;
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import { assert } from './helper';
|
import { assert } from './helper';
|
||||||
import { NetworkManager } from './chromium/NetworkManager';
|
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
@ -50,11 +49,11 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]): Network
|
|||||||
|
|
||||||
export function rewriteCookies(cookies: SetNetworkCookieParam[]): SetNetworkCookieParam[] {
|
export function rewriteCookies(cookies: SetNetworkCookieParam[]): SetNetworkCookieParam[] {
|
||||||
return cookies.map(c => {
|
return cookies.map(c => {
|
||||||
assert(c.name, "Cookie should have a name");
|
assert(c.name, 'Cookie should have a name');
|
||||||
assert(c.value, "Cookie should have a value");
|
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 && 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.domain), 'Cookie should have either url or domain');
|
||||||
assert(!(c.url && c.path), "Cookie should have either url or domain");
|
assert(!(c.url && c.path), 'Cookie should have either url or domain');
|
||||||
const copy = {...c};
|
const copy = {...c};
|
||||||
if (copy.url) {
|
if (copy.url) {
|
||||||
assert(copy.url !== 'about:blank', `Blank page can not have cookie "${c.name}"`);
|
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);
|
return this.mainFrame().waitForXPath(xpath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForFunction(pageFunction: Function, options: {
|
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
|
||||||
polling?: string | number;
|
|
||||||
timeout?: number; } = {},
|
|
||||||
...args: any[]): Promise<js.JSHandle> {
|
|
||||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ function checkSources(sources) {
|
|||||||
properties = type.getProperties().map(property => serializeSymbol(property, nextCircular));
|
properties = type.getProperties().map(property => serializeSymbol(property, nextCircular));
|
||||||
return new Documentation.Type('Object', properties);
|
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 types = type.types.map(type => serializeType(type, circular));
|
||||||
const name = types.map(type => type.name).join('|');
|
const name = types.map(type => type.name).join('|');
|
||||||
const properties = [].concat(...types.map(type => type.properties));
|
const properties = [].concat(...types.map(type => type.properties));
|
||||||
|
@ -56,8 +56,10 @@ class MDOutline {
|
|||||||
const type = findType(str);
|
const type = findType(str);
|
||||||
const properties = [];
|
const properties = [];
|
||||||
const comment = str.substring(str.indexOf('<') + type.length + 2).trim();
|
const comment = str.substring(str.indexOf('<') + type.length + 2).trim();
|
||||||
// Strings have enum values instead of properties
|
const hasNonEnumProperties = type.split('|').some(part => {
|
||||||
if (type !== 'string' && type !== 'string|number' && type !== 'string|Array<string>' && type !== 'Array<string>') {
|
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')) {
|
for (const childElement of element.querySelectorAll(':scope > ul > li')) {
|
||||||
const property = parseProperty(childElement);
|
const property = parseProperty(childElement);
|
||||||
property.required = property.comment.includes('***required***');
|
property.required = property.comment.includes('***required***');
|
||||||
|
Loading…
Reference in New Issue
Block a user