diff --git a/ui/package-lock.json b/ui/package-lock.json
index 4e0401444..ace7fd269 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -3100,6 +3100,40 @@
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
},
+ "diffable-html": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/diffable-html/-/diffable-html-3.0.0.tgz",
+ "integrity": "sha512-lUxHiU00DexR/wKcY56OiJZmB0D66ghidYfU4VxUMG09TDx+1jjO7/dFrZKI2p9z00tWY/7ZeO9BBEi6n0jUYQ==",
+ "dev": true,
+ "requires": {
+ "htmlparser2": "3.9.2"
+ },
+ "dependencies": {
+ "domhandler": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+ "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+ "dev": true,
+ "requires": {
+ "domelementtype": "1.3.0"
+ }
+ },
+ "htmlparser2": {
+ "version": "3.9.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+ "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+ "dev": true,
+ "requires": {
+ "domelementtype": "1.3.0",
+ "domhandler": "2.4.2",
+ "domutils": "1.5.1",
+ "entities": "1.1.1",
+ "inherits": "2.0.3",
+ "readable-stream": "2.3.6"
+ }
+ }
+ }
+ },
"diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -6534,6 +6568,12 @@
"pretty-format": "20.0.3"
}
},
+ "jest-date-mock": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/jest-date-mock/-/jest-date-mock-1.0.3.tgz",
+ "integrity": "sha512-PLwqL0KI+zDKc6SoytvApudwFD8uDLOM7Bf4Z5C3KpJrHDJv5RawgSZUQOUqSukQ+TOhdHzliUOvGtE3aA+fWA==",
+ "dev": true
+ },
"jest-diff": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-20.0.3.tgz",
diff --git a/ui/package.json b/ui/package.json
index cc5f4caac..3a20f3b8f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -51,11 +51,13 @@
"watch-css": "npm run build-css && node_modules/.bin/node-sass-chokidar src/ -o src/ --watch --recursive"
},
"devDependencies": {
+ "diffable-html": "3.0.0",
"enzyme": "3.5.0",
"enzyme-adapter-react-16": "1.3.1",
"enzyme-to-json": "3.3.4",
"eslint-plugin-react": "7.11.1",
"jest-canvas-mock": "1.1.0",
+ "jest-date-mock": "1.0.3",
"jest-fetch-mock": "1.6.5",
"jest-localstorage-mock": "2.2.0",
"jest-mock-console": "0.4.0",
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap
new file mode 100644
index 000000000..2c7daac4d
--- /dev/null
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/__snapshots__/index.test.js.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot when data is not present in alertStore 1`] = `
+"
+
+
+ Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179
+
+
+"
+`;
+
+exports[` matches snapshot when data is present in alertStore 1`] = `
+"
+
+
+
+ Fake silence
+
+
+
+
+"
+`;
+
+exports[` matches snapshot with expaned details 1`] = `
+"
+
+
+
+ Fake silence
+
+
+
+
+
+"
+`;
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
index e546a1280..a265583e9 100644
--- a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.js
@@ -228,4 +228,4 @@ const Silence = inject("alertStore")(
)
);
-export { Silence };
+export { Silence, SilenceDetails, SilenceExpiryBadgeWithProgress };
diff --git a/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
new file mode 100644
index 000000000..56ddd651b
--- /dev/null
+++ b/ui/src/Components/Grid/AlertGrid/AlertGroup/Silence/index.test.js
@@ -0,0 +1,194 @@
+import React from "react";
+
+import { Provider } from "mobx-react";
+
+import { mount, shallow } from "enzyme";
+
+import toDiffableHtml from "diffable-html";
+
+import { advanceTo, clear } from "jest-date-mock";
+
+import { AlertStore } from "Stores/AlertStore";
+import { Silence, SilenceDetails, SilenceExpiryBadgeWithProgress } from ".";
+
+const mockAfterUpdate = jest.fn();
+
+const alertmanager = {
+ name: "default",
+ uri: "file:///mock",
+ state: "suppressed",
+ startsAt: "2000-01-01T10:00:00Z",
+ endsAt: "0001-01-01T00:00:00Z",
+ source: "localhost/prometheus",
+ silencedBy: ["4cf5fd82-1edd-4169-99d1-ff8415e72179"]
+};
+
+const silence = {
+ id: "4cf5fd82-1edd-4169-99d1-ff8415e72179",
+ matchers: [
+ {
+ name: "alertname",
+ value: "MockAlert",
+ isRegex: false
+ }
+ ],
+ startsAt: "2000-01-01T10:00:00Z",
+ endsAt: "2000-01-01T20:00:00Z",
+ createdAt: "0001-01-01T00:00:00Z",
+ createdBy: "me@example.com",
+ comment: "Fake silence",
+ jiraID: "",
+ jiraURL: ""
+};
+
+let alertStore;
+
+beforeEach(() => {
+ advanceTo(new Date(2000, 0, 1, 15, 0, 0));
+ alertStore = new AlertStore([]);
+ alertStore.data.upstreams = {
+ counters: {
+ total: 1,
+ healthy: 1,
+ failed: 0
+ },
+ instances: [
+ {
+ name: "default",
+ uri: "file:///mock",
+ error: ""
+ }
+ ]
+ };
+ alertStore.data.silences = {
+ default: {
+ "4cf5fd82-1edd-4169-99d1-ff8415e72179": silence
+ }
+ };
+});
+
+afterEach(() => {
+ // reset Date() to current time
+ clear();
+});
+
+const MountedSilence = () => {
+ return mount(
+
+
+
+ );
+};
+
+describe("", () => {
+ it("matches snapshot when data is present in alertStore", () => {
+ const tree = MountedSilence().find("Silence");
+ expect(toDiffableHtml(tree.html())).toMatchSnapshot();
+ });
+
+ it("renders full silence when data is present in alertStore", () => {
+ const tree = MountedSilence().find("Silence");
+ const fallback = tree.find("FallbackSilenceDesciption");
+ expect(fallback).toHaveLength(0);
+ });
+
+ it("matches snapshot when data is not present in alertStore", () => {
+ alertStore.data.silences = {};
+ const tree = MountedSilence().find("Silence");
+ expect(toDiffableHtml(tree.html())).toMatchSnapshot();
+ });
+
+ it("renders FallbackSilenceDesciption when Alertmanager data is not present in alertStore", () => {
+ alertStore.data.silences = {};
+ const tree = MountedSilence();
+ const fallback = tree.find("FallbackSilenceDesciption");
+ expect(fallback).toHaveLength(1);
+ expect(tree.text()).toBe(
+ "Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179"
+ );
+ });
+
+ it("renders FallbackSilenceDesciption when silence data is not present in alertStore", () => {
+ alertStore.data.silences.default = {};
+ const tree = MountedSilence();
+ const fallback = tree.find("FallbackSilenceDesciption");
+ expect(fallback).toHaveLength(1);
+ expect(tree.text()).toBe(
+ "Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179"
+ );
+ });
+
+ it("clicking on expand toggle shows silence details", () => {
+ const tree = MountedSilence();
+ const toggle = tree.find("a.float-right.cursor-pointer");
+ toggle.simulate("click");
+ const details = tree.find("SilenceDetails");
+ expect(details).toHaveLength(1);
+ });
+
+ it("matches snapshot with expaned details", () => {
+ const tree = MountedSilence().find("Silence");
+ tree.instance().collapse.toggle();
+ expect(toDiffableHtml(tree.html())).toMatchSnapshot();
+ });
+
+ it("renders comment as link when jiraURL is set", () => {
+ alertStore.data.silences.default[silence.id].jiraURL =
+ "http://jira.example.com";
+ const tree = MountedSilence().find("Silence");
+ const link = tree.find("a[href='http://jira.example.com']");
+ expect(link).toHaveLength(1);
+ expect(link.text()).toBe("Fake silence");
+ });
+});
+
+const ShallowSilenceDetails = () => {
+ return shallow(
+
+ );
+};
+
+describe("", () => {
+ it("unexpired silence endsAt label uses 'secondary' class", () => {
+ const tree = ShallowSilenceDetails();
+ const endsAt = tree.find("span.badge").at(1);
+ expect(endsAt.html()).toMatch(/badge-secondary/);
+ });
+
+ it("expired silence endsAt label uses 'danger' class", () => {
+ advanceTo(new Date(2000, 0, 1, 23, 0, 0));
+ const tree = ShallowSilenceDetails();
+ const endsAt = tree.find("span.badge").at(1);
+ expect(endsAt.html()).toMatch(/badge-danger/);
+ });
+});
+
+const ShallowSilenceExpiryBadgeWithProgress = () => {
+ return shallow();
+};
+
+describe("", () => {
+ it("renders with class 'danger' and no progressbar when expired", () => {
+ advanceTo(new Date(2001, 0, 1, 23, 0, 0));
+ const tree = ShallowSilenceExpiryBadgeWithProgress();
+ expect(tree.html()).toMatch(/badge-danger/);
+ expect(tree.text()).toBe("Expired ");
+ });
+
+ it("progressbar uses class 'danger' when > 90%", () => {
+ advanceTo(new Date(2000, 0, 1, 19, 30, 0));
+ const tree = ShallowSilenceExpiryBadgeWithProgress();
+ expect(tree.html()).toMatch(/progress-bar bg-danger/);
+ });
+
+ it("progressbar uses class 'danger' when > 75%", () => {
+ advanceTo(new Date(2000, 0, 1, 17, 45, 0));
+ const tree = ShallowSilenceExpiryBadgeWithProgress();
+ expect(tree.html()).toMatch(/progress-bar bg-warning/);
+ });
+});
diff --git a/ui/src/setupTests.js b/ui/src/setupTests.js
index a18bc9688..da247227d 100644
--- a/ui/src/setupTests.js
+++ b/ui/src/setupTests.js
@@ -15,6 +15,9 @@ require("jest-localstorage-mock");
// favico.js needs canvas
require("jest-canvas-mock");
+// used to mock current time since we render moment.fromNow() in some places
+require("jest-date-mock");
+
// fetch is used in multiple places to interact with Go backend
// or upstream Alertmanager API
global.fetch = require("jest-fetch-mock");