Browse Source

feat: add webFrameMain.send() / webFrameMain.postMessage() (#26807) (#27366)

Milan Burda 4 years ago
parent
commit
e25de07657

+ 41 - 0
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_

+ 23 - 20
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)');
+
+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])');
   }
+}
 
-  return this._sendToFrame(false /* internal */, frame, channel, args);
+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.

+ 26 - 1
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

+ 3 - 0
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';
 

+ 0 - 75
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<v8::Value> message_value,
-                              base::Optional<v8::Local<v8::Value>> 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<gin::Handle<MessagePort>> 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<mojom::ElectronRenderer> 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<v8::Value> frame,
-                                        const std::string& channel,
-                                        v8::Local<v8::Value> 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<int32_t> 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<mojom::ElectronRenderer> 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<v8::Value> input_event) {
   content::RenderWidgetHostView* view =
@@ -3656,8 +3583,6 @@ v8::Local<v8::ObjectTemplate> 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)

+ 0 - 9
shell/browser/api/electron_api_web_contents.h

@@ -260,15 +260,6 @@ class WebContents : public gin::Wrappable<WebContents>,
                                 blink::CloneableMessage args,
                                 int32_t sender_id = 0);
 
-  bool SendIPCMessageToFrame(bool internal,
-                             v8::Local<v8::Value> frame,
-                             const std::string& channel,
-                             v8::Local<v8::Value> args);
-
-  void PostMessage(const std::string& channel,
-                   v8::Local<v8::Value> message,
-                   base::Optional<v8::Local<v8::Value>> transfer);
-
   // Send WebInputEvent to the page.
   void SendInputEvent(v8::Isolate* isolate, v8::Local<v8::Value> input_event);
 

+ 77 - 4
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<v8::Value> 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<mojom::ElectronRenderer> 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<v8::Value> message_value,
+                               base::Optional<v8::Local<v8::Value>> transfer) {
+  blink::TransferableMessage transferable_message;
+  if (!electron::SerializeV8Value(isolate, message_value,
+                                  &transferable_message)) {
+    // SerializeV8Value sets an exception.
+    return;
+  }
+
+  std::vector<gin::Handle<MessagePort>> 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<mojom::ElectronRenderer> 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<content::RenderFrameHost*> WebFrameMain::FramesInSubtree(
   return frame_hosts;
 }
 
+// static
+gin::Handle<WebFrameMain> WebFrameMain::New(v8::Isolate* isolate) {
+  return gin::Handle<WebFrameMain>();
+}
+
 // static
 gin::Handle<WebFrameMain> 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<WebFrameMain>::GetObjectTemplateBuilder(isolate)
+// static
+v8::Local<v8::ObjectTemplate> WebFrameMain::FillObjectTemplate(
+    v8::Isolate* isolate,
+    v8::Local<v8::ObjectTemplate> 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<v8::Object> exports,
                 void* priv) {
   v8::Isolate* isolate = context->GetIsolate();
   gin_helper::Dictionary dict(isolate, exports);
+  dict.Set("WebFrameMain", WebFrameMain::GetConstructor(context));
   dict.SetMethod("fromId", &FromID);
 }
 

+ 17 - 3
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<WebFrameMain> {
+class WebFrameMain : public gin::Wrappable<WebFrameMain>,
+                     public gin_helper::Constructible<WebFrameMain> {
  public:
+  // Create a new WebFrameMain and return the V8 wrapper of it.
+  static gin::Handle<WebFrameMain> New(v8::Isolate* isolate);
+
   static gin::Handle<WebFrameMain> FromID(v8::Isolate* isolate,
                                           int render_process_id,
                                           int render_frame_id);
@@ -51,8 +56,9 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain> {
 
   // gin::Wrappable
   static gin::WrapperInfo kWrapperInfo;
-  gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
-      v8::Isolate* isolate) override;
+  static v8::Local<v8::ObjectTemplate> FillObjectTemplate(
+      v8::Isolate*,
+      v8::Local<v8::ObjectTemplate>);
   const char* GetTypeName() override;
 
  protected:
@@ -71,6 +77,14 @@ class WebFrameMain : public gin::Wrappable<WebFrameMain> {
       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<v8::Value> args);
+  void PostMessage(v8::Isolate* isolate,
+                   const std::string& channel,
+                   v8::Local<v8::Value> message_value,
+                   base::Optional<v8::Local<v8::Value>> transfer);
 
   int FrameTreeNodeID(v8::Isolate* isolate) const;
   std::string Name(v8::Isolate* isolate) const;

+ 0 - 2
shell/common/api/electron_api_native_image.h

@@ -75,8 +75,6 @@ class NativeImage : public gin::Wrappable<NativeImage> {
       const gfx::Size& size);
 #endif
 
-  static v8::Local<v8::FunctionTemplate> GetConstructor(v8::Isolate* isolate);
-
   static bool TryConvertNativeImage(v8::Isolate* isolate,
                                     v8::Local<v8::Value> image,
                                     NativeImage** native_image);

+ 87 - 82
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);
-          });
-        }})()`);
-        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/);
+    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' });
         });
 
-        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/);
-        });
+        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/);
+          });
 
-        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 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 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 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 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 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 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 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 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 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 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 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 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 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));
   });
 });

+ 36 - 9
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 () => {

+ 19 - 1
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;

+ 4 - 0
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'];

+ 6 - 2
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<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): 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<Buffer>;
     _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;