Skip to content

Commit

Permalink
feat(api): introduce BrowserContext.waitForEvent (#1252)
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s authored Mar 6, 2020
1 parent 8c9933e commit 9bc6dce
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 133 deletions.
17 changes: 13 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ await context.close();
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
- [browserContext.setOffline(offline)](#browsercontextsetofflineoffline)
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
- [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate)
<!-- GEN:stop -->

#### event: 'close'
Expand Down Expand Up @@ -545,6 +546,15 @@ await browserContext.setGeolocation({latitude: 59.95, longitude: 30.31667});
- `'payment-handler'`
- returns: <[Promise]>

#### browserContext.waitForEvent(event[, optionsOrPredicate])
- `event` <[string]> Event name, same one would pass into `browserContext.on(event)`.
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object.
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
- `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 [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout).
- returns: <[Promise]<[any]>> Promise which resolves to the event data value.

Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value. Will throw an error if the context closes before the event
is fired.

```js
const context = await browser.newContext();
Expand Down Expand Up @@ -1611,13 +1621,11 @@ Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, .
- `event` <[string]> Event name, same one would pass into `page.on(event)`.
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object.
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
- `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 [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]<[any]>> Promise which resolves to the event data value.

Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value.
Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value. Will throw an error if the page is closed before the event
is fired.

#### page.waitForFunction(pageFunction[, options[, ...args]])
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
Expand Down Expand Up @@ -3729,6 +3737,7 @@ const backgroundPage = await backroundPageTarget.page();
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
- [browserContext.setOffline(offline)](#browsercontextsetofflineoffline)
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
- [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate)
<!-- GEN:stop -->

#### event: 'backgroundpage'
Expand Down
75 changes: 68 additions & 7 deletions src/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
* limitations under the License.
*/

import { Page, PageBinding } from './page';
import * as network from './network';
import * as types from './types';
import { helper } from './helper';
import * as network from './network';
import { Page, PageBinding } from './page';
import * as platform from './platform';
import { TimeoutSettings } from './timeoutSettings';
import * as types from './types';
import { Events } from './events';

export type BrowserContextOptions = {
viewport?: types.Viewport | null,
Expand Down Expand Up @@ -50,15 +52,74 @@ export interface BrowserContext {
setOffline(offline: boolean): Promise<void>;
addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<void>;
exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise<any>;
close(): Promise<void>;
}

_existingPages(): Page[];
readonly _timeoutSettings: TimeoutSettings;
export abstract class BrowserContextBase extends platform.EventEmitter implements BrowserContext {
readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: BrowserContextOptions;
readonly _pageBindings: Map<string, PageBinding>;
_closed = false;
private readonly _closePromise: Promise<Error>;
private _closePromiseFulfill: ((error: Error) => void) | undefined;

constructor(options: BrowserContextOptions) {
super();
this._options = options;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
}

abstract _existingPages(): Page[];

_browserClosed() {
for (const page of this._existingPages())
page._didClose();
this._didCloseInternal();
}

_didCloseInternal() {
this._closed = true;
this.emit(Events.BrowserContext.Close);
this._closePromiseFulfill!(new Error('Context closed'));
}

// BrowserContext methods.
abstract pages(): Promise<Page[]>;
abstract newPage(): Promise<Page>;
abstract cookies(...urls: string[]): Promise<network.NetworkCookie[]>;
abstract setCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
abstract clearCookies(): Promise<void>;
abstract setPermissions(origin: string, permissions: string[]): Promise<void>;
abstract clearPermissions(): Promise<void>;
abstract setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
abstract setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
abstract setOffline(offline: boolean): Promise<void>;
abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, ...args: any[]): Promise<void>;
abstract exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
abstract close(): Promise<void>;

setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}

setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}

async waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise<any> {
if (!optionsOrPredicate)
optionsOrPredicate = {};
if (typeof optionsOrPredicate === 'function')
optionsOrPredicate = { predicate: optionsOrPredicate };
const { timeout = this._timeoutSettings.timeout(), predicate = () => true } = optionsOrPredicate;

const abortPromise = (event === Events.BrowserContext.Close) ? new Promise<Error>(() => { }) : this._closePromise;
return helper.waitForEvent(this, event, (...args: any[]) => !!predicate(...args), timeout, abortPromise);
}
}

export function assertBrowserContextIsNotOwned(context: BrowserContext) {
export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
const pages = context._existingPages();
for (const page of pages) {
if (page._ownedContext)
Expand Down
51 changes: 14 additions & 37 deletions src/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@
* limitations under the License.
*/

import { Events } from './events';
import { Events as CommonEvents } from '../events';
import { assert, helper, debugError } from '../helper';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page, PageEvent, PageBinding } from '../page';
import { CRTarget } from './crTarget';
import { Protocol } from './protocol';
import { CRPage } from './crPage';
import { Browser, createPageInNewContext } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events as CommonEvents } from '../events';
import { assert, debugError, helper } from '../helper';
import * as network from '../network';
import * as types from '../types';
import { Page, PageBinding, PageEvent } from '../page';
import * as platform from '../platform';
import { readProtocolStream } from './crProtocolHelper';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
import * as types from '../types';
import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
import { CRPage } from './crPage';
import { readProtocolStream } from './crProtocolHelper';
import { CRTarget } from './crTarget';
import { Events } from './events';
import { Protocol } from './protocol';

export class CRBrowser extends platform.EventEmitter implements Browser {
_connection: CRConnection;
Expand Down Expand Up @@ -226,21 +225,15 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
}
}

export class CRBrowserContext extends platform.EventEmitter implements BrowserContext {
export class CRBrowserContext extends BrowserContextBase {
readonly _browser: CRBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();
private _closed = false;

constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
super(options);
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
this._evaluateOnNewDocumentSources = [];
}

Expand All @@ -262,14 +255,6 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
return pages;
}

setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}

setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}

async pages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.pageOrError()));
Expand Down Expand Up @@ -385,8 +370,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._client.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(CommonEvents.BrowserContext.Close);
this._didCloseInternal();
}

async backgroundPages(): Promise<Page[]> {
Expand All @@ -398,11 +382,4 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
async createSession(page: Page): Promise<CRSession> {
return CRTarget.fromPage(page).sessionFactory();
}

_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(CommonEvents.BrowserContext.Close);
}
}
6 changes: 3 additions & 3 deletions src/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class CRPage implements PageDelegate {
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }),
];
const options = this._page.context()._options;
const options = this._browserContext._options;
if (options.bypassCSP)
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
if (options.ignoreHTTPSErrors)
Expand Down Expand Up @@ -336,7 +336,7 @@ export class CRPage implements PageDelegate {

async updateExtraHTTPHeaders(): Promise<void> {
const headers = network.mergeHeaders([
this._page.context()._options.extraHTTPHeaders,
this._browserContext._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders
]);
await this._client.send('Network.setExtraHTTPHeaders', { headers });
Expand All @@ -348,7 +348,7 @@ export class CRPage implements PageDelegate {
}

async _updateViewport(updateTouch: boolean): Promise<void> {
let viewport = this._page.context()._options.viewport || { width: 0, height: 0 };
let viewport = this._browserContext._options.viewport || { width: 0, height: 0 };
const viewportSize = this._page._state.viewportSize;
if (viewportSize)
viewport = { ...viewport, ...viewportSize };
Expand Down
2 changes: 1 addition & 1 deletion src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const frameId = await this._page._delegate.getOwnerFrame(this);
if (!frameId)
return null;
const pages = this._page.context()._existingPages();
const pages = this._page._browserContext._existingPages();
for (const page of pages) {
const frame = page._frameManager.frame(frameId);
if (frame)
Expand Down
33 changes: 9 additions & 24 deletions src/firefox/ffBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,18 @@
*/

import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import * as types from '../types';
import { Page, PageEvent, PageBinding } from '../page';
import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection';
import { FFPage } from './ffPage';
import { Page, PageBinding, PageEvent } from '../page';
import * as platform from '../platform';
import { Protocol } from './protocol';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
import * as types from '../types';
import { ConnectionEvents, FFConnection, FFSession, FFSessionEvents } from './ffConnection';
import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage';
import { Protocol } from './protocol';

export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection;
Expand Down Expand Up @@ -269,21 +268,15 @@ class Target {
}
}

export class FFBrowserContext extends platform.EventEmitter implements BrowserContext {
export class FFBrowserContext extends BrowserContextBase {
readonly _browser: FFBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
private readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();

constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
super(options);
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
this._evaluateOnNewDocumentSources = [];
}

Expand Down Expand Up @@ -412,14 +405,6 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._connection.send('Target.removeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(Events.BrowserContext.Close);
}

_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
this._didCloseInternal();
}
}
24 changes: 12 additions & 12 deletions src/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@
* limitations under the License.
*/

import * as frames from '../frames';
import { helper, RegisteredListener, debugError, assert } from '../helper';
import * as dialog from '../dialog';
import * as dom from '../dom';
import { Events } from '../events';
import * as frames from '../frames';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { Page, PageBinding, PageDelegate, Worker } from '../page';
import * as platform from '../platform';
import { kScreenshotDuringNavigationError } from '../screenshotter';
import * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility';
import { FFBrowserContext } from './ffBrowser';
import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext';
import { Page, PageDelegate, Worker, PageBinding } from '../page';
import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Events } from '../events';
import * as dialog from '../dialog';
import { Protocol } from './protocol';
import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
import { BrowserContext } from '../browserContext';
import { getAccessibilityTree } from './ffAccessibility';
import * as types from '../types';
import * as platform from '../platform';
import { kScreenshotDuringNavigationError } from '../screenshotter';

const UTILITY_WORLD_NAME = '__playwright_utility_world__';

Expand All @@ -45,7 +45,7 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();

constructor(session: FFSession, browserContext: BrowserContext, openerResolver: () => Promise<Page | null>) {
constructor(session: FFSession, browserContext: FFBrowserContext, openerResolver: () => Promise<Page | null>) {
this._session = session;
this._openerResolver = openerResolver;
this.rawKeyboard = new RawKeyboardImpl(session);
Expand Down
Loading

0 comments on commit 9bc6dce

Please sign in to comment.