From 700eb324a26970689f44473bc1527d98cdeff5b8 Mon Sep 17 00:00:00 2001 From: Iku-turso Date: Fri, 10 Jun 2022 15:15:34 +0300 Subject: [PATCH] Make loading contents of a window unit testable Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso Signed-off-by: Janne Savolainen --- ...ning-application-window-using-tray.test.ts | 140 +++++++++++++----- src/main/getDiForUnitTesting.ts | 9 +- .../application-window.injectable.ts | 15 +- ...s => create-electron-window.injectable.ts} | 45 +++--- .../create-lens-window.injectable.ts | 37 +++-- ...undled-extensions-are-loaded.injectable.ts | 29 ++++ .../test-utils/get-application-builder.tsx | 4 +- 7 files changed, 192 insertions(+), 87 deletions(-) rename src/main/start-main-application/lens-window/application-window/{create-electron-window-for.injectable.ts => create-electron-window.injectable.ts} (84%) create mode 100644 src/main/start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable.ts diff --git a/src/behaviours/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts b/src/behaviours/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts index 4a598c8d68..6e43978fde 100644 --- a/src/behaviours/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts +++ b/src/behaviours/quitting-and-restarting-the-app/opening-application-window-using-tray.test.ts @@ -7,7 +7,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { lensWindowInjectionToken } from "../../main/start-main-application/lens-window/application-window/lens-window-injection-token"; import applicationWindowInjectable from "../../main/start-main-application/lens-window/application-window/application-window.injectable"; -import createElectronWindowForInjectable from "../../main/start-main-application/lens-window/application-window/create-electron-window-for.injectable"; +import createElectronWindowForInjectable from "../../main/start-main-application/lens-window/application-window/create-electron-window.injectable"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; @@ -16,37 +16,57 @@ import type { LensWindowConfiguration, } from "../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; -import { flushPromises } from "../../common/test-utils/flush-promises"; import type { DiContainer } from "@ogre-tools/injectable"; +import { flushPromises } from "../../common/test-utils/flush-promises"; +import lensProxyPortInjectable from "../../main/lens-proxy/lens-proxy-port.injectable"; +import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; describe("opening application window using tray", () => { describe("given application has started", () => { let applicationBuilder: ApplicationBuilder; - - let createElectronWindowMock: AsyncFnMock< - (configuration: LensWindowConfiguration) => ElectronWindow - >; - + let createElectronWindowMock: jest.Mock; let expectWindowsToBeOpen: (windowIds: string[]) => void; - let resolveOpeningOfWindow: (windowId: string) => Promise; + let callForSplashWindowHtmlMock: AsyncFnMock<() => void>; + let callForApplicationWindowHtmlMock: AsyncFnMock<() => void>; beforeEach(async () => { applicationBuilder = getApplicationBuilder().beforeApplicationStart( ({ mainDi }) => { - createElectronWindowMock = asyncFn(); + mainDi.override(lensResourcesDirInjectable, () => "some-lens-resources-directory"); + + createElectronWindowMock = jest.fn((configuration: LensWindowConfiguration) => + ({ + splash: { + send: () => {}, + close: () => {}, + show: () => {}, + loadFile: callForSplashWindowHtmlMock, + loadUrl: () => { throw new Error("Should never come here"); }, + }, + + "only-application-window": { + send: () => {}, + close: () => {}, + show: () => {}, + loadFile: () => { throw new Error("Should never come here"); }, + loadUrl: callForApplicationWindowHtmlMock, + }, + }[configuration.id] as ElectronWindow)); mainDi.override( createElectronWindowForInjectable, - () => (configuration) => () => - createElectronWindowMock(configuration), + () => createElectronWindowMock, ); expectWindowsToBeOpen = expectWindowsToBeOpenFor(mainDi); - resolveOpeningOfWindow = resolveOpeningOfWindowFor( - createElectronWindowMock, - ); + callForSplashWindowHtmlMock = asyncFn(); + callForApplicationWindowHtmlMock = asyncFn(); + + const lensProxyPort = mainDi.inject(lensProxyPortInjectable); + + lensProxyPort.set(42); }, ); @@ -54,8 +74,8 @@ describe("opening application window using tray", () => { await flushPromises(); - await resolveOpeningOfWindow("splash"); - await resolveOpeningOfWindow("only-application-window"); + await callForSplashWindowHtmlMock.resolve(); + await callForApplicationWindowHtmlMock.resolve(); await renderPromise; }); @@ -64,6 +84,16 @@ describe("opening application window using tray", () => { expectWindowsToBeOpen(["only-application-window"]); }); + describe("when an attempt to reopen the already started application is made using tray", () => { + beforeEach(() => { + applicationBuilder.tray.click("open-app"); + }); + + it("still shows only the application window", () => { + expectWindowsToBeOpen(["only-application-window"]); + }); + }); + describe("when the application window is closed", () => { beforeEach(() => { const applicationWindow = applicationBuilder.dis.mainDi.inject( @@ -79,6 +109,9 @@ describe("opening application window using tray", () => { describe("when an application window is reopened using tray", () => { beforeEach(() => { + callForSplashWindowHtmlMock.mockClear(); + callForApplicationWindowHtmlMock.mockClear(); + applicationBuilder.tray.click("open-app"); }); @@ -86,6 +119,55 @@ describe("opening application window using tray", () => { expectWindowsToBeOpen([]); }); + it("starts loading static HTML of splash window", () => { + expect(callForSplashWindowHtmlMock).toHaveBeenCalledWith("/some-absolute-root-directory/some-lens-resources-directory/static/splash.html"); + }); + + describe("when loading of splash window HTML resolves", () => { + beforeEach(async () => { + await callForSplashWindowHtmlMock.resolve(); + }); + + it("shows just the splash window", () => { + expectWindowsToBeOpen(["splash"]); + }); + + it("starts loading of content for the application window", () => { + expect(callForApplicationWindowHtmlMock).toHaveBeenCalledWith("http://localhost:42"); + }); + + describe("when static HTML of application window resolves", () => { + beforeEach(async () => { + await callForApplicationWindowHtmlMock.resolve(); + }); + + it("shows just the application window", () => { + expectWindowsToBeOpen(["only-application-window"]); + }); + + describe("when reopening the application using tray", () => { + beforeEach(() => { + callForSplashWindowHtmlMock.mockClear(); + callForApplicationWindowHtmlMock.mockClear(); + + applicationBuilder.tray.click("open-app"); + }); + + it("still shows just the application window", () => { + expectWindowsToBeOpen(["only-application-window"]); + }); + + it("does not load HTML for splash window again", () => { + expect(callForSplashWindowHtmlMock).not.toHaveBeenCalled(); + }); + + it("does not load HTML for application window again", () => { + expect(callForApplicationWindowHtmlMock).not.toHaveBeenCalled(); + }); + }); + }); + }); + describe("given opening of splash window has not finished yet, but another attempt to open the application is made", () => { beforeEach(() => { createElectronWindowMock.mockClear(); @@ -100,15 +182,15 @@ describe("opening application window using tray", () => { describe("when opening of splash window resolves", () => { beforeEach(async () => { - await resolveOpeningOfWindow("splash"); + await callForSplashWindowHtmlMock.resolve(); }); it("still only splash window is open", () => { expectWindowsToBeOpen(["splash"]); }); - it("when opening finishes, only an application window is open", async () => { - await resolveOpeningOfWindow("only-application-window"); + it("when opening of application window finishes, only an application window is open", async () => { + await callForApplicationWindowHtmlMock.resolve(); expectWindowsToBeOpen(["only-application-window"]); }); @@ -125,7 +207,7 @@ describe("opening application window using tray", () => { }); it("when opening finishes, only an application window is open", async () => { - await resolveOpeningOfWindow("only-application-window"); + await callForApplicationWindowHtmlMock.resolve(); expectWindowsToBeOpen(["only-application-window"]); }); @@ -143,21 +225,3 @@ const expectWindowsToBeOpenFor = (di: DiContainer) => (windowIds: string[]) => { windows.filter((window) => window.visible).map((window) => window.id), ).toEqual(windowIds); }; - -const resolveOpeningOfWindowFor = - ( - createElectronWindowMock: AsyncFnMock< - (configuration: LensWindowConfiguration) => ElectronWindow - >, - ) => - async (windowId: string) => { - await createElectronWindowMock.resolveSpecific( - [{ id: windowId }], - - { - send: () => {}, - close: () => {}, - show: () => {}, - }, - ); - }; diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 5f2e968f14..e7f0968237 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -64,7 +64,7 @@ import { observable } from "mobx"; import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable"; import setupListenerForCurrentClusterFrameInjectable from "./start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable"; import ipcMainInjectable from "./utils/channel/ipc-main/ipc-main.injectable"; -import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window-for.injectable"; +import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window.injectable"; import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable"; import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; @@ -97,6 +97,7 @@ import installHelmChartInjectable from "./helm/helm-service/install-helm-chart.i import listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable"; import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable"; import updateHelmReleaseInjectable from "./helm/helm-service/update-helm-release.injectable"; +import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable"; export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { const { @@ -120,6 +121,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) di.preventSideEffects(); if (doGeneralOverrides) { + di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {}); di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(hotbarStoreInjectable, () => ({ load: () => {} })); di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore); @@ -256,7 +258,7 @@ const overrideElectronFeatures = (di: DiContainer) => { throw new Error("Tried to check for platform updates without explicit override."); }); - di.override(createElectronWindowForInjectable, () => () => async () => ({ + di.override(createElectronWindowForInjectable, () => () => ({ show: () => {}, close: () => {}, @@ -266,6 +268,9 @@ const overrideElectronFeatures = (di: DiContainer) => { sendFake(null, arg); }, + + loadFile: async () => {}, + loadUrl: async () => {}, })); di.override( diff --git a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts index c63191c71c..45d7631415 100644 --- a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts @@ -9,9 +9,7 @@ import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injecta import isMacInjectable from "../../../../common/vars/is-mac.injectable"; import appNameInjectable from "../../../app-paths/app-name/app-name.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; -import { delay } from "../../../../common/utils"; -import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; -import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable"; +import waitUntilBundledExtensionsAreLoadedInjectable from "./wait-until-bundled-extensions-are-loaded.injectable"; const applicationWindowInjectable = getInjectable({ id: "application-window", @@ -21,7 +19,7 @@ const applicationWindowInjectable = getInjectable({ const isMac = di.inject(isMacInjectable); const applicationName = di.inject(appNameInjectable); const appEventBus = di.inject(appEventBusInjectable); - const ipcMain = di.inject(ipcMainInjectable); + const waitUntilBundledExtensionsAreLoaded = di.inject(waitUntilBundledExtensionsAreLoadedInjectable); const lensProxyPort = di.inject(lensProxyPortInjectable); return createLensWindow({ @@ -45,14 +43,7 @@ const applicationWindowInjectable = getInjectable({ onDomReady: () => { appEventBus.emit({ name: "app", action: "dom-ready" }); }, - beforeOpen: async () => { - const viewHasLoaded = new Promise((resolve) => { - ipcMain.once(bundledExtensionsLoaded, () => resolve()); - }); - - await viewHasLoaded; - await delay(50); // wait just a bit longer to let the first round of rendering happen - }, + beforeOpen: waitUntilBundledExtensionsAreLoaded, }); }, diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts similarity index 84% rename from src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts rename to src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts index 143f147660..00fcdb6231 100644 --- a/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts @@ -38,21 +38,16 @@ export interface ElectronWindowConfiguration { onDomReady?: () => void; } -export type CreateElectronWindow = () => Promise; -export type CreateElectronWindowFor = (config: ElectronWindowConfiguration) => CreateElectronWindow; +export type CreateElectronWindow = (config: ElectronWindowConfiguration) => ElectronWindow; -function isFileSource(src: ContentSource): src is FileSource { - return typeof (src as FileSource).file === "string"; -} +const createElectronWindowInjectable = getInjectable({ + id: "create-electron-window", -const createElectronWindowFor = getInjectable({ - id: "create-electron-window-for", - - instantiate: (di): CreateElectronWindowFor => { + instantiate: (di): CreateElectronWindow => { const logger = di.inject(loggerInjectable); const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable); - return (configuration) => async () => { + return (configuration) => { const applicationWindowState = di.inject( applicationWindowStateInjectable, { @@ -172,19 +167,23 @@ const createElectronWindowFor = getInjectable({ return { action: "deny" }; }); - const contentSource = configuration.getContentSource(); - - if (isFileSource(contentSource)) { - logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${contentSource.file}...`); - await browserWindow.loadFile(contentSource.file); - } else { - logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentSource.url}...`); - await browserWindow.loadURL(contentSource.url); - } - - await configuration.beforeOpen?.(); - return { + loadFile: async (filePath) => { + logger.info( + `[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${filePath}...`, + ); + + await browserWindow.loadFile(filePath); + }, + + loadUrl: async (url) => { + logger.info( + `[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${url}...`, + ); + + await browserWindow.loadURL(url); + }, + show: () => browserWindow.show(), close: () => browserWindow.close(), send: (args) => sendToChannelInLensWindow(browserWindow, args), @@ -195,4 +194,4 @@ const createElectronWindowFor = getInjectable({ causesSideEffects: true, }); -export default createElectronWindowFor; +export default createElectronWindowInjectable; diff --git a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts index 2971a82614..dcd42f2333 100644 --- a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts @@ -4,13 +4,15 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { LensWindow, SendToViewArgs } from "./lens-window-injection-token"; -import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable"; -import createElectronWindowForInjectable from "./create-electron-window-for.injectable"; +import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window.injectable"; +import createElectronWindowForInjectable from "./create-electron-window.injectable"; export interface ElectronWindow { show: () => void; close: () => void; send: (args: SendToViewArgs) => void; + loadFile: (filePath: string) => Promise; + loadUrl: (url: string) => Promise; } export interface LensWindowConfiguration { @@ -33,23 +35,19 @@ const createLensWindowInjectable = getInjectable({ id: "create-lens-window", instantiate: (di) => { - const createElectronWindowFor = di.inject(createElectronWindowForInjectable); + const createElectronWindow = di.inject(createElectronWindowForInjectable); return (configuration: LensWindowConfiguration): LensWindow => { let browserWindow: ElectronWindow | undefined; - const createElectronWindow = createElectronWindowFor({ - ...configuration, - onClose: () => browserWindow = undefined, - }); - let windowIsOpening = false; + let contentIsLoading = false; return { id: configuration.id, get visible() { - return !!browserWindow; + return !!browserWindow && !contentIsLoading; }, get opening() { @@ -59,7 +57,26 @@ const createLensWindowInjectable = getInjectable({ show: async () => { if (!browserWindow) { windowIsOpening = true; - browserWindow = await createElectronWindow(); + + browserWindow = createElectronWindow({ + ...configuration, + onClose: () => browserWindow = undefined, + }); + + const windowFilePath = configuration.getContentSource().file; + const windowUrl = configuration.getContentSource().url; + + contentIsLoading = true; + + if (windowFilePath) { + await browserWindow.loadFile(windowFilePath); + } else if (windowUrl) { + await browserWindow.loadUrl(windowUrl); + } + + await configuration.beforeOpen?.(); + + contentIsLoading = false; } browserWindow.show(); diff --git a/src/main/start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable.ts b/src/main/start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable.ts new file mode 100644 index 0000000000..2c0f30b83b --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; +import { delay } from "../../../../common/utils"; +import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable"; + +const waitUntilBundledExtensionsAreLoadedInjectable = getInjectable({ + id: "wait-until-bundled-extensions-are-loaded", + + instantiate: (di) => { + const ipcMain = di.inject(ipcMainInjectable); + + return async () => { + const viewHasLoaded = new Promise((resolve) => { + ipcMain.once(bundledExtensionsLoaded, () => resolve()); + }); + + await viewHasLoaded; + await delay(50); // wait just a bit longer to let the first round of rendering happen + }; + }, + + causesSideEffects: true, +}); + +export default waitUntilBundledExtensionsAreLoadedInjectable; diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 3e052d9f93..f6cd766ce6 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -181,13 +181,13 @@ export const getApplicationBuilder = () => { computed(() => []), ); - const iconPaths = mainDi.inject(trayIconPathsInjectable); - let trayMenuItemsStateFake: TrayMenuItem[]; let trayMenuIconPath: string; mainDi.override(electronTrayInjectable, () => ({ start: () => { + const iconPaths = mainDi.inject(trayIconPathsInjectable); + trayMenuIconPath = iconPaths.normal; }, stop: () => {},