api-app-spec.js 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. const chai = require('chai')
  2. const chaiAsPromised = require('chai-as-promised')
  3. const dirtyChai = require('dirty-chai')
  4. const ChildProcess = require('child_process')
  5. const https = require('https')
  6. const net = require('net')
  7. const fs = require('fs')
  8. const path = require('path')
  9. const cp = require('child_process')
  10. const { ipcRenderer, remote } = require('electron')
  11. const { emittedOnce } = require('./events-helpers')
  12. const { closeWindow } = require('./window-helpers')
  13. const { expect } = chai
  14. const { app, BrowserWindow, Menu, ipcMain } = remote
  15. const isCI = remote.getGlobal('isCi')
  16. chai.use(chaiAsPromised)
  17. chai.use(dirtyChai)
  18. describe('electron module', () => {
  19. it('does not expose internal modules to require', () => {
  20. expect(() => {
  21. require('clipboard')
  22. }).to.throw(/Cannot find module 'clipboard'/)
  23. })
  24. describe('require("electron")', () => {
  25. let window = null
  26. beforeEach(() => {
  27. window = new BrowserWindow({
  28. show: false,
  29. width: 400,
  30. height: 400,
  31. webPreferences: {
  32. nodeIntegration: true
  33. }
  34. })
  35. })
  36. afterEach(() => {
  37. return closeWindow(window).then(() => { window = null })
  38. })
  39. it('always returns the internal electron module', (done) => {
  40. ipcMain.once('answer', () => done())
  41. window.loadFile(path.join(__dirname, 'fixtures', 'api', 'electron-module-app', 'index.html'))
  42. })
  43. })
  44. })
  45. describe('app module', () => {
  46. let server, secureUrl
  47. const certPath = path.join(__dirname, 'fixtures', 'certificates')
  48. before((done) => {
  49. const options = {
  50. key: fs.readFileSync(path.join(certPath, 'server.key')),
  51. cert: fs.readFileSync(path.join(certPath, 'server.pem')),
  52. ca: [
  53. fs.readFileSync(path.join(certPath, 'rootCA.pem')),
  54. fs.readFileSync(path.join(certPath, 'intermediateCA.pem'))
  55. ],
  56. requestCert: true,
  57. rejectUnauthorized: false
  58. }
  59. server = https.createServer(options, (req, res) => {
  60. if (req.client.authorized) {
  61. res.writeHead(200)
  62. res.end('<title>authorized</title>')
  63. } else {
  64. res.writeHead(401)
  65. res.end('<title>denied</title>')
  66. }
  67. })
  68. server.listen(0, '127.0.0.1', () => {
  69. const port = server.address().port
  70. secureUrl = `https://127.0.0.1:${port}`
  71. done()
  72. })
  73. })
  74. after(done => {
  75. server.close(() => done())
  76. })
  77. describe('app.getVersion()', () => {
  78. it('returns the version field of package.json', () => {
  79. expect(app.getVersion()).to.equal('0.1.0')
  80. })
  81. })
  82. describe('app.setVersion(version)', () => {
  83. it('overrides the version', () => {
  84. expect(app.getVersion()).to.equal('0.1.0')
  85. app.setVersion('test-version')
  86. expect(app.getVersion()).to.equal('test-version')
  87. app.setVersion('0.1.0')
  88. })
  89. })
  90. describe('app.getName()', () => {
  91. it('returns the name field of package.json', () => {
  92. expect(app.getName()).to.equal('Electron Test')
  93. })
  94. })
  95. describe('app.setName(name)', () => {
  96. it('overrides the name', () => {
  97. expect(app.getName()).to.equal('Electron Test')
  98. app.setName('test-name')
  99. expect(app.getName()).to.equal('test-name')
  100. app.setName('Electron Test')
  101. })
  102. })
  103. describe('app.getLocale()', () => {
  104. it('should not be empty', () => {
  105. expect(app.getLocale()).to.not.be.empty()
  106. })
  107. })
  108. describe('app.getLocaleCountryCode()', () => {
  109. it('should be empty or have length of two', () => {
  110. let expectedLength = 2
  111. if (isCI && process.platform === 'linux') {
  112. // Linux CI machines have no locale.
  113. expectedLength = 0
  114. }
  115. expect(app.getLocaleCountryCode()).to.be.a('string').and.have.lengthOf(expectedLength)
  116. })
  117. })
  118. describe('app.isPackaged', () => {
  119. it('should be false durings tests', () => {
  120. expect(app.isPackaged).to.be.false()
  121. })
  122. })
  123. describe('app.isInApplicationsFolder()', () => {
  124. before(function () {
  125. if (process.platform !== 'darwin') {
  126. this.skip()
  127. }
  128. })
  129. it('should be false during tests', () => {
  130. expect(app.isInApplicationsFolder()).to.be.false()
  131. })
  132. })
  133. describe('app.exit(exitCode)', () => {
  134. let appProcess = null
  135. afterEach(() => {
  136. if (appProcess != null) appProcess.kill()
  137. })
  138. it('emits a process exit event with the code', async () => {
  139. const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
  140. const electronPath = remote.getGlobal('process').execPath
  141. let output = ''
  142. appProcess = ChildProcess.spawn(electronPath, [appPath])
  143. appProcess.stdout.on('data', data => { output += data })
  144. const [code] = await emittedOnce(appProcess, 'close')
  145. if (process.platform !== 'win32') {
  146. expect(output).to.include('Exit event with code: 123')
  147. }
  148. expect(code).to.equal(123)
  149. })
  150. it('closes all windows', async function () {
  151. const appPath = path.join(__dirname, 'fixtures', 'api', 'exit-closes-all-windows-app')
  152. const electronPath = remote.getGlobal('process').execPath
  153. appProcess = ChildProcess.spawn(electronPath, [appPath])
  154. const [code, signal] = await emittedOnce(appProcess, 'close')
  155. expect(signal).to.equal(null, 'exit signal should be null, if you see this please tag @MarshallOfSound')
  156. expect(code).to.equal(123, 'exit code should be 123, if you see this please tag @MarshallOfSound')
  157. })
  158. it('exits gracefully', async function () {
  159. if (!['darwin', 'linux'].includes(process.platform)) {
  160. this.skip()
  161. return
  162. }
  163. const electronPath = remote.getGlobal('process').execPath
  164. const appPath = path.join(__dirname, 'fixtures', 'api', 'singleton')
  165. appProcess = ChildProcess.spawn(electronPath, [appPath])
  166. // Singleton will send us greeting data to let us know it's running.
  167. // After that, ask it to exit gracefully and confirm that it does.
  168. appProcess.stdout.on('data', data => appProcess.kill())
  169. const [code, signal] = await emittedOnce(appProcess, 'close')
  170. const message = `code:\n${code}\nsignal:\n${signal}`
  171. expect(code).to.equal(0, message)
  172. expect(signal).to.be.null(message)
  173. })
  174. })
  175. describe('app.requestSingleInstanceLock', () => {
  176. it('prevents the second launch of app', function (done) {
  177. this.timeout(120000)
  178. const appPath = path.join(__dirname, 'fixtures', 'api', 'singleton')
  179. const first = ChildProcess.spawn(remote.process.execPath, [appPath])
  180. first.once('exit', code => {
  181. expect(code).to.equal(0)
  182. })
  183. // Start second app when received output.
  184. first.stdout.once('data', () => {
  185. const second = ChildProcess.spawn(remote.process.execPath, [appPath])
  186. second.once('exit', code => {
  187. expect(code).to.equal(1)
  188. done()
  189. })
  190. })
  191. })
  192. })
  193. describe('app.relaunch', () => {
  194. let server = null
  195. const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch'
  196. beforeEach(done => {
  197. fs.unlink(socketPath, () => {
  198. server = net.createServer()
  199. server.listen(socketPath)
  200. done()
  201. })
  202. })
  203. afterEach((done) => {
  204. server.close(() => {
  205. if (process.platform === 'win32') {
  206. done()
  207. } else {
  208. fs.unlink(socketPath, () => done())
  209. }
  210. })
  211. })
  212. it('relaunches the app', function (done) {
  213. this.timeout(120000)
  214. let state = 'none'
  215. server.once('error', error => done(error))
  216. server.on('connection', client => {
  217. client.once('data', data => {
  218. if (String(data) === 'false' && state === 'none') {
  219. state = 'first-launch'
  220. } else if (String(data) === 'true' && state === 'first-launch') {
  221. done()
  222. } else {
  223. done(`Unexpected state: ${state}`)
  224. }
  225. })
  226. })
  227. const appPath = path.join(__dirname, 'fixtures', 'api', 'relaunch')
  228. ChildProcess.spawn(remote.process.execPath, [appPath])
  229. })
  230. })
  231. describe('app.setUserActivity(type, userInfo)', () => {
  232. before(function () {
  233. if (process.platform !== 'darwin') {
  234. this.skip()
  235. }
  236. })
  237. it('sets the current activity', () => {
  238. app.setUserActivity('com.electron.testActivity', { testData: '123' })
  239. expect(app.getCurrentActivityType()).to.equal('com.electron.testActivity')
  240. })
  241. })
  242. xdescribe('app.importCertificate', () => {
  243. let w = null
  244. before(function () {
  245. if (process.platform !== 'linux') {
  246. this.skip()
  247. }
  248. })
  249. afterEach(() => closeWindow(w).then(() => { w = null }))
  250. it('can import certificate into platform cert store', done => {
  251. const options = {
  252. certificate: path.join(certPath, 'client.p12'),
  253. password: 'electron'
  254. }
  255. w = new BrowserWindow({
  256. show: false,
  257. webPreferences: {
  258. nodeIntegration: true
  259. }
  260. })
  261. w.webContents.on('did-finish-load', () => {
  262. expect(w.webContents.getTitle()).to.equal('authorized')
  263. done()
  264. })
  265. ipcRenderer.once('select-client-certificate', (event, webContentsId, list) => {
  266. expect(webContentsId).to.equal(w.webContents.id)
  267. expect(list).to.have.lengthOf(1)
  268. expect(list[0]).to.deep.equal({
  269. issuerName: 'Intermediate CA',
  270. subjectName: 'Client Cert',
  271. issuer: { commonName: 'Intermediate CA' },
  272. subject: { commonName: 'Client Cert' }
  273. })
  274. event.sender.send('client-certificate-response', list[0])
  275. })
  276. app.importCertificate(options, result => {
  277. expect(result).toNotExist()
  278. ipcRenderer.sendSync('set-client-certificate-option', false)
  279. w.loadURL(secureUrl)
  280. })
  281. })
  282. })
  283. describe('BrowserWindow events', () => {
  284. let w = null
  285. afterEach(() => closeWindow(w).then(() => { w = null }))
  286. it('should emit browser-window-focus event when window is focused', (done) => {
  287. app.once('browser-window-focus', (e, window) => {
  288. expect(w.id).to.equal(window.id)
  289. done()
  290. })
  291. w = new BrowserWindow({ show: false })
  292. w.emit('focus')
  293. })
  294. it('should emit browser-window-blur event when window is blured', (done) => {
  295. app.once('browser-window-blur', (e, window) => {
  296. expect(w.id).to.equal(window.id)
  297. done()
  298. })
  299. w = new BrowserWindow({ show: false })
  300. w.emit('blur')
  301. })
  302. it('should emit browser-window-created event when window is created', (done) => {
  303. app.once('browser-window-created', (e, window) => {
  304. setImmediate(() => {
  305. expect(w.id).to.equal(window.id)
  306. done()
  307. })
  308. })
  309. w = new BrowserWindow({ show: false })
  310. })
  311. it('should emit web-contents-created event when a webContents is created', (done) => {
  312. app.once('web-contents-created', (e, webContents) => {
  313. setImmediate(() => {
  314. expect(w.webContents.id).to.equal(webContents.id)
  315. done()
  316. })
  317. })
  318. w = new BrowserWindow({ show: false })
  319. })
  320. it('should emit desktop-capturer-get-sources event when desktopCapturer.getSources() is invoked', async () => {
  321. w = new BrowserWindow({
  322. show: false,
  323. webPreferences: {
  324. nodeIntegration: true
  325. }
  326. })
  327. await w.loadURL('about:blank')
  328. const promise = emittedOnce(app, 'desktop-capturer-get-sources')
  329. w.webContents.executeJavaScript(`require('electron').desktopCapturer.getSources({ types: ['screen'] }, () => {})`)
  330. const [, webContents] = await promise
  331. expect(webContents).to.equal(w.webContents)
  332. })
  333. it('should emit remote-require event when remote.require() is invoked', async () => {
  334. w = new BrowserWindow({
  335. show: false,
  336. webPreferences: {
  337. nodeIntegration: true
  338. }
  339. })
  340. await w.loadURL('about:blank')
  341. const promise = emittedOnce(app, 'remote-require')
  342. w.webContents.executeJavaScript(`require('electron').remote.require('test')`)
  343. const [, webContents, moduleName] = await promise
  344. expect(webContents).to.equal(w.webContents)
  345. expect(moduleName).to.equal('test')
  346. })
  347. it('should emit remote-get-global event when remote.getGlobal() is invoked', async () => {
  348. w = new BrowserWindow({
  349. show: false,
  350. webPreferences: {
  351. nodeIntegration: true
  352. }
  353. })
  354. await w.loadURL('about:blank')
  355. const promise = emittedOnce(app, 'remote-get-global')
  356. w.webContents.executeJavaScript(`require('electron').remote.getGlobal('test')`)
  357. const [, webContents, globalName] = await promise
  358. expect(webContents).to.equal(w.webContents)
  359. expect(globalName).to.equal('test')
  360. })
  361. it('should emit remote-get-builtin event when remote.getBuiltin() is invoked', async () => {
  362. w = new BrowserWindow({
  363. show: false,
  364. webPreferences: {
  365. nodeIntegration: true
  366. }
  367. })
  368. await w.loadURL('about:blank')
  369. const promise = emittedOnce(app, 'remote-get-builtin')
  370. w.webContents.executeJavaScript(`require('electron').remote.app`)
  371. const [, webContents, moduleName] = await promise
  372. expect(webContents).to.equal(w.webContents)
  373. expect(moduleName).to.equal('app')
  374. })
  375. it('should emit remote-get-current-window event when remote.getCurrentWindow() is invoked', async () => {
  376. w = new BrowserWindow({
  377. show: false,
  378. webPreferences: {
  379. nodeIntegration: true
  380. }
  381. })
  382. await w.loadURL('about:blank')
  383. const promise = emittedOnce(app, 'remote-get-current-window')
  384. w.webContents.executeJavaScript(`require('electron').remote.getCurrentWindow()`)
  385. const [, webContents] = await promise
  386. expect(webContents).to.equal(w.webContents)
  387. })
  388. it('should emit remote-get-current-web-contents event when remote.getCurrentWebContents() is invoked', async () => {
  389. w = new BrowserWindow({
  390. show: false,
  391. webPreferences: {
  392. nodeIntegration: true
  393. }
  394. })
  395. await w.loadURL('about:blank')
  396. const promise = emittedOnce(app, 'remote-get-current-web-contents')
  397. w.webContents.executeJavaScript(`require('electron').remote.getCurrentWebContents()`)
  398. const [, webContents] = await promise
  399. expect(webContents).to.equal(w.webContents)
  400. })
  401. })
  402. describe('app.setBadgeCount', () => {
  403. const platformIsNotSupported =
  404. (process.platform === 'win32') ||
  405. (process.platform === 'linux' && !app.isUnityRunning())
  406. const platformIsSupported = !platformIsNotSupported
  407. const expectedBadgeCount = 42
  408. let returnValue = null
  409. beforeEach(() => { returnValue = app.setBadgeCount(expectedBadgeCount) })
  410. after(() => {
  411. // Remove the badge.
  412. app.setBadgeCount(0)
  413. })
  414. describe('on supported platform', () => {
  415. before(function () {
  416. if (platformIsNotSupported) {
  417. this.skip()
  418. }
  419. })
  420. it('returns true', () => {
  421. expect(returnValue).to.be.true()
  422. })
  423. it('sets a badge count', () => {
  424. expect(app.getBadgeCount()).to.equal(expectedBadgeCount)
  425. })
  426. })
  427. describe('on unsupported platform', () => {
  428. before(function () {
  429. if (platformIsSupported) {
  430. this.skip()
  431. }
  432. })
  433. it('returns false', () => {
  434. expect(returnValue).to.be.false()
  435. })
  436. it('does not set a badge count', () => {
  437. expect(app.getBadgeCount()).to.equal(0)
  438. })
  439. })
  440. })
  441. describe('app.get/setLoginItemSettings API', function () {
  442. const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe')
  443. const processStartArgs = [
  444. '--processStart', `"${path.basename(process.execPath)}"`,
  445. '--process-start-args', `"--hidden"`
  446. ]
  447. before(function () {
  448. if (process.platform === 'linux' || process.mas) this.skip()
  449. })
  450. beforeEach(() => {
  451. app.setLoginItemSettings({ openAtLogin: false })
  452. app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs })
  453. })
  454. afterEach(() => {
  455. app.setLoginItemSettings({ openAtLogin: false })
  456. app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs })
  457. })
  458. it('sets and returns the app as a login item', done => {
  459. app.setLoginItemSettings({ openAtLogin: true })
  460. expect(app.getLoginItemSettings()).to.deep.equal({
  461. openAtLogin: true,
  462. openAsHidden: false,
  463. wasOpenedAtLogin: false,
  464. wasOpenedAsHidden: false,
  465. restoreState: false
  466. })
  467. done()
  468. })
  469. it('adds a login item that loads in hidden mode', done => {
  470. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true })
  471. expect(app.getLoginItemSettings()).to.deep.equal({
  472. openAtLogin: true,
  473. openAsHidden: process.platform === 'darwin' && !process.mas, // Only available on macOS
  474. wasOpenedAtLogin: false,
  475. wasOpenedAsHidden: false,
  476. restoreState: false
  477. })
  478. done()
  479. })
  480. it('correctly sets and unsets the LoginItem', function () {
  481. expect(app.getLoginItemSettings().openAtLogin).to.be.false()
  482. app.setLoginItemSettings({ openAtLogin: true })
  483. expect(app.getLoginItemSettings().openAtLogin).to.be.true()
  484. app.setLoginItemSettings({ openAtLogin: false })
  485. expect(app.getLoginItemSettings().openAtLogin).to.be.false()
  486. })
  487. it('correctly sets and unsets the LoginItem as hidden', function () {
  488. if (process.platform !== 'darwin') this.skip()
  489. expect(app.getLoginItemSettings().openAtLogin).to.be.false()
  490. expect(app.getLoginItemSettings().openAsHidden).to.be.false()
  491. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true })
  492. expect(app.getLoginItemSettings().openAtLogin).to.be.true()
  493. expect(app.getLoginItemSettings().openAsHidden).to.be.true()
  494. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: false })
  495. expect(app.getLoginItemSettings().openAtLogin).to.be.true()
  496. expect(app.getLoginItemSettings().openAsHidden).to.be.false()
  497. })
  498. it('allows you to pass a custom executable and arguments', function () {
  499. if (process.platform !== 'win32') this.skip()
  500. app.setLoginItemSettings({ openAtLogin: true, path: updateExe, args: processStartArgs })
  501. expect(app.getLoginItemSettings().openAtLogin).to.be.false()
  502. expect(app.getLoginItemSettings({
  503. path: updateExe,
  504. args: processStartArgs
  505. }).openAtLogin).to.be.true()
  506. })
  507. })
  508. describe('isAccessibilitySupportEnabled API', () => {
  509. it('returns whether the Chrome has accessibility APIs enabled', () => {
  510. expect(app.isAccessibilitySupportEnabled()).to.be.a('boolean')
  511. })
  512. })
  513. describe('getAppPath', () => {
  514. it('works for directories with package.json', async () => {
  515. const { appPath } = await runTestApp('app-path')
  516. expect(appPath).to.equal(path.resolve(__dirname, 'fixtures/api/app-path'))
  517. })
  518. it('works for directories with index.js', async () => {
  519. const { appPath } = await runTestApp('app-path/lib')
  520. expect(appPath).to.equal(path.resolve(__dirname, 'fixtures/api/app-path/lib'))
  521. })
  522. it('works for files without extension', async () => {
  523. const { appPath } = await runTestApp('app-path/lib/index')
  524. expect(appPath).to.equal(path.resolve(__dirname, 'fixtures/api/app-path/lib'))
  525. })
  526. it('works for files', async () => {
  527. const { appPath } = await runTestApp('app-path/lib/index.js')
  528. expect(appPath).to.equal(path.resolve(__dirname, 'fixtures/api/app-path/lib'))
  529. })
  530. })
  531. describe('getPath(name)', () => {
  532. it('returns paths that exist', () => {
  533. const paths = [
  534. fs.existsSync(app.getPath('exe')),
  535. fs.existsSync(app.getPath('home')),
  536. fs.existsSync(app.getPath('temp'))
  537. ]
  538. expect(paths).to.deep.equal([true, true, true])
  539. })
  540. it('throws an error when the name is invalid', () => {
  541. expect(() => {
  542. app.getPath('does-not-exist')
  543. }).to.throw(/Failed to get 'does-not-exist' path/)
  544. })
  545. it('returns the overridden path', () => {
  546. app.setPath('music', __dirname)
  547. expect(app.getPath('music')).to.equal(__dirname)
  548. })
  549. })
  550. describe('select-client-certificate event', () => {
  551. let w = null
  552. before(function () {
  553. if (process.platform === 'linux') {
  554. this.skip()
  555. }
  556. })
  557. beforeEach(() => {
  558. w = new BrowserWindow({
  559. show: false,
  560. webPreferences: {
  561. nodeIntegration: true,
  562. partition: 'empty-certificate'
  563. }
  564. })
  565. })
  566. afterEach(() => closeWindow(w).then(() => { w = null }))
  567. it('can respond with empty certificate list', done => {
  568. w.webContents.on('did-finish-load', () => {
  569. expect(w.webContents.getTitle()).to.equal('denied')
  570. done()
  571. })
  572. ipcRenderer.sendSync('set-client-certificate-option', true)
  573. w.webContents.loadURL(secureUrl)
  574. })
  575. })
  576. describe('setAsDefaultProtocolClient(protocol, path, args)', () => {
  577. const protocol = 'electron-test'
  578. const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe')
  579. const processStartArgs = [
  580. '--processStart', `"${path.basename(process.execPath)}"`,
  581. '--process-start-args', `"--hidden"`
  582. ]
  583. let Winreg
  584. let classesKey
  585. before(function () {
  586. if (process.platform !== 'win32') {
  587. this.skip()
  588. } else {
  589. Winreg = require('winreg')
  590. classesKey = new Winreg({
  591. hive: Winreg.HKCU,
  592. key: '\\Software\\Classes\\'
  593. })
  594. }
  595. })
  596. after(function (done) {
  597. if (process.platform !== 'win32') {
  598. done()
  599. } else {
  600. const protocolKey = new Winreg({
  601. hive: Winreg.HKCU,
  602. key: `\\Software\\Classes\\${protocol}`
  603. })
  604. // The last test leaves the registry dirty,
  605. // delete the protocol key for those of us who test at home
  606. protocolKey.destroy(() => done())
  607. }
  608. })
  609. beforeEach(() => {
  610. app.removeAsDefaultProtocolClient(protocol)
  611. app.removeAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  612. })
  613. afterEach(() => {
  614. app.removeAsDefaultProtocolClient(protocol)
  615. expect(app.isDefaultProtocolClient(protocol)).to.be.false()
  616. app.removeAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  617. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.be.false()
  618. })
  619. it('sets the app as the default protocol client', () => {
  620. expect(app.isDefaultProtocolClient(protocol)).to.be.false()
  621. app.setAsDefaultProtocolClient(protocol)
  622. expect(app.isDefaultProtocolClient(protocol)).to.be.true()
  623. })
  624. it('allows a custom path and args to be specified', () => {
  625. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.be.false()
  626. app.setAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  627. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.be.true()
  628. expect(app.isDefaultProtocolClient(protocol)).to.be.false()
  629. })
  630. it('creates a registry entry for the protocol class', (done) => {
  631. app.setAsDefaultProtocolClient(protocol)
  632. classesKey.keys((error, keys) => {
  633. if (error) throw error
  634. const exists = !!keys.find(key => key.key.includes(protocol))
  635. expect(exists).to.be.true()
  636. done()
  637. })
  638. })
  639. it('completely removes a registry entry for the protocol class', (done) => {
  640. app.setAsDefaultProtocolClient(protocol)
  641. app.removeAsDefaultProtocolClient(protocol)
  642. classesKey.keys((error, keys) => {
  643. if (error) throw error
  644. const exists = !!keys.find(key => key.key.includes(protocol))
  645. expect(exists).to.be.false()
  646. done()
  647. })
  648. })
  649. it('only unsets a class registry key if it contains other data', (done) => {
  650. app.setAsDefaultProtocolClient(protocol)
  651. const protocolKey = new Winreg({
  652. hive: Winreg.HKCU,
  653. key: `\\Software\\Classes\\${protocol}`
  654. })
  655. protocolKey.set('test-value', 'REG_BINARY', '123', () => {
  656. app.removeAsDefaultProtocolClient(protocol)
  657. classesKey.keys((error, keys) => {
  658. if (error) throw error
  659. const exists = !!keys.find(key => key.key.includes(protocol))
  660. expect(exists).to.be.true()
  661. done()
  662. })
  663. })
  664. })
  665. })
  666. describe('app launch through uri', () => {
  667. before(function () {
  668. if (process.platform !== 'win32') {
  669. this.skip()
  670. }
  671. })
  672. it('does not launch for argument following a URL', done => {
  673. const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
  674. // App should exit with non 123 code.
  675. const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'electron-test:?', 'abc'])
  676. first.once('exit', code => {
  677. expect(code).to.not.equal(123)
  678. done()
  679. })
  680. })
  681. it('launches successfully for argument following a file path', done => {
  682. const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
  683. // App should exit with code 123.
  684. const first = ChildProcess.spawn(remote.process.execPath, [appPath, 'e:\\abc', 'abc'])
  685. first.once('exit', code => {
  686. expect(code).to.equal(123)
  687. done()
  688. })
  689. })
  690. it('launches successfully for multiple URIs following --', done => {
  691. const appPath = path.join(__dirname, 'fixtures', 'api', 'quit-app')
  692. // App should exit with code 123.
  693. const first = ChildProcess.spawn(remote.process.execPath, [appPath, '--', 'http://electronjs.org', 'electron-test://testdata'])
  694. first.once('exit', code => {
  695. expect(code).to.equal(123)
  696. done()
  697. })
  698. })
  699. })
  700. describe('getFileIcon() API', () => {
  701. const iconPath = path.join(__dirname, 'fixtures/assets/icon.ico')
  702. const sizes = {
  703. small: 16,
  704. normal: 32,
  705. large: process.platform === 'win32' ? 32 : 48
  706. }
  707. // (alexeykuzmin): `.skip()` called in `before`
  708. // doesn't affect nested `describe`s.
  709. beforeEach(function () {
  710. // FIXME Get these specs running on Linux CI
  711. if (process.platform === 'linux' && isCI) {
  712. this.skip()
  713. }
  714. })
  715. it('fetches a non-empty icon', async () => {
  716. const icon = await app.getFileIcon(iconPath)
  717. expect(icon.isEmpty()).to.be.false()
  718. })
  719. // TODO(codebytere): remove when promisification is complete
  720. it('fetches a non-empty icon (callback)', (done) => {
  721. app.getFileIcon(iconPath, (icon) => {
  722. expect(icon.isEmpty()).to.be.false()
  723. done()
  724. })
  725. })
  726. it('fetches normal icon size by default', async () => {
  727. const icon = await app.getFileIcon(iconPath)
  728. const size = icon.getSize()
  729. expect(size.height).to.equal(sizes.normal)
  730. expect(size.width).to.equal(sizes.normal)
  731. })
  732. // TODO(codebytere): remove when promisification is complete
  733. it('fetches normal icon size by default (callback)', (done) => {
  734. app.getFileIcon(iconPath, (icon) => {
  735. const size = icon.getSize()
  736. expect(size.height).to.equal(sizes.normal)
  737. expect(size.width).to.equal(sizes.normal)
  738. done()
  739. })
  740. })
  741. describe('size option', () => {
  742. it('fetches a small icon', async () => {
  743. const icon = await app.getFileIcon(iconPath, { size: 'small' })
  744. const size = icon.getSize()
  745. expect(size.height).to.equal(sizes.small)
  746. expect(size.width).to.equal(sizes.small)
  747. })
  748. it('fetches a normal icon', async () => {
  749. const icon = await app.getFileIcon(iconPath, { size: 'normal' })
  750. const size = icon.getSize()
  751. expect(size.height).to.equal(sizes.normal)
  752. expect(size.width).to.equal(sizes.normal)
  753. })
  754. // TODO(codebytere): remove when promisification is complete
  755. it('fetches a normal icon (callback)', (done) => {
  756. app.getFileIcon(iconPath, { size: 'normal' }, (icon) => {
  757. const size = icon.getSize()
  758. expect(size.height).to.equal(sizes.normal)
  759. expect(size.width).to.equal(sizes.normal)
  760. done()
  761. })
  762. })
  763. it('fetches a large icon', async () => {
  764. // macOS does not support large icons
  765. if (process.platform === 'darwin') return
  766. const icon = await app.getFileIcon(iconPath, { size: 'large' })
  767. const size = icon.getSize()
  768. expect(size.height).to.equal(sizes.large)
  769. expect(size.width).to.equal(sizes.large)
  770. })
  771. })
  772. })
  773. describe('getAppMetrics() API', () => {
  774. it('returns memory and cpu stats of all running electron processes', () => {
  775. const appMetrics = app.getAppMetrics()
  776. expect(appMetrics).to.be.an('array').and.have.lengthOf.at.least(1, 'App memory info object is not > 0')
  777. const types = []
  778. for (const { pid, type, cpu } of appMetrics) {
  779. expect(pid).to.be.above(0, 'pid is not > 0')
  780. expect(type).to.be.a('string').that.is.not.empty()
  781. types.push(type)
  782. expect(cpu).to.have.own.property('percentCPUUsage').that.is.a('number')
  783. expect(cpu).to.have.own.property('idleWakeupsPerSecond').that.is.a('number')
  784. }
  785. if (process.platform === 'darwin') {
  786. expect(types).to.include('GPU')
  787. }
  788. expect(types).to.include('Browser')
  789. expect(types).to.include('Tab')
  790. })
  791. })
  792. describe('getGPUFeatureStatus() API', () => {
  793. it('returns the graphic features statuses', () => {
  794. const features = app.getGPUFeatureStatus()
  795. expect(features).to.have.own.property('webgl').that.is.a('string')
  796. expect(features).to.have.own.property('gpu_compositing').that.is.a('string')
  797. })
  798. })
  799. describe('getGPUInfo() API', () => {
  800. const appPath = path.join(__dirname, 'fixtures', 'api', 'gpu-info.js')
  801. const getGPUInfo = async (type) => {
  802. const appProcess = ChildProcess.spawn(remote.process.execPath, [appPath, type])
  803. let gpuInfoData = ''
  804. let errorData = ''
  805. appProcess.stdout.on('data', (data) => {
  806. gpuInfoData += data
  807. })
  808. appProcess.stderr.on('data', (data) => {
  809. errorData += data
  810. })
  811. const [exitCode] = await emittedOnce(appProcess, 'exit')
  812. if (exitCode === 0) {
  813. // return info data on successful exit
  814. return JSON.parse(gpuInfoData)
  815. } else {
  816. // return error if not clean exit
  817. return Promise.reject(new Error(errorData))
  818. }
  819. }
  820. const verifyBasicGPUInfo = async (gpuInfo) => {
  821. // Devices information is always present in the available info.
  822. expect(gpuInfo).to.have.own.property('gpuDevice')
  823. .that.is.an('array')
  824. .and.is.not.empty()
  825. const device = gpuInfo.gpuDevice[0]
  826. expect(device).to.be.an('object')
  827. .and.to.have.property('deviceId')
  828. .that.is.a('number')
  829. .not.lessThan(0)
  830. }
  831. it('succeeds with basic GPUInfo', async () => {
  832. const gpuInfo = await getGPUInfo('basic')
  833. await verifyBasicGPUInfo(gpuInfo)
  834. })
  835. it('succeeds with complete GPUInfo', async () => {
  836. const completeInfo = await getGPUInfo('complete')
  837. if (process.platform === 'linux') {
  838. // For linux and macOS complete info is same as basic info
  839. await verifyBasicGPUInfo(completeInfo)
  840. const basicInfo = await getGPUInfo('basic')
  841. expect(completeInfo).to.deep.equal(basicInfo)
  842. } else {
  843. // Gl version is present in the complete info.
  844. expect(completeInfo).to.have.own.property('auxAttributes')
  845. .that.is.an('object')
  846. expect(completeInfo.auxAttributes).to.have.own.property('glVersion')
  847. .that.is.a('string')
  848. .and.not.empty()
  849. }
  850. })
  851. it('fails for invalid info_type', () => {
  852. const invalidType = 'invalid'
  853. const expectedErrorMessage = "Invalid info type. Use 'basic' or 'complete'"
  854. return expect(app.getGPUInfo(invalidType)).to.eventually.be.rejectedWith(expectedErrorMessage)
  855. })
  856. })
  857. describe('sandbox options', () => {
  858. let appProcess = null
  859. let server = null
  860. const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox'
  861. beforeEach(function (done) {
  862. if (process.platform === 'linux' && (process.arch === 'arm64' || process.arch === 'arm')) {
  863. // Our ARM tests are run on VSTS rather than CircleCI, and the Docker
  864. // setup on VSTS disallows syscalls that Chrome requires for setting up
  865. // sandboxing.
  866. // See:
  867. // - https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
  868. // - https://chromium.googlesource.com/chromium/src/+/70.0.3538.124/sandbox/linux/services/credentials.cc#292
  869. // - https://github.com/docker/docker-ce/blob/ba7dfc59ccfe97c79ee0d1379894b35417b40bca/components/engine/profiles/seccomp/seccomp_default.go#L497
  870. // - https://blog.jessfraz.com/post/how-to-use-new-docker-seccomp-profiles/
  871. //
  872. // Adding `--cap-add SYS_ADMIN` or `--security-opt seccomp=unconfined`
  873. // to the Docker invocation allows the syscalls that Chrome needs, but
  874. // are probably more permissive than we'd like.
  875. this.skip()
  876. }
  877. fs.unlink(socketPath, () => {
  878. server = net.createServer()
  879. server.listen(socketPath)
  880. done()
  881. })
  882. })
  883. afterEach(done => {
  884. if (appProcess != null) appProcess.kill()
  885. server.close(() => {
  886. if (process.platform === 'win32') {
  887. done()
  888. } else {
  889. fs.unlink(socketPath, () => done())
  890. }
  891. })
  892. })
  893. describe('when app.enableSandbox() is called', () => {
  894. it('adds --enable-sandbox to all renderer processes', done => {
  895. const appPath = path.join(__dirname, 'fixtures', 'api', 'mixed-sandbox-app')
  896. appProcess = ChildProcess.spawn(remote.process.execPath, [appPath, '--app-enable-sandbox'])
  897. server.once('error', error => { done(error) })
  898. server.on('connection', client => {
  899. client.once('data', data => {
  900. const argv = JSON.parse(data)
  901. expect(argv.sandbox).to.include('--enable-sandbox')
  902. expect(argv.sandbox).to.not.include('--no-sandbox')
  903. expect(argv.noSandbox).to.include('--enable-sandbox')
  904. expect(argv.noSandbox).to.not.include('--no-sandbox')
  905. expect(argv.noSandboxDevtools).to.be.true()
  906. expect(argv.sandboxDevtools).to.be.true()
  907. done()
  908. })
  909. })
  910. })
  911. })
  912. describe('when the app is launched with --enable-sandbox', () => {
  913. it('adds --enable-sandbox to all renderer processes', done => {
  914. const appPath = path.join(__dirname, 'fixtures', 'api', 'mixed-sandbox-app')
  915. appProcess = ChildProcess.spawn(remote.process.execPath, [appPath, '--enable-sandbox'])
  916. server.once('error', error => { done(error) })
  917. server.on('connection', client => {
  918. client.once('data', data => {
  919. const argv = JSON.parse(data)
  920. expect(argv.sandbox).to.include('--enable-sandbox')
  921. expect(argv.sandbox).to.not.include('--no-sandbox')
  922. expect(argv.noSandbox).to.include('--enable-sandbox')
  923. expect(argv.noSandbox).to.not.include('--no-sandbox')
  924. expect(argv.noSandboxDevtools).to.be.true()
  925. expect(argv.sandboxDevtools).to.be.true()
  926. done()
  927. })
  928. })
  929. })
  930. })
  931. })
  932. describe('disableDomainBlockingFor3DAPIs() API', () => {
  933. it('throws when called after app is ready', () => {
  934. expect(() => {
  935. app.disableDomainBlockingFor3DAPIs()
  936. }).to.throw(/before app is ready/)
  937. })
  938. })
  939. describe('dock.setMenu', () => {
  940. before(function () {
  941. if (process.platform !== 'darwin') {
  942. this.skip()
  943. }
  944. })
  945. it('keeps references to the menu', () => {
  946. app.dock.setMenu(new Menu())
  947. const v8Util = process.atomBinding('v8_util')
  948. v8Util.requestGarbageCollectionForTesting()
  949. })
  950. })
  951. describe('whenReady', () => {
  952. it('returns a Promise', () => {
  953. expect(app.whenReady()).to.be.a('promise')
  954. })
  955. it('becomes fulfilled if the app is already ready', () => {
  956. expect(app.isReady()).to.be.true()
  957. return expect(app.whenReady()).to.be.eventually.fulfilled
  958. })
  959. })
  960. describe('commandLine.hasSwitch', () => {
  961. it('returns true when present', () => {
  962. app.commandLine.appendSwitch('foobar1')
  963. expect(app.commandLine.hasSwitch('foobar1')).to.be.true()
  964. })
  965. it('returns false when not present', () => {
  966. expect(app.commandLine.hasSwitch('foobar2')).to.be.false()
  967. })
  968. })
  969. describe('commandLine.hasSwitch (existing argv)', () => {
  970. it('returns true when present', async () => {
  971. const { hasSwitch } = await runTestApp('command-line', '--foobar')
  972. expect(hasSwitch).to.be.true()
  973. })
  974. it('returns false when not present', async () => {
  975. const { hasSwitch } = await runTestApp('command-line')
  976. expect(hasSwitch).to.be.false()
  977. })
  978. })
  979. describe('commandLine.getSwitchValue', () => {
  980. it('returns the value when present', () => {
  981. app.commandLine.appendSwitch('foobar', 'æøåü')
  982. expect(app.commandLine.getSwitchValue('foobar')).to.equal('æøåü')
  983. })
  984. it('returns an empty string when present without value', () => {
  985. app.commandLine.appendSwitch('foobar1')
  986. expect(app.commandLine.getSwitchValue('foobar1')).to.equal('')
  987. })
  988. it('returns an empty string when not present', () => {
  989. expect(app.commandLine.getSwitchValue('foobar2')).to.equal('')
  990. })
  991. })
  992. describe('commandLine.getSwitchValue (existing argv)', () => {
  993. it('returns the value when present', async () => {
  994. const { getSwitchValue } = await runTestApp('command-line', '--foobar=test')
  995. expect(getSwitchValue).to.equal('test')
  996. })
  997. it('returns an empty string when present without value', async () => {
  998. const { getSwitchValue } = await runTestApp('command-line', '--foobar')
  999. expect(getSwitchValue).to.equal('')
  1000. })
  1001. it('returns an empty string when not present', async () => {
  1002. const { getSwitchValue } = await runTestApp('command-line')
  1003. expect(getSwitchValue).to.equal('')
  1004. })
  1005. })
  1006. })
  1007. describe('default behavior', () => {
  1008. describe('application menu', () => {
  1009. it('creates the default menu if the app does not set it', async () => {
  1010. const result = await runTestApp('default-menu')
  1011. expect(result).to.equal(false)
  1012. })
  1013. it('does not create the default menu if the app sets a custom menu', async () => {
  1014. const result = await runTestApp('default-menu', '--custom-menu')
  1015. expect(result).to.equal(true)
  1016. })
  1017. it('does not create the default menu if the app sets a null menu', async () => {
  1018. const result = await runTestApp('default-menu', '--null-menu')
  1019. expect(result).to.equal(true)
  1020. })
  1021. })
  1022. describe('window-all-closed', () => {
  1023. it('quits when the app does not handle the event', async () => {
  1024. const result = await runTestApp('window-all-closed')
  1025. expect(result).to.equal(false)
  1026. })
  1027. it('does not quit when the app handles the event', async () => {
  1028. const result = await runTestApp('window-all-closed', '--handle-event')
  1029. expect(result).to.equal(true)
  1030. })
  1031. })
  1032. })
  1033. async function runTestApp (name, ...args) {
  1034. const appPath = path.join(__dirname, 'fixtures', 'api', name)
  1035. const electronPath = remote.getGlobal('process').execPath
  1036. const appProcess = cp.spawn(electronPath, [appPath, ...args])
  1037. let output = ''
  1038. appProcess.stdout.on('data', (data) => { output += data })
  1039. await emittedOnce(appProcess.stdout, 'end')
  1040. return JSON.parse(output)
  1041. }