api(popups): emit PageEvent immediately, and resolve page() once initialized (#1229)

This way we do not miss any popups, even immediately closed ones.
This commit is contained in:
Dmitry Gozman 2020-03-05 15:18:27 -08:00 committed by GitHub
parent c734b4b715
commit e5f82af47c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 162 additions and 119 deletions

View file

@ -304,7 +304,15 @@ Emitted when Browser context gets closed. This might happen because of one of th
- <[PageEvent]>
Emitted when a new Page is created in the BrowserContext. The event will also fire for popup
pages.
pages. See also [`Page.on('popup')`](#event-popup) to receive events about popups relevant to a specific page.
```js
const [event] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target=_blank]'),
]);
const newPage = await event.page();
```
#### browserContext.addInitScript(script[, ...args])
- `script` <[function]|[string]|[Object]> Script to be evaluated in all pages in the browser context.
@ -726,22 +734,24 @@ Emitted when the JavaScript [`load`](https://developer.mozilla.org/en-US/docs/We
Emitted when an uncaught exception happens within the page.
#### event: 'popup'
- <[Page]> Page corresponding to "popup" window
- <[PageEvent]> Page event corresponding to "popup" window
Emitted when the page opens a new tab or window.
Emitted when the page opens a new tab or window. This event is emitted in addition to the [`browserContext.on('page')`](#event-page), but only for popups relevant to this page.
```js
const [popup] = await Promise.all([
new Promise(resolve => page.once('popup', resolve)),
const [event] = await Promise.all([
page.waitForEvent('popup'),
page.click('a[target=_blank]'),
]);
const popup = await event.page();
```
```js
const [popup] = await Promise.all([
new Promise(resolve => page.once('popup', resolve)),
const [event] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => window.open('https://example.com')),
]);
const popup = await event.page();
```
#### event: 'request'
@ -1753,8 +1763,7 @@ This method returns all of the dedicated [WebWorkers](https://developer.mozilla.
### class: PageEvent
Event object passed to the listeners of ['page'](#event-page) on [`BrowserContext`](#class-browsercontext). Provides access
to the newly created page.
Event object passed to the listeners of [`browserContext.on('page')`](#event-page) and [`page.on('popup')`](#event-popup) events. Provides access to the newly created page.
#### pageEvent.page()
- returns: <[Promise]<[Page]>> Promise which resolves to the created page.

View file

@ -71,7 +71,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
constructor(connection: CRConnection) {
super();
this._connection = connection;
this._client = connection.rootSession;
this._client = this._connection.rootSession;
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
this._connection.on(ConnectionEvents.Disconnected, () => {
@ -120,14 +120,18 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
try {
switch (targetInfo.type) {
case 'page': {
const page = await target.page();
const event = new PageEvent(page!);
const event = new PageEvent(target.pageOrError());
context.emit(CommonEvents.BrowserContext.Page, event);
const opener = target.opener();
if (!opener)
break;
const openerPage = await opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(CommonEvents.Page.Popup, new PageEvent(target.pageOrError()));
break;
}
case 'background_page': {
const page = await target.page();
const event = new PageEvent(page!);
const event = new PageEvent(target.pageOrError());
context.emit(Events.CRBrowserContext.BackgroundPage, event);
break;
}
@ -268,16 +272,21 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
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.page()));
return pages.filter(page => !!page) as Page[];
const pages = await Promise.all(targets.map(target => target.pageOrError()));
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
const target = this._browser._targets.get(targetId)!;
const page = await target.page();
return page!;
const result = await target.pageOrError();
if (result instanceof Page) {
if (result.isClosed())
throw new Error('Page has been closed.');
return result;
}
throw result;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
@ -382,8 +391,8 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
async backgroundPages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'background_page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page) as Page[];
const pages = await Promise.all(targets.map(target => target.pageOrError()));
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
}
async createSession(page: Page): Promise<CRSession> {

View file

@ -388,7 +388,10 @@ export class CRPage implements PageDelegate {
const openerTarget = CRTarget.fromPage(this._page).opener();
if (!openerTarget)
return null;
return await openerTarget.page();
const openerPage = await openerTarget.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
return openerPage;
return null;
}
async reload(): Promise<void> {

View file

@ -17,7 +17,6 @@
import { CRBrowser, CRBrowserContext } from './crBrowser';
import { CRSession, CRSessionEvents } from './crConnection';
import { Events } from '../events';
import { Page, Worker } from '../page';
import { Protocol } from './protocol';
import { debugError } from '../helper';
@ -32,9 +31,8 @@ export class CRTarget {
private readonly _browserContext: CRBrowserContext;
readonly _targetId: string;
readonly sessionFactory: () => Promise<CRSession>;
private _pagePromiseFulfill: ((page: Page) => void) | null = null;
private _pagePromiseReject: ((error: Error) => void) | null = null;
private _pagePromise: Promise<Page> | null = null;
private _pagePromiseCallback: ((pageOrError: Page | Error) => void) | null = null;
private _pagePromise: Promise<Page | Error> | null = null;
_crPage: CRPage | null = null;
private _workerPromise: Promise<Worker> | null = null;
@ -56,12 +54,8 @@ export class CRTarget {
this._browserContext = browserContext;
this._targetId = targetInfo.targetId;
this.sessionFactory = sessionFactory;
if (CRTarget.isPageType(targetInfo.type)) {
this._pagePromise = new Promise<Page>((fulfill, reject) => {
this._pagePromiseFulfill = fulfill;
this._pagePromiseReject = reject;
});
}
if (CRTarget.isPageType(targetInfo.type))
this._pagePromise = new Promise<Page | Error>(f => this._pagePromiseCallback = f);
}
_didClose() {
@ -69,10 +63,6 @@ export class CRTarget {
this._crPage.didClose();
}
async page(): Promise<Page | null> {
return this._pagePromise;
}
async initializePageSession(session: CRSession) {
this._crPage = new CRPage(session, this._browser, this._browserContext);
const page = this._crPage.page();
@ -80,20 +70,16 @@ export class CRTarget {
session.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
try {
await this._crPage.initialize();
this._pagePromiseFulfill!(page);
} catch (error) {
this._pagePromiseReject!(error);
this._pagePromiseCallback!(page);
} catch (e) {
this._pagePromiseCallback!(e);
}
}
if (this.type() !== 'page')
return;
const opener = this.opener();
if (!opener)
return;
const openerPage = await opener.page();
if (!openerPage)
return;
openerPage.emit(Events.Page.Popup, page);
async pageOrError(): Promise<Page | Error> {
if (this._targetInfo.type !== 'page' && this._targetInfo.type !== 'background_page')
throw new Error('Not a page.');
return this._pagePromise!;
}
async serviceWorker(): Promise<Worker | null> {

View file

@ -18,7 +18,7 @@
import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import * as types from '../types';
import { Page, PageEvent, PageBinding } from '../page';
@ -165,17 +165,16 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
const {targetId} = payload.targetInfo;
const target = this._targets.get(targetId)!;
target._initPagePromise(this._connection.getSession(payload.sessionId)!);
const page = await target.page();
if (!page)
return;
target.context().emit(Events.BrowserContext.Page, new PageEvent(page));
const pageEvent = new PageEvent(target.pageOrError());
target.context().emit(Events.BrowserContext.Page, pageEvent);
const opener = target.opener();
if (opener && opener._pagePromise) {
const openerPage = await opener._pagePromise;
if (openerPage.listenerCount(Events.Page.Popup))
openerPage.emit(Events.Page.Popup, page);
}
if (!opener)
return;
const openerPage = await opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(Events.Page.Popup, pageEvent);
}
async close() {
@ -192,7 +191,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
}
class Target {
_pagePromise?: Promise<Page>;
_pagePromise?: Promise<Page | Error>;
_ffPage: FFPage | null = null;
private readonly _browser: FFBrowser;
private readonly _context: FFBrowserContext;
@ -233,7 +232,7 @@ class Target {
return this._context;
}
async page(): Promise<Page> {
async pageOrError(): Promise<Page | Error> {
if (this._type !== 'page')
throw new Error(`Cannot create page for "${this._type}" target`);
if (!this._pagePromise)
@ -247,12 +246,21 @@ class Target {
const openerTarget = this.opener();
if (!openerTarget)
return null;
return await openerTarget.page();
const result = await openerTarget.pageOrError();
if (result instanceof Page && !result.isClosed())
return result;
return null;
});
const page = this._ffPage._page;
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
await this._ffPage._initialize().catch(debugError);
f(page);
let pageOrError: Page | Error;
try {
await this._ffPage._initialize();
pageOrError = page;
} catch (e) {
pageOrError = e;
}
f(pageOrError);
});
}
@ -309,8 +317,8 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
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.page()));
return pages.filter(page => !!page);
const pages = await Promise.all(targets.map(target => target.pageOrError()));
return pages.filter(page => page instanceof Page && !page.isClosed()) as Page[];
}
async newPage(): Promise<Page> {
@ -319,7 +327,13 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
browserContextId: this._browserContextId || undefined
});
const target = this._browser._targets.get(targetId)!;
return target.page();
const result = await target.pageOrError();
if (result instanceof Page) {
if (result.isClosed())
throw new Error('Page has been closed.');
return result;
}
throw result;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {

View file

@ -89,14 +89,20 @@ export type FileChooser = {
};
export class PageEvent {
private readonly _page: Page;
private readonly _pageOrError: Promise<Page | Error>;
constructor(page: Page) {
this._page = page;
constructor(pageOrErrorPromise: Promise<Page | Error>) {
this._pageOrError = pageOrErrorPromise;
}
async page(/* options?: frames.NavigateOptions */): Promise<Page> {
return this._page;
const result = await this._pageOrError;
if (result instanceof Page) {
if (result.isClosed())
throw new Error('Page has been closed.');
return result;
}
throw result;
}
}

View file

@ -70,10 +70,16 @@ export class Chromium implements BrowserType {
const { timeout = 30000 } = options || {};
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await CRBrowser.connect(transport!, true);
const firstPage = new Promise(r => browser._defaultContext.once(Events.BrowserContext.Page, r));
await helper.waitWithTimeout(firstPage, 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
const browserContext = browser._defaultContext;
function targets() {
return browser._allTargets().filter(target => target.context() === browserContext && target.type() === 'page');
}
const firstTarget = targets().length ? Promise.resolve() : new Promise(f => browserContext.once('page', f));
const firstPage = firstTarget.then(() => targets()[0].pageOrError());
await helper.waitWithTimeout(firstPage, 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
browserContext.close = () => browserServer.close();
return browserContext;
}

View file

@ -17,7 +17,7 @@
import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page, PageBinding, PageEvent } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport';
@ -126,17 +126,14 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
this._firstPageProxyCallback = undefined;
}
pageProxy.page().then(async page => {
if (!page)
return;
context!.emit(Events.BrowserContext.Page, new PageEvent(page));
if (!opener)
return;
const openerPage = await opener.page();
if (!openerPage || page.isClosed())
return;
openerPage.emit(Events.Page.Popup, page);
}).catch(debugError); // Just not emit the event in case of initialization failure.
const pageEvent = new PageEvent(pageProxy.pageOrError());
context.emit(Events.BrowserContext.Page, pageEvent);
if (!opener)
return;
opener.pageOrError().then(openerPage => {
if (openerPage instanceof Page && !openerPage.isClosed())
openerPage.emit(Events.Page.Popup, pageEvent);
});
}
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
@ -233,16 +230,21 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
async pages(): Promise<Page[]> {
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
const pages = await Promise.all(pageProxies.map(proxy => proxy.page()));
return pages.filter(page => !!page) as Page[];
const pages = await Promise.all(pageProxies.map(proxy => proxy.pageOrError()));
return pages.filter(page => page instanceof Page && !page.isClosed()) as Page[];
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
const page = await pageProxy.page();
return page!;
const result = await pageProxy.pageOrError();
if (result instanceof Page) {
if (result.isClosed())
throw new Error('Page has been closed.');
return result;
}
throw result;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {

View file

@ -437,8 +437,12 @@ export class WKPage implements PageDelegate {
}
async opener(): Promise<Page | null> {
const openerPage = this._opener ? await this._opener.page() : null;
return openerPage && !openerPage.isClosed() ? openerPage : null;
if (!this._opener)
return null;
const openerPage = await this._opener.pageOrError();
if (openerPage instanceof Page && !openerPage.isClosed())
return openerPage;
return null;
}
async reload(): Promise<void> {

View file

@ -27,9 +27,8 @@ export class WKPageProxy {
private readonly _pageProxySession: WKSession;
readonly _browserContext: WKBrowserContext;
private readonly _opener: WKPageProxy | null;
private readonly _pagePromise: Promise<Page | null>;
private _pagePromiseFulfill: (page: Page | null) => void = () => {};
private _pagePromiseReject: (error: Error) => void = () => {};
private readonly _pagePromise: Promise<Page | Error>;
private _pagePromiseCallback: (page: Page | Error) => void = () => {};
private readonly _wkPage: WKPage;
private _initialized = false;
private readonly _sessions = new Map<string, WKSession>();
@ -45,10 +44,7 @@ export class WKPageProxy {
helper.addEventListener(this._pageProxySession, 'Target.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
];
this._pagePromise = new Promise((f, r) => {
this._pagePromiseFulfill = f;
this._pagePromiseReject = r;
});
this._pagePromise = new Promise(f => this._pagePromiseCallback = f);
this._wkPage = new WKPage(this._browserContext, this._pageProxySession, this._opener);
}
@ -89,7 +85,7 @@ export class WKPageProxy {
this._wkPage._page._frameManager.provisionalLoadFailed(this._wkPage._page.mainFrame(), event.loaderId, errorText);
}
async page(): Promise<Page | null> {
async pageOrError(): Promise<Page | Error> {
return this._pagePromise;
}
@ -112,21 +108,16 @@ export class WKPageProxy {
if (!this._initialized) {
assert(!targetInfo.isProvisional);
this._initialized = true;
let page: Page | null = null;
let error: Error | undefined;
let pageOrError: Page | Error;
try {
await this._wkPage.initialize(session);
page = this._wkPage._page;
pageOrError = this._wkPage._page;
} catch (e) {
if (!this._pageProxySession.isDisposed())
error = e;
pageOrError = e;
}
if (targetInfo.isPaused)
this._resumeTarget(targetInfo.targetId);
if (error)
this._pagePromiseReject(error);
else
this._pagePromiseFulfill(page);
this._pagePromiseCallback(pageOrError);
} else {
assert(targetInfo.isProvisional);
(session as any)[isPovisionalSymbol] = true;

View file

@ -39,7 +39,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([
utils.waitEvent(page, 'popup'),
utils.waitEvent(page, 'popup').then(e => e.page()),
page.evaluate(url => window.open(url), server.EMPTY_PAGE)
]);
expect(popup.context()).toBe(context);

View file

@ -209,7 +209,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
it('should work for adopted elements', async({page,server}) => {
await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }),
page.evaluate(url => window.__popup = window.open(url), server.EMPTY_PAGE),
]);
const divHandle = await page.evaluateHandle(() => {

View file

@ -132,7 +132,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
describe('Page.opener', function() {
it('should provide access to the opener page', async({page}) => {
const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)),
page.waitForEvent('popup').then(e => e.page()),
page.evaluate(() => window.open('about:blank')),
]);
const opener = await popup.opener();
@ -140,7 +140,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
});
it('should return null if parent page has been closed', async({page}) => {
const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)),
page.waitForEvent('popup').then(e => e.page()),
page.evaluate(() => window.open('about:blank')),
]);
await page.close();
@ -1077,7 +1077,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
describe('Page.Events.Close', function() {
it('should work with window.close', async function({ page, context, server }) {
const newPagePromise = new Promise(f => page.once('popup', f));
const newPagePromise = page.waitForEvent('popup').then(e => e.page());
await page.evaluate(() => window['newPage'] = window.open('about:blank'));
const newPage = await newPagePromise;
const closedPromise = new Promise(x => newPage.on('close', x));

View file

@ -139,18 +139,31 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)),
page.waitForEvent('popup').then(e => e.page()),
page.evaluate(() => window.__popup = window.open('about:blank')),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
await context.close();
});
it('should emit for immediately closed popups', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popupEvent] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => {
const win = window.open('about:blank');
win.close();
}),
]);
expect(popupEvent).toBeTruthy();
await context.close();
});
it('should work with empty url', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)),
page.waitForEvent('popup').then(e => e.page()),
page.evaluate(() => window.__popup = window.open('')),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
@ -161,7 +174,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)),
page.waitForEvent('popup').then(e => e.page()),
page.evaluate(() => window.__popup = window.open('about:blank', null, 'noopener')),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
@ -174,7 +187,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>');
const [popup] = await Promise.all([
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }),
page.click('a'),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
@ -188,7 +201,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
const [popup] = await Promise.all([
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }),
page.$eval('a', a => a.click()),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
@ -203,7 +216,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
const [popup] = await Promise.all([
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }),
page.click('a'),
]);
expect(await page.evaluate(() => !!window.opener)).toBe(false);
@ -216,7 +229,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
const [popup] = await Promise.all([
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }),
page.click('a'),
]);
let badSecondPopup = false;