feat: preloads and nodeIntegration in iframes (#16425)

* feat: add support for node / preloads in subframes

This feature has delibrately been built / implemented in such a way
that it has minimum impact on existing apps / code-paths.
Without enabling the new "nodeSupportInSubFrames" option basically none of this
new code will be hit.

The things that I believe need extra scrutiny are:

* Introduction of `event.reply` for IPC events and usage of `event.reply` instead of `event.sender.send()`
* Usage of `node::FreeEnvironment(env)` when the new option is enabled in order to avoid memory leaks.  I have tested this quite a bit and haven't managed to cause a crash but it is still feature flagged behind the "nodeSupportInSubFrames" flag to avoid potential impact.

Closes #10569
Closes #10401
Closes #11868
Closes #12505
Closes #14035

* feat: add support preloads in subframes for sandboxed renderers

* spec: add tests for new nodeSupportInSubFrames option

* spec: fix specs for .reply and ._replyInternal for internal messages

* chore: revert change to use flag instead of environment set size

* chore: clean up subframe impl

* chore: apply suggestions from code review

Co-Authored-By: MarshallOfSound <samuel.r.attard@gmail.com>

* chore: clean up reply usage

* chore: fix TS docs generation

* chore: cleanup after rebase

* chore: rename wrap to add in event fns
This commit is contained in:
Samuel Attard 2019-01-22 11:24:46 -08:00 коммит произвёл GitHub
Родитель 92b9525cfd
Коммит 58a6fe13d6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 332 добавлений и 49 удалений

Просмотреть файл

@ -57,9 +57,10 @@ v8::Local<v8::Object> CreateJSEvent(v8::Isolate* isolate,
} else {
event = CreateEventObject(isolate);
}
mate::Dictionary(isolate, event).Set("sender", object);
mate::Dictionary dict(isolate, event);
dict.Set("sender", object);
if (sender)
mate::Dictionary(isolate, event).Set("frameId", sender->GetRoutingID());
dict.Set("frameId", sender->GetRoutingID());
return event;
}

Просмотреть файл

@ -123,6 +123,7 @@ WebContentsPreferences::WebContentsPreferences(
SetDefaultBoolIfUndefined(options::kPlugins, false);
SetDefaultBoolIfUndefined(options::kExperimentalFeatures, false);
SetDefaultBoolIfUndefined(options::kNodeIntegration, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInSubFrames, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInWorker, false);
SetDefaultBoolIfUndefined(options::kWebviewTag, false);
SetDefaultBoolIfUndefined(options::kSandbox, false);
@ -369,6 +370,9 @@ void WebContentsPreferences::AppendCommandLineSwitches(
}
}
if (IsEnabled(options::kNodeIntegrationInSubFrames))
command_line->AppendSwitch(switches::kNodeIntegrationInSubFrames);
// We are appending args to a webContents so let's save the current state
// of our preferences object so that during the lifetime of the WebContents
// we can fetch the options used to initally configure the WebContents

Просмотреть файл

@ -154,6 +154,8 @@ const char kAllowRunningInsecureContent[] = "allowRunningInsecureContent";
const char kOffscreen[] = "offscreen";
const char kNodeIntegrationInSubFrames[] = "nodeIntegrationInSubFrames";
} // namespace options
namespace switches {
@ -205,6 +207,10 @@ const char kWebviewTag[] = "webview-tag";
// Command switch passed to renderer process to control nodeIntegration.
const char kNodeIntegrationInWorker[] = "node-integration-in-worker";
// Command switch passed to renderer process to control whether node
// environments will be created in sub-frames.
const char kNodeIntegrationInSubFrames[] = "node-integration-in-subframes";
// Widevine options
// Path to Widevine CDM binaries.
const char kWidevineCdmPath[] = "widevine-cdm-path";

Просмотреть файл

@ -75,6 +75,7 @@ extern const char kSandbox[];
extern const char kWebSecurity[];
extern const char kAllowRunningInsecureContent[];
extern const char kOffscreen[];
extern const char kNodeIntegrationInSubFrames[];
} // namespace options
@ -106,6 +107,7 @@ extern const char kHiddenPage[];
extern const char kNativeWindowOpen[];
extern const char kNodeIntegrationInWorker[];
extern const char kWebviewTag[];
extern const char kNodeIntegrationInSubFrames[];
extern const char kWidevineCdmPath[];
extern const char kWidevineCdmVersion[];

Просмотреть файл

