api-menu-spec.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. const assert = require('assert')
  2. const {ipcRenderer, remote} = require('electron')
  3. const {BrowserWindow, Menu, MenuItem} = remote
  4. const {sortMenuItems} = require('../lib/browser/api/menu-utils')
  5. const {closeWindow} = require('./window-helpers')
  6. describe('Menu module', () => {
  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. }
  14. ])
  15. assert.equal(menu.items[0].extra, 'field')
  16. })
  17. it('does not modify the specified template', () => {
  18. const template = [{label: 'text', submenu: [{label: 'sub'}]}]
  19. const result = ipcRenderer.sendSync('eval', `const template = [{label: 'text', submenu: [{label: 'sub'}]}]\nrequire('electron').Menu.buildFromTemplate(template)\ntemplate`)
  20. assert.deepStrictEqual(result, template)
  21. })
  22. it('does not throw exceptions for undefined/null values', () => {
  23. assert.doesNotThrow(() => {
  24. Menu.buildFromTemplate([
  25. {
  26. label: 'text',
  27. accelerator: undefined
  28. },
  29. {
  30. label: 'text again',
  31. accelerator: null
  32. }
  33. ])
  34. })
  35. })
  36. describe('Menu sorting and building', () => {
  37. describe('sorts groups', () => {
  38. it('does a simple sort', () => {
  39. const items = [
  40. {
  41. label: 'two',
  42. id: '2',
  43. afterGroupContaining: ['1'] },
  44. { type: 'separator' },
  45. {
  46. id: '1',
  47. label: 'one'
  48. }
  49. ]
  50. const expected = [
  51. {
  52. id: '1',
  53. label: 'one'
  54. },
  55. { type: 'separator' },
  56. {
  57. id: '2',
  58. label: 'two',
  59. afterGroupContaining: ['1']
  60. }
  61. ]
  62. assert.deepEqual(sortMenuItems(items), expected)
  63. })
  64. it('resolves cycles by ignoring things that conflict', () => {
  65. const items = [
  66. {
  67. id: '2',
  68. label: 'two',
  69. afterGroupContaining: ['1']
  70. },
  71. { type: 'separator' },
  72. {
  73. id: '1',
  74. label: 'one',
  75. afterGroupContaining: ['2']
  76. }
  77. ]
  78. const expected = [
  79. {
  80. id: '1',
  81. label: 'one',
  82. afterGroupContaining: ['2']
  83. },
  84. { type: 'separator' },
  85. {
  86. id: '2',
  87. label: 'two',
  88. afterGroupContaining: ['1']
  89. }
  90. ]
  91. assert.deepEqual(sortMenuItems(items), expected)
  92. })
  93. it('ignores references to commands that do not exist', () => {
  94. const items = [
  95. {
  96. id: '1',
  97. label: 'one'
  98. },
  99. { type: 'separator' },
  100. {
  101. id: '2',
  102. label: 'two',
  103. afterGroupContaining: ['does-not-exist']
  104. }
  105. ]
  106. const expected = [
  107. {
  108. id: '1',
  109. label: 'one'
  110. },
  111. { type: 'separator' },
  112. {
  113. id: '2',
  114. label: 'two',
  115. afterGroupContaining: ['does-not-exist']
  116. }
  117. ]
  118. assert.deepEqual(sortMenuItems(items), expected)
  119. })
  120. it('only respects the first matching [before|after]GroupContaining rule in a given group', () => {
  121. const items = [
  122. {
  123. id: '1',
  124. label: 'one'
  125. },
  126. { type: 'separator' },
  127. {
  128. id: '3',
  129. label: 'three',
  130. beforeGroupContaining: ['1']
  131. },
  132. {
  133. id: '4',
  134. label: 'four',
  135. afterGroupContaining: ['2']
  136. },
  137. { type: 'separator' },
  138. {
  139. id: '2',
  140. label: 'two'
  141. }
  142. ]
  143. const expected = [
  144. {
  145. id: '3',
  146. label: 'three',
  147. beforeGroupContaining: ['1']
  148. },
  149. {
  150. id: '4',
  151. label: 'four',
  152. afterGroupContaining: ['2']
  153. },
  154. { type: 'separator' },
  155. {
  156. id: '1',
  157. label: 'one'
  158. },
  159. { type: 'separator' },
  160. {
  161. id: '2',
  162. label: 'two'
  163. }
  164. ]
  165. assert.deepEqual(sortMenuItems(items), expected)
  166. })
  167. })
  168. describe('moves an item to a different group by merging groups', () => {
  169. it('can move a group of one item', () => {
  170. const items = [
  171. {
  172. id: '1',
  173. label: 'one'
  174. },
  175. { type: 'separator' },
  176. {
  177. id: '2',
  178. label: 'two'
  179. },
  180. { type: 'separator' },
  181. {
  182. id: '3',
  183. label: 'three',
  184. after: ['1']
  185. },
  186. { type: 'separator' }
  187. ]
  188. const expected = [
  189. {
  190. id: '1',
  191. label: 'one'
  192. },
  193. {
  194. id: '3',
  195. label: 'three',
  196. after: ['1']
  197. },
  198. { type: 'separator' },
  199. {
  200. id: '2',
  201. label: 'two'
  202. }
  203. ]
  204. assert.deepEqual(sortMenuItems(items), expected)
  205. })
  206. it("moves all items in the moving item's group", () => {
  207. const items = [
  208. {
  209. id: '1',
  210. label: 'one'
  211. },
  212. { type: 'separator' },
  213. {
  214. id: '2',
  215. label: 'two'
  216. },
  217. { type: 'separator' },
  218. {
  219. id: '3',
  220. label: 'three',
  221. after: ['1']
  222. },
  223. {
  224. id: '4',
  225. label: 'four'
  226. },
  227. { type: 'separator' }
  228. ]
  229. const expected = [
  230. {
  231. id: '1',
  232. label: 'one'
  233. },
  234. {
  235. id: '3',
  236. label: 'three',
  237. after: ['1']
  238. },
  239. {
  240. id: '4',
  241. label: 'four'
  242. },
  243. { type: 'separator' },
  244. {
  245. id: '2',
  246. label: 'two'
  247. }
  248. ]
  249. assert.deepEqual(sortMenuItems(items), expected)
  250. })
  251. it("ignores positions relative to commands that don't exist", () => {
  252. const items = [
  253. {
  254. id: '1',
  255. label: 'one'
  256. },
  257. { type: 'separator' },
  258. {
  259. id: '2',
  260. label: 'two'
  261. },
  262. { type: 'separator' },
  263. {
  264. id: '3',
  265. label: 'three',
  266. after: ['does-not-exist']
  267. },
  268. {
  269. id: '4',
  270. label: 'four',
  271. after: ['1']
  272. },
  273. { type: 'separator' }
  274. ]
  275. const expected = [
  276. {
  277. id: '1',
  278. label: 'one'
  279. },
  280. {
  281. id: '3',
  282. label: 'three',
  283. after: ['does-not-exist']
  284. },
  285. {
  286. id: '4',
  287. label: 'four',
  288. after: ['1']
  289. },
  290. { type: 'separator' },
  291. {
  292. id: '2',
  293. label: 'two'
  294. }
  295. ]
  296. assert.deepEqual(sortMenuItems(items), expected)
  297. })
  298. it('can handle recursive group merging', () => {
  299. const items = [
  300. {
  301. id: '1',
  302. label: 'one',
  303. after: ['3']
  304. },
  305. {
  306. id: '2',
  307. label: 'two',
  308. before: ['1']
  309. },
  310. {
  311. id: '3',
  312. label: 'three'
  313. }
  314. ]
  315. const expected = [
  316. {
  317. id: '3',
  318. label: 'three'
  319. },
  320. {
  321. id: '2',
  322. label: 'two',
  323. before: ['1']
  324. },
  325. {
  326. id: '1',
  327. label: 'one',
  328. after: ['3']
  329. }
  330. ]
  331. assert.deepEqual(sortMenuItems(items), expected)
  332. })
  333. it('can merge multiple groups when given a list of before/after commands', () => {
  334. const items = [
  335. {
  336. id: '1',
  337. label: 'one'
  338. },
  339. { type: 'separator' },
  340. {
  341. id: '2',
  342. label: 'two'
  343. },
  344. { type: 'separator' },
  345. {
  346. id: '3',
  347. label: 'three',
  348. after: ['1', '2']
  349. }
  350. ]
  351. const expected = [
  352. {
  353. id: '2',
  354. label: 'two'
  355. },
  356. {
  357. id: '1',
  358. label: 'one'
  359. },
  360. {
  361. id: '3',
  362. label: 'three',
  363. after: ['1', '2']
  364. }
  365. ]
  366. assert.deepEqual(sortMenuItems(items), expected)
  367. })
  368. it('can merge multiple groups based on both before/after commands', () => {
  369. const items = [
  370. {
  371. id: '1',
  372. label: 'one'
  373. },
  374. { type: 'separator' },
  375. {
  376. id: '2',
  377. label: 'two'
  378. },
  379. { type: 'separator' },
  380. {
  381. id: '3',
  382. label: 'three',
  383. after: ['1'],
  384. before: ['2']
  385. }
  386. ]
  387. const expected = [
  388. {
  389. id: '1',
  390. label: 'one'
  391. },
  392. {
  393. id: '3',
  394. label: 'three',
  395. after: ['1'],
  396. before: ['2']
  397. },
  398. {
  399. id: '2',
  400. label: 'two'
  401. }
  402. ]
  403. assert.deepEqual(sortMenuItems(items), expected)
  404. })
  405. })
  406. it('should position before existing item', () => {
  407. const menu = Menu.buildFromTemplate([
  408. {
  409. id: '2',
  410. label: 'two'
  411. }, {
  412. id: '3',
  413. label: 'three'
  414. }, {
  415. id: '1',
  416. label: 'one',
  417. before: ['2']
  418. }
  419. ])
  420. assert.equal(menu.items[0].label, 'one')
  421. assert.equal(menu.items[1].label, 'two')
  422. assert.equal(menu.items[2].label, 'three')
  423. })
  424. it('should position after existing item', () => {
  425. const menu = Menu.buildFromTemplate([
  426. {
  427. id: '2',
  428. label: 'two',
  429. after: ['1']
  430. },
  431. {
  432. id: '1',
  433. label: 'one'
  434. }, {
  435. id: '3',
  436. label: 'three'
  437. }
  438. ])
  439. assert.equal(menu.items[0].label, 'one')
  440. assert.equal(menu.items[1].label, 'two')
  441. assert.equal(menu.items[2].label, 'three')
  442. })
  443. it('should filter excess menu separators', () => {
  444. const menuOne = Menu.buildFromTemplate([
  445. {
  446. type: 'separator'
  447. }, {
  448. label: 'a'
  449. }, {
  450. label: 'b'
  451. }, {
  452. label: 'c'
  453. }, {
  454. type: 'separator'
  455. }
  456. ])
  457. assert.equal(menuOne.items.length, 3)
  458. assert.equal(menuOne.items[0].label, 'a')
  459. assert.equal(menuOne.items[1].label, 'b')
  460. assert.equal(menuOne.items[2].label, 'c')
  461. const menuTwo = Menu.buildFromTemplate([
  462. {
  463. type: 'separator'
  464. }, {
  465. type: 'separator'
  466. }, {
  467. label: 'a'
  468. }, {
  469. label: 'b'
  470. }, {
  471. label: 'c'
  472. }, {
  473. type: 'separator'
  474. }, {
  475. type: 'separator'
  476. }
  477. ])
  478. assert.equal(menuTwo.items.length, 3)
  479. assert.equal(menuTwo.items[0].label, 'a')
  480. assert.equal(menuTwo.items[1].label, 'b')
  481. assert.equal(menuTwo.items[2].label, 'c')
  482. })
  483. it('should continue inserting items at next index when no specifier is present', () => {
  484. const menu = Menu.buildFromTemplate([
  485. {
  486. id: '2',
  487. label: 'two'
  488. }, {
  489. id: '3',
  490. label: 'three'
  491. }, {
  492. id: '4',
  493. label: 'four'
  494. }, {
  495. id: '5',
  496. label: 'five'
  497. }, {
  498. id: '1',
  499. label: 'one',
  500. before: ['2']
  501. }
  502. ])
  503. assert.equal(menu.items[0].label, 'one')
  504. assert.equal(menu.items[1].label, 'two')
  505. assert.equal(menu.items[2].label, 'three')
  506. assert.equal(menu.items[3].label, 'four')
  507. assert.equal(menu.items[4].label, 'five')
  508. })
  509. })
  510. })
  511. describe('Menu.getMenuItemById', () => {
  512. it('should return the item with the given id', () => {
  513. const menu = Menu.buildFromTemplate([
  514. {
  515. label: 'View',
  516. submenu: [
  517. {
  518. label: 'Enter Fullscreen',
  519. accelerator: 'ControlCommandF',
  520. id: 'fullScreen'
  521. }
  522. ]
  523. }
  524. ])
  525. const fsc = menu.getMenuItemById('fullScreen')
  526. assert.equal(menu.items[0].submenu.items[0], fsc)
  527. })
  528. })
  529. describe('Menu.insert', () => {
  530. it('should store item in @items by its index', () => {
  531. const menu = Menu.buildFromTemplate([
  532. {label: '1'},
  533. {label: '2'},
  534. {label: '3'}
  535. ])
  536. const item = new MenuItem({ label: 'inserted' })
  537. menu.insert(1, item)
  538. assert.equal(menu.items[0].label, '1')
  539. assert.equal(menu.items[1].label, 'inserted')
  540. assert.equal(menu.items[2].label, '2')
  541. assert.equal(menu.items[3].label, '3')
  542. })
  543. })
  544. describe('Menu.append', () => {
  545. it('should add the item to the end of the menu', () => {
  546. const menu = Menu.buildFromTemplate([
  547. {label: '1'},
  548. {label: '2'},
  549. {label: '3'}
  550. ])
  551. const item = new MenuItem({ label: 'inserted' })
  552. menu.append(item)
  553. assert.equal(menu.items[0].label, '1')
  554. assert.equal(menu.items[1].label, '2')
  555. assert.equal(menu.items[2].label, '3')
  556. assert.equal(menu.items[3].label, 'inserted')
  557. })
  558. })
  559. describe('Menu.popup', () => {
  560. let w = null
  561. let menu
  562. beforeEach(() => {
  563. w = new BrowserWindow({show: false, width: 200, height: 200})
  564. menu = Menu.buildFromTemplate([
  565. {label: '1'},
  566. {label: '2'},
  567. {label: '3'}
  568. ])
  569. })
  570. afterEach(() => {
  571. menu.closePopup()
  572. menu.closePopup(w)
  573. return closeWindow(w).then(() => { w = null })
  574. })
  575. it('throws an error if options is not an object', () => {
  576. assert.throws(() => {
  577. menu.popup()
  578. }, /Options must be an object/)
  579. })
  580. it('should emit menu-will-show event', (done) => {
  581. menu.on('menu-will-show', () => { done() })
  582. menu.popup({window: w})
  583. })
  584. it('should emit menu-will-close event', (done) => {
  585. menu.on('menu-will-close', () => { done() })
  586. menu.popup({window: w})
  587. menu.closePopup()
  588. })
  589. it('returns immediately', () => {
  590. const input = {window: w, x: 100, y: 101}
  591. const output = menu.popup(input)
  592. assert.equal(output.x, input.x)
  593. assert.equal(output.y, input.y)
  594. assert.equal(output.browserWindow, input.window)
  595. })
  596. it('works without a given BrowserWindow and options', () => {
  597. const {browserWindow, x, y} = menu.popup({x: 100, y: 101})
  598. assert.equal(browserWindow.constructor.name, 'BrowserWindow')
  599. assert.equal(x, 100)
  600. assert.equal(y, 101)
  601. })
  602. it('works with a given BrowserWindow, options and callback', (done) => {
  603. const {x, y} = menu.popup({
  604. window: w,
  605. x: 100,
  606. y: 101,
  607. callback: () => done()
  608. })
  609. assert.equal(x, 100)
  610. assert.equal(y, 101)
  611. menu.closePopup()
  612. })
  613. it('works with a given BrowserWindow, no options, and a callback', (done) => {
  614. menu.popup({window: w, callback: () => done()})
  615. menu.closePopup()
  616. })
  617. })
  618. describe('Menu.setApplicationMenu', () => {
  619. it('sets a menu', () => {
  620. const menu = Menu.buildFromTemplate([
  621. {label: '1'},
  622. {label: '2'}
  623. ])
  624. Menu.setApplicationMenu(menu)
  625. assert.notEqual(Menu.getApplicationMenu(), null)
  626. })
  627. it('unsets a menu with null', () => {
  628. Menu.setApplicationMenu(null)
  629. assert.equal(Menu.getApplicationMenu(), null)
  630. })
  631. })
  632. })