From 036513f1a18ffd09bf40899f4d37e107473f35b6 Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Tue, 11 Jun 2024 15:24:09 +0200 Subject: [PATCH 1/4] feat: show msm connection info Signed-off-by: Peter Makowski --- .../components/StatusBar/StatusBar.test.tsx | 14 +++++++ .../base/components/StatusBar/StatusBar.tsx | 30 ++++++++++++-- src/app/store/msm/index.ts | 1 + src/app/store/msm/selectors.ts | 17 ++++++++ src/app/store/msm/slice.ts | 41 +++++++++++++++++++ src/app/store/msm/types/base.ts | 11 +++++ src/app/store/msm/types/enum.ts | 3 ++ src/app/store/root/types.ts | 13 ++++-- src/app/store/utils/slice.ts | 3 ++ src/root-reducer.ts | 2 + src/testing/factories/index.ts | 2 + src/testing/factories/msm.ts | 17 ++++++++ src/testing/factories/state.ts | 13 ++++++ 13 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 src/app/store/msm/index.ts create mode 100644 src/app/store/msm/selectors.ts create mode 100644 src/app/store/msm/slice.ts create mode 100644 src/app/store/msm/types/base.ts create mode 100644 src/app/store/msm/types/enum.ts create mode 100644 src/testing/factories/msm.ts diff --git a/src/app/base/components/StatusBar/StatusBar.test.tsx b/src/app/base/components/StatusBar/StatusBar.test.tsx index 0a12194c1e..f37464de1f 100644 --- a/src/app/base/components/StatusBar/StatusBar.test.tsx +++ b/src/app/base/components/StatusBar/StatusBar.test.tsx @@ -248,3 +248,17 @@ it("hides the feedback link in development environment", () => { screen.queryByRole("button", { name: "Give feedback" }) ).not.toBeInTheDocument(); }); + +it("displays the status message when connected to MAAS Site Manager", () => { + state.msm = factory.msmState({ + status: factory.msmStatus({ + running: "connected", + }), + }); + + renderWithMockStore(, { state }); + + expect( + screen.getByText("Connected to MAAS Site Manager") + ).toBeInTheDocument(); +}); diff --git a/src/app/base/components/StatusBar/StatusBar.tsx b/src/app/base/components/StatusBar/StatusBar.tsx index 33e5184b9b..82d054c527 100644 --- a/src/app/base/components/StatusBar/StatusBar.tsx +++ b/src/app/base/components/StatusBar/StatusBar.tsx @@ -1,7 +1,9 @@ -import type { ReactNode } from "react"; +import { useEffect, type ReactNode } from "react"; -import { Button, Link } from "@canonical/react-components"; -import { useSelector } from "react-redux"; +import { Button, Icon, Link } from "@canonical/react-components"; +import { useDispatch, useSelector } from "react-redux"; + +import TooltipButton from "../TooltipButton"; import { useUsabilla } from "@/app/base/hooks"; import configSelectors from "@/app/store/config/selectors"; @@ -18,6 +20,8 @@ import { isDeployedWithHardwareSync, isMachineDetails, } from "@/app/store/machine/utils"; +import { msmActions } from "@/app/store/msm"; +import msmSelectors from "@/app/store/msm/selectors"; import type { UtcDatetime } from "@/app/store/types/model"; import { NodeStatus } from "@/app/store/types/node"; import { formatUtcDatetime, getTimeDistanceString } from "@/app/utils/time"; @@ -57,6 +61,12 @@ export const StatusBar = (): JSX.Element | null => { const version = useSelector(versionSelectors.get); const maasName = useSelector(configSelectors.maasName); const allowUsabilla = useUsabilla(); + const msmRunning = useSelector(msmSelectors.running); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(msmActions.fetch()); + }, [dispatch]); if (!(maasName && version)) { return null; @@ -101,6 +111,20 @@ export const StatusBar = (): JSX.Element | null => { :  {version} +
+ + {msmRunning === "connected" ? ( + + + Connected to MAAS Site Manager + + ) : null} + +
  • state.msm.status; +const running = createSelector(status, (status) => status?.running); +const loading = (state: RootState) => state.msm.loading; +const errors = (state: RootState) => state.msm.errors; + +const msmSelectors = { + status, + running, + loading, + errors, +}; + +export default msmSelectors; diff --git a/src/app/store/msm/slice.ts b/src/app/store/msm/slice.ts new file mode 100644 index 0000000000..79f1d64e5d --- /dev/null +++ b/src/app/store/msm/slice.ts @@ -0,0 +1,41 @@ +import type { PayloadAction } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; + +import type { MsmState, MsmStatus } from "./types/base"; + +import { genericInitialState } from "@/app/store/utils/slice"; + +const initialState: MsmState = { + ...genericInitialState, + status: null, +}; + +const msmSlice = createSlice({ + name: "msm", + initialState, + reducers: { + fetch: { + prepare: () => ({ + meta: { + model: "msm", + method: "status", + }, + payload: null, + }), + reducer: () => {}, + }, + fetchSuccess(state, action: PayloadAction) { + state.status = action.payload; + state.loading = false; + state.errors = null; + }, + fetchError(state, action: PayloadAction) { + state.errors = action.payload; + state.loading = false; + }, + }, +}); + +export const { actions: msmActions } = msmSlice; + +export default msmSlice.reducer; diff --git a/src/app/store/msm/types/base.ts b/src/app/store/msm/types/base.ts new file mode 100644 index 0000000000..cd78c35136 --- /dev/null +++ b/src/app/store/msm/types/base.ts @@ -0,0 +1,11 @@ +export interface MsmStatus { + smUrl: string | null; + running: "not_connected" | "pending" | "connected"; + startTime: string | null; +} + +export interface MsmState { + status: MsmStatus | null; + loading: boolean; + errors: string | null; +} diff --git a/src/app/store/msm/types/enum.ts b/src/app/store/msm/types/enum.ts new file mode 100644 index 0000000000..7ec78101ef --- /dev/null +++ b/src/app/store/msm/types/enum.ts @@ -0,0 +1,3 @@ +export enum MsmMeta { + MODEL = "msm", +} diff --git a/src/app/store/root/types.ts b/src/app/store/root/types.ts index ad2ed8f87e..83e1fa6ebf 100644 --- a/src/app/store/root/types.ts +++ b/src/app/store/root/types.ts @@ -1,9 +1,5 @@ import type { RouterState } from "redux-first-history"; -import type { ReservedIpState } from "../reservedip/types"; -import type { ReservedIpMeta } from "../reservedip/types/enum"; -import type { VMClusterMeta, VMClusterState } from "../vmcluster/types"; - import type { BootResourceState, BootResourceMeta, @@ -33,6 +29,8 @@ import type { } from "@/app/store/licensekeys/types"; import type { MachineState, MachineMeta } from "@/app/store/machine/types"; import type { MessageState, MessageMeta } from "@/app/store/message/types"; +import type { MsmState } from "@/app/store/msm/types/base"; +import type { MsmMeta } from "@/app/store/msm/types/enum"; import type { NodeDeviceState, NodeDeviceMeta, @@ -50,6 +48,8 @@ import type { PackageRepositoryMeta, } from "@/app/store/packagerepository/types"; import type { PodState, PodMeta } from "@/app/store/pod/types"; +import type { ReservedIpState } from "@/app/store/reservedip/types"; +import type { ReservedIpMeta } from "@/app/store/reservedip/types/enum"; import type { ResourcePoolState, ResourcePoolMeta, @@ -73,6 +73,10 @@ import type { TagState, TagMeta } from "@/app/store/tag/types"; import type { TokenState, TokenMeta } from "@/app/store/token/types"; import type { UserState, UserMeta } from "@/app/store/user/types"; import type { VLANState, VLANMeta } from "@/app/store/vlan/types"; +import type { + VMClusterMeta, + VMClusterState, +} from "@/app/store/vmcluster/types"; import type { ZoneState, ZoneMeta } from "@/app/store/zone/types"; export type RootState = { @@ -90,6 +94,7 @@ export type RootState = { [LicenseKeysMeta.MODEL]: LicenseKeysState; [MachineMeta.MODEL]: MachineState; [MessageMeta.MODEL]: MessageState; + [MsmMeta.MODEL]: MsmState; [NodeDeviceMeta.MODEL]: NodeDeviceState; [NodeScriptResultMeta.MODEL]: NodeScriptResultState; [NotificationMeta.MODEL]: NotificationState; diff --git a/src/app/store/utils/slice.ts b/src/app/store/utils/slice.ts index 52672cccef..64dfc8bc60 100644 --- a/src/app/store/utils/slice.ts +++ b/src/app/store/utils/slice.ts @@ -5,6 +5,8 @@ import type { SliceCaseReducers, } from "@reduxjs/toolkit"; +import type { MsmMeta } from "../msm/types/enum"; + import type { KeysOfUnion } from "@/app/base/types"; import type { BootResourceMeta } from "@/app/store/bootresource/types"; import type { ConfigMeta } from "@/app/store/config/types"; @@ -50,6 +52,7 @@ export type CommonStates = Omit< | ConfigMeta.MODEL | GeneralMeta.MODEL | MessageMeta.MODEL + | MsmMeta.MODEL | NodeScriptResultMeta.MODEL | StatusMeta.MODEL | ZoneMeta.MODEL diff --git a/src/root-reducer.ts b/src/root-reducer.ts index c0b6715c16..fdb1083ff6 100644 --- a/src/root-reducer.ts +++ b/src/root-reducer.ts @@ -18,6 +18,7 @@ import iprange from "@/app/store/iprange"; import licensekeys from "@/app/store/licensekeys"; import machine from "@/app/store/machine"; import message from "@/app/store/message"; +import msm from "@/app/store/msm"; import nodedevice from "@/app/store/nodedevice"; import nodescriptresult from "@/app/store/nodescriptresult"; import notification from "@/app/store/notification"; @@ -61,6 +62,7 @@ const createAppReducer = (routerReducer: Reducer) => licensekeys, machine, message, + msm, nodedevice, nodescriptresult, notification, diff --git a/src/testing/factories/index.ts b/src/testing/factories/index.ts index 06e856b4ac..4af43aa6d1 100644 --- a/src/testing/factories/index.ts +++ b/src/testing/factories/index.ts @@ -44,6 +44,8 @@ export { machineStatus, machineStatuses, messageState, + msmState, + msmStatus, nodeDeviceState, nodeScriptResultState, notificationState, diff --git a/src/testing/factories/msm.ts b/src/testing/factories/msm.ts new file mode 100644 index 0000000000..7665681445 --- /dev/null +++ b/src/testing/factories/msm.ts @@ -0,0 +1,17 @@ +import { define } from "cooky-cutter"; + +import { timestamp } from "./general"; + +import type { MsmState, MsmStatus } from "@/app/store/msm/types/base"; + +const msmStatus = define({ + smUrl: "https://example.com", + running: "not_connected", + startTime: timestamp("Wed, 08 Jul. 2022 05:35:45"), +}); + +export const msm = define({ + status: msmStatus(), + loading: false, + errors: null, +}); diff --git a/src/testing/factories/state.ts b/src/testing/factories/state.ts index d8410e726a..089fc250e7 100644 --- a/src/testing/factories/state.ts +++ b/src/testing/factories/state.ts @@ -70,6 +70,7 @@ import type { } from "@/app/store/machine/types"; import { FilterGroupKey, FilterGroupType } from "@/app/store/machine/types"; import type { MessageState } from "@/app/store/message/types"; +import type { MsmState, MsmStatus } from "@/app/store/msm/types/base"; import type { NodeDeviceState } from "@/app/store/nodedevice/types"; import type { NodeScriptResultState } from "@/app/store/nodescriptresult/types"; import type { NotificationState } from "@/app/store/notification/types"; @@ -424,6 +425,17 @@ export const messageState = define({ items: () => [], }); +export const msmStatus = define({ + running: "not_connected", + smUrl: "http://example.com", + startTime: "2021-01-01", +}); +export const msmState = define({ + status: msmStatus(), + loading: false, + errors: null, +}); + export const architecturesState = define({ ...defaultGeneralState, }); @@ -681,6 +693,7 @@ export const rootState = define({ licensekeys: licenseKeysState, machine: machineState, message: messageState, + msm: msmState, nodedevice: nodeDeviceState, notification: notificationState, nodescriptresult: nodeScriptResultState, From b588855a7fdf847c5f619d748589633e7a760592 Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Tue, 11 Jun 2024 15:54:01 +0200 Subject: [PATCH 2/4] fix: update strip-literal to 2.1.0 - fixes https://github.com/vitest-dev/vitest/issues/5387#issuecomment-2027845454 Signed-off-by: Peter Makowski --- .../base/components/StatusBar/StatusBar.tsx | 11 ++--- src/app/store/msm/slice.ts | 7 +-- src/app/store/msm/types/base.ts | 1 + yarn.lock | 49 +++++-------------- 4 files changed, 21 insertions(+), 47 deletions(-) diff --git a/src/app/base/components/StatusBar/StatusBar.tsx b/src/app/base/components/StatusBar/StatusBar.tsx index 82d054c527..083adcb8a3 100644 --- a/src/app/base/components/StatusBar/StatusBar.tsx +++ b/src/app/base/components/StatusBar/StatusBar.tsx @@ -1,11 +1,11 @@ -import { useEffect, type ReactNode } from "react"; +import { type ReactNode } from "react"; import { Button, Icon, Link } from "@canonical/react-components"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import TooltipButton from "../TooltipButton"; -import { useUsabilla } from "@/app/base/hooks"; +import { useFetchActions, useUsabilla } from "@/app/base/hooks"; import configSelectors from "@/app/store/config/selectors"; import controllerSelectors from "@/app/store/controller/selectors"; import { @@ -62,11 +62,8 @@ export const StatusBar = (): JSX.Element | null => { const maasName = useSelector(configSelectors.maasName); const allowUsabilla = useUsabilla(); const msmRunning = useSelector(msmSelectors.running); - const dispatch = useDispatch(); - useEffect(() => { - dispatch(msmActions.fetch()); - }, [dispatch]); + useFetchActions([msmActions.fetch]); if (!(maasName && version)) { return null; diff --git a/src/app/store/msm/slice.ts b/src/app/store/msm/slice.ts index 79f1d64e5d..fb16a679ea 100644 --- a/src/app/store/msm/slice.ts +++ b/src/app/store/msm/slice.ts @@ -3,11 +3,11 @@ import { createSlice } from "@reduxjs/toolkit"; import type { MsmState, MsmStatus } from "./types/base"; -import { genericInitialState } from "@/app/store/utils/slice"; - const initialState: MsmState = { - ...genericInitialState, status: null, + errors: null, + loading: false, + loaded: false, }; const msmSlice = createSlice({ @@ -27,6 +27,7 @@ const msmSlice = createSlice({ fetchSuccess(state, action: PayloadAction) { state.status = action.payload; state.loading = false; + state.loaded = true; state.errors = null; }, fetchError(state, action: PayloadAction) { diff --git a/src/app/store/msm/types/base.ts b/src/app/store/msm/types/base.ts index cd78c35136..f38ac63e20 100644 --- a/src/app/store/msm/types/base.ts +++ b/src/app/store/msm/types/base.ts @@ -7,5 +7,6 @@ export interface MsmStatus { export interface MsmState { status: MsmStatus | null; loading: boolean; + loaded: boolean; errors: string | null; } diff --git a/yarn.lock b/yarn.lock index 7c803c3000..6fc401b1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9721,10 +9721,10 @@ js-file-download@0.4.12: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^8.0.2: - version "8.0.3" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-8.0.3.tgz#1c407ec905643603b38b6be6977300406ec48775" - integrity sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw== +js-tokens@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" + integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== js-yaml@^3.13.1: version "3.14.1" @@ -12563,7 +12563,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12581,15 +12581,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -12655,7 +12646,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12669,13 +12660,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -12717,12 +12701,12 @@ strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.0.0.tgz#5d063580933e4e03ebb669b12db64d2200687527" - integrity sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA== +strip-literal@2.1.0, strip-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" + integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== dependencies: - js-tokens "^8.0.2" + js-tokens "^9.0.0" style-loader@^3.3.1: version "3.3.1" @@ -13877,7 +13861,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13895,15 +13879,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From caa29c1f9e63c4977eccba3d5bb9f9d5557bc90d Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Wed, 12 Jun 2024 14:40:49 +0200 Subject: [PATCH 3/4] test: add rerender with state updates in renderWithMockStore - support rerendering components with updated immutable state with Immer `produce`. - add `immer` as a dev dependency Signed-off-by: Peter Makowski --- .../components/StatusBar/StatusBar.test.tsx | 18 +++++++++++-- src/testing/utils.tsx | 27 ++++++++++++++----- yarn.lock | 5 +++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/app/base/components/StatusBar/StatusBar.test.tsx b/src/app/base/components/StatusBar/StatusBar.test.tsx index f37464de1f..6c95d38cc7 100644 --- a/src/app/base/components/StatusBar/StatusBar.test.tsx +++ b/src/app/base/components/StatusBar/StatusBar.test.tsx @@ -252,11 +252,25 @@ it("hides the feedback link in development environment", () => { it("displays the status message when connected to MAAS Site Manager", () => { state.msm = factory.msmState({ status: factory.msmStatus({ - running: "connected", + running: "not_connected", }), }); - renderWithMockStore(, { state }); + const { rerender } = renderWithMockStore(, { state }); + + expect( + screen.queryByText("Connected to MAAS Site Manager") + ).not.toBeInTheDocument(); + + rerender(, { + state: (draft) => { + draft.msm = factory.msmState({ + status: factory.msmStatus({ + running: "connected", + }), + }); + }, + }); expect( screen.getByText("Connected to MAAS Site Manager") diff --git a/src/testing/utils.tsx b/src/testing/utils.tsx index 7904f52617..f6ba717fce 100644 --- a/src/testing/utils.tsx +++ b/src/testing/utils.tsx @@ -4,6 +4,7 @@ import type { ValueOf } from "@canonical/react-components"; import type { RenderOptions, RenderResult } from "@testing-library/react"; import { render, screen, renderHook } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { produce } from "immer"; import { Provider } from "react-redux"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import type { MockStoreEnhanced } from "redux-mock-store"; @@ -175,13 +176,17 @@ export const renderWithBrowserRouter = ( }; }; +interface WithStoreRenderOptions extends RenderOptions { + state?: RootState | ((stateDraft: RootState) => void); + store?: WrapperProps["store"]; +} + export const renderWithMockStore = ( ui: React.ReactNode, - options?: RenderOptions & { - state?: RootState; - store?: WrapperProps["store"]; - } -): RenderResult => { + options?: WithStoreRenderOptions +): Omit & { + rerender: (ui: React.ReactNode, newOptions?: WithStoreRenderOptions) => void; +} => { const { state, store, ...renderOptions } = options ?? {}; const rendered = render(ui, { wrapper: (props) => ( @@ -191,8 +196,16 @@ export const renderWithMockStore = ( }); return { ...rendered, - rerender: (ui: React.ReactNode) => - renderWithMockStore(ui, { container: rendered.container, ...options }), + rerender: (ui: React.ReactNode, newOptions?: WithStoreRenderOptions) => + renderWithMockStore(ui, { + container: rendered.container, + ...options, + ...newOptions, + state: + state && typeof newOptions?.state === "function" + ? produce(state, newOptions.state) + : newOptions?.state || state, + }), }; }; diff --git a/yarn.lock b/yarn.lock index 6fc401b1a2..58d0da9f55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12564,6 +12564,7 @@ string-natural-compare@^3.0.1: integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12647,6 +12648,7 @@ string_decoder@~1.1.1: safe-buffer "~5.1.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12701,7 +12703,7 @@ strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-literal@2.1.0, strip-literal@^2.0.0: +strip-literal@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== @@ -13862,6 +13864,7 @@ wordwrap@^1.0.0: integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From bae3c2bd9ea38e45c0cc602f953abe4443e9220a Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Wed, 12 Jun 2024 16:19:18 +0200 Subject: [PATCH 4/4] update msmState factory and snapshots Signed-off-by: Peter Makowski --- src/__snapshots__/root-reducer.test.ts.snap | 6 ++++++ src/app/store/utils/slice.ts | 3 +-- src/testing/factories/msm.ts | 5 +++-- src/testing/factories/state.ts | 5 +++-- src/testing/utils.tsx | 7 ++++++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/__snapshots__/root-reducer.test.ts.snap b/src/__snapshots__/root-reducer.test.ts.snap index 3fed244ef7..08254a06fc 100644 --- a/src/__snapshots__/root-reducer.test.ts.snap +++ b/src/__snapshots__/root-reducer.test.ts.snap @@ -228,6 +228,12 @@ exports[`rootReducer > should reset app to initial state on LOGOUT_SUCCESS, exce "message": { "items": [], }, + "msm": { + "errors": null, + "loaded": false, + "loading": false, + "status": null, + }, "nodedevice": { "errors": null, "items": [], diff --git a/src/app/store/utils/slice.ts b/src/app/store/utils/slice.ts index 64dfc8bc60..7095263fd0 100644 --- a/src/app/store/utils/slice.ts +++ b/src/app/store/utils/slice.ts @@ -5,8 +5,6 @@ import type { SliceCaseReducers, } from "@reduxjs/toolkit"; -import type { MsmMeta } from "../msm/types/enum"; - import type { KeysOfUnion } from "@/app/base/types"; import type { BootResourceMeta } from "@/app/store/bootresource/types"; import type { ConfigMeta } from "@/app/store/config/types"; @@ -18,6 +16,7 @@ import type { DeviceMeta, DeviceStatus } from "@/app/store/device/types"; import type { GeneralMeta } from "@/app/store/general/types"; import type { MachineMeta, MachineStatus } from "@/app/store/machine/types"; import type { MessageMeta } from "@/app/store/message/types"; +import type { MsmMeta } from "@/app/store/msm/types/enum"; import type { NodeScriptResultMeta } from "@/app/store/nodescriptresult/types"; import type { PodMeta, PodStatus } from "@/app/store/pod/types"; import type { RootState } from "@/app/store/root/types"; diff --git a/src/testing/factories/msm.ts b/src/testing/factories/msm.ts index 7665681445..c894c9db94 100644 --- a/src/testing/factories/msm.ts +++ b/src/testing/factories/msm.ts @@ -4,14 +4,15 @@ import { timestamp } from "./general"; import type { MsmState, MsmStatus } from "@/app/store/msm/types/base"; -const msmStatus = define({ +const msmStatus = define({ smUrl: "https://example.com", running: "not_connected", startTime: timestamp("Wed, 08 Jul. 2022 05:35:45"), }); export const msm = define({ - status: msmStatus(), + status: msmStatus, loading: false, + loaded: false, errors: null, }); diff --git a/src/testing/factories/state.ts b/src/testing/factories/state.ts index 089fc250e7..908820220f 100644 --- a/src/testing/factories/state.ts +++ b/src/testing/factories/state.ts @@ -425,14 +425,15 @@ export const messageState = define({ items: () => [], }); -export const msmStatus = define({ +export const msmStatus = define({ running: "not_connected", smUrl: "http://example.com", startTime: "2021-01-01", }); export const msmState = define({ - status: msmStatus(), + status: msmStatus, loading: false, + loaded: false, errors: null, }); diff --git a/src/testing/utils.tsx b/src/testing/utils.tsx index f6ba717fce..f51eafe593 100644 --- a/src/testing/utils.tsx +++ b/src/testing/utils.tsx @@ -188,9 +188,14 @@ export const renderWithMockStore = ( rerender: (ui: React.ReactNode, newOptions?: WithStoreRenderOptions) => void; } => { const { state, store, ...renderOptions } = options ?? {}; + const initialState = + typeof state === "function" + ? produce(rootStateFactory(), state) + : state || rootStateFactory(); + const rendered = render(ui, { wrapper: (props) => ( - + ), ...renderOptions, });