Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Commit

Permalink
feat: add WebContents.opener and webContents.fromFrame() (electron#35140
Browse files Browse the repository at this point in the history
)

* feat: add WebContents.opener

* feat: add webContents.fromFrame(frame)

* fix: unknown type name

* test: fix and add more fromFrame cases

* docs: clarified terminology
  • Loading branch information
samuelmaddock authored and khalwa committed Feb 22, 2023
1 parent a371a2f commit 61c760a
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 1 deletion.
12 changes: 12 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -45,6 +45,13 @@ returns `null`.
Returns `WebContents` | undefined - A WebContents instance with the given ID, or
`undefined` if there is no WebContents associated with the given ID.

### `webContents.fromFrame(frame)`

* `frame` WebFrameMain

Returns `WebContents` | undefined - A WebContents instance with the given WebFrameMain, or
`undefined` if there is no WebContents associated with the given WebFrameMain.

### `webContents.fromDevToolsTargetId(targetId)`

* `targetId` string - The Chrome DevTools Protocol [TargetID](https://chromedevtools.github.io/devtools-protocol/tot/Target/#type-TargetID) associated with the WebContents instance.
Expand Down Expand Up @@ -2044,6 +2051,11 @@ when the page becomes backgrounded. This also affects the Page Visibility API.

A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of the page's frame hierarchy.

#### `contents.opener` _Readonly_

A [`WebFrameMain`](web-frame-main.md) property that represents the frame that opened this WebContents, either
with open(), or by navigating a link with a target attribute.

[keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
Expand Down
4 changes: 4 additions & 0 deletions lib/browser/api/web-contents.ts
Expand Up @@ -843,6 +843,10 @@ export function fromId (id: string) {
return binding.fromId(id);
}

export function fromFrame (frame: Electron.WebFrameMain) {
return binding.fromFrame(frame);
}

export function fromDevToolsTargetId (targetId: string) {
return binding.fromDevToolsTargetId(targetId);
}
Expand Down
16 changes: 16 additions & 0 deletions shell/browser/api/electron_api_web_contents.cc
Expand Up @@ -3449,6 +3449,10 @@ content::RenderFrameHost* WebContents::MainFrame() {
return web_contents()->GetPrimaryMainFrame();
}

content::RenderFrameHost* WebContents::Opener() {
return web_contents()->GetOpener();
}

void WebContents::NotifyUserActivation() {
content::RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame();
if (frame)
Expand Down Expand Up @@ -4060,6 +4064,7 @@ v8::Local<v8::ObjectTemplate> WebContents::FillObjectTemplate(
.SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents)
.SetProperty("debugger", &WebContents::Debugger)
.SetProperty("mainFrame", &WebContents::MainFrame)
.SetProperty("opener", &WebContents::Opener)
.Build();
}

Expand Down Expand Up @@ -4174,13 +4179,23 @@ namespace {

using electron::api::GetAllWebContents;
using electron::api::WebContents;
using electron::api::WebFrameMain;

gin::Handle<WebContents> WebContentsFromID(v8::Isolate* isolate, int32_t id) {
WebContents* contents = WebContents::FromID(id);
return contents ? gin::CreateHandle(isolate, contents)
: gin::Handle<WebContents>();
}

gin::Handle<WebContents> WebContentsFromFrame(v8::Isolate* isolate,
WebFrameMain* web_frame) {
content::RenderFrameHost* rfh = web_frame->render_frame_host();
content::WebContents* source = content::WebContents::FromRenderFrameHost(rfh);
WebContents* contents = WebContents::From(source);
return contents ? gin::CreateHandle(isolate, contents)
: gin::Handle<WebContents>();
}

gin::Handle<WebContents> WebContentsFromDevToolsTargetID(
v8::Isolate* isolate,
std::string target_id) {
Expand Down Expand Up @@ -4209,6 +4224,7 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict(isolate, exports);
dict.Set("WebContents", WebContents::GetConstructor(context));
dict.SetMethod("fromId", &WebContentsFromID);
dict.SetMethod("fromFrame", &WebContentsFromFrame);
dict.SetMethod("fromDevToolsTargetId", &WebContentsFromDevToolsTargetID);
dict.SetMethod("getAllWebContents", &GetAllWebContentsAsV8);
}
Expand Down
1 change: 1 addition & 0 deletions shell/browser/api/electron_api_web_contents.h
Expand Up @@ -333,6 +333,7 @@ class WebContents : public ExclusiveAccessContext,
v8::Local<v8::Value> DevToolsWebContents(v8::Isolate* isolate);
v8::Local<v8::Value> Debugger(v8::Isolate* isolate);
content::RenderFrameHost* MainFrame();
content::RenderFrameHost* Opener();

WebContentsZoomController* GetZoomController() { return zoom_controller_; }

Expand Down
72 changes: 71 additions & 1 deletion spec/api-web-contents-spec.ts
Expand Up @@ -6,7 +6,7 @@ import * as http from 'http';
import { BrowserWindow, ipcMain, webContents, session, WebContents, app, BrowserView } from 'electron/main';
import { emittedOnce } from './events-helpers';
import { closeAllWindows } from './window-helpers';
import { ifdescribe, delay, defer } from './spec-helpers';
import { ifdescribe, delay, defer, waitUntil } from './spec-helpers';

const pdfjs = require('pdfjs-dist');
const fixturesPath = path.resolve(__dirname, 'fixtures');
Expand Down Expand Up @@ -46,6 +46,28 @@ describe('webContents module', () => {
});
});

describe('fromFrame()', () => {
it('returns WebContents for mainFrame', () => {
const contents = (webContents as any).create() as WebContents;
expect(webContents.fromFrame(contents.mainFrame)).to.equal(contents);
});
it('returns undefined for disposed frame', async () => {
const contents = (webContents as any).create() as WebContents;
const { mainFrame } = contents;
contents.destroy();
await waitUntil(() => typeof webContents.fromFrame(mainFrame) === 'undefined');
});
it('throws when passing invalid argument', async () => {
let errored = false;
try {
webContents.fromFrame({} as any);
} catch {
errored = true;
}
expect(errored).to.be.true();
});
});

describe('fromDevToolsTargetId()', () => {
it('returns WebContents for attached DevTools target', async () => {
const w = new BrowserWindow({ show: false });
Expand Down Expand Up @@ -1281,6 +1303,54 @@ describe('webContents module', () => {
});
});

describe('opener api', () => {
afterEach(closeAllWindows);
it('can get opener with window.open()', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
await w.loadURL('about:blank');
const childPromise = emittedOnce(w.webContents, 'did-create-window');
w.webContents.executeJavaScript('window.open("about:blank")', true);
const [childWindow] = await childPromise;
expect(childWindow.webContents.opener).to.equal(w.webContents.mainFrame);
});
it('has no opener when using "noopener"', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
await w.loadURL('about:blank');
const childPromise = emittedOnce(w.webContents, 'did-create-window');
w.webContents.executeJavaScript('window.open("about:blank", undefined, "noopener")', true);
const [childWindow] = await childPromise;
expect(childWindow.webContents.opener).to.be.null();
});
it('can get opener with a[target=_blank][rel=opener]', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
await w.loadURL('about:blank');
const childPromise = emittedOnce(w.webContents, 'did-create-window');
w.webContents.executeJavaScript(`(function() {
const a = document.createElement('a');
a.target = '_blank';
a.rel = 'opener';
a.href = 'about:blank';
a.click();
}())`, true);
const [childWindow] = await childPromise;
expect(childWindow.webContents.opener).to.equal(w.webContents.mainFrame);
});
it('has no opener with a[target=_blank][rel=noopener]', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } });
await w.loadURL('about:blank');
const childPromise = emittedOnce(w.webContents, 'did-create-window');
w.webContents.executeJavaScript(`(function() {
const a = document.createElement('a');
a.target = '_blank';
a.rel = 'noopener';
a.href = 'about:blank';
a.click();
}())`, true);
const [childWindow] = await childPromise;
expect(childWindow.webContents.opener).to.be.null();
});
});

describe('render view deleted events', () => {
let server: http.Server;
let serverUrl: string;
Expand Down

0 comments on commit 61c760a

Please sign in to comment.