diff --git a/src/behaviours/application-update/installing-update-using-tray.test.ts b/src/behaviours/application-update/installing-update-using-tray.test.ts index f6570bb8fa..5428457b5d 100644 --- a/src/behaviours/application-update/installing-update-using-tray.test.ts +++ b/src/behaviours/application-update/installing-update-using-tray.test.ts @@ -59,15 +59,14 @@ describe("installing update using tray", () => { }); it("user cannot install update yet", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); describe("when user checks for updates using tray", () => { let processCheckingForUpdatesPromise: Promise; beforeEach(async () => { - processCheckingForUpdatesPromise = - applicationBuilder.tray.click("check-for-updates"); + processCheckingForUpdatesPromise = applicationBuilder.tray.click("check-for-updates"); }); it("does not show application window yet", () => { @@ -76,18 +75,18 @@ describe("installing update using tray", () => { it("user cannot check for updates again", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + applicationBuilder.tray.get("check-for-updates")?.enabled, ).toBe(false); }); it("name of tray item for checking updates indicates that checking is happening", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Checking for updates..."); }); it("user cannot install update yet", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("renders", () => { @@ -108,18 +107,18 @@ describe("installing update using tray", () => { }); it("user cannot install update", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("user can check for updates again", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + applicationBuilder.tray.get("check-for-updates")?.enabled, ).toBe(true); }); it("name of tray item for checking updates no longer indicates that checking is happening", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Check for updates"); }); @@ -144,13 +143,13 @@ describe("installing update using tray", () => { it("user cannot check for updates again yet", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + applicationBuilder.tray.get("check-for-updates")?.enabled, ).toBe(false); }); it("name of tray item for checking updates indicates that downloading is happening", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Downloading update some-version (0%)..."); }); @@ -162,12 +161,12 @@ describe("installing update using tray", () => { progressOfUpdateDownload.set({ percentage: 42.424242 }); expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Downloading update some-version (42%)..."); }); it("user still cannot install update", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("renders", () => { @@ -182,18 +181,18 @@ describe("installing update using tray", () => { it("user cannot install update", () => { expect( applicationBuilder.tray.get("install-update"), - ).toBeUndefined(); + ).toBeNull(); }); it("user can check for updates again", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + applicationBuilder.tray.get("check-for-updates")?.enabled, ).toBe(true); }); it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Check for updates"); }); @@ -209,19 +208,19 @@ describe("installing update using tray", () => { it("user can install update", () => { expect( - applicationBuilder.tray.get("install-update")?.label?.get(), + applicationBuilder.tray.get("install-update")?.label, ).toBe("Install update some-version"); }); it("user can check for updates again", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.enabled.get(), + applicationBuilder.tray.get("check-for-updates")?.enabled, ).toBe(true); }); it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( - applicationBuilder.tray.get("check-for-updates")?.label?.get(), + applicationBuilder.tray.get("check-for-updates")?.label, ).toBe("Check for updates"); }); diff --git a/src/main/electron/build-from-template.injectable.ts b/src/main/electron/build-from-template.injectable.ts new file mode 100644 index 0000000000..da53720862 --- /dev/null +++ b/src/main/electron/build-from-template.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { MenuItemConstructorOptions } from "electron"; +import { Menu } from "electron"; + +export type BuildMenuFromTemplate = (template: MenuItemConstructorOptions[]) => Menu; + +const buildMenuFromTemplateInjectable = getInjectable({ + id: "build-menu-from-template", + instantiate: (): BuildMenuFromTemplate => (template) => Menu.buildFromTemplate(template), + causesSideEffects: true, // Not really but isn't defined +}); + +export default buildMenuFromTemplateInjectable; diff --git a/src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts b/src/main/tray/menu-icon/reactive.injectable.ts similarity index 75% rename from src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts rename to src/main/tray/menu-icon/reactive.injectable.ts index 03c7c703fc..86f1658c74 100644 --- a/src/main/tray/reactive-menu-icon/reactive-menu-icon.injectable.ts +++ b/src/main/tray/menu-icon/reactive.injectable.ts @@ -9,8 +9,8 @@ import { getStartableStoppable } from "../../../common/utils/get-startable-stopp import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; import trayIconPathsInjectable from "../tray-icon-path.injectable"; -const reactiveMenuIconInjectable = getInjectable({ - id: "reactive-menu-icon", +const reactiveTrayMenuIconInjectable = getInjectable({ + id: "reactive-tray-menu-icon", instantiate: (di) => { const discoveredUpdateVersion = di.inject(discoveredUpdateVersionInjectable); const electronTray = di.inject(electronTrayInjectable); @@ -20,11 +20,12 @@ const reactiveMenuIconInjectable = getInjectable({ reaction( () => discoveredUpdateVersion.value.get(), updateVersion => { - if (updateVersion) { - electronTray.setIconPath(trayIconPaths.updateAvailable); - } else { - electronTray.setIconPath(trayIconPaths.normal); - } + void updateVersion; + electronTray.setIconPath(trayIconPaths.updateAvailable); + // if (updateVersion) { + // } else { + // electronTray.setIconPath(trayIconPaths.normal); + // } }, { fireImmediately: true, @@ -34,4 +35,4 @@ const reactiveMenuIconInjectable = getInjectable({ }, }); -export default reactiveMenuIconInjectable; +export default reactiveTrayMenuIconInjectable; diff --git a/src/main/tray/menu-icon/start-reactivity.injectable.ts b/src/main/tray/menu-icon/start-reactivity.injectable.ts new file mode 100644 index 0000000000..373c3cf8fb --- /dev/null +++ b/src/main/tray/menu-icon/start-reactivity.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import startTrayInjectable from "../electron-tray/start-tray.injectable"; +import reactiveTrayMenuIconInjectable from "./reactive.injectable"; + +const startReactiveTrayMenuIconInjectable = getInjectable({ + id: "start-reactive-tray-menu-icon", + + instantiate: (di) => { + const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); + + return { + run: async () => { + await reactiveTrayMenuIcon.start(); + }, + + runAfter: di.inject(startTrayInjectable), + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startReactiveTrayMenuIconInjectable; diff --git a/src/main/tray/menu-icon/stop-reactivity.injectable.ts b/src/main/tray/menu-icon/stop-reactivity.injectable.ts new file mode 100644 index 0000000000..4b60aaaa54 --- /dev/null +++ b/src/main/tray/menu-icon/stop-reactivity.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; +import reactiveTrayMenuIconInjectable from "./reactive.injectable"; + +const stopReactiveTrayMenuIconInjectable = getInjectable({ + id: "stop-reactive-tray-menu-icon", + + instantiate: (di) => { + const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); + + return { + run: async () => { + await reactiveTrayMenuIcon.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopReactiveTrayMenuIconInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts b/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts index 8bf29f9941..ddfbdd9357 100644 --- a/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/tray-menu.injectable.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { Menu } from "electron"; import { computed } from "mobx"; import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable"; +import buildMenuFromTemplateInjectable from "../../electron/build-from-template.injectable"; import convertToElectronMenuTemplateInjectable from "./convert-to-electron-menu-template.injectable"; const trayMenuInjectable = getInjectable({ @@ -13,9 +13,10 @@ const trayMenuInjectable = getInjectable({ instantiate: (di) => { const trayMenuItems = di.inject(trayMenuItemsInjectable); const convertToElectronMenuTemplate = di.inject(convertToElectronMenuTemplateInjectable); + const buildMenuFromTemplate = di.inject(buildMenuFromTemplateInjectable); return computed(() => ( - Menu.buildFromTemplate( + buildMenuFromTemplate( convertToElectronMenuTemplate( trayMenuItems.get(), ), diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 5c8162cd2d..87184c37fb 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -24,12 +24,12 @@ import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; import { pipeline } from "@ogre-tools/fp"; -import { flatMap, compact, join, get, filter, find, map, matches } from "lodash/fp"; +import { flatMap, compact, join, get, filter, map } from "lodash/fp"; import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable"; import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable"; -import type { MenuItem, MenuItemConstructorOptions } from "electron"; +import type { MenuItemConstructorOptions, MenuItem, Menu } from "electron"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; @@ -43,7 +43,6 @@ import { flushPromises } from "../../../common/test-utils/flush-promises"; import type { NamespaceStore } from "../+namespaces/store"; import namespaceStoreInjectable from "../+namespaces/store.injectable"; import historyInjectable from "../../navigation/history.injectable"; -import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable"; import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable"; import { Notifications } from "../notifications/notifications"; @@ -51,6 +50,7 @@ import broadcastThatRootFrameIsRenderedInjectable from "../../frames/root-frame/ import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting"; import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting"; import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels"; +import buildMenuFromTemplateInjectable from "../../../main/electron/build-from-template.injectable"; type Callback = (dis: DiContainers) => void | Promise; @@ -65,7 +65,7 @@ export interface ApplicationBuilder { tray: { click: (id: string) => Promise; - get: (id: string) => TrayMenuItem | undefined; + get: (id: string) => Electron.MenuItem | null; }; applicationMenu: { @@ -96,6 +96,10 @@ interface Environment { onAllowKubeResource: () => void; } +const getAllSubMenuItems = (item: MenuItem): MenuItem[] => { + return [item, ...(item.submenu?.items ?? []).flatMap(getAllSubMenuItems)]; +}; + export const getApplicationBuilder = () => { const mainDi = getMainDi({ doGeneralOverrides: true, @@ -166,15 +170,84 @@ export const getApplicationBuilder = () => { computed(() => []), ); - let trayMenuItemsStateFake: TrayMenuItem[]; + let commandId = 0; + const makeFakeMenuItem = (opts: MenuItemConstructorOptions, menu: Menu): MenuItem => { + const menuItemFake: MenuItem = { + accelerator: opts.accelerator, + checked: opts.checked ?? false, + click: () => opts.click?.(menuItemFake, undefined, new KeyboardEvent("fake")), + commandId: commandId += 1, + enabled: opts.enabled ?? false, + icon: opts.icon, + id: opts.id ?? "", + label: opts.label ?? "", + menu, + registerAccelerator: opts.registerAccelerator ?? true, + sharingItem: opts.sharingItem ?? {}, + sublabel: opts.sublabel ?? "", + toolTip: opts.toolTip ?? "", + type: opts.type ?? "normal", + visible: opts.visible ?? true, + role: opts.role, + submenu: opts.submenu === undefined + ? undefined + : Array.isArray(opts.submenu) + ? makeFakeMenu(opts.submenu) + : opts.submenu, + }; + + return menuItemFake; + }; + const makeFakeMenu = (templates: MenuItemConstructorOptions[]): Menu => { + const menuFake: Electron.Menu = { + addListener: () => { + throw new Error("Adding listeners is not supported currently"); + }, + on: () => { + throw new Error("Adding listeners is not supported currently"); + }, + once: () => { + throw new Error("Adding listeners is not supported currently"); + }, + removeListener: () => { + throw new Error("Removing listeners is not supported currently"); + }, + append: () => { + throw new Error("Adding new menu items is not supported currently"); + }, + insert: () => { + throw new Error("Adding new menu items is not supported currently"); + }, + popup: () => { + throw new Error("Popping up menu is not supported currently"); + }, + closePopup: () => { + throw new Error("Popping up menu is not supported currently"); + }, + get items() { + return [...menuItems]; + }, + getMenuItemById: (id) => menuItems + .flatMap(getAllSubMenuItems) + .find(menuItem => menuItem.id === id) + ?? null, + }; + const menuItems = templates.map(template => makeFakeMenuItem(template, menuFake)); + + return menuFake; + }; + + mainDi.override(buildMenuFromTemplateInjectable, () => makeFakeMenu); + + let trayMenuStateFake: Electron.Menu | undefined; mainDi.override(electronTrayInjectable, () => ({ start: () => {}, stop: () => {}, - - setMenuItems: (items) => { - trayMenuItemsStateFake = items; + setMenu: (menu) => { + trayMenuStateFake = menu; }, + setIconPath: () => {}, })); let allowedResourcesState: IObservableArray; @@ -221,18 +294,20 @@ export const getApplicationBuilder = () => { tray: { get: (id: string) => { - return trayMenuItemsStateFake.find(matches({ id })); + return trayMenuStateFake?.getMenuItemById(id) ?? null; }, click: async (id: string) => { - const menuItem = pipeline( - trayMenuItemsStateFake, - find((menuItem) => menuItem.id === id), - ); + if (!trayMenuStateFake) { + throw new Error(`Tried to click tray menu with ID ${id}, but tray menu has not been set yet`); + } + + const menuItem = trayMenuStateFake.getMenuItemById(id); if (!menuItem) { const availableIds = pipeline( - trayMenuItemsStateFake, + trayMenuStateFake.items, + flatMap(getAllSubMenuItems), filter(item => !!item.click), map(item => item.id), join(", "),