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

Add happy path behavioural tests for upgrade chart tab

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-19 11:28:24 -04:00
parent f14e603592
commit fce3c95733
10 changed files with 6426 additions and 35 deletions

View File

@ -11,7 +11,7 @@ import { isDefined } from "../../../utils";
const requestVersionsEndpoint = urlBuilderFor("/v2/charts/:repo/:name/versions");
export type RequestHelmChartVersions = (repo: string, name: string) => Promise<HelmChart[]>;
export type RequestHelmChartVersions = (repo: string, chartName: string) => Promise<HelmChart[]>;
const requestHelmChartVersionsInjectable = getInjectable({
id: "request-helm-chart-versions",

View File

@ -0,0 +1,264 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RenderResult } from "@testing-library/react";
import type { NavigateToHelmReleases } from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmReleaseConfiguration } from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import requestHelmReleaseConfigurationInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import type { RequestHelmReleases } from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
describe("New Upgrade Helm Chart Dock Tab", () => {
let builder: ApplicationBuilder;
let renderResult: RenderResult;
let requestHelmReleaseConfigurationMock: AsyncFnMock<RequestHelmReleaseConfiguration>;
let requestHelmReleasesMock: AsyncFnMock<RequestHelmReleases>;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let navigateToHelmReleases: NavigateToHelmReleases;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
requestHelmReleaseConfigurationMock = asyncFn();
windowDi.override(requestHelmReleaseConfigurationInjectable, () => requestHelmReleaseConfigurationMock);
requestHelmReleasesMock = asyncFn();
windowDi.override(requestHelmReleasesInjectable, () => requestHelmReleasesMock);
requestHelmChartsMock = asyncFn();
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
requestHelmChartVersionsMock = asyncFn();
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
navigateToHelmReleases = windowDi.inject(navigateToHelmReleasesInjectable);
});
builder.namespaces.add("my-first-namespace");
builder.namespaces.add("my-second-namespace");
renderResult = await builder.render();
const dockStore = builder.applicationWindow.only.di.inject(dockStoreInjectable);
// TODO: Make TerminalWindow unit testable to allow realistic behaviour
dockStore.closeTab("terminal");
});
describe("given a namespace is selected", () => {
beforeEach(() => {
builder.namespaces.select("my-second-namespace");
});
describe("when navigating to the helm releases view", () => {
beforeEach(() => {
navigateToHelmReleases();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("requests helm releases for the selected namespace", () => {
expect(requestHelmReleasesMock).toBeCalledWith("my-second-namespace");
});
describe("when helm releases resolves", () => {
beforeEach(async () => {
await requestHelmReleasesMock.resolve([
{
appVersion: "some-app-version",
name: "some-name",
namespace: "my-second-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
]);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when clicking the menu for a helm release", () => {
beforeEach(() => {
const helmReleaseMenu = renderResult.getByTestId("menu-actions-icon-for-release-menu-for-my-second-namespace/some-name");
helmReleaseMenu.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when clicking the upgrade chart menu item", () => {
beforeEach(() => {
const upgradeHelmChartMenuItem = renderResult.getByTestId("upgrade-chart-menu-item-for-my-second-namespace/some-name");
upgradeHelmChartMenuItem.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("requests all helm charts", () => {
expect(requestHelmChartsMock).toBeCalled();
});
describe("when request for all helm charts resolves", () => {
beforeEach(async () => {
await requestHelmChartsMock.resolve([
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-third-time",
name: "some-chart",
repo: "some-third-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.0",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
]);
});
it("requests versions of the helm charts", () => {
expect(requestHelmChartVersionsMock).toBeCalledWith(
"some-repo",
"some-chart",
);
expect(requestHelmChartVersionsMock).toBeCalledWith(
"some-third-repo",
"some-chart",
);
});
it("shows the dock tab of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("dock-tab-content-for-some-irrelevant-random-id")).toBeInTheDocument();
});
it("does not yet show the dock contents of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("upgrade-chart-dock-tab-contents-for-my-second-namespace/some-name")).not.toBeInTheDocument();
});
describe("when the requests of versions of the helm charts resolves", () => {
beforeEach(async () => {
await requestHelmChartVersionsMock.resolveSpecific(
["some-repo", "some-chart"],
[
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.0",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "1.0.1",
created: "at-some-time",
name: "some-chart",
repo: "some-repo",
}),
],
);
await requestHelmChartVersionsMock.resolveSpecific(
["some-third-repo", "some-chart"],
[
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.0",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
HelmChart.create({
apiVersion: "1.2.3",
version: "0.9.1",
created: "at-some-other-time",
name: "some-other-chart",
repo: "some-repo",
}),
],
);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows the dock contents of the upgrade chart tab", () => {
expect(renderResult.queryByTestId("upgrade-chart-dock-tab-contents-for-my-second-namespace/some-name")).toBeInTheDocument();
});
it("requests the configuration for the specific helm release", () => {
expect(requestHelmReleaseConfigurationMock).toBeCalledWith(
"some-name",
"my-second-namespace",
true,
);
});
describe("when the configuration request resolves", () => {
beforeEach(async () => {
await requestHelmReleaseConfigurationMock.resolve("some confiration for the helm release");
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
describe("when closing the upgrade chart tab", () => {
beforeEach(() => {
const closeDockTab = renderResult.getByTestId("dock-tab-close-for-some-irrelevant-random-id");
closeDockTab.click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("does so", () => {
expect(renderResult.queryByTestId("dock-tab-content-for-some-irrelevant-random-id")).not.toBeInTheDocument();
});
});
});
});
});
});
});
});
});
});
});

View File

@ -61,7 +61,10 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
<span className="title">Rollback</span>
</MenuItem>
)}
<MenuItem onClick={this.upgrade}>
<MenuItem
onClick={this.upgrade}
data-testid={`upgrade-chart-menu-item-for-${release.getId()}`}
>
<Icon
material="refresh"
interactive={toolbar}
@ -79,6 +82,10 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
return (
<MenuActions
id={`menu-actions-for-release-menu-for-${release.getId()}`}
triggerIcon={{
material: "more_vert",
"data-testid": `menu-actions-icon-for-release-menu-for-${release.getId()}`,
}}
{...menuProps}
className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove}

View File

@ -4,7 +4,6 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
import releaseSecretsInjectable from "./release-secrets.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
@ -15,28 +14,17 @@ const releasesInjectable = getInjectable({
instantiate: (di) => {
const clusterContext = di.inject(clusterFrameContextInjectable);
const namespaceStore = di.inject(namespaceStoreInjectable);
const releaseSecrets = di.inject(releaseSecretsInjectable);
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
const toHelmRelease = di.inject(toHelmReleaseInjectable);
return asyncComputed(async () => {
const contextNamespaces = namespaceStore.contextNamespaces || [];
void releaseSecrets.get();
const isLoadingAll =
clusterContext.allNamespaces?.length > 1 &&
clusterContext.cluster?.accessibleNamespaces.length === 0 &&
clusterContext.allNamespaces.every((namespace) =>
contextNamespaces.includes(namespace),
);
const releaseArrays = await (isLoadingAll ? requestHelmReleases() : Promise.all(
contextNamespaces.map((namespace) =>
requestHelmReleases(namespace),
),
));
const releaseArrays = await (clusterContext.hasSelectedAll
? requestHelmReleases()
: Promise.all(clusterContext.contextNamespaces.map(namespace => requestHelmReleases(namespace)))
);
return releaseArrays.flat().map(toHelmRelease);
}, []);

View File

@ -103,6 +103,7 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
material="close"
tooltip={`Close ${this.props.isMac ? "⌘+W" : "Ctrl+W"}`}
onClick={close}
data-testid={`dock-tab-close-for-${id}`}
/>
</div>
)}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { waitUntilDefined } from "../../../utils";
import upgradeChartTabStoreInjectable from "./store.injectable";
const upgradeChartTabDataInjectable = getInjectable({
id: "upgrade-chart-tab-data",
instantiate: (di, tabId) => {
const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable);
return waitUntilDefined(() => upgradeChartTabStore.getData(tabId));
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, tabId: string) => tabId,
}),
});
export default upgradeChartTabDataInjectable;

