Skip to content

Commit 9b86c63

Browse files
authored
api: make BrowserContext.pages() synchronous (#1369)
Returns all pages which have been initialized already. References #1348.
1 parent 8aba111 commit 9b86c63

15 files changed

+129
-168
lines changed

docs/api.md

+3-7
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,7 @@ const crypto = require('crypto');
452452
Creates a new page in the browser context.
453453

454454
#### browserContext.pages()
455-
- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using
456-
[chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages).
457-
458-
An array of all pages inside the browser context.
455+
- returns: <[Array]<[Page]>> All open pages in the context. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [chromiumBrowserContext.backgroundPages()](#chromiumbrowsercontextbackgroundpages).
459456

460457
#### browserContext.route(url, handler)
461458
- `url` <[string]|[RegExp]|[function]\([string]\):[boolean]> A glob pattern, regex pattern or predicate receiving [URL] to match while routing.
@@ -3804,7 +3801,7 @@ Emitted when new background page is created in the context.
38043801
Emitted when new service worker is created in the context.
38053802

38063803
#### chromiumBrowserContext.backgroundPages()
3807-
- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all existing background pages in the context.
3804+
- returns: <[Array]<[Page]>> All existing background pages in the context.
38083805

38093806
#### chromiumBrowserContext.newCDPSession(page)
38103807
- `page` <[Page]> Page to create new session for.
@@ -4011,8 +4008,7 @@ const { chromium } = require('playwright');
40114008
`--load-extension=${pathToExtension}`
40124009
]
40134010
});
4014-
const backgroundPages = await browserContext.backgroundPages();
4015-
const backgroundPage = backgroundPages[0];
4011+
const backgroundPage = browserContext.backgroundPages()[0];
40164012
// Test the background page as you would any other page.
40174013
await browserContext.close();
40184014
})();

src/browserContext.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type BrowserContextOptions = {
4141
export interface BrowserContext {
4242
setDefaultNavigationTimeout(timeout: number): void;
4343
setDefaultTimeout(timeout: number): void;
44-
pages(): Promise<Page[]>;
44+
pages(): Page[];
4545
newPage(): Promise<Page>;
4646
cookies(urls?: string | string[]): Promise<network.NetworkCookie[]>;
4747
addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
@@ -74,10 +74,8 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement
7474
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
7575
}
7676

77-
abstract _existingPages(): Page[];
78-
7977
_browserClosed() {
80-
for (const page of this._existingPages())
78+
for (const page of this.pages())
8179
page._didClose();
8280
this._didCloseInternal();
8381
}
@@ -89,7 +87,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement
8987
}
9088

9189
// BrowserContext methods.
92-
abstract pages(): Promise<Page[]>;
90+
abstract pages(): Page[];
9391
abstract newPage(): Promise<Page>;
9492
abstract cookies(...urls: string[]): Promise<network.NetworkCookie[]>;
9593
abstract addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
@@ -126,8 +124,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement
126124
}
127125

128126
export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
129-
const pages = context._existingPages();
130-
for (const page of pages) {
127+
for (const page of context.pages()) {
131128
if (page._ownedContext)
132129
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
133130
}

src/chromium/crBrowser.ts

+35-50
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
3838
readonly _defaultContext: CRBrowserContext;
3939
readonly _contexts = new Map<string, CRBrowserContext>();
4040
_targets = new Map<string, CRTarget>();
41+
readonly _firstPagePromise: Promise<void>;
42+
private _firstPageCallback = () => {};
4143

4244
private _tracingRecording = false;
4345
private _tracingPath: string | null = '';
@@ -83,6 +85,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
8385
this._session.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
8486
this._session.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
8587
this._session.on('Target.attachedToTarget', this._onAttachedToTarget.bind(this));
88+
this._firstPagePromise = new Promise(f => this._firstPageCallback = f);
8689
}
8790

