1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Respond to PR comments

- Revert changes to structure of electronTrayInjectable

- Add some behavioural tests for the tray icon

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-06-07 11:30:47 -04:00
parent 9f3140db77
commit b7a2bb5385
12 changed files with 155 additions and 235 deletions

View File

@ -44,7 +44,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_13"
data-testid="close-notification-for-notification_16"
tabindex="0"
>
<span
@ -95,7 +95,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_96"
data-testid="close-notification-for-notification_115"
tabindex="0"
>
<span
@ -135,7 +135,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_99"
data-testid="close-notification-for-notification_118"
tabindex="0"
>
<span
@ -186,7 +186,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_149"
data-testid="close-notification-for-notification_183"
tabindex="0"
>
<span
@ -226,7 +226,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_152"
data-testid="close-notification-for-notification_186"
tabindex="0"
>
<span
@ -266,7 +266,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_157"
data-testid="close-notification-for-notification_191"
tabindex="0"
>
<span
@ -317,7 +317,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_215"
data-testid="close-notification-for-notification_266"
tabindex="0"
>
<span
@ -357,7 +357,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_218"
data-testid="close-notification-for-notification_269"
tabindex="0"
>
<span
@ -478,7 +478,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_48"
data-testid="close-notification-for-notification_59"
tabindex="0"
>
<span
@ -518,7 +518,7 @@ exports[`installing update using tray when started when user checks for updates
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-notification_51"
data-testid="close-notification-for-notification_62"
tabindex="0"
>
<span

View File

@ -15,12 +15,15 @@ import type { DownloadPlatformUpdate } from "../../main/application-update/downl
import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
import showApplicationWindowInjectable from "../../main/start-main-application/lens-window/show-application-window.injectable";
import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
import type { TrayIconPaths } from "../../main/tray/tray-icon-path.injectable";
import trayIconPathsInjectable from "../../main/tray/tray-icon-path.injectable";
describe("installing update using tray", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let showApplicationWindowMock: jest.Mock;
let trayIconPaths: TrayIconPaths;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
@ -44,6 +47,7 @@ describe("installing update using tray", () => {
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
trayIconPaths = mainDi.inject(trayIconPathsInjectable);
});
});
@ -58,6 +62,10 @@ describe("installing update using tray", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("should use the normal tray icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal);
});
it("user cannot install update yet", () => {
expect(applicationBuilder.tray.get("install-update")).toBeNull();
});
@ -73,15 +81,19 @@ describe("installing update using tray", () => {
expect(showApplicationWindowMock).not.toHaveBeenCalled();
});
it("should still use the normal tray icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal);
});
it("user cannot check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled,
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(false);
});
it("name of tray item for checking updates indicates that checking is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Checking for updates...");
});
@ -106,19 +118,23 @@ describe("installing update using tray", () => {
expect(showApplicationWindowMock).toHaveBeenCalled();
});
it("should still use the normal tray icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal);
});
it("user cannot install update", () => {
expect(applicationBuilder.tray.get("install-update")).toBeNull();
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled,
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).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,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});
@ -141,15 +157,19 @@ describe("installing update using tray", () => {
expect(showApplicationWindowMock).toHaveBeenCalled();
});
it("should use the update available icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.updateAvailable);
});
it("user cannot check for updates again yet", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled,
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).toBe(false);
});
it("name of tray item for checking updates indicates that downloading is happening", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.label,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Downloading update some-version (0%)...");
});
@ -161,7 +181,7 @@ describe("installing update using tray", () => {
progressOfUpdateDownload.set({ percentage: 42.424242 });
expect(
applicationBuilder.tray.get("check-for-updates")?.label,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Downloading update some-version (42%)...");
});
@ -184,15 +204,19 @@ describe("installing update using tray", () => {
).toBeNull();
});
it("should revert to use the normal tray icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal);
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled,
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).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,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});
@ -208,19 +232,23 @@ describe("installing update using tray", () => {
it("user can install update", () => {
expect(
applicationBuilder.tray.get("install-update")?.label,
applicationBuilder.tray.get("install-update")?.label?.get(),
).toBe("Install update some-version");
});
it("should use the update available icon", () => {
expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.updateAvailable);
});
it("user can check for updates again", () => {
expect(
applicationBuilder.tray.get("check-for-updates")?.enabled,
applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
).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,
applicationBuilder.tray.get("check-for-updates")?.label?.get(),
).toBe("Check for updates");
});

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type UpdateChannelId = "alpha" | "beta" | "latest";
const latestChannel: UpdateChannel = {

View File

@ -4,12 +4,21 @@
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { JsonValue } from "type-fest";
export interface SyncBox<TValue extends JsonValue> {
type AsJson<T> = T extends string | number | boolean | null
? T
: T extends Function
? never
: T extends Array<infer V>
? AsJson<V>[]
: T extends object
? { [K in keyof T]: AsJson<T[K]> }
: never;
export interface SyncBox<TValue> {
id: string;
value: IComputedValue<TValue>;
set: (value: TValue) => void;
value: IComputedValue<AsJson<TValue>>;
set: (value: AsJson<TValue>) => void;
}
export const syncBoxInjectionToken = getInjectionToken<SyncBox<any>>({

View File

@ -1,17 +0,0 @@
/**
* 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;

View File

@ -3,20 +3,28 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Menu } from "electron";
import { Tray } from "electron";
import { Menu, Tray } from "electron";
import packageJsonInjectable from "../../../common/vars/package-json.injectable";
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import trayIconPathsInjectable from "../tray-icon-path.injectable";
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
import { convertToElectronMenuTemplate } from "../reactive-tray-menu-items/converters";
const TRAY_LOG_PREFIX = "[TRAY]";
export interface ElectronTray {
start(): void;
stop(): void;
setMenuItems(menuItems: TrayMenuItem[]): void;
setIconPath(iconPath: string): void;
}
const electronTrayInjectable = getInjectable({
id: "electron-tray",
instantiate: (di) => {
instantiate: (di): ElectronTray => {
const packageJson = di.inject(packageJsonInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const isWindows = di.inject(isWindowsInjectable);
@ -42,10 +50,13 @@ const electronTrayInjectable = getInjectable({
stop: () => {
tray.destroy();
},
setMenu: (menu: Menu) => {
setMenuItems: (menuItems) => {
const template = convertToElectronMenuTemplate(menuItems);
const menu = Menu.buildFromTemplate(template);
tray.setContextMenu(menu);
},
setIconPath: (iconPath: string) => {
setIconPath: (iconPath) => {
tray.setImage(iconPath);
},
};

View File

@ -1,63 +0,0 @@
/**
* 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 loggerInjectable from "../../../common/logger.injectable";
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
const convertToElectronMenuTemplateInjectable = getInjectable({
id: "convert-to-electron-menu-template",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return (trayMenuItems: TrayMenuItem[]) => {
const toTrayMenuOptions = (parentId: string | null) => (
trayMenuItems
.filter((item) => item.parentId === parentId)
.map((trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(childItems.length === 0
? {
type: "normal",
submenu: toTrayMenuOptions(trayMenuItem.id),
click: () => {
(async () => {
try {
await trayMenuItem.click?.();
} catch (error) {
logger.error(
`[TRAY]: clicking item "${trayMenuItem.id} failed."`,
{ error },
);
}
})();
},
}
: {
type: "submenu",
submenu: toTrayMenuOptions(trayMenuItem.id),
}),
};
})
);
return toTrayMenuOptions(null);
};
},
});
export default convertToElectronMenuTemplateInjectable;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
export function convertToElectronMenuTemplate(trayMenuItems: TrayMenuItem[]): Electron.MenuItemConstructorOptions[] {
const toTrayMenuOptions = (parentId: string | null) => (
trayMenuItems
.filter((item) => item.parentId === parentId)
.map((trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(childItems.length === 0
? {
type: "normal",
submenu: toTrayMenuOptions(trayMenuItem.id),
click: trayMenuItem.click,
}
: {
type: "submenu",
submenu: toTrayMenuOptions(trayMenuItem.id),
}),
};
})
);
return toTrayMenuOptions(null);
}

View File

@ -6,19 +6,19 @@ import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import { reaction } from "mobx";
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
import trayMenuInjectable from "./tray-menu.injectable";
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
const reactiveTrayMenuItemsInjectable = getInjectable({
id: "reactive-tray-menu-items",
instantiate: (di) => {
const electronTray = di.inject(electronTrayInjectable);
const trayMenu = di.inject(trayMenuInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
return getStartableStoppable("reactive-tray-menu-items", () => (
reaction(
() => trayMenu.get(),
electronTray.setMenu,
() => trayMenuItems.get(),
electronTray.setMenuItems,
{
fireImmediately: true,
},

View File

@ -1,28 +0,0 @@
/**
* 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 { 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({
id: "tray-menu",
instantiate: (di) => {
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const convertToElectronMenuTemplate = di.inject(convertToElectronMenuTemplateInjectable);
const buildMenuFromTemplate = di.inject(buildMenuFromTemplateInjectable);
return computed(() => (
buildMenuFromTemplate(
convertToElectronMenuTemplate(
trayMenuItems.get(),
),
)
));
},
});
export default trayMenuInjectable;

View File

@ -8,10 +8,15 @@ import staticFilesDirectoryInjectable from "../../common/vars/static-files-direc
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
export interface TrayIconPaths {
normal: string;
updateAvailable: string;
}
const trayIconPathsInjectable = getInjectable({
id: "tray-icon-paths",
instantiate: (di) => {
instantiate: (di): TrayIconPaths => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
const isDevelopment = di.inject(isDevelopmentInjectable);

View File

@ -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, map } from "lodash/fp";
import { flatMap, compact, join, get, filter, map, matches, find } 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 { MenuItemConstructorOptions, MenuItem, Menu } from "electron";
import type { MenuItemConstructorOptions, MenuItem } 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";
@ -50,7 +50,8 @@ 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";
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import trayIconPathsInjectable from "../../../main/tray/tray-icon-path.injectable";
type Callback = (dis: DiContainers) => void | Promise<void>;
@ -65,7 +66,8 @@ export interface ApplicationBuilder {
tray: {
click: (id: string) => Promise<void>;
get: (id: string) => Electron.MenuItem | null;
get: (id: string) => TrayMenuItem | null;
getIconPath: () => string;
};
applicationMenu: {
@ -96,10 +98,6 @@ interface Environment {
onAllowKubeResource: () => void;
}
const getAllSubMenuItems = (item: MenuItem): MenuItem[] => {
return [item, ...(item.submenu?.items ?? []).flatMap(getAllSubMenuItems)];
};
export const getApplicationBuilder = () => {
const mainDi = getMainDi({
doGeneralOverrides: true,
@ -170,84 +168,22 @@ export const getApplicationBuilder = () => {
computed(() => []),
);
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,
};
const iconPaths = mainDi.inject(trayIconPathsInjectable);
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;
let trayMenuItemsStateFake: TrayMenuItem[];
let trayMenuIconPath: string;
mainDi.override(electronTrayInjectable, () => ({
start: () => {},
stop: () => {},
setMenu: (menu) => {
trayMenuStateFake = menu;
start: () => {
trayMenuIconPath = iconPaths.normal;
},
stop: () => {},
setMenuItems: (items) => {
trayMenuItemsStateFake = items;
},
setIconPath: (path) => {
trayMenuIconPath = path;
},
setIconPath: () => {},
}));
let allowedResourcesState: IObservableArray<KubeResource>;
@ -294,20 +230,18 @@ export const getApplicationBuilder = () => {
tray: {
get: (id: string) => {
return trayMenuStateFake?.getMenuItemById(id) ?? null;
return trayMenuItemsStateFake.find(matches({ id })) ?? null;
},
getIconPath: () => trayMenuIconPath,
click: async (id: string) => {
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);
const menuItem = pipeline(
trayMenuItemsStateFake,
find((menuItem) => menuItem.id === id),
);
if (!menuItem) {
const availableIds = pipeline(
trayMenuStateFake.items,
flatMap(getAllSubMenuItems),
trayMenuItemsStateFake,
filter(item => !!item.click),
map(item => item.id),
join(", "),