Merge pull request #84 from prymitive/grid-tests

Alert grid components test coverage
This commit is contained in:
Łukasz Mierzwa
2018-09-02 09:13:00 +01:00
committed by GitHub
25 changed files with 1282 additions and 39 deletions

View File

@@ -76,6 +76,7 @@ clean:
run: $(NAME)
ALERTMANAGER_INTERVAL=36000h \
ALERTMANAGER_URI=$(ALERTMANAGER_URI) \
ANNOTATIONS_HIDDEN="help" \
LABELS_COLOR_UNIQUE="@receiver instance cluster" \
LABELS_COLOR_STATIC="job" \
FILTERS_DEFAULT="@state=active @receiver=by-cluster-service" \
@@ -94,6 +95,7 @@ run-docker: docker-image
-v $(MOCK_PATH):$(MOCK_PATH) \
-e ALERTMANAGER_INTERVAL=36000h \
-e ALERTMANAGER_URI=$(ALERTMANAGER_URI) \
-e ANNOTATIONS_HIDDEN="help" \
-e LABELS_COLOR_UNIQUE="instance cluster" \
-e LABELS_COLOR_STATIC="job" \
-e FILTERS_DEFAULT="@state=active @receiver=by-cluster-service" \

View File

@@ -0,0 +1,6 @@
// mock copy-to-clipboard since it throws errors in tests
// and we don't really need to copy anything, only ensure we're calling it
const copy = jest.fn();
export default copy;

