diff --git a/src/dom.ts b/src/dom.ts index 8f65bee20c..f2aee71297 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -377,7 +377,7 @@ function normalizeSelector(selector: string): string { export type Task = (context: FrameExecutionContext) => Promise; -export function waitForFunctionTask(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) { +export function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) { const { polling = 'raf' } = options; if (helper.isString(polling)) assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); @@ -386,33 +386,40 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty else throw new Error('Unknown polling options: ' + polling); const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)'; + if (selector !== undefined) + selector = normalizeSelector(selector); - return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { - const predicate = new Function('...args', predicateBody); + return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string | undefined, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { + const innerPredicate = new Function('...args', predicateBody); if (polling === 'raf') - return injected.pollRaf(predicate, timeout, ...args); + return injected.pollRaf(selector, predicate, timeout); if (polling === 'mutation') - return injected.pollMutation(predicate, timeout, ...args); - return injected.pollInterval(polling, predicate, timeout, ...args); - }, await context._injected(), predicateBody, polling, options.timeout, ...args); + return injected.pollMutation(selector, predicate, timeout); + return injected.pollInterval(selector, polling, predicate, timeout); + + function predicate(element: Element | undefined): any { + if (selector === undefined) + return innerPredicate(...args); + return innerPredicate(element, ...args); + } + }, await context._injected(), selector, predicateBody, polling, options.timeout, ...args); } export function waitForSelectorTask(selector: string, visibility: types.Visibility | undefined, timeout: number): Task { return async (context: FrameExecutionContext) => { selector = normalizeSelector(selector); - return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number, scope?: Node) => { + return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { if (visibility !== 'any') - return injected.pollRaf(predicate, timeout); - return injected.pollMutation(predicate, timeout); + return injected.pollRaf(selector, predicate, timeout); + return injected.pollMutation(selector, predicate, timeout); - function predicate(): Element | boolean { - const element = injected.querySelector(selector, scope || document); + function predicate(element: Element | undefined): Element | boolean { if (!element) return visibility === 'hidden'; if (visibility === 'any') return element; return injected.isVisible(element) === (visibility === 'visible') ? element : false; } - }, await context._injected(), selector, visibility, timeout, undefined); + }, await context._injected(), selector, visibility, timeout); }; } diff --git a/src/frames.ts b/src/frames.ts index 7777255c9e..f49f9b0ebf 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -701,9 +701,15 @@ export class Frame { return result.asElement() as dom.ElementHandle; } - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise { - options = { timeout: this._page._timeoutSettings.timeout(), ...options }; - const task = dom.waitForFunctionTask(pageFunction, options, ...args); + waitForFunction(pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise { + options = { timeout: this._page._timeoutSettings.timeout(), ...(options || {}) }; + const task = dom.waitForFunctionTask(undefined, pageFunction, options, ...args); + return this._scheduleRerunnableTask(task, 'main', options.timeout); + } + + async $wait(selector: string, pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise { + options = { timeout: this._page._timeoutSettings.timeout(), ...(options || {}) }; + const task = dom.waitForFunctionTask(selector, pageFunction, options, ...args); return this._scheduleRerunnableTask(task, 'main', options.timeout); } diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 885a64ea4c..663f0f5e42 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -30,6 +30,7 @@ function createAttributeEngine(attribute: string): SelectorEngine { } type ParsedSelector = { engine: SelectorEngine, selector: string }[]; +type Predicate = (element: Element | undefined) => any; class Injected { readonly utils: Utils; @@ -129,23 +130,26 @@ class Injected { return !!(rect.top || rect.bottom || rect.width || rect.height); } - pollMutation(predicate: Function, timeout: number, ...args: any[]): Promise { + pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); - const success = predicate.apply(null, args); + const element = selector === undefined ? undefined : this.querySelector(selector, document); + const success = predicate(element); if (success) return Promise.resolve(success); let fulfill: (result?: any) => void; const result = new Promise(x => fulfill = x); - const observer = new MutationObserver(mutations => { + const observer = new MutationObserver(() => { if (timedOut) { observer.disconnect(); fulfill(); + return; } - const success = predicate.apply(null, args); + const element = selector === undefined ? undefined : this.querySelector(selector, document); + const success = predicate(element); if (success) { observer.disconnect(); fulfill(success); @@ -159,50 +163,53 @@ class Injected { return result; } - pollRaf(predicate: Function, timeout: number, ...args: any[]): Promise { + pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); let fulfill: (result?: any) => void; const result = new Promise(x => fulfill = x); - onRaf(); - return result; - function onRaf() { + const onRaf = () => { if (timedOut) { fulfill(); return; } - const success = predicate.apply(null, args); + const element = selector === undefined ? undefined : this.querySelector(selector, document); + const success = predicate(element); if (success) fulfill(success); else requestAnimationFrame(onRaf); - } + }; + + onRaf(); + return result; } - pollInterval(pollInterval: number, predicate: Function, timeout: number, ...args: any[]): Promise { + pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) setTimeout(() => timedOut = true, timeout); let fulfill: (result?: any) => void; const result = new Promise(x => fulfill = x); - onTimeout(); - return result; - - function onTimeout() { + const onTimeout = () => { if (timedOut) { fulfill(); return; } - const success = predicate.apply(null, args); + const element = selector === undefined ? undefined : this.querySelector(selector, document); + const success = predicate(element); if (success) fulfill(success); else setTimeout(onTimeout, pollInterval); - } + }; + + onTimeout(); + return result; } } diff --git a/src/page.ts b/src/page.ts index c14946be5d..c8570a76c7 100644 --- a/src/page.ts +++ b/src/page.ts @@ -445,7 +445,11 @@ export class Page extends EventEmitter { return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); } - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise { + async waitForFunction(pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } + + async $wait(selector: string, pageFunction: Function | string, options?: types.WaitForFunctionOptions, ...args: any[]): Promise { + return this.mainFrame().$wait(selector, pageFunction, options, ...args); + } } diff --git a/test/page.spec.js b/test/page.spec.js index bb12ccc2b7..e95fb96a19 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -169,7 +169,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF expect(await page.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false); }); - it.skip(WEBKIT)('should not treat navigations as new popups', async({page, server}) => { + it.skip(WEBKIT || FFOX)('should not treat navigations as new popups', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.setContent('yo'); const [popup] = await Promise.all([ diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 929b0e597e..f2671f0de6 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -205,6 +205,36 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO }); }); + describe('Frame.$wait', function() { + it('should accept arguments', async({page, server}) => { + await page.setContent('
'); + const result = await page.$wait('div', (e, foo, bar) => e.nodeName + foo + bar, {}, 'foo1', 'bar2'); + expect(await result.jsonValue()).toBe('DIVfoo1bar2'); + }); + it('should query selector constantly', async({page, server}) => { + await page.setContent('
'); + let done = null; + const resultPromise = page.$wait('span', e => e).then(r => done = r); + expect(done).toBe(null); + await page.setContent('
'); + expect(done).toBe(null); + await page.setContent('text'); + await resultPromise; + expect(done).not.toBe(null); + expect(await done.evaluate(e => e.textContent)).toBe('text'); + }); + it('should be able to wait for removal', async({page}) => { + await page.setContent('
'); + let done = null; + const resultPromise = page.$wait('div', e => !e).then(r => done = r); + expect(done).toBe(null); + await page.setContent('
'); + await resultPromise; + expect(done).not.toBe(null); + expect(await done.jsonValue()).toBe(true); + }); + }); + describe('Frame.waitForSelector', function() { const addElement = tag => document.body.appendChild(document.createElement(tag));