mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Technical requirements for behavioural unit tests (#5084)
* Implement a lot of technical requirements for behavioural unit tests Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced. Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables. Note: another chunk of global shared state is no more. Note: a lot of explicit side effects have been cornered to injectables. Note: a lot of stuff has become reactive as part if this. Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make a directory commonly available Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Require id for <Select /> to prevent non-deterministic renders This was caused by global state in a 3rd party lib: "react-select". Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Specify id for all <Select /> to satisfy previous commit Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent explicit side effect in component by using existing dependency instead Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Extract instantiation of "conf" as injectables for causing side effects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Make legacy unit tests for hotbar green and more simple by using the new legacy helper Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Temporarily kludge all unit tests green with a disclaimer about allowing side-effects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent old unit test with side effects from accessing file system Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Migrate to actual typing for di.permitSideEffects Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Adapt integration tests to recent changes Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix code style Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix artifact from bad rebase Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Add a deprecation from a review comment Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove change that is not required Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant comment Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix code style Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant file Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix bad merge Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Improve variable name Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Tweak logger interface to be more descriptive Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make injecting legacy singleton always provide new instance Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove conditional typing when not needed Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Improve naming of variable Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove unnecessary code style changes Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove flag for causing side effects from too broad scope Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Override side-effects in unit test using injectable instead of monkey patching Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Flag some side-effects and add general overrides Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Fix unit tests in CI by removing explicit side-effect Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove explicit side-effect from getting default shell Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce abstraction for getting absolute paths Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using abstraction for getting absolute path to control explicit side effect Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Introduce abstraction for joining paths Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Switch to using abstraction for joining paths to control explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix fake implementation for join paths Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix test after removing explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove explicit side effects from kubeconfig-syncs Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix arguments after removing explicit side effect Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make registrators not async for not being needed anymore Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Make generalCatalogEntities non-observable, as there is no requirement Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Remove redundant code Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Simplify logic for registering general catalog entity sources Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add TODO Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Replace function for getting application menu items with reactive solution Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Fix typo in interface name Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove global shared state usages of hot bar store Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove redundant enum Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
1 parent
ab2cd27a27
commit
a277cfcf02
583 files changed
+13073
-6556
No files matched your search
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
|
||||
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
|
||||
import { computed, IObservableArray, observable, runInAction } from "mobx";
|
||||
import { renderFor } from "./renderFor";
|
||||
import observableHistoryInjectable from "../../navigation/observable-history.injectable";
|
||||
import React from "react";
|
||||
import { Router } from "react-router";
|
||||
import { Observer } from "mobx-react";
|
||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||
import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable";
|
||||
import { fireEvent, RenderResult } from "@testing-library/react";
|
||||
import type { KubeResource } from "../../../common/rbac";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import { Sidebar } from "../layout/sidebar";
|
||||
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import type { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
||||
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
|
||||
import currentRouteComponentInjectable from "../../routes/current-route-component.injectable";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { flatMap, compact, join, get, filter } from "lodash/fp";
|
||||
import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
|
||||
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import applicationMenuItemsInjectable, { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
|
||||
|
||||
type Callback = (dis: DiContainers) => void | Promise<void>;
|
||||
|
||||
export interface ApplicationBuilder {
|
||||
dis: DiContainers;
|
||||
setEnvironmentToClusterFrame: () => ApplicationBuilder;
|
||||
addExtensions: (...extensions: LensRendererExtension[]) => Promise<ApplicationBuilder>;
|
||||
allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder;
|
||||
beforeSetups: (callback: Callback) => ApplicationBuilder;
|
||||
beforeRender: (callback: Callback) => ApplicationBuilder;
|
||||
render: () => Promise<RenderResult>;
|
||||
|
||||
applicationMenu: {
|
||||
click: (path: string) => void;
|
||||
};
|
||||
|
||||
preferences: {
|
||||
close: () => void;
|
||||
navigate: () => void;
|
||||
navigation: {
|
||||
click: (id: string) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DiContainers {
|
||||
rendererDi: DiContainer;
|
||||
mainDi: DiContainer;
|
||||
}
|
||||
|
||||
interface Environment {
|
||||
renderSidebar: () => React.ReactNode;
|
||||
onAllowKubeResource: () => void;
|
||||
}
|
||||
|
||||
export const getApplicationBuilder = () => {
|
||||
const { rendererDi, mainDi, runSetups } = getDisForUnitTesting({
|
||||
doGeneralOverrides: true,
|
||||
});
|
||||
|
||||
const dis = { rendererDi, mainDi };
|
||||
|
||||
const clusterStoreStub = {
|
||||
getById: (): null => null,
|
||||
} as unknown as ClusterStore;
|
||||
|
||||
rendererDi.override(clusterStoreInjectable, () => clusterStoreStub);
|
||||
mainDi.override(clusterStoreInjectable, () => clusterStoreStub);
|
||||
|
||||
const beforeSetupsCallbacks: Callback[] = [];
|
||||
const beforeRenderCallbacks: Callback[] = [];
|
||||
|
||||
const extensionsState = observable.array<LensRendererExtension>();
|
||||
|
||||
rendererDi.override(subscribeStoresInjectable, () => () => () => {});
|
||||
|
||||
const environments: Record<string, Environment> = {
|
||||
application: {
|
||||
renderSidebar: () => null,
|
||||
|
||||
onAllowKubeResource: () => {
|
||||
throw new Error(
|
||||
"Tried to allow kube resource when environment is not cluster frame.",
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
clusterFrame: {
|
||||
renderSidebar: () => <Sidebar />,
|
||||
onAllowKubeResource: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
let environment = environments.application;
|
||||
|
||||
rendererDi.override(
|
||||
currentlyInClusterFrameInjectable,
|
||||
() => environment === environments.clusterFrame,
|
||||
);
|
||||
|
||||
rendererDi.override(rendererExtensionsInjectable, () =>
|
||||
computed(() => extensionsState),
|
||||
);
|
||||
|
||||
mainDi.override(mainExtensionsInjectable, () =>
|
||||
computed((): LensMainExtension[] => []),
|
||||
);
|
||||
|
||||
let allowedResourcesState: IObservableArray<KubeResource>;
|
||||
let rendered: RenderResult;
|
||||
|
||||
const builder: ApplicationBuilder = {
|
||||
dis,
|
||||
|
||||
applicationMenu: {
|
||||
click: (path: string) => {
|
||||
const applicationMenuItems = mainDi.inject(
|
||||
applicationMenuItemsInjectable,
|
||||
);
|
||||
|
||||
const menuItems = pipeline(
|
||||
applicationMenuItems.get(),
|
||||
flatMap(toFlatChildren(null)),
|
||||
filter((menuItem) => !!menuItem.click),
|
||||
);
|
||||
|
||||
const menuItem = menuItems.find((menuItem) => menuItem.path === path);
|
||||
|
||||
if (!menuItem) {
|
||||
const availableIds = menuItems.map(get("path")).join('", "');
|
||||
|
||||
throw new Error(
|
||||
`Tried to click application menu item with ID "${path}" which does not exist. Available IDs are: "${availableIds}"`,
|
||||
);
|
||||
}
|
||||
|
||||
menuItem.click(undefined, undefined, undefined);
|
||||
},
|
||||
},
|
||||
|
||||
preferences: {
|
||||
close: () => {
|
||||
const link = rendered.getByTestId("close-preferences");
|
||||
|
||||
fireEvent.click(link);
|
||||
},
|
||||
|
||||
navigate: () => {
|
||||
const navigateToPreferences = rendererDi.inject(navigateToPreferencesInjectable);
|
||||
|
||||
navigateToPreferences();
|
||||
},
|
||||
|
||||
navigation: {
|
||||
click: (id: string) => {
|
||||
const link = rendered.queryByTestId(`tab-link-for-${id}`);
|
||||
|
||||
if (!link) {
|
||||
const preferencesNavigationItems = rendererDi.inject(
|
||||
preferenceNavigationItemsInjectable,
|
||||
);
|
||||
|
||||
const availableIds = preferencesNavigationItems
|
||||
.get()
|
||||
.map(get("id"));
|
||||
|
||||
throw new Error(
|
||||
`Tried to click navigation item "${id}" which does not exist in preferences. Available IDs are "${availableIds.join(
|
||||
'", "',
|
||||
)}"`,
|
||||
);
|
||||
}
|
||||
|
||||
fireEvent.click(link);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
setEnvironmentToClusterFrame: () => {
|
||||
environment = environments.clusterFrame;
|
||||
|
||||
allowedResourcesState = observable.array();
|
||||
|
||||
rendererDi.override(allowedResourcesInjectable, () =>
|
||||
computed(() => new Set([...allowedResourcesState])),
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/irrelevant",
|
||||
);
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
addExtensions: async (...extensions) => {
|
||||
const extensionRegistrators = rendererDi.injectMany(
|
||||
extensionRegistratorInjectionToken,
|
||||
);
|
||||
|
||||
const addAndEnableExtensions = async () => {
|
||||
const registratorPromises = extensions.flatMap((extension) =>
|
||||
extensionRegistrators.map((registrator) => registrator(extension, 1)),
|
||||
);
|
||||
|
||||
await Promise.all(registratorPromises);
|
||||
|
||||
runInAction(() => {
|
||||
extensions.forEach((extension) => {
|
||||
extensionsState.push(extension);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (rendered) {
|
||||
await addAndEnableExtensions();
|
||||
} else {
|
||||
builder.beforeRender(addAndEnableExtensions);
|
||||
}
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
allowKubeResource: (resourceName) => {
|
||||
environment.onAllowKubeResource();
|
||||
|
||||
runInAction(() => {
|
||||
allowedResourcesState.push(resourceName);
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
beforeSetups(callback: (dis: DiContainers) => void) {
|
||||
beforeSetupsCallbacks.push(callback);
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
beforeRender(callback: (dis: DiContainers) => void) {
|
||||
beforeRenderCallbacks.push(callback);
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
async render() {
|
||||
for (const callback of beforeSetupsCallbacks) {
|
||||
await callback(dis);
|
||||
}
|
||||
|
||||
await runSetups();
|
||||
|
||||
const render = renderFor(rendererDi);
|
||||
|
||||
const history = rendererDi.inject(observableHistoryInjectable);
|
||||
|
||||
const currentRouteComponent = rendererDi.inject(
|
||||
currentRouteComponentInjectable,
|
||||
);
|
||||
|
||||
for (const callback of beforeRenderCallbacks) {
|
||||
await callback(dis);
|
||||
}
|
||||
|
||||
rendered = render(
|
||||
<Router history={history}>
|
||||
{environment.renderSidebar()}
|
||||
|
||||
<Observer>
|
||||
{() => {
|
||||
const Component = currentRouteComponent.get();
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component />;
|
||||
}}
|
||||
</Observer>
|
||||
</Router>,
|
||||
);
|
||||
|
||||
return rendered;
|
||||
},
|
||||
};
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
const toFlatChildren =
|
||||
(parentId: string) =>
|
||||
({
|
||||
submenu = [],
|
||||
...menuItem
|
||||
}: MenuItemOpts): (MenuItemOpts & { path: string })[] =>
|
||||
[
|
||||
{
|
||||
...menuItem,
|
||||
path: pipeline([parentId, menuItem.id], compact, join(".")),
|
||||
},
|
||||
...submenu.flatMap(toFlatChildren(menuItem.id)),
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
|
||||
export class TestExtension extends LensRendererExtension {}
|
||||
|
||||
export const getRendererExtensionFake = ({ id, ...rest }: Partial<LensRendererExtension>) => {
|
||||
const instance = new TestExtension({
|
||||
id,
|
||||
absolutePath: "irrelevant",
|
||||
isBundled: false,
|
||||
isCompatible: false,
|
||||
isEnabled: false,
|
||||
manifest: { name: id, version: "some-version" },
|
||||
manifestPath: "irrelevant",
|
||||
});
|
||||
|
||||
Object.assign(instance, rest);
|
||||
|
||||
return instance;
|
||||
};
|
||||
Reference in new issue
Block a user