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

Make loading contents of a window unit testable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Iku-turso 2022-06-10 15:15:34 +03:00 committed by Janne Savolainen
parent 7b043afe42
commit 700eb324a2
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
7 changed files with 192 additions and 87 deletions

View File

@ -7,7 +7,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; 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 { 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 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 type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
@ -16,37 +16,57 @@ import type {
LensWindowConfiguration, LensWindowConfiguration,
} from "../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; } 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 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("opening application window using tray", () => {
describe("given application has started", () => { describe("given application has started", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
let createElectronWindowMock: jest.Mock;
let createElectronWindowMock: AsyncFnMock<
(configuration: LensWindowConfiguration) => ElectronWindow
>;
let expectWindowsToBeOpen: (windowIds: string[]) => void; let expectWindowsToBeOpen: (windowIds: string[]) => void;
let resolveOpeningOfWindow: (windowId: string) => Promise<void>; let callForSplashWindowHtmlMock: AsyncFnMock<() => void>;
let callForApplicationWindowHtmlMock: AsyncFnMock<() => void>;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder().beforeApplicationStart( applicationBuilder = getApplicationBuilder().beforeApplicationStart(
({ mainDi }) => { ({ 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( mainDi.override(
createElectronWindowForInjectable, createElectronWindowForInjectable,
() => (configuration) => () => () => createElectronWindowMock,
createElectronWindowMock(configuration),
); );
expectWindowsToBeOpen = expectWindowsToBeOpenFor(mainDi); expectWindowsToBeOpen = expectWindowsToBeOpenFor(mainDi);
resolveOpeningOfWindow = resolveOpeningOfWindowFor( callForSplashWindowHtmlMock = asyncFn();
createElectronWindowMock, callForApplicationWindowHtmlMock = asyncFn();
);
const lensProxyPort = mainDi.inject(lensProxyPortInjectable);
lensProxyPort.set(42);
}, },
); );
@ -54,8 +74,8 @@ describe("opening application window using tray", () => {
await flushPromises(); await flushPromises();
await resolveOpeningOfWindow("splash"); await callForSplashWindowHtmlMock.resolve();
await resolveOpeningOfWindow("only-application-window"); await callForApplicationWindowHtmlMock.resolve();
await renderPromise; await renderPromise;
}); });
@ -64,6 +84,16 @@ describe("opening application window using tray", () => {
expectWindowsToBeOpen(["only-application-window"]); 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", () => { describe("when the application window is closed", () => {
beforeEach(() => { beforeEach(() => {
const applicationWindow = applicationBuilder.dis.mainDi.inject( 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", () => { describe("when an application window is reopened using tray", () => {
beforeEach(() => { beforeEach(() => {
callForSplashWindowHtmlMock.mockClear();
callForApplicationWindowHtmlMock.mockClear();
applicationBuilder.tray.click("open-app"); applicationBuilder.tray.click("open-app");
}); });
@ -86,6 +119,55 @@ describe("opening application window using tray", () => {
expectWindowsToBeOpen([]); 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", () => { describe("given opening of splash window has not finished yet, but another attempt to open the application is made", () => {
beforeEach(() => { beforeEach(() => {
createElectronWindowMock.mockClear(); createElectronWindowMock.mockClear();
@ -100,15 +182,15 @@ describe("opening application window using tray", () => {
describe("when opening of splash window resolves", () => { describe("when opening of splash window resolves", () => {
beforeEach(async () => { beforeEach(async () => {
await resolveOpeningOfWindow("splash"); await callForSplashWindowHtmlMock.resolve();
}); });
it("still only splash window is open", () => { it("still only splash window is open", () => {
expectWindowsToBeOpen(["splash"]); expectWindowsToBeOpen(["splash"]);
}); });
it("when opening finishes, only an application window is open", async () => { it("when opening of application window finishes, only an application window is open", async () => {
await resolveOpeningOfWindow("only-application-window"); await callForApplicationWindowHtmlMock.resolve();
expectWindowsToBeOpen(["only-application-window"]); 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 () => { it("when opening finishes, only an application window is open", async () => {
await resolveOpeningOfWindow("only-application-window"); await callForApplicationWindowHtmlMock.resolve();
expectWindowsToBeOpen(["only-application-window"]); expectWindowsToBeOpen(["only-application-window"]);
}); });
@ -143,21 +225,3 @@ const expectWindowsToBeOpenFor = (di: DiContainer) => (windowIds: string[]) => {
windows.filter((window) => window.visible).map((window) => window.id), windows.filter((window) => window.visible).map((window) => window.id),
).toEqual(windowIds); ).toEqual(windowIds);
}; };
const resolveOpeningOfWindowFor =
(
createElectronWindowMock: AsyncFnMock<
(configuration: LensWindowConfiguration) => ElectronWindow
>,
) =>
async (windowId: string) => {
await createElectronWindowMock.resolveSpecific(
[{ id: windowId }],
{
send: () => {},
close: () => {},
show: () => {},
},
);
};

View File

@ -64,7 +64,7 @@ import { observable } from "mobx";
import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable"; 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 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 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 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 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"; 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 listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable";
import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable"; import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable";
import updateHelmReleaseInjectable from "./helm/helm-service/update-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 } = {}) { export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const { const {
@ -120,6 +121,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.preventSideEffects(); di.preventSideEffects();
if (doGeneralOverrides) { if (doGeneralOverrides) {
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(hotbarStoreInjectable, () => ({ load: () => {} })); di.override(hotbarStoreInjectable, () => ({ load: () => {} }));
di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore); 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."); throw new Error("Tried to check for platform updates without explicit override.");
}); });
di.override(createElectronWindowForInjectable, () => () => async () => ({ di.override(createElectronWindowForInjectable, () => () => ({
show: () => {}, show: () => {},
close: () => {}, close: () => {},
@ -266,6 +268,9 @@ const overrideElectronFeatures = (di: DiContainer) => {
sendFake(null, arg); sendFake(null, arg);
}, },
loadFile: async () => {},
loadUrl: async () => {},
})); }));
di.override( di.override(

View File

@ -9,9 +9,7 @@ import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injecta
import isMacInjectable from "../../../../common/vars/is-mac.injectable"; import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import appNameInjectable from "../../../app-paths/app-name/app-name.injectable"; import appNameInjectable from "../../../app-paths/app-name/app-name.injectable";
import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
import { delay } from "../../../../common/utils"; import waitUntilBundledExtensionsAreLoadedInjectable from "./wait-until-bundled-extensions-are-loaded.injectable";
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
const applicationWindowInjectable = getInjectable({ const applicationWindowInjectable = getInjectable({
id: "application-window", id: "application-window",
@ -21,7 +19,7 @@ const applicationWindowInjectable = getInjectable({
const isMac = di.inject(isMacInjectable); const isMac = di.inject(isMacInjectable);
const applicationName = di.inject(appNameInjectable); const applicationName = di.inject(appNameInjectable);
const appEventBus = di.inject(appEventBusInjectable); const appEventBus = di.inject(appEventBusInjectable);
const ipcMain = di.inject(ipcMainInjectable); const waitUntilBundledExtensionsAreLoaded = di.inject(waitUntilBundledExtensionsAreLoadedInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable); const lensProxyPort = di.inject(lensProxyPortInjectable);
return createLensWindow({ return createLensWindow({
@ -45,14 +43,7 @@ const applicationWindowInjectable = getInjectable({
onDomReady: () => { onDomReady: () => {
appEventBus.emit({ name: "app", action: "dom-ready" }); appEventBus.emit({ name: "app", action: "dom-ready" });
}, },
beforeOpen: async () => { beforeOpen: waitUntilBundledExtensionsAreLoaded,
const viewHasLoaded = new Promise<void>((resolve) => {
ipcMain.once(bundledExtensionsLoaded, () => resolve());
});
await viewHasLoaded;
await delay(50); // wait just a bit longer to let the first round of rendering happen
},
}); });
}, },

View File

@ -38,21 +38,16 @@ export interface ElectronWindowConfiguration {
onDomReady?: () => void; onDomReady?: () => void;
} }
export type CreateElectronWindow = () => Promise<ElectronWindow>; export type CreateElectronWindow = (config: ElectronWindowConfiguration) => ElectronWindow;
export type CreateElectronWindowFor = (config: ElectronWindowConfiguration) => CreateElectronWindow;
function isFileSource(src: ContentSource): src is FileSource { const createElectronWindowInjectable = getInjectable({
return typeof (src as FileSource).file === "string"; id: "create-electron-window",
}
const createElectronWindowFor = getInjectable({ instantiate: (di): CreateElectronWindow => {
id: "create-electron-window-for",
instantiate: (di): CreateElectronWindowFor => {
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable); const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable);
return (configuration) => async () => { return (configuration) => {
const applicationWindowState = di.inject( const applicationWindowState = di.inject(
applicationWindowStateInjectable, applicationWindowStateInjectable,
{ {
@ -172,19 +167,23 @@ const createElectronWindowFor = getInjectable({
return { action: "deny" }; 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 { 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(), show: () => browserWindow.show(),
close: () => browserWindow.close(), close: () => browserWindow.close(),
send: (args) => sendToChannelInLensWindow(browserWindow, args), send: (args) => sendToChannelInLensWindow(browserWindow, args),
@ -195,4 +194,4 @@ const createElectronWindowFor = getInjectable({
causesSideEffects: true, causesSideEffects: true,
}); });
export default createElectronWindowFor; export default createElectronWindowInjectable;

View File

@ -4,13 +4,15 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { LensWindow, SendToViewArgs } from "./lens-window-injection-token"; import type { LensWindow, SendToViewArgs } from "./lens-window-injection-token";
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable"; import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window.injectable";
import createElectronWindowForInjectable from "./create-electron-window-for.injectable"; import createElectronWindowForInjectable from "./create-electron-window.injectable";
export interface ElectronWindow { export interface ElectronWindow {
show: () => void; show: () => void;
close: () => void; close: () => void;
send: (args: SendToViewArgs) => void; send: (args: SendToViewArgs) => void;
loadFile: (filePath: string) => Promise<void>;
loadUrl: (url: string) => Promise<void>;
} }
export interface LensWindowConfiguration { export interface LensWindowConfiguration {
@ -33,23 +35,19 @@ const createLensWindowInjectable = getInjectable({
id: "create-lens-window", id: "create-lens-window",
instantiate: (di) => { instantiate: (di) => {
const createElectronWindowFor = di.inject(createElectronWindowForInjectable); const createElectronWindow = di.inject(createElectronWindowForInjectable);
return (configuration: LensWindowConfiguration): LensWindow => { return (configuration: LensWindowConfiguration): LensWindow => {
let browserWindow: ElectronWindow | undefined; let browserWindow: ElectronWindow | undefined;
const createElectronWindow = createElectronWindowFor({
...configuration,
onClose: () => browserWindow = undefined,
});
let windowIsOpening = false; let windowIsOpening = false;
let contentIsLoading = false;
return { return {
id: configuration.id, id: configuration.id,
get visible() { get visible() {
return !!browserWindow; return !!browserWindow && !contentIsLoading;
}, },
get opening() { get opening() {
@ -59,7 +57,26 @@ const createLensWindowInjectable = getInjectable({
show: async () => { show: async () => {
if (!browserWindow) { if (!browserWindow) {
windowIsOpening = true; 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(); browserWindow.show();

View File

@ -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<void>((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;

View File

@ -181,13 +181,13 @@ export const getApplicationBuilder = () => {
computed(() => []), computed(() => []),
); );
const iconPaths = mainDi.inject(trayIconPathsInjectable);
let trayMenuItemsStateFake: TrayMenuItem[]; let trayMenuItemsStateFake: TrayMenuItem[];
let trayMenuIconPath: string; let trayMenuIconPath: string;
mainDi.override(electronTrayInjectable, () => ({ mainDi.override(electronTrayInjectable, () => ({
start: () => { start: () => {
const iconPaths = mainDi.inject(trayIconPathsInjectable);
trayMenuIconPath = iconPaths.normal; trayMenuIconPath = iconPaths.normal;
}, },
stop: () => {}, stop: () => {},