diff --git a/ui/package-lock.json b/ui/package-lock.json
index e09004ed4..33326b5d6 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -6355,6 +6355,12 @@
"p-map": "1.2.0"
}
},
+ "jest-localstorage-mock": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.2.0.tgz",
+ "integrity": "sha512-x+P0vcwr4540bCAYzTEpiD9rs+zh/QZzyiABV+MU6yM2OPwPlrrLyUx/6gValMyt6tg5lX6Z53o2rHWfUht5Xw==",
+ "dev": true
+ },
"jest-matcher-utils": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz",
diff --git a/ui/package.json b/ui/package.json
index 372070f65..01417fb43 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -52,6 +52,7 @@
},
"devDependencies": {
"eslint-plugin-react": "7.11.1",
+ "jest-localstorage-mock": "2.2.0",
"markdownlint-cli": "0.13.0",
"node-sass-chokidar": "1.3.3",
"onchange": "4.1.0",
diff --git a/ui/src/Components/Fetcher/index.js b/ui/src/Components/Fetcher/index.js
index f1f85e896..e20f0332a 100644
--- a/ui/src/Components/Fetcher/index.js
+++ b/ui/src/Components/Fetcher/index.js
@@ -28,7 +28,7 @@ const Fetcher = observer(
this.interval = newInterval;
this.timer = setInterval(
- () => alertStore.fetch(),
+ () => alertStore.fetchWithThrottle(),
this.interval * 1000
);
}
@@ -37,7 +37,7 @@ const Fetcher = observer(
componentDidUpdate() {
const { alertStore } = this.props;
- alertStore.fetch();
+ alertStore.fetchWithThrottle();
this.setTimer();
}
@@ -45,7 +45,7 @@ const Fetcher = observer(
componentDidMount() {
const { alertStore } = this.props;
- alertStore.fetch();
+ alertStore.fetchWithThrottle();
this.setTimer();
}
diff --git a/ui/src/Components/Fetcher/index.test.js b/ui/src/Components/Fetcher/index.test.js
new file mode 100644
index 000000000..02c2c7cfa
--- /dev/null
+++ b/ui/src/Components/Fetcher/index.test.js
@@ -0,0 +1,124 @@
+import React from "react";
+import renderer from "react-test-renderer";
+
+import { FetchMock, EmptyAPIResponse } from "__mocks__/Fetch";
+
+import { AlertStore } from "Stores/AlertStore";
+import { Settings } from "Stores/Settings";
+
+import { Fetcher } from ".";
+
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+let alertStore;
+let settingsStore;
+
+beforeEach(() => {
+ const response = EmptyAPIResponse();
+ global.fetch = FetchMock(response);
+
+ alertStore = new AlertStore(["label=value"]);
+ settingsStore = new Settings();
+});
+
+afterEach(() => {
+ jest.clearAllTimers();
+
+ global.fetch.mockRestore();
+});
+
+describe("", () => {
+ it("renders correctly with 'label=value' filter", () => {
+ const tree = renderer
+ .create()
+ .toJSON();
+
+ expect(tree.props["data-filters"]).toBe("label=value");
+ expect(tree.props["data-interval"]).toBe(30);
+ });
+
+ it("re-renders on fetch interval change", () => {
+ const fetcher = renderer.create(
+
+ );
+
+ expect(fetcher.toJSON().props["data-interval"]).toBe(30);
+ settingsStore.fetchConfig.config.interval = 60;
+ expect(fetcher.toJSON().props["data-interval"]).toBe(60);
+ });
+
+ it("re-renders on filters change", () => {
+ const fetcher = renderer.create(
+
+ );
+
+ expect(fetcher.toJSON().props["data-filters"]).toBe("label=value");
+ alertStore.filters.values = [];
+ expect(fetcher.toJSON().props["data-filters"]).toBe("");
+ });
+
+ it("calls alertStore.fetchWithThrottle on mount", () => {
+ const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
+
+ renderer.create(
+
+ );
+
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("calls alertStore.fetchWithThrottle again after interval change", () => {
+ const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
+
+ renderer.create(
+
+ );
+ settingsStore.fetchConfig.config.interval = 60;
+
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it("calls alertStore.fetchWithThrottle again after filter change", () => {
+ const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
+
+ renderer.create(
+
+ );
+ alertStore.filters.values = [];
+
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it("keeps calling alertStore.fetchWithThrottle after running pending timers", () => {
+ const fetchSpy = jest.spyOn(alertStore, "fetchWithThrottle");
+
+ renderer.create(
+
+ );
+ jest.runOnlyPendingTimers();
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ jest.runOnlyPendingTimers();
+ expect(fetchSpy).toHaveBeenCalledTimes(3);
+ jest.runOnlyPendingTimers();
+ expect(fetchSpy).toHaveBeenCalledTimes(4);
+ });
+
+ it("internal timer is armed after render", () => {
+ const fetcher = renderer.create(
+
+ );
+ const instance = fetcher.getInstance();
+ expect(instance.timer).toBeGreaterThanOrEqual(0);
+ });
+
+ it("internal timer is null after unmount", () => {
+ const fetcher = renderer.create(
+
+ );
+ const instance = fetcher.getInstance();
+ instance.componentWillUnmount();
+ expect(instance.timer).toBeNull();
+ });
+});
diff --git a/ui/src/Components/Labels/FilteringCounterBadge/index.test.js b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js
new file mode 100644
index 000000000..45f15921d
--- /dev/null
+++ b/ui/src/Components/Labels/FilteringCounterBadge/index.test.js
@@ -0,0 +1,112 @@
+import React from "react";
+import renderer from "react-test-renderer";
+
+import { AlertStore } from "Stores/AlertStore";
+
+import { FilteringCounterBadge } from ".";
+
+let alertStore;
+
+beforeEach(() => {
+ alertStore = new AlertStore([]);
+});
+
+const validateClassName = (value, className) => {
+ const tree = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(tree.props.className.split(" ")).toContain(className);
+};
+
+const validateStyle = value => {
+ const tree = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(tree.props.style).toMatchObject({});
+};
+
+const validateOnClick = value => {
+ const tree = renderer
+ .create(
+
+ )
+ .toJSON();
+
+ tree.props.onClick({ preventDefault: () => {} });
+
+ expect(alertStore.filters.values).toHaveLength(1);
+ expect(alertStore.filters.values).toContainEqual({
+ applied: false,
+ isValid: true,
+ raw: `@state=${value}`,
+ hits: 0,
+ name: "",
+ matcher: "",
+ value: ""
+ });
+};
+
+describe("", () => {
+ it("@state=unprocessed counter badge should have className 'badge-secondary'", () => {
+ validateClassName("unprocessed", "badge-secondary");
+ });
+ it("@state=active counter badge should have className 'badge-secondary'", () => {
+ validateClassName("active", "badge-danger");
+ });
+ it("@state=suppressed counter badge should have className 'badge-secondary'", () => {
+ validateClassName("suppressed", "badge-success");
+ });
+
+ it("@state=unprocessed counter badge should have empty style", () => {
+ validateStyle("unprocessed");
+ });
+ it("@state=active counter badge should have empty style", () => {
+ validateStyle("active");
+ });
+ it("@state=suppressed counter badge should have empty style", () => {
+ validateStyle("suppressed");
+ });
+
+ it("counter badge should have correct children based on the counter prop value", () => {
+ const tree = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(tree.children).toEqual(["123"]);
+ });
+
+ it("onClick method on @state=unprocessed counter badge should add a new filter", () => {
+ validateOnClick("unprocessed");
+ });
+ it("onClick method on @state=active counter badge should add a new filter", () => {
+ validateOnClick("active");
+ });
+ it("onClick method on @state=suppressed counter badge should add a new filter", () => {
+ validateOnClick("suppressed");
+ });
+});
diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js
index fea8bcc69..419ebd4e1 100644
--- a/ui/src/Stores/AlertStore.js
+++ b/ui/src/Stores/AlertStore.js
@@ -212,7 +212,7 @@ class AlertStore {
});
});
- fetchWithThrottle = throttle(this.fetch, 500);
+ fetchWithThrottle = throttle(this.fetch, 300);
parseAPIResponse = action(result => {
if (result.error) {
@@ -220,18 +220,20 @@ class AlertStore {
return;
}
- const queryFilters = new Set(
- this.filters.values
- .map(f => f.raw)
- .slice()
- .sort()
- );
- const responseFilters = new Set(result.filters.map(m => m.text).sort());
- if (
- JSON.stringify([...queryFilters]) !== JSON.stringify([...responseFilters])
- ) {
+ const queryFilters = [
+ ...new Set(
+ this.filters.values
+ .map(f => f.raw)
+ .slice()
+ .sort()
+ )
+ ];
+ const responseFilters = [
+ ...new Set(result.filters.map(m => m.text).sort())
+ ];
+ if (JSON.stringify(queryFilters) !== JSON.stringify(responseFilters)) {
console.info(
- `Got response with filters=${responseFilters} while expecting results for ${queryFilters}, ignoring`
+ `Got response with filters '${responseFilters}' while expecting results for '${queryFilters}', ignoring`
);
return;
}
diff --git a/ui/src/setupTests.js b/ui/src/setupTests.js
new file mode 100644
index 000000000..5b9840092
--- /dev/null
+++ b/ui/src/setupTests.js
@@ -0,0 +1 @@
+require("jest-localstorage-mock");