40
ui/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Alert /> matches snapshot with showAlertmanagers=false showReceiver=false 1`] = `
"
<li class=\\"components-grid-alertgrid-alertgroup-alert list-group-item pl-1 pr-0 py-0 my-1 rounded-0 border-left-1 border-right-0 border-top-0 border-bottom-0 border-danger\\">
<div class=\\"mb-1\\">
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-minus\\"
class=\\"svg-inline--fa fa-search-minus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
<span class=\\"text-muted\\">
help:
</span>
<span class=\\"Linkify\\">
some long text
</span>
</div>
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-plus\\"
class=\\"svg-inline--fa fa-search-plus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
hidden
</div>
</div>
<span class=\\"text-nowrap text-truncate px-1 mr-1 badge badge-secondary\\">
<time datetime=\\"1534268200017\\">
a day ago
</time>
</span>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
job: node_exporter
</span>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
cluster: dev
</span>
<a href=\\"http://localhost\\"
target=\\"_blank\\"
rel=\\"noopener noreferrer\\"
class=\\"text-nowrap text-truncate badge badge-secondary mr-1\\"
>
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"external-link-alt\\"
class=\\"svg-inline--fa fa-external-link-alt fa-w-18 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 576 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M576 24v127.984c0 21.461-25.96 31.98-40.971 16.971l-35.707-35.709-243.523 243.523c-9.373 9.373-24.568 9.373-33.941 0l-22.627-22.627c-9.373-9.373-9.373-24.569 0-33.941L442.756 76.676l-35.703-35.705C391.982 25.9 402.656 0 424.024 0H552c13.255 0 24 10.745 24 24zM407.029 270.794l-16 16A23.999 23.999 0 0 0 384 303.765V448H64V128h264a24.003 24.003 0 0 0 16.97-7.029l16-16C376.089 89.851 365.381 64 344 64H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V287.764c0-21.382-25.852-32.09-40.971-16.97z\\"
>
</path>
</svg>
link
</a>
</li>
"
`;

View File

@@ -50,6 +50,7 @@ const Alert = observer(
key={a.name}
name={a.name}
value={a.value}
visible={a.visible}
afterUpdate={afterUpdate}
/>
))}

View File

@@ -0,0 +1,87 @@
import React from "react";
import { Provider } from "mobx-react";
import { mount } from "enzyme";
import { advanceTo, clear } from "jest-date-mock";
import toDiffableHtml from "diffable-html";
import { MockAlert, MockAnnotation } from "__mocks__/Alerts.js";
import { AlertStore } from "Stores/AlertStore";
import { Alert } from ".";
let alertStore;
beforeEach(() => {
advanceTo(new Date(2018, 7, 15, 20, 40, 0));
alertStore = new AlertStore([]);
});
afterEach(() => {
// reset Date() to current time
clear();
});
const MockAfterUpdate = jest.fn();
const MockedAlert = () => {
return MockAlert(
[
MockAnnotation("help", "some long text", true, false),
MockAnnotation("hidden", "some hidden text", false, false),
MockAnnotation("link", "http://localhost", true, true)
],
{ job: "node_exporter", cluster: "dev" },
"active"
);
};
const MountedAlert = (alert, showAlertmanagers, showReceiver) => {
return mount(
<Provider alertStore={alertStore}>
<Alert
alert={alert}
showAlertmanagers={showAlertmanagers}
showReceiver={showReceiver}
afterUpdate={MockAfterUpdate}
/>
</Provider>
);
};
describe("<Alert />", () => {
it("matches snapshot with showAlertmanagers=false showReceiver=false", () => {
const alert = MockedAlert();
const tree = MountedAlert(alert, false, false);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("renders @alertmanager label with showAlertmanagers=true", () => {
const alert = MockedAlert();
const tree = MountedAlert(alert, true, false);
const label = tree
.find("FilteringLabel")
.filterWhere(elem => elem.props().name === "@alertmanager");
expect(label.text()).toBe("@alertmanager: default");
});
it("renders @receiver label with showReceiver=true", () => {
const alert = MockedAlert();
const tree = MountedAlert(alert, false, true);
const label = tree
.find("FilteringLabel")
.filterWhere(elem => elem.props().name === "@receiver");
expect(label.text()).toBe("@receiver: by-name");
});
it("renders a silence if alert is silenced", () => {
const alert = MockedAlert();
alert.alertmanager[0].silencedBy = ["silence123456789"];
const tree = MountedAlert(alert, false, false);
const silence = tree.find("Silence");
expect(silence).toHaveLength(1);
expect(silence.html()).toMatch(/silence123456789/);
});
});

View File

@@ -0,0 +1,90 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RenderLinkAnnotation /> matches snapshot 1`] = `
<a
className="text-nowrap text-truncate badge badge-secondary mr-1"
href="http://localhost/foo"
key="annotation name"
rel="noopener noreferrer"
target="_blank"
>
<FontAwesomeIcon
border={false}
className=""
fixedWidth={false}
flip={null}
icon={
Object {
"icon": Array [
576,
512,
Array [],
"f35d",
"M576 24v127.984c0 21.461-25.96 31.98-40.971 16.971l-35.707-35.709-243.523 243.523c-9.373 9.373-24.568 9.373-33.941 0l-22.627-22.627c-9.373-9.373-9.373-24.569 0-33.941L442.756 76.676l-35.703-35.705C391.982 25.9 402.656 0 424.024 0H552c13.255 0 24 10.745 24 24zM407.029 270.794l-16 16A23.999 23.999 0 0 0 384 303.765V448H64V128h264a24.003 24.003 0 0 0 16.97-7.029l16-16C376.089 89.851 365.381 64 344 64H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V287.764c0-21.382-25.852-32.09-40.971-16.97z",
],
"iconName": "external-link-alt",
"prefix": "fas",
}
}
inverse={false}
listItem={false}
mask={null}
pull={null}
pulse={false}
rotation={null}
size={null}
spin={false}
symbol={false}
transform={null}
/>
annotation name
</a>
`;
exports[`<RenderNonLinkAnnotation /> matches snapshot when visible=false 1`] = `
"
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-plus\\"
class=\\"svg-inline--fa fa-search-plus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
foo
</div>
"
`;
exports[`<RenderNonLinkAnnotation /> matches snapshot when visible=true 1`] = `
"
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-minus\\"
class=\\"svg-inline--fa fa-search-minus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
<span class=\\"text-muted\\">
foo:
</span>
<span class=\\"Linkify\\">
some long text
</span>
</div>
"
`;

View File

