diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000000..742642b7eb --- /dev/null +++ b/.swcrc @@ -0,0 +1,18 @@ +{ + "module": { + "type": "commonjs" + }, + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": true, + "dynamicImport": false + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2019" + } +} diff --git a/package.json b/package.json index b30076ec10..54be0d0766 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,12 @@ "collectCoverage": false, "verbose": true, "transform": { - "^.+\\.tsx?$": "ts-jest" + "^.+\\.(t|j)sx?$": [ + "@swc/jest" + ] }, + "testEnvironment": "jsdom", + "resolver": "/src/jest-28-resolver.js", "moduleNameMapper": { "\\.(css|scss)$": "identity-obj-proxy", "\\.(svg|png|jpg|eot|woff2?|ttf)$": "/__mocks__/assetMock.ts" @@ -76,11 +80,7 @@ "setupFilesAfterEnv": [ "/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", diff --git a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap index e323205008..72075146e7 100644 --- a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap +++ b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap @@ -444,8 +444,14 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts
+ class="NoItems flex box grow" + > +
+ Item list is empty +
+
{ 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", () => { diff --git a/src/common/test-utils/flush-promises.ts b/src/common/test-utils/flush-promises.ts index 55335fe445..e9f0ebea69 100644 --- a/src/common/test-utils/flush-promises.ts +++ b/src/common/test-utils/flush-promises.ts @@ -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)); diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index ae150e4f99..765762c9bc 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -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 () => { diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index 4e71f5ff7c..0ae51623ad 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -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(); }); }); - diff --git a/src/jest-28-resolver.js b/src/jest-28-resolver.js new file mode 100644 index 0000000000..46a94a5288 --- /dev/null +++ b/src/jest-28-resolver.js @@ -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; + }, + }); +}; diff --git a/src/jest.setup.ts b/src/jest.setup.ts index da68884b19..d73c85bb40 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -22,6 +22,8 @@ fetchMock.enableMocks(); // Mock __non_webpack_require__ for tests globalThis.__non_webpack_require__ = jest.fn(); +global.setImmediate = jest.useRealTimers as unknown as typeof setImmediate; + process.on("unhandledRejection", (err: any) => { fail(err); }); diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 1fd6797038..05c8f8a7d9 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -63,19 +63,11 @@ describe("protocol router tests", () => { }); it("should throw on non-lens URLS", async () => { - try { - expect(await lpr.route("https://google.ca")).toBeUndefined(); - } catch (error) { - expect(error).toBeInstanceOf(Error); - } + expect(lpr.route("https://google.ca")).rejects.toBeDefined(); }); it("should throw when host not internal or extension", async () => { - try { - expect(await lpr.route("lens://foobar")).toBeUndefined(); - } catch (error) { - expect(error).toBeInstanceOf(Error); - } + expect(lpr.route("lens://foobar")).rejects.toBeDefined(); }); it("should not throw when has valid host", async () => { diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 069846c0ac..c27f3f05a1 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -134,7 +134,7 @@ class NonInjectedAddCluster extends React.Component { {this.allErrors.length > 0 && ( <>

KubeConfig Yaml Validation Errors:

- {...this.allErrors.map(error =>
{error}
)} + {this.allErrors.map(error =>
{error}
)} )}
diff --git a/src/renderer/components/badge/has-text-selected.injectable.ts b/src/renderer/components/badge/has-text-selected.injectable.ts new file mode 100644 index 0000000000..f2cba73cbb --- /dev/null +++ b/src/renderer/components/badge/has-text-selected.injectable.ts @@ -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; diff --git a/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap b/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap index 253a79f941..336f0fadc4 100644 --- a/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap +++ b/src/renderer/components/tooltip/__snapshots__/tooltip.test.tsx.snap @@ -18,6 +18,7 @@ exports[` renders to DOM when forced to by visibile prop 1`] = ` @@ -34,7 +35,7 @@ exports[` renders to DOM when hovering over target 1`] = `