diff --git a/docs/api.md b/docs/api.md index 45816e00e5..5fede05aea 100644 --- a/docs/api.md +++ b/docs/api.md @@ -467,6 +467,7 @@ page.removeListener('request', logRequest); - [page.keyboard](#pagekeyboard) - [page.mainFrame()](#pagemainframe) - [page.mouse](#pagemouse) +- [page.opener()](#pageopener) - [page.pdf([options])](#pagepdfoptions) - [page.reload([options])](#pagereloadoptions) - [page.screenshot([options])](#pagescreenshotoptions) @@ -1098,6 +1099,10 @@ Page is guaranteed to have a main frame which persists during navigations. - returns: <[Mouse]> +#### page.opener() + +- returns: <[Promise]> 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`. + #### page.pdf([options]) - `options` <[Object]> Options object which might have the following properties: - `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. diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index c94daa444e..bebe76be0e 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -38,6 +38,7 @@ import { BrowserContext } from '../browserContext'; import * as types from '../types'; import { ConsoleMessage } from '../console'; import * as platform from '../platform'; +import { CRTarget } from './crTarget'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -349,6 +350,13 @@ export class CRPage implements PageDelegate { await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. } + async opener() : Promise { + const openerTarget = CRTarget.fromPage(this._page).opener(); + if (!openerTarget) + return null; + return await openerTarget.page(); + } + async reload(): Promise { await this._client.send('Page.reload'); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index c46814a76c..35f0b3348b 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -280,7 +280,12 @@ class Target { if (!this._pagePromise) { this._pagePromise = new Promise(async f => { const session = await this._connection.createSession(this._targetId); - this._ffPage = new FFPage(session, this._context); + this._ffPage = new FFPage(session, this._context, async () => { + const openerTarget = this.opener(); + if (!openerTarget) + return null; + return await openerTarget.page(); + }); const page = this._ffPage._page; session.once(FFSessionEvents.Disconnected, () => page._didDisconnect()); await this._ffPage._initialize(); diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index af4b362f7c..847a85a9f3 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -41,12 +41,14 @@ export class FFPage implements PageDelegate { readonly _session: FFSession; readonly _page: Page; readonly _networkManager: FFNetworkManager; + private readonly _openerResolver: () => Promise; private readonly _contextIdToContext: Map; private _eventListeners: RegisteredListener[]; private _workers = new Map(); - constructor(session: FFSession, browserContext: BrowserContext) { + constructor(session: FFSession, browserContext: BrowserContext, openerResolver: () => Promise) { this._session = session; + this._openerResolver = openerResolver; this.rawKeyboard = new RawKeyboardImpl(session); this.rawMouse = new RawMouseImpl(session); this._contextIdToContext = new Map(); @@ -305,6 +307,10 @@ export class FFPage implements PageDelegate { await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. } + async opener() : Promise { + return await this._openerResolver(); + } + async reload(): Promise { await this._session.send('Page.reload', { frameId: this._page.mainFrame()._id }); } diff --git a/src/page.ts b/src/page.ts index da30913364..46cf767194 100644 --- a/src/page.ts +++ b/src/page.ts @@ -34,6 +34,8 @@ export interface PageDelegate { readonly rawMouse: input.RawMouse; readonly rawKeyboard: input.RawKeyboard; + opener(): Promise; + reload(): Promise; goBack(): Promise; goForward(): Promise; @@ -173,6 +175,10 @@ export class Page extends platform.EventEmitter { return this._browserContext; } + async opener(): Promise { + return await this._delegate.opener(); + } + mainFrame(): frames.Frame { return this._frameManager.mainFrame(); } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index af84c23f85..d49f9b56a4 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -113,7 +113,14 @@ export class WKBrowser extends platform.EventEmitter implements Browser { const pageProxySession = new WKSession(this._connection, pageProxyId, `The page has been closed.`, (message: any) => { this._connection.rawSend({ ...message, pageProxyId }); }); - const pageProxy = new WKPageProxy(pageProxySession, context); + const pageProxy = new WKPageProxy(pageProxySession, context, () => { + if (!pageProxyInfo.openerId) + return null; + const opener = this._pageProxies.get(pageProxyInfo.openerId); + if (!opener) + return null; + return opener; + }); this._pageProxies.set(pageProxyId, pageProxy); if (pageProxyInfo.openerId) { diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 8a49093b6a..13629bb8b5 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -45,6 +45,7 @@ export class WKPage implements PageDelegate { private _provisionalPage: WKProvisionalPage | null = null; readonly _page: Page; private readonly _pageProxySession: WKSession; + private readonly _openerResolver: () => Promise; private readonly _requestIdToRequest = new Map(); private readonly _workers: WKWorkers; private readonly _contextIdToContext: Map; @@ -52,8 +53,9 @@ export class WKPage implements PageDelegate { private _sessionListeners: RegisteredListener[] = []; private readonly _bootstrapScripts: string[] = []; - constructor(browserContext: BrowserContext, pageProxySession: WKSession) { + constructor(browserContext: BrowserContext, pageProxySession: WKSession, openerResolver: () => Promise) { this._pageProxySession = pageProxySession; + this._openerResolver = openerResolver; this.rawKeyboard = new RawKeyboardImpl(pageProxySession); this.rawMouse = new RawMouseImpl(pageProxySession); this._contextIdToContext = new Map(); @@ -415,6 +417,10 @@ export class WKPage implements PageDelegate { await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. } + async opener() { + return await this._openerResolver(); + } + async reload(): Promise { await this._session.send('Page.reload'); } diff --git a/src/webkit/wkPageProxy.ts b/src/webkit/wkPageProxy.ts index a18b87fe9d..5124e5499e 100644 --- a/src/webkit/wkPageProxy.ts +++ b/src/webkit/wkPageProxy.ts @@ -28,6 +28,7 @@ const isPovisionalSymbol = Symbol('isPovisional'); export class WKPageProxy { private readonly _pageProxySession: WKSession; readonly _browserContext: BrowserContext; + private readonly _openerResolver: () => WKPageProxy | null; private _pagePromise: Promise | null = null; private _wkPage: WKPage | null = null; private readonly _firstTargetPromise: Promise; @@ -36,9 +37,10 @@ export class WKPageProxy { private readonly _sessions = new Map(); private readonly _eventListeners: RegisteredListener[]; - constructor(pageProxySession: WKSession, browserContext: BrowserContext) { + constructor(pageProxySession: WKSession, browserContext: BrowserContext, openerResolver: () => (WKPageProxy | null)) { this._pageProxySession = pageProxySession; this._browserContext = browserContext; + this._openerResolver = openerResolver; this._firstTargetPromise = new Promise(r => this._firstTargetCallback = r); this._eventListeners = [ helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)), @@ -111,7 +113,12 @@ export class WKPageProxy { } } assert(session, 'One non-provisional target session must exist'); - this._wkPage = new WKPage(this._browserContext, this._pageProxySession); + this._wkPage = new WKPage(this._browserContext, this._pageProxySession, async () => { + const pageProxy = this._openerResolver(); + if (!pageProxy) + return null; + return await pageProxy.page(); + }); await this._wkPage.initialize(session!); if (this._pagePausedOnStart) { this._resumeTarget(session!.sessionId); diff --git a/test/page.spec.js b/test/page.spec.js index 30538a05d0..c44d0522c9 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -186,6 +186,26 @@ 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.evaluate(() => window.open('about:blank')), + ]); + const opener = await popup.opener(); + expect(opener).toBe(page); + }); + 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.evaluate(() => window.open('about:blank')), + ]); + await page.close(); + const opener = await popup.opener(); + expect(opener).toBe(null); + }); + }); + describe('Page.Events.Console', function() { it('should work', async({page, server}) => { let message = null;