From 77cba5ec47b0912abbd9b42d2bae6c51eb6389a0 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Mon, 23 Feb 2026 09:24:52 +0000 Subject: [PATCH] fix(ci): update tests --- .../__snapshots__/index.test.tsx.snap | 2 +- ui/src/Components/AlertHistory/index.test.tsx | 45 ++++++++++- .../Components/Grid/AlertGrid/index.test.tsx | 32 ++++++++ .../Labels/FilterInputLabel/index.test.tsx | 26 +++++++ ui/src/Components/NavBar/index.test.tsx | 40 ++++++++++ .../SilenceModal/Browser/index.test.tsx | 76 +++++++++++++++++++ 6 files changed, 219 insertions(+), 2 deletions(-) diff --git a/ui/src/Components/AlertHistory/__snapshots__/index.test.tsx.snap b/ui/src/Components/AlertHistory/__snapshots__/index.test.tsx.snap index d3db30998..7a6850b59 100644 --- a/ui/src/Components/AlertHistory/__snapshots__/index.test.tsx.snap +++ b/ui/src/Components/AlertHistory/__snapshots__/index.test.tsx.snap @@ -26,7 +26,7 @@ exports[` handles fetch errors 1`] = ` `; -exports[` handles reponses with errors 1`] = ` +exports[` handles responses with errors 1`] = `
", () => { }); it("doesn't fetch when not in view", async () => { + // Verifies that react-intersection-observer prevents fetch when component is outside viewport fetchMock.resetHistory(); fetchMock.mock( "*", @@ -305,6 +306,48 @@ describe("", () => { expect(fetchMock.calls()).toHaveLength(0); }); + it("fetches when component transitions from out-of-view to in-view", async () => { + // Verifies that react-intersection-observer triggers fetch when component becomes visible + fetchMock.resetHistory(); + fetchMock.mock( + "*", + { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(RainbowHistoryResponse), + }, + { + overwriteRoutes: true, + }, + ); + + (useInView as jest.MockedFunction).mockReturnValue([ + jest.fn(), + false, + ] as any); + + MockAlerts(3); + const { rerender, unmount } = render( + , + ); + await act(async () => { + await fetchMock.flush(true); + }); + expect(fetchMock.calls()).toHaveLength(0); + + (useInView as jest.MockedFunction).mockReturnValue([ + jest.fn(), + true, + ] as any); + + rerender(); + await act(async () => { + await fetchMock.flush(true); + }); + expect(fetchMock.calls()).toHaveLength(1); + + unmount(); + }); + it("fetches an update after 300 seconds", async () => { fetchMock.resetHistory(); fetchMock.mock( @@ -348,7 +391,7 @@ describe("", () => { unmount(); }); - it("handles reponses with errors", async () => { + it("handles responses with errors", async () => { fetchMock.resetHistory(); fetchMock.mock( "*", diff --git a/ui/src/Components/Grid/AlertGrid/index.test.tsx b/ui/src/Components/Grid/AlertGrid/index.test.tsx index be4de90f6..f6b007e13 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.tsx +++ b/ui/src/Components/Grid/AlertGrid/index.test.tsx @@ -671,4 +671,36 @@ describe("", () => { const { unmount } = renderAlertGrid(); unmount(); }); + + it("alt+space hotkey toggles pause state", () => { + // Verifies that pressing alt+space triggers alertStore.status.togglePause + MockGroupList(1, 1); + renderAlertGrid(); + + expect(alertStore.status.paused).toBe(false); + + act(() => { + document.dispatchEvent( + new KeyboardEvent("keydown", { + key: " ", + code: "Space", + altKey: true, + } as KeyboardEventInit), + ); + }); + + expect(alertStore.status.paused).toBe(true); + + act(() => { + document.dispatchEvent( + new KeyboardEvent("keydown", { + key: " ", + code: "Space", + altKey: true, + } as KeyboardEventInit), + ); + }); + + expect(alertStore.status.paused).toBe(false); + }); }); diff --git a/ui/src/Components/Labels/FilterInputLabel/index.test.tsx b/ui/src/Components/Labels/FilterInputLabel/index.test.tsx index ab71364f7..cfd292894 100644 --- a/ui/src/Components/Labels/FilterInputLabel/index.test.tsx +++ b/ui/src/Components/Labels/FilterInputLabel/index.test.tsx @@ -174,6 +174,32 @@ describe(" onChange", () => { NewUnappliedFilter("bar=baz"), ); }); + + + it("editing filter to new value replaces it in alertStore", () => { + // Verifies that onChange replaces filter when edited to new value + const filter = createFilter("=", true, true, 1); + alertStore.filters.setFilterValues([filter]); + + const { container } = render( + , + ); + + const editSpan = container.querySelector( + ".components-filteredinputlabel-text span", + ); + fireEvent.click(editSpan!); + + const input = container.querySelector("input"); + fireEvent.change(input!, { target: { value: "foo=newvalue" } }); + fireEvent.keyDown(input!, { keyCode: 13 }); + + expect(alertStore.filters.values).toHaveLength(1); + expect(alertStore.filters.values[0].raw).toBe("foo=newvalue"); + }); }); describe(" render", () => { diff --git a/ui/src/Components/NavBar/index.test.tsx b/ui/src/Components/NavBar/index.test.tsx index 2ee3c5fcc..8a91a7fbf 100644 --- a/ui/src/Components/NavBar/index.test.tsx +++ b/ui/src/Components/NavBar/index.test.tsx @@ -90,6 +90,46 @@ const renderNavbar = (fixedTop?: boolean) => { }; describe("", () => { + it("sets isIdle to true after idle timeout", () => { + // Verifies that react-idle-timer triggers onIdle callback after timeout period + renderNavbar(); + expect(alertStore.ui.isIdle).toBe(false); + + act(() => { + jest.advanceTimersByTime(1000 * 60 * 3 + 1000); + }); + + expect(alertStore.ui.isIdle).toBe(true); + }); + + it("navbar becomes invisible when idle", () => { + // Verifies that navbar container class changes to invisible when idle + const { container } = renderNavbar(); + + expect(container.querySelector(".visible")).toBeInTheDocument(); + expect(container.querySelector(".invisible")).not.toBeInTheDocument(); + + act(() => { + jest.advanceTimersByTime(1000 * 60 * 3 + 1000); + }); + + expect(alertStore.ui.isIdle).toBe(true); + expect(container.querySelector(".invisible")).toBeInTheDocument(); + }); + + it("pauses idle timer when alertStore.status.paused is true", () => { + // Verifies that idle timer is paused when alerts are paused + renderNavbar(); + + alertStore.status.pause(); + + act(() => { + jest.advanceTimersByTime(1000 * 60 * 3 + 1000); + }); + + expect(alertStore.ui.isIdle).toBe(false); + }); + it("renders null with no upstreams", () => { alertStore.data.setUpstreams({ counters: { total: 0, healthy: 0, failed: 0 }, diff --git a/ui/src/Components/SilenceModal/Browser/index.test.tsx b/ui/src/Components/SilenceModal/Browser/index.test.tsx index f8f847f02..a29e9366b 100644 --- a/ui/src/Components/SilenceModal/Browser/index.test.tsx +++ b/ui/src/Components/SilenceModal/Browser/index.test.tsx @@ -881,4 +881,80 @@ describe("", () => { await act(() => promise); }); + + it("displays errors when delete requests fail on all cluster members", async () => { + // Verifies that MassDelete shows error messages when all retries fail + const promise = Promise.resolve(); + + alertStore.data.setUpstreams({ + counters: { total: 1, healthy: 1, failed: 0 }, + instances: [ + { + name: "am1", + cluster: "am", + clusterMembers: ["am1"], + uri: "http://m1.example.com", + publicURI: "http://example.com", + readonly: false, + error: "", + version: "0.24.0", + headers: {}, + corsCredentials: "include", + }, + ], + clusters: { am: ["am1"] }, + }); + + fetchMock.reset(); + fetchMock.mock("http://m1.example.com/api/v2/silence/1", { + status: 500, + body: "Internal Server Error", + }); + + const newSilence = (id: string): APISilenceT => { + const s = MockSilence(); + s.id = id; + return s; + }; + + useFetchGetMock.fetch.setMockedData({ + response: [ + { + cluster: cluster, + alertCount: 1, + silence: newSilence("1"), + isExpired: false, + }, + ], + error: null, + isLoading: false, + isRetrying: false, + retryCount: 0, + get: jest.fn(), + cancelGet: jest.fn(), + }); + + const { container } = renderBrowser(); + + const checkboxes = container.querySelectorAll( + "input.form-check-input[type='checkbox']", + ); + act(() => { + fireEvent.click(checkboxes[1]); + }); + + const del = container.querySelector(".btn.btn-danger"); + fireEvent.click(del!); + + await act(async () => { + jest.advanceTimersByTime(10 * 60); + await fetchMock.flush(true); + }); + + const errorDisplay = document.body.querySelector(".bg-dark.text-white"); + expect(errorDisplay).toBeInTheDocument(); + expect(errorDisplay?.querySelector("samp")).toBeInTheDocument(); + + await act(() => promise); + }); });