chore: simplify page initialization logic across browser types (#34002)

This commit is contained in:
Dmitry Gozman 2024-12-14 20:15:58 +00:00 committed by GitHub
parent 1e4239f48d
commit f713d3adaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 102 additions and 191 deletions

View file

@ -22,7 +22,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext } from '../browserContext';
import type { SdkObject } from '../instrumentation';
import * as network from '../network';
import type { InitScript, Page, PageDelegate } from '../page';
import type { InitScript, Page } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
@ -99,8 +99,8 @@ export class BidiBrowser extends Browser {
browser._defaultContext = new BidiBrowserContext(browser, undefined, options.persistent);
await (browser._defaultContext as BidiBrowserContext)._initialize();
// Create default page as we cannot get access to the existing one.
const pageDelegate = await browser._defaultContext.newPageDelegate();
await pageDelegate.pageOrError();
const page = await browser._defaultContext.doCreateNewPage();
await page.waitForInitializedOrError();
}
return browser;
}
@ -207,21 +207,17 @@ export class BidiBrowserContext extends BrowserContext {
return [...this._browser._bidiPages.values()].filter(bidiPage => bidiPage._browserContext === this);
}
pages(): Page[] {
return this._bidiPages().map(bidiPage => bidiPage._initializedPage).filter(Boolean) as Page[];
override possiblyUninitializedPages(): Page[] {
return this._bidiPages().map(bidiPage => bidiPage._page);
}
pagesOrErrors() {
return this._bidiPages().map(bidiPage => bidiPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
override async doCreateNewPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { context } = await this._browser._browserSession.send('browsingContext.create', {
type: bidi.BrowsingContext.CreateType.Window,
userContext: this._browserContextId,
});
return this._browser._bidiPages.get(context)!;
return this._browser._bidiPages.get(context)!._page;
}
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {

View file

@ -43,7 +43,6 @@ export class BidiPage implements PageDelegate {
readonly rawKeyboard: RawKeyboardImpl;
readonly rawTouchscreen: RawTouchscreenImpl;
readonly _page: Page;
private readonly _pagePromise: Promise<Page | Error>;
readonly _session: BidiSession;
readonly _opener: BidiPage | null;
private readonly _realmToContext: Map<string, dom.FrameExecutionContext>;
@ -51,7 +50,6 @@ export class BidiPage implements PageDelegate {
readonly _browserContext: BidiBrowserContext;
readonly _networkManager: BidiNetworkManager;
private readonly _pdf: BidiPDF;
_initializedPage: Page | null = null;
private _initScriptIds: string[] = [];
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
@ -81,16 +79,10 @@ export class BidiPage implements PageDelegate {
];
// Initialize main frame.
this._pagePromise = this._initialize().finally(async () => {
await this._page.initOpener(this._opener);
}).then(() => {
this._initializedPage = this._page;
this._page.reportAsNew();
return this._page;
}).catch(e => {
this._page.reportAsNew(e);
return e;
});
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
this._initialize().then(
() => this._page.reportAsNew(this._opener?._page),
error => this._page.reportAsNew(this._opener?._page, error));
}
private async _initialize() {
@ -109,21 +101,12 @@ export class BidiPage implements PageDelegate {
return Promise.all(this._page.allInitScripts().map(initScript => this.addInitScript(initScript)));
}
potentiallyUninitializedPage(): Page {
return this._page;
}
didClose() {
this._session.dispose();
eventsHelper.removeEventListeners(this._sessionListeners);
this._page._didClose();
}
async pageOrError(): Promise<Page | Error> {
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
return this._pagePromise;
}
private _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame {
return this._page._frameManager.frameAttached(frameId, parentFrameId);
}
@ -372,7 +355,7 @@ export class BidiPage implements PageDelegate {
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
if (event.channel !== kPlaywrightBindingChannel)
return;
const pageOrError = await this.pageOrError();
const pageOrError = await this._page.waitForInitializedOrError();
if (pageOrError instanceof Error)
return;
const context = this._realmToContext.get(event.source.realm);

View file

@ -24,7 +24,6 @@ import type * as frames from './frames';
import { helper } from './helper';
import * as network from './network';
import { InitScript } from './page';
import type { PageDelegate } from './page';
import { Page, PageBinding } from './page';
import type { Progress, ProgressController } from './progress';
import type { Selectors } from './selectors';
@ -257,10 +256,13 @@ export abstract class BrowserContext extends SdkObject {
this.emit(BrowserContext.Events.Close);
}
pages(): Page[] {
return this.possiblyUninitializedPages().filter(page => page.initializedOrUndefined());
}
// BrowserContext methods.
abstract pages(): Page[];
abstract pagesOrErrors(): Promise<Page | Error>[];
abstract newPageDelegate(): Promise<PageDelegate>;
abstract possiblyUninitializedPages(): Page[];
abstract doCreateNewPage(): Promise<Page>;
abstract addCookies(cookies: channels.SetNetworkCookie[]): Promise<void>;
abstract setGeolocation(geolocation?: types.Geolocation): Promise<void>;
abstract setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void>;
@ -359,38 +361,34 @@ export abstract class BrowserContext extends SdkObject {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async _loadDefaultContextAsIs(progress: Progress): Promise<Page> {
let pageOrError;
if (!this.pagesOrErrors().length) {
async _loadDefaultContextAsIs(progress: Progress): Promise<Page | undefined> {
if (!this.possiblyUninitializedPages().length) {
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
progress.cleanupWhenAborted(() => waitForEvent.dispose);
// Race against BrowserContext.close
pageOrError = await Promise.race([
waitForEvent.promise as Promise<Page>,
this._closePromise,
]);
// Consider Page initialization errors
if (pageOrError instanceof Page)
pageOrError = await pageOrError._delegate.pageOrError();
} else {
pageOrError = await this.pagesOrErrors()[0];
await Promise.race([waitForEvent.promise, this._closePromise]);
}
const page = this.possiblyUninitializedPages()[0];
if (!page)
return;
const pageOrError = await page.waitForInitializedOrError();
if (pageOrError instanceof Error)
throw pageOrError;
await pageOrError.mainFrame()._waitForLoadState(progress, 'load');
return pageOrError;
await page.mainFrame()._waitForLoadState(progress, 'load');
return page;
}
async _loadDefaultContext(progress: Progress) {
const defaultPage = await this._loadDefaultContextAsIs(progress);
if (!defaultPage)
return;
const browserName = this._browser.options.name;
if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) {
// Workaround for:
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
const oldPage = defaultPage;
await this.newPage(progress.metadata);
await oldPage.close(progress.metadata);
await defaultPage.close(progress.metadata);
}
}
@ -488,10 +486,10 @@ export abstract class BrowserContext extends SdkObject {
}
async newPage(metadata: CallMetadata): Promise<Page> {
const pageDelegate = await this.newPageDelegate();
const page = await this.doCreateNewPage();
if (metadata.isServerSide)
pageDelegate.potentiallyUninitializedPage().markAsServerSideOnly();
const pageOrError = await pageDelegate.pageOrError();
page.markAsServerSideOnly();
const pageOrError = await page.waitForInitializedOrError();
if (pageOrError instanceof Page) {
if (pageOrError.isClosed())
throw new Error('Page has been closed.');

View file

@ -21,7 +21,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert, createGuid } from '../../utils';
import * as network from '../network';
import type { InitScript, PageDelegate, Worker } from '../page';
import type { InitScript, Worker } from '../page';
import { Page } from '../page';
import { Frame } from '../frames';
import type { Dialog } from '../dialog';
@ -146,7 +146,7 @@ export class CRBrowser extends Browser {
}
async _waitForAllPagesToBeInitialized() {
await Promise.all([...this._crPages.values()].map(page => page.pageOrError()));
await Promise.all([...this._crPages.values()].map(crPage => crPage._page.waitForInitializedOrError()));
}
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }: Protocol.Target.attachedToTargetPayload) {
@ -259,10 +259,10 @@ export class CRBrowser extends Browser {
}
page.willBeginDownload();
let originPage = page._initializedPage;
let originPage = page._page.initializedOrUndefined();
// If it's a new window download, report it on the opener page.
if (!originPage && page._opener)
originPage = page._opener._initializedPage;
originPage = page._opener._page.initializedOrUndefined();
if (!originPage)
return;
this._downloadCreated(originPage, payload.guid, payload.url, payload.suggestedFilename);
@ -364,15 +364,11 @@ export class CRBrowserContext extends BrowserContext {
return [...this._browser._crPages.values()].filter(crPage => crPage._browserContext === this);
}
pages(): Page[] {
return this._crPages().map(crPage => crPage._initializedPage).filter(Boolean) as Page[];
override possiblyUninitializedPages(): Page[] {
return this._crPages().map(crPage => crPage._page);
}
pagesOrErrors() {
return this._crPages().map(crPage => crPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
override async doCreateNewPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const oldKeys = this._browser.isClank() ? new Set(this._browser._crPages.keys()) : undefined;
@ -395,7 +391,7 @@ export class CRBrowserContext extends BrowserContext {
assert(newKeys.size === 1);
[targetId] = [...newKeys];
}
return this._browser._crPages.get(targetId)!;
return this._browser._crPages.get(targetId)!._page;
}
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
@ -548,7 +544,7 @@ export class CRBrowserContext extends BrowserContext {
// When persistent context is closed, we do not necessary get Target.detachedFromTarget
// for all the background pages.
for (const [targetId, backgroundPage] of this._browser._backgroundPages.entries()) {
if (backgroundPage._browserContext === this && backgroundPage._initializedPage) {
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) {
backgroundPage.didClose();
this._browser._backgroundPages.delete(targetId);
}
@ -573,8 +569,8 @@ export class CRBrowserContext extends BrowserContext {
backgroundPages(): Page[] {
const result: Page[] = [];
for (const backgroundPage of this._browser._backgroundPages.values()) {
if (backgroundPage._browserContext === this && backgroundPage._initializedPage)
result.push(backgroundPage._initializedPage);
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined())
result.push(backgroundPage._page);
}
return result;
}

View file

@ -65,8 +65,6 @@ export class CRPage implements PageDelegate {
private readonly _pdf: CRPDF;
private readonly _coverage: CRCoverage;
readonly _browserContext: CRBrowserContext;
private readonly _pagePromise: Promise<Page | Error>;
_initializedPage: Page | null = null;
private _isBackgroundPage: boolean;
// Holds window features for the next popup being opened via window.open,
@ -108,30 +106,11 @@ export class CRPage implements PageDelegate {
if (viewportSize)
this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize };
}
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
// so that anyone who awaits pageOrError got a ready and reported page.
this._pagePromise = this._mainFrameSession._initialize(bits.hasUIWindow).then(async r => {
await this._page.initOpener(this._opener);
return r;
}).catch(async e => {
await this._page.initOpener(this._opener);
throw e;
}).then(() => {
this._initializedPage = this._page;
this._reportAsNew();
return this._page;
}).catch(e => {
this._reportAsNew(e);
return e;
});
}
potentiallyUninitializedPage(): Page {
return this._page;
}
private _reportAsNew(error?: Error) {
this._page.reportAsNew(error, this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page);
const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page;
this._mainFrameSession._initialize(bits.hasUIWindow).then(
() => this._page.reportAsNew(this._opener?._page, undefined, createdEvent),
error => this._page.reportAsNew(this._opener?._page, error, createdEvent));
}
private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise<any>) {
@ -168,10 +147,6 @@ export class CRPage implements PageDelegate {
this._mainFrameSession._willBeginDownload();
}
async pageOrError(): Promise<Page | Error> {
return this._pagePromise;
}
didClose() {
for (const session of this._sessions.values())
session.dispose();
@ -492,7 +467,7 @@ class FrameSession {
// Note: it is important to start video recorder before sending Page.startScreencast,
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
await this._createVideoRecorder(screencastId, screencastOptions);
this._crPage.pageOrError().then(p => {
this._crPage._page.waitForInitializedOrError().then(p => {
if (p instanceof Error)
this._stopVideoRecording().catch(() => {});
});
@ -833,7 +808,7 @@ class FrameSession {
}
async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
const pageOrError = await this._crPage.pageOrError();
const pageOrError = await this._crPage._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(event.executionContextId);
if (context)
@ -898,8 +873,7 @@ class FrameSession {
}
_willBeginDownload() {
const originPage = this._crPage._initializedPage;
if (!originPage) {
if (!this._crPage._page.initializedOrUndefined()) {
// Resume the page creation with an error. The page will automatically close right
// after the download begins.
this._firstNonInitialNavigationCommittedReject(new Error('Starting new page download'));
@ -939,7 +913,7 @@ class FrameSession {
});
// Wait for the first frame before reporting video to the client.
gotFirstFrame.then(() => {
this._crPage._browserContext._browser._videoStarted(this._crPage._browserContext, screencastId, options.outputFile, this._crPage.pageOrError());
this._crPage._browserContext._browser._videoStarted(this._crPage._browserContext, screencastId, options.outputFile, this._crPage._page.waitForInitializedOrError());
});
}

View file

@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network';
import type { InitScript, Page, PageDelegate } from '../page';
import type { InitScript, Page } from '../page';
import { PageBinding } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
@ -136,14 +136,14 @@ export class FFBrowser extends Browser {
// Abort the navigation that turned into download.
ffPage._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting');
let originPage = ffPage._initializedPage;
let originPage = ffPage._page.initializedOrUndefined();
// If it's a new window download, report it on the opener page.
if (!originPage) {
// Resume the page creation with an error. The page will automatically close right
// after the download begins.
ffPage._markAsError(new Error('Starting new page download'));
if (ffPage._opener)
originPage = ffPage._opener._initializedPage;
originPage = ffPage._opener._page.initializedOrUndefined();
}
if (!originPage)
return;
@ -267,15 +267,11 @@ export class FFBrowserContext extends BrowserContext {
return Array.from(this._browser._ffPages.values()).filter(ffPage => ffPage._browserContext === this);
}
pages(): Page[] {
return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
override possiblyUninitializedPages(): Page[] {
return this._ffPages().map(ffPage => ffPage._page);
}
pagesOrErrors() {
return this._ffPages().map(ffPage => ffPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
override async doCreateNewPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser.session.send('Browser.newPage', {
browserContextId: this._browserContextId
@ -284,7 +280,7 @@ export class FFBrowserContext extends BrowserContext {
throw new Error(`Invalid timezone ID: ${this._options.timezoneId}`);
throw e;
});
return this._browser._ffPages.get(targetId)!;
return this._browser._ffPages.get(targetId)!._page;
}
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {

View file

@ -34,7 +34,6 @@ import type { Protocol } from './protocol';
import type { Progress } from '../progress';
import { splitErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger';
import { ManualPromise } from '../../utils/manualPromise';
import { BrowserContext } from '../browserContext';
import { TargetClosedError } from '../errors';
@ -49,9 +48,7 @@ export class FFPage implements PageDelegate {
readonly _page: Page;
readonly _networkManager: FFNetworkManager;
readonly _browserContext: FFBrowserContext;
private _pagePromise = new ManualPromise<Page | Error>();
_initializedPage: Page | null = null;
private _initializationFailed = false;
private _reportedAsNew = false;
readonly _opener: FFPage | null;
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
private _eventListeners: RegisteredListener[];
@ -102,40 +99,23 @@ export class FFPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Page.screencastFrame', this._onScreencastFrame.bind(this)),
];
this._session.once('Page.ready', async () => {
await this._page.initOpener(this._opener);
if (this._initializationFailed)
this._session.once('Page.ready', () => {
if (this._reportedAsNew)
return;
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
// so that anyone who awaits pageOrError got a ready and reported page.
this._initializedPage = this._page;
this._page.reportAsNew();
this._pagePromise.resolve(this._page);
this._reportedAsNew = true;
this._page.reportAsNew(this._opener?._page);
});
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
this.addInitScript(new InitScript('', true), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
}
potentiallyUninitializedPage(): Page {
return this._page;
}
async _markAsError(error: Error) {
// Same error may be report twice: channer disconnected and session.send fails.
if (this._initializationFailed)
// Same error may be reported twice: channel disconnected and session.send fails.
if (this._reportedAsNew)
return;
this._initializationFailed = true;
if (!this._initializedPage) {
await this._page.initOpener(this._opener);
this._page.reportAsNew(error);
this._pagePromise.resolve(error);
}
}
async pageOrError(): Promise<Page | Error> {
return this._pagePromise;
this._reportedAsNew = true;
this._page.reportAsNew(this._opener?._page, error);
}
_onWebSocketCreated(event: Protocol.Page.webSocketCreatedPayload) {
@ -268,7 +248,7 @@ export class FFPage implements PageDelegate {
}
async _onBindingCalled(event: Protocol.Page.bindingCalledPayload) {
const pageOrError = await this.pageOrError();
const pageOrError = await this._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(event.executionContextId);
if (context)
@ -333,7 +313,7 @@ export class FFPage implements PageDelegate {
}
_onVideoRecordingStarted(event: Protocol.Page.videoRecordingStartedPayload) {
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError());
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this._page.waitForInitializedOrError());
}
didClose() {

View file

@ -59,8 +59,6 @@ export interface PageDelegate {
addInitScript(initScript: InitScript): Promise<void>;
removeNonInternalInitScripts(): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
potentiallyUninitializedPage(): Page;
pageOrError(): Promise<Page | Error>;
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
@ -139,7 +137,8 @@ export class Page extends SdkObject {
private _closedState: 'open' | 'closing' | 'closed' = 'open';
private _closedPromise = new ManualPromise<void>();
private _initialized = false;
private _initialized: Page | Error | undefined;
private _initializedPromise = new ManualPromise<Page | Error>();
private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = [];
private _crashed = false;
readonly openScope = new LongStandingScope();
@ -193,15 +192,16 @@ export class Page extends SdkObject {
this.coverage = delegate.coverage ? delegate.coverage() : null;
}
async initOpener(opener: PageDelegate | null) {
if (!opener)
return;
const openerPage = await opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
this._opener = openerPage;
async reportAsNew(opener: Page | undefined, error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
if (opener) {
const openerPageOrError = await opener.waitForInitializedOrError();
if (openerPageOrError instanceof Page && !openerPageOrError.isClosed())
this._opener = openerPageOrError;
}
this._markInitialized(error, contextEvent);
}
reportAsNew(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
private _markInitialized(error: Error | undefined = undefined, contextEvent: string = BrowserContext.Events.Page) {
if (error) {
// Initialization error could have happened because of
// context/browser closure. Just ignore the page.
@ -209,7 +209,7 @@ export class Page extends SdkObject {
return;
this._frameManager.createDummyMainFrameIfNeeded();
}
this._initialized = true;
this._initialized = error || this;
this.emitOnContext(contextEvent, this);
for (const { event, args } of this._eventsToEmitAfterInitialized)
@ -223,12 +223,20 @@ export class Page extends SdkObject {
this.emit(Page.Events.Close);
else
this.instrumentation.onPageOpen(this);
// Note: it is important to resolve _initializedPromise at the end,
// so that anyone who awaits waitForInitializedOrError got a ready and reported page.
this._initializedPromise.resolve(this._initialized);
}
initializedOrUndefined() {
initializedOrUndefined(): Page | undefined {
return this._initialized ? this : undefined;
}
waitForInitializedOrError(): Promise<Page | Error> {
return this._initializedPromise;
}
emitOnContext(event: string | symbol, ...args: any[]) {
if (this._isServerSideOnly)
return;

View file

@ -20,7 +20,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert } from '../../utils';
import * as network from '../network';
import type { InitScript, Page, PageDelegate } from '../page';
import type { InitScript, Page } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@ -121,14 +121,14 @@ export class WKBrowser extends Browser {
// abort navigation that is still running. We should be able to fix this by
// instrumenting policy decision start/proceed/cancel.
page._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting');
let originPage = page._initializedPage;
let originPage = page._page.initializedOrUndefined();
// If it's a new window download, report it on the opener page.
if (!originPage) {
// Resume the page creation with an error. The page will automatically close right
// after the download begins.
page._firstNonInitialNavigationCommittedReject(new Error('Starting new page download'));
if (page._opener)
originPage = page._opener._initializedPage;
originPage = page._opener._page.initializedOrUndefined();
}
if (!originPage)
return;
@ -239,18 +239,14 @@ export class WKBrowserContext extends BrowserContext {
return Array.from(this._browser._wkPages.values()).filter(wkPage => wkPage._browserContext === this);
}
pages(): Page[] {
return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
override possiblyUninitializedPages(): Page[] {
return this._wkPages().map(wkPage => wkPage._page);
}
pagesOrErrors() {
return this._wkPages().map(wkPage => wkPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
override async doCreateNewPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId });
return this._browser._wkPages.get(pageProxyId)!;
return this._browser._wkPages.get(pageProxyId)!._page;
}
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {

View file

@ -43,7 +43,6 @@ import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
import { WKProvisionalPage } from './wkProvisionalPage';
import { WKWorkers } from './wkWorkers';
import { debugLogger } from '../../utils/debugLogger';
import { ManualPromise } from '../../utils/manualPromise';
import { BrowserContext } from '../browserContext';
import { TargetClosedError } from '../errors';
@ -56,7 +55,6 @@ export class WKPage implements PageDelegate {
_session: WKSession;
private _provisionalPage: WKProvisionalPage | null = null;
readonly _page: Page;
private readonly _pagePromise = new ManualPromise<Page | Error>();
private readonly _pageProxySession: WKSession;
readonly _opener: WKPage | null;
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
@ -66,7 +64,6 @@ export class WKPage implements PageDelegate {
private _sessionListeners: RegisteredListener[] = [];
private _eventListeners: RegisteredListener[];
readonly _browserContext: WKBrowserContext;
_initializedPage: Page | null = null;
private _firstNonInitialNavigationCommittedPromise: Promise<void>;
private _firstNonInitialNavigationCommittedFulfill = () => {};
_firstNonInitialNavigationCommittedReject = (e: Error) => {};
@ -111,10 +108,6 @@ export class WKPage implements PageDelegate {
}
}
potentiallyUninitializedPage(): Page {
return this._page;
}
private async _initializePageProxySession() {
if (this._page._browserContext.isSettingStorageState())
return;
@ -283,7 +276,7 @@ export class WKPage implements PageDelegate {
}
handleProvisionalLoadFailed(event: Protocol.Playwright.provisionalLoadFailedPayload) {
if (!this._initializedPage) {
if (!this._page.initializedOrUndefined()) {
this._firstNonInitialNavigationCommittedReject(new Error('Initial load failed'));
return;
}
@ -300,10 +293,6 @@ export class WKPage implements PageDelegate {
this._nextWindowOpenPopupFeatures = event.windowFeatures;
}
async pageOrError(): Promise<Page | Error> {
return this._pagePromise;
}
private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
const { targetInfo } = event;
const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, (message: any) => {
@ -316,7 +305,7 @@ export class WKPage implements PageDelegate {
assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type);
if (!targetInfo.isProvisional) {
assert(!this._initializedPage);
assert(!this._page.initializedOrUndefined());
let pageOrError: Page | Error;
try {
this._setSession(session);
@ -343,12 +332,7 @@ export class WKPage implements PageDelegate {
// Avoid rejection on disconnect.
this._firstNonInitialNavigationCommittedPromise.catch(() => {});
}
await this._page.initOpener(this._opener);
// Note: it is important to call |reportAsNew| before resolving pageOrError promise,
// so that anyone who awaits pageOrError got a ready and reported page.
this._initializedPage = pageOrError instanceof Page ? pageOrError : null;
this._page.reportAsNew(pageOrError instanceof Page ? undefined : pageOrError);
this._pagePromise.resolve(pageOrError);
this._page.reportAsNew(this._opener?._page, pageOrError instanceof Page ? undefined : pageOrError);
} else {
assert(targetInfo.isProvisional);
assert(!this._provisionalPage);
@ -515,7 +499,7 @@ export class WKPage implements PageDelegate {
}
private async _onBindingCalled(contextId: Protocol.Runtime.ExecutionContextId, argument: string) {
const pageOrError = await this.pageOrError();
const pageOrError = await this._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(contextId);
if (context)
@ -821,7 +805,7 @@ export class WKPage implements PageDelegate {
toolbarHeight: this._toolbarHeight()
});
this._recordingVideoFile = options.outputFile;
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this.pageOrError());
this._browserContext._browser._videoStarted(this._browserContext, screencastId, options.outputFile, this._page.waitForInitializedOrError());
}
async _stopVideo(): Promise<void> {