mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Prevent duplicate app windows (#5533)
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
parent
5c1f0daf50
commit
6451df1f17
@ -37,8 +37,8 @@ describe("extensions - navigation using application menu", () => {
|
||||
});
|
||||
|
||||
describe("when navigating to extensions using application menu", () => {
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.applicationMenu.click("root.extensions");
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("root.extensions");
|
||||
});
|
||||
|
||||
it("focuses the window", () => {
|
||||
|
||||
@ -28,8 +28,8 @@ describe("preferences - navigation using application menu", () => {
|
||||
});
|
||||
|
||||
describe("when navigating to preferences using application menu", () => {
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.applicationMenu.click("root.preferences");
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("root.preferences");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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 { 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.injectable";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { ElectronWindow, LensWindowConfiguration } from "../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { flushPromises } from "../../common/test-utils/flush-promises";
|
||||
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: jest.Mock;
|
||||
let expectWindowsToBeOpen: (windowIds: string[]) => void;
|
||||
let callForSplashWindowHtmlMock: AsyncFnMock<() => void>;
|
||||
let callForApplicationWindowHtmlMock: AsyncFnMock<() => void>;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationBuilder = getApplicationBuilder().beforeApplicationStart(
|
||||
({ mainDi }) => {
|
||||
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,
|
||||
|
||||
() => createElectronWindowMock,
|
||||
);
|
||||
|
||||
expectWindowsToBeOpen = expectWindowsToBeOpenFor(mainDi);
|
||||
|
||||
callForSplashWindowHtmlMock = asyncFn();
|
||||
callForApplicationWindowHtmlMock = asyncFn();
|
||||
},
|
||||
);
|
||||
|
||||
const renderPromise = applicationBuilder.render();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
await callForSplashWindowHtmlMock.resolve();
|
||||
await callForApplicationWindowHtmlMock.resolve();
|
||||
|
||||
await renderPromise;
|
||||
});
|
||||
|
||||
it("only an application window is open", () => {
|
||||
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(
|
||||
applicationWindowInjectable,
|
||||
);
|
||||
|
||||
applicationWindow.close();
|
||||
});
|
||||
|
||||
it("no windows are open", () => {
|
||||
expectWindowsToBeOpen([]);
|
||||
});
|
||||
|
||||
describe("when an application window is reopened using tray", () => {
|
||||
beforeEach(() => {
|
||||
callForSplashWindowHtmlMock.mockClear();
|
||||
callForApplicationWindowHtmlMock.mockClear();
|
||||
|
||||
applicationBuilder.tray.click("open-app");
|
||||
});
|
||||
|
||||
it("still no windows are open", () => {
|
||||
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("given static HTML of application window has not resolved yet, when opening from tray again", () => {
|
||||
beforeEach(() => {
|
||||
callForApplicationWindowHtmlMock.mockClear();
|
||||
callForSplashWindowHtmlMock.mockClear();
|
||||
|
||||
applicationBuilder.tray.click("open-app");
|
||||
});
|
||||
|
||||
it("does not load contents of splash window again", () => {
|
||||
expect(callForSplashWindowHtmlMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not load contents of application window again", () => {
|
||||
expect(callForApplicationWindowHtmlMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows just the blank application window to permit developer tool access", () => {
|
||||
expectWindowsToBeOpen(["only-application-window"]);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
applicationBuilder.tray.click("open-app");
|
||||
});
|
||||
|
||||
it("does not open any new windows", () => {
|
||||
expect(createElectronWindowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when opening of splash window resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForSplashWindowHtmlMock.resolve();
|
||||
});
|
||||
|
||||
it("still only splash window is open", () => {
|
||||
expectWindowsToBeOpen(["splash"]);
|
||||
});
|
||||
|
||||
it("when opening of application window finishes, only an application window is open", async () => {
|
||||
await callForApplicationWindowHtmlMock.resolve();
|
||||
|
||||
expectWindowsToBeOpen(["only-application-window"]);
|
||||
});
|
||||
|
||||
describe("given opening of application window has not finished yet, but another attempt to open the application is made", () => {
|
||||
beforeEach(() => {
|
||||
createElectronWindowMock.mockClear();
|
||||
|
||||
applicationBuilder.tray.click("open-app");
|
||||
});
|
||||
|
||||
it("does not open any new windows", () => {
|
||||
expect(createElectronWindowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when opening finishes, only an application window is open", async () => {
|
||||
await callForApplicationWindowHtmlMock.resolve();
|
||||
|
||||
expectWindowsToBeOpen(["only-application-window"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const expectWindowsToBeOpenFor = (di: DiContainer) => (windowIds: string[]) => {
|
||||
const windows = di.injectMany(lensWindowInjectionToken);
|
||||
|
||||
expect(
|
||||
windows.filter((window) => window.isVisible).map((window) => window.id),
|
||||
).toEqual(windowIds);
|
||||
};
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 type { ClusterManager } from "../../main/cluster-manager";
|
||||
import { lensWindowInjectionToken } from "../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||
import exitAppInjectable from "../../main/electron-app/features/exit-app.injectable";
|
||||
import clusterManagerInjectable from "../../main/cluster-manager.injectable";
|
||||
import stopServicesAndExitAppInjectable from "../../main/stop-services-and-exit-app.injectable";
|
||||
|
||||
describe("quitting the app using application menu", () => {
|
||||
describe("given application has started", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
let clusterManagerStub: ClusterManager;
|
||||
let exitAppMock: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
applicationBuilder = getApplicationBuilder().beforeApplicationStart(
|
||||
({ mainDi }) => {
|
||||
mainDi.unoverride(stopServicesAndExitAppInjectable);
|
||||
|
||||
clusterManagerStub = { stop: jest.fn() } as unknown as ClusterManager;
|
||||
mainDi.override(clusterManagerInjectable, () => clusterManagerStub);
|
||||
|
||||
exitAppMock = jest.fn();
|
||||
mainDi.override(exitAppInjectable, () => exitAppMock);
|
||||
},
|
||||
);
|
||||
|
||||
await applicationBuilder.render();
|
||||
});
|
||||
|
||||
it("only an application window is open", () => {
|
||||
const windows = applicationBuilder.dis.mainDi.injectMany(
|
||||
lensWindowInjectionToken,
|
||||
);
|
||||
|
||||
expect(
|
||||
windows.map((window) => ({ id: window.id, visible: window.isVisible })),
|
||||
).toEqual([
|
||||
{ id: "only-application-window", visible: true },
|
||||
{ id: "splash", visible: false },
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when application is quit", () => {
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("root.quit");
|
||||
});
|
||||
|
||||
it("closes all windows", () => {
|
||||
const windows = applicationBuilder.dis.mainDi.injectMany(
|
||||
lensWindowInjectionToken,
|
||||
);
|
||||
|
||||
expect(
|
||||
windows.map((window) => ({ id: window.id, visible: window.isVisible })),
|
||||
).toEqual([
|
||||
{ id: "only-application-window", visible: false },
|
||||
{ id: "splash", visible: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("disconnects all clusters", () => {
|
||||
expect(clusterManagerStub.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("after insufficient time passes, does not terminate application yet", () => {
|
||||
jest.advanceTimersByTime(999);
|
||||
|
||||
expect(exitAppMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("after sufficient time passes", () => {
|
||||
beforeEach(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
it("terminates application", () => {
|
||||
expect(exitAppMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -28,8 +28,8 @@ describe("welcome - navigation using application menu", () => {
|
||||
});
|
||||
|
||||
describe("when navigating to welcome using application menu", () => {
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.applicationMenu.click("help.welcome");
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("help.welcome");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -70,13 +70,13 @@ describe("channel", () => {
|
||||
closeAllWindows();
|
||||
});
|
||||
|
||||
describe("given window is shown", () => {
|
||||
describe("given window is started", () => {
|
||||
let someWindowFake: LensWindow;
|
||||
|
||||
beforeEach(async () => {
|
||||
someWindowFake = createTestWindow(mainDi, "some-window");
|
||||
|
||||
await someWindowFake.show();
|
||||
await someWindowFake.start();
|
||||
});
|
||||
|
||||
it("when sending message, triggers listener in window", () => {
|
||||
@ -94,12 +94,12 @@ describe("channel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("given multiple shown windows, when sending message, triggers listeners in all windows", async () => {
|
||||
it("given multiple started windows, when sending message, triggers listeners in all windows", async () => {
|
||||
const someWindowFake = createTestWindow(mainDi, "some-window");
|
||||
const someOtherWindowFake = createTestWindow(mainDi, "some-other-window");
|
||||
|
||||
await someWindowFake.show();
|
||||
await someOtherWindowFake.show();
|
||||
await someWindowFake.start();
|
||||
await someOtherWindowFake.start();
|
||||
|
||||
messageToChannel(testMessageChannel, "some-message");
|
||||
|
||||
|
||||
@ -5,23 +5,19 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { beforeQuitOfFrontEndInjectionToken } from "../../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
|
||||
import electronAppInjectable from "../../electron-app.injectable";
|
||||
import { lensWindowInjectionToken } from "../../../start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { filter, isEmpty } from "lodash/fp";
|
||||
import { isEmpty } from "lodash/fp";
|
||||
import getVisibleWindowsInjectable from "../../../start-main-application/lens-window/get-visible-windows.injectable";
|
||||
|
||||
const hideDockForLastClosedWindowInjectable = getInjectable({
|
||||
id: "hide-dock-when-there-are-no-windows",
|
||||
|
||||
instantiate: (di) => {
|
||||
const app = di.inject(electronAppInjectable);
|
||||
const getLensWindows = () => di.injectMany(lensWindowInjectionToken);
|
||||
const getVisibleWindows = di.inject(getVisibleWindowsInjectable);
|
||||
|
||||
return {
|
||||
run: () => {
|
||||
const visibleWindows = pipeline(
|
||||
getLensWindows(),
|
||||
filter(window => !!window.visible),
|
||||
);
|
||||
const visibleWindows = getVisibleWindows();
|
||||
|
||||
if (isEmpty(visibleWindows)) {
|
||||
app.dock?.hide();
|
||||
|
||||
@ -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);
|
||||
@ -242,7 +244,7 @@ const overrideElectronFeatures = (di: DiContainer) => {
|
||||
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
|
||||
di.override(requestSingleInstanceLockInjectable, () => () => true);
|
||||
di.override(disableHardwareAccelerationInjectable, () => () => {});
|
||||
di.override(shouldStartHiddenInjectable, () => true);
|
||||
di.override(shouldStartHiddenInjectable, () => false);
|
||||
di.override(showMessagePopupInjectable, () => () => {});
|
||||
di.override(waitForElectronToBeReadyInjectable, () => () => Promise.resolve());
|
||||
di.override(ipcMainInjectable, () => ({}));
|
||||
@ -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(
|
||||
|
||||
@ -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<void>((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,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import applicationWindowStateInjectable from "./application-window-state.injecta
|
||||
import { BrowserWindow } from "electron";
|
||||
import { openBrowser } from "../../../../common/utils";
|
||||
import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable";
|
||||
import type { LensWindow } from "./create-lens-window.injectable";
|
||||
import type { ElectronWindow } from "./create-lens-window.injectable";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
|
||||
export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover";
|
||||
@ -38,21 +38,16 @@ export interface ElectronWindowConfiguration {
|
||||
onDomReady?: () => void;
|
||||
}
|
||||
|
||||
export type CreateElectronWindow = () => Promise<LensWindow>;
|
||||
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;
|
||||
@ -3,14 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { 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 { LensWindow, SendToViewArgs } from "./lens-window-injection-token";
|
||||
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window.injectable";
|
||||
import createElectronWindowForInjectable from "./create-electron-window.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export interface LensWindow {
|
||||
export interface ElectronWindow {
|
||||
show: () => void;
|
||||
close: () => void;
|
||||
send: (args: SendToViewArgs) => void;
|
||||
loadFile: (filePath: string) => Promise<void>;
|
||||
loadUrl: (url: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface LensWindowConfiguration {
|
||||
@ -33,31 +36,69 @@ const createLensWindowInjectable = getInjectable({
|
||||
id: "create-lens-window",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createElectronWindowFor = di.inject(createElectronWindowForInjectable);
|
||||
const createElectronWindow = di.inject(createElectronWindowForInjectable);
|
||||
|
||||
return (configuration: LensWindowConfiguration) => {
|
||||
let browserWindow: LensWindow | undefined;
|
||||
return (configuration: LensWindowConfiguration): LensWindow => {
|
||||
let browserWindow: ElectronWindow | undefined;
|
||||
|
||||
const createElectronWindow = createElectronWindowFor({
|
||||
...configuration,
|
||||
onClose: () => browserWindow = undefined,
|
||||
});
|
||||
let windowIsShown = false;
|
||||
let windowIsStarting = false;
|
||||
|
||||
return {
|
||||
get visible() {
|
||||
return !!browserWindow;
|
||||
},
|
||||
show: async () => {
|
||||
if (!browserWindow) {
|
||||
browserWindow = await createElectronWindow();
|
||||
}
|
||||
const showWindow = () => {
|
||||
assert(browserWindow);
|
||||
|
||||
browserWindow.show();
|
||||
windowIsShown = true;
|
||||
};
|
||||
|
||||
return {
|
||||
id: configuration.id,
|
||||
|
||||
get isVisible() {
|
||||
return windowIsShown;
|
||||
},
|
||||
|
||||
get isStarting() {
|
||||
return windowIsStarting;
|
||||
},
|
||||
|
||||
start: async () => {
|
||||
if (!browserWindow) {
|
||||
windowIsStarting = true;
|
||||
|
||||
browserWindow = createElectronWindow({
|
||||
...configuration,
|
||||
onClose: () => {
|
||||
browserWindow = undefined;
|
||||
windowIsShown = false;
|
||||
},
|
||||
});
|
||||
|
||||
const { file: filePathForContent, url: urlForContent } =
|
||||
configuration.getContentSource();
|
||||
|
||||
if (filePathForContent) {
|
||||
await browserWindow.loadFile(filePathForContent);
|
||||
} else if (urlForContent) {
|
||||
await browserWindow.loadUrl(urlForContent);
|
||||
}
|
||||
|
||||
await configuration.beforeOpen?.();
|
||||
}
|
||||
|
||||
showWindow();
|
||||
|
||||
windowIsStarting = false;
|
||||
},
|
||||
|
||||
show: showWindow,
|
||||
|
||||
close: () => {
|
||||
browserWindow?.close();
|
||||
browserWindow = undefined;
|
||||
windowIsShown = false;
|
||||
},
|
||||
|
||||
send: (args: SendToViewArgs) => {
|
||||
if (!browserWindow) {
|
||||
throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`);
|
||||
|
||||
@ -12,10 +12,13 @@ export interface SendToViewArgs {
|
||||
}
|
||||
|
||||
export interface LensWindow {
|
||||
show: () => Promise<void>;
|
||||
id: string;
|
||||
start: () => Promise<void>;
|
||||
close: () => void;
|
||||
show: () => void;
|
||||
send: (args: SendToViewArgs) => void;
|
||||
visible: boolean;
|
||||
isVisible: boolean;
|
||||
isStarting: boolean;
|
||||
}
|
||||
|
||||
export const lensWindowInjectionToken = getInjectionToken<LensWindow>({
|
||||
|
||||
@ -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;
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { filter } from "lodash/fp";
|
||||
import { lensWindowInjectionToken } from "./application-window/lens-window-injection-token";
|
||||
|
||||
const getVisibleWindowsInjectable = getInjectable({
|
||||
id: "get-visible-windows",
|
||||
|
||||
instantiate: (di) => {
|
||||
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
|
||||
|
||||
return () =>
|
||||
pipeline(
|
||||
getAllLensWindows(),
|
||||
filter((lensWindow) => !!lensWindow.isVisible),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default getVisibleWindowsInjectable;
|
||||
@ -5,25 +5,36 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import splashWindowInjectable from "./splash-window/splash-window.injectable";
|
||||
import applicationWindowInjectable from "./application-window/application-window.injectable";
|
||||
import { identity, some } from "lodash/fp";
|
||||
const someIsTruthy = some(identity);
|
||||
|
||||
const showApplicationWindowInjectable = getInjectable({
|
||||
id: "show-application-window",
|
||||
|
||||
instantiate: (di) => {
|
||||
const applicationWindow = di.inject(applicationWindowInjectable);
|
||||
|
||||
const splashWindow = di.inject(
|
||||
splashWindowInjectable,
|
||||
);
|
||||
const splashWindow = di.inject(splashWindowInjectable);
|
||||
|
||||
return async () => {
|
||||
if (applicationWindow.visible) {
|
||||
if (applicationWindow.isStarting) {
|
||||
applicationWindow.show();
|
||||
splashWindow.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await splashWindow.show();
|
||||
const windowIsAlreadyBeingShown = someIsTruthy([
|
||||
applicationWindow.isVisible,
|
||||
splashWindow.isStarting,
|
||||
]);
|
||||
|
||||
await applicationWindow.show();
|
||||
if (windowIsAlreadyBeingShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
await splashWindow.start();
|
||||
|
||||
await applicationWindow.start();
|
||||
|
||||
splashWindow.close();
|
||||
};
|
||||
|
||||
@ -49,7 +49,7 @@ const startMainApplicationInjectable = getInjectable({
|
||||
await beforeApplicationIsLoading();
|
||||
|
||||
if (!shouldStartHidden) {
|
||||
await splashWindow.show();
|
||||
await splashWindow.start();
|
||||
}
|
||||
|
||||
await onLoadOfApplication();
|
||||
@ -60,7 +60,7 @@ const startMainApplicationInjectable = getInjectable({
|
||||
if (deepLinkUrl) {
|
||||
await openDeepLink(deepLinkUrl);
|
||||
} else {
|
||||
await applicationWindow.show();
|
||||
await applicationWindow.start();
|
||||
}
|
||||
|
||||
splashWindow.close();
|
||||
|
||||
@ -2,32 +2,24 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { filter } from "lodash/fp";
|
||||
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
|
||||
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
|
||||
import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
|
||||
import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable";
|
||||
|
||||
const messageToChannelInjectable = getInjectable({
|
||||
id: "message-to-channel",
|
||||
|
||||
instantiate: (di) => {
|
||||
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
|
||||
const getVisibleWindows = di.inject(getVisibleWindowsInjectable);
|
||||
|
||||
// TODO: Figure out way to improve typing in internals
|
||||
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
|
||||
return (channel: MessageChannel<any>, message?: unknown) => {
|
||||
const stringifiedMessage = tentativeStringifyJson(message);
|
||||
|
||||
|
||||
const visibleWindows = pipeline(
|
||||
getAllLensWindows(),
|
||||
filter((lensWindow) => !!lensWindow.visible),
|
||||
);
|
||||
|
||||
visibleWindows.forEach((lensWindow) =>
|
||||
getVisibleWindows().forEach((lensWindow) =>
|
||||
lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }),
|
||||
);
|
||||
};
|
||||
|
||||
@ -43,9 +43,9 @@ describe("message to channel from main", () => {
|
||||
expect(sendToChannelInBrowserMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("given visible window", () => {
|
||||
describe("given started window", () => {
|
||||
beforeEach(async () => {
|
||||
await someTestWindow.show();
|
||||
await someTestWindow.start();
|
||||
});
|
||||
|
||||
it("when messaging to channel, messages to window", () => {
|
||||
@ -109,9 +109,9 @@ describe("message to channel from main", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("given multiple visible windows, when messaging to channel, messages to window", async () => {
|
||||
await someTestWindow.show();
|
||||
await someOtherTestWindow.show();
|
||||
it("given multiple started windows, when messaging to channel, messages to window", async () => {
|
||||
await someTestWindow.start();
|
||||
await someOtherTestWindow.start();
|
||||
|
||||
messageToChannel(someChannel, "some-message");
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@ import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||
import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable";
|
||||
import startFrameInjectable from "../../start-frame/start-frame.injectable";
|
||||
import { flushPromises } from "../../../common/test-utils/flush-promises";
|
||||
import type { NamespaceStore } from "../+namespaces/store";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import historyInjectable from "../../navigation/history.injectable";
|
||||
@ -56,6 +55,7 @@ import assert from "assert";
|
||||
import { openMenu } from "react-select-event";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { StatusBar } from "../status-bar/status-bar";
|
||||
import lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable";
|
||||
|
||||
type Callback = (dis: DiContainers) => void | Promise<void>;
|
||||
|
||||
@ -75,7 +75,7 @@ export interface ApplicationBuilder {
|
||||
};
|
||||
|
||||
applicationMenu: {
|
||||
click: (path: string) => Promise<void>;
|
||||
click: (path: string) => void;
|
||||
};
|
||||
|
||||
preferences: {
|
||||
@ -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: () => {},
|
||||
@ -206,7 +206,7 @@ export const getApplicationBuilder = () => {
|
||||
dis,
|
||||
|
||||
applicationMenu: {
|
||||
click: async (path: string) => {
|
||||
click: (path: string) => {
|
||||
const applicationMenuItems = mainDi.inject(
|
||||
applicationMenuItemsInjectable,
|
||||
);
|
||||
@ -237,8 +237,6 @@ export const getApplicationBuilder = () => {
|
||||
undefined,
|
||||
{},
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
},
|
||||
},
|
||||
|
||||
@ -398,6 +396,8 @@ export const getApplicationBuilder = () => {
|
||||
},
|
||||
|
||||
async render() {
|
||||
mainDi.inject(lensProxyPortInjectable).set(42);
|
||||
|
||||
for (const callback of beforeApplicationStartCallbacks) {
|
||||
await callback(dis);
|
||||
}
|
||||
@ -408,7 +408,7 @@ export const getApplicationBuilder = () => {
|
||||
|
||||
const applicationWindow = mainDi.inject(applicationWindowInjectable);
|
||||
|
||||
await applicationWindow.show();
|
||||
await applicationWindow.start();
|
||||
|
||||
const startFrame = rendererDi.inject(startFrameInjectable);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user