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);
+ });
});