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

Fix memory leak in unit tests (#5597)

This commit is contained in:
Sebastian Malton 2022-06-13 11:00:07 -04:00 committed by GitHub
parent 2eb585e88e
commit 3058bea88f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1502 additions and 1777 deletions

18
.swcrc Normal file
View File

@ -0,0 +1,18 @@
{
"module": {
"type": "commonjs"
},
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true,
"dynamicImport": false
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2019"
}
}

View File

@ -59,8 +59,12 @@
"collectCoverage": false,
"verbose": true,
"transform": {
"^.+\\.tsx?$": "ts-jest"
"^.+\\.(t|j)sx?$": [
"@swc/jest"
]
},
"testEnvironment": "jsdom",
"resolver": "<rootDir>/src/jest-28-resolver.js",
"moduleNameMapper": {
"\\.(css|scss)$": "identity-obj-proxy",
"\\.(svg|png|jpg|eot|woff2?|ttf)$": "<rootDir>/__mocks__/assetMock.ts"
@ -76,11 +80,7 @@
"setupFilesAfterEnv": [
"<rootDir>/src/jest-after-env.setup.ts"
],
"globals": {
"ts-jest": {
"isolatedModules": true
}
}
"runtime": "@side/jest-runtime"
},
"build": {
"generateUpdatesFilesForAllChannels": true,
@ -208,12 +208,13 @@
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.3",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/injectable": "7.1.0",
"@ogre-tools/injectable-react": "7.1.0",
"@ogre-tools/fp": "7.1.0",
"@ogre-tools/injectable": "7.1.0",
"@ogre-tools/injectable-extension-for-auto-registration": "7.1.0",
"@ogre-tools/injectable-react": "7.1.0",
"@sentry/electron": "^3.0.7",
"@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.0.0",
"@types/circular-dependency-plugin": "5.0.5",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0",
@ -288,6 +289,8 @@
"@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@sentry/types": "^6.19.7",
"@swc/core": "^1.2.197",
"@swc/jest": "^0.2.21",
"@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",
@ -306,7 +309,7 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.9",
"@types/jest": "^26.0.24",
"@types/jest": "^28.1.1",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14",
"@types/lodash": "^4.14.181",
@ -374,10 +377,11 @@
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "26.6.3",
"jest": "^28.1.1",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.1",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^1.0.18",
"jest-mock-extended": "^2.0.6",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.6.0",
"mock-http": "^1.1.0",
@ -402,7 +406,6 @@
"style-loader": "^3.3.1",
"tailwindcss": "^3.0.23",
"tar-stream": "^2.2.0",
"ts-jest": "26.5.6",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"type-fest": "^2.13.0",

View File

@ -4518,7 +4518,7 @@ exports[`add custom helm repository in preferences when navigating to preference
class="flex gaps align-center"
>
<div
class="Input box grow invalid dirty validating validatingLine"
class="Input box grow dirty"
>
<label
class="input-area flex gaps align-center"

View File

@ -444,8 +444,14 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts
</div>
</div>
<div
class="Spinner singleColor center"
/>
class="NoItems flex box grow"
>
<div
class="box center"
>
Item list is empty
</div>
</div>
</div>
<div
class="AddRemoveButtons flex gaps"

View File

@ -34,7 +34,7 @@ describe("add custom helm repository in preferences", () => {
let getActiveHelmRepositoriesMock: AsyncFnMock<() => AsyncResult<HelmRepo[]>>;
beforeEach(async () => {
jest.useFakeTimers("modern");
jest.useFakeTimers();
applicationBuilder = getApplicationBuilder();

View File

@ -840,7 +840,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="flex gaps"
>
<div
class="Select theme-lens box grow Select--is-disabled css-3iigni-container"
class="Select theme-lens box grow css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
@ -853,7 +853,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control Select__control--is-disabled css-1insrsq-control"
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container css-319lph-ValueContainer"
@ -865,7 +865,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
Repositories
</div>
<div
class="Select__input-container css-jzldcf-Input"
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
@ -877,7 +877,6 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
autocomplete="off"
autocorrect="off"
class="Select__input"
disabled=""
id="selection-of-active-public-helm-repository"
role="combobox"
spellcheck="false"
@ -891,22 +890,8 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<div
aria-hidden="true"
class="Select__indicator Select__loading-indicator css-at12u2-loadingIndicator"
>
<span
class="css-1xtdfmb-LoadingDot"
/>
<span
class="css-zoievk-LoadingDot"
/>
<span
class="css-x748d8-LoadingDot"
/>
</div>
<span
class="Select__indicator-separator css-109onse-indicatorSeparator"
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
@ -938,16 +923,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
</div>
<div
class="repos"
>
<div
class="pt-5 relative"
>
<div
class="Spinner singleColor center"
data-testid="helm-repositories-are-loading"
/>
</div>
</div>
/>
<div />
</div>
</div>

View File

@ -449,7 +449,7 @@ describe("KubeApi", () => {
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything());
});
it("aborts watch using abortController", async (done) => {
it("aborts watch using abortController", (done) => {
const spy = jest.spyOn(request, "getResponse");
mockFetch.mockResponse(async request => {
@ -472,10 +472,7 @@ describe("KubeApi", () => {
});
expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything());
await delay(100);
abortController.abort();
delay(100).then(() => abortController.abort());
});
describe("retries", () => {

View File

@ -63,9 +63,9 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
}
}
interface Dependencies {
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
export interface LensProtocolRouterDependencies {
readonly extensionLoader: ExtensionLoader;
readonly extensionsStore: ExtensionsStore;
}
export abstract class LensProtocolRouter {
@ -76,7 +76,7 @@ export abstract class LensProtocolRouter {
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
constructor(protected dependencies: Dependencies) {}
constructor(protected readonly dependencies: LensProtocolRouterDependencies) {}
/**
* Attempts to route the given URL to all internal routes that have been registered

View File

@ -2,4 +2,4 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const flushPromises = () => new Promise(setImmediate);
export const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));

View File

@ -11,6 +11,7 @@ import { runInAction } from "mobx";
import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import { delay } from "../../renderer/utils";
console = new Console(stdout, stderr);
@ -125,42 +126,39 @@ describe("ExtensionLoader", () => {
mockFs.restore();
});
it("renderer updates extension after ipc broadcast", async done => {
it("renderer updates extension after ipc broadcast", async () => {
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
await extensionLoader.init();
await delay(10);
setTimeout(() => {
// Assert the extensions after the extension broadcast event
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
Map {
"manifest/path" => Object {
"absolutePath": "/test/1",
"id": "manifest/path",
"isBundled": false,
"isEnabled": true,
"manifest": Object {
"name": "TestExtension",
"version": "1.0.0",
},
"manifestPath": "manifest/path",
// Assert the extensions after the extension broadcast event
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
Map {
"manifest/path" => Object {
"absolutePath": "/test/1",
"id": "manifest/path",
"isBundled": false,
"isEnabled": true,
"manifest": Object {
"name": "TestExtension",
"version": "1.0.0",
},
"manifest/path3" => Object {
"absolutePath": "/test/3",
"id": "manifest/path3",
"isBundled": false,
"isEnabled": true,
"manifest": Object {
"name": "TestExtension3",
"version": "3.0.0",
},
"manifestPath": "manifest/path3",
"manifestPath": "manifest/path",
},
"manifest/path3" => Object {
"absolutePath": "/test/3",
"id": "manifest/path3",
"isBundled": false,
"isEnabled": true,
"manifest": Object {
"name": "TestExtension3",
"version": "3.0.0",
},
}
`);
done();
}, 10);
"manifestPath": "manifest/path3",
},
}
`);
});
it("updates ExtensionsStore after isEnabled is changed", async () => {

View File

@ -17,6 +17,8 @@ import installExtensionInjectable
import directoryForUserDataInjectable
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
import { delay } from "../../renderer/utils";
import { observable, when } from "mobx";
jest.setTimeout(60_000);
@ -64,7 +66,8 @@ describe("ExtensionDiscovery", () => {
mockFs.restore();
});
it("emits add for added extension", async (done) => {
it("emits add for added extension", async () => {
const letTestFinish = observable.box(false);
let addHandler!: (filePath: string) => void;
mockedFse.readJson.mockImplementation((p) => {
@ -114,13 +117,14 @@ describe("ExtensionDiscovery", () => {
},
manifestPath: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"),
});
done();
letTestFinish.set(true);
});
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
await when(() => letTestFinish.get());
});
it("doesn't emit add for added file under extension", async done => {
it("doesn't emit add for added file under extension", async () => {
let addHandler!: (filePath: string) => void;
const mockWatchInstance = {
@ -146,10 +150,8 @@ describe("ExtensionDiscovery", () => {
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json"));
setTimeout(() => {
expect(onAdd).not.toHaveBeenCalled();
done();
}, 10);
await delay(10);
expect(onAdd).not.toHaveBeenCalled();
});
});

View File

@ -17,6 +17,11 @@ export interface LensExtensionState {
name: string;
}
export interface IsEnabledExtensionDescriptor {
id: string;
isBundled: boolean;
}
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
readonly displayName = "ExtensionsStore";
constructor() {
@ -36,7 +41,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
protected state = observable.map<LensExtensionId, LensExtensionState>();
isEnabled({ id, isBundled }: { id: string; isBundled: boolean }): boolean {
isEnabled({ id, isBundled }: IsEnabledExtensionDescriptor): boolean {
// By default false, so that copied extensions are disabled by default.
// If user installs the extension from the UI, the Extensions component will specifically enable it.
return isBundled || Boolean(this.state.get(id)?.enabled);

32
src/jest-28-resolver.js Normal file
View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
module.exports = (path, options) => {
// Call the defaultResolver, so we leverage its cache, error handling, etc.
return options.defaultResolver(path, {
...options,
// Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb)
packageFilter: pkg => {
// This is a workaround for https://github.com/uuidjs/uuid/pull/616
//
// jest-environment-jsdom 28+ tries to use browser exports instead of default exports,
// but uuid only offers an ESM browser export and not a CommonJS one. Jest does not yet
// support ESM modules natively, so this causes a Jest error related to trying to parse
// "export" syntax.
//
// This workaround prevents Jest from considering uuid's module-based exports at all;
// it falls back to uuid's CommonJS+node "main" property.
//
// Once we're able to migrate our Jest config to ESM and a browser crypto
// implementation is available for the browser+ESM version of uuid to use (eg, via
// https://github.com/jsdom/jsdom/pull/3352 or a similar polyfill), this can go away.
if (pkg.name === "uuid") {
delete pkg["exports"];
delete pkg["module"];
}
return pkg;
},
});
};

View File

@ -22,6 +22,8 @@ fetchMock.enableMocks();
// Mock __non_webpack_require__ for tests
globalThis.__non_webpack_require__ = jest.fn();
global.setImmediate = global.setImmediate ?? (<TArgs extends any[]>(callback: (...args: TArgs) => void, ...args: TArgs) => setTimeout(() => callback(...args), 0));
process.on("unhandledRejection", (err: any) => {
fail(err);
});

View File

@ -41,8 +41,8 @@ import { broadcastMessage } from "../../common/ipc";
import type { ChildProcess } from "child_process";
import { spawn } from "child_process";
import { Kubectl } from "../kubectl/kubectl";
import type { MockProxy } from "jest-mock-extended";
import { mock } from "jest-mock-extended";
import type { DeepMockProxy } from "jest-mock-extended";
import { mockDeep, mock } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used";
import type { Readable } from "stream";
import { EventEmitter } from "stream";
@ -140,12 +140,12 @@ describe("kube auth proxy tests", () => {
});
describe("spawn tests", () => {
let mockedCP: MockProxy<ChildProcess>;
let mockedCP: DeepMockProxy<ChildProcess>;
let listeners: EventEmitter;
let proxy: KubeAuthProxy;
beforeEach(async () => {
mockedCP = mock<ChildProcess>();
mockedCP = mockDeep<ChildProcess>();
listeners = new EventEmitter();
const stderr = mockedCP.stderr = mock<Readable>();
const stdout = mockedCP.stdout = mock<Readable>();

View File

@ -6,11 +6,10 @@
import * as uuid from "uuid";
import { broadcastMessage } from "../../../common/ipc";
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
import { ProtocolHandlerExtension, ProtocolHandlerInternal, ProtocolHandlerInvalid } from "../../../common/protocol-handler";
import { delay, noop } from "../../../common/utils";
import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
import type { ExtensionsStore, IsEnabledExtensionDescriptor } from "../../../extensions/extensions-store/extensions-store";
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
@ -33,16 +32,16 @@ function throwIfDefined(val: any): void {
describe("protocol router tests", () => {
let extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
let lpr: LensProtocolRouterMain;
let extensionsStore: ExtensionsStore;
let enabledExtensions: Set<string>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
"tmp": {},
});
enabledExtensions = new Set();
di.override(extensionsStoreInjectable, () => ExtensionsStore.createInstance());
di.override(extensionsStoreInjectable, () => ({
isEnabled: ({ id, isBundled }: IsEnabledExtensionDescriptor) => isBundled || enabledExtensions.has(id),
} as unknown as ExtensionsStore));
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
@ -50,32 +49,19 @@ describe("protocol router tests", () => {
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
extensionInstances = di.inject(extensionInstancesInjectable);
extensionsStore = di.inject(extensionsStoreInjectable);
lpr = di.inject(lensProtocolRouterMainInjectable);
lpr.rendererLoaded = true;
});
afterEach(() => {
jest.clearAllMocks();
mockFs.restore();
it("should broadcast invalid protocol on non-lens URLs", async () => {
await lpr.route("https://google.ca");
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInvalid, "invalid protocol", "https://google.ca");
});
it("should throw on non-lens URLS", async () => {
try {
expect(await lpr.route("https://google.ca")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
it("should throw when host not internal or extension", async () => {
try {
expect(await lpr.route("lens://foobar")).toBeUndefined();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
it("should broadcast invalid host on non internal or non extension URLs", async () => {
await lpr.route("lens://foobar");
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar");
});
it("should not throw when has valid host", async () => {
@ -100,7 +86,7 @@ describe("protocol router tests", () => {
});
extensionInstances.set(extId, ext);
extensionsStore.mergeState([[extId, { enabled: true, name: "@mirantis/minikube" }]]);
enabledExtensions.add(extId);
lpr.addInternalHandler("/", noop);
@ -180,7 +166,7 @@ describe("protocol router tests", () => {
});
extensionInstances.set(extId, ext);
extensionsStore.mergeState([[extId, { enabled: true, name: "@foobar/icecream" }]]);
enabledExtensions.add(extId);
try {
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
@ -219,7 +205,7 @@ describe("protocol router tests", () => {
});
extensionInstances.set(extId, ext);
extensionsStore.mergeState([[extId, { enabled: true, name: "@foobar/icecream" }]]);
enabledExtensions.add(extId);
}
{
@ -245,13 +231,11 @@ describe("protocol router tests", () => {
});
extensionInstances.set(extId, ext);
extensionsStore.mergeState([[extId, { enabled: true, name: "icecream" }]]);
enabledExtensions.add(extId);
}
extensionsStore.mergeState([
["@foobar/icecream", { enabled: true, name: "@foobar/icecream" }],
["icecream", { enabled: true, name: "icecream" }],
]);
enabledExtensions.add("@foobar/icecream");
enabledExtensions.add("icecream");
try {
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();

View File

@ -9,11 +9,9 @@ import URLParse from "url-parse";
import type { LensExtension } from "../../../extensions/lens-extension";
import { broadcastMessage } from "../../../common/ipc";
import { observable, when, makeObservable } from "mobx";
import type { RouteAttempt } from "../../../common/protocol-handler";
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler";
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
import { disposer, noop } from "../../../common/utils";
import type { ExtensionLoader } from "../../../extensions/extension-loader";
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
export interface FallbackHandler {
(name: string): Promise<boolean>;
@ -36,9 +34,7 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
}
}
interface Dependencies {
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies {
showApplicationWindow: () => Promise<void>;
}
@ -50,7 +46,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
protected disposers = disposer();
constructor(protected dependencies: Dependencies) {
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
super(dependencies);
makeObservable(this);

View File

@ -134,7 +134,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
{this.allErrors.length > 0 && (
<>
<h3>KubeConfig Yaml Validation Errors:</h3>
{...this.allErrors.map(error => <div key={error} className="error">{error}</div>)}
{this.allErrors.map(error => <div key={error} className="error">{error}</div>)}
</>
)}
<div className="actions-panel">

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
const badgeHasTextSelectedStateInjectable = getInjectable({
id: "badge-has-text-selected-state",
instantiate: () => observable.box(false),
});
export default badgeHasTextSelectedStateInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
const closeWindowInjectable = getInjectable({
id: "close-window",
instantiate: () => () => requestWindowAction(WindowAction.CLOSE),
causesSideEffects: true,
});
export default closeWindowInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
const goBackInjectable = getInjectable({
id: "go-back",
instantiate: () => () => requestWindowAction(WindowAction.GO_BACK),
causesSideEffects: true,
});
export default goBackInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
const goForwardInjectable = getInjectable({
id: "go-forward",
instantiate: () => () => requestWindowAction(WindowAction.GO_FORWARD),
causesSideEffects: true,
});
export default goForwardInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
const maximizeWindowInjectable = getInjectable({
id: "maximize-window",
instantiate: () => () => requestWindowAction(WindowAction.MINIMIZE),
causesSideEffects: true,
});
export default maximizeWindowInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import topBarStateInjectable from "./state.injectable";
const topBarNextEnabledInjectable = getInjectable({
id: "top-bar-next-enabled",
instantiate: (di) => {
const state = di.inject(topBarStateInjectable);
return computed(() => state.nextEnabled);
},
});
export default topBarNextEnabledInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { emitOpenAppMenuAsContextMenu } from "../../../ipc";
const openAppContextMenuInjectable = getInjectable({
id: "open-app-context-menu",
instantiate: () => emitOpenAppMenuAsContextMenu,
causesSideEffects: true,
});
export default openAppContextMenuInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import topBarStateInjectable from "./state.injectable";
const topBarPrevEnabledInjectable = getInjectable({
id: "top-bar-prev-enabled",
instantiate: (di) => {
const state = di.inject(topBarStateInjectable);
return computed(() => state.prevEnabled);
},
});
export default topBarPrevEnabledInjectable;

View File

@ -0,0 +1,34 @@
/**
* 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 { action } from "mobx";
import { beforeFrameStartsInjectionToken } from "../../../before-frame-starts/before-frame-starts-injection-token";
import ipcRendererInjectable from "../../../utils/channel/ipc-renderer.injectable";
import topBarStateInjectable from "./state.injectable";
// TODO: replace with a SyncBox
const startTopbarStateSyncInjectable = getInjectable({
id: "start-topbar-state-sync",
instantiate: (di) => {
const state = di.inject(topBarStateInjectable);
const ipcRenderer = di.inject(ipcRendererInjectable);
return {
run: () => {
ipcRenderer.on("history:can-go-back", action((event, canGoBack: boolean) => {
state.prevEnabled = canGoBack;
}));
ipcRenderer.on("history:can-go-forward", action((event, canGoForward: boolean) => {
state.nextEnabled = canGoForward;
}));
},
};
},
injectionToken: beforeFrameStartsInjectionToken,
causesSideEffects: true,
});
export default startTopbarStateSyncInjectable;

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
const topBarStateInjectable = getInjectable({
id: "top-bar-state",
instantiate: () => observable.object({
prevEnabled: false,
nextEnabled: false,
}),
});
export default topBarStateInjectable;

View File

@ -0,0 +1,15 @@
/**
* 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 { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
const toggleMaximizeWindowInjectable = getInjectable({
id: "toggle-maximize-window",
instantiate: () => () => requestWindowAction(WindowAction.TOGGLE_MAXIMIZE),
causesSideEffects: true,
});
export default toggleMaximizeWindowInjectable;

View File

@ -1,88 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "./top-bar";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
jest.mock("../../../../common/ipc");
jest.mock("../../../ipc");
describe("<TopBar/> in Windows and Linux", () => {
let render: DiRender;
let di: DiContainer;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(isMacInjectable, () => false);
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
mockFs();
render = renderFor(di);
});
afterEach(() => {
mockFs.restore();
});
it("shows window controls on Windows", () => {
di.override(isWindowsInjectable, () => true);
const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument();
expect(getByTestId("window-maximize")).toBeInTheDocument();
expect(getByTestId("window-close")).toBeInTheDocument();
});
it("shows window controls on Linux", () => {
di.override(isLinuxInjectable, () => true);
const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument();
expect(getByTestId("window-maximize")).toBeInTheDocument();
expect(getByTestId("window-close")).toBeInTheDocument();
});
it("triggers ipc events on click", () => {
di.override(isWindowsInjectable, () => true);
const { getByTestId } = render(<TopBar />);
const menu = getByTestId("window-menu");
const minimize = getByTestId("window-minimize");
const maximize = getByTestId("window-maximize");
const close = getByTestId("window-close");
fireEvent.click(menu);
expect(emitOpenAppMenuAsContextMenu).toHaveBeenCalledWith();
fireEvent.click(minimize);
expect(requestWindowAction).toHaveBeenCalledWith("minimize");
fireEvent.click(maximize);
expect(requestWindowAction).toHaveBeenCalledWith("toggle-maximize");
fireEvent.click(close);
expect(requestWindowAction).toHaveBeenCalledWith("close");
});
});

View File

@ -12,163 +12,191 @@ import type { DiContainer } from "@ogre-tools/injectable";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { computed } from "mobx";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
jest.mock("../../../../common/vars", () => {
const { SemVer } = require("semver");
return {
...jest.requireActual<{}>("../../../../common/vars"),
appSemVer: new SemVer("1.0.0"),
};
});
const goBack = jest.fn();
const goForward = jest.fn();
jest.mock(
"electron",
() => ({
ipcRenderer: {
on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => {
if (channel === "history:can-go-back") {
listener({}, true);
}
if (channel === "history:can-go-forward") {
listener({}, true);
}
},
),
invoke: jest.fn(
(channel: string, action: string) => {
console.log("channel", channel, action);
if (channel !== "window:window-action") return;
switch(action) {
case "back": {
goBack();
break;
}
case "forward": {
goForward();
break;
}
}
},
),
},
}),
);
jest.mock("../../+catalog", () => ({
previousActiveTab: jest.fn(),
}));
import { computed, observable } from "mobx";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import closeWindowInjectable from "./close-window.injectable";
import goBackInjectable from "./go-back.injectable";
import goForwardInjectable from "./go-forward.injectable";
import maximizeWindowInjectable from "./maximize-window.injectable";
import openAppContextMenuInjectable from "./open-app-context-menu.injectable";
import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
import topBarStateInjectable from "./state.injectable";
import platformInjectable from "../../../../common/vars/platform.injectable";
describe("<TopBar/>", () => {
let di: DiContainer;
let render: DiRender;
let goBack: jest.MockedFunction<() => void>;
let goForward: jest.MockedFunction<() => void>;
let openAppContextMenu: jest.MockedFunction<() => void>;
let closeWindow: jest.MockedFunction<() => void>;
let maximizeWindow: jest.MockedFunction<() => void>;
let toggleMaximizeWindow: jest.MockedFunction<() => void>;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(rendererExtensionsInjectable, () => computed(() => []));
di.override(openAppContextMenuInjectable, () => openAppContextMenu = jest.fn());
di.override(goBackInjectable, () => goBack = jest.fn());
di.override(goForwardInjectable, () => goForward = jest.fn());
di.override(closeWindowInjectable, () => closeWindow = jest.fn());
di.override(maximizeWindowInjectable, () => maximizeWindow = jest.fn());
di.override(toggleMaximizeWindowInjectable, () => toggleMaximizeWindow = jest.fn());
render = renderFor(di);
});
afterEach(() => {
mockFs.restore();
});
describe("with both previous and next history enabled", () => {
beforeEach(() => {
di.override(topBarStateInjectable, () => observable.object({
prevEnabled: true,
nextEnabled: true,
}));
});
it("renders w/o errors", () => {
const { container } = render(<TopBar/>);
it("renders w/o errors", () => {
const { container } = render(<TopBar/>);
expect(container).toBeInstanceOf(HTMLElement);
});
expect(container).toBeInstanceOf(HTMLElement);
});
it("renders home button", async () => {
const { findByTestId } = render(<TopBar/>);
it("renders home button", async () => {
const { findByTestId } = render(<TopBar/>);
expect(await findByTestId("home-button")).toBeInTheDocument();
});
expect(await findByTestId("home-button")).toBeInTheDocument();
});
it("renders history arrows", async () => {
const { findByTestId } = render(<TopBar/>);
it("renders history arrows", async () => {
const { findByTestId } = render(<TopBar/>);
expect(await findByTestId("history-back")).toBeInTheDocument();
expect(await findByTestId("history-forward")).toBeInTheDocument();
});
expect(await findByTestId("history-back")).toBeInTheDocument();
expect(await findByTestId("history-forward")).toBeInTheDocument();
});
it("enables arrow by ipc event", async () => {
const { findByTestId } = render(<TopBar/>);
it("enables arrow by ipc event", async () => {
const { findByTestId } = render(<TopBar/>);
expect(await findByTestId("history-back")).not.toHaveClass("disabled");
expect(await findByTestId("history-forward")).not.toHaveClass("disabled");
});
expect(await findByTestId("history-back")).not.toHaveClass("disabled");
expect(await findByTestId("history-forward")).not.toHaveClass("disabled");
});
it("triggers browser history back and forward", async () => {
const { findByTestId } = render(<TopBar/>);
it("triggers browser history back and forward", async () => {
const { findByTestId } = render(<TopBar/>);
const prevButton = await findByTestId("history-back");
const nextButton = await findByTestId("history-forward");
const prevButton = await findByTestId("history-back");
const nextButton = await findByTestId("history-forward");
fireEvent.click(prevButton);
fireEvent.click(prevButton);
expect(goBack).toBeCalled();
expect(goBack).toBeCalled();
fireEvent.click(nextButton);
fireEvent.click(nextButton);
expect(goForward).toBeCalled();
});
expect(goForward).toBeCalled();
});
it("renders items", async () => {
const testId = "testId";
const text = "an item";
it("renders items", async () => {
const testId = "testId";
const text = "an item";
di.override(topBarItemsInjectable, () => computed(() => [
{
components: {
Item: () => <span data-testid={testId}>{text}</span>,
di.override(topBarItemsInjectable, () => computed(() => [
{
components: {
Item: () => <span data-testid={testId}>{text}</span>,
},
},
},
]));
]));
const { findByTestId } = render(<TopBar/>);
const { findByTestId } = render(<TopBar/>);
expect(await findByTestId(testId)).toHaveTextContent(text);
});
expect(await findByTestId(testId)).toHaveTextContent(text);
});
it("doesn't show windows title buttons on macos", () => {
di.override(isLinuxInjectable, () => false);
di.override(isWindowsInjectable, () => false);
describe("on macos", () => {
beforeEach(() => {
di.override(platformInjectable, () => "darwin");
});
const { queryByTestId } = render(<TopBar/>);
it("doesn't show windows title", () => {
const { queryByTestId } = render(<TopBar/>);
expect(queryByTestId("window-menu")).not.toBeInTheDocument();
expect(queryByTestId("window-minimize")).not.toBeInTheDocument();
expect(queryByTestId("window-maximize")).not.toBeInTheDocument();
expect(queryByTestId("window-close")).not.toBeInTheDocument();
});
expect(queryByTestId("window-menu")).not.toBeInTheDocument();
expect(queryByTestId("window-minimize")).not.toBeInTheDocument();
expect(queryByTestId("window-maximize")).not.toBeInTheDocument();
expect(queryByTestId("window-close")).not.toBeInTheDocument();
});
});
it("does show windows title buttons on linux", () => {
di.override(isLinuxInjectable, () => true);
di.override(isWindowsInjectable, () => false);
describe("on linux", () => {
beforeEach(() => {
di.override(platformInjectable, () => "linux");
});
const { queryByTestId } = render(<TopBar/>);
it("does show windows title buttons", () => {
const { queryByTestId } = render(<TopBar/>);
expect(queryByTestId("window-menu")).toBeInTheDocument();
expect(queryByTestId("window-minimize")).toBeInTheDocument();
expect(queryByTestId("window-maximize")).toBeInTheDocument();
expect(queryByTestId("window-close")).toBeInTheDocument();
expect(queryByTestId("window-menu")).toBeInTheDocument();
expect(queryByTestId("window-minimize")).toBeInTheDocument();
expect(queryByTestId("window-maximize")).toBeInTheDocument();
expect(queryByTestId("window-close")).toBeInTheDocument();
});
it("triggers ipc events on click", () => {
const { getByTestId } = render(<TopBar />);
const menu = getByTestId("window-menu");
const minimize = getByTestId("window-minimize");
const maximize = getByTestId("window-maximize");
const close = getByTestId("window-close");
fireEvent.click(menu);
expect(openAppContextMenu).toBeCalled();
fireEvent.click(minimize);
expect(maximizeWindow).toBeCalled();
fireEvent.click(maximize);
expect(toggleMaximizeWindow).toBeCalled();
fireEvent.click(close);
expect(closeWindow).toBeCalled();
});
});
describe("on windows", () => {
beforeEach(() => {
di.override(platformInjectable, () => "win32");
});
it("does show windows title buttons", () => {
const { queryByTestId } = render(<TopBar/>);
expect(queryByTestId("window-menu")).toBeInTheDocument();
expect(queryByTestId("window-minimize")).toBeInTheDocument();
expect(queryByTestId("window-maximize")).toBeInTheDocument();
expect(queryByTestId("window-close")).toBeInTheDocument();
});
it("triggers ipc events on click", () => {
const { getByTestId } = render(<TopBar />);
const menu = getByTestId("window-menu");
const minimize = getByTestId("window-minimize");
const maximize = getByTestId("window-maximize");
const close = getByTestId("window-close");
fireEvent.click(menu);
expect(openAppContextMenu).toBeCalled();
fireEvent.click(minimize);
expect(maximizeWindow).toBeCalled();
fireEvent.click(maximize);
expect(toggleMaximizeWindow).toBeCalled();
fireEvent.click(close);
expect(closeWindow).toBeCalled();
});
});
});
});

View File

@ -8,15 +8,11 @@ import React, { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import { Icon } from "../../icon";
import { observable } from "mobx";
import { ipcRendererOn } from "../../../../common/ipc";
import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { cssNames, noop } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration";
import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
import { WindowAction } from "../../../../common/ipc/window";
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
import type { NavigateToCatalog } from "../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
@ -24,6 +20,14 @@ import navigateToCatalogInjectable from "../../../../common/front-end-routing/ro
import catalogRouteInjectable from "../../../../common/front-end-routing/routes/catalog/catalog-route.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { UpdateButton } from "../../update-button";
import topBarPrevEnabledInjectable from "./prev-enabled.injectable";
import topBarNextEnabledInjectable from "./next-enabled.injectable";
import openAppContextMenuInjectable from "./open-app-context-menu.injectable";
import goBackInjectable from "./go-back.injectable";
import goForwardInjectable from "./go-forward.injectable";
import closeWindowInjectable from "./close-window.injectable";
import maximizeWindowInjectable from "./maximize-window.injectable";
import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
interface Dependencies {
navigateToCatalog: NavigateToCatalog;
@ -31,62 +35,43 @@ interface Dependencies {
items: IComputedValue<TopBarRegistration[]>;
isWindows: boolean;
isLinux: boolean;
prevEnabled: IComputedValue<Boolean>;
nextEnabled: IComputedValue<Boolean>;
openAppContextMenu: () => void;
goBack: () => void;
goForward: () => void;
minimizeWindow: () => void;
toggleMaximizeWindow: () => void;
closeWindow: () => void;
}
const prevEnabled = observable.box(false);
const nextEnabled = observable.box(false);
ipcRendererOn("history:can-go-back", (event, state: boolean) => {
prevEnabled.set(state);
});
ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
nextEnabled.set(state);
});
const NonInjectedTopBar = observer(({
items,
navigateToCatalog,
catalogRouteIsActive,
isWindows,
isLinux,
prevEnabled,
nextEnabled,
openAppContextMenu,
goBack,
goForward,
closeWindow,
minimizeWindow,
toggleMaximizeWindow,
}: Dependencies) => {
const elem = useRef<HTMLDivElement | null>(null);
const openAppContextMenu = () => {
emitOpenAppMenuAsContextMenu();
};
const goHome = () => {
navigateToCatalog();
};
const goBack = () => {
requestWindowAction(WindowAction.GO_BACK);
};
const goForward = () => {
requestWindowAction(WindowAction.GO_FORWARD);
};
const windowSizeToggle = (evt: React.MouseEvent) => {
if (elem.current === evt.target) {
toggleMaximize();
toggleMaximizeWindow();
}
};
const minimizeWindow = () => {
requestWindowAction(WindowAction.MINIMIZE);
};
const toggleMaximize = () => {
requestWindowAction(WindowAction.TOGGLE_MAXIMIZE);
};
const closeWindow = () => {
requestWindowAction(WindowAction.CLOSE);
};
useEffect(() => watchHistoryState(), []);
return (
@ -147,14 +132,14 @@ const NonInjectedTopBar = observer(({
width="10"
height="1"
x="1"
y="9"
y="9"
/>
</svg>
</div>
<div
className={styles.maximize}
data-testid="window-maximize"
onClick={toggleMaximize}
onClick={toggleMaximizeWindow}
>
<svg shapeRendering="crispEdges" viewBox="0 0 12 12">
<rect
@ -163,7 +148,7 @@ const NonInjectedTopBar = observer(({
x="1.5"
y="1.5"
fill="none"
stroke="currentColor"
stroke="currentColor"
/>
</svg>
</div>
@ -193,23 +178,23 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => (
})
);
export const TopBar = withInjectables<Dependencies>(
NonInjectedTopBar,
{
getProps: (di) => {
const catalogRoute = di.inject(catalogRouteInjectable);
return {
navigateToCatalog: di.inject(navigateToCatalogInjectable),
items: di.inject(topBarItemsInjectable),
isLinux: di.inject(isLinuxInjectable),
isWindows: di.inject(isWindowsInjectable),
catalogRouteIsActive: di.inject(
routeIsActiveInjectable,
catalogRoute,
),
};
},
},
);
export const TopBar = withInjectables<Dependencies>(NonInjectedTopBar, {
getProps: (di) => ({
navigateToCatalog: di.inject(navigateToCatalogInjectable),
items: di.inject(topBarItemsInjectable),
isLinux: di.inject(isLinuxInjectable),
isWindows: di.inject(isWindowsInjectable),
prevEnabled: di.inject(topBarPrevEnabledInjectable),
nextEnabled: di.inject(topBarNextEnabledInjectable),
catalogRouteIsActive: di.inject(
routeIsActiveInjectable,
di.inject(catalogRouteInjectable),
),
openAppContextMenu: di.inject(openAppContextMenuInjectable),
goBack: di.inject(goBackInjectable),
goForward: di.inject(goForwardInjectable),
closeWindow: di.inject(closeWindowInjectable),
minimizeWindow: di.inject(maximizeWindowInjectable),
toggleMaximizeWindow: di.inject(toggleMaximizeWindowInjectable),
}),
});

View File

@ -18,6 +18,7 @@ exports[`<Tooltip /> renders to DOM when forced to by visibile prop 1`] = `
<div
class="Tooltip visible"
role="tooltip"
style="left: 10px; top: 0px;"
>
I am a tooltip
</div>
@ -34,7 +35,7 @@ exports[`<Tooltip /> renders to DOM when hovering over target 1`] = `
<body>
<div>
<div
class="Tooltip right"
class="Tooltip visible"
role="tooltip"
style="left: 10px; top: 0px;"
>

View File

@ -10,6 +10,23 @@ import React from "react";
import { Tooltip } from "./tooltip";
describe("<Tooltip />", () => {
let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;
beforeEach(() => {
requestAnimationFrameSpy = jest.spyOn(window, "requestAnimationFrame");
requestAnimationFrameSpy.mockImplementation(cb => {
cb(0);
return 0;
});
});
afterEach(() => {
requestAnimationFrameSpy.mockRestore();
});
it("does not render to DOM if not visibile", () => {
const result = render((
<>

View File

@ -79,6 +79,7 @@ export class Tooltip extends React.Component<TooltipProps> {
componentDidMount() {
this.hoverTarget?.addEventListener("mouseenter", this.onEnterTarget);
this.hoverTarget?.addEventListener("mouseleave", this.onLeaveTarget);
this.refreshPosition();
}
componentDidUpdate() {
@ -110,7 +111,8 @@ export class Tooltip extends React.Component<TooltipProps> {
return;
}
let positions = new Set<TooltipPosition>([
const positions = new Set<TooltipPosition>([
...[preferredPositions ?? []].flat(),
TooltipPosition.RIGHT,
TooltipPosition.BOTTOM,
TooltipPosition.TOP,
@ -121,13 +123,6 @@ export class Tooltip extends React.Component<TooltipProps> {
TooltipPosition.BOTTOM_LEFT,
]);
if (preferredPositions) {
positions = new Set([
...[preferredPositions].flat(),
...positions,
]);
}
// reset position first and get all possible client-rect area for tooltip element
this.setPosition(elem, { left: 0, top: 0 });

View File

@ -26,9 +26,6 @@ import type { ClusterStore } from "../common/cluster-store/cluster-store";
import type { Cluster } from "../common/cluster/cluster";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import type { UserStore } from "../common/user-store";
import isMacInjectable from "../common/vars/is-mac.injectable";
import isWindowsInjectable from "../common/vars/is-windows.injectable";
import isLinuxInjectable from "../common/vars/is-linux.injectable";
import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable";
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
import joinPathsInjectable from "../common/path/join-paths.injectable";
@ -52,6 +49,8 @@ import requestAnimationFrameInjectable from "./components/animate/request-animat
import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import getFilePathsInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/get-file-paths.injectable";
import callForPublicHelmRepositoriesInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import platformInjectable from "../common/vars/platform.injectable";
import startTopbarStateSyncInjectable from "./components/layout/top-bar/start-state-sync.injectable";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@ -75,9 +74,10 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
if (doGeneralOverrides) {
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(isMacInjectable, () => true);
di.override(isWindowsInjectable, () => false);
di.override(isLinuxInjectable, () => false);
di.override(platformInjectable, () => "darwin");
di.override(startTopbarStateSyncInjectable, () => ({
run: () => {},
}));
di.override(terminalSpawningPoolInjectable, () => document.createElement("div"));
di.override(hostedClusterIdInjectable, () => undefined);

2276
yarn.lock

File diff suppressed because it is too large Load Diff