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:
parent
c734b4b715
commit
e5f82af47c
27
docs/api.md
27
docs/api.md
|
|
@ -304,7 +304,15 @@ Emitted when Browser context gets closed. This might happen because of one of th
|
||||||
- <[PageEvent]>
|
- <[PageEvent]>
|
||||||
|
|
||||||
Emitted when a new Page is created in the BrowserContext. The event will also fire for popup
|
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])
|
#### browserContext.addInitScript(script[, ...args])
|
||||||
- `script` <[function]|[string]|[Object]> Script to be evaluated in all pages in the browser context.
|
- `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.
|
Emitted when an uncaught exception happens within the page.
|
||||||
|
|
||||||
#### event: 'popup'
|
#### 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
|
```js
|
||||||
const [popup] = await Promise.all([
|
const [event] = await Promise.all([
|
||||||
new Promise(resolve => page.once('popup', resolve)),
|
page.waitForEvent('popup'),
|
||||||
page.click('a[target=_blank]'),
|
page.click('a[target=_blank]'),
|
||||||
]);
|
]);
|
||||||
|
const popup = await event.page();
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const [popup] = await Promise.all([
|
const [event] = await Promise.all([
|
||||||
new Promise(resolve => page.once('popup', resolve)),
|
page.waitForEvent('popup'),
|
||||||
page.evaluate(() => window.open('https://example.com')),
|
page.evaluate(() => window.open('https://example.com')),
|
||||||
]);
|
]);
|
||||||
|
const popup = await event.page();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### event: 'request'
|
#### event: 'request'
|
||||||
|
|
@ -1753,8 +1763,7 @@ This method returns all of the dedicated [WebWorkers](https://developer.mozilla.
|
||||||
|
|
||||||
### class: PageEvent
|
### class: PageEvent
|
||||||
|
|
||||||
Event object passed to the listeners of ['page'](#event-page) on [`BrowserContext`](#class-browsercontext). Provides access
|
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.
|
||||||
to the newly created page.
|
|
||||||
|
|
||||||
#### pageEvent.page()
|
#### pageEvent.page()
|
||||||
- returns: <[Promise]<[Page]>> Promise which resolves to the created page.
|
- returns: <[Promise]<[Page]>> Promise which resolves to the created page.
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
|
||||||
constructor(connection: CRConnection) {
|
constructor(connection: CRConnection) {
|
||||||
super();
|
super();
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._client = connection.rootSession;
|
this._client = this._connection.rootSession;
|
||||||
|
|
||||||
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
|
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
|
||||||
this._connection.on(ConnectionEvents.Disconnected, () => {
|
this._connection.on(ConnectionEvents.Disconnected, () => {
|
||||||
|
|
@ -120,14 +120,18 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
|
||||||
try {
|
try {
|
||||||
switch (targetInfo.type) {
|
switch (targetInfo.type) {
|
||||||
case 'page': {
|
case 'page': {
|
||||||
const page = await target.page();
|
const event = new PageEvent(target.pageOrError());
|
||||||
const event = new PageEvent(page!);
|
|
||||||
context.emit(CommonEvents.BrowserContext.Page, event);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 'background_page': {
|
case 'background_page': {
|
||||||
const page = await target.page();
|
const event = new PageEvent(target.pageOrError());
|
||||||
const event = new PageEvent(page!);
|
|
||||||
context.emit(Events.CRBrowserContext.BackgroundPage, event);
|
context.emit(Events.CRBrowserContext.BackgroundPage, event);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -268,16 +272,21 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
|
||||||
|
|
||||||
async pages(): Promise<Page[]> {
|
async pages(): Promise<Page[]> {
|
||||||
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
|
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
|
||||||
const pages = await Promise.all(targets.map(target => target.page()));
|
const pages = await Promise.all(targets.map(target => target.pageOrError()));
|
||||||
return pages.filter(page => !!page) as Page[];
|
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async newPage(): Promise<Page> {
|
async newPage(): Promise<Page> {
|
||||||
assertBrowserContextIsNotOwned(this);
|
assertBrowserContextIsNotOwned(this);
|
||||||
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
|
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
|
||||||
const target = this._browser._targets.get(targetId)!;
|
const target = this._browser._targets.get(targetId)!;
|
||||||
const page = await target.page();
|
const result = await target.pageOrError();
|
||||||
return page!;
|
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[]> {
|
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
|
||||||
|
|
@ -382,8 +391,8 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
|
||||||
|
|
||||||
async backgroundPages(): Promise<Page[]> {
|
async backgroundPages(): Promise<Page[]> {
|
||||||
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'background_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()));
|
const pages = await Promise.all(targets.map(target => target.pageOrError()));
|
||||||
return pages.filter(page => !!page) as Page[];
|
return pages.filter(page => (page instanceof Page) && !page.isClosed()) as Page[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(page: Page): Promise<CRSession> {
|
async createSession(page: Page): Promise<CRSession> {
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,10 @@ export class CRPage implements PageDelegate {
|
||||||
const openerTarget = CRTarget.fromPage(this._page).opener();
|
const openerTarget = CRTarget.fromPage(this._page).opener();
|
||||||
if (!openerTarget)
|
if (!openerTarget)
|
||||||
return null;
|
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> {
|
async reload(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
import { CRBrowser, CRBrowserContext } from './crBrowser';
|
import { CRBrowser, CRBrowserContext } from './crBrowser';
|
||||||
import { CRSession, CRSessionEvents } from './crConnection';
|
import { CRSession, CRSessionEvents } from './crConnection';
|
||||||
import { Events } from '../events';
|
|
||||||
import { Page, Worker } from '../page';
|
import { Page, Worker } from '../page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { debugError } from '../helper';
|
import { debugError } from '../helper';
|
||||||
|
|
@ -32,9 +31,8 @@ export class CRTarget {
|
||||||
private readonly _browserContext: CRBrowserContext;
|
private readonly _browserContext: CRBrowserContext;
|
||||||
readonly _targetId: string;
|
readonly _targetId: string;
|
||||||
readonly sessionFactory: () => Promise<CRSession>;
|
readonly sessionFactory: () => Promise<CRSession>;
|
||||||
private _pagePromiseFulfill: ((page: Page) => void) | null = null;
|
private _pagePromiseCallback: ((pageOrError: Page | Error) => void) | null = null;
|
||||||
private _pagePromiseReject: ((error: Error) => void) | null = null;
|
private _pagePromise: Promise<Page | Error> | null = null;
|
||||||
private _pagePromise: Promise<Page> | null = null;
|
|
||||||
_crPage: CRPage | null = null;
|
_crPage: CRPage | null = null;
|
||||||
private _workerPromise: Promise<Worker> | null = null;
|
private _workerPromise: Promise<Worker> | null = null;
|
||||||
|
|
||||||
|
|
@ -56,12 +54,8 @@ export class CRTarget {
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
this._targetId = targetInfo.targetId;
|
this._targetId = targetInfo.targetId;
|
||||||
this.sessionFactory = sessionFactory;
|
this.sessionFactory = sessionFactory;
|
||||||
if (CRTarget.isPageType(targetInfo.type)) {
|
if (CRTarget.isPageType(targetInfo.type))
|
||||||
this._pagePromise = new Promise<Page>((fulfill, reject) => {
|
this._pagePromise = new Promise<Page | Error>(f => this._pagePromiseCallback = f);
|
||||||
this._pagePromiseFulfill = fulfill;
|
|
||||||
this._pagePromiseReject = reject;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_didClose() {
|
_didClose() {
|
||||||
|
|
@ -69,10 +63,6 @@ export class CRTarget {
|
||||||
this._crPage.didClose();
|
this._crPage.didClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async page(): Promise<Page | null> {
|
|
||||||
return this._pagePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializePageSession(session: CRSession) {
|
async initializePageSession(session: CRSession) {
|
||||||
this._crPage = new CRPage(session, this._browser, this._browserContext);
|
this._crPage = new CRPage(session, this._browser, this._browserContext);
|
||||||
const page = this._crPage.page();
|
const page = this._crPage.page();
|
||||||
|
|
@ -80,20 +70,16 @@ export class CRTarget {
|
||||||
session.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
|
session.once(CRSessionEvents.Disconnected, () => page._didDisconnect());
|
||||||
try {
|
try {
|
||||||
await this._crPage.initialize();
|
await this._crPage.initialize();
|
||||||
this._pagePromiseFulfill!(page);
|
this._pagePromiseCallback!(page);
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
this._pagePromiseReject!(error);
|
this._pagePromiseCallback!(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.type() !== 'page')
|
async pageOrError(): Promise<Page | Error> {
|
||||||
return;
|
if (this._targetInfo.type !== 'page' && this._targetInfo.type !== 'background_page')
|
||||||
const opener = this.opener();
|
throw new Error('Not a page.');
|
||||||
if (!opener)
|
return this._pagePromise!;
|
||||||
return;
|
|
||||||
const openerPage = await opener.page();
|
|
||||||
if (!openerPage)
|
|
||||||
return;
|
|
||||||
openerPage.emit(Events.Page.Popup, page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async serviceWorker(): Promise<Worker | null> {
|
async serviceWorker(): Promise<Worker | null> {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import { Browser, createPageInNewContext } from '../browser';
|
import { Browser, createPageInNewContext } from '../browser';
|
||||||
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
|
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { assert, helper, RegisteredListener, debugError } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { Page, PageEvent, PageBinding } from '../page';
|
import { Page, PageEvent, PageBinding } from '../page';
|
||||||
|
|
@ -165,17 +165,16 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
|
||||||
const {targetId} = payload.targetInfo;
|
const {targetId} = payload.targetInfo;
|
||||||
const target = this._targets.get(targetId)!;
|
const target = this._targets.get(targetId)!;
|
||||||
target._initPagePromise(this._connection.getSession(payload.sessionId)!);
|
target._initPagePromise(this._connection.getSession(payload.sessionId)!);
|
||||||
const page = await target.page();
|
|
||||||
if (!page)
|
const pageEvent = new PageEvent(target.pageOrError());
|
||||||
return;
|
target.context().emit(Events.BrowserContext.Page, pageEvent);
|
||||||
target.context().emit(Events.BrowserContext.Page, new PageEvent(page));
|
|
||||||
|
|
||||||
const opener = target.opener();
|
const opener = target.opener();
|
||||||
if (opener && opener._pagePromise) {
|
if (!opener)
|
||||||
const openerPage = await opener._pagePromise;
|
return;
|
||||||
if (openerPage.listenerCount(Events.Page.Popup))
|
const openerPage = await opener.pageOrError();
|
||||||
openerPage.emit(Events.Page.Popup, page);
|
if (openerPage instanceof Page && !openerPage.isClosed())
|
||||||
}
|
openerPage.emit(Events.Page.Popup, pageEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
|
@ -192,7 +191,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Target {
|
class Target {
|
||||||
_pagePromise?: Promise<Page>;
|
_pagePromise?: Promise<Page | Error>;
|
||||||
_ffPage: FFPage | null = null;
|
_ffPage: FFPage | null = null;
|
||||||
private readonly _browser: FFBrowser;
|
private readonly _browser: FFBrowser;
|
||||||
private readonly _context: FFBrowserContext;
|
private readonly _context: FFBrowserContext;
|
||||||
|
|
@ -233,7 +232,7 @@ class Target {
|
||||||
return this._context;
|
return this._context;
|
||||||
}
|
}
|
||||||
|
|
||||||
async page(): Promise<Page> {
|
async pageOrError(): Promise<Page | Error> {
|
||||||
if (this._type !== 'page')
|
if (this._type !== 'page')
|
||||||
throw new Error(`Cannot create page for "${this._type}" target`);
|
throw new Error(`Cannot create page for "${this._type}" target`);
|
||||||
if (!this._pagePromise)
|
if (!this._pagePromise)
|
||||||
|
|
@ -247,12 +246,21 @@ class Target {
|
||||||
const openerTarget = this.opener();
|
const openerTarget = this.opener();
|
||||||
if (!openerTarget)
|
if (!openerTarget)
|
||||||
return null;
|
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;
|
const page = this._ffPage._page;
|
||||||
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
|
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
|
||||||
await this._ffPage._initialize().catch(debugError);
|
let pageOrError: Page | Error;
|
||||||
f(page);
|
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[]> {
|
async pages(): Promise<Page[]> {
|
||||||
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
|
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
|
||||||
const pages = await Promise.all(targets.map(target => target.page()));
|
const pages = await Promise.all(targets.map(target => target.pageOrError()));
|
||||||
return pages.filter(page => !!page);
|
return pages.filter(page => page instanceof Page && !page.isClosed()) as Page[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async newPage(): Promise<Page> {
|
async newPage(): Promise<Page> {
|
||||||
|
|
@ -319,7 +327,13 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
|
||||||
browserContextId: this._browserContextId || undefined
|
browserContextId: this._browserContextId || undefined
|
||||||
});
|
});
|
||||||
const target = this._browser._targets.get(targetId)!;
|
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[]> {
|
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
|
||||||
|
|
|
||||||
14
src/page.ts
14
src/page.ts
|
|
@ -89,14 +89,20 @@ export type FileChooser = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PageEvent {
|
export class PageEvent {
|
||||||
private readonly _page: Page;
|
private readonly _pageOrError: Promise<Page | Error>;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(pageOrErrorPromise: Promise<Page | Error>) {
|
||||||
this._page = page;
|
this._pageOrError = pageOrErrorPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async page(/* options?: frames.NavigateOptions */): Promise<Page> {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,16 @@ export class Chromium implements BrowserType {
|
||||||
const { timeout = 30000 } = options || {};
|
const { timeout = 30000 } = options || {};
|
||||||
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
|
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
|
||||||
const browser = await CRBrowser.connect(transport!, true);
|
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;
|
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();
|
browserContext.close = () => browserServer.close();
|
||||||
return browserContext;
|
return browserContext;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import { Browser, createPageInNewContext } from '../browser';
|
import { Browser, createPageInNewContext } from '../browser';
|
||||||
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
|
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 * as network from '../network';
|
||||||
import { Page, PageBinding, PageEvent } from '../page';
|
import { Page, PageBinding, PageEvent } from '../page';
|
||||||
import { ConnectionTransport, SlowMoTransport } from '../transport';
|
import { ConnectionTransport, SlowMoTransport } from '../transport';
|
||||||
|
|
@ -126,17 +126,14 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
|
||||||
this._firstPageProxyCallback = undefined;
|
this._firstPageProxyCallback = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
pageProxy.page().then(async page => {
|
const pageEvent = new PageEvent(pageProxy.pageOrError());
|
||||||
if (!page)
|
context.emit(Events.BrowserContext.Page, pageEvent);
|
||||||
return;
|
if (!opener)
|
||||||
context!.emit(Events.BrowserContext.Page, new PageEvent(page));
|
return;
|
||||||
if (!opener)
|
opener.pageOrError().then(openerPage => {
|
||||||
return;
|
if (openerPage instanceof Page && !openerPage.isClosed())
|
||||||
const openerPage = await opener.page();
|
openerPage.emit(Events.Page.Popup, pageEvent);
|
||||||
if (!openerPage || page.isClosed())
|
});
|
||||||
return;
|
|
||||||
openerPage.emit(Events.Page.Popup, page);
|
|
||||||
}).catch(debugError); // Just not emit the event in case of initialization failure.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
|
_onPageProxyDestroyed(event: Protocol.Browser.pageProxyDestroyedPayload) {
|
||||||
|
|
@ -233,16 +230,21 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
|
||||||
|
|
||||||
async pages(): Promise<Page[]> {
|
async pages(): Promise<Page[]> {
|
||||||
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
|
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
|
||||||
const pages = await Promise.all(pageProxies.map(proxy => proxy.page()));
|
const pages = await Promise.all(pageProxies.map(proxy => proxy.pageOrError()));
|
||||||
return pages.filter(page => !!page) as Page[];
|
return pages.filter(page => page instanceof Page && !page.isClosed()) as Page[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async newPage(): Promise<Page> {
|
async newPage(): Promise<Page> {
|
||||||
assertBrowserContextIsNotOwned(this);
|
assertBrowserContextIsNotOwned(this);
|
||||||
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
|
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
|
||||||
const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
|
const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
|
||||||
const page = await pageProxy.page();
|
const result = await pageProxy.pageOrError();
|
||||||
return page!;
|
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[]> {
|
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
|
||||||
|
|
|
||||||
|
|
@ -437,8 +437,12 @@ export class WKPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async opener(): Promise<Page | null> {
|
async opener(): Promise<Page | null> {
|
||||||
const openerPage = this._opener ? await this._opener.page() : null;
|
if (!this._opener)
|
||||||
return openerPage && !openerPage.isClosed() ? openerPage : null;
|
return null;
|
||||||
|
const openerPage = await this._opener.pageOrError();
|
||||||
|
if (openerPage instanceof Page && !openerPage.isClosed())
|
||||||
|
return openerPage;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,8 @@ export class WKPageProxy {
|
||||||
private readonly _pageProxySession: WKSession;
|
private readonly _pageProxySession: WKSession;
|
||||||
readonly _browserContext: WKBrowserContext;
|
readonly _browserContext: WKBrowserContext;
|
||||||
private readonly _opener: WKPageProxy | null;
|
private readonly _opener: WKPageProxy | null;
|
||||||
private readonly _pagePromise: Promise<Page | null>;
|
private readonly _pagePromise: Promise<Page | Error>;
|
||||||
private _pagePromiseFulfill: (page: Page | null) => void = () => {};
|
private _pagePromiseCallback: (page: Page | Error) => void = () => {};
|
||||||
private _pagePromiseReject: (error: Error) => void = () => {};
|
|
||||||
private readonly _wkPage: WKPage;
|
private readonly _wkPage: WKPage;
|
||||||
private _initialized = false;
|
private _initialized = false;
|
||||||
private readonly _sessions = new Map<string, WKSession>();
|
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.dispatchMessageFromTarget', this._onDispatchMessageFromTarget.bind(this)),
|
||||||
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
|
helper.addEventListener(this._pageProxySession, 'Target.didCommitProvisionalTarget', this._onDidCommitProvisionalTarget.bind(this)),
|
||||||
];
|
];
|
||||||
this._pagePromise = new Promise((f, r) => {
|
this._pagePromise = new Promise(f => this._pagePromiseCallback = f);
|
||||||
this._pagePromiseFulfill = f;
|
|
||||||
this._pagePromiseReject = r;
|
|
||||||
});
|
|
||||||
this._wkPage = new WKPage(this._browserContext, this._pageProxySession, this._opener);
|
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);
|
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;
|
return this._pagePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,21 +108,16 @@ export class WKPageProxy {
|
||||||
if (!this._initialized) {
|
if (!this._initialized) {
|
||||||
assert(!targetInfo.isProvisional);
|
assert(!targetInfo.isProvisional);
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
let page: Page | null = null;
|
let pageOrError: Page | Error;
|
||||||
let error: Error | undefined;
|
|
||||||
try {
|
try {
|
||||||
await this._wkPage.initialize(session);
|
await this._wkPage.initialize(session);
|
||||||
page = this._wkPage._page;
|
pageOrError = this._wkPage._page;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!this._pageProxySession.isDisposed())
|
pageOrError = e;
|
||||||
error = e;
|
|
||||||
}
|
}
|
||||||
if (targetInfo.isPaused)
|
if (targetInfo.isPaused)
|
||||||
this._resumeTarget(targetInfo.targetId);
|
this._resumeTarget(targetInfo.targetId);
|
||||||
if (error)
|
this._pagePromiseCallback(pageOrError);
|
||||||
this._pagePromiseReject(error);
|
|
||||||
else
|
|
||||||
this._pagePromiseFulfill(page);
|
|
||||||
} else {
|
} else {
|
||||||
assert(targetInfo.isProvisional);
|
assert(targetInfo.isProvisional);
|
||||||
(session as any)[isPovisionalSymbol] = true;
|
(session as any)[isPovisionalSymbol] = true;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const [popup] = await Promise.all([
|
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)
|
page.evaluate(url => window.open(url), server.EMPTY_PAGE)
|
||||||
]);
|
]);
|
||||||
expect(popup.context()).toBe(context);
|
expect(popup.context()).toBe(context);
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
|
||||||
it('should work for adopted elements', async({page,server}) => {
|
it('should work for adopted elements', async({page,server}) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const [popup] = await Promise.all([
|
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),
|
page.evaluate(url => window.__popup = window.open(url), server.EMPTY_PAGE),
|
||||||
]);
|
]);
|
||||||
const divHandle = await page.evaluateHandle(() => {
|
const divHandle = await page.evaluateHandle(() => {
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
|
||||||
describe('Page.opener', function() {
|
describe('Page.opener', function() {
|
||||||
it('should provide access to the opener page', async({page}) => {
|
it('should provide access to the opener page', async({page}) => {
|
||||||
const [popup] = await Promise.all([
|
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')),
|
page.evaluate(() => window.open('about:blank')),
|
||||||
]);
|
]);
|
||||||
const opener = await popup.opener();
|
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}) => {
|
it('should return null if parent page has been closed', async({page}) => {
|
||||||
const [popup] = await Promise.all([
|
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')),
|
page.evaluate(() => window.open('about:blank')),
|
||||||
]);
|
]);
|
||||||
await page.close();
|
await page.close();
|
||||||
|
|
@ -1077,7 +1077,7 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
|
||||||
|
|
||||||
describe('Page.Events.Close', function() {
|
describe('Page.Events.Close', function() {
|
||||||
it('should work with window.close', async function({ page, context, server }) {
|
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'));
|
await page.evaluate(() => window['newPage'] = window.open('about:blank'));
|
||||||
const newPage = await newPagePromise;
|
const newPage = await newPagePromise;
|
||||||
const closedPromise = new Promise(x => newPage.on('close', x));
|
const closedPromise = new Promise(x => newPage.on('close', x));
|
||||||
|
|
|
||||||
|
|
@ -139,18 +139,31 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const [popup] = await Promise.all([
|
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')),
|
page.evaluate(() => window.__popup = window.open('about:blank')),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
||||||
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
|
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
|
||||||
await context.close();
|
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}) => {
|
it('should work with empty url', async({browser}) => {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const [popup] = await Promise.all([
|
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('')),
|
page.evaluate(() => window.__popup = window.open('')),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
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 context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const [popup] = await Promise.all([
|
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')),
|
page.evaluate(() => window.__popup = window.open('about:blank', null, 'noopener')),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
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.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>');
|
await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>');
|
||||||
const [popup] = await Promise.all([
|
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'),
|
page.click('a'),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
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.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
||||||
const [popup] = await Promise.all([
|
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()),
|
page.$eval('a', a => a.click()),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
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.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
||||||
const [popup] = await Promise.all([
|
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'),
|
page.click('a'),
|
||||||
]);
|
]);
|
||||||
expect(await page.evaluate(() => !!window.opener)).toBe(false);
|
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.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
|
||||||
const [popup] = await Promise.all([
|
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'),
|
page.click('a'),
|
||||||
]);
|
]);
|
||||||
let badSecondPopup = false;
|
let badSecondPopup = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue