Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add WebContents.opener and webContents.fromFrame() #35819

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -2079,6 +2086,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 @@ -838,6 +838,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 @@ -3435,6 +3435,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 @@ -4045,6 +4049,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 @@ -4159,13 +4164,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 @@ -4194,6 +4209,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 @@ -332,6 +332,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-main/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, '..', 'spec', '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