From e124d44a550952a10347d5be8810e46238a7f641 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 3 Dec 2019 10:51:41 -0800 Subject: [PATCH] chore: rework wait task to accept arbitrary task on dom world (#126) --- .gitignore | 1 + docs/api.md | 4 +- src/chromium/Page.ts | 5 +- src/dom.ts | 76 +++++--- src/firefox/Page.ts | 2 +- src/frames.ts | 132 ++++++++++---- src/injected/injected.ts | 76 ++++++++ src/network.ts | 11 +- src/waitTask.ts | 187 -------------------- src/webkit/Page.ts | 5 +- utils/doclint/check_public_api/JSBuilder.js | 2 +- utils/doclint/check_public_api/MDBuilder.js | 6 +- 12 files changed, 241 insertions(+), 266 deletions(-) delete mode 100644 src/waitTask.ts diff --git a/.gitignore b/.gitignore index 4f873650eb..c2ae7f3ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /node_modules/ /test/output-chromium /test/output-firefox +/test/output-webkit /test/test-user-data-dir* /.local-chromium/ /.local-browser/ diff --git a/docs/api.md b/docs/api.md index 57445e37bc..b9b837d2e1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. diff --git a/src/chromium/Page.ts b/src/chromium/Page.ts index 86f8ca697d..2123365e60 100644 --- a/src/chromium/Page.ts +++ b/src/chromium/Page.ts @@ -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 { + waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } } diff --git a/src/dom.ts b/src/dom.ts index e2e49efd2f..eb35a67f2c 100644 --- a/src/dom.ts +++ b/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; + +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); } diff --git a/src/firefox/Page.ts b/src/firefox/Page.ts index f76bab0024..fa3a07870a 100644 --- a/src/firefox/Page.ts +++ b/src/firefox/Page.ts @@ -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 { + waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args): Promise { return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args); } diff --git a/src/frames.ts b/src/frames.ts index 33128fd4c6..28e1c3d3b5 100644 --- a/src/frames.ts +++ b/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; contextResolveCallback: (c: js.ExecutionContext) => void; context: js.ExecutionContext | null; - waitTasks: Set; + rerunnableTasks: Set; }; 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 { - 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 { - 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 { + options = { timeout: this._timeoutSettings.timeout(), ...options }; + const task = dom.waitForFunctionTask(pageFunction, options, ...args); + return this._scheduleRerunnableTask(task, 'main', options.timeout); } async title(): Promise { @@ -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 { + private _scheduleRerunnableTask(task: dom.Task, worldType: WorldType, timeout?: number, title?: string): Promise { 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; + 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((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); + } +} diff --git a/src/injected/injected.ts b/src/injected/injected.ts index f6ed020362..1ca9fcc0c2 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -81,6 +81,82 @@ class Injected { append(); return result; } + + pollMutation(predicate: Function, timeout: number, ...args: any[]): Promise { + 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 { + 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 { + 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; diff --git a/src/network.ts b/src/network.ts index 27c6707b5c..d8ec7c4e2d 100644 --- a/src/network.ts +++ b/src/network.ts @@ -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}"`); diff --git a/src/waitTask.ts b/src/waitTask.ts deleted file mode 100644 index 6b4a183857..0000000000 --- a/src/waitTask.ts +++ /dev/null @@ -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; - 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((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 { - 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 { - 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 { - 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 { - 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); - } - } -} diff --git a/src/webkit/Page.ts b/src/webkit/Page.ts index 6a78435d1c..730508fd53 100644 --- a/src/webkit/Page.ts +++ b/src/webkit/Page.ts @@ -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 { + waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } } diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index 860c77d30f..436f1102c2 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -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)); diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index 2a349b0e00..ee5181d0cc 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -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' && type !== 'Array') { + const hasNonEnumProperties = type.split('|').some(part => { + return part !== 'string' && part !== 'number' && part !== 'Array' && !(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***');