feat: add support for async waitForTarget (#7885)

* feat: add support for async waitForTarget

* fix: add timeout

* fix: potential async bugs
This commit is contained in:
Tmk 2022-02-18 19:05:29 +08:00 committed by GitHub
parent 543a4d44bc
commit dbf0639822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 17 deletions

View File

@ -983,7 +983,7 @@ the method will return an array with all the targets in all browser contexts.
#### browser.waitForTarget(predicate[, options]) #### browser.waitForTarget(predicate[, options])
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target - `predicate` <[function]\([Target]\):[boolean]|[Promise<boolean>]> A function to be run for every target
- `options` <[Object]> - `options` <[Object]>
- `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds.
- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. - returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function.
@ -1135,7 +1135,7 @@ An array of all active targets inside the browser context.
#### browserContext.waitForTarget(predicate[, options]) #### browserContext.waitForTarget(predicate[, options])
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target - `predicate` <[function]\([Target]\):[boolean]|[Promise<boolean>]> A function to be run for every target
- `options` <[Object]> - `options` <[Object]>
- `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds.
- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. - returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function.

View File

@ -114,7 +114,7 @@ class Browser extends EventEmitter {
} }
/** /**
* @param {function(!Target):boolean} predicate * @param {function(!Target):boolean|Promise<boolean>} predicate
* @param {{timeout?: number}=} options * @param {{timeout?: number}=} options
* @return {!Promise<!Target>} * @return {!Promise<!Target>}
*/ */
@ -122,9 +122,6 @@ class Browser extends EventEmitter {
const { const {
timeout = 30000 timeout = 30000
} = options; } = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve; let resolve;
const targetPromise = new Promise(x => resolve = x); const targetPromise = new Promise(x => resolve = x);
this.on(Events.Browser.TargetCreated, check); this.on(Events.Browser.TargetCreated, check);
@ -132,7 +129,21 @@ class Browser extends EventEmitter {
try { try {
if (!timeout) if (!timeout)
return await targetPromise; return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout); return await helper.waitWithTimeout(
Promise.race([
targetPromise,
(async () => {
for (const target of this.targets()) {
if (await predicate(target)) {
return target;
}
}
await targetPromise;
})(),
]),
'target',
timeout
);
} finally { } finally {
this.removeListener(Events.Browser.TargetCreated, check); this.removeListener(Events.Browser.TargetCreated, check);
this.removeListener('targetchanged', check); this.removeListener('targetchanged', check);
@ -141,8 +152,8 @@ class Browser extends EventEmitter {
/** /**
* @param {!Target} target * @param {!Target} target
*/ */
function check(target) { async function check(target) {
if (predicate(target)) if (await predicate(target))
resolve(target); resolve(target);
} }
} }
@ -334,7 +345,7 @@ class BrowserContext extends EventEmitter {
} }
/** /**
* @param {function(Target):boolean} predicate * @param {function(Target):boolean|Promise<boolean>} predicate
* @param {{timeout?: number}=} options * @param {{timeout?: number}=} options
* @return {!Promise<Target>} * @return {!Promise<Target>}
*/ */

View File

@ -539,12 +539,10 @@ export class Browser extends EventEmitter {
* ``` * ```
*/ */
async waitForTarget( async waitForTarget(
predicate: (x: Target) => boolean, predicate: (x: Target) => boolean | Promise<boolean>,
options: WaitForTargetOptions = {} options: WaitForTargetOptions = {}
): Promise<Target> { ): Promise<Target> {
const { timeout = 30000 } = options; const { timeout = 30000 } = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget) return existingTarget;
let resolve: (value: Target | PromiseLike<Target>) => void; let resolve: (value: Target | PromiseLike<Target>) => void;
const targetPromise = new Promise<Target>((x) => (resolve = x)); const targetPromise = new Promise<Target>((x) => (resolve = x));
this.on(BrowserEmittedEvents.TargetCreated, check); this.on(BrowserEmittedEvents.TargetCreated, check);
@ -552,7 +550,17 @@ export class Browser extends EventEmitter {
try { try {
if (!timeout) return await targetPromise; if (!timeout) return await targetPromise;
return await helper.waitWithTimeout<Target>( return await helper.waitWithTimeout<Target>(
targetPromise, Promise.race([
targetPromise,
(async () => {
for (const target of this.targets()) {
if (await predicate(target)) {
return target;
}
}
await targetPromise;
})(),
]),
'target', 'target',
timeout timeout
); );
@ -561,8 +569,8 @@ export class Browser extends EventEmitter {
this.removeListener(BrowserEmittedEvents.TargetChanged, check); this.removeListener(BrowserEmittedEvents.TargetChanged, check);
} }
function check(target: Target): void { async function check(target: Target): Promise<void> {
if (predicate(target)) resolve(target); if (await predicate(target)) resolve(target);
} }
} }
@ -736,7 +744,7 @@ export class BrowserContext extends EventEmitter {
* that matches the `predicate` function. * that matches the `predicate` function.
*/ */
waitForTarget( waitForTarget(
predicate: (x: Target) => boolean, predicate: (x: Target) => boolean | Promise<boolean>,
options: { timeout?: number } = {} options: { timeout?: number } = {}
): Promise<Target> { ): Promise<Target> {
return this._browser.waitForTarget( return this._browser.waitForTarget(

View File

@ -67,6 +67,30 @@ describe('Target', function () {
).toBe('Hello world'); ).toBe('Hello world');
expect(await originalPage.$('body')).toBeTruthy(); expect(await originalPage.$('body')).toBeTruthy();
}); });
itFailsFirefox('should be able to use async waitForTarget', async () => {
const { page, server, context } = getTestState();
const [otherPage] = await Promise.all([
context
.waitForTarget((target) =>
target
.page()
.then(
(page) =>
page.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'
)
)
.then((target) => target.page()),
page.evaluate(
(url: string) => window.open(url),
server.CROSS_PROCESS_PREFIX + '/empty.html'
),
]);
expect(otherPage.url()).toEqual(
server.CROSS_PROCESS_PREFIX + '/empty.html'
);
expect(page).not.toEqual(otherPage);
});
itFailsFirefox( itFailsFirefox(
'should report when a new page is created and closed', 'should report when a new page is created and closed',
async () => { async () => {