api-menu-spec.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989
  1. import * as cp from 'child_process'
  2. import * as path from 'path'
  3. import { expect } from 'chai'
  4. import { BrowserWindow, globalShortcut, Menu, MenuItem } from 'electron'
  5. import { sortMenuItems } from '../lib/browser/api/menu-utils'
  6. import { emittedOnce } from './events-helpers'
  7. import { ifit } from './spec-helpers'
  8. import { closeWindow } from './window-helpers'
  9. const fixturesPath = path.resolve(__dirname, 'fixtures')
  10. describe('Menu module', function () {
  11. this.timeout(5000)
  12. describe('Menu.buildFromTemplate', () => {
  13. it('should be able to attach extra fields', () => {
  14. const menu = Menu.buildFromTemplate([
  15. {
  16. label: 'text',
  17. extra: 'field'
  18. } as MenuItem | Record<string, any>
  19. ])
  20. expect((menu.items[0] as any).extra).to.equal('field')
  21. })
  22. it('should be able to accept only MenuItems', () => {
  23. const menu = Menu.buildFromTemplate([
  24. new MenuItem({ label: 'one' }),
  25. new MenuItem({ label: 'two' })
  26. ])
  27. expect(menu.items[0].label).to.equal('one')
  28. expect(menu.items[1].label).to.equal('two')
  29. })
  30. it('should be able to accept only MenuItems in a submenu', () => {
  31. const menu = Menu.buildFromTemplate([
  32. {
  33. label: 'one',
  34. submenu: [
  35. new MenuItem({ label: 'two' }) as any
  36. ]
  37. }
  38. ])
  39. expect(menu.items[0].label).to.equal('one')
  40. expect(menu.items[0].submenu!.items[0].label).to.equal('two')
  41. })
  42. it('should be able to accept MenuItems and plain objects', () => {
  43. const menu = Menu.buildFromTemplate([
  44. new MenuItem({ label: 'one' }),
  45. { label: 'two' }
  46. ])
  47. expect(menu.items[0].label).to.equal('one')
  48. expect(menu.items[1].label).to.equal('two')
  49. })
  50. it('does not modify the specified template', () => {
  51. const template = [{ label: 'text', submenu: [{ label: 'sub' }] }]
  52. const templateCopy = JSON.parse(JSON.stringify(template))
  53. Menu.buildFromTemplate(template)
  54. expect(template).to.deep.equal(templateCopy)
  55. })
  56. it('does not throw exceptions for undefined/null values', () => {
  57. expect(() => {
  58. Menu.buildFromTemplate([
  59. {
  60. label: 'text',
  61. accelerator: undefined
  62. },
  63. {
  64. label: 'text again',
  65. accelerator: null as any
  66. }
  67. ])
  68. }).to.not.throw()
  69. })
  70. it('does throw exceptions for empty objects and null values', () => {
  71. expect(() => {
  72. Menu.buildFromTemplate([{}, null as any])
  73. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/)
  74. })
  75. it('does throw exception for object without role, label, or type attribute', () => {
  76. expect(() => {
  77. Menu.buildFromTemplate([{ 'visible': true }])
  78. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/)
  79. })
  80. it('does throw exception for undefined', () => {
  81. expect(() => {
  82. Menu.buildFromTemplate([undefined as any])
  83. }).to.throw(/Invalid template for MenuItem: must have at least one of label, role or type/)
  84. })
  85. describe('Menu sorting and building', () => {
  86. describe('sorts groups', () => {
  87. it('does a simple sort', () => {
  88. const items = [
  89. {
  90. label: 'two',
  91. id: '2',
  92. afterGroupContaining: ['1'] },
  93. { type: 'separator' },
  94. {
  95. id: '1',
  96. label: 'one'
  97. }
  98. ]
  99. const expected = [
  100. {
  101. id: '1',
  102. label: 'one'
  103. },
  104. { type: 'separator' },
  105. {
  106. id: '2',
  107. label: 'two',
  108. afterGroupContaining: ['1']
  109. }
  110. ]
  111. expect(sortMenuItems(items)).to.deep.equal(expected)
  112. })
  113. it('does a simple sort with MenuItems', () => {
  114. const firstItem = new MenuItem({ id: '1', label: 'one' })
  115. const secondItem = new MenuItem({
  116. label: 'two',
  117. id: '2',
  118. afterGroupContaining: ['1']
  119. })
  120. const sep = new MenuItem({ type: 'separator' })
  121. const items = [ secondItem, sep, firstItem ]
  122. const expected = [ firstItem, sep, secondItem ]
  123. expect(sortMenuItems(items)).to.deep.equal(expected)
  124. })
  125. it('resolves cycles by ignoring things that conflict', () => {
  126. const items = [
  127. {
  128. id: '2',
  129. label: 'two',
  130. afterGroupContaining: ['1']
  131. },
  132. { type: 'separator' },
  133. {
  134. id: '1',
  135. label: 'one',
  136. afterGroupContaining: ['2']
  137. }
  138. ]
  139. const expected = [
  140. {
  141. id: '1',
  142. label: 'one',
  143. afterGroupContaining: ['2']
  144. },
  145. { type: 'separator' },
  146. {
  147. id: '2',
  148. label: 'two',
  149. afterGroupContaining: ['1']
  150. }
  151. ]
  152. expect(sortMenuItems(items)).to.deep.equal(expected)
  153. })
  154. it('ignores references to commands that do not exist', () => {
  155. const items = [
  156. {
  157. id: '1',
  158. label: 'one'
  159. },
  160. { type: 'separator' },
  161. {
  162. id: '2',
  163. label: 'two',
  164. afterGroupContaining: ['does-not-exist']
  165. }
  166. ]
  167. const expected = [
  168. {
  169. id: '1',
  170. label: 'one'
  171. },
  172. { type: 'separator' },
  173. {
  174. id: '2',
  175. label: 'two',
  176. afterGroupContaining: ['does-not-exist']
  177. }
  178. ]
  179. expect(sortMenuItems(items)).to.deep.equal(expected)
  180. })
  181. it('only respects the first matching [before|after]GroupContaining rule in a given group', () => {
  182. const items = [
  183. {
  184. id: '1',
  185. label: 'one'
  186. },
  187. { type: 'separator' },
  188. {
  189. id: '3',
  190. label: 'three',
  191. beforeGroupContaining: ['1']
  192. },
  193. {
  194. id: '4',
  195. label: 'four',
  196. afterGroupContaining: ['2']
  197. },
  198. { type: 'separator' },
  199. {
  200. id: '2',
  201. label: 'two'
  202. }
  203. ]
  204. const expected = [
  205. {
  206. id: '3',
  207. label: 'three',
  208. beforeGroupContaining: ['1']
  209. },
  210. {
  211. id: '4',
  212. label: 'four',
  213. afterGroupContaining: ['2']
  214. },
  215. { type: 'separator' },
  216. {
  217. id: '1',
  218. label: 'one'
  219. },
  220. { type: 'separator' },
  221. {
  222. id: '2',
  223. label: 'two'
  224. }
  225. ]
  226. expect(sortMenuItems(items)).to.deep.equal(expected)
  227. })
  228. })
  229. describe('moves an item to a different group by merging groups', () => {
  230. it('can move a group of one item', () => {
  231. const items = [
  232. {
  233. id: '1',
  234. label: 'one'
  235. },
  236. { type: 'separator' },
  237. {
  238. id: '2',
  239. label: 'two'
  240. },
  241. { type: 'separator' },
  242. {
  243. id: '3',
  244. label: 'three',
  245. after: ['1']
  246. },
  247. { type: 'separator' }
  248. ]
  249. const expected = [
  250. {
  251. id: '1',
  252. label: 'one'
  253. },
  254. {
  255. id: '3',
  256. label: 'three',
  257. after: ['1']
  258. },
  259. { type: 'separator' },
  260. {
  261. id: '2',
  262. label: 'two'
  263. }
  264. ]
  265. expect(sortMenuItems(items)).to.deep.equal(expected)
  266. })
  267. it("moves all items in the moving item's group", () => {
  268. const items = [
  269. {
  270. id: '1',
  271. label: 'one'
  272. },
  273. { type: 'separator' },
  274. {
  275. id: '2',
  276. label: 'two'
  277. },
  278. { type: 'separator' },
  279. {
  280. id: '3',
  281. label: 'three',
  282. after: ['1']
  283. },
  284. {
  285. id: '4',
  286. label: 'four'
  287. },
  288. { type: 'separator' }
  289. ]
  290. const expected = [
  291. {
  292. id: '1',
  293. label: 'one'
  294. },
  295. {
  296. id: '3',
  297. label: 'three',
  298. after: ['1']
  299. },
  300. {
  301. id: '4',
  302. label: 'four'
  303. },
  304. { type: 'separator' },
  305. {
  306. id: '2',
  307. label: 'two'
  308. }
  309. ]
  310. expect(sortMenuItems(items)).to.deep.equal(expected)
  311. })
  312. it("ignores positions relative to commands that don't exist", () => {
  313. const items = [
  314. {
  315. id: '1',
  316. label: 'one'
  317. },
  318. { type: 'separator' },
  319. {
  320. id: '2',
  321. label: 'two'
  322. },
  323. { type: 'separator' },
  324. {
  325. id: '3',
  326. label: 'three',
  327. after: ['does-not-exist']
  328. },
  329. {
  330. id: '4',
  331. label: 'four',
  332. after: ['1']
  333. },
  334. { type: 'separator' }
  335. ]
  336. const expected = [
  337. {
  338. id: '1',
  339. label: 'one'
  340. },
  341. {
  342. id: '3',
  343. label: 'three',
  344. after: ['does-not-exist']
  345. },
  346. {
  347. id: '4',
  348. label: 'four',
  349. after: ['1']
  350. },
  351. { type: 'separator' },
  352. {
  353. id: '2',
  354. label: 'two'
  355. }
  356. ]
  357. expect(sortMenuItems(items)).to.deep.equal(expected)
  358. })
  359. it('can handle recursive group merging', () => {
  360. const items = [
  361. {
  362. id: '1',
  363. label: 'one',
  364. after: ['3']
  365. },
  366. {
  367. id: '2',
  368. label: 'two',
  369. before: ['1']
  370. },
  371. {
  372. id: '3',
  373. label: 'three'
  374. }
  375. ]
  376. const expected = [
  377. {
  378. id: '3',
  379. label: 'three'
  380. },
  381. {
  382. id: '2',
  383. label: 'two',
  384. before: ['1']
  385. },
  386. {
  387. id: '1',
  388. label: 'one',
  389. after: ['3']
  390. }
  391. ]
  392. expect(sortMenuItems(items)).to.deep.equal(expected)
  393. })
  394. it('can merge multiple groups when given a list of before/after commands', () => {
  395. const items = [
  396. {
  397. id: '1',
  398. label: 'one'
  399. },
  400. { type: 'separator' },
  401. {
  402. id: '2',
  403. label: 'two'
  404. },
  405. { type: 'separator' },
  406. {
  407. id: '3',
  408. label: 'three',
  409. after: ['1', '2']
  410. }
  411. ]
  412. const expected = [
  413. {
  414. id: '2',
  415. label: 'two'
  416. },
  417. {
  418. id: '1',
  419. label: 'one'
  420. },
  421. {
  422. id: '3',
  423. label: 'three',
  424. after: ['1', '2']
  425. }
  426. ]
  427. expect(sortMenuItems(items)).to.deep.equal(expected)
  428. })
  429. it('can merge multiple groups based on both before/after commands', () => {
  430. const items = [
  431. {
  432. id: '1',
  433. label: 'one'
  434. },
  435. { type: 'separator' },
  436. {
  437. id: '2',
  438. label: 'two'
  439. },
  440. { type: 'separator' },
  441. {
  442. id: '3',
  443. label: 'three',
  444. after: ['1'],
  445. before: ['2']
  446. }
  447. ]
  448. const expected = [
  449. {
  450. id: '1',
  451. label: 'one'
  452. },
  453. {
  454. id: '3',
  455. label: 'three',
  456. after: ['1'],
  457. before: ['2']
  458. },
  459. {
  460. id: '2',
  461. label: 'two'
  462. }
  463. ]
  464. expect(sortMenuItems(items)).to.deep.equal(expected)
  465. })
  466. })
  467. it('should position before existing item', () => {
  468. const menu = Menu.buildFromTemplate([
  469. {
  470. id: '2',
  471. label: 'two'
  472. }, {
  473. id: '3',
  474. label: 'three'
  475. }, {
  476. id: '1',
  477. label: 'one',
  478. before: ['2']
  479. }
  480. ])
  481. expect(menu.items[0].label).to.equal('one')
  482. expect(menu.items[1].label).to.equal('two')
  483. expect(menu.items[2].label).to.equal('three')
  484. })
  485. it('should position after existing item', () => {
  486. const menu = Menu.buildFromTemplate([
  487. {
  488. id: '2',
  489. label: 'two',
  490. after: ['1']
  491. },
  492. {
  493. id: '1',
  494. label: 'one'
  495. }, {
  496. id: '3',
  497. label: 'three'
  498. }
  499. ])
  500. expect(menu.items[0].label).to.equal('one')
  501. expect(menu.items[1].label).to.equal('two')
  502. expect(menu.items[2].label).to.equal('three')
  503. })
  504. it('should filter excess menu separators', () => {
  505. const menuOne = Menu.buildFromTemplate([
  506. {
  507. type: 'separator'
  508. }, {
  509. label: 'a'
  510. }, {
  511. label: 'b'
  512. }, {
  513. label: 'c'
  514. }, {
  515. type: 'separator'
  516. }
  517. ])
  518. expect(menuOne.items).to.have.length(3)
  519. expect(menuOne.items[0].label).to.equal('a')
  520. expect(menuOne.items[1].label).to.equal('b')
  521. expect(menuOne.items[2].label).to.equal('c')
  522. const menuTwo = Menu.buildFromTemplate([
  523. {
  524. type: 'separator'
  525. }, {
  526. type: 'separator'
  527. }, {
  528. label: 'a'
  529. }, {
  530. label: 'b'
  531. }, {
  532. label: 'c'
  533. }, {
  534. type: 'separator'
  535. }, {
  536. type: 'separator'
  537. }
  538. ])
  539. expect(menuTwo.items).to.have.length(3)
  540. expect(menuTwo.items[0].label).to.equal('a')
  541. expect(menuTwo.items[1].label).to.equal('b')
  542. expect(menuTwo.items[2].label).to.equal('c')
  543. })
  544. it('should continue inserting items at next index when no specifier is present', () => {
  545. const menu = Menu.buildFromTemplate([
  546. {
  547. id: '2',
  548. label: 'two'
  549. }, {
  550. id: '3',
  551. label: 'three'
  552. }, {
  553. id: '4',
  554. label: 'four'
  555. }, {
  556. id: '5',
  557. label: 'five'
  558. }, {
  559. id: '1',
  560. label: 'one',
  561. before: ['2']
  562. }
  563. ])
  564. expect(menu.items[0].label).to.equal('one')
  565. expect(menu.items[1].label).to.equal('two')
  566. expect(menu.items[2].label).to.equal('three')
  567. expect(menu.items[3].label).to.equal('four')
  568. expect(menu.items[4].label).to.equal('five')
  569. })
  570. it('should continue inserting MenuItems at next index when no specifier is present', () => {
  571. const menu = Menu.buildFromTemplate([
  572. new MenuItem({
  573. id: '2',
  574. label: 'two'
  575. }), new MenuItem({
  576. id: '3',
  577. label: 'three'
  578. }), new MenuItem({
  579. id: '4',
  580. label: 'four'
  581. }), new MenuItem({
  582. id: '5',
  583. label: 'five'
  584. }), new MenuItem({
  585. id: '1',
  586. label: 'one',
  587. before: ['2']
  588. })
  589. ])
  590. expect(menu.items[0].label).to.equal('one')
  591. expect(menu.items[1].label).to.equal('two')
  592. expect(menu.items[2].label).to.equal('three')
  593. expect(menu.items[3].label).to.equal('four')
  594. expect(menu.items[4].label).to.equal('five')
  595. })
  596. })
  597. })
  598. describe('Menu.getMenuItemById', () => {
  599. it('should return the item with the given id', () => {
  600. const menu = Menu.buildFromTemplate([
  601. {
  602. label: 'View',
  603. submenu: [
  604. {
  605. label: 'Enter Fullscreen',
  606. accelerator: 'ControlCommandF',
  607. id: 'fullScreen'
  608. }
  609. ]
  610. }
  611. ])
  612. const fsc = menu.getMenuItemById('fullScreen')
  613. expect(menu.items[0].submenu!.items[0]).to.equal(fsc)
  614. })
  615. it('should return the separator with the given id', () => {
  616. const menu = Menu.buildFromTemplate([
  617. {
  618. label: 'Item 1',
  619. id: 'item_1'
  620. },
  621. {
  622. id: 'separator',
  623. type: 'separator'
  624. },
  625. {
  626. label: 'Item 2',
  627. id: 'item_2'
  628. }
  629. ])
  630. const separator = menu.getMenuItemById('separator')
  631. expect(separator).to.be.an('object')
  632. expect(separator).to.equal(menu.items[1])
  633. })
  634. })
  635. describe('Menu.insert', () => {
  636. it('should throw when attempting to insert at out-of-range indices', () => {
  637. const menu = Menu.buildFromTemplate([
  638. { label: '1' },
  639. { label: '2' },
  640. { label: '3' }
  641. ])
  642. const item = new MenuItem({ label: 'badInsert' })
  643. expect(() => {
  644. menu.insert(9999, item)
  645. }).to.throw(/Position 9999 cannot be greater than the total MenuItem count/)
  646. expect(() => {
  647. menu.insert(-9999, item)
  648. }).to.throw(/Position -9999 cannot be less than 0/)
  649. })
  650. it('should store item in @items by its index', () => {
  651. const menu = Menu.buildFromTemplate([
  652. { label: '1' },
  653. { label: '2' },
  654. { label: '3' }
  655. ])
  656. const item = new MenuItem({ label: 'inserted' })
  657. menu.insert(1, item)
  658. expect(menu.items[0].label).to.equal('1')
  659. expect(menu.items[1].label).to.equal('inserted')
  660. expect(menu.items[2].label).to.equal('2')
  661. expect(menu.items[3].label).to.equal('3')
  662. })
  663. })
  664. describe('Menu.append', () => {
  665. it('should add the item to the end of the menu', () => {
  666. const menu = Menu.buildFromTemplate([
  667. { label: '1' },
  668. { label: '2' },
  669. { label: '3' }
  670. ])
  671. const item = new MenuItem({ label: 'inserted' })
  672. menu.append(item)
  673. expect(menu.items[0].label).to.equal('1')
  674. expect(menu.items[1].label).to.equal('2')
  675. expect(menu.items[2].label).to.equal('3')
  676. expect(menu.items[3].label).to.equal('inserted')
  677. })
  678. })
  679. describe('Menu.popup', () => {
  680. let w: BrowserWindow
  681. let menu: Menu
  682. beforeEach(() => {
  683. w = new BrowserWindow({ show: false, width: 200, height: 200 })
  684. menu = Menu.buildFromTemplate([
  685. { label: '1' },
  686. { label: '2' },
  687. { label: '3' }
  688. ])
  689. })
  690. afterEach(async () => {
  691. menu.closePopup()
  692. menu.closePopup(w)
  693. await closeWindow(w)
  694. w = null as unknown as BrowserWindow
  695. })
  696. it('throws an error if options is not an object', () => {
  697. expect(() => {
  698. menu.popup('this is a string, not an object' as any)
  699. }).to.throw(/Options must be an object/)
  700. })
  701. it('allows for options to be optional', () => {
  702. expect(() => {
  703. menu.popup({})
  704. }).to.not.throw()
  705. })
  706. it('should emit menu-will-show event', (done) => {
  707. menu.on('menu-will-show', () => { done() })
  708. menu.popup({ window: w })
  709. })
  710. it('should emit menu-will-close event', (done) => {
  711. menu.on('menu-will-close', () => { done() })
  712. menu.popup({ window: w })
  713. // https://github.com/electron/electron/issues/19411
  714. setTimeout(() => {
  715. menu.closePopup()
  716. })
  717. })
  718. it('returns immediately', () => {
  719. const input = { window: w, x: 100, y: 101 }
  720. const output = menu.popup(input) as unknown as {x: number, y: number, browserWindow: BrowserWindow}
  721. expect(output.x).to.equal(input.x)
  722. expect(output.y).to.equal(input.y)
  723. expect(output.browserWindow).to.equal(input.window)
  724. })
  725. it('works without a given BrowserWindow and options', () => {
  726. const { browserWindow, x, y } = menu.popup({ x: 100, y: 101 }) as unknown as {x: number, y: number, browserWindow: BrowserWindow}
  727. expect(browserWindow.constructor.name).to.equal('BrowserWindow')
  728. expect(x).to.equal(100)
  729. expect(y).to.equal(101)
  730. })
  731. it('works with a given BrowserWindow, options and callback', (done) => {
  732. const { x, y } = menu.popup({
  733. window: w,
  734. x: 100,
  735. y: 101,
  736. callback: () => done()
  737. }) as unknown as {x: number, y: number}
  738. expect(x).to.equal(100)
  739. expect(y).to.equal(101)
  740. // https://github.com/electron/electron/issues/19411
  741. setTimeout(() => {
  742. menu.closePopup()
  743. })
  744. })
  745. it('works with a given BrowserWindow, no options, and a callback', (done) => {
  746. menu.popup({ window: w, callback: () => done() })
  747. // https://github.com/electron/electron/issues/19411
  748. setTimeout(() => {
  749. menu.closePopup()
  750. })
  751. })
  752. it('prevents menu from getting garbage-collected when popuping', (done) => {
  753. let menu = Menu.buildFromTemplate([{role: 'paste'}])
  754. menu.popup({ window: w })
  755. // Keep a weak reference to the menu.
  756. const v8Util = process.electronBinding('v8_util')
  757. const map = (v8Util as any).createIDWeakMap() as any
  758. map.set(0, menu)
  759. setTimeout(() => {
  760. // Do garbage collection, since |menu| is not referenced in this closure
  761. // it would be gone after next call.
  762. v8Util.requestGarbageCollectionForTesting()
  763. setTimeout(() => {
  764. // Try to receive menu from weak reference.
  765. if (map.has(0)) {
  766. map.get(0).closePopup()
  767. done()
  768. } else {
  769. done('Menu is garbage-collected while popuping')
  770. }
  771. })
  772. })
  773. })
  774. })
  775. describe('Menu.setApplicationMenu', () => {
  776. it('sets a menu', () => {
  777. const menu = Menu.buildFromTemplate([
  778. { label: '1' },
  779. { label: '2' }
  780. ])
  781. Menu.setApplicationMenu(menu)
  782. expect(Menu.getApplicationMenu()).to.not.be.null('application menu')
  783. })
  784. it('unsets a menu with null', () => {
  785. Menu.setApplicationMenu(null)
  786. expect(Menu.getApplicationMenu()).to.be.null('application menu')
  787. })
  788. ifit(process.platform !== 'darwin')('does not override menu visibility on startup', async () => {
  789. const appPath = path.join(fixturesPath, 'api', 'test-menu-visibility')
  790. const appProcess = cp.spawn(process.execPath, [appPath])
  791. let output = ''
  792. appProcess.stdout.on('data', data => { output += data })
  793. await emittedOnce(appProcess, 'close')
  794. expect(output).to.include('Window has no menu')
  795. })
  796. ifit(process.platform !== 'darwin')('does not override null menu on startup', async () => {
  797. const appPath = path.join(fixturesPath, 'api', 'test-menu-null')
  798. const appProcess = cp.spawn(process.execPath, [appPath])
  799. let output = ''
  800. appProcess.stdout.on('data', data => { output += data })
  801. await emittedOnce(appProcess, 'close')
  802. expect(output).to.include('Window has no menu')
  803. })
  804. })
  805. describe('menu accelerators', async () => {
  806. const sendRobotjsKey = (key: string, modifiers: string | string[] = [], delay = 500) => {
  807. return new Promise((resolve, reject) => {
  808. try {
  809. require('robotjs').keyTap(key, modifiers)
  810. setTimeout(() => {
  811. resolve()
  812. }, delay)
  813. } catch (e) {
  814. reject(e)
  815. }
  816. })
  817. }
  818. before(async function () {
  819. // --ci flag breaks accelerator and robotjs interaction
  820. if (isCI) {
  821. this.skip()
  822. }
  823. // before accelerator tests, use globalShortcut to test if
  824. // RobotJS is working at all
  825. let isKeyPressed = false
  826. globalShortcut.register('q', () => {
  827. isKeyPressed = true
  828. })
  829. try {
  830. await sendRobotjsKey('q')
  831. } catch (e) {
  832. this.skip()
  833. }
  834. if (!isKeyPressed) {
  835. this.skip()
  836. }
  837. globalShortcut.unregister('q')
  838. })
  839. it('should perform the specified action', async () => {
  840. let hasBeenClicked = false
  841. const menu = Menu.buildFromTemplate([
  842. {
  843. label: 'Test',
  844. submenu: [
  845. {
  846. label: 'Test Item',
  847. accelerator: 'T',
  848. click: (a, b, event) => {
  849. hasBeenClicked = true
  850. expect(event).to.deep.equal({
  851. shiftKey: false,
  852. ctrlKey: false,
  853. altKey: false,
  854. metaKey: false,
  855. triggeredByAccelerator: true
  856. })
  857. },
  858. id: 'test'
  859. }
  860. ]
  861. }
  862. ])
  863. Menu.setApplicationMenu(menu)
  864. expect(Menu.getApplicationMenu()).to.not.be.null('application menu')
  865. await sendRobotjsKey('t')
  866. expect(hasBeenClicked).to.equal(true)
  867. })
  868. it('should not activate upon clicking another key combination', async () => {
  869. let hasBeenClicked = false
  870. const menu = Menu.buildFromTemplate([
  871. {
  872. label: 'Test',
  873. submenu: [
  874. {
  875. label: 'Test Item',
  876. accelerator: 'T',
  877. click: (a, b, event) => {
  878. hasBeenClicked = true
  879. },
  880. id: 'test'
  881. }
  882. ]
  883. }
  884. ])
  885. Menu.setApplicationMenu(menu)
  886. expect(Menu.getApplicationMenu()).to.not.be.null('application menu')
  887. await sendRobotjsKey('t', 'shift')
  888. expect(hasBeenClicked).to.equal(false)
  889. })
  890. })
  891. })