From 0e6bde12faeadeb7425c3d337e10cb0962cd9eb2 Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Mon, 7 Jan 2019 21:52:05 +0100 Subject: [PATCH] feat: use default-app behavior in packaged apps (default menu / window-all-closed handling) --- default_app/main.js | 17 +- default_app/menu.js | 168 ------------------ docs/api/menu-item.md | 3 + docs/api/menu.md | 98 +++++----- docs/api/process.md | 11 +- filenames.gni | 2 +- lib/browser/api/menu-item-roles.js | 150 +++++++++------- lib/browser/api/menu.js | 7 +- lib/browser/default-menu.js | 49 +++++ lib/browser/init.js | 18 +- spec/api-app-spec.js | 60 +++++-- spec/api-menu-item-spec.js | 97 +++++++++- spec/fixtures/api/default-menu/main.js | 20 +++ spec/fixtures/api/default-menu/package.json | 4 + spec/fixtures/api/window-all-closed/main.js | 20 +++ .../api/window-all-closed/package.json | 4 + 16 files changed, 412 insertions(+), 316 deletions(-) delete mode 100644 default_app/menu.js create mode 100644 lib/browser/default-menu.js create mode 100644 spec/fixtures/api/default-menu/main.js create mode 100644 spec/fixtures/api/default-menu/package.json create mode 100644 spec/fixtures/api/window-all-closed/main.js create mode 100644 spec/fixtures/api/window-all-closed/package.json diff --git a/default_app/main.js b/default_app/main.js index 7dc7c847340f8..b6cdaf00f6733 100644 --- a/default_app/main.js +++ b/default_app/main.js @@ -5,8 +5,6 @@ const Module = require('module') const path = require('path') const url = require('url') -const { setDefaultApplicationMenu } = require('./menu') - // Parse command line options. const argv = process.argv.slice(1) @@ -57,18 +55,6 @@ if (nextArgIsRequire) { process.exit(1) } -// Quit when all windows are closed and no other one is listening to this. -app.on('window-all-closed', () => { - if (app.listeners('window-all-closed').length === 1 && !option.interactive) { - app.quit() - } -}) - -// Create default menu. -app.once('ready', () => { - setDefaultApplicationMenu() -}) - // Set up preload modules if (option.modules.length > 0) { Module._preloadModules(option.modules) @@ -140,6 +126,9 @@ function startRepl () { process.exit(1) } + // prevent quitting + app.on('window-all-closed', () => {}) + const repl = require('repl') repl.start('> ').on('exit', () => { process.exit(0) diff --git a/default_app/menu.js b/default_app/menu.js deleted file mode 100644 index 6dd37b2d32875..0000000000000 --- a/default_app/menu.js +++ /dev/null @@ -1,168 +0,0 @@ -const { shell, Menu } = require('electron') - -const setDefaultApplicationMenu = () => { - if (Menu.getApplicationMenu()) return - - const template = [ - { - label: 'Edit', - submenu: [ - { - role: 'undo' - }, - { - role: 'redo' - }, - { - type: 'separator' - }, - { - role: 'cut' - }, - { - role: 'copy' - }, - { - role: 'paste' - }, - { - role: 'pasteandmatchstyle' - }, - { - role: 'delete' - }, - { - role: 'selectall' - } - ] - }, - { - label: 'View', - submenu: [ - { - role: 'reload' - }, - { - role: 'forcereload' - }, - { - role: 'toggledevtools' - }, - { - type: 'separator' - }, - { - role: 'resetzoom' - }, - { - role: 'zoomin' - }, - { - role: 'zoomout' - }, - { - type: 'separator' - }, - { - role: 'togglefullscreen' - } - ] - }, - { - role: 'windowMenu' - }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click () { - shell.openExternal('https://electronjs.org') - } - }, - { - label: 'Documentation', - click () { - shell.openExternal( - `https://github.com/electron/electron/tree/v${process.versions.electron}/docs#readme` - ) - } - }, - { - label: 'Community Discussions', - click () { - shell.openExternal('https://discuss.atom.io/c/electron') - } - }, - { - label: 'Search Issues', - click () { - shell.openExternal('https://github.com/electron/electron/issues') - } - } - ] - } - ] - - if (process.platform === 'darwin') { - template.unshift({ - label: 'Electron', - submenu: [ - { - role: 'about' - }, - { - type: 'separator' - }, - { - role: 'services' - }, - { - type: 'separator' - }, - { - role: 'hide' - }, - { - role: 'hideothers' - }, - { - role: 'unhide' - }, - { - type: 'separator' - }, - { - role: 'quit' - } - ] - }) - template[1].submenu.push({ - type: 'separator' - }, { - label: 'Speech', - submenu: [ - { - role: 'startspeaking' - }, - { - role: 'stopspeaking' - } - ] - }) - } else { - template.unshift({ - label: 'File', - submenu: [{ - role: 'quit' - }] - }) - } - - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) -} - -module.exports = { - setDefaultApplicationMenu -} diff --git a/docs/api/menu-item.md b/docs/api/menu-item.md index 5c72b45fe1762..cc924a92b9e0a 100644 --- a/docs/api/menu-item.md +++ b/docs/api/menu-item.md @@ -82,11 +82,14 @@ The `role` property can have following values: * `resetZoom` - Reset the focused page's zoom level to the original size. * `zoomIn` - Zoom in the focused page by 10%. * `zoomOut` - Zoom out the focused page by 10%. +* `fileMenu` - Whole default "File" menu (Close / Quit) * `editMenu` - Whole default "Edit" menu (Undo, Copy, etc.). +* `viewMenu` - Whole default "View" menu (Reload, Toggle Developer Tools, etc.) * `windowMenu` - Whole default "Window" menu (Minimize, Close, etc.). The following additional roles are available on _macOS_: +* `appMenu` - Whole default "App" menu (About, Services, etc.) * `about` - Map to the `orderFrontStandardAboutPanel` action. * `hide` - Map to the `hide` action. * `hideOthers` - Map to the `hideOtherApplications` action. diff --git a/docs/api/menu.md b/docs/api/menu.md index 0363ffdb49506..b69d3a9dfc235 100644 --- a/docs/api/menu.md +++ b/docs/api/menu.md @@ -30,6 +30,9 @@ effect on macOS. **Note:** This API has to be called after the `ready` event of `app` module. +**Note:** Default menu is created automatically if the app does not set one. +You can also set `process.noDefaultMenu` to `true` to disable this behavior. + #### `Menu.getApplicationMenu()` Returns `Menu | null` - The application menu, if set, or `null`, if not set. @@ -158,6 +161,29 @@ simple template API: const { app, Menu } = require('electron') const template = [ + // { role: 'appMenu' } + process.platform === 'darwin' ? { + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + } : null, + // { role: 'fileMenu' } + { + label: 'File', + submenu: [ + isMac ? { role: 'close' } : { role: 'quit' } + ] + }, + // { role: 'editMenu' } { label: 'Edit', submenu: [ @@ -167,11 +193,26 @@ const template = [ { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' } + ...(isMac ? [ + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' } + ] + } + ] : [ + { role: 'delete' }, + { type: 'separator' }, + { role: 'selectAll' } + ]) ] }, + // { role: 'viewMenu' } { label: 'View', submenu: [ @@ -186,11 +227,20 @@ const template = [ { role: 'togglefullscreen' } ] }, + // { role: 'windowMenu' } { - role: 'window', + label: 'Window', submenu: [ { role: 'minimize' }, - { role: 'close' } + { role: 'zoom' }, + ...(isMac ? [ + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, + { role: 'window' } + ] : [ + { role: 'close' } + ]) ] }, { @@ -204,44 +254,6 @@ const template = [ } ] -if (process.platform === 'darwin') { - template.unshift({ - label: app.getName(), - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - }) - - // Edit menu - template[1].submenu.push( - { type: 'separator' }, - { - label: 'Speech', - submenu: [ - { role: 'startspeaking' }, - { role: 'stopspeaking' } - ] - } - ) - - // Window menu - template[3].submenu = [ - { role: 'close' }, - { role: 'minimize' }, - { role: 'zoom' }, - { type: 'separator' }, - { role: 'front' } - ] -} - const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) ``` diff --git a/docs/api/process.md b/docs/api/process.md index 9513c15cf5483..2aaa7e181867c 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -69,6 +69,11 @@ A `Boolean`. For Mac App Store build, this property is `true`, for other builds A `Boolean` that controls ASAR support inside your application. Setting this to `true` will disable the support for `asar` archives in Node's built-in modules. +### `process.noDefaultMenu` + +A `Boolean` that controls whether the default menu is created when no menu is created +by the app. + ### `process.noDeprecation` A `Boolean` that controls whether or not deprecation warnings are printed to `stderr`. @@ -78,7 +83,7 @@ instead of the `--no-deprecation` command line flag. ### `process.enablePromiseAPIs` A `Boolean` that controls whether or not deprecation warnings are printed to `stderr` when -formerly callback-based APIs converted to Promises are invoked using callbacks. Setting this to `true` +formerly callback-based APIs converted to Promises are invoked using callbacks. Setting this to `true` will enable deprecation warnings. ### `process.resourcesPath` @@ -168,7 +173,7 @@ Returns an object with V8 heap statistics. Note that all statistics are reported Returns `Object`: -* `residentSet` Integer _Linux_ and _Windows_ - The amount of memory +* `residentSet` Integer _Linux_ and _Windows_ - The amount of memory currently pinned to actual physical RAM in Kilobytes. * `private` Integer - The amount of memory not shared by other processes, such as JS heap or HTML content in Kilobytes. @@ -179,7 +184,7 @@ Returns an object giving memory usage statistics about the current process. Note that all statistics are reported in Kilobytes. This api should be called after app ready. -Chromium does not provide `residentSet` value for macOS. This is because macOS +Chromium does not provide `residentSet` value for macOS. This is because macOS performs in-memory compression of pages that haven't been recently used. As a result the resident set size value is not what one would expect. `private` memory is more representative of the actual pre-compression memory usage of the process diff --git a/filenames.gni b/filenames.gni index 3b93b6f70dbb9..52563106d4807 100644 --- a/filenames.gni +++ b/filenames.gni @@ -40,6 +40,7 @@ filenames = { "lib/browser/api/web-contents.js", "lib/browser/api/web-contents-view.js", "lib/browser/chrome-extension.js", + "lib/browser/default-menu.js", "lib/browser/guest-view-manager.js", "lib/browser/guest-window-manager.js", "lib/browser/init.js", @@ -96,7 +97,6 @@ filenames = { "default_app/icon.png", "default_app/index.html", "default_app/main.js", - "default_app/menu.js", "default_app/package.json", "default_app/renderer.js", "default_app/styles.css", diff --git a/lib/browser/api/menu-item-roles.js b/lib/browser/api/menu-item-roles.js index 9162bc1e68964..192f993b501da 100644 --- a/lib/browser/api/menu-item-roles.js +++ b/lib/browser/api/menu-item-roles.js @@ -2,14 +2,18 @@ const { app } = require('electron') +const isMac = process.platform === 'darwin' +const isWindows = process.platform === 'win32' +const isLinux = process.platform === 'linux' + const roles = { about: { get label () { - return process.platform === 'linux' ? 'About' : `About ${app.getName()}` + return isLinux ? 'About' : `About ${app.getName()}` } }, close: { - label: process.platform === 'darwin' ? 'Close Window' : 'Close', + label: isMac ? 'Close Window' : 'Close', accelerator: 'CommandOrControl+W', windowMethod: 'close' }, @@ -78,12 +82,12 @@ const roles = { default: return 'Quit' } }, - accelerator: process.platform === 'win32' ? null : 'CommandOrControl+Q', + accelerator: isWindows ? null : 'CommandOrControl+Q', appMethod: 'quit' }, redo: { label: 'Redo', - accelerator: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z', + accelerator: isWindows ? 'Control+Y' : 'Shift+CommandOrControl+Z', webContentsMethod: 'redo' }, reload: { @@ -122,13 +126,13 @@ const roles = { }, toggledevtools: { label: 'Toggle Developer Tools', - accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', + accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I', nonNativeMacOSRole: true, windowMethod: 'toggleDevTools' }, togglefullscreen: { label: 'Toggle Full Screen', - accelerator: process.platform === 'darwin' ? 'Control+Command+F' : 'F11', + accelerator: isMac ? 'Control+Command+F' : 'F11', windowMethod: (window) => { window.setFullScreen(!window.isFullScreen()) } @@ -167,79 +171,95 @@ const roles = { }) } }, - // Edit submenu (should fit both Mac & Windows) + // App submenu should be used for Mac only + appmenu: { + get label () { + return app.getName() + }, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + // File submenu + filemenu: { + label: 'File', + submenu: [ + isMac ? { role: 'close' } : { role: 'quit' } + ] + }, + // Edit submenu editmenu: { label: 'Edit', submenu: [ - { - role: 'undo' - }, - { - role: 'redo' - }, - { - type: 'separator' - }, - { - role: 'cut' - }, - { - role: 'copy' - }, - { - role: 'paste' - }, - - process.platform === 'darwin' ? { - role: 'pasteAndMatchStyle' - } : null, - - { - role: 'delete' - }, - - process.platform === 'win32' ? { - type: 'separator' - } : null, - - { - role: 'selectAll' - } + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...(isMac ? [ + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' } + ] + } + ] : [ + { role: 'delete' }, + { type: 'separator' }, + { role: 'selectAll' } + ]) ] }, - - // Window submenu should be used for Mac only + // View submenu + viewmenu: { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin' }, + { role: 'zoomout' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + // Window submenu windowmenu: { label: 'Window', submenu: [ - { - role: 'minimize' - }, - { - role: 'zoom' - }, - process.platform !== 'darwin' ? { - label: 'close' - } : null, - process.platform === 'darwin' ? { - type: 'separator' - } : null, - process.platform === 'darwin' ? { - role: 'front' - } : null, - process.platform === 'darwin' ? { - type: 'separator' - } : null, - process.platform === 'darwin' ? { - role: 'window' - } : null + { role: 'minimize' }, + { role: 'zoom' }, + ...(isMac ? [ + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, + { role: 'window' } + ] : [ + { role: 'close' } + ]) ] } } const canExecuteRole = (role) => { if (!roles.hasOwnProperty(role)) return false - if (process.platform !== 'darwin') return true + if (!isMac) return true // macOS handles all roles natively except for a few return roles[role].nonNativeMacOSRole diff --git a/lib/browser/api/menu.js b/lib/browser/api/menu.js index 14fcaba0a2dd7..98ba6befc12b4 100644 --- a/lib/browser/api/menu.js +++ b/lib/browser/api/menu.js @@ -7,7 +7,6 @@ const v8Util = process.atomBinding('v8_util') const bindings = process.atomBinding('menu') const { Menu } = bindings -let applicationMenu = null let groupIdIndex = 0 Object.setPrototypeOf(Menu.prototype, EventEmitter.prototype) @@ -134,7 +133,9 @@ Menu.prototype._callMenuWillShow = function () { /* Static Methods */ -Menu.getApplicationMenu = () => applicationMenu +Menu.getApplicationMenu = function () { + return (typeof Menu._applicationMenu !== 'undefined') ? Menu._applicationMenu : null +} Menu.sendActionToFirstResponder = bindings.sendActionToFirstResponder @@ -144,7 +145,7 @@ Menu.setApplicationMenu = function (menu) { throw new TypeError('Invalid menu') } - applicationMenu = menu + Menu._applicationMenu = menu if (process.platform === 'darwin') { if (!menu) return menu._callMenuWillShow() diff --git a/lib/browser/default-menu.js b/lib/browser/default-menu.js new file mode 100644 index 0000000000000..a8244305e29ad --- /dev/null +++ b/lib/browser/default-menu.js @@ -0,0 +1,49 @@ +'use strict' + +const { shell, Menu } = require('electron') +const isMac = process.platform === 'darwin' + +module.exports = () => { + const helpMenu = { + role: 'help', + submenu: [ + { + label: 'Learn More', + click () { + shell.openExternal('https://electronjs.org') + } + }, + { + label: 'Documentation', + click () { + shell.openExternal( + `https://github.com/electron/electron/tree/v${process.versions.electron}/docs#readme` + ) + } + }, + { + label: 'Community Discussions', + click () { + shell.openExternal('https://discuss.atom.io/c/electron') + } + }, + { + label: 'Search Issues', + click () { + shell.openExternal('https://github.com/electron/electron/issues') + } + } + ] + } + + const template = [ + isMac ? { role: 'appMenu' } : null, + { role: 'fileMenu' }, + { role: 'editMenu' }, + { role: 'viewMenu' }, + { role: 'windowMenu' }, + helpMenu + ] + + return Menu.buildFromTemplate(template) +} diff --git a/lib/browser/init.js b/lib/browser/init.js index fc526e1f06544..bc2f11b8c9f97 100644 --- a/lib/browser/init.js +++ b/lib/browser/init.js @@ -57,7 +57,7 @@ process.on('uncaughtException', function (error) { }) // Emit 'exit' event on quit. -const { app } = require('electron') +const { app, Menu } = require('electron') app.on('quit', function (event, exitCode) { process.emit('exit', exitCode) @@ -184,5 +184,21 @@ if (currentPlatformSupportsAppIndicator()) { process.env.XDG_CURRENT_DESKTOP = 'Unity' } +// Quit when all windows are closed and no other one is listening to this. +app.on('window-all-closed', () => { + if (app.listenerCount('window-all-closed') === 1) { + app.quit() + } +}) + +// Create default menu. +app.once('ready', () => { + if (process.noDefaultMenu) return + if (Menu._applicationMenu === undefined) { + const menu = require('@electron/internal/browser/default-menu')() + Menu.setApplicationMenu(menu) + } +}) + // Finally load app's main.js and transfer control to C++. Module._load(path.join(packagePath, mainStartupScript), Module, true) diff --git a/spec/api-app-spec.js b/spec/api-app-spec.js index c86484ebd85cc..4714e2964dd05 100644 --- a/spec/api-app-spec.js +++ b/spec/api-app-spec.js @@ -1130,12 +1130,12 @@ describe('app module', () => { describe('commandLine.hasSwitch (existing argv)', () => { it('returns true when present', async () => { - const { hasSwitch } = await runCommandLineTestApp('--foobar') + const { hasSwitch } = await runTestApp('command-line', '--foobar') expect(hasSwitch).to.be.true() }) it('returns false when not present', async () => { - const { hasSwitch } = await runCommandLineTestApp() + const { hasSwitch } = await runTestApp('command-line') expect(hasSwitch).to.be.false() }) }) @@ -1158,31 +1158,63 @@ describe('app module', () => { describe('commandLine.getSwitchValue (existing argv)', () => { it('returns the value when present', async () => { - const { getSwitchValue } = await runCommandLineTestApp('--foobar=test') + const { getSwitchValue } = await runTestApp('command-line', '--foobar=test') expect(getSwitchValue).to.equal('test') }) it('returns an empty string when present without value', async () => { - const { getSwitchValue } = await runCommandLineTestApp('--foobar') + const { getSwitchValue } = await runTestApp('command-line', '--foobar') expect(getSwitchValue).to.equal('') }) it('returns an empty string when not present', async () => { - const { getSwitchValue } = await runCommandLineTestApp() + const { getSwitchValue } = await runTestApp('command-line') expect(getSwitchValue).to.equal('') }) }) +}) - async function runCommandLineTestApp (...args) { - const appPath = path.join(__dirname, 'fixtures', 'api', 'command-line') - const electronPath = remote.getGlobal('process').execPath - const appProcess = cp.spawn(electronPath, [appPath, ...args]) +describe('default behavior', () => { + describe('application menu', () => { + it('creates the default menu if the app does not set it', async () => { + const result = await runTestApp('default-menu') + expect(result).to.equal(false) + }) - let output = '' - appProcess.stdout.on('data', (data) => { output += data }) + it('does not create the default menu if the app sets a custom menu', async () => { + const result = await runTestApp('default-menu', '--custom-menu') + expect(result).to.equal(true) + }) - await emittedOnce(appProcess.stdout, 'end') + it('does not create the default menu if the app sets a null menu', async () => { + const result = await runTestApp('default-menu', '--null-menu') + expect(result).to.equal(true) + }) + }) + + describe('window-all-closed', () => { + it('quits when the app does not handle the event', async () => { + const result = await runTestApp('window-all-closed') + expect(result).to.equal(false) + }) - return JSON.parse(output) - } + it('does not quit when the app handles the event', async () => { + const result = await runTestApp('window-all-closed', '--handle-event') + expect(result).to.equal(true) + }) + }) }) + +async function runTestApp (name, ...args) { + const appPath = path.join(__dirname, 'fixtures', 'api', name) + const electronPath = remote.getGlobal('process').execPath + const appProcess = cp.spawn(electronPath, [appPath, ...args]) + + let output = '' + appProcess.stdout.on('data', (data) => { output += data }) + + await emittedOnce(appProcess.stdout, 'end') + + console.log(output) + return JSON.parse(output) +} diff --git a/spec/api-menu-item-spec.js b/spec/api-menu-item-spec.js index 49d0366f34137..10d8e1279c5db 100644 --- a/spec/api-menu-item-spec.js +++ b/spec/api-menu-item-spec.js @@ -306,6 +306,64 @@ describe('MenuItems', () => { }) }) + describe('MenuItem appMenu', () => { + before(function () { + if (process.platform !== 'darwin') { + this.skip() + } + }) + + it('includes a default submenu layout when submenu is empty', () => { + const item = new MenuItem({ role: 'appMenu' }) + + expect(item.label).to.equal(app.getName()) + expect(item.submenu.items[0].role).to.equal('about') + expect(item.submenu.items[1].type).to.equal('separator') + expect(item.submenu.items[2].role).to.equal('services') + expect(item.submenu.items[3].type).to.equal('separator') + expect(item.submenu.items[4].role).to.equal('hide') + expect(item.submenu.items[5].role).to.equal('hideothers') + expect(item.submenu.items[6].role).to.equal('unhide') + expect(item.submenu.items[7].type).to.equal('separator') + expect(item.submenu.items[8].role).to.equal('quit') + }) + + it('overrides default layout when submenu is specified', () => { + const item = new MenuItem({ + role: 'appMenu', + submenu: [{ + role: 'close' + }] + }) + expect(item.label).to.equal(app.getName()) + expect(item.submenu.items[0].role).to.equal('close') + }) + }) + + describe('MenuItem fileMenu', () => { + it('includes a default submenu layout when submenu is empty', () => { + const item = new MenuItem({ role: 'fileMenu' }) + + expect(item.label).to.equal('File') + if (process.platform === 'darwin') { + expect(item.submenu.items[0].role).to.equal('close') + } else { + expect(item.submenu.items[0].role).to.equal('quit') + } + }) + + it('overrides default layout when submenu is specified', () => { + const item = new MenuItem({ + role: 'fileMenu', + submenu: [{ + role: 'about' + }] + }) + expect(item.label).to.equal('File') + expect(item.submenu.items[0].role).to.equal('about') + }) + }) + describe('MenuItem editMenu', () => { it('includes a default submenu layout when submenu is empty', () => { const item = new MenuItem({ role: 'editMenu' }) @@ -322,9 +380,11 @@ describe('MenuItems', () => { expect(item.submenu.items[6].role).to.equal('pasteandmatchstyle') expect(item.submenu.items[7].role).to.equal('delete') expect(item.submenu.items[8].role).to.equal('selectall') - } - - if (process.platform === 'win32') { + expect(item.submenu.items[9].type).to.equal('separator') + expect(item.submenu.items[10].label).to.equal('Speech') + expect(item.submenu.items[10].submenu.items[0].role).to.equal('startspeaking') + expect(item.submenu.items[10].submenu.items[1].role).to.equal('stopspeaking') + } else { expect(item.submenu.items[6].role).to.equal('delete') expect(item.submenu.items[7].type).to.equal('separator') expect(item.submenu.items[8].role).to.equal('selectall') @@ -343,6 +403,34 @@ describe('MenuItems', () => { }) }) + describe('MenuItem viewMenu', () => { + it('includes a default submenu layout when submenu is empty', () => { + const item = new MenuItem({ role: 'viewMenu' }) + + expect(item.label).to.equal('View') + expect(item.submenu.items[0].role).to.equal('reload') + expect(item.submenu.items[1].role).to.equal('forcereload') + expect(item.submenu.items[2].role).to.equal('toggledevtools') + expect(item.submenu.items[3].type).to.equal('separator') + expect(item.submenu.items[4].role).to.equal('resetzoom') + expect(item.submenu.items[5].role).to.equal('zoomin') + expect(item.submenu.items[6].role).to.equal('zoomout') + expect(item.submenu.items[7].type).to.equal('separator') + expect(item.submenu.items[8].role).to.equal('togglefullscreen') + }) + + it('overrides default layout when submenu is specified', () => { + const item = new MenuItem({ + role: 'viewMenu', + submenu: [{ + role: 'close' + }] + }) + expect(item.label).to.equal('View') + expect(item.submenu.items[0].role).to.equal('close') + }) + }) + describe('MenuItem windowMenu', () => { it('includes a default submenu layout when submenu is empty', () => { const item = new MenuItem({ role: 'windowMenu' }) @@ -354,9 +442,10 @@ describe('MenuItems', () => { if (process.platform === 'darwin') { expect(item.submenu.items[2].type).to.equal('separator') expect(item.submenu.items[3].role).to.equal('front') - expect(item.submenu.items[4].type).to.equal('separator') expect(item.submenu.items[5].role).to.equal('window') + } else { + expect(item.submenu.items[2].role).to.equal('close') } }) diff --git a/spec/fixtures/api/default-menu/main.js b/spec/fixtures/api/default-menu/main.js new file mode 100644 index 0000000000000..5c2ed821d2fb6 --- /dev/null +++ b/spec/fixtures/api/default-menu/main.js @@ -0,0 +1,20 @@ +const { app, Menu } = require('electron') + +let expectedMenu + +if (app.commandLine.hasSwitch('custom-menu')) { + expectedMenu = new Menu() + Menu.setApplicationMenu(expectedMenu) +} else if (app.commandLine.hasSwitch('null-menu')) { + expectedMenu = null + Menu.setApplicationMenu(null) +} + +app.on('ready', () => { + setImmediate(() => { + process.stdout.write(JSON.stringify(Menu.getApplicationMenu() === expectedMenu)) + process.stdout.end() + + app.quit() + }) +}) diff --git a/spec/fixtures/api/default-menu/package.json b/spec/fixtures/api/default-menu/package.json new file mode 100644 index 0000000000000..7e253de58f853 --- /dev/null +++ b/spec/fixtures/api/default-menu/package.json @@ -0,0 +1,4 @@ +{ + "name": "default-menu", + "main": "main.js" +} diff --git a/spec/fixtures/api/window-all-closed/main.js b/spec/fixtures/api/window-all-closed/main.js new file mode 100644 index 0000000000000..67450d93e751c --- /dev/null +++ b/spec/fixtures/api/window-all-closed/main.js @@ -0,0 +1,20 @@ +const { app, BrowserWindow } = require('electron') + +let handled = false + +if (app.commandLine.hasSwitch('handle-event')) { + app.on('window-all-closed', () => { + handled = true + app.quit() + }) +} + +app.on('quit', () => { + process.stdout.write(JSON.stringify(handled)) + process.stdout.end() +}) + +app.on('ready', () => { + const win = new BrowserWindow() + win.close() +}) diff --git a/spec/fixtures/api/window-all-closed/package.json b/spec/fixtures/api/window-all-closed/package.json new file mode 100644 index 0000000000000..6486c5bc6e98a --- /dev/null +++ b/spec/fixtures/api/window-all-closed/package.json @@ -0,0 +1,4 @@ +{ + "name": "window-all-closed", + "main": "main.js" +}