8891
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
@@ -102,7 +105,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
102105
return createPageInNewContext(this, options);
103106
}
104107

105-
async _onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
108+
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
106109
const session = this._connection.session(sessionId)!;
107110
if (!CRTarget.isPageType(targetInfo.type)) {
108111
assert(targetInfo.type === 'service_worker' || targetInfo.type === 'browser' || targetInfo.type === 'other');
@@ -115,29 +118,24 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
115118
return;
116119
}
117120
const { context, target } = this._createTarget(targetInfo, session);
118-
try {
119-
switch (targetInfo.type) {
120-
case 'page': {
121-
const event = new PageEvent(context, target.pageOrError());
122-
context.emit(CommonEvents.BrowserContext.Page, event);
123-
const opener = target.opener();
124-
if (!opener)
125-
break;
126-
const openerPage = await opener.pageOrError();
127-
if (openerPage instanceof Page && !openerPage.isClosed())
128-
openerPage.emit(CommonEvents.Page.Popup, new PageEvent(context, target.pageOrError()));
129-
break;
130-
}
131-
case 'background_page': {
132-
const event = new PageEvent(context, target.pageOrError());
133-
context.emit(Events.CRBrowserContext.BackgroundPage, event);
134-
break;
135-
}
121+
122+
if (!CRTarget.isPageType(targetInfo.type))
123+
return;
124+
const pageEvent = new PageEvent(context, target.pageOrError());
125+
target.pageOrError().then(async () => {
126+
if (targetInfo.type === 'page') {
127+
this._firstPageCallback();
128+
context.emit(CommonEvents.BrowserContext.Page, pageEvent);
129+
const opener = target.opener();
130+
if (!opener)
131+
return;
132+
const openerPage = await opener.pageOrError();
133+
if (openerPage instanceof Page && !openerPage.isClosed())
134+
openerPage.emit(CommonEvents.Page.Popup, pageEvent);
135+
} else if (targetInfo.type === 'background_page') {
136+
context.emit(Events.CRBrowserContext.BackgroundPage, pageEvent);
136137
}
137-
} catch (e) {
138-
// Do not dispatch the event if initialization failed.
139-
debugError(e);
140-
}
138+
});
141139
}
142140

143141
async _targetCreated({targetInfo}: Protocol.Target.targetCreatedPayload) {
@@ -176,10 +174,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
176174
await this._session.send('Target.closeTarget', { targetId: CRTarget.fromPage(page)._targetId });
177175
}
178176

