api-menu-spec.ts 25 KB

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