Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,554 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`legacy extension adding cluster frame components given custom components for cluster view available renders 1`] = `
<div>
<div
class="Animate slide-right Drawer KubeObjectDetails flex column right enter leave"
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="drawer-wrapper flex column"
>
<div
class="drawer-title flex align-center"
>
<div
class="drawer-title-text flex gaps align-center"
>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div>
Close
</div>
</div>
<div
class="drawer-content flex column box grow"
/>
</div>
<div
class="ResizingAnchor horizontal leading"
/>
</div>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-workloads"
>
<a
aria-current="page"
class="navItem active"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Workloads
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span>
Network
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Helm
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span>
Custom Resources
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-overview"
role="tab"
tabindex="0"
>
<div
class="label"
>
Overview
</div>
</div>
</div>
<main
class="scrollable"
>
<div
class="WorkloadsOverview flex column gaps"
data-testid="page-for-workloads-overview"
>
<div
class="header flex gaps align-center"
>
<h5
class="box grow"
>
Overview
</h5>
<div
class="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
>
<div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-overview-namespace-select-filter-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-13cymwt-control"
>
<div
class="Select__value-container Select__value-container--is-multi css-1fdsijx-ValueContainer"
>
<div
class="Select__placeholder css-1jqq78o-placeholder"
id="react-select-overview-namespace-select-filter-input-placeholder"
>
All namespaces
</div>
<div
class="Select__input-container css-qbdosj-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="overview-namespace-select-filter-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1u9des2-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-1xc3v61-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="OverviewStatuses"
>
<div
class="workloads"
/>
</div>
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-for-terminal"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-terminal"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-terminal"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu-actions-for-dock"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
</i>
<div>
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_up"
>
keyboard_arrow_up
</span>
</i>
<div>
Open
</div>
</div>
</div>
</div>
</div>
</div>
<div
data-testid="test-modal"
>
test modal
</div>
</div>
`;
@@ -0,0 +1,605 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`workload overview when navigating to workload overview renders 1`] = `
<body>
<div>
<div
class="Animate slide-right Drawer KubeObjectDetails flex column right enter leave"
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="drawer-wrapper flex column"
>
<div
class="drawer-title flex align-center"
>
<div
class="drawer-title-text flex gaps align-center"
>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div>
Close
</div>
</div>
<div
class="drawer-content flex column box grow"
/>
</div>
<div
class="ResizingAnchor horizontal leading"
/>
</div>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-workloads"
>
<a
aria-current="page"
class="navItem active"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Workloads
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span>
Network
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span>
Helm
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="navItem"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span>
Custom Resources
</span>
<i
class="Icon expandIcon material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-overview"
role="tab"
tabindex="0"
>
<div
class="label"
>
Overview
</div>
</div>
<div
class="Tab flex gaps align-center"
data-is-active-test="false"
data-testid="tab-link-for-pods"
role="tab"
tabindex="0"
>
<div
class="label"
>
Pods
</div>
</div>
</div>
<main
class="scrollable"
>
<div
class="WorkloadsOverview flex column gaps"
data-testid="page-for-workloads-overview"
>
<div
class="header flex gaps align-center"
>
<h5
class="box grow"
>
Overview
</h5>
<div
class="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
>
<div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-overview-namespace-select-filter-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-13cymwt-control"
>
<div
class="Select__value-container Select__value-container--is-multi css-1fdsijx-ValueContainer"
>
<div
class="Select__placeholder css-1jqq78o-placeholder"
id="react-select-overview-namespace-select-filter-input-placeholder"
>
All namespaces
</div>
<div
class="Select__input-container css-qbdosj-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="overview-namespace-select-filter-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1u9des2-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-1xc3v61-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="OverviewStatuses"
>
<div
class="workloads"
>
<div
class="workload"
>
<div
class="title"
>
<a>
Pods (0)
</a>
</div>
<div
class="OverviewWorkloadStatus"
>
<div
class="flex column align-center box grow"
>
<div
class="Chart PieChart flex column align-center"
data-testid="workload-overview-status-chart-pods"
>
<div
class="chart-container"
>
<canvas
class="chartjs-render-monitor"
height="0"
style="display: block;"
width="0"
/>
<div
class="chartjs-tooltip flex column"
/>
</div>
<div
class="legend flex wrap"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-for-terminal"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
data-testid="dock-tab-close-for-terminal"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div
data-testid="tooltip-content-for-dock-tab-close-for-terminal"
>
Close ⌘+W
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu-actions-for-dock"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
</i>
<div>
New tab
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
</i>
<div>
Fit to window
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_up"
>
keyboard_arrow_up
</span>
</i>
<div>
Open
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
export type ClearClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
export const clearClusterAsDeletingChannel: ClearClusterAsDeletingChannel = {
id: "clear-cluster-as-deleting",
};
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
export type DeleteClusterChannel = RequestChannel<ClusterId, void>;
export const deleteClusterChannel: DeleteClusterChannel = {
id: "delete-cluster",
};
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
export type SetClusterAsDeletingChannel = RequestChannel<ClusterId, void>;
export const setClusterAsDeletingChannel: SetClusterAsDeletingChannel = {
id: "set-cluster-as-deleting",
};
@@ -0,0 +1,267 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node";
import type { RenderResult } from "@testing-library/react";
import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../main/context-handler/create-context-handler.injectable";
import createKubeconfigManagerInjectable from "../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable";
import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { Cluster } from "../../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { advanceFakeTime } from "../../../common/test-utils/use-fake-time";
const currentClusterServerUrl = "https://localhost";
const nonCurrentClusterServerUrl = "http://localhost";
const multiClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${currentClusterServerUrl}
name: some-current-context-cluster
- cluster:
server: ${nonCurrentClusterServerUrl}
name: some-non-current-context-cluster
contexts:
- context:
cluster: some-current-context-cluster
user: some-user
name: some-current-context
- context:
cluster: some-non-current-context-cluster
user: some-user
name: some-non-current-context
current-context: some-current-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
const singleClusterServerUrl = "http://localhost";
const singleClusterConfig = `
apiVersion: v1
clusters:
- cluster:
server: ${singleClusterServerUrl}
name: some-cluster
contexts:
- context:
cluster: some-cluster
user: some-user
name: some-context
current-context: some-context
kind: Config
preferences: {}
users:
- name: some-user
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
describe("Deleting a cluster", () => {
let builder: ApplicationBuilder;
let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let rendered: RenderResult;
let config: KubeConfig;
beforeEach(async () => {
config = new KubeConfig();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
mainDi.override(normalizedPlatformInjectable, () => "darwin");
});
builder.beforeWindowStart((windowDi) => {
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
});
builder.afterWindowStart(windowDi => {
createCluster = windowDi.inject(createClusterInjectionToken);
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
rendered = await builder.render();
});
describe("when the kubeconfig has multiple clusters", () => {
let currentCluster: Cluster;
let nonCurrentCluster: Cluster;
beforeEach(() => {
config.loadFromString(multiClusterConfig);
currentCluster = createCluster({
id: "some-current-context-cluster",
contextName: "some-current-context",
preferences: {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
nonCurrentCluster = createCluster({
id: "some-non-current-context-cluster",
contextName: "some-non-current-context",
preferences: {
clusterName: "some-non-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
});
describe("when the dialog is opened for the current cluster", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog(config, currentCluster);
advanceFakeTime(1000);
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows context switcher", () => {
expect(rendered.queryByText("Select new context...")).toBeInTheDocument();
});
it("shows warning", () => {
expect(rendered.queryByTestId("current-context-warning")).toBeInTheDocument();
});
});
describe("when the dialog is opened for not the current cluster", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog(config, nonCurrentCluster);
advanceFakeTime(1000);
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("kubeconfig-change-warning")).toBeInTheDocument();
});
it("does not show context switcher", () => {
expect(rendered.queryByText("Select new context...")).not.toBeInTheDocument();
});
describe("when context switching checkbox is clicked", () => {
beforeEach(() => {
rendered.getByTestId("delete-cluster-dialog-context-switch").click();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows context switcher", () => {
expect(rendered.queryByText("Select new context...")).toBeInTheDocument();
});
});
});
});
describe("when an internal kubeconfig cluster is used", () => {
let currentCluster: Cluster;
beforeEach(() => {
config.loadFromString(singleClusterConfig);
const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable);
const joinPaths = builder.applicationWindow.only.di.inject(joinPathsInjectable);
currentCluster = createCluster({
id: "some-cluster",
contextName: "some-context",
preferences: {
clusterName: "some-cluster",
},
kubeConfigPath: joinPaths(directoryForKubeConfigs, "some-cluster.json"),
}, {
clusterServerUrl: singleClusterServerUrl,
});
});
describe("when the dialog is opened", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog(config, currentCluster);
advanceFakeTime(1000);
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("internal-kubeconfig-warning")).toBeInTheDocument();
});
});
});
describe("when the kubeconfig has only one cluster", () => {
let currentCluster: Cluster;
beforeEach(() => {
config.loadFromString(singleClusterConfig);
currentCluster = createCluster({
id: "some-cluster",
contextName: "some-context",
preferences: {
clusterName: "some-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: singleClusterServerUrl,
});
});
describe("when the dialog is opened", () => {
// TODO: replace with actual behaviour instead of technical use
beforeEach(async () => {
openDeleteClusterDialog(config, currentCluster);
advanceFakeTime(1000);
await rendered.findByTestId("delete-cluster-dialog");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows warning", () => {
expect(rendered.queryByTestId("no-more-contexts-warning")).toBeInTheDocument();
});
});
});
});
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel";
const clearClusterAsDeletingChannelListenerInjectable = getRequestChannelListenerInjectable({
channel: clearClusterAsDeletingChannel,
handler: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return (clusterId) => {
clustersThatAreBeingDeleted.delete(clusterId);
};
},
});
export default clearClusterAsDeletingChannelListenerInjectable;
@@ -0,0 +1,48 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
import clusterFramesInjectable from "../../../../common/cluster-frames.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import removePathInjectable from "../../../../common/fs/remove.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import { noop } from "../../../../common/utils";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import { deleteClusterChannel } from "../common/delete-channel";
const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectable({
channel: deleteClusterChannel,
handler: (di) => {
const emitAppEvent = di.inject(emitAppEventInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const clusterFrames = di.inject(clusterFramesInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
const deleteFile = di.inject(removePathInjectable);
return async (clusterId) => {
emitAppEvent({ name: "cluster", action: "remove" });
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
cluster.disconnect();
clusterFrames.delete(cluster.id);
// Remove from the cluster store as well, this should clear any old settings
clusterStore.clusters.delete(cluster.id);
// remove the local storage file
const localStorageFilePath = joinPaths(directoryForLensLocalStorage, `${cluster.id}.json`);
await deleteFile(localStorageFilePath).catch(noop);
};
},
});
export default deleteClusterChannelListenerInjectable;
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel";
const setClusterAsDeletingChannelHandlerInjectable = getRequestChannelListenerInjectable({
channel: setClusterAsDeletingChannel,
handler: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return (clusterId) => {
clustersThatAreBeingDeleted.add(clusterId);
};
},
});
export default setClusterAsDeletingChannelHandlerInjectable;
@@ -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 } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel";
export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
const requestClearClusterAsDeletingInjectable = getInjectable({
id: "request-clear-cluster-as-deleting",
instantiate: (di): RequestClearClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable);
return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId);
},
});
export default requestClearClusterAsDeletingInjectable;
@@ -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 } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import { deleteClusterChannel } from "../common/delete-channel";
export type RequestDeleteCluster = (clusterId: ClusterId) => Promise<void>;
const requestDeleteClusterInjectable = getInjectable({
id: "request-delete-cluster",
instantiate: (di): RequestDeleteCluster => {
const requestChannel = di.inject(requestFromChannelInjectable);
return (clusterId) => requestChannel(deleteClusterChannel, clusterId);
},
});
export default requestDeleteClusterInjectable;
@@ -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 } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel";
export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
const requestSetClusterAsDeletingInjectable = getInjectable({
id: "request-set-cluster-as-deleting",
instantiate: (di): RequestSetClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable);
return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId);
},
});
export default requestSetClusterAsDeletingInjectable;
@@ -0,0 +1,101 @@
/**
* 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 { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { TestExtensionRenderer } from "../../../renderer/components/test-utils/get-extension-fake";
import type { KubernetesCluster } from "../../../common/catalog-entities";
import React from "react";
describe("disable-cluster-pages-when-cluster-is-not-relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let rendererTestExtension: TestExtensionRenderer;
let isEnabledForClusterMock: AsyncFnMock<(cluster: KubernetesCluster) => Promise<boolean>>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
clusterPages: [{
components: {
Page: () => <div data-testid="some-test-page">Some page</div>,
},
}],
},
};
rendered = await builder.render();
builder.extensions.enable(testExtension);
rendererTestExtension =
builder.extensions.get("test-extension-id").applicationWindows.only;
});
describe("given not yet known if extension should be enabled for the cluster, when navigating", () => {
beforeEach(() => {
rendererTestExtension.navigate();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the page", () => {
const actual = rendered.queryByTestId("some-test-page");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension shouldn't be enabled for the cluster, when navigating", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
rendererTestExtension.navigate();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the page", () => {
const actual = rendered.queryByTestId("some-test-page");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension should be enabled for the cluster, when navigating", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
rendererTestExtension.navigate();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the page", () => {
const actual = rendered.getByTestId("some-test-page");
expect(actual).toBeInTheDocument();
});
});
});
@@ -0,0 +1,105 @@
/**
* 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 { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { KubernetesCluster } from "../../../common/catalog-entities";
import React from "react";
describe("disable sidebar items when cluster is not relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let isEnabledForClusterMock: AsyncFnMock<(cluster: KubernetesCluster) => Promise<boolean>>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
clusterPages: [{
components: {
Page: () => <div data-testid="some-test-page">Some page</div>,
},
}],
clusterPageMenus: [
{
id: "some-sidebar-item",
title: "Some sidebar item",
components: {
Icon: () => <div>Some icon</div>,
},
},
],
},
};
rendered = await builder.render();
builder.extensions.enable(testExtension);
});
describe("given not yet known if extension should be enabled for the cluster", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the sidebar item", () => {
const actual = rendered.queryByTestId(
"sidebar-item-some-extension-name-some-sidebar-item",
);
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension shouldn't be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the sidebar item", () => {
const actual = rendered.queryByTestId(
"sidebar-item-some-extension-name-some-sidebar-item",
);
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension should be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the sidebar item", () => {
const actual = rendered.getByTestId(
"sidebar-item-test-extension-some-sidebar-item",
);
expect(actual).toBeInTheDocument();
});
});
});
@@ -0,0 +1,68 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { IObservableValue } from "mobx";
import { observable, runInAction, computed } from "mobx";
import React from "react";
import type { TestExtensionRenderer } from "../../../renderer/components/test-utils/get-extension-fake";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
describe("reactively disable cluster pages", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let someObservable: IObservableValue<boolean>;
let testExtensionInstance: TestExtensionRenderer;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
someObservable = observable.box(false);
const testExtensionOptions = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
clusterPages: [{
components: {
Page: () => <div data-testid="some-test-page">Some page</div>,
},
enabled: computed(() => someObservable.get()),
}],
},
};
rendered = await builder.render();
builder.extensions.enable(testExtensionOptions);
testExtensionInstance =
builder.extensions.get("test-extension-id").applicationWindows.only;
});
it("when navigating to the page, does not show the page", () => {
testExtensionInstance.navigate();
const actual = rendered.queryByTestId("some-test-page");
expect(actual).not.toBeInTheDocument();
});
it("given page becomes enabled, when navigating to the page, shows the page", () => {
runInAction(() => {
someObservable.set(true);
});
testExtensionInstance.navigate();
const actual = rendered.queryByTestId("some-test-page");
expect(actual).toBeInTheDocument();
});
});
@@ -0,0 +1,134 @@
/**
* 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 { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { KubernetesCluster } from "../../../../common/catalog-entities";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import React from "react";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import type { KubeApi } from "../../../../common/k8s-api/kube-api";
import showDetailsInjectable from "../../../../renderer/components/kube-detail-params/show-details.injectable";
describe("disable kube object detail items when cluster is not relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let isEnabledForClusterMock: AsyncFnMock<
(cluster: KubernetesCluster) => Promise<boolean>
>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.afterWindowStart((windowDi) => {
const apiManager = windowDi.inject(apiManagerInjectable);
const api = {
apiBase: "/apis/some-api-version/some-kind",
} as Partial<KubeApi<KubeObject>> as KubeApi<KubeObject>;
const store = {
api,
loadFromPath: async () => getKubeObjectStub("some-kind", "some-api-version"),
} as Partial<KubeObjectStore<KubeObject>> as KubeObjectStore<KubeObject>;
apiManager.registerApi(api);
apiManager.registerStore(store);
});
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
kubeObjectDetailItems: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
components: {
Details: () => (
<div data-testid="some-kube-object-detail-item">
Some detail
</div>
),
},
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const showDetails = windowDi.inject(showDetailsInjectable);
showDetails("/apis/some-api-version/namespaces/some-namespace/some-kind/some-name");
builder.extensions.enable(testExtension);
});
describe("given not yet known if extension should be enabled for the cluster", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the kube object detail item", () => {
const actual = rendered.queryByTestId("some-kube-object-detail-item");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension shouldn't be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the kube object detail item", () => {
const actual = rendered.queryByTestId("some-kube-object-detail-item");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension should be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the kube object detail item", () => {
const actual = rendered.getByTestId("some-kube-object-detail-item");
expect(actual).toBeInTheDocument();
});
});
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "",
},
});
@@ -0,0 +1,122 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { IObservableValue } from "mobx";
import { runInAction, computed, observable } from "mobx";
import React from "react";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
import type { KubeApi } from "../../../../common/k8s-api/kube-api";
import showDetailsInjectable from "../../../../renderer/components/kube-detail-params/show-details.injectable";
import assert from "assert";
describe("reactively hide kube object detail item", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.afterWindowStart((windowDi) => {
const apiManager = windowDi.inject(apiManagerInjectable);
const api = {
apiBase: "/apis/some-api-version/some-kind",
} as Partial<KubeApi<KubeObject>> as KubeApi<KubeObject>;
const store = {
api,
loadFromPath: async () => getKubeObjectStub("some-kind", "some-api-version"),
} as Partial<KubeObjectStore<KubeObject>> as KubeObjectStore<KubeObject>;
apiManager.registerApi(api);
apiManager.registerStore(store);
});
someObservable = observable.box(false);
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
kubeObjectDetailItems: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
components: {
Details: () => (
<div data-testid="some-kube-object-detail-item">
Some detail
</div>
),
},
visible: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const showDetails = windowDi.inject(showDetailsInjectable);
showDetails("/apis/some-api-version/namespaces/some-namespace/some-kind/some-name");
builder.extensions.enable(testExtension);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the kube object detail item", () => {
const actual = rendered.queryByTestId("some-kube-object-detail-item");
expect(actual).not.toBeInTheDocument();
});
describe("when the item is shown", () => {
beforeEach(() => {
runInAction(() => {
someObservable.set(true);
});
const apiManager = builder.applicationWindow.only.di.inject(apiManagerInjectable);
assert(apiManager.getStore("/apis/some-api-version/some-kind"));
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the kube object detail item", () => {
const actual = rendered.queryByTestId("some-kube-object-detail-item");
expect(actual).toBeInTheDocument();
});
});
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "",
},
});
@@ -0,0 +1,158 @@
/**
* 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 { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { KubernetesCluster } from "../../../../common/catalog-entities";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getInjectable } from "@ogre-tools/injectable";
import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
import { computed, runInAction } from "mobx";
import React from "react";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectMenu } from "../../../../renderer/components/kube-object-menu";
describe("disable kube object menu items when cluster is not relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let isEnabledForClusterMock: AsyncFnMock<
(cluster: KubernetesCluster) => Promise<boolean>
>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable, testRouteComponentInjectable);
});
});
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
kubeObjectMenuItems: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
components: {
MenuItem: () => (
<div data-testid="some-test-id">Some menu item</div>
),
},
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const testRoute = windowDi.inject(testRouteInjectable);
navigateToRoute(testRoute);
builder.extensions.enable(testExtension);
});
describe("given not yet known if extension should be enabled for the cluster", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the kube object menu item", () => {
const actual = rendered.queryByTestId("some-test-id");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension shouldn't be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the kube object menu item", () => {
const actual = rendered.queryByTestId("some-test-id");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension should be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the kube object menu item", () => {
const actual = rendered.getByTestId("some-test-id");
expect(actual).toBeInTheDocument();
});
});
});
const testRouteInjectable = getInjectable({
id: "test-route",
instantiate: () => ({
path: "/test-route",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "test-route-component",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => (
<KubeObjectMenu
toolbar={true}
object={getKubeObjectStub("some-kind", "some-api-version")}
/>
),
}),
injectionToken: routeSpecificComponentInjectionToken,
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "",
},
});
@@ -0,0 +1,127 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getInjectable } from "@ogre-tools/injectable";
import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
import type { IObservableValue } from "mobx";
import { observable, runInAction, computed } from "mobx";
import React from "react";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectMenu } from "../../../../renderer/components/kube-object-menu";
describe("reactively hide kube object menu item", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable, testRouteComponentInjectable);
});
});
someObservable = observable.box(false);
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
kubeObjectMenuItems: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
components: {
MenuItem: () => (
<div data-testid="some-kube-object-menu-item">Some menu item</div>
),
},
visible: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const testRoute = windowDi.inject(testRouteInjectable);
navigateToRoute(testRoute);
builder.extensions.enable(testExtension);
});
it("does not show the kube object menu item", () => {
const actual = rendered.queryByTestId("some-kube-object-menu-item");
expect(actual).not.toBeInTheDocument();
});
it("given item should be shown, shows the kube object menu item", () => {
runInAction(() => {
someObservable.set(true);
});
const actual = rendered.queryByTestId("some-kube-object-menu-item");
expect(actual).toBeInTheDocument();
});
});
const testRouteInjectable = getInjectable({
id: "test-route",
instantiate: () => ({
path: "/test-route",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "test-route-component",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => (
<KubeObjectMenu
toolbar={true}
object={getKubeObjectStub("some-kind", "some-api-version")}
/>
),
}),
injectionToken: routeSpecificComponentInjectionToken,
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "",
},
});
@@ -0,0 +1,158 @@
/**
* 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 { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { KubernetesCluster } from "../../../../common/catalog-entities";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getInjectable } from "@ogre-tools/injectable";
import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
import { computed, runInAction } from "mobx";
import React from "react";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
import { KubeObjectStatusIcon } from "../../../../renderer/components/kube-object-status-icon/kube-object-status-icon";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectStatusLevel } from "../../../../common/k8s-api/kube-object-status";
describe("disable kube object statuses when cluster is not relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let isEnabledForClusterMock: AsyncFnMock<
(cluster: KubernetesCluster) => Promise<boolean>
>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable, testRouteComponentInjectable);
});
});
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
kubeObjectStatusTexts: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
resolve: () => ({
level: KubeObjectStatusLevel.CRITICAL,
text: "some-kube-object-status-text",
}),
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const testRoute = windowDi.inject(testRouteInjectable);
navigateToRoute(testRoute);
builder.extensions.enable(testExtension);
});
describe("given not yet known if extension should be enabled for the cluster", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the status", () => {
const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
expect(actual).toHaveLength(0);
});
});
describe("given extension shouldn't be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the status", () => {
const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
expect(actual).toHaveLength(0);
});
});
describe("given extension should be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the status", () => {
const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
expect(actual).toHaveLength(1);
});
});
});
const testRouteInjectable = getInjectable({
id: "test-route",
instantiate: () => ({
path: "/test-route",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "test-route-component",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => (
<KubeObjectStatusIcon
object={getKubeObjectStub("some-kind", "some-api-version")}
/>
),
}),
injectionToken: routeSpecificComponentInjectionToken,
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "/foo",
},
});
@@ -0,0 +1,130 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getInjectable } from "@ogre-tools/injectable";
import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
import type { IObservableValue } from "mobx";
import { observable, runInAction, computed } from "mobx";
import React from "react";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
import { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectStatusLevel } from "../../../../common/k8s-api/kube-object-status";
import { KubeObjectStatusIcon } from "../../../../renderer/components/kube-object-status-icon";
describe("reactively hide kube object status", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable, testRouteComponentInjectable);
});
});
someObservable = observable.box(false);
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
kubeObjectStatusTexts: [
{
kind: "some-kind",
apiVersions: ["some-api-version"],
resolve: () => ({
level: KubeObjectStatusLevel.CRITICAL,
text: "some-kube-object-status-text",
}),
visible: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const testRoute = windowDi.inject(testRouteInjectable);
navigateToRoute(testRoute);
builder.extensions.enable(testExtension);
});
it("does not show the kube object status", () => {
const actual = rendered.baseElement.querySelectorAll(
".KubeObjectStatusIcon",
);
expect(actual).toHaveLength(0);
});
it("given item should be shown, shows the kube object status", () => {
runInAction(() => {
someObservable.set(true);
});
const actual = rendered.baseElement.querySelectorAll(
".KubeObjectStatusIcon",
);
expect(actual).toHaveLength(1);
});
});
const testRouteInjectable = getInjectable({
id: "test-route",
instantiate: () => ({
path: "/test-route",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "test-route-component",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => (
<KubeObjectStatusIcon
object={getKubeObjectStub("some-kind", "some-api-version")}
/>
),
}),
injectionToken: routeSpecificComponentInjectionToken,
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "",
},
});
@@ -0,0 +1,358 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { KubeObjectStatusLevel } from "../../../common/k8s-api/kube-object-status";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import React from "react";
import { testUsingFakeTime } from "../../../common/test-utils/use-fake-time";
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { IAtom } from "mobx";
import { runInAction, createAtom, computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../common/front-end-routing/front-end-route-injection-token";
import { routeSpecificComponentInjectionToken } from "../../../renderer/routes/route-specific-component-injection-token";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { navigateToRouteInjectionToken } from "../../../common/front-end-routing/navigate-to-route-injection-token";
import type { RenderResult } from "@testing-library/react";
import { act } from "@testing-library/react";
import { observer } from "mobx-react";
import { kubeObjectStatusTextInjectionToken } from "../../../renderer/components/kube-object-status-icon/kube-object-status-text-injection-token";
import { KubeObjectStatusIcon } from "../../../renderer/components/kube-object-status-icon";
describe("show status for a kube object", () => {
let builder: ApplicationBuilder;
let infoStatusIsShown: boolean;
let warningStatusIsShown: boolean;
let criticalStatusIsShown: boolean;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
builder = getApplicationBuilder();
infoStatusIsShown = false;
const infoStatusInjectable = getInjectable({
id: "some-info-status",
injectionToken: kubeObjectStatusTextInjectionToken,
instantiate: () => ({
apiVersions: ["some-api-version"],
kind: "some-kind",
enabled: computed(() => true),
resolve: (resource) => infoStatusIsShown ? ({
level: KubeObjectStatusLevel.INFO,
text: `Some info status for ${resource.getName()}`,
timestamp: "2015-10-19T07:28:00Z",
}) : null,
}),
});
warningStatusIsShown = false;
const warningStatusInjectable = getInjectable({
id: "some-warning-status",
injectionToken: kubeObjectStatusTextInjectionToken,
instantiate: () => ({
apiVersions: ["some-api-version"],
kind: "some-kind",
enabled: computed(() => true),
resolve: (resource) => warningStatusIsShown ? ({
level: KubeObjectStatusLevel.WARNING,
text: `Some warning status for ${resource.getName()}`,
timestamp: "2015-10-19T07:28:00Z",
}) : null,
}),
});
criticalStatusIsShown = false;
const criticalStatusInjectable = getInjectable({
id: "some-critical-status",
injectionToken: kubeObjectStatusTextInjectionToken,
instantiate: () => ({
apiVersions: ["some-api-version"],
kind: "some-kind",
enabled: computed(() => true),
resolve: (resource) => {
return criticalStatusIsShown
? {
level: KubeObjectStatusLevel.CRITICAL,
text: `Some critical status for ${resource.getName()}`,
timestamp: "2015-10-19T07:28:00Z",
}
: null;
},
}),
});
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(
testRouteInjectable,
testRouteComponentInjectable,
infoStatusInjectable,
warningStatusInjectable,
criticalStatusInjectable,
someAtomInjectable,
);
});
});
builder.setEnvironmentToClusterFrame();
});
describe("given application starts and in test page", () => {
let windowDi: DiContainer;
let rendered: RenderResult;
let rerenderParent: () => void;
beforeEach(async () => {
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
const someAtom = windowDi.inject(someAtomInjectable);
rerenderParent = rerenderParentFor(someAtom);
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const testRoute = windowDi.inject(testRouteInjectable);
navigateToRoute(testRoute);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show any statuses yet", () => {
const status = rendered.queryByTestId(
"kube-object-status-icon-for-some-uid",
);
expect(status).not.toBeInTheDocument();
});
describe("when status for irrelevant kube object kind emerges", () => {
beforeEach(() => {
runInAction(() => {
windowDi.register(statusForIrrelevantKubeObjectKindInjectable);
});
rerenderParent();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show any statuses", () => {
const status = rendered.queryByTestId(
"kube-object-status-icon-for-some-uid",
);
expect(status).not.toBeInTheDocument();
});
});
describe("when status for irrelevant kube object api version emerges", () => {
beforeEach(() => {
runInAction(() => {
windowDi.register(statusForIrrelevantKubeObjectApiVersionInjectable);
});
rerenderParent();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show any statuses", () => {
const status = rendered.queryByTestId(
"kube-object-status-icon-for-some-uid",
);
expect(status).not.toBeInTheDocument();
});
});
describe("when info status emerges", () => {
beforeEach(() => {
infoStatusIsShown = true;
rerenderParent();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows status", () => {
const status = rendered.getByTestId(
"kube-object-status-icon-for-some-uid",
);
expect(status).toBeInTheDocument();
});
it("show info status", () => {
const tooltipContent = rendered.getByTestId(
"tooltip-content-for-kube-object-status-icon-for-some-uid",
);
expect(tooltipContent).toHaveTextContent(
"Some info status for some-name",
);
});
});
describe("when warning status emerges", () => {
beforeEach(() => {
warningStatusIsShown = true;
rerenderParent();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("show warning status", () => {
const tooltipContent = rendered.getByTestId(
"tooltip-content-for-kube-object-status-icon-for-some-uid",
);
expect(tooltipContent).toHaveTextContent(
"Some warning status for some-name",
);
});
});
describe("when critical status emerges", () => {
beforeEach(() => {
criticalStatusIsShown = true;
rerenderParent();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("show critical status", () => {
const tooltipContent = rendered.getByTestId(
"tooltip-content-for-kube-object-status-icon-for-some-uid",
);
expect(tooltipContent).toHaveTextContent(
"Some critical status for some-name",
);
});
});
});
});
const testRouteInjectable = getInjectable({
id: "test-route",
instantiate: () => ({
path: "/test-route",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const rerenderParentFor = (atom: IAtom) => () => {
act(() => {
atom.reportChanged();
});
};
const TestComponent = observer(({ someAtom }: { someAtom: IAtom }) => {
void someAtom.reportObserved();
return (
<KubeObjectStatusIcon
object={getKubeObjectStub("some-kind", "some-api-version")}
/>
);
});
const testRouteComponentInjectable = getInjectable({
id: "test-route-component",
instantiate: (di) => {
const someAtom = di.inject(someAtomInjectable);
return {
route: di.inject(testRouteInjectable),
Component: () => <TestComponent someAtom={someAtom} />,
};
},
injectionToken: routeSpecificComponentInjectionToken,
});
const someAtomInjectable = getInjectable({
id: "some-atom",
instantiate: () => createAtom("some-atom"),
});
const getKubeObjectStub = (kind: string, apiVersion: string) =>
KubeObject.create({
apiVersion,
kind,
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
namespace: "some-namespace",
selfLink: "/foo",
},
});
const statusForIrrelevantKubeObjectKindInjectable = getInjectable({
id: "status-for-irrelevant-kube-object-kind",
injectionToken: kubeObjectStatusTextInjectionToken,
instantiate: () => ({
apiVersions: ["some-api-version"],
kind: "some-other-kind",
enabled: computed(() => true),
resolve: () => ({
level: KubeObjectStatusLevel.INFO,
text: "irrelevant",
timestamp: "2015-10-19T07:28:00Z",
}),
}),
});
const statusForIrrelevantKubeObjectApiVersionInjectable = getInjectable({
id: "status-for-irrelevant-kube-object-api-version",
injectionToken: kubeObjectStatusTextInjectionToken,
instantiate: () => ({
apiVersions: ["some-other-api-version"],
kind: "some-kind",
enabled: computed(() => true),
resolve: () => ({
level: KubeObjectStatusLevel.INFO,
text: "irrelevant",
timestamp: "2015-10-19T07:28:00Z",
}),
}),
});
@@ -0,0 +1,78 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { act } from "@testing-library/react";
import type { IObservableValue } from "mobx";
import { computed, observable, runInAction } from "mobx";
import React from "react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("legacy extension adding cluster frame components", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
});
describe("given custom components for cluster view available", () => {
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
someObservable = observable.box(false);
const testExtension = {
id: "some-extension-id",
name: "some-extension-name",
rendererOptions: {
clusterFrameComponents: [
{
id: "test-modal-id",
Component: () => <div data-testid="test-modal">test modal</div>,
shouldRender: computed(() => true),
},
{
id: "dialog-with-observable-visibility-id",
Component: () => <div data-testid="dialog-with-observable-visibility">dialog contents</div>,
shouldRender: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
builder.extensions.enable(testExtension);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("renders provided component html", () => {
const modal = rendered.getByTestId("test-modal");
expect(modal).toBeInTheDocument();
});
it("doesn't render component which should be invisible", () => {
const dialog = rendered.queryByTestId("dialog-with-observable-visibility");
expect(dialog).not.toBeInTheDocument();
});
it("when injectable component becomes visible, shows it", () => {
runInAction(() => {
act(() => someObservable.set(true));
});
const dialog = rendered.getByTestId("dialog-with-observable-visibility");
expect(dialog).toBeInTheDocument();
});
});
});
@@ -0,0 +1,930 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import navigateToNamespacesInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable";
import createEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-tab.injectable";
import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import showSuccessNotificationInjectable from "../../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../../renderer/components/notifications/show-error-notification.injectable";
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
describe("cluster/namespaces - edit namespace from new tab", () => {
let builder: ApplicationBuilder;
let callForResourceMock: AsyncFnMock<CallForResource>;
let callForPatchResourceMock: AsyncFnMock<CallForPatchResource>;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
windowDi.override(
directoryForLensLocalStorageInjectable,
() => "/some-directory-for-lens-local-storage",
);
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
showSuccessNotificationMock = jest.fn();
windowDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
showErrorNotificationMock = jest.fn();
windowDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
windowDi.override(getRandomIdForEditResourceTabInjectable, () =>
jest
.fn(() => "some-irrelevant-random-id")
.mockReturnValueOnce("some-first-tab-id")
.mockReturnValueOnce("some-second-tab-id"),
);
callForResourceMock = asyncFn();
windowDi.override(callForResourceInjectable, () => callForResourceMock);
callForPatchResourceMock = asyncFn();
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
});
builder.allowKubeResource({
apiName: "namespaces",
group: "",
});
});
describe("when navigating to namespaces", () => {
let rendered: RenderResult;
let windowDi: DiContainer;
beforeEach(async () => {
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
const navigateToNamespaces = windowDi.inject(navigateToNamespacesInjectable);
const dockStore = windowDi.inject(dockStoreInjectable);
navigateToNamespaces();
// TODO: Make TerminalWindow unit testable to allow realistic behaviour
dockStore.closeTab("terminal");
});
// TODO: Implement skipped tests when loading of resources can be tested
xit("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
xit("calls for namespaces", () => {
});
xit("shows spinner", () => {
});
describe("when namespaces resolve", () => {
beforeEach(() => {
});
xit("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
xit("does not show spinner anymore", () => {
});
describe("when clicking the context menu for a namespace", () => {
beforeEach(() => {
});
xit("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
xit("does not show edit resource tab yet", () => {
});
describe("when clicking to edit namespace", () => {
beforeEach(() => {
// TODO: Make implementation match the description (tests above)
const namespaceStub = new Namespace(someNamespaceDataStub);
const createEditResourceTab = windowDi.inject(createEditResourceTabInjectable);
createEditResourceTab(namespaceStub);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows spinner in the dock tab", () => {
expect(
rendered.getByTestId("edit-resource-tab-spinner"),
).toBeInTheDocument();
});
it("calls for namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
);
});
describe("when call for namespace resolves with namespace", () => {
let someNamespace: Namespace;
beforeEach(async () => {
someNamespace = new Namespace({
apiVersion: "some-api-version",
kind: "Namespace",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-uid",
somePropertyToBeRemoved: "some-value",
somePropertyToBeChanged: "some-old-value",
},
});
await callForResourceMock.resolve({
callWasSuccessful: true,
response: someNamespace,
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show spinner anymore", () => {
expect(
rendered.queryByTestId("edit-resource-tab-spinner"),
).not.toBeInTheDocument();
});
it("has the configuration in editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe(`apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
`);
});
describe("given no changes in the configuration, when selecting to save", () => {
beforeEach(() => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(saveButton);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("calls for save with empty values", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[],
);
});
it("shows spinner", () => {
expect(
rendered.getByTestId("saving-edit-resource-from-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("save button is disabled", () => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).toHaveAttribute("disabled");
});
it("save and close button is disabled", () => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).toHaveAttribute("disabled");
});
describe("when saving resolves with success", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: { name: "some-name", kind: "Namespace" },
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show spinner anymore", () => {
expect(
rendered.queryByTestId("saving-edit-resource-from-tab-for-some-first-tab-id"),
).not.toBeInTheDocument();
});
it("save button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
it("save and close button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
it("shows success notification", () => {
expect(showSuccessNotificationMock).toHaveBeenCalled();
});
it("does not show error notification", () => {
expect(showErrorNotificationMock).not.toHaveBeenCalled();
});
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
});
describe("when saving resolves with failure", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: false,
error: "some-error",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show spinner anymore", () => {
expect(
rendered.queryByTestId("edit-resource-tab-spinner"),
).not.toBeInTheDocument();
});
it("save button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
it("save and close button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
it("does not show success notification", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalled();
});
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
});
});
describe("when selecting to save and close", () => {
beforeEach(() => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(saveButton);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not close the tab yet", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
describe("when saving resolves with success", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: { name: "some-name", kind: "Namespace" },
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("closes the dock tab", () => {
expect(
rendered.queryByTestId("dock-tab-for-some-first-tab-id"),
).not.toBeInTheDocument();
});
});
describe("when saving resolves with failure", () => {
beforeEach(async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: false,
error: "Some error",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
// TODO: Not doable at the moment because info panel controls closing of the tab
xit("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
});
});
describe("when selecting to cancel", () => {
beforeEach(() => {
const cancelButton = rendered.getByTestId(
"cancel-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(cancelButton);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not have dock tab anymore", () => {
expect(
rendered.queryByTestId("dock-tab-for-some-first-tab-id"),
).not.toBeInTheDocument();
});
});
describe("given change in configuration", () => {
beforeEach(() => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLInputElement;
fireEvent.change(input, {
target: {
value: `apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
`,
},
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("has the changed configuration in editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe(`apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
`);
});
it("stores the changed configuration", async () => {
const readJsonFile = windowDi.inject(
readJsonFileInjectable,
);
const actual = (await readJsonFile(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
)) as any;
expect(
actual.edit_resource_store["some-first-tab-id"],
).toEqual({
resource: "/apis/some-api-version/namespaces/some-uid",
firstDraft: `apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
`,
draft: `apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
`,
});
});
describe("when selecting to save", () => {
beforeEach(() => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(saveButton);
});
it("calls for save with changed configuration", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[
{
op: "remove",
path: "/metadata/somePropertyToBeRemoved",
},
{
op: "add",
path: "/metadata/someAddedProperty",
value: "some-new-value",
},
{
op: "replace",
path: "/metadata/somePropertyToBeChanged",
value: "some-changed-value",
},
],
);
});
it("given save resolves and another change in configuration, when saving, calls for save with changed configuration", async () => {
await callForPatchResourceMock.resolve({
callWasSuccessful: true,
response: {
name: "some-name",
kind: "Namespace",
},
});
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLInputElement;
fireEvent.change(input, {
target: {
value: `apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value
someOtherAddedProperty: some-other-new-value
`,
},
});
callForPatchResourceMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[
{
op: "add",
path: "/metadata/someOtherAddedProperty",
value: "some-other-new-value",
},
],
);
});
});
});
describe("given invalid change in configuration", () => {
beforeEach(() => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLInputElement;
fireEvent.change(input, {
target: {
value: "@some-invalid-configuration@",
},
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("has the changed configuration in editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe(`@some-invalid-configuration@`);
});
it("save button is disabled", () => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).toHaveAttribute("disabled");
});
it("save and close button is disabled", () => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).toHaveAttribute("disabled");
});
describe("when valid change in configuration", () => {
beforeEach(() => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLInputElement;
fireEvent.change(input, {
target: {
value: `apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
`,
},
});
});
it("save button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
it("save and close button is enabled", () => {
const saveButton = rendered.getByTestId(
"save-and-close-edit-resource-from-tab-for-some-first-tab-id",
);
expect(saveButton).not.toHaveAttribute("disabled");
});
});
});
describe("given clicking the context menu for second namespace, when clicking to edit namespace", () => {
beforeEach(() => {
callForResourceMock.mockClear();
// TODO: Make implementation match the description
const namespaceStub = new Namespace(someOtherNamespaceDataStub);
const createEditResourceTab = windowDi.inject(createEditResourceTabInjectable);
createEditResourceTab(namespaceStub);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dock tab for editing second namespace", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-second-tab-id"),
).toBeInTheDocument();
});
it("still has dock tab for first namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows spinner in the dock tab", () => {
expect(
rendered.getByTestId("edit-resource-tab-spinner"),
).toBeInTheDocument();
});
it("calls for second namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-other-uid",
);
});
describe("when second namespace resolves", () => {
let someOtherNamespace: Namespace;
beforeEach(async () => {
someOtherNamespace = new Namespace({
apiVersion: "some-api-version",
kind: "Namespace",
metadata: {
uid: "some-other-uid",
name: "some-other-name",
resourceVersion: "some-resource-version",
selfLink:
"/apis/some-api-version/namespaces/some-other-uid",
},
});
await callForResourceMock.resolve({
callWasSuccessful: true,
response: someOtherNamespace,
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("has the configuration in editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-second-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe(`apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-other-uid
name: some-other-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-other-uid
`);
});
it("when selecting to save, calls for save of second namespace", () => {
callForPatchResourceMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-second-tab-id",
);
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someOtherNamespace,
[],
);
});
describe("when clicking dock tab for the first namespace", () => {
beforeEach(() => {
callForResourceMock.mockClear();
const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id");
fireEvent.click(tab);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dock tab for editing first namespace", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("still has dock tab for second namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-second-tab-id"),
).toBeInTheDocument();
});
it("does not show spinner in the dock tab", () => {
expect(
rendered.queryByTestId("edit-resource-tab-spinner"),
).not.toBeInTheDocument();
});
it("does not call for namespace", () => {
expect(callForResourceMock).not.toHaveBeenCalledWith("/apis/some-api-version/namespaces/some-uid");
});
it("has configuration in the editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe(`apiVersion: some-api-version
kind: Namespace
metadata:
uid: some-uid
name: some-name
resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value
`);
});
it("when selecting to save, calls for save of first namespace", () => {
callForPatchResourceMock.mockClear();
const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id",
);
fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith(
someNamespace,
[],
);
});
});
});
});
});
describe("when call for namespace resolves without namespace", () => {
beforeEach(async () => {
await callForResourceMock.resolve({
callWasSuccessful: true,
response: undefined,
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows error message", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
).toHaveTextContent("Resource not found");
});
it("does not show error notification", () => {
expect(showErrorNotificationMock).not.toHaveBeenCalled();
});
});
describe("when call for namespace resolves with failure", () => {
beforeEach(async () => {
await callForResourceMock.resolve({
callWasSuccessful: false,
error: "some-error",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows error message", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
).toHaveTextContent("Resource not found");
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalled();
});
});
});
});
});
});
});
const someNamespaceDataStub = {
apiVersion: "some-api-version",
kind: "Namespace",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-uid",
},
};
const someOtherNamespaceDataStub = {
apiVersion: "some-api-version",
kind: "Namespace",
metadata: {
uid: "some-other-uid",
name: "some-other-name",
resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-other-uid",
},
};
@@ -0,0 +1,143 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { act } from "@testing-library/react";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
import { TabKind } from "../../../renderer/components/dock/dock/store";
import { Namespace } from "../../../common/k8s-api/endpoints";
describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
let builder: ApplicationBuilder;
let callForNamespaceMock: AsyncFnMock<CallForResource>;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
callForNamespaceMock = asyncFn();
builder.beforeWindowStart((windowDi) => {
windowDi.override(
directoryForLensLocalStorageInjectable,
() => "/some-directory-for-lens-local-storage",
);
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
});
builder.allowKubeResource({
apiName: "namespaces",
group: "",
});
});
describe("given tab was previously opened, when application is started", () => {
let rendered: RenderResult;
beforeEach(async () => {
builder.beforeWindowStart(async (windowDi) => {
const writeJsonFile = windowDi.inject(writeJsonFileInjectable);
await writeJsonFile(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
dock: {
height: 300,
tabs: [
{
id: "some-first-tab-id",
kind: TabKind.EDIT_RESOURCE,
title: "Namespace: some-namespace",
pinned: false,
},
],
isOpen: true,
},
edit_resource_store: {
"some-first-tab-id": {
resource: "/apis/some-api-version/namespaces/some-uid",
draft: "some-saved-configuration",
},
},
},
);
});
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows spinner in the dock tab", () => {
expect(
rendered.getByTestId("edit-resource-tab-spinner"),
).toBeInTheDocument();
});
it("calls for namespace", () => {
expect(callForNamespaceMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid",
);
});
describe("when call for namespace resolves with namespace", () => {
let someNamespace: Namespace;
beforeEach(async () => {
someNamespace = new Namespace({
apiVersion: "some-api-version",
kind: "Namespace",
metadata: {
uid: "some-uid",
name: "some-name",
resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-uid",
somePropertyToBeRemoved: "some-value",
somePropertyToBeChanged: "some-old-value",
},
});
// TODO: Figure out why act is needed here. In CI it works without it.
await act(async () => {
await callForNamespaceMock.resolve({
callWasSuccessful: true,
response: someNamespace,
});
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("has the saved configuration in editor", () => {
const input = rendered.getByTestId(
"monaco-editor-for-some-first-tab-id",
) as HTMLTextAreaElement;
expect(input.value).toBe("some-saved-configuration");
});
});
});
});
@@ -0,0 +1,131 @@
/**
* 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 { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import type { SidebarItemRegistration } from "../../renderer/components/layout/sidebar-items.injectable";
import { sidebarItemsInjectionToken } from "../../renderer/components/layout/sidebar-items.injectable";
import { computed, runInAction } from "mobx";
import { noop } from "lodash/fp";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("cluster - order of sidebar items", () => {
let rendered: RenderResult;
let builder: ApplicationBuilder;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testSidebarItemsInjectable);
});
});
});
describe("when rendered", () => {
beforeEach(async () => {
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("has parent items in order", () => {
const actual = rendered
.queryAllByTestId(/^sidebar-item-(some-parent-id|some-other-parent-id|some-another-parent-id)/)
.map(elem => elem.dataset.testid);
expect(actual).toEqual([
"sidebar-item-some-parent-id",
"sidebar-item-some-another-parent-id",
"sidebar-item-some-other-parent-id",
]);
});
describe("when parent is expanded", () => {
beforeEach(() => {
const parentLink = rendered.getByTestId(
"sidebar-item-link-for-some-parent-id",
);
fireEvent.click(parentLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("has child items in order", () => {
const actual = rendered
.queryAllByTestId(/^sidebar-item-*/)
.filter((element) => element.dataset.parentIdTest === "some-parent-id")
.map(elem => elem.dataset.testid);
expect(actual).toEqual([
"sidebar-item-some-child-id",
"sidebar-item-some-another-child-id",
"sidebar-item-some-other-child-id",
]);
});
});
});
});
const testSidebarItemsInjectable = getInjectable({
id: "some-sidebar-item-injectable",
instantiate: () =>
computed((): SidebarItemRegistration[] => [
{
id: "some-parent-id",
parentId: null,
title: "Some parent",
onClick: noop,
orderNumber: 42,
},
{
id: "some-other-parent-id",
parentId: null,
title: "Some other parent",
onClick: noop,
orderNumber: 126,
},
{
id: "some-another-parent-id",
parentId: null,
title: "Some another parent",
onClick: noop,
orderNumber: 84,
},
{
id: "some-child-id",
parentId: "some-parent-id",
title: "Some child",
onClick: noop,
orderNumber: 168,
},
{
id: "some-other-child-id",
parentId: "some-parent-id",
title: "Some other child",
onClick: noop,
orderNumber: 252,
},
{
id: "some-another-child-id",
parentId: "some-parent-id",
title: "Some another child",
onClick: noop,
orderNumber: 210,
},
]),
injectionToken: sidebarItemsInjectionToken,
});
@@ -0,0 +1,348 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import type { SidebarItemRegistration } from "../../renderer/components/layout/sidebar-items.injectable";
import { sidebarItemsInjectionToken } from "../../renderer/components/layout/sidebar-items.injectable";
import { computed, runInAction } from "mobx";
import { noop } from "lodash/fp";
import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable";
import { frontEndRouteInjectionToken } from "../../common/front-end-routing/front-end-route-injection-token";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
describe("cluster - sidebar and tab navigation for core", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
windowDi.override(storageSaveDelayInjectable, () => 250);
});
});
describe("given core registrations", () => {
beforeEach(() => {
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable);
windowDi.register(testRouteComponentInjectable);
windowDi.register(testSidebarItemsInjectable);
});
});
});
describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => {
beforeEach(async () => {
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const route = windowDi.inject(testRouteInjectable);
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
navigateToRoute(route);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent is highlighted", () => {
const parent = rendered.queryByTestId("sidebar-item-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("true");
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).toBeNull();
});
it("child page is shown", () => {
expect(rendered.getByTestId("some-child-page")).not.toBeNull();
});
});
describe("given state for expanded sidebar items already exists, when rendered", () => {
beforeEach(async () => {
builder.beforeWindowStart(async (windowDi) => {
const writeJsonFile = windowDi.inject(writeJsonFileInjectable);
await writeJsonFile(
"/some-directory-for-app-data/some-product-name/lens-local-storage/some-cluster-id.json",
{
sidebar: {
expanded: { "some-parent-id": true },
width: 200,
},
},
);
});
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.queryByTestId("sidebar-item-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).not.toBeNull();
});
});
describe("given state for expanded unknown sidebar items already exists, when rendered", () => {
beforeEach(async () => {
builder.beforeWindowStart(async (windowDi) => {
const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable);
await writeJsonFileFake(
"/some-directory-for-app-data/some-product-name/lens-local-storage/some-hosted-cluster-id.json",
{
sidebar: {
expanded: { "some-unknown-parent-id": true },
width: 200,
},
},
);
});
rendered = await builder.render();
});
it("renders without errors", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).toBeNull();
});
});
describe("given empty state for expanded sidebar items already exists, when rendered", () => {
beforeEach(async () => {
builder.beforeWindowStart(async (windowDi) => {
const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable);
await writeJsonFileFake(
"/some-directory-for-app-data/some-product-name/lens-local-storage/some-hosted-cluster-id.json",
{
someThingButSidebar: {},
},
);
});
rendered = await builder.render();
});
it("renders without errors", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).toBeNull();
});
});
describe("given no initially persisted state for sidebar items, when rendered", () => {
let windowDi: DiContainer;
beforeEach(async () => {
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.queryByTestId("sidebar-item-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).toBeNull();
});
describe("when a parent sidebar item is expanded", () => {
beforeEach(() => {
const parentLink = rendered.getByTestId(
"sidebar-item-link-for-some-parent-id",
);
fireEvent.click(parentLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.queryByTestId("sidebar-item-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child).not.toBeNull();
});
describe("when a child of the parent is selected", () => {
beforeEach(() => {
const childLink = rendered.getByTestId(
"sidebar-item-link-for-some-child-id",
);
fireEvent.click(childLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent is highlighted", () => {
const parent = rendered.queryByTestId("sidebar-item-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("true");
});
it("child is highlighted", () => {
const child = rendered.queryByTestId("sidebar-item-some-child-id");
expect(child?.dataset.isActiveTest).toBe("true");
});
it("child page is shown", () => {
expect(rendered.getByTestId("some-child-page")).not.toBeNull();
});
it("when not enough time passes, does not store state for expanded sidebar items to file system yet", async () => {
advanceFakeTime(250 - 1);
const pathExistsFake = windowDi.inject(pathExistsInjectable);
const actual = await pathExistsFake(
"/some-directory-for-app-data/some-product-name/lens-local-storage/some-hosted-cluster-id.json",
);
expect(actual).toBe(false);
});
it("when enough time passes, stores state for expanded sidebar items to file system", async () => {
advanceFakeTime(250);
const readJsonFileFake = windowDi.inject(readJsonFileInjectable);
await flushPromises();
const actual = await readJsonFileFake(
"/some-directory-for-app-data/some-product-name/lens-local-storage/some-cluster-id.json",
);
expect(actual).toEqual({
sidebar: {
expanded: { "some-parent-id": true },
width: 200,
},
});
});
});
});
});
});
});
const testSidebarItemsInjectable = getInjectable({
id: "some-sidebar-items-injectable",
instantiate: (di) => {
const route = di.inject(testRouteInjectable);
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return computed((): SidebarItemRegistration[] => [
{
id: "some-parent-id",
parentId: null,
title: "Some parent",
onClick: noop,
getIcon: () => <div data-testid="some-icon-for-parent" />,
orderNumber: 42,
},
{
id: "some-child-id",
parentId: "some-parent-id",
title: "Some child",
onClick: () => navigateToRoute(route),
isActive: routeIsActive,
orderNumber: 42,
},
]);
},
injectionToken: sidebarItemsInjectionToken,
});
const testRouteInjectable = getInjectable({
id: "some-route-injectable-id",
instantiate: () => ({
path: "/some-child-page",
clusterFrame: true,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "some-child-page-route-component-injectable",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => <div data-testid="some-child-page" />,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
@@ -0,0 +1,456 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import directoryForLensLocalStorageInjectable from "../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import routesInjectable from "../../renderer/routes/routes.injectable";
import { matches } from "lodash/fp";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import assert from "assert";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import type { IObservableValue } from "mobx";
import { runInAction, computed, observable } from "mobx";
import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
describe("cluster - sidebar and tab navigation for extensions", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
applicationBuilder = getApplicationBuilder();
applicationBuilder.setEnvironmentToClusterFrame();
applicationBuilder.beforeWindowStart((windowDi) => {
windowDi.override(storageSaveDelayInjectable, () => 250);
windowDi.override(
directoryForLensLocalStorageInjectable,
() => "/some-directory-for-lens-local-storage",
);
});
});
describe("given extension with cluster pages and cluster page menus", () => {
let someObservable: IObservableValue<boolean>;
beforeEach(() => {
someObservable = observable.box(false);
const testExtension = {
id: "some-extension-id",
name: "some-extension-name",
rendererOptions: {
clusterPages: [
{
components: {
Page: () => {
throw new Error("should never come here");
},
},
},
{
id: "some-child-page-id",
components: {
Page: () => <div data-testid="some-child-page">Some child page</div>,
},
},
{
id: "some-other-child-page-id",
components: {
Page: () => (
<div data-testid="some-other-child-page">Some other child page</div>
),
},
},
],
clusterPageMenus: [
{
id: "some-parent-id",
title: "Parent",
components: {
Icon: () => <div>Some icon</div>,
},
},
{
id: "some-child-id",
target: { pageId: "some-child-page-id" },
parentId: "some-parent-id",
title: "Child 1",
components: {
Icon: null as never,
},
},
{
id: "some-other-child-id",
target: { pageId: "some-other-child-page-id" },
parentId: "some-parent-id",
title: "Child 2",
components: {
Icon: null as never,
},
},
{
id: "some-menu-with-controlled-visibility",
title: "Some menu with controlled visibility",
visible: computed(() => someObservable.get()),
components: {
Icon: () => <div>Some icon</div>,
},
},
],
},
};
applicationBuilder.extensions.enable(testExtension);
});
describe("given no state for expanded sidebar items exists, and navigated to child sidebar item, when rendered", () => {
beforeEach(async () => {
rendered = await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
const navigateToRoute = windowDi.inject(navigateToRouteInjectionToken);
const route = windowDi
.inject(routesInjectable)
.get()
.find(
matches({
path: "/extension/some-extension-name/some-child-page-id",
}),
);
assert(route);
navigateToRoute(route);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent is highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("true");
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).toBeNull();
});
it("child page is shown", () => {
expect(rendered.getByTestId("some-child-page")).not.toBeNull();
});
});
describe("given state for expanded sidebar items already exists, when rendered", () => {
beforeEach(async () => {
applicationBuilder.beforeWindowStart(async (windowDi) => {
const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable);
await writeJsonFileFake(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
sidebar: {
expanded: { "some-extension-name-some-parent-id": true },
width: 200,
},
},
);
});
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).not.toBeNull();
});
});
describe("given state for expanded unknown sidebar items already exists, when rendered", () => {
beforeEach(async () => {
applicationBuilder.beforeWindowStart(async (windowDi) => {
const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable);
await writeJsonFileFake(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
sidebar: {
expanded: { "some-extension-name-some-unknown-parent-id": true },
width: 200,
},
},
);
});
rendered = await applicationBuilder.render();
});
it("renders without errors", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).toBeNull();
});
});
describe("given empty state for expanded sidebar items already exists, when rendered", () => {
beforeEach(async () => {
applicationBuilder.beforeWindowStart(async (windowDi) => {
const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable);
await writeJsonFileFake(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
{
someThingButSidebar: {},
},
);
});
rendered = await applicationBuilder.render();
});
it("renders without errors", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).toBeNull();
});
});
describe("given no initially persisted state for sidebar items, when rendered", () => {
let windowDi: DiContainer;
beforeEach(async () => {
rendered = await applicationBuilder.render();
windowDi = applicationBuilder.applicationWindow.only.di;
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is not expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).toBeNull();
});
it("does not show the sidebar item that should be hidden", () => {
const child = rendered.queryByTestId(
"sidebar-item-some-extension-name-some-menu-with-controlled-visibility",
);
expect(child).not.toBeInTheDocument();
});
it("when sidebar item becomes visible, shows the sidebar item", () => {
runInAction(() => {
someObservable.set(true);
});
const child = rendered.queryByTestId(
"sidebar-item-some-extension-name-some-menu-with-controlled-visibility",
);
expect(child).toBeInTheDocument();
});
describe("when a parent sidebar item is expanded", () => {
beforeEach(() => {
const parentLink = rendered.getByTestId(
"sidebar-item-link-for-some-extension-name-some-parent-id",
);
fireEvent.click(parentLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent sidebar item is not highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("false");
});
it("parent sidebar item is expanded", () => {
const child = rendered.queryByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child).not.toBeNull();
});
describe("when a child of the parent is selected", () => {
beforeEach(() => {
const childLink = rendered.getByTestId(
"sidebar-item-link-for-some-extension-name-some-child-id",
);
fireEvent.click(childLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("parent is highlighted", () => {
const parent = rendered.getByTestId("sidebar-item-some-extension-name-some-parent-id");
expect(parent?.dataset.isActiveTest).toBe("true");
});
it("child is highlighted", () => {
const child = rendered.getByTestId("sidebar-item-some-extension-name-some-child-id");
expect(child?.dataset.isActiveTest).toBe("true");
});
it("child page is shown", () => {
expect(rendered.getByTestId("some-child-page")).not.toBeNull();
});
it("renders tabs", () => {
expect(rendered.getByTestId("tab-layout")).not.toBeNull();
});
it("tab for child page is active", () => {
const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-name-some-child-id",
);
expect(tabLink.dataset.isActiveTest).toBe("true");
});
it("tab for sibling page is not active", () => {
const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-name-some-other-child-id",
);
expect(tabLink.dataset.isActiveTest).toBe("false");
});
it("when not enough time passes, does not store state for expanded sidebar items to file system yet", async () => {
advanceFakeTime(250 - 1);
const pathExistsFake = windowDi.inject(pathExistsInjectable);
const actual = await pathExistsFake(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
);
expect(actual).toBe(false);
});
it("when enough time passes, stores state for expanded sidebar items to file system", async () => {
advanceFakeTime(250);
const readJsonFileFake = windowDi.inject(readJsonFileInjectable);
await flushPromises(); // Needed because of several async calls
const actual = await readJsonFileFake(
"/some-directory-for-lens-local-storage/some-cluster-id.json",
);
expect(actual).toEqual({
sidebar: {
expanded: { "some-extension-name-some-parent-id": true },
width: 200,
},
});
});
describe("when selecting sibling tab", () => {
beforeEach(() => {
const childTabLink = rendered.getByTestId(
"tab-link-for-some-extension-name-some-other-child-id",
);
fireEvent.click(childTabLink);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("sibling child page is shown", () => {
expect(
rendered.getByTestId("some-other-child-page"),
).not.toBeNull();
});
it("tab for sibling page is active", () => {
const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-name-some-other-child-id",
);
expect(tabLink.dataset.isActiveTest).toBe("true");
});
it("tab for previous page is not active", () => {
const tabLink = rendered.getByTestId(
"tab-link-for-some-extension-name-some-child-id",
);
expect(tabLink.dataset.isActiveTest).toBe("false");
});
});
});
});
});
});
});
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId, ClusterState } from "../../../../common/cluster-types";
import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
export interface ClusterStateSync {
clusterId: ClusterId;
state: ClusterState;
}
export const clusterStateSyncChannel: MessageChannel<ClusterStateSync> = {
id: "cluster-state-sync",
};
export const initialClusterStatesChannel: RequestChannel<void, ClusterStateSync[]> = {
id: "initial-cluster-state-sync",
};
@@ -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 } from "@ogre-tools/injectable";
import type { MessageChannelHandler } from "../../../../common/utils/channel/message-channel-listener-injection-token";
import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token";
import { clusterStateSyncChannel } from "../common/channels";
export type EmitClusterStateUpdate = MessageChannelHandler<typeof clusterStateSyncChannel>;
const emitClusterStateUpdateInjectable = getInjectable({
id: "emit-cluster-state-update",
instantiate: (di): EmitClusterStateUpdate => {
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
return (message) => sendMessageToChannel(clusterStateSyncChannel, message);
},
});
export default emitClusterStateUpdateInjectable;
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import { initialClusterStatesChannel } from "../common/channels";
const handleInitialClusterStateSyncInjectable = getRequestChannelListenerInjectable({
channel: initialClusterStatesChannel,
handler: (di) => {
const clusterStore = di.inject(clusterStoreInjectable);
return () => clusterStore.clustersList.map(cluster => ({
clusterId: cluster.id,
state: cluster.getState(),
}));
},
});
export default handleInitialClusterStateSyncInjectable;
@@ -0,0 +1,44 @@
/**
* 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 { isEqual } from "lodash";
import { autorun } from "mobx";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import type { ClusterId, ClusterState } from "../../../../common/cluster-types";
import { beforeApplicationIsLoadingInjectionToken } from "../../../../main/start-main-application/runnable-tokens/before-application-is-loading-injection-token";
import initClusterStoreInjectable from "../../store/main/init.injectable";
import emitClusterStateUpdateInjectable from "./emit-update.injectable";
const setupClusterStateBroadcastingInjectable = getInjectable({
id: "setup-cluster-state-broadcasting",
instantiate: (di) => ({
id: "setup-cluster-state-broadcasting",
run: () => {
const emitClusterStateUpdate = di.inject(emitClusterStateUpdateInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const prevStates = new Map<ClusterId, ClusterState>();
autorun(() => {
for (const cluster of clusterStore.clusters.values()) {
const prevState = prevStates.get(cluster.id);
const curState = cluster.getState();
if (!prevState || !isEqual(prevState, curState)) {
prevStates.set(cluster.id, curState);
emitClusterStateUpdate({
clusterId: cluster.id,
state: cluster.getState(),
});
}
}
});
},
runAfter: di.inject(initClusterStoreInjectable),
}),
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default setupClusterStateBroadcastingInjectable;
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable";
import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token";
import { clusterStateSyncChannel } from "../common/channels";
const clusterStateListenerInjectable = getMessageChannelListenerInjectable({
channel: clusterStateSyncChannel,
id: "main",
handler: (di) => {
const getClusterById = di.inject(getClusterByIdInjectable);
return ({ clusterId, state }) => getClusterById(clusterId)?.setState(state);
},
});
export default clusterStateListenerInjectable;
@@ -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 } from "@ogre-tools/injectable";
import type { RequestChannelHandler } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable";
import { initialClusterStatesChannel } from "../common/channels";
export type RequestInitialClusterStates = RequestChannelHandler<typeof initialClusterStatesChannel>;
const requestInitialClusterStatesInjectable = getInjectable({
id: "request-initial-cluster-states",
instantiate: (di): RequestInitialClusterStates => {
const requestFromChannel = di.inject(requestFromChannelInjectable);
return () => requestFromChannel(initialClusterStatesChannel);
},
});
export default requestInitialClusterStatesInjectable;
@@ -0,0 +1,29 @@
/**
* 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 getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../../../../renderer/before-frame-starts/tokens";
import initClusterStoreInjectable from "../../store/renderer/init.injectable";
import requestInitialClusterStatesInjectable from "./request-initial.injectable";
const setupClusterStateSyncInjectable = getInjectable({
id: "setup-cluster-state-sync",
instantiate: (di) => ({
id: "setup-cluster-state-sync",
run: async () => {
const requestInitialClusterStates = di.inject(requestInitialClusterStatesInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
const initalStates = await requestInitialClusterStates();
for (const { clusterId, state } of initalStates) {
getClusterById(clusterId)?.setState(state);
}
},
runAfter: di.inject(initClusterStoreInjectable),
}),
injectionToken: beforeFrameStartsSecondInjectionToken,
});
export default setupClusterStateSyncInjectable;
@@ -0,0 +1,26 @@
/**
* 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 clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { beforeApplicationIsLoadingInjectionToken } from "../../../../main/start-main-application/runnable-tokens/before-application-is-loading-injection-token";
import initUserStoreInjectable from "../../../../main/stores/init-user-store.injectable";
const initClusterStoreInjectable = getInjectable({
id: "init-cluster-store",
instantiate: (di) => {
const clusterStore = di.inject(clusterStoreInjectable);
return {
id: "init-cluster-store",
run: () => {
clusterStore.load();
},
runAfter: di.inject(initUserStoreInjectable),
};
},
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default initClusterStoreInjectable;
@@ -0,0 +1,24 @@
/**
* 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 clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../../../../renderer/before-frame-starts/tokens";
import initUserStoreInjectable from "../../../../renderer/stores/init-user-store.injectable";
const initClusterStoreInjectable = getInjectable({
id: "init-cluster-store",
instantiate: (di) => ({
id: "init-cluster-store",
run: () => {
const clusterStore = di.inject(clusterStoreInjectable);
clusterStore.load();
},
runAfter: di.inject(initUserStoreInjectable),
}),
injectionToken: beforeFrameStartsSecondInjectionToken,
});
export default initClusterStoreInjectable;
@@ -0,0 +1,119 @@
/**
* 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 { RenderResult } from "@testing-library/react";
import type { SidebarItemRegistration } from "../../renderer/components/layout/sidebar-items.injectable";
import { sidebarItemsInjectionToken } from "../../renderer/components/layout/sidebar-items.injectable";
import { computed, runInAction } from "mobx";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import React from "react";
import { frontEndRouteInjectionToken } from "../../common/front-end-routing/front-end-route-injection-token";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import { shouldShowResourceInjectionToken } from "../../common/cluster-store/allowed-resources-injection-token";
describe("cluster - visibility of sidebar items", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(() => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testRouteInjectable);
windowDi.register(testRouteComponentInjectable);
windowDi.register(testSidebarItemsInjectable);
});
});
});
describe("given kube resource for route is not allowed", () => {
beforeEach(async () => {
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("related sidebar item does not exist", () => {
const item = rendered.queryByTestId("sidebar-item-some-item-id");
expect(item).toBeNull();
});
describe("when kube resource becomes allowed", () => {
beforeEach(() => {
builder.allowKubeResource({
apiName: "namespaces",
group: "",
});
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("related sidebar item exists", () => {
const item = rendered.queryByTestId("sidebar-item-some-item-id");
expect(item).not.toBeNull();
});
});
});
});
const testRouteInjectable = getInjectable({
id: "some-route-injectable-id",
instantiate: (di) => ({
path: "/some-child-page",
clusterFrame: true,
isEnabled: di.inject(shouldShowResourceInjectionToken, {
apiName: "namespaces",
group: "",
}),
}),
injectionToken: frontEndRouteInjectionToken,
});
const testRouteComponentInjectable = getInjectable({
id: "some-child-page-route-component-injectable",
instantiate: (di) => ({
route: di.inject(testRouteInjectable),
Component: () => <div data-testid="some-child-page" />,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
const testSidebarItemsInjectable = getInjectable({
id: "some-sidebar-item-injectable",
instantiate: (di) => {
const testRoute = di.inject(testRouteInjectable);
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
return computed((): SidebarItemRegistration[] => [
{
id: "some-item-id",
parentId: null,
title: "Some item",
onClick: () => navigateToRoute(testRoute),
isVisible: testRoute.isEnabled,
orderNumber: 42,
},
]);
},
injectionToken: sidebarItemsInjectionToken,
});
@@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import navigateToWorkloadsOverviewInjectable from "../../common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("workload overview", () => {
let rendered: RenderResult;
let applicationBuilder: ApplicationBuilder;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame();
applicationBuilder.allowKubeResource({
apiName: "pods",
group: "",
});
rendered = await applicationBuilder.render();
});
describe("when navigating to workload overview", () => {
beforeEach(() => {
applicationBuilder.navigateWith(navigateToWorkloadsOverviewInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows workload overview", () => {
expect(rendered.queryByTestId("page-for-workloads-overview")).toBeInTheDocument();
});
it("shows pods pie chart", () => {
expect(rendered.queryByTestId("workload-overview-status-chart-pods")).toBeInTheDocument();
});
});
});
@@ -0,0 +1,103 @@
/**
* 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 { ApplicationBuilder } from "../../../../../renderer/components/test-utils/get-application-builder";
import type { KubernetesCluster } from "../../../../../common/catalog-entities";
import { getApplicationBuilder } from "../../../../../renderer/components/test-utils/get-application-builder";
import navigateToWorkloadsOverviewInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable";
import React from "react";
describe("disable workloads overview details when cluster is not relevant", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let isEnabledForClusterMock: AsyncFnMock<
(cluster: KubernetesCluster) => Promise<boolean>
>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
isEnabledForClusterMock = asyncFn();
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
isEnabledForCluster: isEnabledForClusterMock,
kubeWorkloadsOverviewItems: [
{
components: {
Details: () => (
<div data-testid="some-detail-component">Some detail component</div>
),
},
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToWorkloadsOverview = windowDi.inject(
navigateToWorkloadsOverviewInjectable,
);
navigateToWorkloadsOverview();
builder.extensions.enable(testExtension);
});
describe("given not yet known if extension should be enabled for the cluster", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the detail", () => {
const actual = rendered.queryByTestId("some-detail-component");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension shouldn't be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show the detail", () => {
const actual = rendered.queryByTestId("some-detail-component");
expect(actual).not.toBeInTheDocument();
});
});
describe("given extension should be enabled for the cluster", () => {
beforeEach(async () => {
await isEnabledForClusterMock.resolve(true);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows the detail", () => {
const actual = rendered.getByTestId("some-detail-component");
expect(actual).toBeInTheDocument();
});
});
});
@@ -0,0 +1,147 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { getApplicationBuilder } from "../../../../../renderer/components/test-utils/get-application-builder";
import React from "react";
import getRandomIdInjectable from "../../../../../common/utils/get-random-id.injectable";
import { workloadOverviewDetailInjectionToken } from "../../../../../renderer/components/+workloads-overview/workload-overview-details/workload-overview-detail-injection-token";
import { getInjectable } from "@ogre-tools/injectable";
import { computed, runInAction } from "mobx";
describe("order of workload overview details", () => {
let rendered: RenderResult;
beforeEach(async () => {
const builder = getApplicationBuilder();
builder.beforeWindowStart((windowDi) => {
windowDi.unoverride(getRandomIdInjectable);
windowDi.permitSideEffects(getRandomIdInjectable);
runInAction(() => {
windowDi.register(
someCoreItemWithLowOrderNumberInjectable,
someCoreItemWithHighOrderNumberInjectable,
someCoreItemWithDefaultOrderNumberInjectable,
);
});
});
builder.setEnvironmentToClusterFrame();
rendered = await builder.render();
const testExtension = {
id: "some-extension-id",
name: "some-extension",
rendererOptions: {
kubeWorkloadsOverviewItems: [
{
components: {
Details: () => (
<div
data-testid="workload-overview-detail"
id="some-extension-item-without-priority"
/>
),
},
},
{
priority: 70,
components: {
Details: () => (
<div
data-testid="workload-overview-detail"
id="some-extension-item-with-high-priority"
/>
),
},
},
{
priority: 10,
components: {
Details: () => (
<div
data-testid="workload-overview-detail"
id="some-extension-item-with-low-priority"
/>
),
},
},
],
},
};
builder.extensions.enable(testExtension);
});
it("shows items in correct order", () => {
const actual = rendered.queryAllByTestId("workload-overview-detail").map(x => x.id);
expect(actual).toEqual([
"some-core-item-with-low-order-number",
"some-extension-item-with-high-priority",
"some-core-item-with-default-order-number",
"some-extension-item-without-priority",
"some-core-item-with-high-order-number",
"some-extension-item-with-low-priority",
]);
});
});
const someCoreItemWithLowOrderNumberInjectable = getInjectable({
id: "some-core-item-with-low-order-number",
instantiate: () => ({
orderNumber: 30,
enabled: computed(() => true),
Component: () => (
<div
data-testid="workload-overview-detail"
id="some-core-item-with-low-order-number"
/>
),
}),
injectionToken: workloadOverviewDetailInjectionToken,
});
const someCoreItemWithDefaultOrderNumberInjectable = getInjectable({
id: "some-core-item-with-default-order-number",
instantiate: () => ({
orderNumber: 50,
enabled: computed(() => true),
Component: () => (
<div
data-testid="workload-overview-detail"
id="some-core-item-with-default-order-number"
/>
),
}),
injectionToken: workloadOverviewDetailInjectionToken,
});
const someCoreItemWithHighOrderNumberInjectable = getInjectable({
id: "some-core-item-high-order-number",
instantiate: () => ({
orderNumber: 60,
enabled: computed(() => true),
Component: () => (
<div
data-testid="workload-overview-detail"
id="some-core-item-with-high-order-number"
/>
),
}),
injectionToken: workloadOverviewDetailInjectionToken,
});
@@ -0,0 +1,72 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { IObservableValue } from "mobx";
import { computed, observable, runInAction } from "mobx";
import React from "react";
import navigateToWorkloadsOverviewInjectable from "../../../../../common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable";
import type { ApplicationBuilder } from "../../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../../renderer/components/test-utils/get-application-builder";
describe("reactively hide workloads overview details item", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let someObservable: IObservableValue<boolean>;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame();
someObservable = observable.box(false);
const testExtension = {
id: "test-extension-id",
name: "test-extension",
rendererOptions: {
kubeWorkloadsOverviewItems: [
{
components: {
Details: () => (
<div data-testid="some-workload-overview-detail-item">Some detail component</div>
),
},
visible: computed(() => someObservable.get()),
},
],
},
};
rendered = await builder.render();
const windowDi = builder.applicationWindow.only.di;
const navigateToWorkloadsOverview = windowDi.inject(
navigateToWorkloadsOverviewInjectable,
);
navigateToWorkloadsOverview();
builder.extensions.enable(testExtension);
});
it("does not show the workload overview detail item", () => {
const actual = rendered.queryByTestId("some-workload-overview-detail-item");
expect(actual).not.toBeInTheDocument();
});
it("given item should be shown, shows the workload overview detail item", () => {
runInAction(() => {
someObservable.set(true);
});
const actual = rendered.queryByTestId("some-workload-overview-detail-item");
expect(actual).toBeInTheDocument();
});
});