api-menu-spec.ts 26 KB

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