@ -187,7 +187,7 @@ void AtomRenderFrameObserver::OnBrowserMessage(bool internal,
return;
blink::WebLocalFrame* frame = render_frame_->GetWebFrame();
if (!frame || !render_frame_->IsMainFrame())
if (!frame)
return;
EmitIPCEvent(frame, internal, channel, args, sender_id);

Просмотреть файл

@ -79,25 +79,27 @@ void AtomRendererClient::DidCreateScriptContext(
content::RenderFrame* render_frame) {
RendererClientBase::DidCreateScriptContext(context, render_frame);
// Only allow node integration for the main frame of the top window, unless it
// is a devtools extension page. Allowing child frames or child windows to
// have node integration would result in memory leak, since we don't destroy
// node environment when script context is destroyed.
//
// DevTools extensions do not follow this rule because our implementation
// requires node integration in iframes to work. And usually DevTools
// extensions do not dynamically add/remove iframes.
//
// TODO(zcbenz): Do not create Node environment if node integration is not
// enabled.
if (!(render_frame->IsMainFrame() &&
!render_frame->GetWebFrame()->Opener()) &&
!IsDevToolsExtension(render_frame))
// Do not load node if we're aren't a main frame or a devtools extension
// unless node support has been explicitly enabled for sub frames
bool is_main_frame =
render_frame->IsMainFrame() && !render_frame->GetWebFrame()->Opener();
bool is_devtools = IsDevToolsExtension(render_frame);
bool allow_node_in_subframes =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_node =
is_main_frame || is_devtools || allow_node_in_subframes;
if (!should_load_node) {
return;
}
injected_frames_.insert(render_frame);
// Prepare the node bindings.
// If this is the first environment we are creating, prepare the node
// bindings.
if (!node_integration_initialized_) {
node_integration_initialized_ = true;
node_bindings_->Initialize();
@ -115,6 +117,8 @@ void AtomRendererClient::DidCreateScriptContext(
// Add Electron extended APIs.
atom_bindings_->BindTo(env->isolate(), env->process_object());
AddRenderBindings(env->isolate(), env->process_object());
mate::Dictionary process_dict(env->isolate(), env->process_object());
process_dict.SetReadOnly("isMainFrame", render_frame->IsMainFrame());
// Load everything.
node_bindings_->LoadEnvironment(env);
@ -146,11 +150,13 @@ void AtomRendererClient::WillReleaseScriptContext(
if (env == node_bindings_->uv_env())
node_bindings_->set_uv_env(nullptr);
// Destroy the node environment.
// This is disabled because pending async tasks may still use the environment
// and would cause crashes later. Node does not seem to clear all async tasks
// when the environment is destroyed.
// node::FreeEnvironment(env);
// Destroy the node environment. We only do this if node support has been
// enabled for sub-frames to avoid a change-of-behavior / introduce crashes
// for existing users.
// TODO(MarshallOfSOund): Free the environment regardless of this switch
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
node::FreeEnvironment(env);
// AtomBindings is tracking node environments.
atom_bindings_->EnvironmentDestroyed(env);

Просмотреть файл

@ -139,7 +139,8 @@ AtomSandboxedRendererClient::~AtomSandboxedRendererClient() {}
void AtomSandboxedRendererClient::InitializeBindings(
v8::Local<v8::Object> binding,
v8::Local<v8::Context> context) {
v8::Local<v8::Context> context,
bool is_main_frame) {
auto* isolate = context->GetIsolate();
mate::Dictionary b(isolate, binding);
b.SetMethod("get", GetBinding);
@ -154,6 +155,7 @@ void AtomSandboxedRendererClient::InitializeBindings(
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
process.SetReadOnly("type", "renderer");
process.SetReadOnly("isMainFrame", is_main_frame);
// Pass in CLI flags needed to setup the renderer
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
@ -180,15 +182,23 @@ void AtomSandboxedRendererClient::DidCreateScriptContext(
// Only allow preload for the main frame or
// For devtools we still want to run the preload_bundle script
if (!render_frame->IsMainFrame() && !IsDevTools(render_frame) &&
!IsDevToolsExtension(render_frame))
// Or when nodeSupport is explicitly enabled in sub frames
bool is_main_frame = render_frame->IsMainFrame();
bool is_devtools =
IsDevTools(render_frame) || IsDevToolsExtension(render_frame);
bool allow_node_in_sub_frames =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_preload =
is_main_frame || is_devtools || allow_node_in_sub_frames;
if (!should_load_preload)
return;
// Wrap the bundle into a function that receives the binding object as
// argument.
auto* isolate = context->GetIsolate();
auto binding = v8::Object::New(isolate);
InitializeBindings(binding, context);
InitializeBindings(binding, context, render_frame->IsMainFrame());
AddRenderBindings(isolate, binding);
std::vector<v8::Local<v8::String>> preload_bundle_params = {
@ -229,7 +239,10 @@ void AtomSandboxedRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) {
// Only allow preload for the main frame
if (!render_frame->IsMainFrame())
// Or for sub frames when explicitly enabled
if (!render_frame->IsMainFrame() &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
return;
auto* isolate = context->GetIsolate();

Просмотреть файл

@ -19,7 +19,8 @@ class AtomSandboxedRendererClient : public RendererClientBase {
~AtomSandboxedRendererClient() override;
void InitializeBindings(v8::Local<v8::Object> binding,
v8::Local<v8::Context> context);
v8::Local<v8::Context> context,
bool is_main_frame);
void InvokeIpcCallback(v8::Handle<v8::Context> context,
const std::string& callback_name,
std::vector<v8::Handle<v8::Value>> args);

Просмотреть файл

@ -255,6 +255,10 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is
enabled in web workers. Default is `false`. More about this can be found
in [Multithreading](../tutorial/multithreading.md).
* `nodeIntegrationInSubFrames` Boolean (optional) - Experimental option for
enabling NodeJS support in sub-frames such as iframes. All your preloads will load for
every iframe, you can use `process.isMainFrame` to determine if you are
in the main frame or not.
* `preload` String (optional) - Specifies a script that will be loaded before other
scripts run in the page. This script will always have access to node APIs
no matter whether node integration is turned on or off. The value should

Просмотреть файл

@ -18,7 +18,9 @@ process, see [webContents.send][web-contents-send] for more information.
* When sending a message, the event name is the `channel`.
* To reply to a synchronous message, you need to set `event.returnValue`.
* To send an asynchronous message back to the sender, you can use
`event.sender.send(...)`.
`event.reply(...)`. This helper method will automatically handle messages
coming from frames that aren't the main frame (e.g. iframes) whereas
`event.sender.send(...)` will always send to the main frame.
An example of sending and handling messages between the render and main
processes:
@ -28,7 +30,7 @@ processes:
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong')
event.reply('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
@ -86,6 +88,10 @@ Removes listeners of the specified `channel`.
The `event` object passed to the `callback` has the following methods:
### `event.frameId`
An `Integer` representing the ID of the renderer frame that sent this message.
### `event.returnValue`
Set this to the value to be returned in a synchronous message.
@ -97,3 +103,10 @@ Returns the `webContents` that sent the message, you can call
[webContents.send][web-contents-send] for more information.
[web-contents-send]: web-contents.md#contentssendchannel-arg1-arg2-
### `event.reply`
A function that will send an IPC message to the renderer frane that sent
the original message that you are currently handling. You should use this
method to "reply" to the sent message in order to guaruntee the reply will go
to the correct process and frame.

Просмотреть файл

@ -59,6 +59,11 @@ process.once('loaded', () => {
A `Boolean`. When app is started by being passed as parameter to the default app, this
property is `true` in the main process, otherwise it is `undefined`.
### `process.isMainFrame`
A `Boolean`, `true` when the current renderer context is the "main" renderer
frame. If you want the ID of the current frame you should use `webFrame.routingId`.
### `process.mas`
A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is

Просмотреть файл

@ -1420,6 +1420,36 @@ app.on('ready', () => {
</html>
```
#### `contents.sendToFrame(frameId, channel[, arg1][, arg2][, ...])`
* `frameId` Integer
* `channel` String
* `...args` any[]
Send an asynchronous message to a specific frame in a renderer process via
`channel`. Arguments will be serialized
as JSON internally and as such no functions or prototype chains will be included.
The renderer process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.
If you want to get the `frameId` of a given renderer context you should use
the `webFrame.routingId` value. E.g.
```js
// In a renderer process
console.log('My frameId is:', require('electron').webFrame.routingId)
```
You can also read `frameId` from all incoming IPC messages in the main process.
```js
// In the main process
ipcMain.on('ping', (event) => {
console.info('Message came from frameId:', event.frameId)
})
```
#### `contents.enableDeviceEmulation(parameters)`
* `parameters` Object

Просмотреть файл

@ -143,6 +143,18 @@ WebContents.prototype._sendInternalToAll = function (channel, ...args) {
return this._send(internal, sendToAll, channel, args)
}
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
} else if (typeof frameId !== 'number') {
throw new Error('Missing required frameId argument')
}
const internal = false
const sendToAll = false
return this._sendToFrame(internal, sendToAll, frameId, channel, args)
}
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
@ -330,6 +342,22 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}))
}
const addReplyToEvent = (event) => {
event.reply = (...args) => {
event.sender.sendToFrame(event.frameId, ...args)
}
}
const addReplyInternalToEvent = (event) => {
Object.defineProperty(event, '_replyInternal', {
configurable: false,
enumerable: false,
value: (...args) => {
event.sender._sendToFrameInternal(event.frameId, ...args)
}
})
}
// Add JavaScript wrappers for WebContents class.
WebContents.prototype._init = function () {
// The navigation controller.
@ -343,6 +371,7 @@ WebContents.prototype._init = function () {
// Dispatch IPC messages to the ipc module.
this.on('-ipc-message', function (event, [channel, ...args]) {
addReplyToEvent(event)
this.emit('ipc-message', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})
@ -354,11 +383,13 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyToEvent(event)
this.emit('ipc-message-sync', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})
this.on('ipc-internal-message', function (event, [channel, ...args]) {
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})
@ -369,6 +400,7 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})

Просмотреть файл

@ -180,7 +180,7 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message,
page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
event._replyInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
})
resultID++
})
@ -196,7 +196,7 @@ ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBa
contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
event._replyInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
})
resultID++
})

Просмотреть файл

@ -18,7 +18,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
event.sender.emit('desktop-capturer-get-sources', customEvent)
if (customEvent.defaultPrevented) {
event.sender._sendInternal(capturerResult(id), [])
event._replyInternal(capturerResult(id), [])
return
}
@ -30,7 +30,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
thumbnailSize,
fetchWindowIcons
},
webContents: event.sender
event
}
requestsQueue.push(request)
if (requestsQueue.length === 1) {
@ -40,14 +40,13 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
// If the WebContents is destroyed before receiving result, just remove the
// reference from requestsQueue to make the module not send the result to it.
event.sender.once('destroyed', () => {
request.webContents = null
request.event = null
})
})
desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
// Receiving sources result from main process, now send them back to renderer.
const handledRequest = requestsQueue.shift()
const handledWebContents = handledRequest.webContents
const unhandledRequestsQueue = []
const result = sources.map(source => {
@ -60,16 +59,16 @@ desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
}
})
if (handledWebContents) {
handledWebContents._sendInternal(capturerResult(handledRequest.id), result)
if (handledRequest.event) {
handledRequest.event._replyInternal(capturerResult(handledRequest.id), result)
}
// Check the queue to see whether there is another identical request & handle
requestsQueue.forEach(request => {
const webContents = request.webContents
const event = request.event
if (deepEqual(handledRequest.options, request.options)) {
if (webContents) {
webContents._sendInternal(capturerResult(request.id), result)
if (event) {
event._replyInternal(capturerResult(request.id), result)
}
} else {
unhandledRequestsQueue.push(request)

Просмотреть файл

@ -246,7 +246,8 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn
['nativeWindowOpen', true],
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true]
['sandbox', true],
['nodeIntegrationInSubFrames', false]
])
// Inherit certain option values from embedder
@ -350,7 +351,7 @@ const handleMessage = function (channel, handler) {
}
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', function (event, params, requestId) {
event.sender._sendInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params))
event._replyInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params))
})
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST_SYNC', function (event, params) {
@ -400,7 +401,7 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, request
}, error => {
return [errorUtils.serialize(error)]
}).then(responseArgs => {
event.sender._sendInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs)
event._replyInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs)
})
})

Просмотреть файл

@ -16,7 +16,8 @@ const inheritedWebPreferences = new Map([
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true],
['webviewTag', false]
['webviewTag', false],
['nodeIntegrationInSubFrames', false]
])
// Copy attribute of |parent| to |child| if it is not defined in |child|.

Просмотреть файл

@ -76,12 +76,16 @@ switch (window.location.protocol) {
require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
// Inject content scripts.
if (process.isMainFrame) {
require('@electron/internal/renderer/content-scripts-injector')
}
}
}
// Load webview tag implementation.
require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId)
if (process.isMainFrame) {
require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId)
}
// Pass the arguments to isolatedWorld.
if (contextIsolation) {
@ -160,4 +164,6 @@ for (const preloadScript of preloadScripts) {
}
// Warn about security issues
require('@electron/internal/renderer/security-warnings')(nodeIntegration)
if (process.isMainFrame) {
require('@electron/internal/renderer/security-warnings')(nodeIntegration)
}

Просмотреть файл

@ -26,7 +26,7 @@
const { defineProperty, defineProperties } = Object
// Helper function to resolve relative url.
const a = window.top.document.createElement('a')
const a = window.document.createElement('a')
const resolveURL = function (url) {
a.href = url
return a.href

88
spec/api-subframe-spec.js Normal file
Просмотреть файл

@ -0,0 +1,88 @@
const { expect } = require('chai')
const { remote } = require('electron')
const path = require('path')
const { emittedNTimes, emittedOnce } = require('./events-helpers')
const { closeWindow } = require('./window-helpers')
const { BrowserWindow } = remote
describe('renderer nodeIntegrationInSubFrames', () => {
const generateTests = (sandboxEnabled) => {
describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'}`, () => {
let w
beforeEach(async () => {
await closeWindow(w)
w = new BrowserWindow({
show: false,
width: 400,
height: 400,
webPreferences: {
sandbox: sandboxEnabled,
preload: path.resolve(__dirname, 'fixtures/sub-frames/preload.js'),
nodeIntegrationInSubFrames: true
}
})
})
afterEach(() => {
return closeWindow(w).then(() => { w = null })
})
it('should load preload scripts in top level iframes', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [event1, event2] = await detailsPromise
expect(event1[0].frameId).to.not.equal(event2[0].frameId)
expect(event1[0].frameId).to.equal(event1[2])
expect(event2[0].frameId).to.equal(event2[2])
})
it('should load preload scripts in nested iframes', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 3)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame-container.html'))
const [event1, event2, event3] = await detailsPromise
expect(event1[0].frameId).to.not.equal(event2[0].frameId)
expect(event1[0].frameId).to.not.equal(event3[0].frameId)
expect(event2[0].frameId).to.not.equal(event3[0].frameId)
expect(event1[0].frameId).to.equal(event1[2])
expect(event2[0].frameId).to.equal(event2[2])
expect(event3[0].frameId).to.equal(event3[2])
})
it('should correctly reply to the main frame with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [event1] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event1[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event1[0].frameId)
})
it('should correctly reply to the sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [, event2] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event2[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event2[0].frameId)
})
it('should correctly reply to the nested sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 3)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame-container.html'))
const [,, event3] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event3[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event3[0].frameId)
})
})
}
generateTests(false)
generateTests(true)
})

Просмотреть файл

@ -20,10 +20,23 @@ const waitForEvent = (target, eventName) => {
* @return {!Promise<!Array>} With Event as the first item.
*/
const emittedOnce = (emitter, eventName) => {
return emittedNTimes(emitter, eventName, 1).then(([result]) => result)
}
const emittedNTimes = (emitter, eventName, times) => {
const events = []
return new Promise(resolve => {
emitter.once(eventName, (...args) => resolve(args))
const handler = (...args) => {
events.push(args)
if (events.length === times) {
emitter.removeListener(eventName, handler)
resolve(events)
}
}
emitter.on(eventName, handler)
})
}
exports.emittedOnce = emittedOnce
exports.emittedNTimes = emittedNTimes
exports.waitForEvent = waitForEvent

13
spec/fixtures/sub-frames/frame-container.html поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is the root page
<iframe src="./frame.html"></iframe>
</body>
</html>

13
spec/fixtures/sub-frames/frame-with-frame-container.html поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is the root page
<iframe src="./frame-with-frame.html"></iframe>
</body>
</html>

13
spec/fixtures/sub-frames/frame-with-frame.html поставляемый Normal file
Просмотреть файл

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a frame, is has one child
<iframe src="./frame.html"></iframe>
</body>
</html>

12
spec/fixtures/sub-frames/frame.html поставляемый Normal file
Просмотреть файл

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a frame, it has no children
</body>
</html>

7
spec/fixtures/sub-frames/preload.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
const { ipcRenderer, webFrame } = require('electron')
ipcRenderer.send('preload-ran', window.location.href, webFrame.routingId)
ipcRenderer.on('preload-ping', () => {
ipcRenderer.send('preload-pong', webFrame.routingId)
})