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() #35140

Merged
merged 5 commits into from Sep 26, 2022
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm... in what situation is there no WebContents for a WebFrameMain?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More accurately, this is for when the WebContents has been destroyed while the frame reference is held around. One of the tests in this PR checks this.


### `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 @@ -2029,6 +2036,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 @@ -830,6 +830,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 @@ -3433,6 +3433,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 @@ -4043,6 +4047,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 @@ -4157,13 +4162,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 @@ -4192,6 +4207,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/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