@@ -1,7 +1,7 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { observable, action, toJS } from "mobx";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import Linkify from "react-linkify";
@@ -20,6 +20,7 @@ const RenderNonLinkAnnotation = inject("alertStore")(
alertStore: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired,
afterUpdate: PropTypes.func.isRequired
};
@@ -45,7 +46,7 @@ const RenderNonLinkAnnotation = inject("alertStore")(
constructor(props) {
super(props);
this.toggle.visible = this.isVisible();
this.toggle.visible = props.visible;
}
componentDidUpdate() {
@@ -54,34 +55,6 @@ const RenderNonLinkAnnotation = inject("alertStore")(
afterUpdate();
}
// determinate if this annotation should be hidden by default or not
isVisible() {
const { alertStore, name } = this.props;
const annotationsHidden = toJS(
alertStore.settings.values.annotationsHidden
);
const isInHidden =
annotationsHidden !== null && annotationsHidden.indexOf(name) >= 0;
const annotationsVisible = toJS(
alertStore.settings.values.annotationsVisible
);
const isInVisible =
annotationsVisible !== null && annotationsVisible.indexOf(name) >= 0;
if (isInVisible) return true;
if (
toJS(alertStore.settings.values.annotationsDefaultHidden) === true ||
isInHidden === true
) {
return false;
}
return true;
}
render() {
const { name, value } = this.props;

View File

@@ -0,0 +1,100 @@
import React from "react";
import { shallow, mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { AlertStore } from "Stores/AlertStore";
import { RenderNonLinkAnnotation, RenderLinkAnnotation } from ".";
let alertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
});
const ShallowLinkAnnotation = () => {
return shallow(
<RenderLinkAnnotation name="annotation name" value="http://localhost/foo" />
);
};
describe("<RenderLinkAnnotation />", () => {
it("matches snapshot", () => {
const tree = ShallowLinkAnnotation();
expect(tree).toMatchSnapshot();
});
it("contains a link", () => {
const tree = ShallowLinkAnnotation();
const link = tree.find("a[href='http://localhost/foo']");
expect(link).toHaveLength(1);
expect(link.text()).toMatch(/annotation name/);
});
});
const MockAfterUpdate = jest.fn();
const ShallowNonLinkAnnotation = visible => {
return shallow(
<RenderNonLinkAnnotation
alertStore={alertStore}
name="foo"
value="some long text"
visible={visible}
afterUpdate={MockAfterUpdate}
/>
);
};
const MountedNonLinkAnnotation = visible => {
return mount(
<RenderNonLinkAnnotation
alertStore={alertStore}
name="foo"
value="some long text"
visible={visible}
afterUpdate={MockAfterUpdate}
/>
);
};
describe("<RenderNonLinkAnnotation />", () => {
it("matches snapshot when visible=true", () => {
const tree = ShallowNonLinkAnnotation(true);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("contains value when visible=true", () => {
const tree = ShallowNonLinkAnnotation(true);
expect(tree.html()).toMatch(/some long text/);
});
it("matches snapshot when visible=false", () => {
const tree = ShallowNonLinkAnnotation(false);
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
it("doesn't contain value when visible=false", () => {
const tree = ShallowNonLinkAnnotation(false);
expect(tree.html()).not.toMatch(/some long text/);
});
it("clicking on + icon hides the value", () => {
const tree = MountedNonLinkAnnotation(true);
expect(tree.html()).toMatch(/fa-search-minus/);
expect(tree.html()).toMatch(/some long text/);
tree.find("div").simulate("click");
expect(tree.html()).toMatch(/fa-search-plus/);
expect(tree.html()).not.toMatch(/some long text/);
});
it("clicking on - icon shows the value", () => {
const tree = MountedNonLinkAnnotation(false);
expect(tree.html()).toMatch(/fa-search-plus/);
expect(tree.html()).not.toMatch(/some long text/);
tree.find("div").simulate("click");
expect(tree.html()).toMatch(/fa-search-minus/);
expect(tree.html()).toMatch(/some long text/);
});
});

View File

@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<GroupFooter /> matches snapshot 1`] = `
"
<div class=\\"card-footer px-2 py-1\\">
<div class=\\"mb-1\\">
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-minus\\"
class=\\"svg-inline--fa fa-search-minus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
<span class=\\"text-muted\\">
summary:
</span>
<span class=\\"Linkify\\">
This is summary
</span>
</div>
<div class=\\"mr-1 mb-1 p-1 bg-light cursor-pointer d-inline-block rounded components-grid-annotation\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"search-plus\\"
class=\\"svg-inline--fa fa-search-plus fa-w-16 mr-1\\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 512 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z\\"
>
</path>
</svg>
hidden
</div>
</div>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
label1: foo
</span>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
label2: bar
</span>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
@alertmanager: default
</span>
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
@receiver: by-name
</span>
<a href=\\"http://link.example.com\\"
target=\\"_blank\\"
rel=\\"noopener noreferrer\\"
class=\\"text-nowrap text-truncate badge badge-secondary mr-1\\"
>
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"external-link-alt\\"
class=\\"svg-inline--fa fa-external-link-alt fa-w-18 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 576 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M576 24v127.984c0 21.461-25.96 31.98-40.971 16.971l-35.707-35.709-243.523 243.523c-9.373 9.373-24.568 9.373-33.941 0l-22.627-22.627c-9.373-9.373-9.373-24.569 0-33.941L442.756 76.676l-35.703-35.705C391.982 25.9 402.656 0 424.024 0H552c13.255 0 24 10.745 24 24zM407.029 270.794l-16 16A23.999 23.999 0 0 0 384 303.765V448H64V128h264a24.003 24.003 0 0 0 16.97-7.029l16-16C376.089 89.851 365.381 64 344 64H48C21.49 64 0 85.49 0 112v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V287.764c0-21.382-25.852-32.09-40.971-16.97z\\"
>
</path>
</svg>
link
</a>
</div>
"
`;

View File

@@ -28,6 +28,7 @@ const GroupFooter = observer(
key={a.name}
name={a.name}
value={a.value}
visible={a.visible}
afterUpdate={afterUpdate}
/>
))}

View File

@@ -0,0 +1,54 @@
import React from "react";
import { Provider } from "mobx-react";
import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { MockAlertGroup, MockAnnotation } from "__mocks__/Alerts.js";
import { AlertStore } from "Stores/AlertStore";
import { GroupFooter } from ".";
let group;
let alertStore;
const MockGroup = () => {
const group = MockAlertGroup(
{ alertname: "Fake Alert" },
[],
[
MockAnnotation("summary", "This is summary", true, false),
MockAnnotation("hidden", "This is hidden annotation", false, false),
MockAnnotation("link", "http://link.example.com", true, true)
],
{ label1: "foo", label2: "bar" }
);
return group;
};
const MockAfterUpdate = jest.fn();
beforeEach(() => {
alertStore = new AlertStore([]);
group = MockGroup();
});
const MountedGroupFooter = () => {
return mount(
<Provider alertStore={alertStore}>
<GroupFooter
group={group}
alertmanagers={["default"]}
afterUpdate={MockAfterUpdate}
/>
</Provider>
);
};
describe("<GroupFooter />", () => {
it("matches snapshot", () => {
const tree = MountedGroupFooter().find("GroupFooter");
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
});

View File

@@ -150,4 +150,4 @@ const GroupMenu = observer(
}
);
export { GroupMenu };
export { GroupMenu, MenuContent };

View File

@@ -0,0 +1,81 @@
import React from "react";
import { mount } from "enzyme";
import copy from "copy-to-clipboard";
import { MockAlertGroup } from "__mocks__/Alerts.js";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { GroupMenu, MenuContent } from "./GroupMenu";
let silenceFormStore;
beforeEach(() => {
silenceFormStore = new SilenceFormStore();
});
const MockAfterClick = jest.fn();
const MountedGroupMenu = group => {
return mount(<GroupMenu group={group} silenceFormStore={silenceFormStore} />);
};
describe("<GroupMenu />", () => {
it("is collapsed by default", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const tree = MountedGroupMenu(group);
expect(tree.instance().collapse.value).toBe(true);
});
it("clicking toggle sets collapse value to 'false'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const tree = MountedGroupMenu(group);
const toggle = tree.find("a.cursor-pointer");
toggle.simulate("click");
expect(tree.instance().collapse.value).toBe(false);
});
it("handleClickOutside() call sets collapse value to 'true'", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const tree = MountedGroupMenu(group);
const toggle = tree.find("a.cursor-pointer");
toggle.simulate("click");
expect(tree.instance().collapse.value).toBe(false);
tree.instance().handleClickOutside();
expect(tree.instance().collapse.value).toBe(true);
});
});
const MountedMenuContent = group => {
return mount(
<MenuContent
popperPlacement="top"
popperRef={null}
popperStyle={{}}
group={group}
afterClick={MockAfterClick}
silenceFormStore={silenceFormStore}
/>
);
};
describe("<MenuContent />", () => {
it("clicking on 'Copy' icon copies the link to clickboard", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const tree = MountedMenuContent(group);
const button = tree.find(".dropdown-item").at(0);
button.simulate("click");
expect(copy).toHaveBeenCalledTimes(1);
});
it("clicking on 'Silence' icon opens the silence form modal", () => {
const group = MockAlertGroup({ alertname: "Fake Alert" }, [], [], {});
const tree = MountedMenuContent(group);
const button = tree.find(".dropdown-item").at(1);
button.simulate("click");
expect(silenceFormStore.toggle.visible).toBe(true);
});
});

View File

@@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Silence /> matches snapshot when data is not present in alertStore 1`] = `
"
<div>
<small class=\\"text-muted\\">
Silenced by default/4cf5fd82-1edd-4169-99d1-ff8415e72179
</small>
</div>
"
`;
exports[`<Silence /> matches snapshot when data is present in alertStore 1`] = `
"
<div class=\\"card mt-1 border-0 p-1\\">
<div class=\\"card-text mb-0\\">
<span class=\\"text-muted my-1\\">
Fake silence
<span class=\\"blockquote-footer pt-1\\">
<a class=\\"float-right cursor-pointer\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"chevron-up\\"
class=\\"svg-inline--fa fa-chevron-up fa-w-14 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z\\"
>
</path>
</svg>
</a>
<cite class=\\"components-grid-alertgroup-silences mr-2\\">
me@example.com
</cite>
<span class=\\"badge badge-light nmb-05 text-nowrap text-truncate mw-100 align-bottom\\">
Expires
<time datetime=\\"946756800000\\">
in 5 hours
</time>
<div class=\\"progress silence-progress bg-white\\">
<div class=\\"progress-bar bg-success\\"
role=\\"progressbar\\"
style=\\"width: 50%;\\"
aria-valuenow=\\"50\\"
aria-valuemin=\\"0\\"
aria-valuemax=\\"100\\"
>
</div>
</div>
</span>
</span>
</span>
</div>
</div>
"
`;
exports[`<Silence /> matches snapshot with expaned details 1`] = `
"
<div class=\\"card mt-1 border-0 p-1\\">
<div class=\\"card-text mb-0\\">
<span class=\\"text-muted my-1\\">
Fake silence
<span class=\\"blockquote-footer pt-1\\">
<a class=\\"float-right cursor-pointer\\">
<svg aria-hidden=\\"true\\"
data-prefix=\\"fas\\"
data-icon=\\"chevron-down\\"
class=\\"svg-inline--fa fa-chevron-down fa-w-14 \\"
role=\\"img\\"
xmlns=\\"http://www.w3.org/2000/svg\\"
viewbox=\\"0 0 448 512\\"
>
<path fill=\\"currentColor\\"
d=\\"M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z\\"
>
</path>
</svg>
</a>
<cite class=\\"components-grid-alertgroup-silences mr-2\\">
me@example.com
</cite>
</span>
</span>
</div>
<div class=\\"mt-1\\">
<span class=\\"components-label components-label-with-hover text-nowrap text-truncate badge badge-warning mw-100\\">
@alertmanager: default
</span>
<a class=\\"badge badge-secondary text-nowrap text-truncate px-1 mr-1\\"
href=\\"file:///mock/#/silences/4cf5fd82-1edd-4169-99d1-ff8415e72179\\"
target=\\"_blank\\"
rel=\\"noopener noreferrer\\"
>
4cf5fd82-1edd-4169-99d1-ff8415e72179
</a>
<span class=\\"badge badge-secondary text-nowrap text-truncate px-1 mr-1\\">
Silenced
<time datetime=\\"946720800000\\">
5 hours ago
</time>
</span>
<span class=\\"badge badge-secondary text-nowrap text-truncate px-1 mr-1\\">
Expires
<time datetime=\\"946756800000\\">
in 5 hours
</time>
</span>
<span class=\\"badge badge-success text-nowrap text-truncate px-1 mr-1\\">
alertname=MockAlert
</span>
</div>
</div>
"
`;

View File

@@ -14,7 +14,7 @@ import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalL
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { StaticLabels } from "Common/Query";
import { StaticLabels, QueryOperators } from "Common/Query";
import { FilteringLabel } from "Components/Labels/FilteringLabel";
import "./index.css";
@@ -114,7 +114,7 @@ const SilenceDetails = ({ alertmanager, silence }) => {
className="badge badge-success text-nowrap text-truncate px-1 mr-1"
>
{matcher.name}
{matcher.isRegex ? "=~" : "="}
{matcher.isRegex ? QueryOperators.Regex : QueryOperators.Equal}
{matcher.value}
</span>
))}
@@ -228,4 +228,4 @@ const Silence = inject("alertStore")(
)
);
export { Silence };
export { Silence, SilenceDetails, SilenceExpiryBadgeWithProgress };

View File

@@ -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(
<Provider alertStore={alertStore}>
<Silence
alertStore={alertStore}
alertmanager={alertmanager}
silenceID="4cf5fd82-1edd-4169-99d1-ff8415e72179"
afterUpdate={mockAfterUpdate}
/>
</Provider>
);
};
describe("<Silence />", () => {
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(
<SilenceDetails alertmanager={alertmanager} silence={silence} />
);
};
describe("<SilenceDetails />", () => {
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(<SilenceExpiryBadgeWithProgress silence={silence} />);
};
describe("<SilenceExpiryBadgeWithProgress />", () => {
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 <t />");
});
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/);
});
});

