api-app-spec.ts 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574
  1. import * as chai from 'chai'
  2. import * as chaiAsPromised from 'chai-as-promised'
  3. import * as cp from 'child_process'
  4. import * as https from 'https'
  5. import * as http from 'http'
  6. import * as net from 'net'
  7. import * as fs from 'fs'
  8. import * as path from 'path'
  9. import { homedir } from 'os'
  10. import split = require('split')
  11. import { app, BrowserWindow, Menu, session } from 'electron'
  12. import { emittedOnce } from './events-helpers'
  13. import { closeWindow, closeAllWindows } from './window-helpers'
  14. import { ifdescribe, ifit } from './spec-helpers'
  15. const features = process.electronBinding('features')
  16. const { expect } = chai
  17. chai.use(chaiAsPromised)
  18. const fixturesPath = path.resolve(__dirname, '../spec/fixtures')
  19. describe('electron module', () => {
  20. it('does not expose internal modules to require', () => {
  21. expect(() => {
  22. require('clipboard')
  23. }).to.throw(/Cannot find module 'clipboard'/)
  24. })
  25. describe('require("electron")', () => {
  26. it('always returns the internal electron module', () => {
  27. require('electron')
  28. })
  29. })
  30. })
  31. describe('app module', () => {
  32. let server: https.Server
  33. let secureUrl: string
  34. const certPath = path.join(fixturesPath, 'certificates')
  35. before((done) => {
  36. const options = {
  37. key: fs.readFileSync(path.join(certPath, 'server.key')),
  38. cert: fs.readFileSync(path.join(certPath, 'server.pem')),
  39. ca: [
  40. fs.readFileSync(path.join(certPath, 'rootCA.pem')),
  41. fs.readFileSync(path.join(certPath, 'intermediateCA.pem'))
  42. ],
  43. requestCert: true,
  44. rejectUnauthorized: false
  45. }
  46. server = https.createServer(options, (req, res) => {
  47. if ((req as any).client.authorized) {
  48. res.writeHead(200)
  49. res.end('<title>authorized</title>')
  50. } else {
  51. res.writeHead(401)
  52. res.end('<title>denied</title>')
  53. }
  54. })
  55. server.listen(0, '127.0.0.1', () => {
  56. const port = (server.address() as net.AddressInfo).port
  57. secureUrl = `https://127.0.0.1:${port}`
  58. done()
  59. })
  60. })
  61. after(done => {
  62. server.close(() => done())
  63. })
  64. describe('app.getVersion()', () => {
  65. it('returns the version field of package.json', () => {
  66. expect(app.getVersion()).to.equal('0.1.0')
  67. })
  68. })
  69. describe('app.setVersion(version)', () => {
  70. it('overrides the version', () => {
  71. expect(app.getVersion()).to.equal('0.1.0')
  72. app.setVersion('test-version')
  73. expect(app.getVersion()).to.equal('test-version')
  74. app.setVersion('0.1.0')
  75. })
  76. })
  77. describe('app name APIs', () => {
  78. it('with properties', () => {
  79. it('returns the name field of package.json', () => {
  80. expect(app.name).to.equal('Electron Test Main')
  81. })
  82. it('overrides the name', () => {
  83. expect(app.name).to.equal('Electron Test Main')
  84. app.name = 'test-name'
  85. expect(app.name).to.equal('test-name')
  86. app.name = 'Electron Test Main'
  87. })
  88. })
  89. it('with functions', () => {
  90. it('returns the name field of package.json', () => {
  91. expect(app.getName()).to.equal('Electron Test Main')
  92. })
  93. it('overrides the name', () => {
  94. expect(app.getName()).to.equal('Electron Test Main')
  95. app.setName('test-name')
  96. expect(app.getName()).to.equal('test-name')
  97. app.setName('Electron Test Main')
  98. })
  99. })
  100. })
  101. describe('app.getLocale()', () => {
  102. it('should not be empty', () => {
  103. expect(app.getLocale()).to.not.equal('')
  104. })
  105. })
  106. describe('app.getLocaleCountryCode()', () => {
  107. it('should be empty or have length of two', () => {
  108. let expectedLength = 2
  109. if (isCI && process.platform === 'linux') {
  110. // Linux CI machines have no locale.
  111. expectedLength = 0
  112. }
  113. expect(app.getLocaleCountryCode()).to.be.a('string').and.have.lengthOf(expectedLength)
  114. })
  115. })
  116. describe('app.isPackaged', () => {
  117. it('should be false durings tests', () => {
  118. expect(app.isPackaged).to.equal(false)
  119. })
  120. })
  121. describe('app.isInApplicationsFolder()', () => {
  122. before(function () {
  123. if (process.platform !== 'darwin') {
  124. this.skip()
  125. }
  126. })
  127. it('should be false during tests', () => {
  128. expect(app.isInApplicationsFolder()).to.equal(false)
  129. })
  130. })
  131. describe('app.exit(exitCode)', () => {
  132. let appProcess: cp.ChildProcess | null = null
  133. afterEach(() => {
  134. if (appProcess) appProcess.kill()
  135. })
  136. it('emits a process exit event with the code', async () => {
  137. const appPath = path.join(fixturesPath, 'api', 'quit-app')
  138. const electronPath = process.execPath
  139. let output = ''
  140. appProcess = cp.spawn(electronPath, [appPath])
  141. if (appProcess && appProcess.stdout) {
  142. appProcess.stdout.on('data', data => { output += data })
  143. }
  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(fixturesPath, 'api', 'exit-closes-all-windows-app')
  152. const electronPath = process.execPath
  153. appProcess = cp.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 = process.execPath
  164. const appPath = path.join(fixturesPath, 'api', 'singleton')
  165. appProcess = cp.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. if (appProcess && appProcess.stdout) {
  169. appProcess.stdout.on('data', data => appProcess!.kill())
  170. }
  171. const [code, signal] = await emittedOnce(appProcess, 'close')
  172. const message = `code:\n${code}\nsignal:\n${signal}`
  173. expect(code).to.equal(0, message)
  174. expect(signal).to.equal(null, message)
  175. })
  176. })
  177. describe('app.requestSingleInstanceLock', () => {
  178. it('prevents the second launch of app', function (done) {
  179. this.timeout(120000)
  180. const appPath = path.join(fixturesPath, 'api', 'singleton')
  181. const first = cp.spawn(process.execPath, [appPath])
  182. first.once('exit', code => {
  183. expect(code).to.equal(0)
  184. })
  185. // Start second app when received output.
  186. first.stdout.once('data', () => {
  187. const second = cp.spawn(process.execPath, [appPath])
  188. second.once('exit', code => {
  189. expect(code).to.equal(1)
  190. done()
  191. })
  192. })
  193. })
  194. it('passes arguments to the second-instance event', async () => {
  195. const appPath = path.join(fixturesPath, 'api', 'singleton')
  196. const first = cp.spawn(process.execPath, [appPath])
  197. const firstExited = emittedOnce(first, 'exit')
  198. // Wait for the first app to boot.
  199. const firstStdoutLines = first.stdout.pipe(split())
  200. while ((await emittedOnce(firstStdoutLines, 'data')).toString() !== 'started') {
  201. // wait.
  202. }
  203. const data2Promise = emittedOnce(firstStdoutLines, 'data')
  204. const secondInstanceArgs = [process.execPath, appPath, '--some-switch', 'some-arg']
  205. const second = cp.spawn(secondInstanceArgs[0], secondInstanceArgs.slice(1))
  206. const [code2] = await emittedOnce(second, 'exit')
  207. expect(code2).to.equal(1)
  208. const [code1] = await firstExited
  209. expect(code1).to.equal(0)
  210. const data2 = (await data2Promise)[0].toString('ascii')
  211. const secondInstanceArgsReceived: string[] = JSON.parse(data2.toString('ascii'))
  212. const expected = process.platform === 'win32'
  213. ? [process.execPath, '--some-switch', '--allow-file-access-from-files', secondInstanceArgsReceived.find(x => x.includes('original-process-start-time')), appPath, 'some-arg']
  214. : secondInstanceArgs
  215. expect(secondInstanceArgsReceived).to.eql(expected,
  216. `expected ${JSON.stringify(expected)} but got ${data2.toString('ascii')}`)
  217. })
  218. })
  219. describe('app.relaunch', () => {
  220. let server: net.Server | null = null
  221. const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-app-relaunch' : '/tmp/electron-app-relaunch'
  222. beforeEach(done => {
  223. fs.unlink(socketPath, () => {
  224. server = net.createServer()
  225. server.listen(socketPath)
  226. done()
  227. })
  228. })
  229. afterEach((done) => {
  230. server!.close(() => {
  231. if (process.platform === 'win32') {
  232. done()
  233. } else {
  234. fs.unlink(socketPath, () => done())
  235. }
  236. })
  237. })
  238. it('relaunches the app', function (done) {
  239. this.timeout(120000)
  240. let state = 'none'
  241. server!.once('error', error => done(error))
  242. server!.on('connection', client => {
  243. client.once('data', data => {
  244. if (String(data) === 'false' && state === 'none') {
  245. state = 'first-launch'
  246. } else if (String(data) === 'true' && state === 'first-launch') {
  247. done()
  248. } else {
  249. done(`Unexpected state: ${state}`)
  250. }
  251. })
  252. })
  253. const appPath = path.join(fixturesPath, 'api', 'relaunch')
  254. cp.spawn(process.execPath, [appPath])
  255. })
  256. })
  257. describe('app.setUserActivity(type, userInfo)', () => {
  258. before(function () {
  259. if (process.platform !== 'darwin') {
  260. this.skip()
  261. }
  262. })
  263. it('sets the current activity', () => {
  264. app.setUserActivity('com.electron.testActivity', { testData: '123' })
  265. expect(app.getCurrentActivityType()).to.equal('com.electron.testActivity')
  266. })
  267. })
  268. describe('certificate-error event', () => {
  269. afterEach(closeAllWindows)
  270. it('is emitted when visiting a server with a self-signed cert', async () => {
  271. const w = new BrowserWindow({ show: false })
  272. w.loadURL(secureUrl)
  273. await emittedOnce(app, 'certificate-error')
  274. })
  275. })
  276. // xdescribe('app.importCertificate', () => {
  277. // let w = null
  278. // before(function () {
  279. // if (process.platform !== 'linux') {
  280. // this.skip()
  281. // }
  282. // })
  283. // afterEach(() => closeWindow(w).then(() => { w = null }))
  284. // it('can import certificate into platform cert store', done => {
  285. // const options = {
  286. // certificate: path.join(certPath, 'client.p12'),
  287. // password: 'electron'
  288. // }
  289. // w = new BrowserWindow({
  290. // show: false,
  291. // webPreferences: {
  292. // nodeIntegration: true
  293. // }
  294. // })
  295. // w.webContents.on('did-finish-load', () => {
  296. // expect(w.webContents.getTitle()).to.equal('authorized')
  297. // done()
  298. // })
  299. // ipcRenderer.once('select-client-certificate', (event, webContentsId, list) => {
  300. // expect(webContentsId).to.equal(w.webContents.id)
  301. // expect(list).to.have.lengthOf(1)
  302. // expect(list[0]).to.deep.equal({
  303. // issuerName: 'Intermediate CA',
  304. // subjectName: 'Client Cert',
  305. // issuer: { commonName: 'Intermediate CA' },
  306. // subject: { commonName: 'Client Cert' }
  307. // })
  308. // event.sender.send('client-certificate-response', list[0])
  309. // })
  310. // app.importCertificate(options, result => {
  311. // expect(result).toNotExist()
  312. // ipcRenderer.sendSync('set-client-certificate-option', false)
  313. // w.loadURL(secureUrl)
  314. // })
  315. // })
  316. // })
  317. describe('BrowserWindow events', () => {
  318. let w: BrowserWindow = null as any
  319. afterEach(() => closeWindow(w).then(() => { w = null as any }))
  320. it('should emit browser-window-focus event when window is focused', async () => {
  321. const emitted = emittedOnce(app, 'browser-window-focus')
  322. w = new BrowserWindow({ show: false })
  323. w.emit('focus')
  324. const [, window] = await emitted
  325. expect(window.id).to.equal(w.id)
  326. })
  327. it('should emit browser-window-blur event when window is blured', async () => {
  328. const emitted = emittedOnce(app, 'browser-window-blur')
  329. w = new BrowserWindow({ show: false })
  330. w.emit('blur')
  331. const [, window] = await emitted
  332. expect(window.id).to.equal(w.id)
  333. })
  334. it('should emit browser-window-created event when window is created', async () => {
  335. const emitted = emittedOnce(app, 'browser-window-created')
  336. w = new BrowserWindow({ show: false })
  337. const [, window] = await emitted
  338. expect(window.id).to.equal(w.id)
  339. })
  340. it('should emit web-contents-created event when a webContents is created', async () => {
  341. const emitted = emittedOnce(app, 'web-contents-created')
  342. w = new BrowserWindow({ show: false })
  343. const [, webContents] = await emitted
  344. expect(webContents.id).to.equal(w.webContents.id)
  345. })
  346. // FIXME: re-enable this test on win32.
  347. ifit(process.platform !== 'win32')('should emit renderer-process-crashed event when renderer crashes', async () => {
  348. w = new BrowserWindow({
  349. show: false,
  350. webPreferences: {
  351. nodeIntegration: true
  352. }
  353. })
  354. await w.loadURL('about:blank')
  355. const emitted = emittedOnce(app, 'renderer-process-crashed')
  356. w.webContents.executeJavaScript('process.crash()')
  357. const [, webContents] = await emitted
  358. expect(webContents).to.equal(w.webContents)
  359. })
  360. it('should emit render-process-gone event when renderer crashes', async function () {
  361. // FIXME: re-enable this test on win32.
  362. if (process.platform === 'win32') { return this.skip(); }
  363. w = new BrowserWindow({
  364. show: false,
  365. webPreferences: {
  366. nodeIntegration: true
  367. }
  368. });
  369. await w.loadURL('about:blank');
  370. const promise = emittedOnce(app, 'render-process-gone');
  371. w.webContents.executeJavaScript('process.crash()');
  372. const [, webContents, details] = await promise;
  373. expect(webContents).to.equal(w.webContents);
  374. expect(details.reason).to.be.oneOf(['crashed', 'abnormal-exit']);
  375. });
  376. ifdescribe(features.isDesktopCapturerEnabled())('desktopCapturer module filtering', () => {
  377. it('should emit desktop-capturer-get-sources event when desktopCapturer.getSources() is invoked', async () => {
  378. w = new BrowserWindow({
  379. show: false,
  380. webPreferences: {
  381. nodeIntegration: true
  382. }
  383. })
  384. await w.loadURL('about:blank')
  385. const promise = emittedOnce(app, 'desktop-capturer-get-sources')
  386. w.webContents.executeJavaScript(`require('electron').desktopCapturer.getSources({ types: ['screen'] })`)
  387. const [, webContents] = await promise
  388. expect(webContents).to.equal(w.webContents)
  389. })
  390. })
  391. ifdescribe(features.isRemoteModuleEnabled())('remote module filtering', () => {
  392. it('should emit remote-require event when remote.require() is invoked', async () => {
  393. w = new BrowserWindow({
  394. show: false,
  395. webPreferences: {
  396. nodeIntegration: true
  397. }
  398. })
  399. await w.loadURL('about:blank')
  400. const promise = emittedOnce(app, 'remote-require')
  401. w.webContents.executeJavaScript(`require('electron').remote.require('test')`)
  402. const [, webContents, moduleName] = await promise
  403. expect(webContents).to.equal(w.webContents)
  404. expect(moduleName).to.equal('test')
  405. })
  406. it('should emit remote-get-global event when remote.getGlobal() is invoked', async () => {
  407. w = new BrowserWindow({
  408. show: false,
  409. webPreferences: {
  410. nodeIntegration: true
  411. }
  412. })
  413. await w.loadURL('about:blank')
  414. const promise = emittedOnce(app, 'remote-get-global')
  415. w.webContents.executeJavaScript(`require('electron').remote.getGlobal('test')`)
  416. const [, webContents, globalName] = await promise
  417. expect(webContents).to.equal(w.webContents)
  418. expect(globalName).to.equal('test')
  419. })
  420. it('should emit remote-get-builtin event when remote.getBuiltin() is invoked', async () => {
  421. w = new BrowserWindow({
  422. show: false,
  423. webPreferences: {
  424. nodeIntegration: true
  425. }
  426. })
  427. await w.loadURL('about:blank')
  428. const promise = emittedOnce(app, 'remote-get-builtin')
  429. w.webContents.executeJavaScript(`require('electron').remote.app`)
  430. const [, webContents, moduleName] = await promise
  431. expect(webContents).to.equal(w.webContents)
  432. expect(moduleName).to.equal('app')
  433. })
  434. it('should emit remote-get-current-window event when remote.getCurrentWindow() is invoked', async () => {
  435. w = new BrowserWindow({
  436. show: false,
  437. webPreferences: {
  438. nodeIntegration: true
  439. }
  440. })
  441. await w.loadURL('about:blank')
  442. const promise = emittedOnce(app, 'remote-get-current-window')
  443. w.webContents.executeJavaScript(`{ require('electron').remote.getCurrentWindow() }`)
  444. const [, webContents] = await promise
  445. expect(webContents).to.equal(w.webContents)
  446. })
  447. it('should emit remote-get-current-web-contents event when remote.getCurrentWebContents() is invoked', async () => {
  448. w = new BrowserWindow({
  449. show: false,
  450. webPreferences: {
  451. nodeIntegration: true
  452. }
  453. })
  454. await w.loadURL('about:blank')
  455. const promise = emittedOnce(app, 'remote-get-current-web-contents')
  456. w.webContents.executeJavaScript(`{ require('electron').remote.getCurrentWebContents() }`)
  457. const [, webContents] = await promise
  458. expect(webContents).to.equal(w.webContents)
  459. })
  460. })
  461. })
  462. describe('app.badgeCount', () => {
  463. const platformIsNotSupported =
  464. (process.platform === 'win32') ||
  465. (process.platform === 'linux' && !app.isUnityRunning())
  466. const platformIsSupported = !platformIsNotSupported
  467. const expectedBadgeCount = 42
  468. after(() => { app.badgeCount = 0 })
  469. describe('on supported platform', () => {
  470. it('with properties', () => {
  471. it('sets a badge count', function () {
  472. if (platformIsNotSupported) return this.skip()
  473. app.badgeCount = expectedBadgeCount
  474. expect(app.badgeCount).to.equal(expectedBadgeCount)
  475. })
  476. })
  477. it('with functions', () => {
  478. it('sets a badge count', function () {
  479. if (platformIsNotSupported) return this.skip()
  480. app.setBadgeCount(expectedBadgeCount)
  481. expect(app.getBadgeCount()).to.equal(expectedBadgeCount)
  482. })
  483. })
  484. })
  485. describe('on unsupported platform', () => {
  486. it('with properties', () => {
  487. it('does not set a badge count', function () {
  488. if (platformIsSupported) return this.skip()
  489. app.badgeCount = 9999
  490. expect(app.badgeCount).to.equal(0)
  491. })
  492. })
  493. it('with functions', () => {
  494. it('does not set a badge count)', function () {
  495. if (platformIsSupported) return this.skip()
  496. app.setBadgeCount(9999)
  497. expect(app.getBadgeCount()).to.equal(0)
  498. })
  499. })
  500. })
  501. })
  502. describe('app.get/setLoginItemSettings API', function () {
  503. const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe')
  504. const processStartArgs = [
  505. '--processStart', `"${path.basename(process.execPath)}"`,
  506. '--process-start-args', `"--hidden"`
  507. ]
  508. before(function () {
  509. if (process.platform === 'linux' || process.mas) this.skip()
  510. })
  511. beforeEach(() => {
  512. app.setLoginItemSettings({ openAtLogin: false })
  513. app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs })
  514. })
  515. afterEach(() => {
  516. app.setLoginItemSettings({ openAtLogin: false })
  517. app.setLoginItemSettings({ openAtLogin: false, path: updateExe, args: processStartArgs })
  518. })
  519. it('sets and returns the app as a login item', done => {
  520. app.setLoginItemSettings({ openAtLogin: true })
  521. expect(app.getLoginItemSettings()).to.deep.equal({
  522. openAtLogin: true,
  523. openAsHidden: false,
  524. wasOpenedAtLogin: false,
  525. wasOpenedAsHidden: false,
  526. restoreState: false
  527. })
  528. done()
  529. })
  530. it('adds a login item that loads in hidden mode', done => {
  531. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true })
  532. expect(app.getLoginItemSettings()).to.deep.equal({
  533. openAtLogin: true,
  534. openAsHidden: process.platform === 'darwin' && !process.mas, // Only available on macOS
  535. wasOpenedAtLogin: false,
  536. wasOpenedAsHidden: false,
  537. restoreState: false
  538. })
  539. done()
  540. })
  541. it('correctly sets and unsets the LoginItem', function () {
  542. expect(app.getLoginItemSettings().openAtLogin).to.equal(false)
  543. app.setLoginItemSettings({ openAtLogin: true })
  544. expect(app.getLoginItemSettings().openAtLogin).to.equal(true)
  545. app.setLoginItemSettings({ openAtLogin: false })
  546. expect(app.getLoginItemSettings().openAtLogin).to.equal(false)
  547. })
  548. it('correctly sets and unsets the LoginItem as hidden', function () {
  549. if (process.platform !== 'darwin') this.skip()
  550. expect(app.getLoginItemSettings().openAtLogin).to.equal(false)
  551. expect(app.getLoginItemSettings().openAsHidden).to.equal(false)
  552. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true })
  553. expect(app.getLoginItemSettings().openAtLogin).to.equal(true)
  554. expect(app.getLoginItemSettings().openAsHidden).to.equal(true)
  555. app.setLoginItemSettings({ openAtLogin: true, openAsHidden: false })
  556. expect(app.getLoginItemSettings().openAtLogin).to.equal(true)
  557. expect(app.getLoginItemSettings().openAsHidden).to.equal(false)
  558. })
  559. it('allows you to pass a custom executable and arguments', function () {
  560. if (process.platform !== 'win32') this.skip()
  561. app.setLoginItemSettings({ openAtLogin: true, path: updateExe, args: processStartArgs })
  562. expect(app.getLoginItemSettings().openAtLogin).to.equal(false)
  563. expect(app.getLoginItemSettings({
  564. path: updateExe,
  565. args: processStartArgs
  566. }).openAtLogin).to.equal(true)
  567. })
  568. })
  569. ifdescribe(process.platform !== 'linux')('accessibilitySupportEnabled property', () => {
  570. it('with properties', () => {
  571. it('can set accessibility support enabled', () => {
  572. expect(app.accessibilitySupportEnabled).to.eql(false)
  573. app.accessibilitySupportEnabled = true
  574. expect(app.accessibilitySupportEnabled).to.eql(true)
  575. })
  576. })
  577. it('with functions', () => {
  578. it('can set accessibility support enabled', () => {
  579. expect(app.isAccessibilitySupportEnabled()).to.eql(false)
  580. app.setAccessibilitySupportEnabled(true)
  581. expect(app.isAccessibilitySupportEnabled()).to.eql(true)
  582. })
  583. })
  584. })
  585. describe('getAppPath', () => {
  586. it('works for directories with package.json', async () => {
  587. const { appPath } = await runTestApp('app-path')
  588. expect(appPath).to.equal(path.resolve(fixturesPath, 'api/app-path'))
  589. })
  590. it('works for directories with index.js', async () => {
  591. const { appPath } = await runTestApp('app-path/lib')
  592. expect(appPath).to.equal(path.resolve(fixturesPath, 'api/app-path/lib'))
  593. })
  594. it('works for files without extension', async () => {
  595. const { appPath } = await runTestApp('app-path/lib/index')
  596. expect(appPath).to.equal(path.resolve(fixturesPath, 'api/app-path/lib'))
  597. })
  598. it('works for files', async () => {
  599. const { appPath } = await runTestApp('app-path/lib/index.js')
  600. expect(appPath).to.equal(path.resolve(fixturesPath, 'api/app-path/lib'))
  601. })
  602. })
  603. describe('getPath("logs")', () => {
  604. const logsPaths = {
  605. 'darwin': path.resolve(homedir(), 'Library', 'Logs'),
  606. 'linux': path.resolve(homedir(), 'AppData', app.name),
  607. 'win32': path.resolve(homedir(), 'AppData', app.name),
  608. }
  609. it('has no logs directory by default', () => {
  610. // this won't be deterministic except on CI since
  611. // users may or may not have this dir
  612. if (!isCI) return
  613. const osLogPath = (logsPaths as any)[process.platform]
  614. expect(fs.existsSync(osLogPath)).to.be.false
  615. })
  616. it('creates a new logs directory if one does not exist', () => {
  617. expect(() => { app.getPath('logs') }).to.not.throw()
  618. const osLogPath = (logsPaths as any)[process.platform]
  619. expect(fs.existsSync(osLogPath)).to.be.true
  620. })
  621. })
  622. describe('getPath(name)', () => {
  623. it('returns paths that exist', () => {
  624. const paths = [
  625. fs.existsSync(app.getPath('exe')),
  626. fs.existsSync(app.getPath('home')),
  627. fs.existsSync(app.getPath('temp'))
  628. ]
  629. expect(paths).to.deep.equal([true, true, true])
  630. })
  631. it('throws an error when the name is invalid', () => {
  632. expect(() => {
  633. app.getPath('does-not-exist' as any)
  634. }).to.throw(/Failed to get 'does-not-exist' path/)
  635. })
  636. it('returns the overridden path', () => {
  637. app.setPath('music', __dirname)
  638. expect(app.getPath('music')).to.equal(__dirname)
  639. })
  640. })
  641. describe('setPath(name, path)', () => {
  642. it('throws when a relative path is passed', () => {
  643. const badPath = 'hey/hi/hello'
  644. expect(() => {
  645. app.setPath('music', badPath)
  646. }).to.throw(/Path must be absolute/)
  647. })
  648. it('does not create a new directory by default', () => {
  649. const badPath = path.join(__dirname, 'music')
  650. expect(fs.existsSync(badPath)).to.be.false
  651. app.setPath('music', badPath)
  652. expect(fs.existsSync(badPath)).to.be.false
  653. expect(() => { app.getPath(badPath as any) }).to.throw()
  654. })
  655. })
  656. describe('setAppLogsPath(path)', () => {
  657. it('throws when a relative path is passed', () => {
  658. const badPath = 'hey/hi/hello'
  659. expect(() => {
  660. app.setAppLogsPath(badPath)
  661. }).to.throw(/Path must be absolute/)
  662. })
  663. })
  664. describe('select-client-certificate event', () => {
  665. let w: BrowserWindow
  666. before(function () {
  667. if (process.platform === 'linux') {
  668. this.skip()
  669. }
  670. session.fromPartition('empty-certificate').setCertificateVerifyProc((req, cb) => { cb(0) })
  671. })
  672. beforeEach(() => {
  673. w = new BrowserWindow({
  674. show: false,
  675. webPreferences: {
  676. nodeIntegration: true,
  677. partition: 'empty-certificate'
  678. }
  679. })
  680. })
  681. afterEach(() => closeWindow(w).then(() => { w = null as any }))
  682. after(() => session.fromPartition('empty-certificate').setCertificateVerifyProc(null))
  683. it('can respond with empty certificate list', async () => {
  684. app.once('select-client-certificate', function (event, webContents, url, list, callback) {
  685. console.log('select-client-certificate emitted')
  686. event.preventDefault()
  687. callback()
  688. })
  689. await w.webContents.loadURL(secureUrl)
  690. expect(w.webContents.getTitle()).to.equal('denied')
  691. })
  692. })
  693. describe('setAsDefaultProtocolClient(protocol, path, args)', () => {
  694. const protocol = 'electron-test'
  695. const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe')
  696. const processStartArgs = [
  697. '--processStart', `"${path.basename(process.execPath)}"`,
  698. '--process-start-args', `"--hidden"`
  699. ]
  700. let Winreg: any
  701. let classesKey: any
  702. before(function () {
  703. if (process.platform !== 'win32') {
  704. this.skip()
  705. } else {
  706. Winreg = require('winreg')
  707. classesKey = new Winreg({
  708. hive: Winreg.HKCU,
  709. key: '\\Software\\Classes\\'
  710. })
  711. }
  712. })
  713. after(function (done) {
  714. if (process.platform !== 'win32') {
  715. done()
  716. } else {
  717. const protocolKey = new Winreg({
  718. hive: Winreg.HKCU,
  719. key: `\\Software\\Classes\\${protocol}`
  720. })
  721. // The last test leaves the registry dirty,
  722. // delete the protocol key for those of us who test at home
  723. protocolKey.destroy(() => done())
  724. }
  725. })
  726. beforeEach(() => {
  727. app.removeAsDefaultProtocolClient(protocol)
  728. app.removeAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  729. })
  730. afterEach(() => {
  731. app.removeAsDefaultProtocolClient(protocol)
  732. expect(app.isDefaultProtocolClient(protocol)).to.equal(false)
  733. app.removeAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  734. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.equal(false)
  735. })
  736. it('sets the app as the default protocol client', () => {
  737. expect(app.isDefaultProtocolClient(protocol)).to.equal(false)
  738. app.setAsDefaultProtocolClient(protocol)
  739. expect(app.isDefaultProtocolClient(protocol)).to.equal(true)
  740. })
  741. it('allows a custom path and args to be specified', () => {
  742. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.equal(false)
  743. app.setAsDefaultProtocolClient(protocol, updateExe, processStartArgs)
  744. expect(app.isDefaultProtocolClient(protocol, updateExe, processStartArgs)).to.equal(true)
  745. expect(app.isDefaultProtocolClient(protocol)).to.equal(false)
  746. })
  747. it('creates a registry entry for the protocol class', (done) => {
  748. app.setAsDefaultProtocolClient(protocol)
  749. classesKey.keys((error: Error, keys: any[]) => {
  750. if (error) throw error
  751. const exists = !!keys.find(key => key.key.includes(protocol))
  752. expect(exists).to.equal(true)
  753. done()
  754. })
  755. })
  756. it('completely removes a registry entry for the protocol class', (done) => {
  757. app.setAsDefaultProtocolClient(protocol)
  758. app.removeAsDefaultProtocolClient(protocol)
  759. classesKey.keys((error: Error, keys: any[]) => {
  760. if (error) throw error
  761. const exists = !!keys.find(key => key.key.includes(protocol))
  762. expect(exists).to.equal(false)
  763. done()
  764. })
  765. })
  766. it('only unsets a class registry key if it contains other data', (done) => {
  767. app.setAsDefaultProtocolClient(protocol)
  768. const protocolKey = new Winreg({
  769. hive: Winreg.HKCU,
  770. key: `\\Software\\Classes\\${protocol}`
  771. })
  772. protocolKey.set('test-value', 'REG_BINARY', '123', () => {
  773. app.removeAsDefaultProtocolClient(protocol)
  774. classesKey.keys((error: Error, keys: any[]) => {
  775. if (error) throw error
  776. const exists = !!keys.find(key => key.key.includes(protocol))
  777. expect(exists).to.equal(true)
  778. done()
  779. })
  780. })
  781. })
  782. it('sets the default client such that getApplicationNameForProtocol returns Electron', () => {
  783. app.setAsDefaultProtocolClient(protocol)
  784. expect(app.getApplicationNameForProtocol(`${protocol}://`)).to.equal('Electron')
  785. })
  786. })
  787. describe('getApplicationNameForProtocol()', () => {
  788. it('returns application names for common protocols', function () {
  789. // We can't expect particular app names here, but these protocols should
  790. // at least have _something_ registered. Except on our Linux CI
  791. // environment apparently.
  792. if (process.platform === 'linux') {
  793. this.skip()
  794. }
  795. const protocols = [
  796. 'http://',
  797. 'https://'
  798. ]
  799. protocols.forEach((protocol) => {
  800. expect(app.getApplicationNameForProtocol(protocol)).to.not.equal('')
  801. })
  802. })
  803. it('returns an empty string for a bogus protocol', () => {
  804. expect(app.getApplicationNameForProtocol('bogus-protocol://')).to.equal('')
  805. })
  806. })
  807. describe('isDefaultProtocolClient()', () => {
  808. it('returns false for a bogus protocol', () => {
  809. expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false)
  810. })
  811. })
  812. describe('app launch through uri', () => {
  813. before(function () {
  814. if (process.platform !== 'win32') {
  815. this.skip()
  816. }
  817. })
  818. it('does not launch for argument following a URL', done => {
  819. const appPath = path.join(fixturesPath, 'api', 'quit-app')
  820. // App should exit with non 123 code.
  821. const first = cp.spawn(process.execPath, [appPath, 'electron-test:?', 'abc'])
  822. first.once('exit', code => {
  823. expect(code).to.not.equal(123)
  824. done()
  825. })
  826. })
  827. it('launches successfully for argument following a file path', done => {
  828. const appPath = path.join(fixturesPath, 'api', 'quit-app')
  829. // App should exit with code 123.
  830. const first = cp.spawn(process.execPath, [appPath, 'e:\\abc', 'abc'])
  831. first.once('exit', code => {
  832. expect(code).to.equal(123)
  833. done()
  834. })
  835. })
  836. it('launches successfully for multiple URIs following --', done => {
  837. const appPath = path.join(fixturesPath, 'api', 'quit-app')
  838. // App should exit with code 123.
  839. const first = cp.spawn(process.execPath, [appPath, '--', 'http://electronjs.org', 'electron-test://testdata'])
  840. first.once('exit', code => {
  841. expect(code).to.equal(123)
  842. done()
  843. })
  844. })
  845. })
  846. describe('getFileIcon() API', () => {
  847. const iconPath = path.join(__dirname, 'fixtures/assets/icon.ico')
  848. const sizes = {
  849. small: 16,
  850. normal: 32,
  851. large: process.platform === 'win32' ? 32 : 48
  852. }
  853. // (alexeykuzmin): `.skip()` called in `before`
  854. // doesn't affect nested `describe`s.
  855. beforeEach(function () {
  856. // FIXME Get these specs running on Linux CI
  857. if (process.platform === 'linux' && isCI) {
  858. this.skip()
  859. }
  860. })
  861. it('fetches a non-empty icon', async () => {
  862. const icon = await app.getFileIcon(iconPath)
  863. expect(icon.isEmpty()).to.equal(false)
  864. })
  865. it('fetches normal icon size by default', async () => {
  866. const icon = await app.getFileIcon(iconPath)
  867. const size = icon.getSize()
  868. expect(size.height).to.equal(sizes.normal)
  869. expect(size.width).to.equal(sizes.normal)
  870. })
  871. describe('size option', () => {
  872. it('fetches a small icon', async () => {
  873. const icon = await app.getFileIcon(iconPath, { size: 'small' })
  874. const size = icon.getSize()
  875. expect(size.height).to.equal(sizes.small)
  876. expect(size.width).to.equal(sizes.small)
  877. })
  878. it('fetches a normal icon', async () => {
  879. const icon = await app.getFileIcon(iconPath, { size: 'normal' })
  880. const size = icon.getSize()
  881. expect(size.height).to.equal(sizes.normal)
  882. expect(size.width).to.equal(sizes.normal)
  883. })
  884. it('fetches a large icon', async () => {
  885. // macOS does not support large icons
  886. if (process.platform === 'darwin') return
  887. const icon = await app.getFileIcon(iconPath, { size: 'large' })
  888. const size = icon.getSize()
  889. expect(size.height).to.equal(sizes.large)
  890. expect(size.width).to.equal(sizes.large)
  891. })
  892. })
  893. })
  894. describe('getAppMetrics() API', () => {
  895. it('returns memory and cpu stats of all running electron processes', () => {
  896. const appMetrics = app.getAppMetrics()
  897. expect(appMetrics).to.be.an('array').and.have.lengthOf.at.least(1, 'App memory info object is not > 0')
  898. const types = []
  899. for (const entry of appMetrics) {
  900. expect(entry.pid).to.be.above(0, 'pid is not > 0')
  901. expect(entry.type).to.be.a('string').that.does.not.equal('')
  902. expect(entry.creationTime).to.be.a('number').that.is.greaterThan(0)
  903. types.push(entry.type)
  904. expect(entry.cpu).to.have.ownProperty('percentCPUUsage').that.is.a('number')
  905. expect(entry.cpu).to.have.ownProperty('idleWakeupsPerSecond').that.is.a('number')
  906. expect(entry.memory).to.have.property('workingSetSize').that.is.greaterThan(0)
  907. expect(entry.memory).to.have.property('peakWorkingSetSize').that.is.greaterThan(0)
  908. if (process.platform === 'win32') {
  909. expect(entry.memory).to.have.property('privateBytes').that.is.greaterThan(0)
  910. }
  911. if (process.platform !== 'linux') {
  912. expect(entry.sandboxed).to.be.a('boolean')
  913. }
  914. if (process.platform === 'win32') {
  915. expect(entry.integrityLevel).to.be.a('string')
  916. }
  917. }
  918. if (process.platform === 'darwin') {
  919. expect(types).to.include('GPU')
  920. }
  921. expect(types).to.include('Browser')
  922. })
  923. })
  924. describe('getGPUFeatureStatus() API', () => {
  925. it('returns the graphic features statuses', () => {
  926. const features = app.getGPUFeatureStatus()
  927. expect(features).to.have.ownProperty('webgl').that.is.a('string')
  928. expect(features).to.have.ownProperty('gpu_compositing').that.is.a('string')
  929. })
  930. })
  931. describe('getGPUInfo() API', () => {
  932. const appPath = path.join(fixturesPath, 'api', 'gpu-info.js')
  933. const getGPUInfo = async (type: string) => {
  934. const appProcess = cp.spawn(process.execPath, [appPath, type])
  935. let gpuInfoData = ''
  936. let errorData = ''
  937. appProcess.stdout.on('data', (data) => {
  938. gpuInfoData += data
  939. })
  940. appProcess.stderr.on('data', (data) => {
  941. errorData += data
  942. })
  943. const [exitCode] = await emittedOnce(appProcess, 'exit')
  944. if (exitCode === 0) {
  945. // return info data on successful exit
  946. return JSON.parse(gpuInfoData)
  947. } else {
  948. // return error if not clean exit
  949. return Promise.reject(new Error(errorData))
  950. }
  951. }
  952. const verifyBasicGPUInfo = async (gpuInfo: any) => {
  953. // Devices information is always present in the available info.
  954. expect(gpuInfo).to.have.ownProperty('gpuDevice')
  955. .that.is.an('array')
  956. .and.does.not.equal([])
  957. const device = gpuInfo.gpuDevice[0]
  958. expect(device).to.be.an('object')
  959. .and.to.have.property('deviceId')
  960. .that.is.a('number')
  961. .not.lessThan(0)
  962. }
  963. it('succeeds with basic GPUInfo', async () => {
  964. const gpuInfo = await getGPUInfo('basic')
  965. await verifyBasicGPUInfo(gpuInfo)
  966. })
  967. it('succeeds with complete GPUInfo', async () => {
  968. const completeInfo = await getGPUInfo('complete')
  969. if (process.platform === 'linux') {
  970. // For linux and macOS complete info is same as basic info
  971. await verifyBasicGPUInfo(completeInfo)
  972. const basicInfo = await getGPUInfo('basic')
  973. expect(completeInfo).to.deep.equal(basicInfo)
  974. } else {
  975. // Gl version is present in the complete info.
  976. expect(completeInfo).to.have.ownProperty('auxAttributes')
  977. .that.is.an('object')
  978. expect(completeInfo.auxAttributes).to.have.ownProperty('glVersion')
  979. .that.is.a('string')
  980. .and.does.not.equal([])
  981. }
  982. })
  983. it('fails for invalid info_type', () => {
  984. const invalidType = 'invalid'
  985. const expectedErrorMessage = "Invalid info type. Use 'basic' or 'complete'"
  986. return expect(app.getGPUInfo(invalidType as any)).to.eventually.be.rejectedWith(expectedErrorMessage)
  987. })
  988. })
  989. describe('sandbox options', () => {
  990. let appProcess: cp.ChildProcess = null as any
  991. let server: net.Server = null as any
  992. const socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\electron-mixed-sandbox' : '/tmp/electron-mixed-sandbox'
  993. beforeEach(function (done) {
  994. if (process.platform === 'linux' && (process.arch === 'arm64' || process.arch === 'arm')) {
  995. // Our ARM tests are run on VSTS rather than CircleCI, and the Docker
  996. // setup on VSTS disallows syscalls that Chrome requires for setting up
  997. // sandboxing.
  998. // See:
  999. // - https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
  1000. // - https://chromium.googlesource.com/chromium/src/+/70.0.3538.124/sandbox/linux/services/credentials.cc#292
  1001. // - https://github.com/docker/docker-ce/blob/ba7dfc59ccfe97c79ee0d1379894b35417b40bca/components/engine/profiles/seccomp/seccomp_default.go#L497
  1002. // - https://blog.jessfraz.com/post/how-to-use-new-docker-seccomp-profiles/
  1003. //
  1004. // Adding `--cap-add SYS_ADMIN` or `--security-opt seccomp=unconfined`
  1005. // to the Docker invocation allows the syscalls that Chrome needs, but
  1006. // are probably more permissive than we'd like.
  1007. this.skip()
  1008. }
  1009. fs.unlink(socketPath, () => {
  1010. server = net.createServer()
  1011. server.listen(socketPath)
  1012. done()
  1013. })
  1014. })
  1015. afterEach(done => {
  1016. if (appProcess != null) appProcess.kill()
  1017. server.close(() => {
  1018. if (process.platform === 'win32') {
  1019. done()
  1020. } else {
  1021. fs.unlink(socketPath, () => done())
  1022. }
  1023. })
  1024. })
  1025. describe('when app.enableSandbox() is called', () => {
  1026. it('adds --enable-sandbox to all renderer processes', done => {
  1027. const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app')
  1028. appProcess = cp.spawn(process.execPath, [appPath, '--app-enable-sandbox'])
  1029. server.once('error', error => { done(error) })
  1030. server.on('connection', client => {
  1031. client.once('data', (data) => {
  1032. const argv = JSON.parse(data.toString())
  1033. expect(argv.sandbox).to.include('--enable-sandbox')
  1034. expect(argv.sandbox).to.not.include('--no-sandbox')
  1035. expect(argv.noSandbox).to.include('--enable-sandbox')
  1036. expect(argv.noSandbox).to.not.include('--no-sandbox')
  1037. expect(argv.noSandboxDevtools).to.equal(true)
  1038. expect(argv.sandboxDevtools).to.equal(true)
  1039. done()
  1040. })
  1041. })
  1042. })
  1043. })
  1044. describe('when the app is launched with --enable-sandbox', () => {
  1045. it('adds --enable-sandbox to all renderer processes', done => {
  1046. const appPath = path.join(fixturesPath, 'api', 'mixed-sandbox-app')
  1047. appProcess = cp.spawn(process.execPath, [appPath, '--enable-sandbox'])
  1048. server.once('error', error => { done(error) })
  1049. server.on('connection', client => {
  1050. client.once('data', data => {
  1051. const argv = JSON.parse(data.toString())
  1052. expect(argv.sandbox).to.include('--enable-sandbox')
  1053. expect(argv.sandbox).to.not.include('--no-sandbox')
  1054. expect(argv.noSandbox).to.include('--enable-sandbox')
  1055. expect(argv.noSandbox).to.not.include('--no-sandbox')
  1056. expect(argv.noSandboxDevtools).to.equal(true)
  1057. expect(argv.sandboxDevtools).to.equal(true)
  1058. done()
  1059. })
  1060. })
  1061. })
  1062. })
  1063. })
  1064. describe('disableDomainBlockingFor3DAPIs() API', () => {
  1065. it('throws when called after app is ready', () => {
  1066. expect(() => {
  1067. app.disableDomainBlockingFor3DAPIs()
  1068. }).to.throw(/before app is ready/)
  1069. })
  1070. })
  1071. const dockDescribe = process.platform === 'darwin' ? describe : describe.skip
  1072. dockDescribe('dock APIs', () => {
  1073. after(async () => {
  1074. await app.dock.show()
  1075. })
  1076. describe('dock.setMenu', () => {
  1077. it('can be retrieved via dock.getMenu', () => {
  1078. expect(app.dock.getMenu()).to.equal(null)
  1079. const menu = new Menu()
  1080. app.dock.setMenu(menu)
  1081. expect(app.dock.getMenu()).to.equal(menu)
  1082. })
  1083. it('keeps references to the menu', () => {
  1084. app.dock.setMenu(new Menu())
  1085. const v8Util = process.electronBinding('v8_util')
  1086. v8Util.requestGarbageCollectionForTesting()
  1087. })
  1088. })
  1089. describe('dock.bounce', () => {
  1090. it('should return -1 for unknown bounce type', () => {
  1091. expect(app.dock.bounce('bad type' as any)).to.equal(-1)
  1092. })
  1093. it('should return a positive number for informational type', () => {
  1094. const appHasFocus = !!BrowserWindow.getFocusedWindow()
  1095. if (!appHasFocus) {
  1096. expect(app.dock.bounce('informational')).to.be.at.least(0)
  1097. }
  1098. })
  1099. it('should return a positive number for critical type', () => {
  1100. const appHasFocus = !!BrowserWindow.getFocusedWindow()
  1101. if (!appHasFocus) {
  1102. expect(app.dock.bounce('critical')).to.be.at.least(0)
  1103. }
  1104. })
  1105. })
  1106. describe('dock.cancelBounce', () => {
  1107. it('should not throw', () => {
  1108. app.dock.cancelBounce(app.dock.bounce('critical'))
  1109. })
  1110. })
  1111. describe('dock.setBadge', () => {
  1112. after(() => {
  1113. app.dock.setBadge('')
  1114. })
  1115. it('should not throw', () => {
  1116. app.dock.setBadge('1')
  1117. })
  1118. it('should be retrievable via getBadge', () => {
  1119. app.dock.setBadge('test')
  1120. expect(app.dock.getBadge()).to.equal('test')
  1121. })
  1122. })
  1123. describe('dock.hide', () => {
  1124. it('should not throw', () => {
  1125. app.dock.hide()
  1126. expect(app.dock.isVisible()).to.equal(false)
  1127. })
  1128. })
  1129. // Note that dock.show tests should run after dock.hide tests, to work
  1130. // around a bug of macOS.
  1131. // See https://github.com/electron/electron/pull/25269 for more.
  1132. describe('dock.show', () => {
  1133. it('should not throw', () => {
  1134. return app.dock.show().then(() => {
  1135. expect(app.dock.isVisible()).to.equal(true)
  1136. })
  1137. })
  1138. it('returns a Promise', () => {
  1139. expect(app.dock.show()).to.be.a('promise')
  1140. })
  1141. it('eventually fulfills', async () => {
  1142. await expect(app.dock.show()).to.eventually.be.fulfilled.equal(undefined)
  1143. })
  1144. })
  1145. })
  1146. describe('whenReady', () => {
  1147. it('returns a Promise', () => {
  1148. expect(app.whenReady()).to.be.a('promise')
  1149. })
  1150. it('becomes fulfilled if the app is already ready', async () => {
  1151. expect(app.isReady()).to.equal(true)
  1152. await expect(app.whenReady()).to.be.eventually.fulfilled.equal(undefined)
  1153. })
  1154. })
  1155. describe('app.applicationMenu', () => {
  1156. it('has the applicationMenu property', () => {
  1157. expect(app).to.have.property('applicationMenu')
  1158. })
  1159. })
  1160. describe('commandLine.hasSwitch', () => {
  1161. it('returns true when present', () => {
  1162. app.commandLine.appendSwitch('foobar1')
  1163. expect(app.commandLine.hasSwitch('foobar1')).to.equal(true)
  1164. })
  1165. it('returns false when not present', () => {
  1166. expect(app.commandLine.hasSwitch('foobar2')).to.equal(false)
  1167. })
  1168. })
  1169. describe('commandLine.hasSwitch (existing argv)', () => {
  1170. it('returns true when present', async () => {
  1171. const { hasSwitch } = await runTestApp('command-line', '--foobar')
  1172. expect(hasSwitch).to.equal(true)
  1173. })
  1174. it('returns false when not present', async () => {
  1175. const { hasSwitch } = await runTestApp('command-line')
  1176. expect(hasSwitch).to.equal(false)
  1177. })
  1178. })
  1179. describe('commandLine.getSwitchValue', () => {
  1180. it('returns the value when present', () => {
  1181. app.commandLine.appendSwitch('foobar', 'æøåü')
  1182. expect(app.commandLine.getSwitchValue('foobar')).to.equal('æøåü')
  1183. })
  1184. it('returns an empty string when present without value', () => {
  1185. app.commandLine.appendSwitch('foobar1')
  1186. expect(app.commandLine.getSwitchValue('foobar1')).to.equal('')
  1187. })
  1188. it('returns an empty string when not present', () => {
  1189. expect(app.commandLine.getSwitchValue('foobar2')).to.equal('')
  1190. })
  1191. })
  1192. describe('commandLine.getSwitchValue (existing argv)', () => {
  1193. it('returns the value when present', async () => {
  1194. const { getSwitchValue } = await runTestApp('command-line', '--foobar=test')
  1195. expect(getSwitchValue).to.equal('test')
  1196. })
  1197. it('returns an empty string when present without value', async () => {
  1198. const { getSwitchValue } = await runTestApp('command-line', '--foobar')
  1199. expect(getSwitchValue).to.equal('')
  1200. })
  1201. it('returns an empty string when not present', async () => {
  1202. const { getSwitchValue } = await runTestApp('command-line')
  1203. expect(getSwitchValue).to.equal('')
  1204. })
  1205. })
  1206. })
  1207. describe('default behavior', () => {
  1208. describe('application menu', () => {
  1209. it('creates the default menu if the app does not set it', async () => {
  1210. const result = await runTestApp('default-menu')
  1211. expect(result).to.equal(false)
  1212. })
  1213. it('does not create the default menu if the app sets a custom menu', async () => {
  1214. const result = await runTestApp('default-menu', '--custom-menu')
  1215. expect(result).to.equal(true)
  1216. })
  1217. it('does not create the default menu if the app sets a null menu', async () => {
  1218. const result = await runTestApp('default-menu', '--null-menu')
  1219. expect(result).to.equal(true)
  1220. })
  1221. })
  1222. describe('window-all-closed', () => {
  1223. it('quits when the app does not handle the event', async () => {
  1224. const result = await runTestApp('window-all-closed')
  1225. expect(result).to.equal(false)
  1226. })
  1227. it('does not quit when the app handles the event', async () => {
  1228. const result = await runTestApp('window-all-closed', '--handle-event')
  1229. expect(result).to.equal(true)
  1230. })
  1231. })
  1232. describe('user agent fallback', () => {
  1233. let initialValue: string
  1234. before(() => {
  1235. initialValue = app.userAgentFallback!
  1236. })
  1237. it('should have a reasonable default', () => {
  1238. expect(initialValue).to.include(`Electron/${process.versions.electron}`)
  1239. expect(initialValue).to.include(`Chrome/${process.versions.chrome}`)
  1240. })
  1241. it('should be overridable', () => {
  1242. app.userAgentFallback = 'test-agent/123'
  1243. expect(app.userAgentFallback).to.equal('test-agent/123')
  1244. })
  1245. it('should be restorable', () => {
  1246. app.userAgentFallback = 'test-agent/123'
  1247. app.userAgentFallback = ''
  1248. expect(app.userAgentFallback).to.equal(initialValue)
  1249. })
  1250. })
  1251. describe('app.allowRendererProcessReuse', () => {
  1252. it('should default to false', () => {
  1253. expect(app.allowRendererProcessReuse).to.equal(false)
  1254. })
  1255. it('should cause renderer processes to get new PIDs when false', async () => {
  1256. const output = await runTestApp('site-instance-overrides', 'false')
  1257. expect(output[0]).to.be.a('number').that.is.greaterThan(0)
  1258. expect(output[1]).to.be.a('number').that.is.greaterThan(0)
  1259. expect(output[0]).to.not.equal(output[1])
  1260. })
  1261. it('should cause renderer processes to keep the same PID when true', async () => {
  1262. const output = await runTestApp('site-instance-overrides', 'true')
  1263. expect(output[0]).to.be.a('number').that.is.greaterThan(0)
  1264. expect(output[1]).to.be.a('number').that.is.greaterThan(0)
  1265. expect(output[0]).to.equal(output[1])
  1266. })
  1267. })
  1268. describe('login event', () => {
  1269. afterEach(closeAllWindows)
  1270. let server: http.Server
  1271. let serverUrl: string
  1272. before((done) => {
  1273. server = http.createServer((request, response) => {
  1274. if (request.headers.authorization) {
  1275. return response.end('ok')
  1276. }
  1277. response
  1278. .writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' })
  1279. .end()
  1280. }).listen(0, '127.0.0.1', () => {
  1281. serverUrl = 'http://127.0.0.1:' + (server.address() as net.AddressInfo).port
  1282. done()
  1283. })
  1284. })
  1285. it('should emit a login event on app when a WebContents hits a 401', async () => {
  1286. const w = new BrowserWindow({ show: false })
  1287. w.loadURL(serverUrl)
  1288. const [, webContents] = await emittedOnce(app, 'login')
  1289. expect(webContents).to.equal(w.webContents)
  1290. })
  1291. })
  1292. })
  1293. async function runTestApp (name: string, ...args: any[]) {
  1294. const appPath = path.join(fixturesPath, 'api', name)
  1295. const electronPath = process.execPath
  1296. const appProcess = cp.spawn(electronPath, [appPath, ...args])
  1297. let output = ''
  1298. appProcess.stdout.on('data', (data) => { output += data })
  1299. await emittedOnce(appProcess.stdout, 'end')
  1300. return JSON.parse(output)
  1301. }