Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: browser.waitForTarget #3356

Merged
merged 3 commits into from
Oct 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
* [browser.targets()](#browsertargets)
* [browser.userAgent()](#browseruseragent)
* [browser.version()](#browserversion)
* [browser.waitForTarget(predicate[, options])](#browserwaitfortargetpredicate-options)
* [browser.wsEndpoint()](#browserwsendpoint)
- [class: BrowserContext](#class-browsercontext)
* [event: 'targetchanged'](#event-targetchanged-1)
Expand All @@ -65,6 +66,7 @@
* [browserContext.overridePermissions(origin, permissions)](#browsercontextoverridepermissionsorigin-permissions)
* [browserContext.pages()](#browsercontextpages)
* [browserContext.targets()](#browsercontexttargets)
* [browserContext.waitForTarget(predicate[, options])](#browsercontextwaitfortargetpredicate-options)
- [class: Page](#class-page)
* [event: 'close'](#event-close)
* [event: 'console'](#event-console)
Expand Down Expand Up @@ -699,6 +701,20 @@ the method will return an array with all the targets in all browser contexts.

> **NOTE** the format of browser.version() might change with future releases of Chromium.

#### browser.waitForTarget(predicate[, options])
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target
- `options` <[Object]>
- `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.

JoelEinbinder marked this conversation as resolved.
Show resolved Hide resolved
This searches for a target in all browser contexts.

An example of finding a target for a page opened via `window.open`:
```js
await page.evaluate(() => window.open('https://www.example.com/'));
const newWindowTarget = await browser.waitForTarget(target => target.url() === 'https://www.example.com/');
```

#### browser.wsEndpoint()
- returns: <[string]> Browser websocket url.

Expand Down Expand Up @@ -823,6 +839,20 @@ An array of all pages inside the browser context.

An array of all active targets inside the browser context.

#### browserContext.waitForTarget(predicate[, options])
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target
- `options` <[Object]>
- `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.
JoelEinbinder marked this conversation as resolved.
Show resolved Hide resolved

This searches for a target in this specific browser context.

An example of finding a target for a page opened via `window.open`:
```js
await page.evaluate(() => window.open('https://www.example.com/'));
const newWindowTarget = await browserContext.waitForTarget(target => target.url() === 'https://www.example.com/');
```

### class: Page

* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
Expand Down
41 changes: 41 additions & 0 deletions lib/Browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,39 @@ class Browser extends EventEmitter {
return this.targets().find(target => target.type() === 'browser');
}

/**
* @param {function(!Target):boolean} predicate
* @param {{timeout?: number}=} options
*/
async waitForTarget(predicate, options = {}) {
const {
timeout = 30000
} = options;
const existingTarget = this.targets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve;
const targetPromise = new Promise(x => resolve = x);
this.on(Browser.Events.TargetCreated, check);
this.on(Browser.Events.TargetChanged, check);
try {
if (!timeout)
return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally {
this.removeListener(Browser.Events.TargetCreated, check);
this.removeListener(Browser.Events.TargetChanged, check);
}

/**
* @param {!Target} target
*/
function check(target) {
JoelEinbinder marked this conversation as resolved.
Show resolved Hide resolved
if (predicate(target))
resolve(target);
}
}

/**
* @return {!Promise<!Array<!Puppeteer.Page>>}
*/
Expand Down Expand Up @@ -262,6 +295,14 @@ class BrowserContext extends EventEmitter {
return this._browser.targets().filter(target => target.browserContext() === this);
}

/**
* @param {function(!Target):boolean} predicate
* @param {{timeout?: number}=} options
*/
waitForTarget(predicate, options) {
return this._browser.waitForTarget(target => target.browserContext() === this && predicate(target), options);
}

/**
* @return {!Promise<!Array<!Puppeteer.Page>>}
*/
Expand Down
19 changes: 19 additions & 0 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,25 @@ class Helper {
}
return promise;
}

/**
* @template T
* @param {!Promise<T>} promise
* @param {string} taskName
* @param {number} timeout
* @return {!Promise<T>}
*/
static async waitWithTimeout(promise, taskName, timeout) {
let reject;
const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`);
const timeoutPromise = new Promise((resolve, x) => reject = x);
const timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutTimer);
}
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions test/browsercontext.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const utils = require('./utils');
const puppeteer = utils.requireRoot('index');
const {TimeoutError} = utils.requireRoot('Errors');

module.exports.addTests = function({testRunner, expect}) {
const {describe, xdescribe, fdescribe} = testRunner;
Expand Down Expand Up @@ -79,6 +80,24 @@ module.exports.addTests = function({testRunner, expect}) {
]);
await context.close();
});
it('should wait for a target', async function({browser, server}) {
const context = await browser.createIncognitoBrowserContext();
let resolved = false;
const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE);
targetPromise.then(() => resolved = true);
const page = await context.newPage();
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
const target = await targetPromise;
expect(await target.page()).toBe(page);
await context.close();
});
it('should timeout waiting for a non-existent target', async function({browser, server}) {
const context = await browser.createIncognitoBrowserContext();
const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e);
expect(error).toBeInstanceOf(TimeoutError);
await context.close();
});
it('should isolate localStorage and cookies', async function({browser, server}) {
// Create two incognito contexts.
const context1 = await browser.createIncognitoBrowserContext();
Expand Down
2 changes: 1 addition & 1 deletion test/target.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ module.exports.addTests = function({testRunner, expect}) {
server.waitForRequest('/one-style.css')
]);
// Connect to the opened page.
const target = context.targets().find(target => target.url().includes('one-style.html'));
const target = await context.waitForTarget(target => target.url().includes('one-style.html'));
const newPage = await target.page();
// Issue a redirect.
serverResponse.writeHead(302, { location: '/injectedstyle.css' });
Expand Down