api-menu-item-spec.js 12 KB

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