View File

@@ -0,0 +1,173 @@
import React from "react";
import { Provider } from "mobx-react";
import { mount } from "enzyme";
import moment from "moment";
import { MockAlert, MockAlertGroup } from "__mocks__/Alerts.js";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertGroup } from ".";
let alertStore;
let settingsStore;
let silenceFormStore;
let group;
const MockGroup = (groupName, alertCount) => {
const group = MockAlertGroup(
{ alertname: "Fake Alert", group: groupName },
[],
[],
{}
);
return group;
};
beforeEach(() => {
alertStore = new AlertStore([]);
settingsStore = new Settings();
silenceFormStore = new SilenceFormStore();
group = MockGroup();
});
const MockAlerts = alertCount => {
for (let i = 1; i <= alertCount; i++) {
let alert = MockAlert([], { instance: `instance${i}` });
const startsAt = moment().toISOString();
alert.startsAt = startsAt;
alert.alertmanager[0].startsAt = startsAt;
group.alerts.push(alert);
}
};
const MountedAlertGroup = (afterUpdate, showAlertmanagers) => {
return mount(
<Provider alertStore={alertStore}>
<AlertGroup
afterUpdate={afterUpdate}
group={group}
showAlertmanagers={showAlertmanagers}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
</Provider>
);
};
describe("<AlertGroup />", () => {
it("renders Alertmanager labels in footer if showAlertmanagersInFooter=true", () => {
MockAlerts(2);
const tree = MountedAlertGroup(jest.fn(), true).find("AlertGroup");
expect(tree.find("GroupFooter").html()).toMatch(/@alertmanager: default/);
});
it("only renders titlebar when collapsed", () => {
MockAlerts(10);
const tree = MountedAlertGroup(jest.fn(), false);
const alertGroup = tree.find("AlertGroup");
alertGroup.instance().collapse.toggle();
expect(alertGroup.instance().collapse.value).toBe(true);
tree.update();
expect(tree.find("Alert")).toHaveLength(0);
expect(tree.find("ul.list-group")).toHaveLength(0);
});
});
const ValidateLoadButtonPresent = (totalAlerts, isPresent) => {
MockAlerts(totalAlerts);
const tree = MountedAlertGroup(jest.fn(), false);
const buttons = tree.find("button");
expect(buttons).toHaveLength(isPresent ? 2 : 0);
};
const ValidateLoadButtonAction = (
totalAlerts,
buttonIndex,
iconMatch,
loadedAlerts,
alertsToRenderBeforeClick
) => {
MockAlerts(totalAlerts);
const tree = MountedAlertGroup(jest.fn(), false);
if (alertsToRenderBeforeClick !== undefined) {
tree
.find("AlertGroup")
.instance().renderConfig.alertsToRender = alertsToRenderBeforeClick;
tree.update();
}
const loadMore = tree.find("button").at(buttonIndex);
expect(loadMore.html()).toMatch(iconMatch);
loadMore.simulate("click");
tree.update();
expect(tree.find("Alert")).toHaveLength(loadedAlerts);
};
describe("<AlertGroup /> renderConfig", () => {
it("settingsStore.alertGroupConfig.config.defaultRenderCount should be 5 by default", () => {
expect(settingsStore.alertGroupConfig.config.defaultRenderCount).toBe(5);
});
it("renderConfig.alertsToRender should be 5 by default", () => {
const tree = MountedAlertGroup(jest.fn(), false).find("AlertGroup");
expect(tree.instance().renderConfig.alertsToRender).toBe(5);
});
it("renders only up to renderConfig.alertsToRender alerts", () => {
MockAlerts(50);
const tree = MountedAlertGroup(jest.fn(), false).find("AlertGroup");
const alerts = tree.find("Alert");
expect(alerts).toHaveLength(tree.instance().renderConfig.alertsToRender);
});
it("load buttons are not rendered for 1 alert", () => {
ValidateLoadButtonPresent(1, false);
});
it("load buttons are not rendered for 5 alerts", () => {
ValidateLoadButtonPresent(5, false);
});
it("load buttons are rendered for 6 alert", () => {
ValidateLoadButtonPresent(6, true);
});
it("clicking - icon hides 1 alert if there's 6 in total", () => {
ValidateLoadButtonAction(6, 0, /fa-minus/, 5, 6);
});
it("clicking - icon hides 1 alert if there's 6 in total and we're showing 3", () => {
ValidateLoadButtonAction(6, 0, /fa-minus/, 2, 3);
});
it("clicking - icon hides 2 alerts if there's 7 in total and we're showing 7", () => {
ValidateLoadButtonAction(7, 0, /fa-minus/, 5, 7);
});
it("clicking - icon hides 5 alerts if there's 10 in total and we're showing 10", () => {
ValidateLoadButtonAction(10, 0, /fa-minus/, 5, 10);
});
it("clicking - icon hides 5 alerts if there's 18 in total and we're showing 17", () => {
ValidateLoadButtonAction(18, 0, /fa-minus/, 12, 17);
});
it("clicking + icon loads 1 more alert if there's 6 in total", () => {
ValidateLoadButtonAction(6, 1, /fa-plus/, 6);
});
it("clicking + icon loads 4 more alert if there's 9 in total", () => {
ValidateLoadButtonAction(9, 1, /fa-plus/, 9);
});
it("clicking + icon loads 5 more alert if there's 14 in total", () => {
ValidateLoadButtonAction(14, 1, /fa-plus/, 10);
});
it("clicking + icon loads 5 more alert if there's 25 in total and we're showing 16", () => {
ValidateLoadButtonAction(25, 1, /fa-plus/, 22, 17);
});
});