View File

@ -16,7 +16,7 @@ import requestHelmReleaseConfigurationInjectable from "../../../../common/k8s-ap
import { waitUntilDefined } from "../../../utils";
import type { SelectOption } from "../../select";
import type { DockTab } from "../dock/store";
import upgradeChartTabStoreInjectable from "./store.injectable";
import upgradeChartTabDataInjectable from "./tab-data.injectable";
export interface UpgradeChartModel {
readonly release: HelmRelease;
@ -41,13 +41,19 @@ export interface UpgradeChartSubmitResult {
const upgradeChartModelInjectable = getInjectable({
id: "upgrade-chart-model",
instantiate: async (di, tab): Promise<UpgradeChartModel> => {
const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable);
const releases = di.inject(releasesInjectable);
const requestHelmReleaseConfiguration = di.inject(requestHelmReleaseConfigurationInjectable);
const updateRelease = di.inject(updateReleaseInjectable);
const tabData = await di.inject(upgradeChartTabDataInjectable, tab.id);
const tabData = await waitUntilDefined(() => upgradeChartTabStore.getData(tab.id));
const release = await waitUntilDefined(() => releases.value.get().find(release => release.getName() === tabData.releaseName));
const release = await waitUntilDefined(() => (
releases.value
.get()
.find(release => (
release.getName() === tabData.releaseName
&& release.getNs() === tabData.releaseNamespace
))
));
const versions = di.inject(helmChartVersionsInjectable, release);
const storedConfiguration = asyncComputed(() => requestHelmReleaseConfiguration(
release.getName(),

View File

@ -6,7 +6,6 @@
import "./upgrade-chart.scss";
import React from "react";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import { cssNames } from "../../../utils";
import type { DockTab } from "../dock/store";
@ -32,13 +31,6 @@ interface Dependencies {
@observer
export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps & Dependencies> {
@observable error?: string;
constructor(props: UpgradeChartProps & Dependencies) {
super(props);
makeObservable(this);
}
upgrade = async () => {
const { model } = this.props;
const { completedSuccessfully } = await model.submit();
@ -63,7 +55,10 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
const { release } = model;
return (
<div className={cssNames("UpgradeChart flex column", className)}>
<div
className={cssNames("UpgradeChart flex column", className)}
data-testid={`upgrade-chart-dock-tab-contents-for-${release.getId()}`}
>
<InfoPanel
tabId={tabId}
error={model.configration.error.get()}

View File

@ -6,7 +6,7 @@ import type { LensRendererExtension } from "../../../extensions/lens-renderer-ex
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
import type { IComputedValue, ObservableMap } from "mobx";
import { computed, observable, runInAction } from "mobx";
import { action, computed, observable, runInAction } from "mobx";
import React from "react";
import { Router } from "react-router";
import allowedResourcesInjectable from "../../cluster-frame-context/allowed-resources.injectable";
@ -120,7 +120,10 @@ export interface ApplicationBuilder {
click: (id: string) => void;
};
};
namespaces: {
add: (namespace: string) => void;
select: (namespace: string) => void;
};
helmCharts: {
navigate: NavigateToHelmCharts;
};
@ -244,6 +247,9 @@ export const getApplicationBuilder = () => {
let applicationHasStarted = false;
const namespaces = observable.array<string>();
const selectedNamespaces = observable.array<string>();
const builder: ApplicationBuilder = {
mainDi,
applicationWindow: {
@ -304,7 +310,10 @@ export const getApplicationBuilder = () => {
return builder.applicationWindow.get(id);
},
},
namespaces: {
add: action((namespace) => namespaces.push(namespace)),
select: action((namespace) => selectedNamespaces.push(namespace)),
},
applicationMenu: {
click: (path: string) => {
const applicationMenuItems = mainDi.inject(
@ -470,7 +479,8 @@ export const getApplicationBuilder = () => {
// TODO: Figure out a way to remove this stub.
const namespaceStoreStub = {
isLoaded: true,
contextNamespaces: [],
contextNamespaces: selectedNamespaces,
allowedNamespaces: namespaces,
contextItems: [],
api: {
kind: "Namespace",