Skip to content

Commit 25f2a32

Browse files
authored
feat: add Page.opener() to the API (#790)
Fixes #783
1 parent 1489fbd commit 25f2a32

File tree

9 files changed

+76
-6
lines changed

9 files changed

+76
-6
lines changed

docs/api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ page.removeListener('request', logRequest);
467467
- [page.keyboard](#pagekeyboard)
468468
- [page.mainFrame()](#pagemainframe)
469469
- [page.mouse](#pagemouse)
470+
- [page.opener()](#pageopener)
470471
- [page.pdf([options])](#pagepdfoptions)
471472
- [page.reload([options])](#pagereloadoptions)
472473
- [page.screenshot([options])](#pagescreenshotoptions)
@@ -1098,6 +1099,10 @@ Page is guaranteed to have a main frame which persists during navigations.
10981099

10991100
- returns: <[Mouse]>
11001101

1102+
#### page.opener()
1103+
1104+
- returns: <[Promise]<?[Page]>> Promise which resolves to the opener for popup pages and `null` for others. If the opener has been closed already the promise may resolve to `null`.
1105+
11011106
#### page.pdf([options])
11021107
- `options` <[Object]> Options object which might have the following properties:
11031108
- `path` <[string]> The file path to save the PDF to. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the PDF won't be saved to the disk.

src/chromium/crPage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { BrowserContext } from '../browserContext';
3838
import * as types from '../types';
3939
import { ConsoleMessage } from '../console';
4040
import * as platform from '../platform';
41+
import { CRTarget } from './crTarget';
4142

4243
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
4344

@@ -349,6 +350,13 @@ export class CRPage implements PageDelegate {
349350
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
350351
}
351352

353+
async opener() : Promise<Page | null> {
354+
const openerTarget = CRTarget.fromPage(this._page).opener();
355+
if (!openerTarget)
356+
return null;
357+
return await openerTarget.page();
358+
}
359+
352360
async reload(): Promise<void> {
353361
await this._client.send('Page.reload');
354362
}

src/firefox/ffBrowser.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,12 @@ class Target {
280280
if (!this._pagePromise) {
281281
this._pagePromise = new Promise(async f => {
282282
const session = await this._connection.createSession(this._targetId);
283-
this._ffPage = new FFPage(session, this._context);
283+
this._ffPage = new FFPage(session, this._context, async () => {
284+
const openerTarget = this.opener();
285+
if (!openerTarget)
286+
return null;
287+
return await openerTarget.page();
288+
});
284289
const page = this._ffPage._page;
285290
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
286291
await this._ffPage._initialize();

src/firefox/ffPage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ export class FFPage implements PageDelegate {
4141
readonly _session: FFSession;
4242
readonly _page: Page;
4343
readonly _networkManager: FFNetworkManager;
44+
private readonly _openerResolver: () => Promise<Page | null>;
4445
private readonly _contextIdToContext: Map<string, dom.FrameExecutionContext>;
4546
private _eventListeners: RegisteredListener[];
4647
private _workers = new Map<string, { frameId: string, session: FFSession }>();
4748

48-
constructor(session: FFSession, browserContext: BrowserContext) {
49+
constructor(session: FFSession, browserContext: BrowserContext, openerResolver: () => Promise<Page | null>) {
4950
this._session = session;
51+
this._openerResolver = openerResolver;
5052
this.rawKeyboard = new RawKeyboardImpl(session);
5153
this.rawMouse = new RawMouseImpl(session);
5254
this._contextIdToContext = new Map();
@@ -305,6 +307,10 @@ export class FFPage implements PageDelegate {
305307
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
306308
}
307309

310+
async opener() : Promise<Page | null> {
311+
return await this._openerResolver();
312+
}
313+
308314
async reload(): Promise<void> {
309315
await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id });
310316
}

src/page.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface PageDelegate {
3434
readonly rawMouse: input.RawMouse;
3535
readonly rawKeyboard: input.RawKeyboard;
3636

37+
opener(): Promise<Page | null>;
38+
3739
reload(): Promise<void>;
3840
goBack(): Promise<boolean>;
3941
goForward(): Promise<boolean>;
@@ -173,6 +175,10 @@ export class Page extends platform.EventEmitter {
173175
return this._browserContext;
174176
}
175177

178+
async opener(): Promise<Page | null> {
179+
return await this._delegate.opener();
180+
}
181+
176182
mainFrame(): frames.Frame {
177183
return this._frameManager.mainFrame();
178184
}

src/webkit/wkBrowser.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
113113
const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => {
114114
this._connection.rawSend({ ...message, pageProxyId });
115115
});
116-
const pageProxy = new WKPageProxy(pageProxySession, context);
116+
const pageProxy = new WKPageProxy(pageProxySession, context, () => {
117+
if (!pageProxyInfo.openerId)
118+
return null;
119+
const opener = this._pageProxies.get(pageProxyInfo.openerId);
120+
if (!opener)
121+
return null;
122+
return opener;
123+
});
117124
this._pageProxies.set(pageProxyId, pageProxy);
118125

119126
if (pageProxyInfo.openerId) {

src/webkit/wkPage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ export class WKPage implements PageDelegate {
4545
private _provisionalPage: WKProvisionalPage | null = null;
4646
readonly _page: Page;
4747
private readonly _pageProxySession: WKSession;
48+
private readonly _openerResolver: () => Promise<Page | null>;
4849
private readonly _requestIdToRequest = new Map<string, WKInterceptableRequest>();
4950
private readonly _workers: WKWorkers;
5051
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
5152
private _mainFrameContextId?: number;
5253
private _sessionListeners: RegisteredListener[] = [];
5354
private readonly _bootstrapScripts: string[] = [];
5455

55-
constructor(browserContext: BrowserContext, pageProxySession: WKSession) {
56+
constructor(browserContext: BrowserContext, pageProxySession: WKSession, openerResolver: () => Promise<Page | null>) {
5657
this._pageProxySession = pageProxySession;
58+
this._openerResolver = openerResolver;
5759
this.rawKeyboard = new RawKeyboardImpl(pageProxySession);
5860
this.rawMouse = new RawMouseImpl(pageProxySession);
5961
this._contextIdToContext = new Map();
@@ -415,6 +417,10 @@ export class WKPage implements PageDelegate {
415417
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
416418
}
417419

420+
async opener() {
421+
return await this._openerResolver();
422+
}
423+
418424
async reload(): Promise<void> {
419425
await this._session.send('Page.reload');
420426
}

src/webkit/wkPageProxy.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const isPovisionalSymbol = Symbol('isPovisional');
2828
export class WKPageProxy {
2929
private readonly _pageProxySession: WKSession;
3030
readonly _browserContext: BrowserContext;
31+
private readonly _openerResolver: () => WKPageProxy | null;
3132
private _pagePromise: Promise<Page> | null = null;
3233
private _wkPage: WKPage | null = null;
3334
private readonly _firstTargetPromise: Promise<void>;
@@ -36,9 +37,10 @@ export class WKPageProxy {
3637
private readonly _sessions = new Map<string, WKSession>();
3738
private readonly _eventListeners: RegisteredListener[];
3839

39-
constructor(pageProxySession: WKSession, browserContext: BrowserContext) {
40+
constructor(pageProxySession: WKSession, browserContext: BrowserContext, openerResolver: () => (WKPageProxy | null)) {
4041
this._pageProxySession = pageProxySession;
4142
this._browserContext = browserContext;
43+
this._openerResolver = openerResolver;
4244
this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r);
4345
this._eventListeners = [
4446
helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)),
@@ -111,7 +113,12 @@ export class WKPageProxy {
111113
}
112114
}
113115
assert(session, 'One non-provisional target session must exist');
114-
this._wkPage = new WKPage(this._browserContext, this._pageProxySession);
116+
this._wkPage = new WKPage(this._browserContext, this._pageProxySession, async () => {
117+
const pageProxy = this._openerResolver();
118+
if (!pageProxy)
119+
return null;
120+
return await pageProxy.page();
121+
});
115122
await this._wkPage.initialize(session!);
116123
if (this._pagePausedOnStart) {
117124
this._resumeTarget(session!.sessionId);

test/page.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
186186
});
187187
});
188188

189+
describe('Page.opener', function() {
190+
it('should provide access to the opener page', async({page}) => {
191+
const [popup] = await Promise.all([
192+
new Promise(x => page.once('popup', x)),
193+
page.evaluate(() => window.open('about:blank')),
194+
]);
195+
const opener = await popup.opener();
196+
expect(opener).toBe(page);
197+
});
198+
it('should return null if parent page has been closed', async({page}) => {
199+
const [popup] = await Promise.all([
200+
new Promise(x => page.once('popup', x)),
201+
page.evaluate(() => window.open('about:blank')),
202+
]);
203+
await page.close();
204+
const opener = await popup.opener();
205+
expect(opener).toBe(null);
206+
});
207+
});
208+
189209
describe('Page.Events.Console', function() {
190210
it('should work', async({page, server}) => {
191211
let message = null;

0 commit comments

Comments
 (0)