diff --git a/src/common/utils/get-startable-stoppable.test.ts b/src/common/utils/get-startable-stoppable.test.ts index 88279744af..dc692b3f0a 100644 --- a/src/common/utils/get-startable-stoppable.test.ts +++ b/src/common/utils/get-startable-stoppable.test.ts @@ -2,16 +2,20 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; import { getStartableStoppable } from "./get-startable-stoppable"; +import { getPromiseStatus } from "../test-utils/get-promise-status"; +import { flushPromises } from "../test-utils/flush-promises"; describe("getStartableStoppable", () => { - let stopMock: jest.Mock<() => void>; - let startMock: jest.Mock<() => () => void>; - let actual: { stop: () => void; start: () => void; started: boolean }; + let stopMock: AsyncFnMock<() => Promise>; + let startMock: AsyncFnMock<() => Promise<() => Promise>>; + let actual: { stop: () => Promise; start: () => Promise; started: boolean }; beforeEach(() => { - stopMock = jest.fn(); - startMock = jest.fn(() => stopMock); + stopMock = asyncFn(); + startMock = asyncFn(); actual = getStartableStoppable("some-id", startMock); }); @@ -25,9 +29,7 @@ describe("getStartableStoppable", () => { }); it("when stopping before ever starting, throws", () => { - expect(() => { - actual.stop(); - }).toThrow("Tried to stop \"some-id\", but it has not started yet."); + expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has not started yet."); }); it("is not started", () => { @@ -35,69 +37,180 @@ describe("getStartableStoppable", () => { }); describe("when started", () => { + let startPromise: Promise; + beforeEach(() => { - actual.start(); + startPromise = actual.start(); }); - it("starts", () => { + it("starts starting", () => { expect(startMock).toHaveBeenCalled(); }); - it("is started", () => { - expect(actual.started).toBe(true); + it("starting does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(startPromise); + + expect(promiseStatus.fulfilled).toBe(false); }); - it("when started again, throws", () => { - expect(() => { - actual.start(); - }).toThrow("Tried to start \"some-id\", but it has already started."); + it("is not started yet", () => { + expect(actual.started).toBe(false); }); - it("does not stop yet", () => { - expect(stopMock).not.toHaveBeenCalled(); - }); - - describe("when stopped", () => { - beforeEach(() => { - actual.stop(); + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); }); - it("stops", () => { - expect(stopMock).toHaveBeenCalled(); + it("is started", () => { + expect(actual.started).toBe(true); }); - it("is not started", () => { - expect(actual.started).toBe(false); + it("starting resolves", async () => { + const promiseStatus = await getPromiseStatus(startPromise); + + expect(promiseStatus.fulfilled).toBe(true); }); - it("when stopped again, throws", () => { - expect(() => { - actual.stop(); - }).toThrow("Tried to stop \"some-id\", but it has already stopped."); + it("when started again, throws", () => { + expect(actual.start).rejects.toThrow("Tried to start \"some-id\", but it has already started."); }); - describe("when started again", () => { + it("does not stop yet", () => { + expect(stopMock).not.toHaveBeenCalled(); + }); + + describe("when stopped", () => { + let stopPromise: Promise; + beforeEach(() => { - startMock.mockClear(); - - actual.start(); + stopPromise = actual.stop(); }); - it("starts", () => { - expect(startMock).toHaveBeenCalled(); + it("starts stopping", () => { + expect(stopMock).toHaveBeenCalled(); }); - it("is started", () => { + it("stopping does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("is not stopped yet", () => { expect(actual.started).toBe(true); }); - it("when stopped, stops", () => { - stopMock.mockClear(); + describe("when stopping finishes", () => { + beforeEach(async () => { + await stopMock.resolve(); + }); - actual.stop(); + it("is not started", () => { + expect(actual.started).toBe(false); + }); + it("stopping resolves", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + + it("when stopped again, throws", () => { + expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has already stopped."); + }); + + describe("when started again", () => { + beforeEach( + () => { + startMock.mockClear(); + + actual.start(); + }); + + it("starts", () => { + expect(startMock).toHaveBeenCalled(); + }); + + it("is not started yet", () => { + expect(actual.started).toBe(false); + }); + + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); + }); + + it("is started", () => { + expect(actual.started).toBe(true); + }); + + it("when stopped again, starts stopping again", async () => { + stopMock.mockClear(); + + actual.stop(); + + await flushPromises(); + + expect(stopMock).toHaveBeenCalled(); + }); + }); + }); + }); + }); + }); + + describe("when stopped before starting finishes", () => { + let stopPromise: Promise; + + beforeEach(() => { + stopPromise = actual.stop(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("is not started yet", () => { + expect(actual.started).toBe(false); + }); + + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); + }); + + it("starts stopping", () => { expect(stopMock).toHaveBeenCalled(); }); + + it("is not stopped yet", () => { + expect(actual.started).toBe(true); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when stopping finishes", () => { + beforeEach(async () => { + await stopMock.resolve(); + }); + + it("is stopped", () => { + expect(actual.started).toBe(false); + }); + + it("resolves", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + }); }); }); }); diff --git a/src/common/utils/get-startable-stoppable.ts b/src/common/utils/get-startable-stoppable.ts index 094166de24..59f7964d97 100644 --- a/src/common/utils/get-startable-stoppable.ts +++ b/src/common/utils/get-startable-stoppable.ts @@ -2,32 +2,39 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ + +type Stopper = () => Promise | void; +type Starter = () => Promise | Stopper; + export const getStartableStoppable = ( id: string, - startAndGetStopCallback: () => () => void, + startAndGetStopCallback: Starter, ) => { - let dispose: () => void; + let stop: Stopper; let stopped = false; let started = false; + let startingPromise: Promise | Stopper; return { get started() { return started; }, - start: () => { + start: async () => { if (started) { throw new Error(`Tried to start "${id}", but it has already started.`); } + startingPromise = startAndGetStopCallback(); + stop = await startingPromise; + stopped = false; - - dispose = startAndGetStopCallback(); - started = true; }, - stop: () => { + stop: async () => { + await startingPromise; + if (stopped) { throw new Error(`Tried to stop "${id}", but it has already stopped.`); } @@ -36,10 +43,9 @@ export const getStartableStoppable = ( throw new Error(`Tried to stop "${id}", but it has not started yet.`); } + await stop(); + started = false; - - dispose(); - stopped = true; }, }; diff --git a/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts index b9a3e65838..71afc4d895 100644 --- a/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts +++ b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts @@ -13,8 +13,8 @@ const startCatalogSyncInjectable = getInjectable({ const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable); return { - run: () => { - catalogSyncToRenderer.start(); + run: async () => { + await catalogSyncToRenderer.start(); }, }; }, diff --git a/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts index 9142089d0b..71c5be55f1 100644 --- a/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts +++ b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts @@ -13,9 +13,9 @@ const stopCatalogSyncInjectable = getInjectable({ const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable); return { - run: () => { + run: async () => { if (catalogSyncToRenderer.started) { - catalogSyncToRenderer.stop(); + await catalogSyncToRenderer.stop(); } }, }; diff --git a/src/main/menu/start-application-menu.injectable.ts b/src/main/menu/start-application-menu.injectable.ts index 4fc042c5e5..8cab9bf43a 100644 --- a/src/main/menu/start-application-menu.injectable.ts +++ b/src/main/menu/start-application-menu.injectable.ts @@ -15,8 +15,8 @@ const startApplicationMenuInjectable = getInjectable({ ); return { - run: () => { - applicationMenu.start(); + run: async () => { + await applicationMenu.start(); }, }; }, diff --git a/src/main/menu/stop-application-menu.injectable.ts b/src/main/menu/stop-application-menu.injectable.ts index c84700928a..1492da32de 100644 --- a/src/main/menu/stop-application-menu.injectable.ts +++ b/src/main/menu/stop-application-menu.injectable.ts @@ -15,8 +15,8 @@ const stopApplicationMenuInjectable = getInjectable({ ); return { - run: () => { - applicationMenu.stop(); + run: async () => { + await applicationMenu.stop(); }, }; }, diff --git a/src/main/tray/start-tray.injectable.ts b/src/main/tray/start-tray.injectable.ts index 6892f6186a..5fefc8c1e4 100644 --- a/src/main/tray/start-tray.injectable.ts +++ b/src/main/tray/start-tray.injectable.ts @@ -13,8 +13,8 @@ const startTrayInjectable = getInjectable({ const trayInitializer = di.inject(trayInjectable); return { - run: () => { - trayInitializer.start(); + run: async () => { + await trayInitializer.start(); }, }; }, diff --git a/src/main/tray/stop-tray.injectable.ts b/src/main/tray/stop-tray.injectable.ts index a98f228d17..b1acfccf6a 100644 --- a/src/main/tray/stop-tray.injectable.ts +++ b/src/main/tray/stop-tray.injectable.ts @@ -13,8 +13,8 @@ const stopTrayInjectable = getInjectable({ const trayInitializer = di.inject(trayInjectable); return { - run: () => { - trayInitializer.stop(); + run: async () => { + await trayInitializer.stop(); }, }; }, diff --git a/src/main/tray/tray-icon-path.injectable.ts b/src/main/tray/tray-icon-path.injectable.ts deleted file mode 100644 index 5195b31395..0000000000 --- a/src/main/tray/tray-icon-path.injectable.ts +++ /dev/null @@ -1,26 +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 getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable"; -import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; -import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; - -const trayIconPathInjectable = getInjectable({ - id: "tray-icon-path", - - instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); - const isDevelopment = di.inject(isDevelopmentInjectable); - const staticDir = di.inject(staticFilesDirectoryInjectable); - - return getAbsolutePath( - staticDir, - isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras - "trayIconTemplate.png", - ); - }, -}); - -export default trayIconPathInjectable; diff --git a/src/main/tray/tray.injectable.ts b/src/main/tray/tray.injectable.ts index bfbe6a18f8..e8a0aac131 100644 --- a/src/main/tray/tray.injectable.ts +++ b/src/main/tray/tray.injectable.ts @@ -9,10 +9,8 @@ import navigateToPreferencesInjectable from "../../common/front-end-routing/rout import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable"; import { getStartableStoppable } from "../../common/utils/get-startable-stoppable"; import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; -import trayIconPathInjectable from "./tray-icon-path.injectable"; import showAboutInjectable from "../menu/show-about.injectable"; -import showApplicationWindowInjectable - from "../start-main-application/lens-window/show-application-window.injectable"; +import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; const trayInjectable = getInjectable({ id: "tray", @@ -22,7 +20,6 @@ const trayInjectable = getInjectable({ const navigateToPreferences = di.inject(navigateToPreferencesInjectable); const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); - const trayIconPath = di.inject(trayIconPathInjectable); const showApplicationWindow = di.inject(showApplicationWindowInjectable); const showAboutPopup = di.inject(showAboutInjectable); @@ -32,7 +29,6 @@ const trayInjectable = getInjectable({ navigateToPreferences, stopServicesAndExitApp, isAutoUpdateEnabled, - trayIconPath, showApplicationWindow, showAboutPopup, ),