Merge pull request #76 from prymitive/tests-6

More test coverage for the UI code
This commit is contained in:
Łukasz Mierzwa
2018-08-30 22:46:39 +01:00
committed by GitHub
22 changed files with 974 additions and 43 deletions
+6
View File
@@ -6506,6 +6506,12 @@
}
}
},
"jest-canvas-mock": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-1.1.0.tgz",
"integrity": "sha512-D2VoKl+L6r9VpqTPygXKvIOQ1aou7gz3PvstlWDZqPT7EVYcSz0Nj+yjJ9G+Y9EqJd2X95f3dzcmmXb2dvQ1DQ==",
"dev": true
},
"jest-changed-files": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-20.0.3.tgz",
+1
View File
@@ -55,6 +55,7 @@
"enzyme-adapter-react-16": "1.3.0",
"enzyme-to-json": "3.3.4",
"eslint-plugin-react": "7.11.1",
"jest-canvas-mock": "1.1.0",
"jest-fetch-mock": "1.6.5",
"jest-localstorage-mock": "2.2.0",
"jest-mock-console": "0.4.0",
+1 -1
View File
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { observer } from "mobx-react";
import * as Favico from "favico.js";
import Favico from "favico.js";
const FaviconBadge = observer(
class FaviconBadge extends Component {
@@ -0,0 +1,40 @@
import React from "react";
import { mount } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { FaviconBadge } from ".";
let alertStore;
beforeEach(() => {
alertStore = new AlertStore([]);
});
const MountedFaviconBadge = () => {
return mount(<FaviconBadge alertStore={alertStore} />);
};
describe("<FaviconBadge />", () => {
it("creates Favico instance on mount", () => {
const tree = MountedFaviconBadge();
const instance = tree.instance();
expect(instance.favicon).toBeInstanceOf(Object);
});
it("updateBadge is called when alertStore.info.totalAlerts changes", () => {
const tree = MountedFaviconBadge();
const instance = tree.instance();
const updateSpy = jest.spyOn(instance, "updateBadge");
alertStore.info.totalAlerts = 99;
expect(updateSpy).toHaveBeenCalledTimes(1);
});
it("updateBadge is called when alertStore.status.error changes", () => {
const tree = MountedFaviconBadge();
const instance = tree.instance();
const updateSpy = jest.spyOn(instance, "updateBadge");
alertStore.status.error = "foo";
expect(updateSpy).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,105 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CustomMultiSelect /> matches snapshot with a value 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={
Object {
"label": "foo",
"value": "foo",
}
}
options={
Array [
Object {
"label": "foo",
"value": "foo",
},
]
}
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
}
}
/>
`;
exports[`<CustomMultiSelect /> matches snapshot with defaults 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={null}
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
}
}
/>
`;
exports[`<CustomMultiSelect /> matches snapshot with isMulti=true 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={null}
isMulti={true}
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
}
}
/>
`;
exports[`<CustomMultiSelect /> matches snapshot with isMulti=true and a value 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={
Object {
"label": "foo",
"value": "foo",
}
}
isMulti={true}
options={
Array [
Object {
"label": "foo",
"value": "foo",
},
]
}
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
}
}
/>
`;
-5
View File
@@ -41,11 +41,6 @@ const ReactSelectStyles = {
borderRadius: "0.25rem",
backgroundColor: "#fff"
},
valueLabel: (base, state) => ({
...base,
whiteSpace: "normal",
wordWrap: "break-word"
}),
multiValue: (base, state) => ({
...base,
borderRadius: "4px",
@@ -0,0 +1,49 @@
import React from "react";
import { shallow } from "enzyme";
import { MultiSelect } from ".";
const Option = value => ({ label: value, value: value });
class CustomMultiSelect extends MultiSelect {
constructor(props) {
super(props);
this.extraProps = props;
}
renderProps = () => this.extraProps;
}
describe("<CustomMultiSelect />", () => {
it("matches snapshot with defaults", () => {
const tree = shallow(<CustomMultiSelect />);
expect(tree).toMatchSnapshot();
});
it("matches snapshot with isMulti=true", () => {
const tree = shallow(<CustomMultiSelect isMulti />);
expect(tree).toMatchSnapshot();
});
it("matches snapshot with a value", () => {
const tree = shallow(
<CustomMultiSelect
defaultValue={Option("foo")}
options={[Option("foo", Option("bar"))]}
/>
);
expect(tree).toMatchSnapshot();
});
it("matches snapshot with isMulti=true and a value", () => {
const tree = shallow(
<CustomMultiSelect
isMulti
defaultValue={Option("foo")}
options={[Option("foo", Option("bar"))]}
/>
);
expect(tree).toMatchSnapshot();
});
});
@@ -19,3 +19,9 @@ input.components-filterinput-wrapper {
input.components-filterinput-wrapper:focus {
width: auto;
}
/* highlighted part of the suggestion - phrase in the input that matches it */
mark.highlight {
padding: 0;
background-color: inherit;
}
@@ -31,6 +31,7 @@ const FilterInput = observer(
{
ref: null,
suggestions: [],
suggestionsFetch: null,
value: "",
storeInputReference(ref) {
this.ref = ref;
@@ -49,11 +50,10 @@ const FilterInput = observer(
}
onChange = action((event, { newValue, method }) => {
if (method === "enter") {
event.preventDefault();
} else {
this.inputStore.value = newValue;
}
// onChange here handles change for the user input in the filter bar
// we need to update inputStore.value every time user types in something
event.preventDefault();
this.inputStore.value = newValue;
});
onSubmit = action(event => {
@@ -71,7 +71,9 @@ const FilterInput = observer(
onSuggestionsFetchRequested = debounce(
action(({ value }) => {
if (value !== "") {
fetch(FormatUnseeBackendURI(`autocomplete.json?term=${value}`))
this.inputStore.suggestionsFetch = fetch(
FormatUnseeBackendURI(`autocomplete.json?term=${value}`)
)
.then(
result => result.json(),
err => {
@@ -22,6 +22,12 @@ const MountedInput = () => {
);
};
const WaitForFetch = tree => {
return expect(
tree.instance().inputStore.suggestionsFetch
).resolves.toBeUndefined();
};
describe("<FilterInput />", () => {
it("matches snapshot on default render", () => {
const tree = render(
@@ -68,38 +74,66 @@ describe("<FilterInput />", () => {
});
describe("<FilterInput Autosuggest />", () => {
it("fetches suggestions on input change", done => {
it("fetches suggestions on input change", async () => {
fetch.mockResponseOnce(JSON.stringify(["foo=bar", "foo=~bar"]));
const tree = MountedInput();
const instance = tree.instance();
tree.find("input").simulate("change", { target: { value: "foo" } });
await WaitForFetch(tree);
// need to wait on fetch to resolve, but can't find any better way here
setTimeout(() => {
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0]).toContain("./autocomplete.json?term=foo");
expect(instance.inputStore.suggestions).toHaveLength(2);
expect(instance.inputStore.suggestions).toContain("foo=bar");
expect(instance.inputStore.suggestions).toContain("foo=~bar");
done();
}, 1000);
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0]).toContain("./autocomplete.json?term=foo");
expect(instance.inputStore.suggestions).toHaveLength(2);
expect(instance.inputStore.suggestions).toContain("foo=bar");
expect(instance.inputStore.suggestions).toContain("foo=~bar");
});
it("handles failed suggestion fetches", done => {
it("clicking on a suggestion adds it to filters", async () => {
fetch.mockResponse(JSON.stringify(["foo=bar", "foo=~bar"]));
const tree = MountedInput();
tree.find("input").simulate("change", { target: { value: "foo" } });
// suggestions are rendered only when input is focused
tree.find("input").simulate("focus");
await WaitForFetch(tree);
// find() doesn't pick up suggestions even when tree.html() shows them
// forcing update seems to solve it
// https://github.com/airbnb/enzyme/issues/1233#issuecomment-343449560
tree.update();
// not sure why but suggestions are being found twice
const suggestion = tree.find(".dropdown-item").at(2);
expect(suggestion.text()).toBe("foo=~bar");
suggestion.simulate("click");
expect(alertStore.filters.values).toHaveLength(1);
expect(alertStore.filters.values[0]).toMatchObject({ raw: "foo=~bar" });
});
it("handles failed suggestion fetches", async () => {
fetch.mockRejectOnce("Fetch error");
const tree = MountedInput();
const instance = tree.instance();
tree.find("input").simulate("change", { target: { value: "bar" } });
await WaitForFetch(tree);
// need to wait on fetch to resolve, but can't find any better way here
setTimeout(() => {
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0]).toContain("./autocomplete.json?term=bar");
expect(instance.inputStore.suggestions).toHaveLength(0);
done();
}, 1000);
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0]).toContain("./autocomplete.json?term=bar");
expect(instance.inputStore.suggestions).toHaveLength(0);
});
it("handles invalid JSON in suggestion fetches", async () => {
fetch.mockResponseOnce("this is not JSON");
const tree = MountedInput();
const instance = tree.instance();
tree.find("input").simulate("change", { target: { value: "bar" } });
await WaitForFetch(tree);
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0]).toContain("./autocomplete.json?term=bar");
expect(instance.inputStore.suggestions).toHaveLength(0);
});
it("clearing input clears suggestions", () => {
@@ -0,0 +1,110 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { AlertManagerInput } from "./AlertManagerInput";
let alertStore;
let silenceFormStore;
const AlertmanagerOption = index => ({
label: `am${index}`,
value: `http://am${index}.example.com`
});
beforeEach(() => {
alertStore = new AlertStore([]);
alertStore.data.upstreams.instances = [
{ name: "am1", uri: "http://am1.example.com", error: "" },
{ name: "am2", uri: "http://am2.example.com", error: "" },
{ name: "am3", uri: "http://am3.example.com", error: "" }
];
silenceFormStore = new SilenceFormStore();
});
const ShallowAlertManagerInput = () => {
return shallow(
<AlertManagerInput
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};
const MountedAlertManagerInput = () => {
return mount(
<AlertManagerInput
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};
const ValidateSuggestions = () => {
const tree = MountedAlertManagerInput();
// clear all selected instances, they are selected by default
const clear = tree.find("ClearIndicator");
// https://github.com/JedWatson/react-select/blob/c22d296d50917e210836fb011ae3e565895e6440/src/__tests__/Select.test.js#L1873
clear.simulate("mousedown", { button: 0 });
// click on the react-select component doesn't seem to trigger options
// rendering in tests, so change the input instead
tree.find("input").simulate("change", { target: { value: "am" } });
return tree;
};
describe("<AlertManagerInput />", () => {
it("matches snapshot", () => {
const tree = ShallowAlertManagerInput();
expect(tree).toMatchSnapshot();
});
it("all available Alertmanager instances are selected by default", () => {
ShallowAlertManagerInput();
expect(silenceFormStore.data.alertmanagers).toHaveLength(3);
for (let i = 1; i <= 3; i++) {
expect(silenceFormStore.data.alertmanagers).toContainEqual(
AlertmanagerOption(i)
);
}
});
it("renders all 3 suggestions", () => {
const tree = ValidateSuggestions();
const options = tree.find("[role='option']");
expect(options).toHaveLength(3);
expect(options.at(0).text()).toBe("am1");
expect(options.at(1).text()).toBe("am2");
expect(options.at(2).text()).toBe("am3");
});
it("clicking on options appends them to silenceFormStore.data.alertmanagers", () => {
const tree = ValidateSuggestions();
const options = tree.find("[role='option']");
options.at(0).simulate("click");
options.at(2).simulate("click");
expect(silenceFormStore.data.alertmanagers).toHaveLength(2);
expect(silenceFormStore.data.alertmanagers).toContainEqual(
AlertmanagerOption(1)
);
expect(silenceFormStore.data.alertmanagers).toContainEqual(
AlertmanagerOption(3)
);
});
it("silenceFormStore.data.alertmanagers gets updated from alertStore.data.upstreams.instances on mismatch", () => {
const tree = ShallowAlertManagerInput();
alertStore.data.upstreams.instances[0] = {
name: "am1",
uri: "http://am1.example.com/new",
error: ""
};
// force update since this is where the mismatch check lives
tree.instance().componentDidUpdate();
expect(silenceFormStore.data.alertmanagers).toContainEqual({
label: "am1",
value: "http://am1.example.com/new"
});
});
});
@@ -11,7 +11,7 @@ const Duration = observer(
class Duration extends Component {
static propTypes = {
value: PropTypes.number.isRequired,
label: PropTypes.string,
label: PropTypes.string.isRequired,
onInc: PropTypes.func.isRequired,
onDec: PropTypes.func.isRequired
};
@@ -40,9 +40,7 @@ const Duration = observer(
<h2>{value}</h2>
</td>
<td className="w-50">
{label ? (
<span className="text-muted ml-2">{label}</span>
) : null}
<span className="text-muted ml-2">{label}</span>
</td>
</tr>
<tr>
@@ -107,8 +107,8 @@ const TabContentEnd = observer(({ silenceFormStore }) => {
);
});
// calculate value for duration increase and decrease buttons using a goal step
const CalculateChangeValue = (currentValue, step) => {
// calculate value for duration increase button using a goal step
const CalculateChangeValueUp = (currentValue, step) => {
// if current value is less than step (but >0) then use 1
if (currentValue > 0 && currentValue < step) {
return 1;
@@ -117,6 +117,16 @@ const CalculateChangeValue = (currentValue, step) => {
return step - (currentValue % step) || step;
};
// calculate value for duration decrease button using a goal step
const CalculateChangeValueDown = (currentValue, step) => {
// if current value is less than step (but >0) then use 1
if (currentValue > 0 && currentValue < step) {
return 1;
}
// otherwise use step or a value that moves current value to the next step
return currentValue % step || step;
};
const TabContentDuration = observer(({ silenceFormStore }) => {
return (
<div className="d-flex flex-sm-row flex-column justify-content-around mt-2 mx-3">
@@ -137,12 +147,15 @@ const TabContentDuration = observer(({ silenceFormStore }) => {
value={silenceFormStore.data.toDuration.minutes}
onInc={() =>
silenceFormStore.data.incEnd(
CalculateChangeValue(silenceFormStore.data.toDuration.minutes, 5)
CalculateChangeValueUp(silenceFormStore.data.toDuration.minutes, 5)
)
}
onDec={() =>
silenceFormStore.data.decEnd(
CalculateChangeValue(silenceFormStore.data.toDuration.minutes, 5)
CalculateChangeValueDown(
silenceFormStore.data.toDuration.minutes,
5
)
)
}
/>
@@ -243,4 +256,4 @@ const DateTimeSelect = observer(
}
);
export { DateTimeSelect };
export { DateTimeSelect, TabContentStart, TabContentEnd, TabContentDuration };
@@ -0,0 +1,306 @@
import React from "react";
import { mount, shallow } from "enzyme";
import moment from "moment";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import {
DateTimeSelect,
TabContentStart,
TabContentEnd,
TabContentDuration
} from ".";
let silenceFormStore;
beforeEach(() => {
silenceFormStore = new SilenceFormStore();
silenceFormStore.data.startsAt = moment([2060, 1, 1, 0, 0, 0]);
silenceFormStore.data.endsAt = moment([2061, 1, 1, 0, 0, 0]);
});
const ShallowDateTimeSelect = () => {
return shallow(<DateTimeSelect silenceFormStore={silenceFormStore} />);
};
const MountedDateTimeSelect = () => {
return mount(<DateTimeSelect silenceFormStore={silenceFormStore} />);
};
describe("<DateTimeSelect />", () => {
it("renders 3 tabs", () => {
const tree = ShallowDateTimeSelect();
const tabs = tree.find("Tab");
expect(tabs).toHaveLength(3);
});
it("renders 'Duration' tab by default", () => {
const tree = MountedDateTimeSelect();
const tab = tree.find(".nav-link.active");
expect(tab).toHaveLength(1);
// check tab title
expect(tab.text()).toMatch(/Duration/);
// check tab content
expect(tree.find(".tab-content").text()).toBe("366days0hours0minutes");
});
it("clicking on the 'Starts' tab switches content to 'startsAt' selection", () => {
const tree = MountedDateTimeSelect();
const tab = tree.find(".nav-link").at(0);
expect(tab.text()).toMatch(/Starts/);
tab.simulate("click");
expect(tree.find(".tab-content").text()).toMatch(/2060/);
});
it("clicking on the 'Ends' tab switches content to 'endsAt' selection", () => {
const tree = MountedDateTimeSelect();
const tab = tree.find(".nav-link").at(1);
expect(tab.text()).toMatch(/Ends/);
tab.simulate("click");
expect(tree.find(".tab-content").text()).toMatch(/2061/);
});
it("clicking on the 'Duration' tabs switches content to duration selection", () => {
const tree = MountedDateTimeSelect();
// first switch to 'Starts'
tree
.find(".nav-link")
.at(0)
.simulate("click");
// then switch back to 'Duration'
const tab = tree.find(".nav-link").at(2);
expect(tab.text()).toMatch(/Duration/);
tab.simulate("click");
expect(tree.find(".tab-content").text()).toBe("366days0hours0minutes");
});
});
const ValidateTimeButton = (
tab,
storeKey,
elemIndex,
iconMatch,
expectedDiff
) => {
const button = tab.find("td > span").at(elemIndex);
expect(button.html()).toMatch(iconMatch);
const oldTimeValue = moment(silenceFormStore.data[storeKey]);
button.simulate("click");
expect(silenceFormStore.data[storeKey].toISOString()).not.toBe(
oldTimeValue.toISOString()
);
const diffMS = silenceFormStore.data[storeKey].diff(oldTimeValue);
expect(diffMS).toBe(expectedDiff);
};
const ShallowTabContentStart = () => {
return shallow(<TabContentStart silenceFormStore={silenceFormStore} />);
};
const MountedTabContentStart = () => {
return mount(<TabContentStart silenceFormStore={silenceFormStore} />);
};
describe("<TabContentStart />", () => {
it("selecting date on DatePicker updates startsAt", () => {
const tree = ShallowTabContentStart();
const picker = tree.find("DatePicker");
const startsAt = moment([2063, 10, 10, 0, 1, 2]);
picker.simulate("change", startsAt);
expect(silenceFormStore.data.startsAt.toISOString()).toBe(
startsAt.toISOString()
);
});
it("clicking on the hour inc button adds 1h to startsAt", () => {
const tree = MountedTabContentStart();
ValidateTimeButton(tree, "startsAt", 0, /angle-up/, 3600 * 1000);
});
it("clicking on the minute inc button adds 1m to startsAt", () => {
const tree = MountedTabContentStart();
ValidateTimeButton(tree, "startsAt", 1, /angle-up/, 60 * 1000);
});
it("clicking on the hour dec button subtracts 1h from startsAt", () => {
const tree = MountedTabContentStart();
ValidateTimeButton(tree, "startsAt", 2, /angle-down/, -1 * 3600 * 1000);
});
it("clicking on the minute dec button subtracts 1m from startsAt", () => {
const tree = MountedTabContentStart();
ValidateTimeButton(tree, "startsAt", 3, /angle-down/, -1 * 60 * 1000);
});
});
const ShallowTabContentEnd = () => {
return shallow(<TabContentEnd silenceFormStore={silenceFormStore} />);
};
const MountedTabContentEnd = () => {
return mount(<TabContentEnd silenceFormStore={silenceFormStore} />);
};
describe("<TabContentEnd />", () => {
it("Selecting date on DatePicker updates endsAt", () => {
const tree = ShallowTabContentEnd();
const picker = tree.find("DatePicker");
const endsAt = moment([2063, 11, 5, 1, 3, 2]);
picker.simulate("change", endsAt);
expect(silenceFormStore.data.endsAt.toISOString()).toBe(
endsAt.toISOString()
);
});
it("clicking on the hour inc button adds 1h to endsAt", () => {
const tree = MountedTabContentEnd();
ValidateTimeButton(tree, "endsAt", 0, /angle-up/, 3600 * 1000);
});
it("clicking on the minute inc button adds 1m to endsAt", () => {
const tree = MountedTabContentEnd();
ValidateTimeButton(tree, "endsAt", 1, /angle-up/, 60 * 1000);
});
it("clicking on the hour dec button subtracts 1h from endsAt", () => {
const tree = MountedTabContentEnd();
ValidateTimeButton(tree, "endsAt", 2, /angle-down/, -1 * 3600 * 1000);
});
it("clicking on the minute dec button subtracts 1m from endsAt", () => {
const tree = MountedTabContentEnd();
ValidateTimeButton(tree, "endsAt", 3, /angle-down/, -1 * 60 * 1000);
});
});
const ValidateDurationButton = (elemIndex, iconMatch, expectedDiff) => {
const tree = mount(
<TabContentDuration silenceFormStore={silenceFormStore} />
);
const button = tree.find("td > span").at(elemIndex);
expect(button.html()).toMatch(iconMatch);
const oldEndsAt = moment(silenceFormStore.data.endsAt);
button.simulate("click");
expect(silenceFormStore.data.endsAt.toISOString()).not.toBe(
oldEndsAt.toISOString()
);
const diffMS = silenceFormStore.data.endsAt.diff(oldEndsAt);
expect(diffMS).toBe(expectedDiff);
};
describe("<TabContentDuration />", () => {
it("clicking on the day inc button adds 1d to endsAt", () => {
ValidateDurationButton(0, /angle-up/, 24 * 3600 * 1000);
});
it("clicking on the day dec button subtracts 1d from endsAt", () => {
ValidateDurationButton(2, /angle-down/, -1 * 24 * 3600 * 1000);
});
it("clicking on the hour inc button adds 1h to endsAt", () => {
ValidateDurationButton(3, /angle-up/, 3600 * 1000);
});
it("clicking on the hour dec button subtracts 1h from endsAt", () => {
ValidateDurationButton(5, /angle-down/, -1 * 3600 * 1000);
});
it("clicking on the minute inc button adds 5m to endsAt", () => {
ValidateDurationButton(6, /angle-up/, 5 * 60 * 1000);
});
it("clicking on the minute dec button subtracts 5m from endsAt", () => {
ValidateDurationButton(8, /angle-down/, -1 * 5 * 60 * 1000);
});
});
const SetDurationTo = (hours, minutes) => {
const startsAt = moment([2060, 1, 1, 0, 0, 0]);
const endsAt = moment(startsAt)
.add(hours, "hours")
.add(minutes, "minutes");
silenceFormStore.data.startsAt = startsAt;
silenceFormStore.data.endsAt = endsAt;
};
describe("<TabContentDuration /> inc minute CalculateChangeValue", () => {
it("inc on 0:1:0 duration sets 0:1:5", () => {
SetDurationTo(1, 0);
ValidateDurationButton(6, /angle-up/, 5 * 60 * 1000);
});
it("inc on 0:1:1 duration sets 0:1:2", () => {
SetDurationTo(1, 1);
ValidateDurationButton(6, /angle-up/, 60 * 1000);
});
it("inc on 0:1:4 duration sets 0:1:5", () => {
SetDurationTo(1, 4);
ValidateDurationButton(6, /angle-up/, 60 * 1000);
});
it("inc on 0:1:5 duration sets 0:1:10", () => {
SetDurationTo(1, 5);
ValidateDurationButton(6, /angle-up/, 5 * 60 * 1000);
});
it("inc on 0:1:6 duration sets 0:1:10", () => {
SetDurationTo(1, 6);
ValidateDurationButton(6, /angle-up/, 4 * 60 * 1000);
});
it("inc on 0:0:55 duration sets 0:1:0", () => {
SetDurationTo(0, 55);
ValidateDurationButton(6, /angle-up/, 5 * 60 * 1000);
});
});
describe("<TabContentDuration /> dec minute CalculateChangeValue", () => {
it("inc on 0:1:0 duration sets 0:0:55", () => {
SetDurationTo(1, 0);
ValidateDurationButton(8, /angle-down/, -5 * 60 * 1000);
});
it("inc on 0:0:59 duration sets 0:0:55", () => {
SetDurationTo(0, 59);
ValidateDurationButton(8, /angle-down/, -4 * 60 * 1000);
});
it("inc on 0:0:56 duration sets 0:0:55", () => {
SetDurationTo(0, 56);
ValidateDurationButton(8, /angle-down/, -1 * 60 * 1000);
});
it("inc on 0:0:55 duration sets 0:0:50", () => {
SetDurationTo(1, 0);
ValidateDurationButton(8, /angle-down/, -5 * 60 * 1000);
});
it("inc on 0:1:10 duration sets 0:1:5", () => {
SetDurationTo(1, 10);
ValidateDurationButton(8, /angle-down/, -5 * 60 * 1000);
});
it("inc on 0:1:6 duration sets 0:1:5", () => {
SetDurationTo(1, 6);
ValidateDurationButton(8, /angle-down/, -1 * 60 * 1000);
});
it("inc on 0:1:5 duration sets 0:1:0", () => {
SetDurationTo(1, 5);
ValidateDurationButton(8, /angle-down/, -5 * 60 * 1000);
});
it("inc on 0:1:4 duration sets 0:1:3", () => {
SetDurationTo(1, 4);
ValidateDurationButton(8, /angle-down/, -1 * 60 * 1000);
});
it("inc on 0:1:1 duration sets 0:1:0", () => {
SetDurationTo(1, 1);
ValidateDurationButton(8, /angle-down/, -1 * 60 * 1000);
});
});
@@ -51,6 +51,8 @@ const SilenceSubmitProgress = observer(
submitState = observable(
{
// store fetch result here, useful for testing
fetch: null,
value: SubmitState.InProgress,
result: null,
markDone(result) {
@@ -68,7 +70,7 @@ const SilenceSubmitProgress = observer(
handleAlertmanagerRequest = () => {
const { uri, payload } = this.props;
fetch(`${uri}/api/v1/silences`, {
this.submitState.fetch = fetch(`${uri}/api/v1/silences`, {
method: "POST",
body: JSON.stringify(payload),
headers: {
@@ -91,8 +93,11 @@ const SilenceSubmitProgress = observer(
} else if (response.status === "error") {
this.submitState.markFailed(response.error);
} else {
this.submitState.markFailed(JSON.strigify(response));
this.submitState.markFailed(JSON.stringify(response));
}
// return status so we can assert it in tests
return response.status;
};
componentDidMount() {
@@ -0,0 +1,88 @@
import React from "react";
import { mount } from "enzyme";
import { SilenceSubmitProgress } from "./SilenceSubmitProgress";
const MountedSilenceSubmitProgress = () => {
return mount(
<SilenceSubmitProgress
name="mockAlertmanager"
uri="http://localhost/mock"
payload={{ foo: "bar" }}
/>
);
};
describe("<SilenceSubmitProgress />", () => {
it("sends a request on mount", () => {
MountedSilenceSubmitProgress();
expect(fetch.mock.calls).toHaveLength(1);
});
it("appends /api/v1/silences to the passed URI", () => {
MountedSilenceSubmitProgress();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://localhost/mock/api/v1/silences");
});
it("sends correct JSON payload", () => {
MountedSilenceSubmitProgress();
const payload = fetch.mock.calls[0][1];
expect(payload).toMatchObject({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ foo: "bar" })
});
});
it("renders returned silence ID on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
// force re-render
tree.update();
const silenceLink = tree.find("a");
expect(silenceLink).toHaveLength(1);
expect(silenceLink.text()).toBe("123456789");
});
it("renders returned error message on failed fetch", async () => {
fetch.mockRejectOnce(new Error("mock error message"));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
expect(tree.text()).toBe("mockAlertmanagermock error message");
});
it("renders success icon on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(1);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
it("renders error icon on failed fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "error" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("error");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
it("renders unhandled 'status' values in the response as error", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "unhandled" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("unhandled");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
expect(tree.text()).toBe('mockAlertmanager{"status":"unhandled"}');
});
});
@@ -0,0 +1,154 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AlertManagerInput /> matches snapshot 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={
Array [
Object {
"label": "am1",
"value": "http://am1.example.com",
Symbol(mobx administration): ObservableObjectAdministration$$1 {
"defaultEnhancer": [Function],
"keysAtom": Atom$$1 {
"diffValue": 0,
"isBeingObserved": false,
"isPendingUnobservation": false,
"lastAccessedBy": 0,
"lowestObserverState": 2,
"name": "Silence form store.alertmanagers[..].keys",
"observers": Set {},
},
"name": "Silence form store.alertmanagers[..]",
"pendingKeys": Map {
"cheerio" => false,
"nodeType" => false,
"$$typeof" => false,
"@@__IMMUTABLE_LIST__@@" => false,
"@@__IMMUTABLE_SET__@@" => false,
"@@__IMMUTABLE_MAP__@@" => false,
"@@__IMMUTABLE_STACK__@@" => false,
"toJSON" => false,
},
"proxy": [Circular],
"target": Object {
"label": "am1",
"value": "http://am1.example.com",
Symbol(mobx administration): [Circular],
},
"values": Map {
"label" => "am1",
"value" => "http://am1.example.com",
},
},
},
Object {
"label": "am2",
"value": "http://am2.example.com",
Symbol(mobx administration): ObservableObjectAdministration$$1 {
"defaultEnhancer": [Function],
"keysAtom": Atom$$1 {
"diffValue": 0,
"isBeingObserved": false,
"isPendingUnobservation": false,
"lastAccessedBy": 0,
"lowestObserverState": 2,
"name": "Silence form store.alertmanagers[..].keys",
"observers": Set {},
},
"name": "Silence form store.alertmanagers[..]",
"pendingKeys": Map {
"cheerio" => false,
"nodeType" => false,
"$$typeof" => false,
"@@__IMMUTABLE_LIST__@@" => false,
"@@__IMMUTABLE_SET__@@" => false,
"@@__IMMUTABLE_MAP__@@" => false,
"@@__IMMUTABLE_STACK__@@" => false,
"toJSON" => false,
},
"proxy": [Circular],
"target": Object {
"label": "am2",
"value": "http://am2.example.com",
Symbol(mobx administration): [Circular],
},
"values": Map {
"label" => "am2",
"value" => "http://am2.example.com",
},
},
},
Object {
"label": "am3",
"value": "http://am3.example.com",
Symbol(mobx administration): ObservableObjectAdministration$$1 {
"defaultEnhancer": [Function],
"keysAtom": Atom$$1 {
"diffValue": 0,
"isBeingObserved": false,
"isPendingUnobservation": false,
"lastAccessedBy": 0,
"lowestObserverState": 2,
"name": "Silence form store.alertmanagers[..].keys",
"observers": Set {},
},
"name": "Silence form store.alertmanagers[..]",
"pendingKeys": Map {
"cheerio" => false,
"nodeType" => false,
"$$typeof" => false,
"@@__IMMUTABLE_LIST__@@" => false,
"@@__IMMUTABLE_SET__@@" => false,
"@@__IMMUTABLE_MAP__@@" => false,
"@@__IMMUTABLE_STACK__@@" => false,
"toJSON" => false,
},
"proxy": [Circular],
"target": Object {
"label": "am3",
"value": "http://am3.example.com",
Symbol(mobx administration): [Circular],
},
"values": Map {
"label" => "am3",
"value" => "http://am3.example.com",
},
},
},
]
}
instanceId="silence-input-alertmanagers"
isMulti={true}
onChange={[Function]}
options={
Array [
Object {
"label": "am1",
"value": "http://am1.example.com",
},
Object {
"label": "am2",
"value": "http://am2.example.com",
},
Object {
"label": "am3",
"value": "http://am3.example.com",
},
]
}
placeholder="Alertmanager"
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
}
}
/>
`;
@@ -34,7 +34,6 @@ exports[`<LabelNameInput /> matches snapshot 1`] = `
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
"valueLabel": [Function],
}
}
/>
@@ -30,7 +30,6 @@ exports[`<LabelValueInput /> matches snapshot 1`] = `
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
"valueLabel": [Function],
}
}
/>
+17
View File
@@ -238,6 +238,23 @@ class AlertStore {
return;
}
for (const filter of result.filters) {
const storedIndex = this.filters.values.findIndex(
f => f.raw === filter.text
);
this.filters.values[storedIndex] = Object.assign(
this.filters.values[storedIndex],
{
applied: true,
isValid: filter.isValid,
hits: filter.hits,
name: filter.name,
matcher: filter.matcher,
value: filter.value
}
);
}
let updates = {};
// update data dicts if they changed
for (const key of [
+1
View File
@@ -204,6 +204,7 @@ describe("AlertStore.fetch", () => {
expect(store.status.value).toEqual(AlertStoreStatuses.Idle);
expect(store.info.version).toBe("fakeVersion");
expect(store.filters.values[0].applied).toBe(true);
});
it("fetch() works with valid response", async () => {
+3
View File
@@ -12,6 +12,9 @@ mockConsole(["error", "warn", "info", "log", "trace"]);
// localStorage is used for Settings store
require("jest-localstorage-mock");
// favico.js needs canvas
require("jest-canvas-mock");
// fetch is used in multiple places to interact with Go backend
// or upstream Alertmanager API
global.fetch = require("jest-fetch-mock");