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:
Iku-tursoandJanne Savolainen authored and GitHub committed 2022-03-31 16:57:05 +03:00
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;
};