diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index e59e4f224a..6ec8218b11 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -134,6 +134,10 @@ export class CRBrowser extends BrowserBase { const opener = targetInfo.openerId ? this._crPages.get(targetInfo.openerId) || null : null; const crPage = new CRPage(session, targetInfo.targetId, context, opener); this._crPages.set(targetInfo.targetId, crPage); + if (opener && opener._initializedPage) { + for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) + signalBarrier.addPopup(crPage.pageOrError()); + } crPage.pageOrError().then(() => { this._firstPageCallback(); context.emit(CommonEvents.BrowserContext.Page, crPage._page); diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index c8e6de810e..6e992bc444 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -315,7 +315,8 @@ class FrameSession { private _eventListeners: RegisteredListener[] = []; readonly _targetId: string; private _firstNonInitialNavigationCommittedPromise: Promise; - private _firstNonInitialNavigationCommittedCallback = () => {}; + private _firstNonInitialNavigationCommittedFulfill = () => {}; + private _firstNonInitialNavigationCommittedReject = (e: Error) => {}; constructor(crPage: CRPage, client: CRSession, targetId: string) { this._client = client; @@ -323,7 +324,13 @@ class FrameSession { this._page = crPage._page; this._targetId = targetId; this._networkManager = new CRNetworkManager(client, this._page); - this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); + this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => { + this._firstNonInitialNavigationCommittedFulfill = f; + this._firstNonInitialNavigationCommittedReject = r; + }); + client.once(CRSessionEvents.Disconnected, () => { + this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); + }); } private _isMainFrame(): boolean { @@ -386,7 +393,7 @@ class FrameSession { this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); }); } else { - this._firstNonInitialNavigationCommittedCallback(); + this._firstNonInitialNavigationCommittedFulfill(); this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); } }), @@ -479,7 +486,7 @@ class FrameSession { _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial); if (!initial) - this._firstNonInitialNavigationCommittedCallback(); + this._firstNonInitialNavigationCommittedFulfill(); } _onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) { diff --git a/src/dom.ts b/src/dom.ts index e4b2e0ba2c..6f5adcb641 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -55,7 +55,7 @@ export class FrameExecutionContext extends js.ExecutionContext { } async _doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise { - return await this.frame._page._frameManager.waitForNavigationsCreatedBy(async () => { + return await this.frame._page._frameManager.waitForSignalsCreatedBy(async () => { return this._delegate.evaluate(this, returnByValue, pageFunction, ...args); }, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { waitUntil: 'nowait' }); } @@ -227,7 +227,7 @@ export class ElementHandle extends js.JSHandle { if (!force) await this._waitForHitTargetAt(point, deadline); - await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + await this._page._frameManager.waitForSignalsCreatedBy(async () => { let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); @@ -269,7 +269,7 @@ export class ElementHandle extends js.JSHandle { if (option.index !== undefined) assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"'); } - return await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + return await this._page._frameManager.waitForSignalsCreatedBy(async () => { return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); }, deadline, options); } @@ -277,7 +277,7 @@ export class ElementHandle extends js.JSHandle { async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise { assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); const deadline = this._page._timeoutSettings.computeDeadline(options); - await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + await this._page._frameManager.waitForSignalsCreatedBy(async () => { const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); if (typeof errorOrNeedsInput === 'string') throw new Error(errorOrNeedsInput); @@ -323,7 +323,7 @@ export class ElementHandle extends js.JSHandle { filePayloads.push(item); } } - await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this._page._delegate.setInputFiles(this as any as ElementHandle, filePayloads); }, deadline, options); } @@ -341,7 +341,7 @@ export class ElementHandle extends js.JSHandle { async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { const deadline = this._page._timeoutSettings.computeDeadline(options); - await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this.focus(); await this._page.keyboard.type(text, options); }, deadline, options, true); @@ -349,7 +349,7 @@ export class ElementHandle extends js.JSHandle { async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { const deadline = this._page._timeoutSettings.computeDeadline(options); - await this._page._frameManager.waitForNavigationsCreatedBy(async () => { + await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this.focus(); await this._page.keyboard.press(key, options); }, deadline, options, true); diff --git a/src/download.ts b/src/download.ts index 4f03fb60a7..200bd75f29 100644 --- a/src/download.ts +++ b/src/download.ts @@ -39,6 +39,8 @@ export class Download { this._url = url; this._finishedCallback = () => {}; this._finishedPromise = new Promise(f => this._finishedCallback = f); + for (const barrier of this._page._frameManager._signalBarriers) + barrier.addDownload(); this._page.emit(Events.Page.Download, this); page._browserContext._downloads.add(this); this._acceptDownloads = !!this._page._browserContext._options.acceptDownloads; diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index c77f59e86d..a0e3732e1c 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -129,6 +129,10 @@ export class FFBrowser extends BrowserBase { const ffPage = new FFPage(session, context, opener); this._ffPages.set(targetId, ffPage); + if (opener && opener._initializedPage) { + for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) + signalBarrier.addPopup(ffPage.pageOrError()); + } ffPage.pageOrError().then(async () => { this._firstPageCallback(); const page = ffPage._page; @@ -200,7 +204,7 @@ export class FFBrowserContext extends BrowserContextBase { } pages(): Page[] { - return this._ffPages().map(ffPage => ffPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[]; + return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; } async newPage(): Promise { diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index ca0f7a890a..b6222b42a7 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -43,7 +43,7 @@ export class FFPage implements PageDelegate { readonly _browserContext: FFBrowserContext; private _pagePromise: Promise; private _pageCallback: (pageOrError: Page | Error) => void = () => {}; - private _initialized = false; + _initializedPage: Page | null = null; private readonly _opener: FFPage | null; private readonly _contextIdToContext: Map; private _eventListeners: RegisteredListener[]; @@ -70,6 +70,7 @@ export class FFPage implements PageDelegate { helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), helper.addEventListener(this._session, 'Page.linkClicked', event => this._onLinkClicked(event.phase)), + helper.addEventListener(this._session, 'Page.willOpenNewWindowAsynchronously', this._onWillOpenNewWindowAsynchronously.bind(this)), helper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)), helper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)), helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)), @@ -84,17 +85,13 @@ export class FFPage implements PageDelegate { session.once(FFSessionEvents.Disconnected, () => this._page._didDisconnect()); this._session.once('Page.ready', () => { this._pageCallback(this._page); - this._initialized = true; + this._initializedPage = this._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._session.send('Page.addScriptToEvaluateOnNewDocument', { script: '', worldName: UTILITY_WORLD_NAME }).catch(this._pageCallback); } - _initializedPage(): Page | null { - return this._initialized ? this._page : null; - } - async pageOrError(): Promise { return this._pagePromise; } @@ -136,12 +133,21 @@ export class FFPage implements PageDelegate { this._page._frameManager.frameDidPotentiallyRequestNavigation(); } + _onWillOpenNewWindowAsynchronously() { + for (const barrier of this._page._frameManager._signalBarriers) + barrier.expectPopup(); + } + _onNavigationStarted(params: Protocol.Page.navigationStartedPayload) { this._page._frameManager.frameRequestedNavigation(params.frameId, params.navigationId); } _onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) { const frame = this._page._frameManager.frame(params.frameId)!; + if (params.errorText === 'Will download to file') { + for (const barrier of this._page._frameManager._signalBarriers) + barrier.expectDownload(); + } for (const task of frame._frameTasks) task.onNewDocument(params.navigationId, new Error(params.errorText)); } diff --git a/src/frames.ts b/src/frames.ts index e7aab1384c..ad7e844dd0 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -51,7 +51,7 @@ export class FrameManager { private _frames = new Map(); private _mainFrame: Frame; readonly _consoleMessageTags = new Map(); - private _pendingNavigationBarriers = new Set(); + readonly _signalBarriers = new Set(); constructor(page: Page) { this._page = page; @@ -100,11 +100,11 @@ export class FrameManager { } } - async waitForNavigationsCreatedBy(action: () => Promise, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise { + async waitForSignalsCreatedBy(action: () => Promise, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise { if (options.waitUntil === 'nowait') return action(); - const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline); - this._pendingNavigationBarriers.add(barrier); + const barrier = new SignalBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline); + this._signalBarriers.add(barrier); try { const result = await action(); if (input) @@ -114,17 +114,17 @@ export class FrameManager { await new Promise(helper.makeWaitForNextTask()); return result; } finally { - this._pendingNavigationBarriers.delete(barrier); + this._signalBarriers.delete(barrier); } } frameWillPotentiallyRequestNavigation() { - for (const barrier of this._pendingNavigationBarriers) + for (const barrier of this._signalBarriers) barrier.retain(); } frameDidPotentiallyRequestNavigation() { - for (const barrier of this._pendingNavigationBarriers) + for (const barrier of this._signalBarriers) barrier.release(); } @@ -132,8 +132,8 @@ export class FrameManager { const frame = this._frames.get(frameId); if (!frame) return; - for (const barrier of this._pendingNavigationBarriers) - barrier.addFrame(frame); + for (const barrier of this._signalBarriers) + barrier.addFrameNavigation(frame); frame._pendingDocumentId = documentId; } @@ -939,10 +939,12 @@ function selectorToString(selector: string, waitFor: 'attached' | 'detached' | ' return `${label}${selector}`; } -class PendingNavigationBarrier { +export class SignalBarrier { private _frameIds = new Map(); private _options: types.NavigatingActionWaitOptions; private _protectCount = 0; + private _expectedPopups = 0; + private _expectedDownloads = 0; private _promise: Promise; private _promiseCallback = () => {}; private _deadline: number; @@ -959,7 +961,7 @@ class PendingNavigationBarrier { return this._promise; } - async addFrame(frame: Frame) { + async addFrameNavigation(frame: Frame) { this.retain(); const timeout = helper.timeUntilDeadline(this._deadline); const options = { ...this._options, timeout } as types.NavigateOptions; @@ -967,6 +969,33 @@ class PendingNavigationBarrier { this.release(); } + async expectPopup() { + ++this._expectedPopups; + } + + async unexpectPopup() { + --this._expectedPopups; + this._maybeResolve(); + } + + async addPopup(pageOrError: Promise) { + if (this._expectedPopups) + --this._expectedPopups; + this.retain(); + await pageOrError; + this.release(); + } + + async expectDownload() { + ++this._expectedDownloads; + } + + async addDownload() { + if (this._expectedDownloads) + --this._expectedDownloads; + this._maybeResolve(); + } + retain() { ++this._protectCount; } @@ -977,7 +1006,7 @@ class PendingNavigationBarrier { } private async _maybeResolve() { - if (!this._protectCount && !this._frameIds.size) + if (!this._protectCount && !this._expectedPopups && !this._expectedDownloads && !this._frameIds.size) this._promiseCallback(); } } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index a3a6e7c807..449e261318 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -128,6 +128,10 @@ export class WKBrowser extends BrowserBase { const wkPage = new WKPage(context, pageProxySession, opener || null); this._wkPages.set(pageProxyId, wkPage); + if (opener && opener._initializedPage) { + for (const signalBarrier of opener._initializedPage._frameManager._signalBarriers) + signalBarrier.addPopup(wkPage.pageOrError()); + } wkPage.pageOrError().then(async () => { this._firstPageCallback(); const page = wkPage._page; @@ -216,7 +220,7 @@ export class WKBrowserContext extends BrowserContextBase { } pages(): Page[] { - return this._wkPages().map(wkPage => wkPage._initializedPage()).filter(pageOrNull => !!pageOrNull) as Page[]; + return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[]; } async newPage(): Promise { diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 03b21bc110..895387471e 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -58,9 +58,10 @@ export class WKPage implements PageDelegate { private _eventListeners: RegisteredListener[]; private readonly _evaluateOnNewDocumentSources: string[] = []; readonly _browserContext: WKBrowserContext; - private _initialized = false; + _initializedPage: Page | null = null; private _firstNonInitialNavigationCommittedPromise: Promise; - private _firstNonInitialNavigationCommittedCallback = () => {}; + private _firstNonInitialNavigationCommittedFulfill = () => {}; + private _firstNonInitialNavigationCommittedReject = (e: Error) => {}; constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPage | null) { this._pageProxySession = pageProxySession; @@ -80,11 +81,10 @@ export class WKPage implements PageDelegate { helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)), ]; this._pagePromise = new Promise(f => this._pagePromiseCallback = f); - this._firstNonInitialNavigationCommittedPromise = new Promise(f => this._firstNonInitialNavigationCommittedCallback = f); - } - - _initializedPage(): Page | null { - return this._initialized ? this._page : null; + this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => { + this._firstNonInitialNavigationCommittedFulfill = f; + this._firstNonInitialNavigationCommittedReject = r; + }); } private async _initializePageProxySession() { @@ -217,6 +217,7 @@ export class WKPage implements PageDelegate { this._provisionalPage = null; } this._page._didDisconnect(); + this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); } dispatchMessageToSession(message: any) { @@ -224,7 +225,11 @@ export class WKPage implements PageDelegate { } handleProvisionalLoadFailed(event: Protocol.Playwright.provisionalLoadFailedPayload) { - if (!this._initialized || !this._provisionalPage) + if (!this._initializedPage) { + this._firstNonInitialNavigationCommittedReject(new Error('Initial load failed')); + return; + } + if (!this._provisionalPage) return; let errorText = event.error; if (errorText.includes('cancelled')) @@ -247,7 +252,7 @@ export class WKPage implements PageDelegate { }); assert(targetInfo.type === 'page', 'Only page targets are expected in WebKit, received: ' + targetInfo.type); - if (!this._initialized) { + if (!this._initializedPage) { assert(!targetInfo.isProvisional); let pageOrError: Page | Error; try { @@ -263,12 +268,19 @@ export class WKPage implements PageDelegate { if (targetInfo.isPaused) this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') { - // Initial empty page has an empty url. We should wait until the first real url has been loaded, - // even if that url is about:blank. This is especially important for popups, where we need the - // actual url before interacting with it. - await this._firstNonInitialNavigationCommittedPromise; + try { + // Initial empty page has an empty url. We should wait until the first real url has been loaded, + // even if that url is about:blank. This is especially important for popups, where we need the + // actual url before interacting with it. + await this._firstNonInitialNavigationCommittedPromise; + } catch (e) { + pageOrError = e; + } + } else { + // Avoid rejection on disconnect. + this._firstNonInitialNavigationCommittedPromise.catch(() => {}); } - this._initialized = true; + this._initializedPage = pageOrError instanceof Page ? pageOrError : null; this._pagePromiseCallback(pageOrError); } else { assert(targetInfo.isProvisional); @@ -302,6 +314,8 @@ export class WKPage implements PageDelegate { helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), helper.addEventListener(this._session, 'Page.loadEventFired', event => this._onLifecycleEvent(event.frameId, 'load')), helper.addEventListener(this._session, 'Page.domContentEventFired', event => this._onLifecycleEvent(event.frameId, 'domcontentloaded')), + helper.addEventListener(this._session, 'Page.willRequestOpenWindow', event => this._onWillRequestOpenWindow()), + helper.addEventListener(this._session, 'Page.didRequestOpenWindow', event => this._onDidRequestOpenWindow(event)), helper.addEventListener(this._session, 'Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context)), helper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)), helper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)), @@ -344,6 +358,18 @@ export class WKPage implements PageDelegate { this._page._frameManager.frameLifecycleEvent(frameId, event); } + private _onWillRequestOpenWindow() { + for (const barrier of this._page._frameManager._signalBarriers) + barrier.expectPopup(); + } + + private _onDidRequestOpenWindow(event: Protocol.Page.didRequestOpenWindowPayload) { + if (!event.opened) { + for (const barrier of this._page._frameManager._signalBarriers) + barrier.unexpectPopup(); + } + } + private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null); this._onFrameNavigated(frameTree.frame, true); @@ -368,7 +394,7 @@ export class WKPage implements PageDelegate { this._workers.clear(); this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url, framePayload.name || '', framePayload.loaderId, initial); if (!initial) - this._firstNonInitialNavigationCommittedCallback(); + this._firstNonInitialNavigationCommittedFulfill(); } private _onFrameNavigatedWithinDocument(frameId: string, url: string) { diff --git a/test/autowaiting.spec.js b/test/autowaiting.spec.js index 5995e88e6e..c0d630b287 100644 --- a/test/autowaiting.spec.js +++ b/test/autowaiting.spec.js @@ -34,6 +34,40 @@ describe('Auto waiting', () => { ]); expect(messages.join('|')).toBe('route|domcontentloaded|click'); }); + it('should await popup when clicking anchor', async function({page, server}) { + await page.goto(server.EMPTY_PAGE); + await page.setContent('link'); + const messages = []; + await Promise.all([ + page.waitForEvent('popup').then(() => messages.push('popup')), + page.click('a').then(() => messages.push('click')), + ]); + expect(messages.join('|')).toBe('popup|click'); + }); + it('should await popup when clicking anchor with noopener', async function({page, server}) { + await page.goto(server.EMPTY_PAGE); + await page.setContent('link'); + const messages = []; + await Promise.all([ + page.waitForEvent('popup').then(() => messages.push('popup')), + page.click('a').then(() => messages.push('click')), + ]); + expect(messages.join('|')).toBe('popup|click'); + }); + it('should await download when clicking anchor', async function({page, server}) { + server.setRoute('/download', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment'); + res.end(`Hello world`); + }); + await page.setContent(`download`); + const messages = []; + await Promise.all([ + page.waitForEvent('download').then(() => messages.push('download')), + page.click('a').then(() => messages.push('click')), + ]); + expect(messages.join('|')).toBe('download|click'); + }); it('should await cross-process navigation when clicking anchor', async({page, server}) => { const messages = []; server.setRoute('/empty.html', async (req, res) => { @@ -130,6 +164,15 @@ describe('Auto waiting', () => { ]); expect(messages.join('|')).toBe('route|domcontentloaded|evaluate'); }); + it('should await new popup when evaluating', async function({page, server}) { + await page.goto(server.EMPTY_PAGE); + const messages = []; + await Promise.all([ + page.waitForEvent('popup').then(() => messages.push('popup')), + page.evaluate(() => window._popup = window.open(window.location.href)).then(() => messages.push('evaluate')), + ]); + expect(messages.join('|')).toBe('popup|evaluate'); + }); it('should await navigating specified target', async({page, server}) => { const messages = []; server.setRoute('/empty.html', async (req, res) => { diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 1364bca2eb..0ffcfc6ab1 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -630,7 +630,6 @@ describe('Events.BrowserContext.Page', function() { context.waitForEvent('page'), page.goto(server.PREFIX + '/popup/window-open.html') ]); - // The url is still about:blank in FF when 'page' event is fired. expect(popup.url()).toBe(server.PREFIX + '/popup/popup.html'); expect(await popup.opener()).toBe(page); expect(await page.opener()).toBe(null); diff --git a/test/popup.spec.js b/test/popup.spec.js index e5f323fa0f..e75813dc32 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -217,6 +217,20 @@ describe('Page.Events.Popup', function() { expect(popup).toBeTruthy(); await context.close(); }); + it('should emit for immediately closed popups', async({browser, server}) => { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(() => { + const win = window.open(window.location.href); + win.close(); + }), + ]); + expect(popup).toBeTruthy(); + await context.close(); + }); it('should be able to capture alert', async({browser}) => { const context = await browser.newContext(); const page = await context.newPage();