diff --git a/spec-main/api-ipc-renderer-spec.ts b/spec-main/api-ipc-renderer-spec.ts new file mode 100644 index 0000000000000..01add860cd1e7 --- /dev/null +++ b/spec-main/api-ipc-renderer-spec.ts @@ -0,0 +1,197 @@ +import { expect } from 'chai' +import * as path from 'path' +import { ipcMain, BrowserWindow, WebContents, WebPreferences, webContents } from 'electron' +import { emittedOnce } from './events-helpers' +import { closeWindow } from './window-helpers'; + +describe('ipcRenderer module', () => { + const fixtures = path.join(__dirname, '..', 'spec', 'fixtures') + + let w: BrowserWindow + before(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + await w.loadURL('about:blank') + }) + after(async () => { + await closeWindow(w) + w = null as unknown as BrowserWindow + }) + + describe('send()', () => { + it('should work when sending an object containing id property', async () => { + const obj = { + id: 1, + name: 'ly' + } + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + ipcRenderer.send('message', ${JSON.stringify(obj)}) + }`) + const [, received] = await emittedOnce(ipcMain, 'message') + expect(received).to.deep.equal(obj) + }) + + it('can send instances of Date as ISO strings', async () => { + const isoDate = new Date().toISOString() + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + ipcRenderer.send('message', new Date(${JSON.stringify(isoDate)})) + }`) + const [, received] = await emittedOnce(ipcMain, 'message') + expect(received).to.equal(isoDate) + }) + + it('can send instances of Buffer', async () => { + const data = 'hello' + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + ipcRenderer.send('message', Buffer.from(${JSON.stringify(data)})) + }`) + const [, received] = await emittedOnce(ipcMain, 'message') + expect(received).to.be.an.instanceOf(Buffer) + expect(Buffer.from(data).equals(received)).to.be.true() + }) + + it('can send objects with DOM class prototypes', async () => { + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + ipcRenderer.send('message', document.location) + }`) + const [, value] = await emittedOnce(ipcMain, 'message') + expect(value.protocol).to.equal('about:') + expect(value.hostname).to.equal('') + }) + + it('does not crash on external objects (regression)', async () => { + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + const http = require('http') + + const request = http.request({ port: 5000, hostname: '127.0.0.1', method: 'GET', path: '/' }) + const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream + request.on('error', () => {}) + + ipcRenderer.send('message', request, stream) + }`) + const [, requestValue, externalStreamValue] = await emittedOnce(ipcMain, 'message') + + expect(requestValue.method).to.equal('GET') + expect(requestValue.path).to.equal('/') + expect(externalStreamValue).to.be.null() + }) + + it('can send objects that both reference the same object', async () => { + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + + const child = { hello: 'world' } + const foo = { name: 'foo', child: child } + const bar = { name: 'bar', child: child } + const array = [foo, bar] + + ipcRenderer.send('message', array, foo, bar, child) + }`) + + const child = { hello: 'world' } + const foo = { name: 'foo', child: child } + const bar = { name: 'bar', child: child } + const array = [foo, bar] + + const [, arrayValue, fooValue, barValue, childValue] = await emittedOnce(ipcMain, 'message') + expect(arrayValue).to.deep.equal(array) + expect(fooValue).to.deep.equal(foo) + expect(barValue).to.deep.equal(bar) + expect(childValue).to.deep.equal(child) + }) + + it('inserts null for cyclic references', async () => { + w.webContents.executeJavaScript(`{ + const { ipcRenderer } = require('electron') + const array = [5] + array.push(array) + + const child = { hello: 'world' } + child.child = child + ipcRenderer.send('message', array, child) + }`) + + const [, arrayValue, childValue] = await emittedOnce(ipcMain, 'message') + expect(arrayValue[0]).to.equal(5) + expect(arrayValue[1]).to.be.null() + + expect(childValue.hello).to.equal('world') + expect(childValue.child).to.be.null() + }) + }) + + describe('sendSync()', () => { + it('can be replied to by setting event.returnValue', async () => { + ipcMain.once('echo', (event, msg) => { + event.returnValue = msg + }) + const msg = await w.webContents.executeJavaScript(`new Promise(resolve => { + const { ipcRenderer } = require('electron') + resolve(ipcRenderer.sendSync('echo', 'test')) + })`) + expect(msg).to.equal('test') + }) + }) + + describe('sendTo()', () => { + const generateSpecs = (description: string, webPreferences: WebPreferences) => { + describe(description, () => { + let contents: WebContents + const payload = 'Hello World!' + + before(async () => { + contents = (webContents as any).create({ + preload: path.join(fixtures, 'module', 'preload-ipc-ping-pong.js'), + ...webPreferences + }) + + await contents.loadURL('about:blank') + }) + + after(() => { + (contents as any).destroy() + contents = null as unknown as WebContents + }) + + it('sends message to WebContents', async () => { + const data = await w.webContents.executeJavaScript(`new Promise(resolve => { + const { ipcRenderer } = require('electron') + ipcRenderer.sendTo(${contents.id}, 'ping', ${JSON.stringify(payload)}) + ipcRenderer.once('pong', (event, data) => resolve(data)) + })`) + expect(data).to.equal(payload) + }) + + it('sends message on channel with non-ASCII characters to WebContents', async () => { + const data = await w.webContents.executeJavaScript(`new Promise(resolve => { + const { ipcRenderer } = require('electron') + ipcRenderer.sendTo(${contents.id}, 'ping-æøåü', ${JSON.stringify(payload)}) + ipcRenderer.once('pong-æøåü', (event, data) => resolve(data)) + })`) + expect(data).to.equal(payload) + }) + }) + } + + generateSpecs('without sandbox', {}) + generateSpecs('with sandbox', { sandbox: true }) + generateSpecs('with contextIsolation', { contextIsolation: true }) + generateSpecs('with contextIsolation + sandbox', { contextIsolation: true, sandbox: true }) + }) + /* + + */ + + describe('ipcRenderer.on', () => { + it('is not used for internals', async () => { + const result = await w.webContents.executeJavaScript(` + require('electron').ipcRenderer.eventNames() + `) + expect(result).to.deep.equal([]) + }) + }) +}) diff --git a/spec/api-ipc-renderer-spec.js b/spec/api-ipc-renderer-spec.js deleted file mode 100644 index 475a0ac0cb265..0000000000000 --- a/spec/api-ipc-renderer-spec.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const http = require('http') -const path = require('path') -const { closeWindow } = require('./window-helpers') - -const { expect } = chai -chai.use(dirtyChai) - -const { ipcRenderer, remote } = require('electron') -const { ipcMain, webContents, BrowserWindow } = remote - -describe('ipc renderer module', () => { - const fixtures = path.join(__dirname, 'fixtures') - - let w = null - - afterEach(() => closeWindow(w).then(() => { w = null })) - - describe('ipc.sender.send', () => { - it('should work when sending an object containing id property', done => { - const obj = { - id: 1, - name: 'ly' - } - ipcRenderer.once('message', (event, message) => { - expect(message).to.deep.equal(obj) - done() - }) - ipcRenderer.send('message', obj) - }) - - it('can send instances of Date', done => { - const currentDate = new Date() - ipcRenderer.once('message', (event, value) => { - expect(value).to.equal(currentDate.toISOString()) - done() - }) - ipcRenderer.send('message', currentDate) - }) - - it('can send instances of Buffer', done => { - const buffer = Buffer.from('hello') - ipcRenderer.once('message', (event, message) => { - expect(buffer.equals(message)).to.be.true() - done() - }) - ipcRenderer.send('message', buffer) - }) - - it('can send objects with DOM class prototypes', done => { - ipcRenderer.once('message', (event, value) => { - expect(value.protocol).to.equal('file:') - expect(value.hostname).to.equal('') - done() - }) - ipcRenderer.send('message', document.location) - }) - - it('can send Electron API objects', done => { - const webContents = remote.getCurrentWebContents() - ipcRenderer.once('message', (event, value) => { - expect(value.browserWindowOptions).to.deep.equal(webContents.browserWindowOptions) - done() - }) - ipcRenderer.send('message', webContents) - }) - - it('does not crash on external objects (regression)', done => { - const request = http.request({ port: 5000, hostname: '127.0.0.1', method: 'GET', path: '/' }) - const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream - request.on('error', () => {}) - ipcRenderer.once('message', (event, requestValue, externalStreamValue) => { - expect(requestValue.method).to.equal('GET') - expect(requestValue.path).to.equal('/') - expect(externalStreamValue).to.be.null() - done() - }) - - ipcRenderer.send('message', request, stream) - }) - - it('can send objects that both reference the same object', done => { - const child = { hello: 'world' } - const foo = { name: 'foo', child: child } - const bar = { name: 'bar', child: child } - const array = [foo, bar] - - ipcRenderer.once('message', (event, arrayValue, fooValue, barValue, childValue) => { - expect(arrayValue).to.deep.equal(array) - expect(fooValue).to.deep.equal(foo) - expect(barValue).to.deep.equal(bar) - expect(childValue).to.deep.equal(child) - done() - }) - ipcRenderer.send('message', array, foo, bar, child) - }) - - it('inserts null for cyclic references', done => { - const array = [5] - array.push(array) - - const child = { hello: 'world' } - child.child = child - - ipcRenderer.once('message', (event, arrayValue, childValue) => { - expect(arrayValue[0]).to.equal(5) - expect(arrayValue[1]).to.be.null() - - expect(childValue.hello).to.equal('world') - expect(childValue.child).to.be.null() - - done() - }) - ipcRenderer.send('message', array, child) - }) - }) - - describe('ipc.sendSync', () => { - afterEach(() => { - ipcMain.removeAllListeners('send-sync-message') - }) - - it('can be replied by setting event.returnValue', () => { - const msg = ipcRenderer.sendSync('echo', 'test') - expect(msg).to.equal('test') - }) - }) - - describe('ipcRenderer.sendTo', () => { - let contents = null - - afterEach(() => { - ipcRenderer.removeAllListeners('pong') - contents.destroy() - contents = null - }) - - const generateSpecs = (description, webPreferences) => { - describe(description, () => { - it('sends message to WebContents', done => { - contents = webContents.create({ - preload: path.join(fixtures, 'module', 'preload-ipc-ping-pong.js'), - ...webPreferences - }) - - const payload = 'Hello World!' - - ipcRenderer.once('pong', (event, data) => { - expect(payload).to.equal(data) - done() - }) - - contents.once('did-finish-load', () => { - ipcRenderer.sendTo(contents.id, 'ping', payload) - }) - - contents.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - }) - - it('sends message to WebContents (channel has special chars)', done => { - contents = webContents.create({ - preload: path.join(fixtures, 'module', 'preload-ipc-ping-pong.js'), - ...webPreferences - }) - - const payload = 'Hello World!' - - ipcRenderer.once('pong-æøåü', (event, data) => { - expect(payload).to.equal(data) - done() - }) - - contents.once('did-finish-load', () => { - ipcRenderer.sendTo(contents.id, 'ping-æøåü', payload) - }) - - contents.loadFile(path.join(fixtures, 'pages', 'base-page.html')) - }) - }) - } - - generateSpecs('without sandbox', {}) - generateSpecs('with sandbox', { sandbox: true }) - generateSpecs('with contextIsolation', { contextIsolation: true }) - generateSpecs('with contextIsolation + sandbox', { contextIsolation: true, sandbox: true }) - }) - - describe('remote listeners', () => { - it('detaches listeners subscribed to destroyed renderers, and shows a warning', (done) => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - - w.webContents.once('did-finish-load', () => { - w.webContents.once('did-finish-load', () => { - const expectedMessage = [ - 'Attempting to call a function in a renderer window that has been closed or released.', - 'Function provided here: remote-event-handler.html:11:33', - 'Remote event names: remote-handler, other-remote-handler' - ].join('\n') - - const results = ipcRenderer.sendSync('try-emit-web-contents-event', w.webContents.id, 'remote-handler') - - expect(results).to.deep.equal({ - warningMessage: expectedMessage, - listenerCountBefore: 2, - listenerCountAfter: 1 - }) - done() - }) - - w.webContents.reload() - }) - w.loadFile(path.join(fixtures, 'api', 'remote-event-handler.html')) - }) - }) - - describe('ipcRenderer.on', () => { - it('is not used for internals', async () => { - w = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }) - await w.loadURL('about:blank') - - const script = `require('electron').ipcRenderer.eventNames()` - const result = await w.webContents.executeJavaScript(script) - expect(result).to.deep.equal([]) - }) - }) -}) diff --git a/spec/api-remote-spec.js b/spec/api-remote-spec.js index 4564560d74ea8..6ad3b9534580b 100644 --- a/spec/api-remote-spec.js +++ b/spec/api-remote-spec.js @@ -508,4 +508,40 @@ describe('remote module', () => { w.loadURL('about:blank') }) }) + + describe('remote listeners', () => { + let w = null + afterEach(() => closeWindow(w).then(() => { w = null })) + + it('detaches listeners subscribed to destroyed renderers, and shows a warning', (done) => { + w = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true + } + }) + + w.webContents.once('did-finish-load', () => { + w.webContents.once('did-finish-load', () => { + const expectedMessage = [ + 'Attempting to call a function in a renderer window that has been closed or released.', + 'Function provided here: remote-event-handler.html:11:33', + 'Remote event names: remote-handler, other-remote-handler' + ].join('\n') + + const results = ipcRenderer.sendSync('try-emit-web-contents-event', w.webContents.id, 'remote-handler') + + expect(results).to.deep.equal({ + warningMessage: expectedMessage, + listenerCountBefore: 2, + listenerCountAfter: 1 + }) + done() + }) + + w.webContents.reload() + }) + w.loadFile(path.join(fixtures, 'api', 'remote-event-handler.html')) + }) + }) })