View File

@@ -82,9 +82,7 @@ const AlertGrid = observer(
ref={this.storeMasonryRef}
pack={true}
sizes={GridSizesConfig}
loadMore={() => {
this.loadMore();
}}
loadMore={this.loadMore}
hasMore={
this.groupsToRender.value <
Object.keys(alertStore.data.groups).length

View File

@@ -0,0 +1,98 @@
import React from "react";
import { shallow } from "enzyme";
import { MockAlert, MockAlertGroup } from "__mocks__/Alerts.js";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertGrid } from ".";
let alertStore;
let settingsStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
settingsStore = new Settings();
silenceFormStore = new SilenceFormStore();
});
const ShallowAlertGrid = () => {
return shallow(
<AlertGrid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
);
};
const MockGroup = (groupName, alertCount) => {
let alerts = [];
for (let i = 1; i <= alertCount; i++) {
alerts.push(MockAlert([], { instance: `instance${i}` }));
}
const group = MockAlertGroup(
{ alertname: "Fake Alert", group: groupName },
alerts,
[],
{}
);
return group;
};
const MockGroupList = count => {
let groups = {};
for (let i = 1; i <= count; i++) {
let id = `id${i}`;
let hash = `hash${i}`;
let group = MockGroup(`group${i}`, count);
group.id = id;
group.hash = hash;
groups[id] = group;
}
alertStore.data.upstreams = {
counters: { total: 0, healthy: 1, failed: 0 },
instances: [{ name: "am", uri: "http://am", error: "" }]
};
alertStore.data.groups = groups;
};
describe("<AlertGrid />", () => {
it("renders only first 50 alert groups", () => {
MockGroupList(60);
const tree = ShallowAlertGrid();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(50);
});
it("appends 30 groups after loadMore() call", () => {
MockGroupList(100);
const tree = ShallowAlertGrid();
// call it directly, it should happen on scroll to the bottom of the page
tree.instance().loadMore();
const alertGroups = tree.find("AlertGroup");
expect(alertGroups).toHaveLength(80);
});
it("calls masonryRepack() after update`", () => {
const tree = ShallowAlertGrid();
const instance = tree.instance();
const repackSpy = jest.spyOn(instance, "masonryRepack");
// it's a shallow render so we don't really have masonry mounted, fake it
instance.masonryComponentReference.ref = {
forcePack: jest.fn()
};
instance.componentDidUpdate();
expect(repackSpy).toHaveBeenCalled();
expect(instance.masonryComponentReference.ref.forcePack).toHaveBeenCalled();
});
it("calling storeMasonryRef() saves the ref in local store", () => {
const tree = ShallowAlertGrid();
const instance = tree.instance();
instance.storeMasonryRef("foo");
expect(instance.masonryComponentReference.ref).toBe("foo");
});
});

