From ea6e6e7490361982b2b8c08c307ebfabbd59f0e5 Mon Sep 17 00:00:00 2001 From: Florent Benoit Date: Fri, 6 Sep 2024 11:56:01 +0200 Subject: [PATCH] refactor: move menu creation to a dedicated file also add tests prior to add new entries for zoom in / zoom out Signed-off-by: Florent Benoit --- .../main/src/application-menu-builder.spec.ts | 103 ++++++++++++++++++ packages/main/src/application-menu-builder.ts | 43 ++++++++ packages/main/src/index.spec.ts | 38 ++++++- packages/main/src/index.ts | 11 +- packages/main/src/mainWindow.ts | 26 +---- 5 files changed, 194 insertions(+), 27 deletions(-) create mode 100644 packages/main/src/application-menu-builder.spec.ts create mode 100644 packages/main/src/application-menu-builder.ts diff --git a/packages/main/src/application-menu-builder.spec.ts b/packages/main/src/application-menu-builder.spec.ts new file mode 100644 index 000000000..f0717508f --- /dev/null +++ b/packages/main/src/application-menu-builder.spec.ts @@ -0,0 +1,103 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Menu } from 'electron'; +import { aboutMenuItem } from 'electron-util/main'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import { ApplicationMenuBuilder } from './application-menu-builder.js'; + +vi.mock('electron', async () => { + class MyCustomWindow { + static readonly singleton = new MyCustomWindow(); + + loadURL(): void {} + setBounds(): void {} + + on(): void {} + + show(): void {} + focus(): void {} + isMinimized(): boolean { + return false; + } + isDestroyed(): boolean { + return false; + } + + static getAllWindows(): unknown[] { + return [MyCustomWindow.singleton]; + } + } + + return { + BrowserWindow: MyCustomWindow, + Menu: { + buildFromTemplate: vi.fn(), + getApplicationMenu: vi.fn(), + setApplicationMenu: vi.fn(), + }, + }; +}); + +vi.mock('electron-util/main', async () => { + return { + aboutMenuItem: vi.fn(), + }; +}); + +let applicationMenuBuilder: ApplicationMenuBuilder; + +beforeEach(() => { + vi.resetAllMocks(); + applicationMenuBuilder = new ApplicationMenuBuilder(); +}); + +test('check about menu is added', () => { + vi.mocked(Menu.buildFromTemplate).mockImplementation(a => { + return a as unknown as Menu; + }); + + // set a menu + vi.mocked(Menu.getApplicationMenu).mockReturnValue({ + items: [ + { + role: 'help', + submenu: { + items: [], + }, + }, + ], + } as unknown as Menu); + + // mock aboutMenuItem + vi.mocked(aboutMenuItem).mockReturnValue({ + label: 'About', + }); + + const menu = applicationMenuBuilder.build(); + expect(menu).toBeDefined(); + + const items = menu?.items[0]?.submenu as unknown as any[]; + expect(items).toBeDefined(); + + // expect to have the about menu + expect(items[1]?.label).toBe('About'); +}); diff --git a/packages/main/src/application-menu-builder.ts b/packages/main/src/application-menu-builder.ts new file mode 100644 index 000000000..684530928 --- /dev/null +++ b/packages/main/src/application-menu-builder.ts @@ -0,0 +1,43 @@ +/********************************************************************** + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import { Menu } from 'electron'; +import { aboutMenuItem } from 'electron-util/main'; + +export class ApplicationMenuBuilder { + build(): Electron.Menu | undefined { + const menu = Menu.getApplicationMenu(); // get default menu + if (!menu) { + return undefined; + } + // Add help/about menu entry + menu.items.forEach(i => { + // add the About entry only in the help menu + if (i.role === 'help' && i.submenu) { + const aboutMenuSubItem = aboutMenuItem({}); + aboutMenuSubItem.label = 'About'; + + // create new submenu + // also add a separator before the About entry + const newSubMenu = Menu.buildFromTemplate([...i.submenu.items, { type: 'separator' }, aboutMenuSubItem]); + i.submenu = newSubMenu; + } + }); + return menu; + } +} diff --git a/packages/main/src/index.spec.ts b/packages/main/src/index.spec.ts index 423a866a3..8a0b78975 100644 --- a/packages/main/src/index.spec.ts +++ b/packages/main/src/index.spec.ts @@ -17,7 +17,7 @@ ***********************************************************************/ import type { App } from 'electron'; -import { app, BrowserWindow, Tray } from 'electron'; +import { app, BrowserWindow, Menu, Tray } from 'electron'; import { afterEach, assert, beforeEach, expect, test, vi } from 'vitest'; import { @@ -26,6 +26,8 @@ import { mainWindowDeferred, sanitizeProtocolForExtension, } from './index.js'; +import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; +import type { Emitter } from './plugin/events/emitter.js'; import { PluginSystem } from './plugin/index.js'; import { Deferred } from './plugin/util/deferred.js'; import * as util from './util.js'; @@ -58,6 +60,10 @@ vi.mock('electron-util/main', async () => { }; }); +const configurationRegistryMock = { + registerConfigurations: vi.fn(), +} as unknown as ConfigurationRegistry; + const fakeWindow = { isDestroyed: vi.fn(), webContents: { @@ -223,6 +229,36 @@ test('app-ready event with activate event', async () => { // expect show and focus have been called expect(spyShow).toHaveBeenCalled(); expect(spyFocus).toHaveBeenCalled(); + + // capture the pluginSystem.initExtensions call + const initExtensionsCalls = vi.mocked(initMock).mock.calls; + expect(initExtensionsCalls).toHaveLength(1); + + // grab onDidConfigurationRegistry parameter + const _onDidConfigurationRegistry = initExtensionsCalls?.[0]?.[0]; + // call the onDidConfigurationRegistry + expect(_onDidConfigurationRegistry).toBeDefined(); + + // cast as Emitter + const onDidConfigurationRegistry = _onDidConfigurationRegistry as Emitter; + + // create a Menu + const menu = { + items: [], + } as unknown as Menu; + + vi.mocked(Menu.getApplicationMenu).mockReturnValue(menu); + + onDidConfigurationRegistry.fire(configurationRegistryMock); + + // check we've called Menu.getApplicationMenu + await vi.waitFor(() => expect(vi.mocked(Menu.getApplicationMenu)).toHaveBeenCalled()); + + // and Menu.buildFromTemplate + expect(vi.mocked(Menu.buildFromTemplate)).toHaveBeenCalled(); + + // and Menu.setApplicationMenu + expect(vi.mocked(Menu.setApplicationMenu)).toHaveBeenCalled(); }); test('should send the URL to open when mainWindow is created', async () => { diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 2b953a497..73fc04ae8 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -21,10 +21,11 @@ import './security-restrictions'; import dns from 'node:dns'; import type { BrowserWindow } from 'electron'; -import { app, ipcMain, Tray } from 'electron'; +import { app, ipcMain, Menu, Tray } from 'electron'; import { createNewWindow, restoreWindow } from '/@/mainWindow.js'; +import { ApplicationMenuBuilder } from './application-menu-builder.js'; import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; import type { Event } from './plugin/events/emitter.js'; import { Emitter } from './plugin/events/emitter.js'; @@ -249,6 +250,14 @@ app.whenReady().then( .then(browserWindow => { const windowHandler = new WindowHandler(configurationRegistry, browserWindow); windowHandler.init(); + // sets the menu + const applicationMenuBuilder = new ApplicationMenuBuilder(); + const menu = applicationMenuBuilder.build(); + + if (menu) { + Menu.setApplicationMenu(menu); + } + // send window Handler ipcMain.emit('window-handler', '', windowHandler); }) diff --git a/packages/main/src/mainWindow.ts b/packages/main/src/mainWindow.ts index 19a97b86e..9c855cb29 100644 --- a/packages/main/src/mainWindow.ts +++ b/packages/main/src/mainWindow.ts @@ -20,9 +20,8 @@ import { join } from 'node:path'; import { URL } from 'node:url'; import type { BrowserWindowConstructorOptions, Rectangle } from 'electron'; -import { app, autoUpdater, BrowserWindow, ipcMain, Menu, nativeTheme, screen } from 'electron'; +import { app, autoUpdater, BrowserWindow, ipcMain, nativeTheme, screen } from 'electron'; import contextMenu from 'electron-context-menu'; -import { aboutMenuItem } from 'electron-util/main'; import { NavigationItemsMenuBuilder } from './navigation-items-menu-builder.js'; import { OpenDevTools } from './open-dev-tools.js'; @@ -239,29 +238,6 @@ async function createWindow(): Promise { }, }); - // Add help/about menu entry - const menu = Menu.getApplicationMenu(); // get default menu - if (menu) { - // build a new menu based on default one but adding about entry in the help menu - const newmenu = Menu.buildFromTemplate( - menu.items.map(i => { - // add the About entry only in the help menu - if (i.role === 'help' && i.submenu) { - const aboutMenuSubItem = aboutMenuItem({}); - aboutMenuSubItem.label = 'About'; - - // create new submenu - // also add a separator before the About entry - const newSubMenu = Menu.buildFromTemplate([...i.submenu.items, { type: 'separator' }, aboutMenuSubItem]); - return { ...i, submenu: newSubMenu }; - } - return i; - }), - ); - - Menu.setApplicationMenu(newmenu); - } - /** * URL for main window. * Vite dev server for development.