api-app-spec.ts 46 KB

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