From 69bd42357f8ede74d16a80bab12c4eef8c8485c3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 24 Aug 2022 12:49:06 -0400 Subject: [PATCH] Fix tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton --- __mocks__/windowMock.ts | 19 --- ...ess-path.global-override-for-injectable.ts | 11 ++ src/common/fs/access-path.injectable.ts | 27 ++++ .../fs/copy.global-override-for-injectable.ts | 11 ++ src/common/fs/copy.injectable.ts | 16 +++ ...sure-dir.global-override-for-injectable.ts | 11 ++ src/common/fs/ensure-dir.injectable.ts | 6 +- .../lstat.global-override-for-injectable.ts | 11 ++ src/common/fs/lstat.injectable.ts | 16 +++ ...read-dir.global-override-for-injectable.ts | 11 ++ src/common/fs/read-dir.injectable.ts | 26 +++- ...ute-path.global-override-for-injectable.ts | 10 ++ ...basename.global-override-for-injectable.ts | 10 ++ src/common/path/get-basename.injectable.ts | 16 +++ ...-dirname.global-override-for-injectable.ts | 10 ++ src/common/path/get-dirname.injectable.ts | 16 +++ ...ive-path.global-override-for-injectable.ts | 10 ++ .../path/get-relative-path.injectable.ts | 16 +++ ...in-paths.global-override-for-injectable.ts | 10 ++ ...eparator.global-override-for-injectable.ts | 10 ++ src/common/path/separator.injectable.ts | 14 ++ .../test-utils/get-absolute-path-fake.ts | 17 --- src/common/test-utils/join-paths-fake.ts | 7 - .../extension-discovery.injectable.ts | 24 ++++ .../extension-discovery.ts | 133 ++++++++++-------- .../extension-loader.injectable.ts | 2 + .../extension-loader/extension-loader.ts | 21 +-- .../get-electron-app-path.test.ts | 3 - src/main/getDiForUnitTesting.ts | 6 - .../+extensions/__tests__/extensions.test.tsx | 27 +--- src/renderer/components/animate/animate.tsx | 8 +- .../__tests__/render-delay.test.tsx | 3 - ...callback.global-override-for-injectable.ts | 9 ++ .../cancel-idle-callback.injectable.ts | 15 ++ .../components/render-delay/render-delay.tsx | 74 ++++++---- ...callback.global-override-for-injectable.ts | 9 ++ .../request-idle-callback.injectable.ts | 15 ++ src/renderer/getDiForUnitTesting.tsx | 7 - types/dom.d.ts | 5 - 39 files changed, 483 insertions(+), 189 deletions(-) delete mode 100644 __mocks__/windowMock.ts create mode 100644 src/common/fs/access-path.global-override-for-injectable.ts create mode 100644 src/common/fs/access-path.injectable.ts create mode 100644 src/common/fs/copy.global-override-for-injectable.ts create mode 100644 src/common/fs/copy.injectable.ts create mode 100644 src/common/fs/ensure-dir.global-override-for-injectable.ts create mode 100644 src/common/fs/lstat.global-override-for-injectable.ts create mode 100644 src/common/fs/lstat.injectable.ts create mode 100644 src/common/fs/read-dir.global-override-for-injectable.ts create mode 100644 src/common/path/get-absolute-path.global-override-for-injectable.ts create mode 100644 src/common/path/get-basename.global-override-for-injectable.ts create mode 100644 src/common/path/get-basename.injectable.ts create mode 100644 src/common/path/get-dirname.global-override-for-injectable.ts create mode 100644 src/common/path/get-dirname.injectable.ts create mode 100644 src/common/path/get-relative-path.global-override-for-injectable.ts create mode 100644 src/common/path/get-relative-path.injectable.ts create mode 100644 src/common/path/join-paths.global-override-for-injectable.ts create mode 100644 src/common/path/separator.global-override-for-injectable.ts create mode 100644 src/common/path/separator.injectable.ts delete mode 100644 src/common/test-utils/get-absolute-path-fake.ts delete mode 100644 src/common/test-utils/join-paths-fake.ts create mode 100644 src/renderer/components/render-delay/cancel-idle-callback.global-override-for-injectable.ts create mode 100644 src/renderer/components/render-delay/cancel-idle-callback.injectable.ts create mode 100644 src/renderer/components/render-delay/request-idle-callback.global-override-for-injectable.ts create mode 100644 src/renderer/components/render-delay/request-idle-callback.injectable.ts diff --git a/__mocks__/windowMock.ts b/__mocks__/windowMock.ts deleted file mode 100644 index bcc3da05a6..0000000000 --- a/__mocks__/windowMock.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * Mock the global window variable - */ -export function mockWindow() { - Object.defineProperty(window, "requestIdleCallback", { - writable: true, - value: jest.fn().mockImplementation(callback => callback()), - }); - - Object.defineProperty(window, "cancelIdleCallback", { - writable: true, - value: jest.fn(), - }); -} diff --git a/src/common/fs/access-path.global-override-for-injectable.ts b/src/common/fs/access-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..747d839682 --- /dev/null +++ b/src/common/fs/access-path.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import accessPathInjectable from "./access-path.injectable"; + +export default getGlobalOverride(accessPathInjectable, () => async () => { + throw new Error("tried to verify path access without override"); +}); diff --git a/src/common/fs/access-path.injectable.ts b/src/common/fs/access-path.injectable.ts new file mode 100644 index 0000000000..0504de9d6f --- /dev/null +++ b/src/common/fs/access-path.injectable.ts @@ -0,0 +1,27 @@ +/** + * 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 fsInjectable from "./fs.injectable"; + +export type AccessPath = (path: string, mode?: number) => Promise; + +const accessPathInjectable = getInjectable({ + id: "access-path", + instantiate: (di): AccessPath => { + const { access } = di.inject(fsInjectable); + + return async (path, mode) => { + try { + await access(path, mode); + + return true; + } catch { + return false; + } + }; + }, +}); + +export default accessPathInjectable; diff --git a/src/common/fs/copy.global-override-for-injectable.ts b/src/common/fs/copy.global-override-for-injectable.ts new file mode 100644 index 0000000000..b6d899d2c4 --- /dev/null +++ b/src/common/fs/copy.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import copyInjectable from "./copy.injectable"; + +export default getGlobalOverride(copyInjectable, () => async () => { + throw new Error("tried to copy filepaths without override"); +}); diff --git a/src/common/fs/copy.injectable.ts b/src/common/fs/copy.injectable.ts new file mode 100644 index 0000000000..6a64ee3751 --- /dev/null +++ b/src/common/fs/copy.injectable.ts @@ -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 type { CopyOptions } from "fs-extra"; +import fsInjectable from "./fs.injectable"; + +export type Copy = (src: string, dest: string, options?: CopyOptions | undefined) => Promise; + +const copyInjectable = getInjectable({ + id: "copy", + instantiate: (di): Copy => di.inject(fsInjectable).copy, +}); + +export default copyInjectable; diff --git a/src/common/fs/ensure-dir.global-override-for-injectable.ts b/src/common/fs/ensure-dir.global-override-for-injectable.ts new file mode 100644 index 0000000000..4dd098b163 --- /dev/null +++ b/src/common/fs/ensure-dir.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import ensureDirInjectable from "./ensure-dir.injectable"; + +export default getGlobalOverride(ensureDirInjectable, () => async () => { + throw new Error("tried to ensure directory without override"); +}); diff --git a/src/common/fs/ensure-dir.injectable.ts b/src/common/fs/ensure-dir.injectable.ts index 88410ceee2..78ec4d91dc 100644 --- a/src/common/fs/ensure-dir.injectable.ts +++ b/src/common/fs/ensure-dir.injectable.ts @@ -5,14 +5,14 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; +export type EnsureDirectory = (dirPath: string) => Promise; + const ensureDirInjectable = getInjectable({ id: "ensure-dir", // TODO: Remove usages of ensureDir from business logic. // TODO: Read, Write, Watch etc. operations should do this internally. - instantiate: (di) => di.inject(fsInjectable).ensureDir, - - causesSideEffects: true, + instantiate: (di): EnsureDirectory => di.inject(fsInjectable).ensureDir, }); export default ensureDirInjectable; diff --git a/src/common/fs/lstat.global-override-for-injectable.ts b/src/common/fs/lstat.global-override-for-injectable.ts new file mode 100644 index 0000000000..9c9f3d4933 --- /dev/null +++ b/src/common/fs/lstat.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import lstatInjectable from "./lstat.injectable"; + +export default getGlobalOverride(lstatInjectable, () => async () => { + throw new Error("tried to lstat a filepath without override"); +}); diff --git a/src/common/fs/lstat.injectable.ts b/src/common/fs/lstat.injectable.ts new file mode 100644 index 0000000000..50c1d4ad12 --- /dev/null +++ b/src/common/fs/lstat.injectable.ts @@ -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 type { Stats } from "fs"; +import fsInjectable from "./fs.injectable"; + +export type LStat = (path: string) => Promise; + +const lstatInjectable = getInjectable({ + id: "lstat", + instantiate: (di): LStat => di.inject(fsInjectable).lstat, +}); + +export default lstatInjectable; diff --git a/src/common/fs/read-dir.global-override-for-injectable.ts b/src/common/fs/read-dir.global-override-for-injectable.ts new file mode 100644 index 0000000000..7f595ba307 --- /dev/null +++ b/src/common/fs/read-dir.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import readDirInjectable from "./read-dir.injectable"; + +export default getGlobalOverride(readDirInjectable, () => async () => { + throw new Error("tried to read a directory's content without override"); +}); diff --git a/src/common/fs/read-dir.injectable.ts b/src/common/fs/read-dir.injectable.ts index 2c7b59d9b2..54835a1c24 100644 --- a/src/common/fs/read-dir.injectable.ts +++ b/src/common/fs/read-dir.injectable.ts @@ -3,11 +3,35 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { Dirent } from "fs"; import fsInjectable from "./fs.injectable"; +export interface ReadDirectory { + ( + path: string, + options: "buffer" | { encoding: "buffer"; withFileTypes?: false | undefined } + ): Promise; + ( + path: string, + options?: + | { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined } + | BufferEncoding + | string + | null, + ): Promise; + ( + path: string, + options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined }, + ): Promise; + ( + path: string, + options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true }, + ): Promise; +} + const readDirInjectable = getInjectable({ id: "read-dir", - instantiate: (di) => di.inject(fsInjectable).readdir, + instantiate: (di): ReadDirectory => di.inject(fsInjectable).readdir, }); export default readDirInjectable; diff --git a/src/common/path/get-absolute-path.global-override-for-injectable.ts b/src/common/path/get-absolute-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..15f377cb2c --- /dev/null +++ b/src/common/path/get-absolute-path.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getAbsolutePathInjectable from "./get-absolute-path.injectable"; + +export default getGlobalOverride(getAbsolutePathInjectable, () => path.posix.resolve); diff --git a/src/common/path/get-basename.global-override-for-injectable.ts b/src/common/path/get-basename.global-override-for-injectable.ts new file mode 100644 index 0000000000..913ec9c5c2 --- /dev/null +++ b/src/common/path/get-basename.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getBasenameOfPathInjectable from "./get-basename.injectable"; + +export default getGlobalOverride(getBasenameOfPathInjectable, () => path.posix.basename); diff --git a/src/common/path/get-basename.injectable.ts b/src/common/path/get-basename.injectable.ts new file mode 100644 index 0000000000..be92bde7f5 --- /dev/null +++ b/src/common/path/get-basename.injectable.ts @@ -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 path from "path"; + +export type GetBasenameOfPath = (path: string) => string; + +const getBasenameOfPathInjectable = getInjectable({ + id: "get-basename-of-path", + instantiate: (): GetBasenameOfPath => path.basename, + causesSideEffects: true, +}); + +export default getBasenameOfPathInjectable; diff --git a/src/common/path/get-dirname.global-override-for-injectable.ts b/src/common/path/get-dirname.global-override-for-injectable.ts new file mode 100644 index 0000000000..ed694de182 --- /dev/null +++ b/src/common/path/get-dirname.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getDirnameOfPathInjectable from "./get-dirname.injectable"; + +export default getGlobalOverride(getDirnameOfPathInjectable, () => path.posix.dirname); diff --git a/src/common/path/get-dirname.injectable.ts b/src/common/path/get-dirname.injectable.ts new file mode 100644 index 0000000000..93b4496767 --- /dev/null +++ b/src/common/path/get-dirname.injectable.ts @@ -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 path from "path"; + +export type GetDirnameOfPath = (path: string) => string; + +const getDirnameOfPathInjectable = getInjectable({ + id: "get-dirname-of-path", + instantiate: (): GetDirnameOfPath => path.dirname, + causesSideEffects: true, +}); + +export default getDirnameOfPathInjectable; diff --git a/src/common/path/get-relative-path.global-override-for-injectable.ts b/src/common/path/get-relative-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..9e96b70301 --- /dev/null +++ b/src/common/path/get-relative-path.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getRelativePathInjectable from "./get-relative-path.injectable"; + +export default getGlobalOverride(getRelativePathInjectable, () => path.posix.relative); diff --git a/src/common/path/get-relative-path.injectable.ts b/src/common/path/get-relative-path.injectable.ts new file mode 100644 index 0000000000..18b5d832de --- /dev/null +++ b/src/common/path/get-relative-path.injectable.ts @@ -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 path from "path"; + +export type GetRelativePath = (from: string, to: string) => string; + +const getRelativePathInjectable = getInjectable({ + id: "get-relative-path", + instantiate: (): GetRelativePath => path.relative, + causesSideEffects: true, +}); + +export default getRelativePathInjectable; diff --git a/src/common/path/join-paths.global-override-for-injectable.ts b/src/common/path/join-paths.global-override-for-injectable.ts new file mode 100644 index 0000000000..d3e9d5e4c2 --- /dev/null +++ b/src/common/path/join-paths.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import joinPathsInjectable from "./join-paths.injectable"; + +export default getGlobalOverride(joinPathsInjectable, () => path.posix.join); diff --git a/src/common/path/separator.global-override-for-injectable.ts b/src/common/path/separator.global-override-for-injectable.ts new file mode 100644 index 0000000000..655f8908b0 --- /dev/null +++ b/src/common/path/separator.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import fileSystemSeparatorInjectable from "./separator.injectable"; + +export default getGlobalOverride(fileSystemSeparatorInjectable, () => path.posix.sep); diff --git a/src/common/path/separator.injectable.ts b/src/common/path/separator.injectable.ts new file mode 100644 index 0000000000..5b0413b56f --- /dev/null +++ b/src/common/path/separator.injectable.ts @@ -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 path from "path"; + +const fileSystemSeparatorInjectable = getInjectable({ + id: "file-system-separator", + instantiate: () => path.sep, + causesSideEffects: true, +}); + +export default fileSystemSeparatorInjectable; diff --git a/src/common/test-utils/get-absolute-path-fake.ts b/src/common/test-utils/get-absolute-path-fake.ts deleted file mode 100644 index 89b5faa446..0000000000 --- a/src/common/test-utils/get-absolute-path-fake.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { GetAbsolutePath } from "../path/get-absolute-path.injectable"; - -export const getAbsolutePathFake: GetAbsolutePath = (...args) => { - const maybeAbsolutePath = args.join("/"); - - if (isAbsolutePath(maybeAbsolutePath)) { - return maybeAbsolutePath; - } - - return `/some-absolute-root-directory/${maybeAbsolutePath}`; -}; - -const isAbsolutePath = (path: string) => path.startsWith("/"); diff --git a/src/common/test-utils/join-paths-fake.ts b/src/common/test-utils/join-paths-fake.ts deleted file mode 100644 index 2796423d3c..0000000000 --- a/src/common/test-utils/join-paths-fake.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { JoinPaths } from "../path/join-paths.injectable"; - -export const joinPathsFake: JoinPaths = (...args) => args.join("/"); diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index df5efadaf6..1a85f30597 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -16,6 +16,18 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import loggerInjectable from "../../common/logger.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import watchInjectable from "../../common/fs/watch/watch.injectable"; +import accessPathInjectable from "../../common/fs/access-path.injectable"; +import copyInjectable from "../../common/fs/copy.injectable"; +import deleteFileInjectable from "../../common/fs/delete-file.injectable"; +import ensureDirInjectable from "../../common/fs/ensure-dir.injectable"; +import isProductionInjectable from "../../common/vars/is-production.injectable"; +import lstatInjectable from "../../common/fs/lstat.injectable"; +import readDirInjectable from "../../common/fs/read-dir.injectable"; +import fileSystemSeparatorInjectable from "../../common/path/separator.injectable"; +import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; +import getRelativePathInjectable from "../../common/path/get-relative-path.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -33,6 +45,18 @@ const extensionDiscoveryInjectable = getInjectable({ pathExists: di.inject(pathExistsInjectable), watch: di.inject(watchInjectable), logger: di.inject(loggerInjectable), + accessPath: di.inject(accessPathInjectable), + copy: di.inject(copyInjectable), + deleteFile: di.inject(deleteFileInjectable), + ensureDirectory: di.inject(ensureDirInjectable), + isProduction: di.inject(isProductionInjectable), + lstat: di.inject(lstatInjectable), + readDirectory: di.inject(readDirInjectable), + fileSystemSeparator: di.inject(fileSystemSeparatorInjectable), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), + getRelativePath: di.inject(getRelativePathInjectable), + joinPaths: di.inject(joinPathsInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index e6f500805a..b6e8bd0aa0 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -5,16 +5,13 @@ import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; -import fse from "fs-extra"; import { makeObservable, observable, reaction, when } from "mobx"; import os from "os"; -import path from "path"; import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc"; import { isErrnoException, toJS } from "../../common/utils"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; -import { isProduction } from "../../common/vars"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { PackageJson } from "type-fest"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; @@ -23,20 +20,44 @@ import type { ReadJson } from "../../common/fs/read-json-file.injectable"; import type { Logger } from "../../common/logger"; import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { Watch } from "../../common/fs/watch/watch.injectable"; +import type { Stats } from "fs"; +import { constants } from "fs"; +import type { DeleteFile } from "../../common/fs/delete-file.injectable"; +import type { LStat } from "../../common/fs/lstat.injectable"; +import type { ReadDirectory } from "../../common/fs/read-dir.injectable"; +import type { EnsureDirectory } from "../../common/fs/ensure-dir.injectable"; +import type { AccessPath } from "../../common/fs/access-path.injectable"; +import type { Copy } from "../../common/fs/copy.injectable"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; - extensionInstallationStateStore: ExtensionInstallationStateStore; + readonly extensionLoader: ExtensionLoader; + readonly extensionsStore: ExtensionsStore; + readonly extensionInstallationStateStore: ExtensionInstallationStateStore; + readonly extensionPackageRootDirectory: string; + readonly staticFilesDirectory: string; + readonly logger: Logger; + readonly isProduction: boolean; + readonly fileSystemSeparator: string; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise; - extensionPackageRootDirectory: string; - staticFilesDirectory: string; readJsonFile: ReadJson; pathExists: PathExists; + deleteFile: DeleteFile; + lstat: LStat; watch: Watch; - logger: Logger; + readDirectory: ReadDirectory; + ensureDirectory: EnsureDirectory; + accessPath: AccessPath; + copy: Copy; + joinPaths: JoinPaths; + getBasenameOfPath: GetBasenameOfPath; + getDirnameOfPath: GetDirnameOfPath; + getRelativePath: GetRelativePath; } export interface InstalledExtension { @@ -67,7 +88,7 @@ interface ExtensionDiscoveryChannelMessage { * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link) * @param lstat the stats to compare */ -const isDirectoryLike = (lstat: fse.Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); +const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); interface LoadFromFolderOptions { isBundled?: boolean; @@ -103,23 +124,23 @@ export class ExtensionDiscovery { } get localFolderPath(): string { - return path.join(os.homedir(), ".k8slens", "extensions"); + return this.dependencies.joinPaths(os.homedir(), ".k8slens", "extensions"); } get packageJsonPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, manifestFilename); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, manifestFilename); } get inTreeTargetPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, "extensions"); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "extensions"); } get inTreeFolderPath(): string { - return path.resolve(this.dependencies.staticFilesDirectory, "../extensions"); + return this.dependencies.joinPaths(this.dependencies.staticFilesDirectory, "../extensions"); } get nodeModulesPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, "node_modules"); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "node_modules"); } /** @@ -184,24 +205,24 @@ export class ExtensionDiscovery { handleWatchFileAdd = async (manifestPath: string): Promise => { // e.g. "foo/package.json" - const relativePath = path.relative(this.localFolderPath, manifestPath); + const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath); // Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies // that the added file is in a folder under local folder path. // This safeguards against a file watch being triggered under a sub-directory which is not an extension. - const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2; + const isUnderLocalFolderPath = relativePath.split(this.dependencies.fileSystemSeparator).length === 2; - if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { + if (this.dependencies.getBasenameOfPath(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath); - const absPath = path.dirname(manifestPath); + const absPath = this.dependencies.getDirnameOfPath(manifestPath); // this.loadExtensionFromPath updates this.packagesJson const extension = await this.loadExtensionFromFolder(absPath); if (extension) { // Remove a broken symlink left by a previous installation if it exists. - await fse.remove(extension.manifestPath); + await this.dependencies.deleteFile(extension.manifestPath); // Install dependencies for the new extension await this.dependencies.installExtension(extension.absolutePath); @@ -226,8 +247,8 @@ export class ExtensionDiscovery { handleWatchUnlinkEvent = async (filePath: string): Promise => { // Check that the removed path is directly under this.localFolderPath // Note that the watcher can create unlink events for subdirectories of the extension - const extensionFolderName = path.basename(filePath); - const expectedPath = path.relative(this.localFolderPath, filePath); + const extensionFolderName = this.dependencies.getBasenameOfPath(filePath); + const expectedPath = this.dependencies.getRelativePath(this.localFolderPath, filePath); if (expectedPath !== extensionFolderName) { return; @@ -264,7 +285,7 @@ export class ExtensionDiscovery { * @param name e.g. "@mirantis/lens-extension-cc" */ removeSymlinkByPackageName(name: string): Promise { - return fse.remove(this.getInstalledPath(name)); + return this.dependencies.deleteFile(this.getInstalledPath(name)); } /** @@ -286,7 +307,7 @@ export class ExtensionDiscovery { await this.removeSymlinkByPackageName(manifest.name); // fs.remove does nothing if the path doesn't exist anymore - await fse.remove(absolutePath); + await this.dependencies.deleteFile(absolutePath); } async load(): Promise> { @@ -301,34 +322,29 @@ export class ExtensionDiscovery { `${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`, ); - // fs.remove won't throw if path is missing - await fse.remove(path.join(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); + await this.dependencies.deleteFile(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); - try { - // Verify write access to static/extensions, which is needed for symlinking - await fse.access(this.inTreeFolderPath, fse.constants.W_OK); + const canWriteToInTreeFolder = await this.dependencies.accessPath(this.inTreeFolderPath, constants.W_OK); + if (canWriteToInTreeFolder) { // Set bundled folder path to static/extensions this.bundledFolderPath = this.inTreeFolderPath; - } catch { - // If there is error accessing static/extensions, we need to copy in-tree extensions so that we can symlink them properly on "npm install". - // The error can happen if there is read-only rights to static/extensions, which would fail symlinking. - + } else { // Remove e.g. /Users//Library/Application Support/LensDev/extensions - await fse.remove(this.inTreeTargetPath); + await this.dependencies.deleteFile(this.inTreeTargetPath); // Create folder e.g. /Users//Library/Application Support/LensDev/extensions - await fse.ensureDir(this.inTreeTargetPath); + await this.dependencies.ensureDirectory(this.inTreeTargetPath); // Copy static/extensions to e.g. /Users//Library/Application Support/LensDev/extensions - await fse.copy(this.inTreeFolderPath, this.inTreeTargetPath); + await this.dependencies.copy(this.inTreeFolderPath, this.inTreeTargetPath); // Set bundled folder path to e.g. /Users//Library/Application Support/LensDev/extensions this.bundledFolderPath = this.inTreeTargetPath; } - await fse.ensureDir(this.nodeModulesPath); - await fse.ensureDir(this.localFolderPath); + await this.dependencies.ensureDirectory(this.nodeModulesPath); + await this.dependencies.ensureDirectory(this.localFolderPath); const extensions = await this.ensureExtensions(); @@ -342,7 +358,7 @@ export class ExtensionDiscovery { * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension" */ protected getInstalledPath(name: string): string { - return path.join(this.nodeModulesPath, name); + return this.dependencies.joinPaths(this.nodeModulesPath, name); } /** @@ -350,7 +366,7 @@ export class ExtensionDiscovery { * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension/package.json" */ protected getInstalledManifestPath(name: string): string { - return path.join(this.getInstalledPath(name), manifestFilename); + return this.dependencies.joinPaths(this.getInstalledPath(name), manifestFilename); } /** @@ -362,10 +378,11 @@ export class ExtensionDiscovery { const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest; const id = this.getInstalledManifestPath(manifest.name); const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled }); - const extensionDir = path.dirname(manifestPath); - const packedName = manifest.name.replaceAll("@", "").replaceAll("/", "-"); - const npmPackage = path.join(extensionDir, `${packedName}-${manifest.version}.tgz`); - const absolutePath = (isProduction && await this.dependencies.pathExists(npmPackage)) ? npmPackage : extensionDir; + const extensionDir = this.dependencies.getDirnameOfPath(manifestPath); + const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`); + const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage) + ? npmPackage + : extensionDir; const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest); return { @@ -416,10 +433,10 @@ export class ExtensionDiscovery { async loadBundledExtensions(): Promise { const extensions: InstalledExtension[] = []; const folderPath = this.bundledFolderPath; - const paths = await fse.readdir(folderPath); + const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { - const absPath = path.resolve(folderPath, fileName); + const absPath = this.dependencies.joinPaths(folderPath, fileName); const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); if (extension) { @@ -433,7 +450,7 @@ export class ExtensionDiscovery { async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise { const extensions: InstalledExtension[] = []; - const paths = await fse.readdir(folderPath); + const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { // do not allow to override bundled extensions @@ -441,17 +458,21 @@ export class ExtensionDiscovery { continue; } - const absPath = path.resolve(folderPath, fileName); + const absPath = this.dependencies.joinPaths(folderPath, fileName); - if (!fse.existsSync(absPath)) { - continue; - } + try { + const lstat = await this.dependencies.lstat(absPath); - const lstat = await fse.lstat(absPath); + // skip non-directories + if (!isDirectoryLike(lstat)) { + continue; + } + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") { + continue; + } - // skip non-directories - if (!isDirectoryLike(lstat)) { - continue; + throw error; } const extension = await this.loadExtensionFromFolder(absPath); @@ -471,7 +492,7 @@ export class ExtensionDiscovery { * @param folderPath Folder path to extension */ async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise { - const manifestPath = path.resolve(folderPath, manifestFilename); + const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); return this.getByManifest(manifestPath, { isBundled }); } diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 7fe1cd5421..78e88a9cde 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -9,6 +9,7 @@ import { createExtensionInstanceInjectionToken } from "./create-extension-instan import extensionInstancesInjectable from "./extension-instances.injectable"; import type { LensExtension } from "../lens-extension"; import extensionInjectable from "./extension/extension.injectable"; +import loggerInjectable from "../../common/logger.injectable"; const extensionLoaderInjectable = getInjectable({ id: "extension-loader", @@ -18,6 +19,7 @@ const extensionLoaderInjectable = getInjectable({ createExtensionInstance: di.inject(createExtensionInstanceInjectionToken), extensionInstances: di.inject(extensionInstancesInjectable), getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance), + logger: di.inject(loggerInjectable), }), }); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 16b28de8e1..dcdd9dd219 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -11,7 +11,6 @@ import path from "path"; import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; import type { Disposer } from "../../common/utils"; import { isDefined, toJS } from "../../common/utils"; -import logger from "../../main/logger"; import type { InstalledExtension } from "../extension-discovery/extension-discovery"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; import type { LensRendererExtension } from "../lens-renderer-extension"; @@ -23,13 +22,15 @@ import assert from "assert"; import { EventEmitter } from "../../common/event-emitter"; import type { CreateExtensionInstance } from "./create-extension-instance.token"; import type { Extension } from "./extension/extension.injectable"; +import type { Logger } from "../../common/logger"; const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { + readonly extensionInstances: ObservableMap; + readonly logger: Logger; updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: CreateExtensionInstance; - readonly extensionInstances: ObservableMap; getExtension: (instance: LensExtension) => Extension; } @@ -159,7 +160,7 @@ export class ExtensionLoader { @action removeInstance(lensExtensionId: LensExtensionId) { - logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); + this.dependencies.logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); const instance = this.dependencies.extensionInstances.get(lensExtensionId); if (!instance) { @@ -177,7 +178,7 @@ export class ExtensionLoader { this.dependencies.extensionInstances.delete(lensExtensionId); this.nonInstancesByName.delete(instance.name); } catch (error) { - logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); + this.dependencies.logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); } } @@ -252,7 +253,7 @@ export class ExtensionLoader { } loadOnClusterManagerRenderer = () => { - logger.debug(`${logModule}: load on main renderer (cluster manager)`); + this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`); return this.autoInitExtensions(async (ext) => { const extension = ext as LensRendererExtension; @@ -274,7 +275,7 @@ export class ExtensionLoader { }; loadOnClusterRenderer = () => { - logger.debug(`${logModule}: load on cluster renderer (dashboard)`); + this.dependencies.logger.debug(`${logModule}: load on cluster renderer (dashboard)`); this.autoInitExtensions(async () => []); }; @@ -313,7 +314,7 @@ export class ExtensionLoader { activated: instance.activate(), }; } catch (err) { - logger.error(`${logModule}: error loading extension`, { ext: extension, err }); + this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); } } else if (!extension.isEnabled && alreadyInit) { this.removeInstance(extId); @@ -330,7 +331,7 @@ export class ExtensionLoader { extensions.map(extension => // If extension activation fails, log error extension.activated.catch((error) => { - logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); + this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); }), ), ); @@ -344,7 +345,7 @@ export class ExtensionLoader { // Return ExtensionLoading[] return extensions.map(extension => { const loaded = extension.instance.enable(register).catch((err) => { - logger.error(`${logModule}: failed to enable`, { ext: extension, err }); + this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); }); return { @@ -377,7 +378,7 @@ export class ExtensionLoader { } catch (error) { const message = (error instanceof Error ? error.stack : undefined) || error; - logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); + this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); } return null; diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts index 8e28c806d7..ca30b690b9 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -6,8 +6,6 @@ import electronAppInjectable from "../../electron-app/electron-app.injectable"; import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import type { App } from "electron"; -import joinPathsInjectable from "../../../common/path/join-paths.injectable"; -import { joinPathsFake } from "../../../common/test-utils/join-paths-fake"; describe("get-electron-app-path", () => { let getElectronAppPath: (name: string) => string; @@ -31,7 +29,6 @@ describe("get-electron-app-path", () => { } as App; di.override(electronAppInjectable, () => appStub); - di.override(joinPathsInjectable, () => joinPathsFake); getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string; }); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index bdb2ab8cb4..df3ca8bf87 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -18,10 +18,6 @@ import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader import type { FileSystemProvisionerStore } from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import type { UserStore } from "../common/user-store"; -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"; -import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbars/store.injectable"; import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable"; import { EventEmitter } from "../common/event-emitter"; @@ -228,8 +224,6 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => { const overrideOperatingSystem = (di: DiContainer) => { di.override(platformInjectable, () => "darwin"); - di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); - di.override(joinPathsInjectable, () => joinPathsFake); di.override(normalizedPlatformArchitectureInjectable, () => "arm64"); }; diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 5de9e15f1f..860dbc8ff2 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -5,14 +5,11 @@ import "@testing-library/jest-dom/extend-expect"; import { fireEvent, screen, waitFor } from "@testing-library/react"; -import fse from "fs-extra"; import React from "react"; import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery"; import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import { ConfirmDialog } from "../../confirm-dialog"; import { Extensions } from "../extensions"; -import mockFs from "mock-fs"; -import { mockWindow } from "../../../../../__mocks__/windowMock"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import type { DiRender } from "../../test-utils/renderFor"; @@ -20,18 +17,15 @@ import { renderFor } from "../../test-utils/renderFor"; import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; -import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import assert from "assert"; import type { InstallFromInput } from "../install-from-input/install-from-input"; import installFromInputInjectable from "../install-from-input/install-from-input.injectable"; import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import { observable, when } from "mobx"; +import type { DeleteFile } from "../../../../common/fs/delete-file.injectable"; +import deleteFileInjectable from "../../../../common/fs/delete-file.injectable"; -mockWindow(); - -jest.setTimeout(30000); -jest.mock("fs-extra"); jest.mock("../../notifications"); jest.mock("../../../../common/utils/downloadFile", () => ({ @@ -55,6 +49,7 @@ describe("Extensions", () => { let installFromInput: jest.MockedFunction; let extensionInstallationStateStore: ExtensionInstallationStateStore; let render: DiRender; + let deleteFileMock: jest.MockedFunction; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -62,18 +57,14 @@ describe("Extensions", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads"); - di.permitSideEffects(getConfigurationFileModelInjectable); - - mockFs({ - "some-directory-for-user-data": {}, - }); - render = renderFor(di); installFromInput = jest.fn(); - di.override(installFromInputInjectable, () => installFromInput); + deleteFileMock = jest.fn(); + di.override(deleteFileInjectable, () => deleteFileMock); + extensionLoader = di.inject(extensionLoaderInjectable); extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); @@ -95,10 +86,6 @@ describe("Extensions", () => { extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve()); }); - afterEach(() => { - mockFs.restore(); - }); - it("disables uninstall and disable buttons while uninstalling", async () => { extensionDiscovery.isLoaded = true; @@ -138,7 +125,7 @@ describe("Extensions", () => { const resolveInstall = observable.box(false); - (fse.unlink as jest.MockedFunction).mockReturnValue(Promise.resolve()); + deleteFileMock.mockReturnValue(Promise.resolve()); installFromInput.mockImplementation(async (input) => { expect(input).toBe("https://test.extensionurl/package.tgz"); diff --git a/src/renderer/components/animate/animate.tsx b/src/renderer/components/animate/animate.tsx index 2432797d26..aa0d673570 100644 --- a/src/renderer/components/animate/animate.tsx +++ b/src/renderer/components/animate/animate.tsx @@ -65,17 +65,23 @@ const NonInjectedAnimate = (propsAndDeps: AnimateProps & Dependencies) => { setShowClassNameEnter(true); onEnterHandler(); }); + + return noop; } else if (isVisible) { setShowClassNameLeave(true); onLeaveHandler(); // Cleanup after duration - setTimeout(() => { + const handle = setTimeout(() => { setIsVisible(false); setShowClassNameEnter(false); setShowClassNameLeave(false); }, leaveDuration); + + return () => clearTimeout(handle); } + + return noop; }, [enter]); if (!isVisible) { diff --git a/src/renderer/components/render-delay/__tests__/render-delay.test.tsx b/src/renderer/components/render-delay/__tests__/render-delay.test.tsx index 444b024df3..2c3f3c2369 100644 --- a/src/renderer/components/render-delay/__tests__/render-delay.test.tsx +++ b/src/renderer/components/render-delay/__tests__/render-delay.test.tsx @@ -6,9 +6,6 @@ import React from "react"; import "@testing-library/jest-dom/extend-expect"; import { render } from "@testing-library/react"; import { RenderDelay } from "../render-delay"; -import { mockWindow } from "../../../../../__mocks__/windowMock"; - -mockWindow(); describe("", () => { it("renders w/o errors", () => { diff --git a/src/renderer/components/render-delay/cancel-idle-callback.global-override-for-injectable.ts b/src/renderer/components/render-delay/cancel-idle-callback.global-override-for-injectable.ts new file mode 100644 index 0000000000..2bd7d0047f --- /dev/null +++ b/src/renderer/components/render-delay/cancel-idle-callback.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; +import cancelIdleCallbackInjectable from "./cancel-idle-callback.injectable"; + +export default getGlobalOverride(cancelIdleCallbackInjectable, () => () => {}); diff --git a/src/renderer/components/render-delay/cancel-idle-callback.injectable.ts b/src/renderer/components/render-delay/cancel-idle-callback.injectable.ts new file mode 100644 index 0000000000..c18d9dfa88 --- /dev/null +++ b/src/renderer/components/render-delay/cancel-idle-callback.injectable.ts @@ -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"; + +export type CancelIdleCallback = (handle: number) => void; + +const cancelIdleCallbackInjectable = getInjectable({ + id: "cancel-idle-callback", + instantiate: (): CancelIdleCallback => window.cancelIdleCallback, + causesSideEffects: true, +}); + +export default cancelIdleCallbackInjectable; diff --git a/src/renderer/components/render-delay/render-delay.tsx b/src/renderer/components/render-delay/render-delay.tsx index 2ccdaff693..efb52cbba3 100644 --- a/src/renderer/components/render-delay/render-delay.tsx +++ b/src/renderer/components/render-delay/render-delay.tsx @@ -3,42 +3,54 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React from "react"; -import { makeObservable, observable } from "mobx"; -import { observer } from "mobx-react"; +import React, { useEffect, useState } from "react"; import type { SingleOrMany } from "../../utils"; +import type { RequestIdleCallback } from "./request-idle-callback.injectable"; +import type { CancelIdleCallback } from "./cancel-idle-callback.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import cancelIdleCallbackInjectable from "./cancel-idle-callback.injectable"; +import requestIdleCallbackInjectable from "./request-idle-callback.injectable"; export interface RenderDelayProps { placeholder?: React.ReactNode; children: SingleOrMany; } -@observer -export class RenderDelay extends React.Component { - @observable isVisible = false; - - constructor(props: RenderDelayProps) { - super(props); - makeObservable(this); - } - - componentDidMount() { - const guaranteedFireTime = 1000; - - window.requestIdleCallback(this.showContents, { timeout: guaranteedFireTime }); - } - - componentWillUnmount() { - window.cancelIdleCallback(this.showContents); - } - - showContents = () => this.isVisible = true; - - render() { - if (!this.isVisible) { - return this.props.placeholder || null; - } - - return this.props.children; - } +interface Dependencies { + requestIdleCallback: RequestIdleCallback; + cancelIdleCallback: CancelIdleCallback; } + +const NonInjectedRenderDelay = (props: RenderDelayProps & Dependencies) => { + const { + cancelIdleCallback, + requestIdleCallback, + children, + placeholder, + } = props; + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const handle = requestIdleCallback(() => setIsVisible(true), { timeout: 1000 }); + + return () => cancelIdleCallback(handle); + }, []); + + return ( + <> + { + isVisible + ? placeholder ?? null + : children + } + + ); +}; + +export const RenderDelay = withInjectables(NonInjectedRenderDelay, { + getProps: (di, props) => ({ + ...props, + cancelIdleCallback: di.inject(cancelIdleCallbackInjectable), + requestIdleCallback: di.inject(requestIdleCallbackInjectable), + }), +}); diff --git a/src/renderer/components/render-delay/request-idle-callback.global-override-for-injectable.ts b/src/renderer/components/render-delay/request-idle-callback.global-override-for-injectable.ts new file mode 100644 index 0000000000..d5b47f0a22 --- /dev/null +++ b/src/renderer/components/render-delay/request-idle-callback.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; +import requestIdleCallbackInjectable from "./request-idle-callback.injectable"; + +export default getGlobalOverride(requestIdleCallbackInjectable, () => (callback) => callback()); diff --git a/src/renderer/components/render-delay/request-idle-callback.injectable.ts b/src/renderer/components/render-delay/request-idle-callback.injectable.ts new file mode 100644 index 0000000000..fd58bcc851 --- /dev/null +++ b/src/renderer/components/render-delay/request-idle-callback.injectable.ts @@ -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"; + +export type RequestIdleCallback = (callback: () => void, options: { timeout: number }) => number; + +const requestIdleCallbackInjectable = getInjectable({ + id: "request-idle-callback", + instantiate: (): RequestIdleCallback => window.requestIdleCallback, + causesSideEffects: true, +}); + +export default requestIdleCallbackInjectable; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 1ffcd0474b..70e579adb9 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -20,10 +20,6 @@ import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader import type { FileSystemProvisionerStore } from "../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import type { UserStore } from "../common/user-store"; -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"; -import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbars/store.injectable"; import terminalSpawningPoolInjectable from "./components/dock/terminal/terminal-spawning-pool.injectable"; import hostedClusterIdInjectable from "./cluster-frame-context/hosted-cluster-id.injectable"; @@ -117,9 +113,6 @@ export const getDiForUnitTesting = ( di.override(terminalSpawningPoolInjectable, () => document.createElement("div")); di.override(hostedClusterIdInjectable, () => undefined); - di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); - di.override(joinPathsInjectable, () => joinPathsFake); - di.override(historyInjectable, () => createMemoryHistory()); di.override(legacyOnChannelListenInjectable, () => () => noop); diff --git a/types/dom.d.ts b/types/dom.d.ts index 0e3e1bcb43..04e99babcd 100644 --- a/types/dom.d.ts +++ b/types/dom.d.ts @@ -8,9 +8,4 @@ declare global { interface Element { scrollIntoViewIfNeeded?(opt_center?: boolean): void; } - - interface Window { - requestIdleCallback(callback: () => void, options: { timeout: number }); - cancelIdleCallback(callback: () => void); - } }