179-
_allTargets(): CRTarget[] {
180-
return Array.from(this._targets.values());
181-
}
182-
183177
async close() {
184178
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
185179
await Promise.all(this.contexts().map(context => context.close()));
@@ -268,19 +262,12 @@ export class CRBrowserContext extends BrowserContextBase {
268262
await this.setHTTPCredentials(this._options.httpCredentials);
269263
}
270264

271-
_existingPages(): Page[] {
272-
const pages: Page[] = [];
273-
for (const target of this._browser._allTargets()) {
274-
if (target.context() === this && target._crPage)
275-
pages.push(target._crPage.page());
276-
}
277-
return pages;
265+
_targets(): CRTarget[] {
266+
return Array.from(this._browser._targets.values()).filter(target => target.context() === this);
278267
}
279268

280-
async pages(): Promise<Page[]> {
281-
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
282-
const pages = await Promise.all(targets.map(target => target.pageOrError()));
283-
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
269+
pages(): Page[] {
270+
return this._targets().filter(target => target.type() === 'page').map(target => target._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
284271
}
285272

286273
async newPage(): Promise<Page> {
@@ -351,51 +338,51 @@ export class CRBrowserContext extends BrowserContextBase {
351338
if (geolocation)
352339
geolocation = verifyGeolocation(geolocation);
353340
this._options.geolocation = geolocation || undefined;
354-
for (const page of this._existingPages())
341+
for (const page of this.pages())
355342
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
356343
}
357344

358345
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
359346
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
360-
for (const page of this._existingPages())
347+
for (const page of this.pages())
361348
await (page._delegate as CRPage).updateExtraHTTPHeaders();
362349
}
363350

364351
async setOffline(offline: boolean): Promise<void> {
365352
this._options.offline = offline;
366-
for (const page of this._existingPages())
353+
for (const page of this.pages())
367354
await (page._delegate as CRPage)._networkManager.setOffline(offline);
368355
}
369356

370357
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
371358
this._options.httpCredentials = httpCredentials || undefined;
372-
for (const page of this._existingPages())
359+
for (const page of this.pages())
373360
await (page._delegate as CRPage)._networkManager.authenticate(httpCredentials);
374361
}
375362

376363
async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) {
377364
const source = await helper.evaluationScript(script, args);
378365
this._evaluateOnNewDocumentSources.push(source);
379-
for (const page of this._existingPages())
366+
for (const page of this.pages())
380367
await (page._delegate as CRPage).evaluateOnNewDocument(source);
381368
}
382369

383370
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
384-
for (const page of this._existingPages()) {
371+
for (const page of this.pages()) {
385372
if (page._pageBindings.has(name))
386373
throw new Error(`Function "${name}" has been already registered in one of the pages`);
387374
}
388375
if (this._pageBindings.has(name))
389376
throw new Error(`Function "${name}" has been already registered`);
390377
const binding = new PageBinding(name, playwrightFunction);
391378
this._pageBindings.set(name, binding);
392-
for (const page of this._existingPages())
379+
for (const page of this.pages())
393380
await (page._delegate as CRPage).exposeBinding(binding);
394381
}
395382

396383
async route(url: types.URLMatch, handler: network.RouteHandler): Promise<void> {
397384
this._routes.push({ url, handler });
398-
for (const page of this._existingPages())
385+
for (const page of this.pages())
399386
await (page._delegate as CRPage).updateRequestInterception();
400387
}
401388

@@ -413,10 +400,8 @@ export class CRBrowserContext extends BrowserContextBase {
413400
this._didCloseInternal();
414401
}
415402

416-
async backgroundPages(): Promise<Page[]> {
417-
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'background_page');
418-
const pages = await Promise.all(targets.map(target => target.pageOrError()));
419-
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
403+
backgroundPages(): Page[] {
404+
return this._targets().filter(target => target.type() === 'background_page').map(target => target._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
420405
}
421406

422407
async newCDPSession(page: Page): Promise<CRSession> {

src/chromium/crPage.ts

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const UTILITY_WORLD_NAME = '__playwright_utility_world__';
4343

4444
export class CRPage implements PageDelegate {
4545
_client: CRSession;
46+
_initialized = false;
4647
private readonly _page: Page;
4748
readonly _networkManager: CRNetworkManager;
4849
private _contextIdToContext = new Map<number, dom.FrameExecutionContext>();
@@ -143,6 +144,7 @@ export class CRPage implements PageDelegate {
143144
promises.push(this.evaluateOnNewDocument(source));
144145
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
145146
await Promise.all(promises);
147+
this._initialized = true;
146148
}
147149

148150
didClose() {

src/chromium/crTarget.ts

+6
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ export class CRTarget {
7070
this._crPage.didClose();
7171
}
7272

73+
_initializedPage(): Page | null {
74+
if (this._crPage && this._crPage._initialized)
75+
return this._crPage.page();
76+
return null;
77+
}
78+
7379
async pageOrError(): Promise<Page | Error> {
7480
if (CRTarget.isPageType(this.type()))
7581
return this._pagePromise!;

src/dom.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
160160
const frameId = await this._page._delegate.getOwnerFrame(this);
161161
if (!frameId)
162162
return null;
163-
const pages = this._page._browserContext._existingPages();
164-
for (const page of pages) {
163+
for (const page of this._page._browserContext.pages()) {
165164
const frame = page._frameManager.frame(frameId);
166165
if (frame)
167166
return frame;

src/firefox/ffBrowser.ts

+13-18
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
119119
ffPage.didClose();
120120
}
121121

122-
async _onAttachedToTarget(payload: Protocol.Browser.attachedToTargetPayload) {
122+
_onAttachedToTarget(payload: Protocol.Browser.attachedToTargetPayload) {
123123
const {targetId, browserContextId, openerId, type} = payload.targetInfo;
124124
assert(type === 'page');
125125
const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext;
@@ -129,15 +129,15 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
129129
this._ffPages.set(targetId, ffPage);
130130

131131
const pageEvent = new PageEvent(context, ffPage.pageOrError());
132-
context.emit(Events.BrowserContext.Page, pageEvent);
133-
134-
ffPage.pageOrError().then(() => this._firstPageCallback());
135-
136-
if (!opener)
137-
return;
138-
const openerPage = await opener.pageOrError();
139-
if (openerPage instanceof Page && !openerPage.isClosed())
140-
openerPage.emit(Events.Page.Popup, pageEvent);
132+
ffPage.pageOrError().then(async () => {
133+
this._firstPageCallback();
134+
context.emit(Events.BrowserContext.Page, pageEvent);
135+
if (!opener)
136+
return;
137+
const openerPage = await opener.pageOrError();
138+
if (openerPage instanceof Page && !openerPage.isClosed())
139+
openerPage.emit(Events.Page.Popup, pageEvent);
140+
});
141141
}
142142

143143
async close() {
@@ -182,10 +182,6 @@ export class FFBrowserContext extends BrowserContextBase {
182182
return Array.from(this._browser._ffPages.values()).filter(ffPage => ffPage._browserContext === this);
183183
}
184184

185-
_existingPages(): Page[] {
186-
return this._ffPages().map(ffPage => ffPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
187-
}
188-
189185
setDefaultNavigationTimeout(timeout: number) {
190186
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
191187
}
@@ -194,9 +190,8 @@ export class FFBrowserContext extends BrowserContextBase {
194190
this._timeoutSettings.setDefaultTimeout(timeout);
195191
}
196192

197-
async pages(): Promise<Page[]> {
198-
const pagesOrErrors = await Promise.all(this._ffPages().map(ffPage => ffPage.pageOrError()));
199-
return pagesOrErrors.filter(pageOrError => pageOrError instanceof Page && !pageOrError.isClosed()) as Page[];
193+
pages(): Page[] {
194+
return this._ffPages().map(ffPage => ffPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[];
200195
}
201196

202197
async newPage(): Promise<Page> {
@@ -279,7 +274,7 @@ export class FFBrowserContext extends BrowserContextBase {
279274
}
280275

281276
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
282-
for (const page of this._existingPages()) {
277+
for (const page of this.pages()) {
283278
if (page._pageBindings.has(name))
284279
throw new Error(`Function "${name}" has been already registered in one of the pages`);
285280
}

src/server/chromium.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,8 @@ export class Chromium implements BrowserType {
6666
const { timeout = 30000 } = options || {};
6767
const { transport } = await this._launchServer(options, 'persistent', userDataDir);
6868
const browser = await CRBrowser.connect(transport!, true);
69-
const browserContext = browser._defaultContext;
70-
71-
function targets() {
72-
return browser._allTargets().filter(target => target.context() === browserContext && target.type() === 'page');
73-
}
74-
const firstTarget = targets().length ? Promise.resolve() : new Promise(f => browserContext.once('page', f));
75-
const firstPage = firstTarget.then(() => targets()[0].pageOrError());
76-
await helper.waitWithTimeout(firstPage, 'first page', timeout);
77-
return browserContext;
69+
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
70+
return browser._defaultContext;
7871
}
7972

8073
private async _launchServer(options: LaunchOptions = {}, launchType: LaunchType, userDataDir?: string, port?: number): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> {

0 commit comments

Comments
 (0)