From c67d2f6e1dec18c319391cbd6fdc7fa7b60fe9c8 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Mon, 9 Mar 2026 12:31:06 +0000 Subject: [PATCH] fix(ui): enable computedRequiresReaction --- ui/src/App.test.tsx | 5 +- ui/src/Components/AlertAck/index.tsx | 6 +- .../AlertGrid/AlertGroup/Alert/AlertMenu.tsx | 191 +++++++++--------- .../AlertGroup/GroupHeader/GroupMenu.tsx | 154 +++++++------- .../ManagedSilence/DeleteSilence.tsx | 6 +- .../Components/ManagedSilence/index.test.tsx | 53 ++--- ui/src/Components/ManagedSilence/index.tsx | 5 +- .../SilenceModal/Browser/MassDelete.tsx | 10 +- .../Components/SilenceModal/SilenceForm.tsx | 5 +- ui/src/Stores/AlertStore.test.ts | 13 +- ui/src/Stores/SilenceFormStore.test.ts | 85 +++++--- ui/src/__fixtures__/MobX.ts | 11 + ui/src/setupTests.ts | 2 +- 13 files changed, 306 insertions(+), 240 deletions(-) create mode 100644 ui/src/__fixtures__/MobX.ts diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index 3e1f2e0ce..b5d0a7a3a 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -18,6 +18,7 @@ import fetchMock from "@fetch-mock/jest"; import { mockMatchMedia } from "__fixtures__/matchMedia"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; +import { inReactiveContext } from "__fixtures__/MobX"; import type { UIDefaults, ThemeT } from "Models/UI"; import { SilenceFormStore, NewEmptyMatcher } from "Stores/SilenceFormStore"; import { StringToOption } from "Common/Select"; @@ -198,7 +199,7 @@ describe("", () => { store.data.setMatchers([m1, m2]); store.data.setComment("base64"); }); - const m = store.data.toBase64; + const m = inReactiveContext(() => store.data.toBase64); // Use history.pushState instead of setting window.location to avoid jsdom navigation error window.history.pushState({}, "App", `/?q=bar&m=${m}`); @@ -242,7 +243,7 @@ describe("", () => { const store = new SilenceFormStore(); store.data.setMatchers([m1, m2]); store.data.setComment("base64"); - const m = store.data.toBase64; + const m = inReactiveContext(() => store.data.toBase64); global.window.location = { href: `http://localhost/?q=bar&m=${m}`, diff --git a/ui/src/Components/AlertAck/index.tsx b/ui/src/Components/AlertAck/index.tsx index e7bc6307c..17bf3fb29 100644 --- a/ui/src/Components/AlertAck/index.tsx +++ b/ui/src/Components/AlertAck/index.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState } from "react"; -import { toJS } from "mobx"; +import { action, toJS } from "mobx"; import { observer } from "mobx-react-lite"; import { addSeconds } from "date-fns/addSeconds"; @@ -47,7 +47,7 @@ const AlertAck: FC<{ const { response, error, inProgress, reset } = useFetchAny(upstreams); - const onACK = () => { + const onACK = action(() => { setIsAcking(true); let author = @@ -95,7 +95,7 @@ const AlertAck: FC<{ }); } setClusters(c); - }; + }); useEffect(() => { if (upstreams.length && !inProgress && (error || response)) { diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.tsx index a91742c3c..8a0b793d7 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Alert/AlertMenu.tsx @@ -69,107 +69,108 @@ interface MenuContentProps { ref?: Ref; } -const MenuContent = ({ - x, - y, - floating, - strategy, - group, - alert, - afterClick, - alertStore, - silenceFormStore, - ref, -}: MenuContentProps) => { - const actions: APIAnnotationT[] = [ - ...alert.annotations - .filter((a) => a.isLink === true) - .filter((a) => a.isAction === true), - ...group.shared.annotations - .filter((a) => a.isLink === true) - .filter((a) => a.isAction === true), - ]; +const MenuContent = observer( + ({ + x, + y, + floating, + strategy, + group, + alert, + afterClick, + alertStore, + silenceFormStore, + ref, + }: MenuContentProps) => { + const actions: APIAnnotationT[] = [ + ...alert.annotations + .filter((a) => a.isLink === true) + .filter((a) => a.isAction === true), + ...group.shared.annotations + .filter((a) => a.isLink === true) + .filter((a) => a.isAction === true), + ]; - return ( - -
{ - if (typeof floating === "function") { - floating(node); - } else if (floating) { - // eslint-disable-next-line react-compiler/react-compiler -- assigning to floating ref in a ref callback is an intentional pattern from @floating-ui - floating.current = node; - } - if (typeof ref === "function") { - ref(node); - } else if (ref) { - ref.current = node; - } - }} - style={{ - position: strategy, - top: y, - left: x, - }} - > -
Alert source links:
- {alert.alertmanager.map((am) => ( - - ))} -
+ return ( +
{ - copy(JSON.stringify(alertToJSON(group, alert))); - afterClick(); - }} - > - - Copy to clipboard -
- {actions.length ? ( - <> -
-
Actions:
- {actions.map((action) => ( - - ))} - - ) : null} -
-
{ - if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) { - onSilenceClick(alertStore, silenceFormStore, group, alert); - afterClick(); + className="dropdown-menu d-block shadow m-0" + ref={(node) => { + if (typeof floating === "function") { + floating(node); + } else if (floating) { + floating.current = node; + } + if (typeof ref === "function") { + ref(node); + } else if (ref) { + ref.current = node; } }} + style={{ + position: strategy, + top: y, + left: x, + }} > - - Silence this alert +
Alert source links:
+ {alert.alertmanager.map((am) => ( + + ))} +
+
{ + copy(JSON.stringify(alertToJSON(group, alert))); + afterClick(); + }} + > + + Copy to clipboard +
+ {actions.length ? ( + <> +
+
Actions:
+ {actions.map((action) => ( + + ))} + + ) : null} +
+
{ + if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) { + onSilenceClick(alertStore, silenceFormStore, group, alert); + afterClick(); + } + }} + > + + Silence this alert +
-
- - ); -}; + + ); + }, +); interface AlertMenuProps { group: APIAlertGroupT; diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx index 0167ccba5..5c7e49187 100644 --- a/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx +++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/GroupHeader/GroupMenu.tsx @@ -1,5 +1,7 @@ import { FC, useRef, useState, useCallback, Ref, CSSProperties } from "react"; +import { observer } from "mobx-react-lite"; + import copy from "copy-to-clipboard"; import { useFloating, shift, offset } from "@floating-ui/react-dom"; @@ -58,88 +60,90 @@ const MenuContent: FC<{ afterClick: () => void; alertStore: AlertStore; silenceFormStore: SilenceFormStore; -}> = ({ - x, - y, - floating, - strategy, - group, - afterClick, - alertStore, - silenceFormStore, -}) => { - const groupFilters = group.labels.map((label) => - FormatQuery(label.name, QueryOperators.Equal, label.value), - ); - groupFilters.push( - FormatQuery(StaticLabels.Receiver, QueryOperators.Equal, group.receiver), - ); - const baseURL = [ - window.location.protocol, - "//", - window.location.host, - window.location.pathname, - ].join(""); - const groupLink = `${baseURL}?${FormatAlertsQ(groupFilters)}`; +}> = observer( + ({ + x, + y, + floating, + strategy, + group, + afterClick, + alertStore, + silenceFormStore, + }) => { + const groupFilters = group.labels.map((label) => + FormatQuery(label.name, QueryOperators.Equal, label.value), + ); + groupFilters.push( + FormatQuery(StaticLabels.Receiver, QueryOperators.Equal, group.receiver), + ); + const baseURL = [ + window.location.protocol, + "//", + window.location.host, + window.location.pathname, + ].join(""); + const groupLink = `${baseURL}?${FormatAlertsQ(groupFilters)}`; - const actions = group.shared.annotations - .filter((a) => a.isLink === true) - .filter((a) => a.isAction === true); + const actions = group.shared.annotations + .filter((a) => a.isLink === true) + .filter((a) => a.isAction === true); - return ( - -
- {actions.length ? ( - <> -
Actions:
- {actions.map((action) => ( - - ))} -
- - ) : null} + return ( +
{ - copy(groupLink); - afterClick(); + className="dropdown-menu d-block shadow m-0" + ref={floating} + style={{ + position: strategy, + top: y, + left: x, }} > - Copy link to this group -
-
{ - if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) { - onSilenceClick(alertStore, silenceFormStore, group); + {actions.length ? ( + <> +
Actions:
+ {actions.map((action) => ( + + ))} +
+ + ) : null} +
{ + copy(groupLink); afterClick(); - } - }} - > - Silence this group + }} + > + Copy link to this group +
+
{ + if (Object.keys(alertStore.data.clustersWithoutReadOnly).length) { + onSilenceClick(alertStore, silenceFormStore, group); + afterClick(); + } + }} + > + Silence this group +
-
-
- ); -}; + + ); + }, +); const GroupMenu: FC<{ group: APIAlertGroupT; diff --git a/ui/src/Components/ManagedSilence/DeleteSilence.tsx b/ui/src/Components/ManagedSilence/DeleteSilence.tsx index 5933cee98..5be500ecf 100644 --- a/ui/src/Components/ManagedSilence/DeleteSilence.tsx +++ b/ui/src/Components/ManagedSilence/DeleteSilence.tsx @@ -1,5 +1,7 @@ import { FC, useEffect, useState, ReactNode } from "react"; +import { observer } from "mobx-react-lite"; + import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; @@ -54,7 +56,7 @@ const DeleteResult: FC<{ alertStore: AlertStore; cluster: string; id: string; -}> = ({ alertStore, cluster, id }) => { +}> = observer(({ alertStore, cluster, id }) => { const [currentTime, setCurrentTime] = useState( Math.floor(Date.now()), ); @@ -101,7 +103,7 @@ const DeleteResult: FC<{ ) : null} ); -}; +}); const DeleteSilenceModalContent: FC<{ alertStore: AlertStore; diff --git a/ui/src/Components/ManagedSilence/index.test.tsx b/ui/src/Components/ManagedSilence/index.test.tsx index 8c8111f4c..1b52b0429 100644 --- a/ui/src/Components/ManagedSilence/index.test.tsx +++ b/ui/src/Components/ManagedSilence/index.test.tsx @@ -4,6 +4,7 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { MockSilence } from "__fixtures__/Alerts"; import { MockThemeContext } from "__fixtures__/Theme"; +import { inReactiveContext } from "__fixtures__/MobX"; import type { APISilenceT } from "Models/APITypes"; import { AlertStore } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; @@ -88,18 +89,20 @@ describe("", () => { }); it("GetAlertmanager() returns alertmanager object from alertStore.data.upstreams.instances", () => { - const am = GetAlertmanager(alertStore, cluster); - expect(am).toEqual({ - name: "am1", - cluster: "am", - clusterMembers: ["am1"], - uri: "http://localhost:9093", - publicURI: "http://example.com", - readonly: false, - error: "", - version: "0.24.0", - headers: {}, - corsCredentials: "include", + inReactiveContext(() => { + const am = GetAlertmanager(alertStore, cluster); + expect(am).toEqual({ + name: "am1", + cluster: "am", + clusterMembers: ["am1"], + uri: "http://localhost:9093", + publicURI: "http://example.com", + readonly: false, + error: "", + version: "0.24.0", + headers: {}, + corsCredentials: "include", + }); }); }); @@ -135,18 +138,20 @@ describe("", () => { clusters: { am: ["am1", "am2"] }, }); - const am = GetAlertmanager(alertStore, cluster); - expect(am).toEqual({ - name: "am1", - cluster: "am", - clusterMembers: ["am1"], - uri: "http://localhost:9093", - publicURI: "http://example.com", - readonly: false, - error: "", - version: "0.24.0", - headers: {}, - corsCredentials: "include", + inReactiveContext(() => { + const am = GetAlertmanager(alertStore, cluster); + expect(am).toEqual({ + name: "am1", + cluster: "am", + clusterMembers: ["am1"], + uri: "http://localhost:9093", + publicURI: "http://example.com", + readonly: false, + error: "", + version: "0.24.0", + headers: {}, + corsCredentials: "include", + }); }); }); diff --git a/ui/src/Components/ManagedSilence/index.tsx b/ui/src/Components/ManagedSilence/index.tsx index 9df93bce6..45f539bae 100644 --- a/ui/src/Components/ManagedSilence/index.tsx +++ b/ui/src/Components/ManagedSilence/index.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useState } from "react"; +import { action } from "mobx"; import { observer } from "mobx-react-lite"; import { parseISO } from "date-fns/parseISO"; @@ -55,14 +56,14 @@ const ManagedSilence: FC<{ const [showDetails, setShowDetails] = useState(isOpen); - const onEditSilence = () => { + const onEditSilence = action(() => { const alertmanager = GetAlertmanager(alertStore, cluster); silenceFormStore.data.fillFormFromSilence(alertmanager, silence); silenceFormStore.data.resetProgress(); silenceFormStore.tab.setTab("editor"); silenceFormStore.toggle.show(); - }; + }); const [progress, setProgress] = useState(() => calculatePercent(silence.startsAt, silence.endsAt), diff --git a/ui/src/Components/SilenceModal/Browser/MassDelete.tsx b/ui/src/Components/SilenceModal/Browser/MassDelete.tsx index 0583d46b4..d49e94014 100644 --- a/ui/src/Components/SilenceModal/Browser/MassDelete.tsx +++ b/ui/src/Components/SilenceModal/Browser/MassDelete.tsx @@ -7,6 +7,8 @@ import React, { useEffect, } from "react"; +import { observer } from "mobx-react-lite"; + import { useFloating, shift, flip, offset, size } from "@floating-ui/react-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -195,10 +197,12 @@ const SilenceDeleteModalContent: FC<{ export const MassDeleteProgress: FC<{ alertStore: AlertStore; silences: ClusterSilenceT[]; -}> = ({ alertStore, silences }) => { +}> = observer(({ alertStore, silences }) => { const [done, setDone] = useState(0); const [errors, setErrors] = useState([]); + const readWriteAlertmanagers = alertStore.data.readWriteAlertmanagers; + useEffect(() => { const deleteSilence = async ( cluster: string, @@ -237,7 +241,7 @@ export const MassDeleteProgress: FC<{ const timers: ReturnType[] = []; silences.forEach((silence, index) => { - const ams = alertStore.data.readWriteAlertmanagers.filter( + const ams = readWriteAlertmanagers.filter( (u) => u.cluster === silence.cluster, ); // eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- false positive: timers are collected and cleared in the useEffect cleanup below @@ -296,4 +300,4 @@ export const MassDeleteProgress: FC<{ ) : null} ); -}; +}); diff --git a/ui/src/Components/SilenceModal/SilenceForm.tsx b/ui/src/Components/SilenceModal/SilenceForm.tsx index 59ea489cc..a361ab827 100644 --- a/ui/src/Components/SilenceModal/SilenceForm.tsx +++ b/ui/src/Components/SilenceModal/SilenceForm.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useState, MouseEvent, SyntheticEvent } from "react"; +import { action } from "mobx"; import { observer } from "mobx-react-lite"; import copy from "copy-to-clipboard"; @@ -177,7 +178,7 @@ const SilenceForm: FC<{ silenceFormStore.data.setComment(comment); }; - const handleSubmit = (event: SyntheticEvent) => { + const handleSubmit = action((event: SyntheticEvent) => { event.preventDefault(); const rbc: { [label: string]: ClusterRequestT } = {}; @@ -192,7 +193,7 @@ const SilenceForm: FC<{ silenceFormStore.data.setStage("preview"); silenceFormStore.data.setWasValidated(true); - }; + }); return (
diff --git a/ui/src/Stores/AlertStore.test.ts b/ui/src/Stores/AlertStore.test.ts index 2978c182f..9597da954 100644 --- a/ui/src/Stores/AlertStore.test.ts +++ b/ui/src/Stores/AlertStore.test.ts @@ -1,6 +1,7 @@ import fetchMock from "@fetch-mock/jest"; import { EmptyAPIResponse } from "__fixtures__/Fetch"; +import { inReactiveContext } from "__fixtures__/MobX"; import { MockGroup } from "__fixtures__/Stories"; import { AlertStore, @@ -129,8 +130,10 @@ describe("AlertStore.data", () => { }, ], }); - expect(store.data.clustersWithoutReadOnly).toEqual({ - default: ["am2", "am1"], + inReactiveContext(() => { + expect(store.data.clustersWithoutReadOnly).toEqual({ + default: ["am2", "am1"], + }); }); }); @@ -178,8 +181,10 @@ describe("AlertStore.data", () => { }, ], }); - expect(store.data.clustersWithoutReadOnly).toEqual({ - default: ["am1", "am2", "am3"], + inReactiveContext(() => { + expect(store.data.clustersWithoutReadOnly).toEqual({ + default: ["am1", "am2", "am3"], + }); }); }); }); diff --git a/ui/src/Stores/SilenceFormStore.test.ts b/ui/src/Stores/SilenceFormStore.test.ts index 45cf78f73..8f2e2e678 100644 --- a/ui/src/Stores/SilenceFormStore.test.ts +++ b/ui/src/Stores/SilenceFormStore.test.ts @@ -10,6 +10,7 @@ import { MockSilence, MockAlertmanager, } from "__fixtures__/Alerts"; +import { inReactiveContext } from "__fixtures__/MobX"; import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select"; import { SilenceFormStore, @@ -826,14 +827,18 @@ describe("SilenceFormStore.data", () => { it("toAlertmanagerPayload contains id when store.data.silenceID is set", () => { store.data.setSilenceID("12345"); - expect(store.data.toAlertmanagerPayload).toMatchObject({ - id: "12345", + inReactiveContext(() => { + expect(store.data.toAlertmanagerPayload).toMatchObject({ + id: "12345", + }); }); }); it("toAlertmanagerPayload doesn't contain id when store.data.silenceID is null", () => { store.data.setSilenceID(null); - expect(store.data.toAlertmanagerPayload.id).toBeUndefined(); + inReactiveContext(() => { + expect(store.data.toAlertmanagerPayload.id).toBeUndefined(); + }); }); it("toAlertmanagerPayload creates payload that matches snapshot", () => { @@ -851,7 +856,9 @@ describe("SilenceFormStore.data", () => { store.data.setEnd(new Date(Date.UTC(2000, 1, 1, 1, 0, 0))); store.data.setAuthor("me@example.com"); store.data.setComment("toAlertmanagerPayload test"); - expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); + inReactiveContext(() => { + expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); + }); }); it("toAlertmanagerPayload creates payload that matches snapshot with regex values", () => { @@ -957,7 +964,9 @@ describe("SilenceFormStore.data", () => { store.data.setEnd(new Date(Date.UTC(2000, 1, 1, 1, 0, 0))); store.data.setAuthor("me@example.com"); store.data.setComment("toAlertmanagerPayload test"); - expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); + inReactiveContext(() => { + expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); + }); }); it("dumps to base64 and back", () => { @@ -970,7 +979,7 @@ describe("SilenceFormStore.data", () => { store.data.setStart(new Date()); store.data.setEnd(addMinutes(addHours(store.data.startsAt, 7), 45)); store.data.setComment("base64"); - const b64 = store.data.toBase64; + const b64 = inReactiveContext(() => store.data.toBase64); store.data.setMatchers([]); store.data.setComment(""); @@ -1007,7 +1016,7 @@ describe("SilenceFormStore.data", () => { it("base64 restore ignores empty matchers", () => { store.data.setMatchers([]); store.data.setComment("base64"); - const b64 = store.data.toBase64; + const b64 = inReactiveContext(() => store.data.toBase64); store.data.setMatchers([]); store.data.setComment("foo"); @@ -1048,7 +1057,9 @@ describe("SilenceFormStore.data.isValid", () => { ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if matchers list is empty", () => { @@ -1056,7 +1067,9 @@ describe("SilenceFormStore.data.isValid", () => { store.data.setMatchers([]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if matchers list is populated when a matcher without any name", () => { @@ -1066,7 +1079,9 @@ describe("SilenceFormStore.data.isValid", () => { ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if matchers list is populated when a matcher without any value ([])", () => { @@ -1074,7 +1089,9 @@ describe("SilenceFormStore.data.isValid", () => { store.data.setMatchers([MockMatcher("foo", [])]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if matchers list is populated when a matcher with empty value ([''])", () => { @@ -1082,7 +1099,9 @@ describe("SilenceFormStore.data.isValid", () => { store.data.setMatchers([MockMatcher("foo", [])]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if author is empty", () => { @@ -1092,7 +1111,9 @@ describe("SilenceFormStore.data.isValid", () => { ]); store.data.setAuthor(""); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'false' if comment is empty", () => { @@ -1102,7 +1123,9 @@ describe("SilenceFormStore.data.isValid", () => { ]); store.data.setAuthor("me@example.com"); store.data.setComment(""); - expect(store.data.isValid).toBe(false); + inReactiveContext(() => { + expect(store.data.isValid).toBe(false); + }); }); it("isValid returns 'true' if all fields are set", () => { @@ -1112,7 +1135,9 @@ describe("SilenceFormStore.data.isValid", () => { ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); - expect(store.data.isValid).toBe(true); + inReactiveContext(() => { + expect(store.data.isValid).toBe(true); + }); }); }); @@ -1120,30 +1145,36 @@ describe("SilenceFormStore.data startsAt & endsAt validation", () => { it("toDuration returns correct duration for 5d 0h 1m", () => { store.data.setStart(new Date(2000, 1, 1, 0, 0, 0)); store.data.setEnd(new Date(2000, 1, 6, 0, 1, 15)); - expect(store.data.toDuration).toMatchObject({ - days: 5, - hours: 0, - minutes: 1, + inReactiveContext(() => { + expect(store.data.toDuration).toMatchObject({ + days: 5, + hours: 0, + minutes: 1, + }); }); }); it("toDuration returns correct duration for 2h 15m", () => { store.data.setStart(new Date(2000, 1, 1, 0, 0, 0)); store.data.setEnd(new Date(2000, 1, 1, 2, 15, 0)); - expect(store.data.toDuration).toMatchObject({ - days: 0, - hours: 2, - minutes: 15, + inReactiveContext(() => { + expect(store.data.toDuration).toMatchObject({ + days: 0, + hours: 2, + minutes: 15, + }); }); }); it("toDuration returns correct duration for 59m", () => { store.data.setStart(new Date(2000, 1, 1, 0, 10, 0)); store.data.setEnd(new Date(2000, 1, 1, 1, 9, 0)); - expect(store.data.toDuration).toMatchObject({ - days: 0, - hours: 0, - minutes: 59, + inReactiveContext(() => { + expect(store.data.toDuration).toMatchObject({ + days: 0, + hours: 0, + minutes: 59, + }); }); }); diff --git a/ui/src/__fixtures__/MobX.ts b/ui/src/__fixtures__/MobX.ts new file mode 100644 index 000000000..4ecf70829 --- /dev/null +++ b/ui/src/__fixtures__/MobX.ts @@ -0,0 +1,11 @@ +import { autorun } from "mobx"; + +function inReactiveContext(fn: () => T): T { + let result: T; + autorun(() => { + result = fn(); + })(); + return result!; +} + +export { inReactiveContext }; diff --git a/ui/src/setupTests.ts b/ui/src/setupTests.ts index 04d423d06..d55a7b591 100644 --- a/ui/src/setupTests.ts +++ b/ui/src/setupTests.ts @@ -23,7 +23,7 @@ fetchMock.mockGlobal(); configure({ enforceActions: "observed", - // computedRequiresReaction: true, + computedRequiresReaction: true, // reactionRequiresObservable: true, // observableRequiresReaction: true, });