From e25de07657b44efe5a0bda3d0fed3faf8d73d680 Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Thu, 21 Jan 2021 07:49:02 +0100 Subject: [PATCH] feat: add webFrameMain.send() / webFrameMain.postMessage() (#26807) (#27366) --- docs/api/web-frame-main.md | 41 +++++ lib/browser/api/web-contents.ts | 45 ++--- lib/browser/api/web-frame-main.ts | 27 ++- lib/browser/init.ts | 3 + .../browser/api/electron_api_web_contents.cc | 75 -------- shell/browser/api/electron_api_web_contents.h | 9 - .../api/electron_api_web_frame_main.cc | 81 ++++++++- .../browser/api/electron_api_web_frame_main.h | 20 ++- shell/common/api/electron_api_native_image.h | 2 - spec-main/api-ipc-spec.ts | 169 +++++++++--------- spec-main/api-subframe-spec.ts | 45 ++++- spec-main/api-web-frame-main-spec.ts | 20 ++- typings/internal-ambient.d.ts | 4 + typings/internal-electron.d.ts | 8 +- 14 files changed, 340 insertions(+), 209 deletions(-) diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md index 703dd2e127..1efcdbe0a1 100644 --- a/docs/api/web-frame-main.md +++ b/docs/api/web-frame-main.md @@ -101,6 +101,47 @@ Works like `executeJavaScript` but evaluates `scripts` in an isolated context. Returns `boolean` - Whether the reload was initiated successfully. Only results in `false` when the frame has no history. +#### `frame.send(channel, ...args)` + +* `channel` String +* `...args` any[] + +Send an asynchronous message to the renderer process via `channel`, along with +arguments. Arguments will be serialized with the [Structured Clone +Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be +included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will +throw an exception. + +The renderer process can handle the message by listening to `channel` with the +[`ipcRenderer`](ipc-renderer.md) module. + +#### `frame.postMessage(channel, message, [transfer])` + +* `channel` String +* `message` any +* `transfer` MessagePortMain[] (optional) + +Send a message to the renderer process, optionally transferring ownership of +zero or more [`MessagePortMain`][] objects. + +The transferred `MessagePortMain` objects will be available in the renderer +process by accessing the `ports` property of the emitted event. When they +arrive in the renderer, they will be native DOM `MessagePort` objects. + +For example: + +```js +// Main process +const { port1, port2 } = new MessageChannelMain() +webContents.mainFrame.postMessage('port', { message: 'hello' }, [port1]) + +// Renderer process +ipcRenderer.on('port', (e, msg) => { + const [port] = e.ports + // ... +}) +``` + ### Instance Properties #### `frame.url` _Readonly_ diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts index 2b67c0ef2b..436943083e 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -126,6 +126,10 @@ const binding = process._linkedBinding('electron_browser_web_contents'); const printing = process._linkedBinding('electron_browser_printing'); const { WebContents } = binding as { WebContents: { prototype: Electron.WebContents } }; +WebContents.prototype.postMessage = function (...args) { + return this.mainFrame.postMessage(...args); +}; + WebContents.prototype.send = function (channel, ...args) { if (typeof channel !== 'string') { throw new Error('Missing required channel argument'); @@ -134,13 +138,6 @@ WebContents.prototype.send = function (channel, ...args) { return this._send(false /* internal */, channel, args); }; -WebContents.prototype.postMessage = function (...args) { - if (Array.isArray(args[2])) { - args[2] = args[2].map(o => o instanceof MessagePortMain ? o._internalPort : o); - } - this._postMessage(...args); -}; - WebContents.prototype._sendInternal = function (channel, ...args) { if (typeof channel !== 'string') { throw new Error('Missing required channel argument'); @@ -148,23 +145,29 @@ WebContents.prototype._sendInternal = function (channel, ...args) { return this._send(true /* internal */, channel, args); }; -WebContents.prototype.sendToFrame = function (frame, channel, ...args) { - if (typeof channel !== 'string') { - throw new Error('Missing required channel argument'); - } else if (!(typeof frame === 'number' || Array.isArray(frame))) { - throw new Error('Missing required frame argument (must be number or array)'); - } - return this._sendToFrame(false /* internal */, frame, channel, args); +function getWebFrame (contents: Electron.WebContents, frame: number | [number, number]) { + if (typeof frame === 'number') { + return webFrameMain.fromId(contents.mainFrame.processId, frame); + } else if (Array.isArray(frame) && frame.length === 2 && frame.every(value => typeof value === 'number')) { + return webFrameMain.fromId(frame[0], frame[1]); + } else { + throw new Error('Missing required frame argument (must be number or [processId, frameId])'); + } +} + +WebContents.prototype.sendToFrame = function (frameId, channel, ...args) { + const frame = getWebFrame(this, frameId); + if (!frame) return false; + frame.send(channel, ...args); + return true; }; -WebContents.prototype._sendToFrameInternal = function (frame, channel, ...args) { - if (typeof channel !== 'string') { - throw new Error('Missing required channel argument'); - } else if (!(typeof frame === 'number' || Array.isArray(frame))) { - throw new Error('Missing required frame argument (must be number or array)'); - } - return this._sendToFrame(true /* internal */, frame, channel, args); +WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) { + const frame = getWebFrame(this, frameId); + if (!frame) return false; + frame._sendInternal(channel, ...args); + return true; }; // Following methods are mapped to webFrame. diff --git a/lib/browser/api/web-frame-main.ts b/lib/browser/api/web-frame-main.ts index 43b9ac3d44..1ef76aa6f8 100644 --- a/lib/browser/api/web-frame-main.ts +++ b/lib/browser/api/web-frame-main.ts @@ -1,4 +1,29 @@ -const { fromId } = process._linkedBinding('electron_browser_web_frame_main'); +import { MessagePortMain } from '@electron/internal/browser/message-port-main'; + +const { WebFrameMain, fromId } = process._linkedBinding('electron_browser_web_frame_main'); + +WebFrameMain.prototype.send = function (channel, ...args) { + if (typeof channel !== 'string') { + throw new Error('Missing required channel argument'); + } + + return this._send(false /* internal */, channel, args); +}; + +WebFrameMain.prototype._sendInternal = function (channel, ...args) { + if (typeof channel !== 'string') { + throw new Error('Missing required channel argument'); + } + + return this._send(true /* internal */, channel, args); +}; + +WebFrameMain.prototype.postMessage = function (...args) { + if (Array.isArray(args[2])) { + args[2] = args[2].map(o => o instanceof MessagePortMain ? o._internalPort : o); + } + this._postMessage(...args); +}; export default { fromId diff --git a/lib/browser/init.ts b/lib/browser/init.ts index 8360dc65e6..76276c1f1d 100644 --- a/lib/browser/init.ts +++ b/lib/browser/init.ts @@ -145,6 +145,9 @@ require('@electron/internal/browser/api/protocol'); // Load web-contents module to ensure it is populated on app ready require('@electron/internal/browser/api/web-contents'); +// Load web-frame-main module to ensure it is populated on app ready +require('@electron/internal/browser/api/web-frame-main'); + // Set main startup script of the app. const mainStartupScript = packageJson.main || 'index.js'; diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index a80e4f9a26..b483384be9 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -1547,39 +1547,6 @@ void WebContents::ReceivePostMessage( channel, message_value, std::move(wrapped_ports)); } -void WebContents::PostMessage(const std::string& channel, - v8::Local message_value, - base::Optional> transfer) { - v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); - blink::TransferableMessage transferable_message; - if (!electron::SerializeV8Value(isolate, message_value, - &transferable_message)) { - // SerializeV8Value sets an exception. - return; - } - - std::vector> wrapped_ports; - if (transfer) { - if (!gin::ConvertFromV8(isolate, *transfer, &wrapped_ports)) { - isolate->ThrowException(v8::Exception::Error( - gin::StringToV8(isolate, "Invalid value for transfer"))); - return; - } - } - - bool threw_exception = false; - transferable_message.ports = - MessagePort::DisentanglePorts(isolate, wrapped_ports, &threw_exception); - if (threw_exception) - return; - - content::RenderFrameHost* frame_host = web_contents()->GetMainFrame(); - mojo::AssociatedRemote electron_renderer; - frame_host->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer); - electron_renderer->ReceivePostMessage(channel, - std::move(transferable_message)); -} - void WebContents::MessageSync( bool internal, const std::string& channel, @@ -2713,46 +2680,6 @@ bool WebContents::SendIPCMessageWithSender(bool internal, return true; } -bool WebContents::SendIPCMessageToFrame(bool internal, - v8::Local frame, - const std::string& channel, - v8::Local args) { - v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); - blink::CloneableMessage message; - if (!gin::ConvertFromV8(isolate, args, &message)) { - isolate->ThrowException(v8::Exception::Error( - gin::StringToV8(isolate, "Failed to serialize arguments"))); - return false; - } - int32_t frame_id; - int32_t process_id; - if (gin::ConvertFromV8(isolate, frame, &frame_id)) { - process_id = web_contents()->GetMainFrame()->GetProcess()->GetID(); - } else { - std::vector id_pair; - if (gin::ConvertFromV8(isolate, frame, &id_pair) && id_pair.size() == 2) { - process_id = id_pair[0]; - frame_id = id_pair[1]; - } else { - isolate->ThrowException(v8::Exception::Error(gin::StringToV8( - isolate, - "frameId must be a number or a pair of [processId, frameId]"))); - return false; - } - } - - auto* rfh = content::RenderFrameHost::FromID(process_id, frame_id); - if (!rfh || !rfh->IsRenderFrameLive() || - content::WebContents::FromRenderFrameHost(rfh) != web_contents()) - return false; - - mojo::AssociatedRemote electron_renderer; - rfh->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer); - electron_renderer->Message(internal, channel, std::move(message), - 0 /* sender_id */); - return true; -} - void WebContents::SendInputEvent(v8::Isolate* isolate, v8::Local input_event) { content::RenderWidgetHostView* view = @@ -3656,8 +3583,6 @@ v8::Local WebContents::FillObjectTemplate( .SetMethod("focus", &WebContents::Focus) .SetMethod("isFocused", &WebContents::IsFocused) .SetMethod("_send", &WebContents::SendIPCMessage) - .SetMethod("_postMessage", &WebContents::PostMessage) - .SetMethod("_sendToFrame", &WebContents::SendIPCMessageToFrame) .SetMethod("sendInputEvent", &WebContents::SendInputEvent) .SetMethod("beginFrameSubscription", &WebContents::BeginFrameSubscription) .SetMethod("endFrameSubscription", &WebContents::EndFrameSubscription) diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index da87e5d389..99234edc88 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -260,15 +260,6 @@ class WebContents : public gin::Wrappable, blink::CloneableMessage args, int32_t sender_id = 0); - bool SendIPCMessageToFrame(bool internal, - v8::Local frame, - const std::string& channel, - v8::Local args); - - void PostMessage(const std::string& channel, - v8::Local message, - base::Optional> transfer); - // Send WebInputEvent to the page. void SendInputEvent(v8::Isolate* isolate, v8::Local input_event); diff --git a/shell/browser/api/electron_api_web_frame_main.cc b/shell/browser/api/electron_api_web_frame_main.cc index 514cc5a2ee..d1d3dfd912 100644 --- a/shell/browser/api/electron_api_web_frame_main.cc +++ b/shell/browser/api/electron_api_web_frame_main.cc @@ -13,9 +13,12 @@ #include "base/logging.h" #include "content/browser/renderer_host/frame_tree_node.h" // nogncheck #include "content/public/browser/render_frame_host.h" +#include "electron/shell/common/api/api.mojom.h" #include "gin/object_template_builder.h" +#include "shell/browser/api/message_port.h" #include "shell/browser/browser.h" #include "shell/browser/javascript_environment.h" +#include "shell/common/gin_converters/blink_converter.h" #include "shell/common/gin_converters/frame_converter.h" #include "shell/common/gin_converters/gurl_converter.h" #include "shell/common/gin_converters/value_converter.h" @@ -24,6 +27,8 @@ #include "shell/common/gin_helper/object_template_builder.h" #include "shell/common/gin_helper/promise.h" #include "shell/common/node_includes.h" +#include "shell/common/v8_value_serializer.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" namespace electron { @@ -157,6 +162,63 @@ bool WebFrameMain::Reload(v8::Isolate* isolate) { return render_frame_->Reload(); } +void WebFrameMain::Send(v8::Isolate* isolate, + bool internal, + const std::string& channel, + v8::Local args) { + blink::CloneableMessage message; + if (!gin::ConvertFromV8(isolate, args, &message)) { + isolate->ThrowException(v8::Exception::Error( + gin::StringToV8(isolate, "Failed to serialize arguments"))); + return; + } + + if (!CheckRenderFrame()) + return; + + mojo::AssociatedRemote electron_renderer; + render_frame_->GetRemoteAssociatedInterfaces()->GetInterface( + &electron_renderer); + electron_renderer->Message(internal, channel, std::move(message), + 0 /* sender_id */); +} + +void WebFrameMain::PostMessage(v8::Isolate* isolate, + const std::string& channel, + v8::Local message_value, + base::Optional> transfer) { + blink::TransferableMessage transferable_message; + if (!electron::SerializeV8Value(isolate, message_value, + &transferable_message)) { + // SerializeV8Value sets an exception. + return; + } + + std::vector> wrapped_ports; + if (transfer) { + if (!gin::ConvertFromV8(isolate, *transfer, &wrapped_ports)) { + isolate->ThrowException(v8::Exception::Error( + gin::StringToV8(isolate, "Invalid value for transfer"))); + return; + } + } + + bool threw_exception = false; + transferable_message.ports = + MessagePort::DisentanglePorts(isolate, wrapped_ports, &threw_exception); + if (threw_exception) + return; + + if (!CheckRenderFrame()) + return; + + mojo::AssociatedRemote electron_renderer; + render_frame_->GetRemoteAssociatedInterfaces()->GetInterface( + &electron_renderer); + electron_renderer->ReceivePostMessage(channel, + std::move(transferable_message)); +} + int WebFrameMain::FrameTreeNodeID(v8::Isolate* isolate) const { if (!CheckRenderFrame()) return -1; @@ -234,6 +296,11 @@ std::vector WebFrameMain::FramesInSubtree( return frame_hosts; } +// static +gin::Handle WebFrameMain::New(v8::Isolate* isolate) { + return gin::Handle(); +} + // static gin::Handle WebFrameMain::From(v8::Isolate* isolate, content::RenderFrameHost* rfh) { @@ -261,13 +328,17 @@ void WebFrameMain::RenderFrameDeleted(content::RenderFrameHost* rfh) { web_frame->MarkRenderFrameDisposed(); } -gin::ObjectTemplateBuilder WebFrameMain::GetObjectTemplateBuilder( - v8::Isolate* isolate) { - return gin::Wrappable::GetObjectTemplateBuilder(isolate) +// static +v8::Local WebFrameMain::FillObjectTemplate( + v8::Isolate* isolate, + v8::Local templ) { + return gin_helper::ObjectTemplateBuilder(isolate, templ) .SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript) .SetMethod("executeJavaScriptInIsolatedWorld", &WebFrameMain::ExecuteJavaScriptInIsolatedWorld) .SetMethod("reload", &WebFrameMain::Reload) + .SetMethod("_send", &WebFrameMain::Send) + .SetMethod("_postMessage", &WebFrameMain::PostMessage) .SetProperty("frameTreeNodeId", &WebFrameMain::FrameTreeNodeID) .SetProperty("name", &WebFrameMain::Name) .SetProperty("osProcessId", &WebFrameMain::OSProcessID) @@ -277,7 +348,8 @@ gin::ObjectTemplateBuilder WebFrameMain::GetObjectTemplateBuilder( .SetProperty("top", &WebFrameMain::Top) .SetProperty("parent", &WebFrameMain::Parent) .SetProperty("frames", &WebFrameMain::Frames) - .SetProperty("framesInSubtree", &WebFrameMain::FramesInSubtree); + .SetProperty("framesInSubtree", &WebFrameMain::FramesInSubtree) + .Build(); } const char* WebFrameMain::GetTypeName() { @@ -311,6 +383,7 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); + dict.Set("WebFrameMain", WebFrameMain::GetConstructor(context)); dict.SetMethod("fromId", &FromID); } diff --git a/shell/browser/api/electron_api_web_frame_main.h b/shell/browser/api/electron_api_web_frame_main.h index 9d381e23f4..462f1f98ff 100644 --- a/shell/browser/api/electron_api_web_frame_main.h +++ b/shell/browser/api/electron_api_web_frame_main.h @@ -12,6 +12,7 @@ #include "base/process/process.h" #include "gin/handle.h" #include "gin/wrappable.h" +#include "shell/common/gin_helper/constructible.h" class GURL; @@ -32,8 +33,12 @@ namespace electron { namespace api { // Bindings for accessing frames from the main process. -class WebFrameMain : public gin::Wrappable { +class WebFrameMain : public gin::Wrappable, + public gin_helper::Constructible { public: + // Create a new WebFrameMain and return the V8 wrapper of it. + static gin::Handle New(v8::Isolate* isolate); + static gin::Handle FromID(v8::Isolate* isolate, int render_process_id, int render_frame_id); @@ -51,8 +56,9 @@ class WebFrameMain : public gin::Wrappable { // gin::Wrappable static gin::WrapperInfo kWrapperInfo; - gin::ObjectTemplateBuilder GetObjectTemplateBuilder( - v8::Isolate* isolate) override; + static v8::Local FillObjectTemplate( + v8::Isolate*, + v8::Local); const char* GetTypeName() override; protected: @@ -71,6 +77,14 @@ class WebFrameMain : public gin::Wrappable { int world_id, const base::string16& code); bool Reload(v8::Isolate* isolate); + void Send(v8::Isolate* isolate, + bool internal, + const std::string& channel, + v8::Local args); + void PostMessage(v8::Isolate* isolate, + const std::string& channel, + v8::Local message_value, + base::Optional> transfer); int FrameTreeNodeID(v8::Isolate* isolate) const; std::string Name(v8::Isolate* isolate) const; diff --git a/shell/common/api/electron_api_native_image.h b/shell/common/api/electron_api_native_image.h index 1c89fcdfa6..395223c313 100644 --- a/shell/common/api/electron_api_native_image.h +++ b/shell/common/api/electron_api_native_image.h @@ -75,8 +75,6 @@ class NativeImage : public gin::Wrappable { const gfx::Size& size); #endif - static v8::Local GetConstructor(v8::Isolate* isolate); - static bool TryConvertNativeImage(v8::Isolate* isolate, v8::Local image, NativeImage** native_image); diff --git a/spec-main/api-ipc-spec.ts b/spec-main/api-ipc-spec.ts index 64ee87c1e6..744100bc63 100644 --- a/spec-main/api-ipc-spec.ts +++ b/spec-main/api-ipc-spec.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; import { expect } from 'chai'; -import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain } from 'electron/main'; +import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main'; import { closeAllWindows } from './window-helpers'; import { emittedOnce } from './events-helpers'; @@ -449,97 +449,102 @@ describe('ipc module', () => { }); }); - describe('WebContents.postMessage', () => { - it('sends a message', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); - w.loadURL('about:blank'); - await w.webContents.executeJavaScript(`(${function () { - const { ipcRenderer } = require('electron'); - ipcRenderer.on('foo', (_e, msg) => { - ipcRenderer.send('bar', msg); + const generateTests = (title: string, postMessage: (contents: WebContents) => typeof WebContents.prototype.postMessage) => { + describe(title, () => { + it('sends a message', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); + w.loadURL('about:blank'); + await w.webContents.executeJavaScript(`(${function () { + const { ipcRenderer } = require('electron'); + ipcRenderer.on('foo', (_e, msg) => { + ipcRenderer.send('bar', msg); + }); + }})()`); + postMessage(w.webContents)('foo', { some: 'message' }); + const [, msg] = await emittedOnce(ipcMain, 'bar'); + expect(msg).to.deep.equal({ some: 'message' }); + }); + + describe('error handling', () => { + it('throws on missing channel', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (postMessage(w.webContents) as any)(); + }).to.throw(/Insufficient number of arguments/); }); - }})()`); - w.webContents.postMessage('foo', { some: 'message' }); - const [, msg] = await emittedOnce(ipcMain, 'bar'); - expect(msg).to.deep.equal({ some: 'message' }); - }); - describe('error handling', () => { - it('throws on missing channel', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - (w.webContents.postMessage as any)(); - }).to.throw(/Insufficient number of arguments/); - }); + it('throws on invalid channel', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + postMessage(w.webContents)(null as any, '', []); + }).to.throw(/Error processing argument at index 0/); + }); - it('throws on invalid channel', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - w.webContents.postMessage(null as any, '', []); - }).to.throw(/Error processing argument at index 0/); - }); + it('throws on missing message', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (postMessage(w.webContents) as any)('channel'); + }).to.throw(/Insufficient number of arguments/); + }); - it('throws on missing message', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - (w.webContents.postMessage as any)('channel'); - }).to.throw(/Insufficient number of arguments/); - }); + it('throws on non-serializable message', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + postMessage(w.webContents)('channel', w); + }).to.throw(/An object could not be cloned/); + }); - it('throws on non-serializable message', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - w.webContents.postMessage('channel', w); - }).to.throw(/An object could not be cloned/); - }); + it('throws on invalid transferable list', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + postMessage(w.webContents)('', '', null as any); + }).to.throw(/Invalid value for transfer/); + }); - it('throws on invalid transferable list', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - w.webContents.postMessage('', '', null as any); - }).to.throw(/Invalid value for transfer/); - }); + it('throws on transferring non-transferable', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + (postMessage(w.webContents) as any)('channel', '', [123]); + }).to.throw(/Invalid value for transfer/); + }); - it('throws on transferring non-transferable', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - (w.webContents.postMessage as any)('channel', '', [123]); - }).to.throw(/Invalid value for transfer/); - }); + it('throws when passing null ports', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + expect(() => { + postMessage(w.webContents)('foo', null, [null] as any); + }).to.throw(/Invalid value for transfer/); + }); - it('throws when passing null ports', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - expect(() => { - w.webContents.postMessage('foo', null, [null] as any); - }).to.throw(/Invalid value for transfer/); - }); + it('throws when passing duplicate ports', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + const { port1 } = new MessageChannelMain(); + expect(() => { + postMessage(w.webContents)('foo', null, [port1, port1]); + }).to.throw(/duplicate/); + }); - it('throws when passing duplicate ports', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - const { port1 } = new MessageChannelMain(); - expect(() => { - w.webContents.postMessage('foo', null, [port1, port1]); - }).to.throw(/duplicate/); - }); - - it('throws when passing ports that have already been neutered', async () => { - const w = new BrowserWindow({ show: false }); - await w.loadURL('about:blank'); - const { port1 } = new MessageChannelMain(); - w.webContents.postMessage('foo', null, [port1]); - expect(() => { - w.webContents.postMessage('foo', null, [port1]); - }).to.throw(/already neutered/); + it('throws when passing ports that have already been neutered', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + const { port1 } = new MessageChannelMain(); + postMessage(w.webContents)('foo', null, [port1]); + expect(() => { + postMessage(w.webContents)('foo', null, [port1]); + }).to.throw(/already neutered/); + }); }); }); - }); + }; + + generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents)); + generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame)); }); }); diff --git a/spec-main/api-subframe-spec.ts b/spec-main/api-subframe-spec.ts index 3415fa0380..9fe962f393 100644 --- a/spec-main/api-subframe-spec.ts +++ b/spec-main/api-subframe-spec.ts @@ -60,9 +60,18 @@ describe('renderer nodeIntegrationInSubFrames', () => { const [event1] = await detailsPromise; const pongPromise = emittedOnce(ipcMain, 'preload-pong'); event1[0].reply('preload-ping'); - const details = await pongPromise; - expect(details[1]).to.equal(event1[0].frameId); - expect(details[1]).to.equal(event1[0].senderFrame.routingId); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event1[0].frameId); + }); + + it('should correctly reply to the main frame with using event.senderFrame.send', async () => { + const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2); + w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)); + const [event1] = await detailsPromise; + const pongPromise = emittedOnce(ipcMain, 'preload-pong'); + event1[0].senderFrame.send('preload-ping'); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event1[0].frameId); }); it('should correctly reply to the sub-frames with using event.reply', async () => { @@ -71,9 +80,18 @@ describe('renderer nodeIntegrationInSubFrames', () => { const [, event2] = await detailsPromise; const pongPromise = emittedOnce(ipcMain, 'preload-pong'); event2[0].reply('preload-ping'); - const details = await pongPromise; - expect(details[1]).to.equal(event2[0].frameId); - expect(details[1]).to.equal(event2[0].senderFrame.routingId); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event2[0].frameId); + }); + + it('should correctly reply to the sub-frames with using event.senderFrame.send', async () => { + const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 2); + w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-container${fixtureSuffix}.html`)); + const [, event2] = await detailsPromise; + const pongPromise = emittedOnce(ipcMain, 'preload-pong'); + event2[0].senderFrame.send('preload-ping'); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event2[0].frameId); }); it('should correctly reply to the nested sub-frames with using event.reply', async () => { @@ -82,9 +100,18 @@ describe('renderer nodeIntegrationInSubFrames', () => { const [, , event3] = await detailsPromise; const pongPromise = emittedOnce(ipcMain, 'preload-pong'); event3[0].reply('preload-ping'); - const details = await pongPromise; - expect(details[1]).to.equal(event3[0].frameId); - expect(details[1]).to.equal(event3[0].senderFrame.routingId); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event3[0].frameId); + }); + + it('should correctly reply to the nested sub-frames with using event.senderFrame.send', async () => { + const detailsPromise = emittedNTimes(ipcMain, 'preload-ran', 3); + w.loadFile(path.resolve(__dirname, `fixtures/sub-frames/frame-with-frame-container${fixtureSuffix}.html`)); + const [, , event3] = await detailsPromise; + const pongPromise = emittedOnce(ipcMain, 'preload-pong'); + event3[0].senderFrame.send('preload-ping'); + const [, frameId] = await pongPromise; + expect(frameId).to.equal(event3[0].frameId); }); it('should not expose globals in main world', async () => { diff --git a/spec-main/api-web-frame-main-spec.ts b/spec-main/api-web-frame-main-spec.ts index 2739c80f3a..21f63d973e 100644 --- a/spec-main/api-web-frame-main-spec.ts +++ b/spec-main/api-web-frame-main-spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import * as http from 'http'; import * as path from 'path'; import * as url from 'url'; -import { BrowserWindow, WebFrameMain, webFrameMain } from 'electron/main'; +import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain } from 'electron/main'; import { closeAllWindows } from './window-helpers'; import { emittedOnce, emittedNTimes } from './events-helpers'; import { AddressInfo } from 'net'; @@ -173,6 +173,24 @@ describe('webFrameMain module', () => { }); }); + describe('WebFrame.send', () => { + it('works', async () => { + const w = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(subframesPath, 'preload.js'), + nodeIntegrationInSubFrames: true + } + }); + await w.loadURL('about:blank'); + const webFrame = w.webContents.mainFrame; + const pongPromise = emittedOnce(ipcMain, 'preload-pong'); + webFrame.send('preload-ping'); + const [, routingId] = await pongPromise; + expect(routingId).to.equal(webFrame.routingId); + }); + }); + describe('disposed WebFrames', () => { let w: BrowserWindow; let webFrame: WebFrameMain; diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 512fabdc34..c487cb1654 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -215,6 +215,10 @@ declare namespace NodeJS { _linkedBinding(name: 'electron_browser_view'): { View: Electron.View }; _linkedBinding(name: 'electron_browser_web_contents_view'): { WebContentsView: typeof Electron.WebContentsView }; _linkedBinding(name: 'electron_browser_web_view_manager'): WebViewManagerBinding; + _linkedBinding(name: 'electron_browser_web_frame_main'): { + WebFrameMain: typeof Electron.WebFrameMain; + fromId(processId: number, routingId: number): Electron.WebFrameMain; + } _linkedBinding(name: 'electron_renderer_crash_reporter'): Electron.CrashReporter; _linkedBinding(name: 'electron_renderer_ipc'): { ipc: IpcRendererBinding }; log: NodeJS.WriteStream['write']; diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index a80c37a378..652a601342 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -68,9 +68,7 @@ declare namespace Electron { _callWindowOpenHandler(event: any, url: string, frameName: string, rawFeatures: string): Electron.BrowserWindowConstructorOptions | null; _setNextChildWebPreferences(prefs: Partial & Pick): void; _send(internal: boolean, channel: string, args: any): boolean; - _sendToFrame(internal: boolean, frameId: number | [number, number], channel: string, args: any): boolean; _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean; - _postMessage(channel: string, message: any, transfer?: any[]): void; _sendInternal(channel: string, ...args: any[]): void; _printToPDF(options: any): Promise; _print(options: any, callback?: (success: boolean, failureReason: string) => void): void; @@ -93,6 +91,12 @@ declare namespace Electron { allowGuestViewElementDefinition(window: Window, context: any): void; } + interface WebFrameMain { + _send(internal: boolean, channel: string, args: any): void; + _sendInternal(channel: string, ...args: any[]): void; + _postMessage(channel: string, message: any, transfer?: any[]): void; + } + interface WebPreferences { guestInstanceId?: number; openerId?: number;