api-ipc-spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. 'use strict'
  2. const assert = require('assert')
  3. const http = require('http')
  4. const path = require('path')
  5. const {closeWindow} = require('./window-helpers')
  6. const {ipcRenderer, remote} = require('electron')
  7. const {ipcMain, webContents, BrowserWindow} = remote
  8. const comparePaths = function (path1, path2) {
  9. if (process.platform === 'win32') {
  10. path1 = path1.toLowerCase()
  11. path2 = path2.toLowerCase()
  12. }
  13. assert.equal(path1, path2)
  14. }
  15. describe('ipc module', function () {
  16. var fixtures = path.join(__dirname, 'fixtures')
  17. var w = null
  18. afterEach(function () {
  19. return closeWindow(w).then(function () { w = null })
  20. })
  21. describe('remote.require', function () {
  22. it('should returns same object for the same module', function () {
  23. var dialog1 = remote.require('electron')
  24. var dialog2 = remote.require('electron')
  25. assert.equal(dialog1, dialog2)
  26. })
  27. it('should work when object contains id property', function () {
  28. var a = remote.require(path.join(fixtures, 'module', 'id.js'))
  29. assert.equal(a.id, 1127)
  30. })
  31. it('should work when object has no prototype', function () {
  32. var a = remote.require(path.join(fixtures, 'module', 'no-prototype.js'))
  33. assert.equal(a.foo.constructor.name, '')
  34. assert.equal(a.foo.bar, 'baz')
  35. assert.equal(a.foo.baz, false)
  36. assert.equal(a.bar, 1234)
  37. assert.equal(a.anonymous.constructor.name, '')
  38. assert.equal(a.getConstructorName(Object.create(null)), '')
  39. assert.equal(a.getConstructorName(new (class {})()), '')
  40. })
  41. it('should search module from the user app', function () {
  42. comparePaths(path.normalize(remote.process.mainModule.filename), path.resolve(__dirname, 'static', 'main.js'))
  43. comparePaths(path.normalize(remote.process.mainModule.paths[0]), path.resolve(__dirname, 'static', 'node_modules'))
  44. })
  45. it('should work with function properties', function () {
  46. var a = remote.require(path.join(fixtures, 'module', 'export-function-with-properties.js'))
  47. assert.equal(typeof a, 'function')
  48. assert.equal(a.bar, 'baz')
  49. a = remote.require(path.join(fixtures, 'module', 'function-with-properties.js'))
  50. assert.equal(typeof a, 'object')
  51. assert.equal(a.foo(), 'hello')
  52. assert.equal(a.foo.bar, 'baz')
  53. assert.equal(a.foo.nested.prop, 'yes')
  54. assert.equal(a.foo.method1(), 'world')
  55. assert.equal(a.foo.method1.prop1(), 123)
  56. assert.ok(Object.keys(a.foo).includes('bar'))
  57. assert.ok(Object.keys(a.foo).includes('nested'))
  58. assert.ok(Object.keys(a.foo).includes('method1'))
  59. a = remote.require(path.join(fixtures, 'module', 'function-with-missing-properties.js')).setup()
  60. assert.equal(a.bar(), true)
  61. assert.equal(a.bar.baz, undefined)
  62. })
  63. it('should work with static class members', function () {
  64. var a = remote.require(path.join(fixtures, 'module', 'remote-static.js'))
  65. assert.equal(typeof a.Foo, 'function')
  66. assert.equal(a.Foo.foo(), 3)
  67. assert.equal(a.Foo.bar, 'baz')
  68. var foo = new a.Foo()
  69. assert.equal(foo.baz(), 123)
  70. })
  71. it('handles circular references in arrays and objects', function () {
  72. var a = remote.require(path.join(fixtures, 'module', 'circular.js'))
  73. var arrayA = ['foo']
  74. var arrayB = [arrayA, 'bar']
  75. arrayA.push(arrayB)
  76. assert.deepEqual(a.returnArgs(arrayA, arrayB), [
  77. ['foo', [null, 'bar']],
  78. [['foo', null], 'bar']
  79. ])
  80. var objectA = {foo: 'bar'}
  81. var objectB = {baz: objectA}
  82. objectA.objectB = objectB
  83. assert.deepEqual(a.returnArgs(objectA, objectB), [
  84. {foo: 'bar', objectB: {baz: null}},
  85. {baz: {foo: 'bar', objectB: null}}
  86. ])
  87. arrayA = [1, 2, 3]
  88. assert.deepEqual(a.returnArgs({foo: arrayA}, {bar: arrayA}), [
  89. {foo: [1, 2, 3]},
  90. {bar: [1, 2, 3]}
  91. ])
  92. objectA = {foo: 'bar'}
  93. assert.deepEqual(a.returnArgs({foo: objectA}, {bar: objectA}), [
  94. {foo: {foo: 'bar'}},
  95. {bar: {foo: 'bar'}}
  96. ])
  97. arrayA = []
  98. arrayA.push(arrayA)
  99. assert.deepEqual(a.returnArgs(arrayA), [
  100. [null]
  101. ])
  102. objectA = {}
  103. objectA.foo = objectA
  104. objectA.bar = 'baz'
  105. assert.deepEqual(a.returnArgs(objectA), [
  106. {foo: null, bar: 'baz'}
  107. ])
  108. objectA = {}
  109. objectA.foo = {bar: objectA}
  110. objectA.bar = 'baz'
  111. assert.deepEqual(a.returnArgs(objectA), [
  112. {foo: {bar: null}, bar: 'baz'}
  113. ])
  114. })
  115. })
  116. describe('remote.createFunctionWithReturnValue', function () {
  117. it('should be called in browser synchronously', function () {
  118. var buf = new Buffer('test')
  119. var call = remote.require(path.join(fixtures, 'module', 'call.js'))
  120. var result = call.call(remote.createFunctionWithReturnValue(buf))
  121. assert.equal(result.constructor.name, 'Buffer')
  122. })
  123. })
  124. describe('remote object in renderer', function () {
  125. it('can change its properties', function () {
  126. var property = remote.require(path.join(fixtures, 'module', 'property.js'))
  127. assert.equal(property.property, 1127)
  128. property.property = 1007
  129. assert.equal(property.property, 1007)
  130. assert.equal(property.getFunctionProperty(), 'foo-browser')
  131. property.func.property = 'bar'
  132. assert.equal(property.getFunctionProperty(), 'bar-browser')
  133. property.func.property = 'foo' // revert back
  134. var property2 = remote.require(path.join(fixtures, 'module', 'property.js'))
  135. assert.equal(property2.property, 1007)
  136. property.property = 1127
  137. })
  138. it('can construct an object from its member', function () {
  139. var call = remote.require(path.join(fixtures, 'module', 'call.js'))
  140. var obj = new call.constructor()
  141. assert.equal(obj.test, 'test')
  142. })
  143. it('can reassign and delete its member functions', function () {
  144. var remoteFunctions = remote.require(path.join(fixtures, 'module', 'function.js'))
  145. assert.equal(remoteFunctions.aFunction(), 1127)
  146. remoteFunctions.aFunction = function () { return 1234 }
  147. assert.equal(remoteFunctions.aFunction(), 1234)
  148. assert.equal(delete remoteFunctions.aFunction, true)
  149. })
  150. it('is referenced by its members', function () {
  151. let stringify = remote.getGlobal('JSON').stringify
  152. global.gc()
  153. stringify({})
  154. })
  155. })
  156. describe('remote value in browser', function () {
  157. const print = path.join(fixtures, 'module', 'print_name.js')
  158. const printName = remote.require(print)
  159. it('keeps its constructor name for objects', function () {
  160. const buf = new Buffer('test')
  161. assert.equal(printName.print(buf), 'Buffer')
  162. })
  163. it('supports instanceof Date', function () {
  164. const now = new Date()
  165. assert.equal(printName.print(now), 'Date')
  166. assert.deepEqual(printName.echo(now), now)
  167. })
  168. it('supports instanceof Buffer', function () {
  169. const buffer = Buffer.from('test')
  170. assert.ok(buffer.equals(printName.echo(buffer)))
  171. const objectWithBuffer = {a: 'foo', b: Buffer.from('bar')}
  172. assert.ok(objectWithBuffer.b.equals(printName.echo(objectWithBuffer).b))
  173. const arrayWithBuffer = [1, 2, Buffer.from('baz')]
  174. assert.ok(arrayWithBuffer[2].equals(printName.echo(arrayWithBuffer)[2]))
  175. })
  176. it('supports TypedArray', function () {
  177. const values = [1, 2, 3, 4]
  178. assert.deepEqual(printName.typedArray(values), values)
  179. const int16values = new Int16Array([1, 2, 3, 4])
  180. assert.deepEqual(printName.typedArray(int16values), int16values)
  181. })
  182. })
  183. describe('remote promise', function () {
  184. it('can be used as promise in each side', function (done) {
  185. var promise = remote.require(path.join(fixtures, 'module', 'promise.js'))
  186. promise.twicePromise(Promise.resolve(1234)).then(function (value) {
  187. assert.equal(value, 2468)
  188. done()
  189. })
  190. })
  191. it('handles rejections via catch(onRejected)', function (done) {
  192. var promise = remote.require(path.join(fixtures, 'module', 'rejected-promise.js'))
  193. promise.reject(Promise.resolve(1234)).catch(function (error) {
  194. assert.equal(error.message, 'rejected')
  195. done()
  196. })
  197. })
  198. it('handles rejections via then(onFulfilled, onRejected)', function (done) {
  199. var promise = remote.require(path.join(fixtures, 'module', 'rejected-promise.js'))
  200. promise.reject(Promise.resolve(1234)).then(function () {}, function (error) {
  201. assert.equal(error.message, 'rejected')
  202. done()
  203. })
  204. })
  205. it('does not emit unhandled rejection events in the main process', function (done) {
  206. remote.process.once('unhandledRejection', function (reason) {
  207. done(reason)
  208. })
  209. var promise = remote.require(path.join(fixtures, 'module', 'unhandled-rejection.js'))
  210. promise.reject().then(function () {
  211. done(new Error('Promise was not rejected'))
  212. }).catch(function (error) {
  213. assert.equal(error.message, 'rejected')
  214. done()
  215. })
  216. })
  217. it('emits unhandled rejection events in the renderer process', function (done) {
  218. window.addEventListener('unhandledrejection', function (event) {
  219. event.preventDefault()
  220. assert.equal(event.reason.message, 'rejected')
  221. done()
  222. })
  223. var promise = remote.require(path.join(fixtures, 'module', 'unhandled-rejection.js'))
  224. promise.reject().then(function () {
  225. done(new Error('Promise was not rejected'))
  226. })
  227. })
  228. })
  229. describe('remote webContents', function () {
  230. it('can return same object with different getters', function () {
  231. var contents1 = remote.getCurrentWindow().webContents
  232. var contents2 = remote.getCurrentWebContents()
  233. assert(contents1 === contents2)
  234. })
  235. })
  236. describe('remote class', function () {
  237. let cl = remote.require(path.join(fixtures, 'module', 'class.js'))
  238. let base = cl.base
  239. let derived = cl.derived
  240. it('can get methods', function () {
  241. assert.equal(base.method(), 'method')
  242. })
  243. it('can get properties', function () {
  244. assert.equal(base.readonly, 'readonly')
  245. })
  246. it('can change properties', function () {
  247. assert.equal(base.value, 'old')
  248. base.value = 'new'
  249. assert.equal(base.value, 'new')
  250. base.value = 'old'
  251. })
  252. it('has unenumerable methods', function () {
  253. assert(!base.hasOwnProperty('method'))
  254. assert(Object.getPrototypeOf(base).hasOwnProperty('method'))
  255. })
  256. it('keeps prototype chain in derived class', function () {
  257. assert.equal(derived.method(), 'method')
  258. assert.equal(derived.readonly, 'readonly')
  259. assert(!derived.hasOwnProperty('method'))
  260. let proto = Object.getPrototypeOf(derived)
  261. assert(!proto.hasOwnProperty('method'))
  262. assert(Object.getPrototypeOf(proto).hasOwnProperty('method'))
  263. })
  264. it('is referenced by methods in prototype chain', function () {
  265. let method = derived.method
  266. derived = null
  267. global.gc()
  268. assert.equal(method(), 'method')
  269. })
  270. })
  271. describe('ipc.sender.send', function () {
  272. it('should work when sending an object containing id property', function (done) {
  273. var obj = {
  274. id: 1,
  275. name: 'ly'
  276. }
  277. ipcRenderer.once('message', function (event, message) {
  278. assert.deepEqual(message, obj)
  279. done()
  280. })
  281. ipcRenderer.send('message', obj)
  282. })
  283. it('can send instances of Date', function (done) {
  284. const currentDate = new Date()
  285. ipcRenderer.once('message', function (event, value) {
  286. assert.equal(value, currentDate.toISOString())
  287. done()
  288. })
  289. ipcRenderer.send('message', currentDate)
  290. })
  291. it('can send instances of Buffer', function (done) {
  292. const buffer = Buffer.from('hello')
  293. ipcRenderer.once('message', function (event, message) {
  294. assert.ok(buffer.equals(message))
  295. done()
  296. })
  297. ipcRenderer.send('message', buffer)
  298. })
  299. it('can send objects with DOM class prototypes', function (done) {
  300. ipcRenderer.once('message', function (event, value) {
  301. assert.equal(value.protocol, 'file:')
  302. assert.equal(value.hostname, '')
  303. done()
  304. })
  305. ipcRenderer.send('message', document.location)
  306. })
  307. it('can send Electron API objects', function (done) {
  308. const webContents = remote.getCurrentWebContents()
  309. ipcRenderer.once('message', function (event, value) {
  310. assert.deepEqual(value.browserWindowOptions, webContents.browserWindowOptions)
  311. done()
  312. })
  313. ipcRenderer.send('message', webContents)
  314. })
  315. it('does not crash on external objects (regression)', function (done) {
  316. const request = http.request({port: 5000, hostname: '127.0.0.1', method: 'GET', path: '/'})
  317. const stream = request.agent.sockets['127.0.0.1:5000:'][0]._handle._externalStream
  318. request.on('error', function () {})
  319. ipcRenderer.once('message', function (event, requestValue, externalStreamValue) {
  320. assert.equal(requestValue.method, 'GET')
  321. assert.equal(requestValue.path, '/')
  322. assert.equal(externalStreamValue, null)
  323. done()
  324. })
  325. ipcRenderer.send('message', request, stream)
  326. })
  327. it('can send objects that both reference the same object', function (done) {
  328. const child = {hello: 'world'}
  329. const foo = {name: 'foo', child: child}
  330. const bar = {name: 'bar', child: child}
  331. const array = [foo, bar]
  332. ipcRenderer.once('message', function (event, arrayValue, fooValue, barValue, childValue) {
  333. assert.deepEqual(arrayValue, array)
  334. assert.deepEqual(fooValue, foo)
  335. assert.deepEqual(barValue, bar)
  336. assert.deepEqual(childValue, child)
  337. done()
  338. })
  339. ipcRenderer.send('message', array, foo, bar, child)
  340. })
  341. it('inserts null for cyclic references', function (done) {
  342. const array = [5]
  343. array.push(array)
  344. const child = {hello: 'world'}
  345. child.child = child
  346. ipcRenderer.once('message', function (event, arrayValue, childValue) {
  347. assert.equal(arrayValue[0], 5)
  348. assert.equal(arrayValue[1], null)
  349. assert.equal(childValue.hello, 'world')
  350. assert.equal(childValue.child, null)
  351. done()
  352. })
  353. ipcRenderer.send('message', array, child)
  354. })
  355. })
  356. describe('ipc.sendSync', function () {
  357. afterEach(function () {
  358. ipcMain.removeAllListeners('send-sync-message')
  359. })
  360. it('can be replied by setting event.returnValue', function () {
  361. var msg = ipcRenderer.sendSync('echo', 'test')
  362. assert.equal(msg, 'test')
  363. })
  364. it('does not crash when reply is not sent and browser is destroyed', function (done) {
  365. w = new BrowserWindow({
  366. show: false
  367. })
  368. ipcMain.once('send-sync-message', function (event) {
  369. event.returnValue = null
  370. done()
  371. })
  372. w.loadURL('file://' + path.join(fixtures, 'api', 'send-sync-message.html'))
  373. })
  374. it('does not crash when reply is sent by multiple listeners', function (done) {
  375. w = new BrowserWindow({
  376. show: false
  377. })
  378. ipcMain.on('send-sync-message', function (event) {
  379. event.returnValue = null
  380. })
  381. ipcMain.on('send-sync-message', function (event) {
  382. event.returnValue = null
  383. done()
  384. })
  385. w.loadURL('file://' + path.join(fixtures, 'api', 'send-sync-message.html'))
  386. })
  387. })
  388. describe('ipcRenderer.sendTo', function () {
  389. let contents = null
  390. beforeEach(function () {
  391. contents = webContents.create({})
  392. })
  393. afterEach(function () {
  394. ipcRenderer.removeAllListeners('pong')
  395. contents.destroy()
  396. contents = null
  397. })
  398. it('sends message to WebContents', function (done) {
  399. const webContentsId = remote.getCurrentWebContents().id
  400. ipcRenderer.once('pong', function (event, id) {
  401. assert.equal(webContentsId, id)
  402. done()
  403. })
  404. contents.once('did-finish-load', function () {
  405. ipcRenderer.sendTo(contents.id, 'ping', webContentsId)
  406. })
  407. contents.loadURL('file://' + path.join(fixtures, 'pages', 'ping-pong.html'))
  408. })
  409. })
  410. describe('remote listeners', function () {
  411. it('can be added and removed correctly', function () {
  412. w = new BrowserWindow({
  413. show: false
  414. })
  415. var listener = function () {}
  416. w.on('test', listener)
  417. assert.equal(w.listenerCount('test'), 1)
  418. w.removeListener('test', listener)
  419. assert.equal(w.listenerCount('test'), 0)
  420. })
  421. it('detaches listeners subscribed to destroyed renderers, and shows a warning', (done) => {
  422. w = new BrowserWindow({
  423. show: false
  424. })
  425. w.webContents.once('did-finish-load', () => {
  426. w.webContents.once('did-finish-load', () => {
  427. const expectedMessage = [
  428. 'Attempting to call a function in a renderer window that has been closed or released.',
  429. 'Function provided here: remote-event-handler.html:11:33',
  430. 'Remote event names: remote-handler, other-remote-handler'
  431. ].join('\n')
  432. const results = ipcRenderer.sendSync('try-emit-web-contents-event', w.webContents.id, 'remote-handler')
  433. assert.deepEqual(results, {
  434. warningMessage: expectedMessage,
  435. listenerCountBefore: 2,
  436. listenerCountAfter: 1
  437. })
  438. done()
  439. })
  440. w.webContents.reload()
  441. })
  442. w.loadURL('file://' + path.join(fixtures, 'api', 'remote-event-handler.html'))
  443. })
  444. })
  445. it('throws an error when removing all the listeners', () => {
  446. ipcMain.on('test-event', () => {})
  447. assert.equal(ipcMain.listenerCount('test-event'), 1)
  448. ipcRenderer.on('test-event', () => {})
  449. assert.equal(ipcRenderer.listenerCount('test-event'), 1)
  450. assert.throws(() => {
  451. ipcMain.removeAllListeners()
  452. }, /Removing all listeners from ipcMain will make Electron internals stop working/)
  453. assert.throws(() => {
  454. ipcRenderer.removeAllListeners()
  455. }, /Removing all listeners from ipcRenderer will make Electron internals stop working/)
  456. ipcMain.removeAllListeners('test-event')
  457. assert.equal(ipcMain.listenerCount('test-event'), 0)
  458. ipcRenderer.removeAllListeners('test-event')
  459. assert.equal(ipcRenderer.listenerCount('test-event'), 0)
  460. })
  461. describe('remote objects registry', function () {
  462. it('does not dereference until the render view is deleted (regression)', function (done) {
  463. w = new BrowserWindow({
  464. show: false
  465. })
  466. ipcMain.once('error-message', (event, message) => {
  467. assert(message.startsWith('Cannot call function \'getURL\' on missing remote object'), message)
  468. done()
  469. })
  470. w.loadURL('file://' + path.join(fixtures, 'api', 'render-view-deleted.html'))
  471. })
  472. })
  473. })