api-menu-item-spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. const chai = require('chai')
  2. const dirtyChai = require('dirty-chai')
  3. const { remote } = require('electron')
  4. const { BrowserWindow, app, Menu, MenuItem } = remote
  5. const roles = require('../lib/browser/api/menu-item-roles')
  6. const { closeWindow } = require('./window-helpers')
  7. const { expect, assert } = chai
  8. chai.use(dirtyChai)
  9. describe('MenuItems', () => {
  10. describe('MenuItem.click', () => {
  11. it('should be called with the item object passed', done => {
  12. const menu = Menu.buildFromTemplate([{
  13. label: 'text',
  14. click: (item) => {
  15. expect(item.constructor.name).to.equal('MenuItem')
  16. expect(item.label).to.equal('text')
  17. done()
  18. }
  19. }])
  20. menu.delegate.executeCommand(menu, {}, menu.items[0].commandId)
  21. })
  22. })
  23. describe('MenuItem with checked/radio property', () => {
  24. it('clicking an checkbox item should flip the checked property', () => {
  25. const menu = Menu.buildFromTemplate([{
  26. label: 'text',
  27. type: 'checkbox'
  28. }])
  29. expect(menu.items[0].checked).to.be.false()
  30. menu.delegate.executeCommand(menu, {}, menu.items[0].commandId)
  31. expect(menu.items[0].checked).to.be.true()
  32. })
  33. it('clicking an radio item should always make checked property true', () => {
  34. const menu = Menu.buildFromTemplate([{
  35. label: 'text',
  36. type: 'radio'
  37. }])
  38. menu.delegate.executeCommand(menu, {}, menu.items[0].commandId)
  39. expect(menu.items[0].checked).to.be.true()
  40. menu.delegate.executeCommand(menu, {}, menu.items[0].commandId)
  41. expect(menu.items[0].checked).to.be.true()
  42. })
  43. describe('MenuItem group properties', () => {
  44. const template = []
  45. const findRadioGroups = (template) => {
  46. const groups = []
  47. let cur = null
  48. for (let i = 0; i <= template.length; i++) {
  49. if (cur && ((i === template.length) || (template[i].type !== 'radio'))) {
  50. cur.end = i
  51. groups.push(cur)
  52. cur = null
  53. } else if (!cur && i < template.length && template[i].type === 'radio') {
  54. cur = { begin: i }
  55. }
  56. }
  57. return groups
  58. }
  59. // returns array of checked menuitems in [begin,end)
  60. const findChecked = (menuItems, begin, end) => {
  61. const checked = []
  62. for (let i = begin; i < end; i++) {
  63. if (menuItems[i].checked) checked.push(i)
  64. }
  65. return checked
  66. }
  67. beforeEach(() => {
  68. for (let i = 0; i <= 10; i++) {
  69. template.push({
  70. label: `${i}`,
  71. type: 'radio'
  72. })
  73. }
  74. template.push({ type: 'separator' })
  75. for (let i = 12; i <= 20; i++) {
  76. template.push({
  77. label: `${i}`,
  78. type: 'radio'
  79. })
  80. }
  81. })
  82. it('at least have one item checked in each group', () => {
  83. const menu = Menu.buildFromTemplate(template)
  84. menu.delegate.menuWillShow(menu)
  85. const groups = findRadioGroups(template)
  86. groups.forEach(g => {
  87. expect(findChecked(menu.items, g.begin, g.end)).to.deep.equal([g.begin])
  88. })
  89. })
  90. it('should assign groupId automatically', () => {
  91. const menu = Menu.buildFromTemplate(template)
  92. const usedGroupIds = new Set()
  93. const groups = findRadioGroups(template)
  94. groups.forEach(g => {
  95. const groupId = menu.items[g.begin].groupId
  96. // groupId should be previously unused
  97. expect(usedGroupIds.has(groupId)).to.be.false()
  98. usedGroupIds.add(groupId)
  99. // everything in the group should have the same id
  100. for (let i = g.begin; i < g.end; ++i) {
  101. expect(menu.items[i].groupId).to.equal(groupId)
  102. }
  103. })
  104. })
  105. it("setting 'checked' should flip other items' 'checked' property", () => {
  106. const menu = Menu.buildFromTemplate(template)
  107. const groups = findRadioGroups(template)
  108. groups.forEach(g => {
  109. expect(findChecked(menu.items, g.begin, g.end)).to.deep.equal([])
  110. menu.items[g.begin].checked = true
  111. expect(findChecked(menu.items, g.begin, g.end)).to.deep.equal([g.begin])
  112. menu.items[g.end - 1].checked = true
  113. expect(findChecked(menu.items, g.begin, g.end)).to.deep.equal([g.end - 1])
  114. })
  115. })
  116. })
  117. })
  118. describe('MenuItem role execution', () => {
  119. it('does not try to execute roles without a valid role property', () => {
  120. let win = new BrowserWindow({ show: false, width: 200, height: 200 })
  121. const item = new MenuItem({ role: 'asdfghjkl' })
  122. const canExecute = roles.execute(item.role, win, win.webContents)
  123. expect(canExecute).to.be.false()
  124. closeWindow(win).then(() => { win = null })
  125. })
  126. it('executes roles with native role functions', () => {
  127. let win = new BrowserWindow({ show: false, width: 200, height: 200 })
  128. const item = new MenuItem({ role: 'reload' })
  129. const canExecute = roles.execute(item.role, win, win.webContents)
  130. expect(canExecute).to.be.true()
  131. closeWindow(win).then(() => { win = null })
  132. })
  133. it('execute roles with non-native role functions', () => {
  134. let win = new BrowserWindow({ show: false, width: 200, height: 200 })
  135. const item = new MenuItem({ role: 'resetzoom' })
  136. const canExecute = roles.execute(item.role, win, win.webContents)
  137. expect(canExecute).to.be.true()
  138. closeWindow(win).then(() => { win = null })
  139. })
  140. })
  141. describe('MenuItem command id', () => {
  142. it('cannot be overwritten', () => {
  143. const item = new MenuItem({ label: 'item' })
  144. const commandId = item.commandId
  145. expect(commandId).to.not.be.undefined()
  146. item.commandId = `${commandId}-modified`
  147. expect(item.commandId).to.equal(commandId)
  148. })
  149. })
  150. describe('MenuItem with invalid type', () => {
  151. it('throws an exception', () => {
  152. expect(() => {
  153. Menu.buildFromTemplate([{
  154. label: 'text',
  155. type: 'not-a-type'
  156. }])
  157. }).to.throw(/Unknown menu item type: not-a-type/)
  158. })
  159. })
  160. describe('MenuItem with submenu type and missing submenu', () => {
  161. it('throws an exception', () => {
  162. expect(() => {
  163. Menu.buildFromTemplate([{
  164. label: 'text',
  165. type: 'submenu'
  166. }])
  167. }).to.throw(/Invalid submenu/)
  168. })
  169. })
  170. describe('MenuItem role', () => {
  171. it('returns undefined for items without default accelerator', () => {
  172. const roleList = [
  173. 'close',
  174. 'copy',
  175. 'cut',
  176. 'forcereload',
  177. 'hide',
  178. 'hideothers',
  179. 'minimize',
  180. 'paste',
  181. 'pasteandmatchstyle',
  182. 'quit',
  183. 'redo',
  184. 'reload',
  185. 'resetzoom',
  186. 'selectall',
  187. 'toggledevtools',
  188. 'togglefullscreen',
  189. 'undo',
  190. 'zoomin',
  191. 'zoomout'
  192. ]
  193. for (const role in roleList) {
  194. const item = new MenuItem({ role })
  195. expect(item.getDefaultRoleAccelerator()).to.be.undefined()
  196. }
  197. })
  198. it('returns the correct default label', () => {
  199. const roleList = {
  200. 'close': process.platform === 'darwin' ? 'Close Window' : 'Close',
  201. 'copy': 'Copy',
  202. 'cut': 'Cut',
  203. 'forcereload': 'Force Reload',
  204. 'hide': 'Hide Electron Test',
  205. 'hideothers': 'Hide Others',
  206. 'minimize': 'Minimize',
  207. 'paste': 'Paste',
  208. 'pasteandmatchstyle': 'Paste and Match Style',
  209. 'quit': (process.platform === 'darwin') ? `Quit ${app.getName()}` : (process.platform === 'win32') ? 'Exit' : 'Quit',
  210. 'redo': 'Redo',
  211. 'reload': 'Reload',
  212. 'resetzoom': 'Actual Size',
  213. 'selectall': 'Select All',
  214. 'toggledevtools': 'Toggle Developer Tools',
  215. 'togglefullscreen': 'Toggle Full Screen',
  216. 'undo': 'Undo',
  217. 'zoomin': 'Zoom In',
  218. 'zoomout': 'Zoom Out'
  219. }
  220. for (const role in roleList) {
  221. const item = new MenuItem({ role })
  222. expect(item.label).to.equal(roleList[role])
  223. }
  224. })
  225. it('returns the correct default accelerator', () => {
  226. const roleList = {
  227. 'close': 'CommandOrControl+W',
  228. 'copy': 'CommandOrControl+C',
  229. 'cut': 'CommandOrControl+X',
  230. 'forcereload': 'Shift+CmdOrCtrl+R',
  231. 'hide': 'Command+H',
  232. 'hideothers': 'Command+Alt+H',
  233. 'minimize': 'CommandOrControl+M',
  234. 'paste': 'CommandOrControl+V',
  235. 'pasteandmatchstyle': 'Shift+CommandOrControl+V',
  236. 'quit': process.platform === 'win32' ? null : 'CommandOrControl+Q',
  237. 'redo': process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z',
  238. 'reload': 'CmdOrCtrl+R',
  239. 'resetzoom': 'CommandOrControl+0',
  240. 'selectall': 'CommandOrControl+A',
  241. 'toggledevtools': process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
  242. 'togglefullscreen': process.platform === 'darwin' ? 'Control+Command+F' : 'F11',
  243. 'undo': 'CommandOrControl+Z',
  244. 'zoomin': 'CommandOrControl+Plus',
  245. 'zoomout': 'CommandOrControl+-'
  246. }
  247. for (const role in roleList) {
  248. const item = new MenuItem({ role })
  249. expect(item.getDefaultRoleAccelerator()).to.equal(roleList[role])
  250. }
  251. })
  252. it('allows a custom accelerator and label to be set', () => {
  253. const item = new MenuItem({
  254. role: 'close',
  255. label: 'Custom Close!',
  256. accelerator: 'D'
  257. })
  258. expect(item.label).to.equal('Custom Close!')
  259. expect(item.accelerator).to.equal('D')
  260. expect(item.getDefaultRoleAccelerator()).to.equal('CommandOrControl+W')
  261. })
  262. })
  263. describe('MenuItem editMenu', () => {
  264. it('includes a default submenu layout when submenu is empty', () => {
  265. const item = new MenuItem({ role: 'editMenu' })
  266. expect(item.label).to.equal('Edit')
  267. expect(item.submenu.items[0].role).to.equal('undo')
  268. expect(item.submenu.items[1].role).to.equal('redo')
  269. expect(item.submenu.items[2].type).to.equal('separator')
  270. expect(item.submenu.items[3].role).to.equal('cut')
  271. expect(item.submenu.items[4].role).to.equal('copy')
  272. expect(item.submenu.items[5].role).to.equal('paste')
  273. if (process.platform === 'darwin') {
  274. expect(item.submenu.items[6].role).to.equal('pasteandmatchstyle')
  275. expect(item.submenu.items[7].role).to.equal('delete')
  276. expect(item.submenu.items[8].role).to.equal('selectall')
  277. }
  278. if (process.platform === 'win32') {
  279. expect(item.submenu.items[6].role).to.equal('delete')
  280. expect(item.submenu.items[7].type).to.equal('separator')
  281. expect(item.submenu.items[8].role).to.equal('selectall')
  282. }
  283. })
  284. it('overrides default layout when submenu is specified', () => {
  285. const item = new MenuItem({
  286. role: 'editMenu',
  287. submenu: [{
  288. role: 'close'
  289. }]
  290. })
  291. expect(item.label).to.equal('Edit')
  292. expect(item.submenu.items[0].role).to.equal('close')
  293. })
  294. })
  295. describe('MenuItem windowMenu', () => {
  296. it('includes a default submenu layout when submenu is empty', () => {
  297. const item = new MenuItem({ role: 'windowMenu' })
  298. expect(item.label).to.equal('Window')
  299. expect(item.submenu.items[0].role).to.equal('minimize')
  300. expect(item.submenu.items[1].role).to.equal('close')
  301. if (process.platform === 'darwin') {
  302. expect(item.submenu.items[2].type).to.equal('separator')
  303. expect(item.submenu.items[3].role).to.equal('front')
  304. }
  305. })
  306. it('overrides default layout when submenu is specified', () => {
  307. const item = new MenuItem({
  308. role: 'windowMenu',
  309. submenu: [{ role: 'copy' }]
  310. })
  311. expect(item.label).to.equal('Window')
  312. expect(item.submenu.items[0].role).to.equal('copy')
  313. })
  314. })
  315. describe('MenuItem with custom properties in constructor', () => {
  316. it('preserves the custom properties', () => {
  317. const template = [{
  318. label: 'menu 1',
  319. customProp: 'foo',
  320. submenu: []
  321. }]
  322. const menu = Menu.buildFromTemplate(template)
  323. menu.items[0].submenu.append(new MenuItem({
  324. label: 'item 1',
  325. customProp: 'bar',
  326. overrideProperty: 'oops not allowed'
  327. }))
  328. expect(menu.items[0].customProp).to.equal('foo')
  329. expect(menu.items[0].submenu.items[0].label).to.equal('item 1')
  330. expect(menu.items[0].submenu.items[0].customProp).to.equal('bar')
  331. expect(menu.items[0].submenu.items[0].overrideProperty).to.be.a('function')
  332. })
  333. })
  334. describe('MenuItem accelerators', () => {
  335. const isDarwin = () => {
  336. return (process.platform === 'darwin')
  337. }
  338. it('should display modifiers correctly for simple keys', () => {
  339. const menu = Menu.buildFromTemplate([
  340. { label: 'text', accelerator: 'CmdOrCtrl+A' },
  341. { label: 'text', accelerator: 'Shift+A' },
  342. { label: 'text', accelerator: 'Alt+A' }
  343. ])
  344. assert.strictEqual(menu.getAcceleratorTextAt(0),
  345. isDarwin() ? '⌘A' : 'Ctrl+A')
  346. assert.strictEqual(menu.getAcceleratorTextAt(1),
  347. isDarwin() ? '⇧A' : 'Shift+A')
  348. assert.strictEqual(menu.getAcceleratorTextAt(2),
  349. isDarwin() ? '⌥A' : 'Alt+A')
  350. })
  351. it('should display modifiers correctly for special keys', () => {
  352. const menu = Menu.buildFromTemplate([
  353. { label: 'text', accelerator: 'CmdOrCtrl+Tab' },
  354. { label: 'text', accelerator: 'Shift+Tab' },
  355. { label: 'text', accelerator: 'Alt+Tab' }
  356. ])
  357. assert.strictEqual(menu.getAcceleratorTextAt(0),
  358. isDarwin() ? '⌘⇥\u0000' : 'Ctrl+Tab')
  359. assert.strictEqual(menu.getAcceleratorTextAt(1),
  360. isDarwin() ? '⇧⇥\u0000' : 'Shift+Tab')
  361. assert.strictEqual(menu.getAcceleratorTextAt(2),
  362. isDarwin() ? '⌥⇥\u0000' : 'Alt+Tab')
  363. })
  364. it('should not display modifiers twice', () => {
  365. const menu = Menu.buildFromTemplate([
  366. { label: 'text', accelerator: 'Shift+Shift+A' },
  367. { label: 'text', accelerator: 'Shift+Shift+Tab' }
  368. ])
  369. assert.strictEqual(menu.getAcceleratorTextAt(0),
  370. isDarwin() ? '⇧A' : 'Shift+A')
  371. assert.strictEqual(menu.getAcceleratorTextAt(1),
  372. isDarwin() ? '⇧⇥\u0000' : 'Shift+Tab')
  373. })
  374. it('should display correctly for edge cases', () => {
  375. const menu = Menu.buildFromTemplate([
  376. { label: 'text', accelerator: 'Control+Shift+=' },
  377. { label: 'text', accelerator: 'Control+Plus' }
  378. ])
  379. assert.strictEqual(menu.getAcceleratorTextAt(0),
  380. isDarwin() ? '⌃⇧=' : 'Ctrl+Shift+=')
  381. assert.strictEqual(menu.getAcceleratorTextAt(1),
  382. isDarwin() ? '⌃⇧=' : 'Ctrl+Shift+=')
  383. })
  384. })
  385. })