View File

@@ -0,0 +1,58 @@
import React from "react";
import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { Grid } from ".";
let alertStore;
let settingsStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
settingsStore = new Settings();
silenceFormStore = new SilenceFormStore();
});
const ShallowGrid = () => {
return shallow(
<Grid
alertStore={alertStore}
settingsStore={settingsStore}
silenceFormStore={silenceFormStore}
/>
);
};
describe("<Grid />", () => {
it("renders only AlertGrid when all upstreams are healthy", () => {
const tree = ShallowGrid();
expect(tree.text()).toBe("<AlertGrid />");
});
it("renders UpstreamError for each unhealthy upstream", () => {
alertStore.data.upstreams = {
counters: { total: 3, healthy: 1, failed: 2 },
instances: [
{ name: "am1", uri: "http://am1", error: "error 1" },
{ name: "am2", uri: "file:///mock", error: "" },
{ name: "am3", uri: "http://am1", error: "error 2" }
]
};
const tree = ShallowGrid();
expect(tree.text()).toBe("<UpstreamError /><UpstreamError /><AlertGrid />");
});
it("renders only FatalError on failed fetch", () => {
alertStore.status.error = "error";
alertStore.data.upstreams = {
counters: { total: 0, healthy: 0, failed: 1 },
instances: [{ name: "am", uri: "http://am1", error: "error" }]
};
const tree = ShallowGrid();
expect(tree.text()).toBe("<FatalError />");
});
});

View File

@@ -16,7 +16,9 @@ SetupRaven(settingsElement);
Moment.startPooledTimer();
const defaultFilters = ParseDefaultFilters(settingsElement);
ReactDOM.render(
// https://wetainment.com/testing-indexjs/
export default ReactDOM.render(
<App defaultFilters={defaultFilters} />,
document.getElementById("root")
document.getElementById("root") || document.createElement("div")
);

5
ui/src/index.test.js Normal file
View File

@@ -0,0 +1,5 @@
import Index from "./index.js";
it("renders without crashing", () => {
expect(Index).toBeTruthy();
});

View File

@@ -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");