diff --git a/ui/src/Components/MainModal/Help.test.js b/ui/src/Components/MainModal/Help.test.js
new file mode 100644
index 000000000..6baa684ae
--- /dev/null
+++ b/ui/src/Components/MainModal/Help.test.js
@@ -0,0 +1,11 @@
+import React from "react";
+import renderer from "react-test-renderer";
+
+import { Help } from "./Help";
+
+describe("", () => {
+ it("matches snapshot", () => {
+ const tree = renderer.create().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/ui/src/Components/MainModal/MainModalContent.test.js b/ui/src/Components/MainModal/MainModalContent.test.js
new file mode 100644
index 000000000..3c17fd0b9
--- /dev/null
+++ b/ui/src/Components/MainModal/MainModalContent.test.js
@@ -0,0 +1,80 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import renderer from "react-test-renderer";
+
+import { AlertStore } from "Stores/AlertStore";
+import { Settings } from "Stores/Settings";
+import { MainModalContent } from "./MainModalContent";
+
+let alertStore;
+let settingsStore;
+const onHide = jest.fn();
+
+beforeAll(() => {
+ // modal renders into document.body using portals, but that isn't working
+ // out of the box with react-test-renderer, a workaround is needed based on
+ // https://github.com/facebook/react/issues/11565#issuecomment-380143358
+ ReactDOM.createPortal = jest.fn((element, node) => {
+ return element;
+ });
+});
+
+beforeEach(() => {
+ alertStore = new AlertStore([]);
+ settingsStore = new Settings();
+ onHide.mockClear();
+});
+
+afterEach(() => {
+ // https://github.com/facebook/react/issues/11565#issuecomment-380143358
+ ReactDOM.createPortal.mockClear();
+});
+
+const FakeModal = () => {
+ return renderer.create(
+
+ );
+};
+
+const ValidateSetTab = (title, callArg) => {
+ const component = FakeModal();
+ const instance = component.getInstance();
+ const setTabSpy = jest.spyOn(instance.tab, "setTab");
+
+ const helpTab = component.root.findByProps({ title: title });
+ helpTab.props.onClick();
+
+ expect(setTabSpy).toHaveBeenCalledWith(callArg);
+};
+
+describe("", () => {
+ it("matches snapshot", () => {
+ const tree = FakeModal().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it("shows 'Configuration' tab by default", () => {
+ const component = FakeModal();
+ const tabs = component.root.findAll(testInstance => {
+ if (!testInstance.props.className) return false;
+ const classNames = testInstance.props.className.split(" ");
+ return classNames.includes("nav-link") && classNames.includes("active");
+ });
+ expect(tabs).toHaveLength(1);
+ expect(tabs[0].children).toContain("Configuration");
+ });
+
+ // modal makes it tricky to verify re-rendered content, so only check if we
+ // update the store for now
+ it("calls setTab('configuration') after clicking on the 'Configuration' tab", () => {
+ ValidateSetTab("Configuration", "configuration");
+ });
+
+ it("calls setTab('help') after clicking on the 'Help' tab", () => {
+ ValidateSetTab("Help", "help");
+ });
+});
diff --git a/ui/src/Components/MainModal/__snapshots__/Help.test.js.snap b/ui/src/Components/MainModal/__snapshots__/Help.test.js.snap
new file mode 100644
index 000000000..f2742e4aa
--- /dev/null
+++ b/ui/src/Components/MainModal/__snapshots__/Help.test.js.snap
@@ -0,0 +1,879 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot 1`] = `
+
+
+ Filter operators
+
+
+ -
+
+ =
+
+
+ Exact match
+
+ -
+
+ Example:
+
+ key
+ =
+ value
+
+
+
+ True if compared alert attribute value is equal to
+
+ value
+
+ .
+
+
+ -
+
+ !=
+
+
+ Negative match
+
+ -
+
+ Example:
+
+ key
+ !=
+ value
+
+
+
+ True if compared alert attribute is missing or have a value that is not equal to
+
+ value
+
+ .
+
+
+ -
+
+ =~
+
+
+ Regular expression match
+
+ -
+
+ Example:
+
+ key
+ =~
+ value
+
+
+
+ True if compared alert attribute value matches
+
+ value
+
+ regex.
+
+
+ -
+
+ !~
+
+
+ Negative regular expression match
+
+ -
+
+ Example:
+
+ key
+ !~
+ value
+
+
+
+ False if compared alert attribute value matches
+
+ value
+
+
+ regex.
+
+
+ -
+
+ >
+
+
+ Greater than match
+
+ -
+
+ Example:
+
+ key
+ >
+ value
+
+
+
+ True if compared alert attribute value is greater than
+
+
+ value
+
+ .
+
+
+ -
+
+ <
+
+
+ Less than match
+
+ -
+
+ Example:
+
+ key
+ <
+ value
+
+
+
+ True if compared alert attribue value is less than
+
+ value
+
+ .
+
+
+
+
+
+ Filtering using alert labels
+
+
+ -
+ Match alerts based on any label
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+ =~
+
+
+ !~
+
+
+ >
+
+
+ <
+
+
+
+ Examples:
+
+
+ -
+
+
+ alertname=UnableToPing
+
+
+
+ Match alerts with label
+
+ alertname
+
+ equal to
+
+
+ UnableToPing
+
+ .
+
+
+ -
+
+
+ hostname=localhost
+
+
+
+ Match alerts with label
+
+ hostname
+
+ equal to
+
+
+ localhost
+
+ .
+
+
+ -
+
+
+ service!=apache3
+
+
+
+ Match alerts with label
+
+ service
+
+ missing or not equal to
+
+
+ apache3
+
+ .
+
+
+ -
+
+
+ service=~apache
+
+
+
+ Match alerts with label
+
+ service
+
+ matching regular expression
+
+ /.*apache.*/
+
+ .
+
+
+ -
+
+
+ service=~apache[1-3]
+
+
+
+ Match alerts with label
+
+ service
+
+ matching regular expression
+
+ /.*apache[1-3].*/
+
+ .
+
+
+ -
+
+
+ priority>4
+
+
+
+ Match alerts with label
+
+ priority
+
+ value
+
+
+ >
+
+ than
+
+ 4
+
+ . Value will be casted to integer if possible, string comparision will be used as fallback.
+
+
+
+
+
+
+
+
+ Filtering alerts using special filters
+
+
+ -
+ Match alerts based on the Alertmanager instance name they were collected from
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+ =~
+
+
+ !~
+
+
+
+ Examples:
+
+
+ -
+
+
+ @alertmanager=prod
+
+
+
+ Match alerts collected from Alertmanager instance named
+
+
+ prod
+
+ .
+
+
+ -
+
+
+ @alertmanager!=dev
+
+
+
+ Match alerts collected from Alertmanager instances except for the one named
+
+ dev
+
+ .
+
+
+ -
+
+
+ @alertmanager=~prod
+
+
+
+ Match alerts collected from Alertmanager instances with names matching regular expression
+
+ /.*prod.*/
+
+ .
+
+
+
+
+ -
+ Match alerts based on the receiver name
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+ =~
+
+
+ !~
+
+
+
+ Examples:
+
+
+ -
+
+
+ @receiver=default
+
+
+
+ Match alerts sent to the default receiver.
+
+
+ -
+
+
+ @receiver!=hipchat
+
+
+
+ Match alerts not sent to the hipchat receiver.
+
+
+ -
+
+
+ @receiver=~email
+
+
+
+ Match alerts sent to any receiver with name matching regular expression
+
+ /.*email.*/
+
+ .
+
+
+
+
+ -
+ Match alerts based on the state
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+
+ Examples:
+
+
+ -
+
+
+ @state=active
+
+
+
+ Match only active alerts.
+
+
+ -
+
+
+ @state!=active
+
+
+
+ Match alerts that are not active, only suppressed and unprocessed will be matched.
+
+
+ -
+
+
+ @state=suppressed
+
+
+
+ Match only suppressed alerts.
+
+
+ -
+
+
+ @state=unprocessed
+
+
+
+ Match only unprocessed alerts.
+
+
+
+
+ -
+ Match alerts based on the author of silence
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+ =~
+
+
+ !~
+
+
+
+ Examples:
+
+
+ -
+
+
+ @silence_author=me@example.com
+
+
+
+ Match alerts silenced by
+
+ me@example.com
+
+ .
+
+
+ -
+
+
+ @silence_author!=me@example.com
+
+
+
+ Match alerts silenced by everyone except
+
+
+ foo@example.com
+
+ .
+
+
+ -
+
+
+ @silence_author=~@example.com
+
+
+
+ Match alerts silenced by author matching regular expression
+
+
+ /.*@example.com.*/
+
+ .
+
+
+
+
+ -
+ Match alerts based on the jira linked in the silence
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+ !=
+
+
+ =~
+
+
+ !~
+
+
+
+ Examples:
+
+
+
+ This is supported only if JIRA regexp are enabled and able to match JIRA ids in the silence comment body.
+
+ -
+
+
+ @silence_jira=PROJECT-123
+
+
+
+ Match silenced alerts where detected JIRA issue id is equal to
+
+
+ PROJECT-123
+
+ .
+
+
+ -
+
+
+ @silence_jira!=PROJECT-123
+
+
+
+ Match silenced alerts where detected JIRA issue id is different than
+
+
+ PROJECT-123
+
+ .
+
+
+ -
+
+
+ @silence_jira=~PROJECT
+
+
+
+ Match silenced alerts where detected JIRA issue id matches regular expression
+
+ /.*PROJECT.*/
+
+ .
+
+
+
+
+ -
+ Limit number of displayed alerts
+
+ -
+
+ Supported operators:
+
+
+ =
+
+
+
+ Examples:
+
+
+
+ Value must be a number >= 1.
+
+ -
+
+
+ @limit=10
+
+
+
+ Limit number of displayed alerts to 10.
+
+
+
+
+ -
+ Match alerts based on creation timestamp
+
+ -
+
+ Supported operators:
+
+
+ >
+
+
+ <
+
+
+
+ Examples:
+
+
+ -
+
+
+ @age>15m
+
+
+
+ Match alerts older than 15 minutes.
+
+
+ -
+
+
+ @age>1h
+
+
+
+ Match alerts older than 1 hour.
+
+
+ -
+
+
+ @age<10h30m
+
+
+
+ Match alerts more recent than 10 hours and 30 minutes.
+
+
+
+
+
+
+
+`;
diff --git a/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap
new file mode 100644
index 000000000..bb0db1bde
--- /dev/null
+++ b/ui/src/Components/MainModal/__snapshots__/MainModalContent.test.js.snap
@@ -0,0 +1,236 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches snapshot 1`] = `
+
+
+
+
+
+
+
+ Version:
+ unknown
+
+
+
+
+
+`;
diff --git a/ui/src/Components/MainModal/index.test.js b/ui/src/Components/MainModal/index.test.js
new file mode 100644
index 000000000..2e19b794b
--- /dev/null
+++ b/ui/src/Components/MainModal/index.test.js
@@ -0,0 +1,54 @@
+import React from "react";
+import sd from "skin-deep";
+
+import { AlertStore } from "Stores/AlertStore";
+import { Settings } from "Stores/Settings";
+import { MainModal } from ".";
+
+let alertStore;
+let settingsStore;
+
+beforeEach(() => {
+ alertStore = new AlertStore([]);
+ settingsStore = new Settings();
+});
+
+const RenderMainModal = () => {
+ return sd.shallowRender(
+
+ );
+};
+
+describe("", () => {
+ it("only renders FontAwesomeIcon when modal is not shown", () => {
+ const tree = RenderMainModal();
+ // is how React.Fragment gets rendered
+ expect(tree.text()).toBe("");
+ });
+
+ it("renders the modal when it is shown", () => {
+ const tree = RenderMainModal();
+ const instance = tree.getMountedInstance();
+ instance.toggle.toggle();
+ // is how React.Fragment gets rendered
+ expect(tree.text()).toBe(
+ ""
+ );
+ });
+
+ it("hides the modal when toggle() is called twice", () => {
+ const tree = RenderMainModal();
+ const instance = tree.getMountedInstance();
+ instance.toggle.toggle();
+ instance.toggle.toggle();
+ expect(tree.text()).toBe("");
+ });
+
+ it("hides the modal when hide() is called", () => {
+ const tree = RenderMainModal();
+ const instance = tree.getMountedInstance();
+ instance.toggle.show = true;
+ instance.toggle.hide();
+ expect(tree.text()).toBe("");
+ });
+});