diff --git a/src/behaviours/update-app/__snapshots__/trigger-updating-using-tray.test.ts.snap b/src/behaviours/update-app/__snapshots__/trigger-updating-using-tray.test.ts.snap new file mode 100644 index 0000000000..fce1a17509 --- /dev/null +++ b/src/behaviours/update-app/__snapshots__/trigger-updating-using-tray.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`trigger updating using tray given no update available, when started renders 1`] = ` +
+ + +`; + +exports[`trigger updating using tray given no update available, when started when an update becomes available renders 1`] = ` + + + +`; + +exports[`trigger updating using tray given no update available, when started when an update becomes available when triggering installation of the update renders 1`] = ` + + + +`; + +exports[`trigger updating using tray given no update available, when started when an update becomes available when update becomes unavailable renders 1`] = ` + + + +`; diff --git a/src/behaviours/update-app/trigger-updating-using-tray.test.ts b/src/behaviours/update-app/trigger-updating-using-tray.test.ts new file mode 100644 index 0000000000..369cc7a047 --- /dev/null +++ b/src/behaviours/update-app/trigger-updating-using-tray.test.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; + +import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable"; +import type { RenderResult } from "@testing-library/react"; + +describe("trigger updating using tray", () => { + let applicationBuilder: ApplicationBuilder; + let quitAndInstallUpdateMock: jest.Mock; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + quitAndInstallUpdateMock = jest.fn(); + + mainDi.override(quitAndInstallUpdateInjectable, () => quitAndInstallUpdateMock); + }); + }); + + describe("given no update available, when started", () => { + let rendered: RenderResult; + + beforeEach(async () => { + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not quit and install update yet", () => { + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + + it("does not have possibility to trigger installation of an update", () => { + const trayItem = applicationBuilder.tray.get("trigger-application-update"); + + expect(trayItem).toBe(undefined); + }); + + describe("when an update becomes available", () => { + beforeEach(() => { + applicationBuilder.applicationUpdater.makeUpdateAvailable(true); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not quit and install update yet", () => { + expect(quitAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + + it("has possibility to trigger installation of the update", () => { + const trayItem = applicationBuilder.tray.get("trigger-application-update"); + + expect(trayItem).not.toBe(undefined); + }); + + describe("when triggering installation of the update", () => { + beforeEach(() => { + applicationBuilder.tray.click("trigger-application-update"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("quits application and installs update", () => { + expect(quitAndInstallUpdateMock).toHaveBeenCalled(); + }); + }); + + describe("when update becomes unavailable", () => { + beforeEach(async () => { + applicationBuilder.applicationUpdater.makeUpdateAvailable(false); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not have possibility to trigger installation of the update", () => { + const trayItem = applicationBuilder.tray.get("trigger-application-update"); + + expect(trayItem).toBe(undefined); + }); + }); + }); + }); +}); diff --git a/src/main/electron-app/features/electron-updater.injectable.ts b/src/main/electron-app/features/electron-updater.injectable.ts new file mode 100644 index 0000000000..f9e3335343 --- /dev/null +++ b/src/main/electron-app/features/electron-updater.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { autoUpdater } from "electron-updater"; + +const electronUpdaterInjectable = getInjectable({ + id: "electron-updater", + instantiate: () => autoUpdater, + causesSideEffects: true, +}); + +export default electronUpdaterInjectable; diff --git a/src/main/electron-app/features/quit-and-install-update.injectable.ts b/src/main/electron-app/features/quit-and-install-update.injectable.ts new file mode 100644 index 0000000000..a36c390bc4 --- /dev/null +++ b/src/main/electron-app/features/quit-and-install-update.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 { autoUpdater } from "electron-updater"; + +const quitAndInstallUpdateInjectable = getInjectable({ + id: "quit-and-install-update", + + instantiate: () => () => { + autoUpdater.quitAndInstall(true, true); + }, + + causesSideEffects: true, +}); + +export default quitAndInstallUpdateInjectable; diff --git a/src/main/electron-app/runnables/update-application/start-synchronizing-update-is-available-state.injectable.ts b/src/main/electron-app/runnables/update-application/start-synchronizing-update-is-available-state.injectable.ts new file mode 100644 index 0000000000..282ac6fdb7 --- /dev/null +++ b/src/main/electron-app/runnables/update-application/start-synchronizing-update-is-available-state.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 synchronizeUpdateIsAvailableStateInjectable from "./synchronize-update-is-available-state.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; + +const startSynchronizingUpdateIsAvailableStateInjectable = getInjectable({ + id: "start-synchronizing-update-is-available-state", + + instantiate: (di) => { + const synchronizeUpdateIsAvailableState = di.inject(synchronizeUpdateIsAvailableStateInjectable); + + return { + run: () => { + synchronizeUpdateIsAvailableState.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startSynchronizingUpdateIsAvailableStateInjectable; diff --git a/src/main/electron-app/runnables/update-application/stop-synchronizing-update-is-available-state.injectable.ts b/src/main/electron-app/runnables/update-application/stop-synchronizing-update-is-available-state.injectable.ts new file mode 100644 index 0000000000..4862ca7f02 --- /dev/null +++ b/src/main/electron-app/runnables/update-application/stop-synchronizing-update-is-available-state.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 synchronizeUpdateIsAvailableStateInjectable from "./synchronize-update-is-available-state.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopSynchronizingUpdateIsAvailableStateInjectable = getInjectable({ + id: "stop-synchronizing-update-is-available-state", + + instantiate: (di) => { + const synchronizeUpdateIsAvailableState = di.inject(synchronizeUpdateIsAvailableStateInjectable); + + return { + run: () => { + synchronizeUpdateIsAvailableState.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopSynchronizingUpdateIsAvailableStateInjectable; diff --git a/src/main/electron-app/runnables/update-application/synchronize-update-is-available-state.injectable.ts b/src/main/electron-app/runnables/update-application/synchronize-update-is-available-state.injectable.ts new file mode 100644 index 0000000000..611c1f0db6 --- /dev/null +++ b/src/main/electron-app/runnables/update-application/synchronize-update-is-available-state.injectable.ts @@ -0,0 +1,40 @@ +/** + * 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 { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; +import electronUpdaterInjectable from "../../features/electron-updater.injectable"; +import updateIsAvailableStateInjectable from "../../../update-app/update-is-available-state.injectable"; + +const synchronizeUpdateIsAvailableStateInjectable = getInjectable({ + id: "synchronize-update-is-available-state", + + instantiate: (di) => { + const electronUpdater = di.inject(electronUpdaterInjectable); + const updateIsAvailableState = di.inject(updateIsAvailableStateInjectable); + + const makeUpdateAvailableFor = (available: boolean) => () => { + updateIsAvailableState.set(available); + }; + + return getStartableStoppable( + "synchronize-update-is-available-state", + () => { + + const makeUpdateAvailable = makeUpdateAvailableFor(true); + const makeUpdateUnavailable = makeUpdateAvailableFor(false); + + electronUpdater.on("update-downloaded", makeUpdateAvailable); + electronUpdater.on("update-not-available", makeUpdateUnavailable); + + return () => { + electronUpdater.off("update-downloaded", makeUpdateAvailable); + electronUpdater.off("update-not-available", makeUpdateUnavailable); + }; + }, + ); + }, +}); + +export default synchronizeUpdateIsAvailableStateInjectable; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 26a1ea5963..ac48a89228 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -78,6 +78,8 @@ import getElectronThemeInjectable from "./electron-app/features/get-electron-the import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable"; import platformInjectable from "../common/vars/platform.injectable"; import productNameInjectable from "./app-paths/app-name/product-name.injectable"; +import synchronizeUpdateIsAvailableStateInjectable from "./electron-app/runnables/update-application/synchronize-update-is-available-state.injectable"; +import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable"; export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) { const { @@ -220,6 +222,8 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(ipcMainInjectable, () => ({})); di.override(getElectronThemeInjectable, () => () => "dark"); di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} })); + di.override(synchronizeUpdateIsAvailableStateInjectable, () => ({ start: () => {}, stop: () => {} })); + di.override(quitAndInstallUpdateInjectable, () => () => {}); di.override(createElectronWindowForInjectable, () => () => async () => ({ show: () => {}, diff --git a/src/main/update-app/trigger-application-update-tray-item.injectable.ts b/src/main/update-app/trigger-application-update-tray-item.injectable.ts new file mode 100644 index 0000000000..edd4f72c29 --- /dev/null +++ b/src/main/update-app/trigger-application-update-tray-item.injectable.ts @@ -0,0 +1,35 @@ +/** + * 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 { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token"; +import updateIsAvailableInjectable from "./update-is-available.injectable"; +import triggerApplicationUpdateInjectable from "./trigger-application-update.injectable"; + +const triggerApplicationUpdateTrayItemInjectable = getInjectable({ + id: "trigger-application-update-tray-item", + + instantiate: (di) => { + const updateIsAvailable = di.inject(updateIsAvailableInjectable); + const triggerApplicationUpdate = di.inject(triggerApplicationUpdateInjectable); + + return { + id: "trigger-application-update", + parentId: null, + orderNumber: 50, + label: "Trigger update", + enabled: computed(() => true), + visible: computed(() => updateIsAvailable.get()), + + click: () => { + triggerApplicationUpdate(); + }, + }; + }, + + injectionToken: trayMenuItemInjectionToken, +}); + +export default triggerApplicationUpdateTrayItemInjectable; diff --git a/src/main/update-app/trigger-application-update.injectable.ts b/src/main/update-app/trigger-application-update.injectable.ts new file mode 100644 index 0000000000..1867814b41 --- /dev/null +++ b/src/main/update-app/trigger-application-update.injectable.ts @@ -0,0 +1,20 @@ +/** + * 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 quitAndInstallUpdateInjectable from "../electron-app/features/quit-and-install-update.injectable"; + +const triggerApplicationUpdateInjectable = getInjectable({ + id: "trigger-application-update", + + instantiate: (di) => { + const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable); + + return () => { + quitAndInstallUpdate(); + }; + }, +}); + +export default triggerApplicationUpdateInjectable; diff --git a/src/main/update-app/update-is-available-state.injectable.ts b/src/main/update-app/update-is-available-state.injectable.ts new file mode 100644 index 0000000000..d19a1064eb --- /dev/null +++ b/src/main/update-app/update-is-available-state.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 { observable } from "mobx"; + +const updateIsAvailableState = getInjectable({ + id: "update-is-available-state", + instantiate: () => observable.box