Merge pull request #70 from prymitive/tests-5

feat(tests): add missing tests for main modal
This commit is contained in:
Łukasz Mierzwa
2018-08-27 18:07:05 +01:00
committed by GitHub
20 changed files with 887 additions and 70 deletions

View File

@@ -1,5 +1,6 @@
import React from "react";
import renderer from "react-test-renderer";
import { shallow, mount } from "enzyme";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
@@ -22,7 +23,7 @@ const MockColors = () => {
};
};
const FakeLabel = (matcher, applied) => {
const ShallowLabel = (matcher, applied, valid) => {
const name = "foo";
const value = "bar";
const filter = NewUnappliedFilter(`${name}${matcher}${value}`);
@@ -30,27 +31,23 @@ const FakeLabel = (matcher, applied) => {
filter.name = name;
filter.matcher = matcher;
filter.value = value;
return renderer.create(
<FilterInputLabel alertStore={alertStore} filter={filter} />
);
filter.isValid = valid;
return shallow(<FilterInputLabel alertStore={alertStore} filter={filter} />);
};
const ValidateClass = (matcher, applied, expectedClass) => {
const tree = FakeLabel(matcher, applied).toJSON();
expect(tree.props.className.split(" ")).toContain(expectedClass);
const tree = ShallowLabel(matcher, applied, true);
expect(tree.props().className.split(" ")).toContain(expectedClass);
};
const ValidateOnChange = newRaw => {
const component = renderer.create(
const tree = shallow(
<FilterInputLabel
alertStore={alertStore}
filter={alertStore.filters.values[0]}
/>
);
const tree = component.toTree();
// call onChange with new raw value
tree.instance.onChange({ raw: newRaw });
tree.instance().onChange({ raw: newRaw });
return tree;
};
@@ -92,40 +89,40 @@ describe("<FilterInputLabel /> className", () => {
describe("<FilterInputLabel /> style", () => {
it("unapplied filter with color information and '=' matcher should have empty style", () => {
MockColors();
const tree = FakeLabel("=", false).toJSON();
expect(tree.props.style).toMatchObject({});
const tree = ShallowLabel("=", false, true);
expect(tree.props().style).toMatchObject({});
});
it("unapplied filter with no color information and '=' matcher should have empty style", () => {
const tree = FakeLabel("=", false).toJSON();
expect(tree.props.style).toMatchObject({});
const tree = ShallowLabel("=", false, true);
expect(tree.props().style).toMatchObject({});
});
it("unapplied filter with no color information and any matcher other than '=' should have empty style", () => {
for (const matcher of NonEqualMatchers) {
const tree = FakeLabel(matcher, false).toJSON();
expect(tree.props.style).toMatchObject({});
const tree = ShallowLabel(matcher, false, true);
expect(tree.props().style).toMatchObject({});
}
});
it("applied filter with color information and '=' matcher should have non empty style", () => {
MockColors();
const tree = FakeLabel("=", true).toJSON();
expect(tree.props.style).toMatchObject({
const tree = ShallowLabel("=", true, true);
expect(tree.props().style).toMatchObject({
color: "rgba(1, 2, 3, 100)",
backgroundColor: "rgba(4, 5, 6, 200)"
});
});
it("applied filter with no color information and '=' matcher should have empty style", () => {
const tree = FakeLabel("=", true).toJSON();
expect(tree.props.style).toMatchObject({});
const tree = ShallowLabel("=", true, true);
expect(tree.props().style).toMatchObject({});
});
it("applied filter with no color information and any matcher other than '=' should have empty style", () => {
for (const matcher of NonEqualMatchers) {
const tree = FakeLabel(matcher, true).toJSON();
expect(tree.props.style).toMatchObject({});
const tree = ShallowLabel(matcher, true, true);
expect(tree.props().style).toMatchObject({});
}
});
});
@@ -168,17 +165,27 @@ describe("<FilterInputLabel /> onChange", () => {
NewUnappliedFilter("foo=bar"),
NewUnappliedFilter("bar=baz")
];
const component = renderer.create(
const tree = mount(
<FilterInputLabel
alertStore={alertStore}
filter={alertStore.filters.values[0]}
/>
);
const button = component.root.findByType("button");
button.props.onClick();
const button = tree.find("button");
button.simulate("click");
expect(alertStore.filters.values).toHaveLength(1);
expect(alertStore.filters.values).toContainEqual(
NewUnappliedFilter("bar=baz")
);
});
});
describe("<FilterInputLabel /> render", () => {
it("invalid filter matches snapshot", () => {
const tree = ShallowLabel("=", true, false);
const errorSpan = tree.find(".text-danger");
expect(errorSpan).toHaveLength(1);
const errorIcon = errorSpan.find("FontAwesomeIcon");
expect(errorIcon).toHaveLength(1);
});
});

View File

@@ -1,6 +1,6 @@
import React from "react";
import { shallow } from "enzyme";
import { shallow, mount } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
@@ -14,27 +14,33 @@ beforeEach(() => {
settingsStore = new Settings();
});
const RenderMainModal = () => {
const ShallowMainModal = () => {
return shallow(
<MainModal alertStore={alertStore} settingsStore={settingsStore} />
);
};
const MountedMainModal = () => {
return mount(
<MainModal alertStore={alertStore} settingsStore={settingsStore} />
);
};
describe("<MainModal />", () => {
it("only renders FontAwesomeIcon when modal is not shown", () => {
const tree = RenderMainModal();
const tree = ShallowMainModal();
expect(tree.text()).toBe("<FontAwesomeIcon />");
});
it("renders the modal when it is shown", () => {
const tree = RenderMainModal();
const tree = ShallowMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.text()).toBe("<FontAwesomeIcon /><MainModalContent />");
});
it("hides the modal when toggle() is called twice", () => {
const tree = RenderMainModal();
const tree = ShallowMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
toggle.simulate("click");
@@ -42,7 +48,7 @@ describe("<MainModal />", () => {
});
it("hides the modal when hide() is called", () => {
const tree = RenderMainModal();
const tree = ShallowMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.text()).toBe("<FontAwesomeIcon /><MainModalContent />");
@@ -50,4 +56,27 @@ describe("<MainModal />", () => {
instance.toggle.hide();
expect(tree.text()).toBe("<FontAwesomeIcon />");
});
it("'modal-open' class is appended to body node when modal is visible", () => {
const tree = MountedMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(document.body.className.split(" ")).toContain("modal-open");
});
it("'modal-open' class is removed from body node after modal is hidden", () => {
const tree = MountedMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
toggle.simulate("click");
expect(document.body.className.split(" ")).not.toContain("modal-open");
});
it("'modal-open' class is removed from body node after modal is unmounted", () => {
const tree = MountedMainModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
tree.unmount();
expect(document.body.className.split(" ")).not.toContain("modal-open");
});
});

View File

@@ -15,8 +15,8 @@ import { SilenceModal } from "Components/SilenceModal";
import "./index.css";
const navbarResize = function(width, height) {
document.body.style["padding-top"] = height + 4 + "px";
const NavbarOnResize = function(width, height) {
document.body.style["padding-top"] = `${height + 4}px`;
};
const NavBar = observer(
@@ -41,7 +41,7 @@ const NavBar = observer(
return (
<div className="container">
<nav className="navbar fixed-top navbar-expand navbar-dark p-1 bg-primary-transparent d-inline-block">
<ReactResizeDetector handleHeight onResize={navbarResize} />
<ReactResizeDetector handleHeight onResize={NavbarOnResize} />
<span className="navbar-brand my-0 mx-2 h1 d-none d-sm-block float-left">
{alertStore.info.totalAlerts}
<FetchIndicator status={alertStore.status.value.toString()} />
@@ -67,4 +67,4 @@ const NavBar = observer(
}
);
export { NavBar };
export { NavBar, NavbarOnResize };

View File

@@ -7,7 +7,7 @@ import moment from "moment";
import { AlertStore, NewUnappliedFilter } from "Stores/AlertStore";
import { Settings } from "Stores/Settings";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { NavBar } from ".";
import { NavBar, NavbarOnResize } from ".";
let alertStore;
let settingsStore;
@@ -76,3 +76,21 @@ describe("<NavBar />", () => {
ValidateNavClass(3, "flex-column");
});
});
describe("NavbarOnResize()", () => {
it("body 'padding-top' style is updated after calling NavbarOnResize()", () => {
NavbarOnResize(0, 10);
expect(
window
.getComputedStyle(document.body, null)
.getPropertyValue("padding-top")
).toBe("14px");
NavbarOnResize(0, 36);
expect(
window
.getComputedStyle(document.body, null)
.getPropertyValue("padding-top")
).toBe("40px");
});
});

View File

@@ -123,25 +123,25 @@ const TabContentDuration = observer(({ silenceFormStore }) => {
<Duration
label="days"
value={silenceFormStore.data.toDuration.days}
onInc={() => silenceFormStore.data.incDuration(60 * 24)}
onDec={() => silenceFormStore.data.decDuration(60 * 24)}
onInc={() => silenceFormStore.data.incEnd(60 * 24)}
onDec={() => silenceFormStore.data.decEnd(60 * 24)}
/>
<Duration
label="hours"
value={silenceFormStore.data.toDuration.hours}
onInc={() => silenceFormStore.data.incDuration(60)}
onDec={() => silenceFormStore.data.decDuration(60)}
onInc={() => silenceFormStore.data.incEnd(60)}
onDec={() => silenceFormStore.data.decEnd(60)}
/>
<Duration
label="minutes"
value={silenceFormStore.data.toDuration.minutes}
onInc={() =>
silenceFormStore.data.incDuration(
silenceFormStore.data.incEnd(
CalculateChangeValue(silenceFormStore.data.toDuration.minutes, 5)
)
}
onDec={() =>
silenceFormStore.data.decDuration(
silenceFormStore.data.decEnd(
CalculateChangeValue(silenceFormStore.data.toDuration.minutes, 5)
)
}

View File

@@ -0,0 +1,89 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { NewEmptyMatcher, MatcherValueToObject } from "Stores/SilenceFormStore";
import { LabelNameInput } from "./LabelNameInput";
let matcher;
beforeEach(() => {
matcher = NewEmptyMatcher();
matcher.name = "name";
matcher.suggestions.names = [
MatcherValueToObject("job"),
MatcherValueToObject("cluster")
];
matcher.suggestions.values = [
MatcherValueToObject("foo"),
MatcherValueToObject("bar")
];
});
const ShallowLabelNameInput = () => {
return shallow(<LabelNameInput matcher={matcher} />);
};
const MountedLabelNameInput = () => {
return mount(<LabelNameInput matcher={matcher} />);
};
const ValidateSuggestions = () => {
const tree = MountedLabelNameInput();
// 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: "f" } });
return tree;
};
describe("<LabelNameInput />", () => {
it("matches snapshot", () => {
const tree = ShallowLabelNameInput();
expect(tree).toMatchSnapshot();
});
it("renders suggestions", () => {
const tree = ValidateSuggestions();
const options = tree.find("[role='option']");
expect(options).toHaveLength(2);
expect(options.at(0).text()).toBe("job");
expect(options.at(1).text()).toBe("cluster");
});
it("clicking on options updates the matcher", () => {
const tree = ValidateSuggestions();
const option = tree.find("[role='option']").at(0);
option.simulate("click");
expect(matcher.name).toBe("job");
});
it("populates suggestions on mount", done => {
fetch
.once(JSON.stringify(["name1", "name2", "name3"]))
.once(JSON.stringify(["value1", "value2", "value3"]));
ShallowLabelNameInput();
// use timeout since mount will call fetch
setTimeout(() => {
expect(matcher.suggestions.names).toHaveLength(3);
for (let i = 0; i < 3; i++) {
expect(matcher.suggestions.names[i]).toMatchObject(
MatcherValueToObject(`name${i + 1}`)
);
expect(matcher.suggestions.values[i]).toMatchObject(
MatcherValueToObject(`value${i + 1}`)
);
}
done();
}, 100);
});
it("suggestions are empited on failed fetch", done => {
fetch.mockReject(new Error("fake error message"));
ShallowLabelNameInput();
// use timeout since mount will call fetch
setTimeout(() => {
expect(matcher.suggestions.names).toHaveLength(0);
done();
}, 100);
});
});

View File

@@ -0,0 +1,88 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { NewEmptyMatcher, MatcherValueToObject } from "Stores/SilenceFormStore";
import { LabelValueInput } from "./LabelValueInput";
let matcher;
beforeEach(() => {
matcher = NewEmptyMatcher();
matcher.name = "name";
matcher.suggestions.names = [
MatcherValueToObject("job"),
MatcherValueToObject("cluster")
];
matcher.suggestions.values = [
MatcherValueToObject("foo"),
MatcherValueToObject("bar")
];
});
const ShallowLabelValueInput = () => {
return shallow(<LabelValueInput matcher={matcher} />);
};
const MountedLabelValueInput = () => {
return mount(<LabelValueInput matcher={matcher} />);
};
const ValidateSuggestions = () => {
const tree = MountedLabelValueInput();
// 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: "f" } });
return tree;
};
describe("<LabelValueInput />", () => {
it("matches snapshot", () => {
const tree = ShallowLabelValueInput();
expect(tree).toMatchSnapshot();
});
it("renders suggestions", () => {
const tree = ValidateSuggestions();
const options = tree.find("[role='option']");
expect(options).toHaveLength(2);
expect(options.at(0).text()).toBe("foo");
expect(options.at(1).text()).toBe("bar");
});
it("clicking on options appends them to matcher.values", () => {
const tree = ValidateSuggestions();
const options = tree.find("[role='option']");
options.at(0).simulate("click");
options.at(1).simulate("click");
expect(matcher.values).toHaveLength(2);
expect(matcher.values).toContainEqual(MatcherValueToObject("foo"));
expect(matcher.values).toContainEqual(MatcherValueToObject("bar"));
});
it("selecting one option doesn't force matcher.isRegex=true", () => {
const tree = ValidateSuggestions();
expect(matcher.isRegex).toBe(false);
const options = tree.find("[role='option']");
options.at(0).simulate("click");
expect(matcher.isRegex).toBe(false);
});
it("selecting one option when matcher.isRegex=true changes it back to false", () => {
matcher.isRegex = true;
const tree = ValidateSuggestions();
expect(matcher.isRegex).toBe(true);
const options = tree.find("[role='option']");
options.at(0).simulate("click");
expect(matcher.isRegex).toBe(false);
});
it("selecting multiple options forces matcher.isRegex=true", () => {
const tree = ValidateSuggestions();
expect(matcher.isRegex).toBe(false);
const options = tree.find("[role='option']");
options.at(0).simulate("click");
options.at(1).simulate("click");
expect(matcher.isRegex).toBe(true);
});
});

View File

@@ -0,0 +1,129 @@
import React from "react";
import { mount, shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceForm } from "./SilenceForm";
let alertStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceForm = () => {
return shallow(
<SilenceForm alertStore={alertStore} silenceFormStore={silenceFormStore} />
);
};
const MountedSilenceForm = () => {
return mount(
<SilenceForm alertStore={alertStore} silenceFormStore={silenceFormStore} />
);
};
describe("<SilenceForm /> matchers", () => {
it("has an empty matcher selects on default render", () => {
const tree = ShallowSilenceForm();
const matchers = tree.find("SilenceMatch");
expect(matchers).toHaveLength(1);
expect(silenceFormStore.data.matchers).toHaveLength(1);
});
it("clicking 'Add more' button adds another matcher", () => {
const tree = ShallowSilenceForm();
const button = tree.find("button[type='button']");
button.simulate("click", { preventDefault: jest.fn() });
const matchers = tree.find("SilenceMatch");
expect(matchers).toHaveLength(2);
expect(silenceFormStore.data.matchers).toHaveLength(2);
});
it("trash icon is not visible when there's only one matcher", () => {
const tree = MountedSilenceForm();
expect(silenceFormStore.data.matchers).toHaveLength(1);
const matcher = tree.find("SilenceMatch");
const button = matcher.find("button");
expect(button).toHaveLength(0);
});
it("trash icon is visible when there are two matchers", () => {
silenceFormStore.data.addEmptyMatcher();
silenceFormStore.data.addEmptyMatcher();
const tree = MountedSilenceForm();
expect(silenceFormStore.data.matchers).toHaveLength(2);
const matcher = tree.find("SilenceMatch");
const button = matcher.find("button");
expect(button).toHaveLength(2);
});
it("clicking trash icon on a matcher select removes it", () => {
silenceFormStore.data.addEmptyMatcher();
silenceFormStore.data.addEmptyMatcher();
silenceFormStore.data.addEmptyMatcher();
const tree = MountedSilenceForm();
expect(silenceFormStore.data.matchers).toHaveLength(3);
const matchers = tree.find("SilenceMatch");
const toDelete = matchers.at(1);
const button = toDelete.find("button");
button.simulate("click");
expect(silenceFormStore.data.matchers).toHaveLength(2);
});
});
describe("<SilenceForm /> preview", () => {
it("doesn't render SilencePreview when previewCollapse.hidden is true", () => {
const tree = ShallowSilenceForm();
const instance = tree.instance();
instance.previewCollapse.hidden = true;
expect(tree.find("SilencePreview")).toHaveLength(0);
});
it("renders SilencePreview when previewCollapse.hidden is false", () => {
const tree = ShallowSilenceForm();
const instance = tree.instance();
instance.previewCollapse.hidden = false;
expect(tree.find("SilencePreview")).toHaveLength(1);
});
it("clicking on the toggle icon toggles SilencePreview", () => {
const tree = ShallowSilenceForm();
const button = tree.find("a.btn.cursor-pointer.text-muted");
expect(tree.find("SilencePreview")).toHaveLength(0);
button.simulate("click");
expect(tree.find("SilencePreview")).toHaveLength(1);
button.simulate("click");
expect(tree.find("SilencePreview")).toHaveLength(0);
});
});
describe("<SilenceForm /> inputs", () => {
it("changing author input updates SilenceFormStore", () => {
const tree = MountedSilenceForm();
const input = tree.find("input[placeholder='Author email']");
input.simulate("change", { target: { value: "me@example.com" } });
expect(silenceFormStore.data.author).toBe("me@example.com");
});
it("changing comment input updates SilenceFormStore", () => {
const tree = MountedSilenceForm();
const input = tree.find("input[placeholder='Comment']");
input.simulate("change", { target: { value: "fake comment" } });
expect(silenceFormStore.data.comment).toBe("fake comment");
});
});
describe("<SilenceForm />", () => {
it("calling submit marks form as in progress", () => {
const tree = ShallowSilenceForm();
tree.simulate("submit", { preventDefault: jest.fn() });
expect(silenceFormStore.data.inProgress).toBe(true);
});
});

View File

@@ -25,7 +25,6 @@ const SilenceMatch = observer(
onIsRegexChange = action(event => {
const { matcher } = this.props;
console.info(matcher.values);
// only allow to change value if we don't have multiple values
if (matcher.values.length <= 1) {

View File

@@ -0,0 +1,37 @@
import React from "react";
import { shallow } from "enzyme";
import { NewEmptyMatcher, MatcherValueToObject } from "Stores/SilenceFormStore";
import { SilenceMatch } from "./SilenceMatch";
let matcher;
beforeEach(() => {
matcher = NewEmptyMatcher();
});
const ShallowLabelValueInput = () => {
return shallow(<SilenceMatch matcher={matcher} />);
};
describe("<SilenceMatch />", () => {
it("allows changing matcher.isRegex value when matcher.values contains 1 element", () => {
matcher.values = [MatcherValueToObject("foo")];
const tree = ShallowLabelValueInput();
expect(matcher.isRegex).toBe(false);
const regex = tree.find("input[type='checkbox']");
regex.simulate("change", { target: { checked: true } });
expect(matcher.isRegex).toBe(true);
});
it("disallows changing matcher.isRegex value when matcher.values contains 2 elements", () => {
matcher.isRegex = true;
matcher.values = [MatcherValueToObject("foo"), MatcherValueToObject("bar")];
const tree = ShallowLabelValueInput();
expect(matcher.isRegex).toBe(true);
const regex = tree.find("input[type='checkbox']");
regex.simulate("change", { target: { checked: false } });
expect(matcher.isRegex).toBe(true);
});
});

View File

@@ -0,0 +1,40 @@
import React from "react";
import { shallow } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceModalContent } from "./SilenceModalContent";
let alertStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceModalContent = () => {
return shallow(
<SilenceModalContent
alertStore={alertStore}
silenceFormStore={silenceFormStore}
/>
);
};
describe("<SilenceModalContent />", () => {
it("renders SilenceForm when silenceFormStore.data.inProgress is false", () => {
silenceFormStore.data.inProgress = false;
const tree = ShallowSilenceModalContent();
const form = tree.find("SilenceForm");
expect(form).toHaveLength(1);
});
it("renders SilenceSubmitController when silenceFormStore.data.inProgress is true", () => {
silenceFormStore.data.inProgress = true;
const tree = ShallowSilenceModalContent();
const ctrl = tree.find("SilenceSubmitController");
expect(ctrl).toHaveLength(1);
});
});

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render } from "enzyme";
import moment from "moment";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilencePreview } from "./SilencePreview";
describe("<SilencePreview />", () => {
it("matches snapshot", () => {
const silenceFormStore = new SilenceFormStore();
silenceFormStore.data.startsAt = moment([2000, 1, 1, 0, 0, 0]);
silenceFormStore.data.endsAt = moment([2000, 1, 1, 1, 0, 0]);
silenceFormStore.data.createdBy = "me@example.com";
silenceFormStore.data.comment = "SilencePreview test";
const tree = render(<SilencePreview silenceFormStore={silenceFormStore} />);
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,39 @@
import React from "react";
import { shallow } from "enzyme";
import {
SilenceFormStore,
MatcherValueToObject
} from "Stores/SilenceFormStore";
import { SilenceSubmitController } from "./SilenceSubmitController";
let silenceFormStore;
beforeEach(() => {
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceSubmitController = () => {
return shallow(
<SilenceSubmitController silenceFormStore={silenceFormStore} />
);
};
describe("<SilenceSubmitController />", () => {
it("renders all passed SilenceSubmitProgress", () => {
silenceFormStore.data.alertmanagers.push(MatcherValueToObject("am1"));
silenceFormStore.data.alertmanagers.push(MatcherValueToObject("am2"));
const tree = ShallowSilenceSubmitController();
const alertmanagers = tree.find("SilenceSubmitProgress");
expect(alertmanagers).toHaveLength(2);
});
it("resets the form on 'Back' button click", () => {
silenceFormStore.data.inProgress = true;
const tree = ShallowSilenceSubmitController();
const button = tree.find("button");
button.simulate("click");
expect(silenceFormStore.data.inProgress).toBe(false);
});
});

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LabelNameInput /> matches snapshot 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={
Object {
"label": "name",
"value": "name",
}
}
instanceId="silence-input-label-name-1"
onChange={[Function]}
options={
Array [
Object {
"label": "job",
"value": "job",
},
Object {
"label": "cluster",
"value": "cluster",
},
]
}
placeholder="Label name"
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
"valueLabel": [Function],
}
}
/>
`;

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LabelValueInput /> matches snapshot 1`] = `
<StateManager
defaultInputValue=""
defaultMenuIsOpen={false}
defaultValue={Array []}
instanceId="silence-input-label-value-1"
isMulti={true}
onChange={[Function]}
options={
Array [
Object {
"label": "foo",
"value": "foo",
},
Object {
"label": "bar",
"value": "bar",
},
]
}
placeholder="Label value"
styles={
Object {
"control": [Function],
"indicatorsContainer": [Function],
"multiValue": [Function],
"multiValueLabel": [Function],
"multiValueRemove": [Function],
"option": [Function],
"valueContainer": [Function],
"valueLabel": [Function],
}
}
/>
`;

View File

@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SilencePreview /> matches snapshot 1`] = `
<div
class="mt-3"
>
<pre
class="json-pretty"
>
{
"
<span
class="json-key"
>
matchers
</span>
": [],
"
<span
class="json-key"
>
startsAt
</span>
":
<span
class="json-string"
>
"2000-02-01T00:00:00.000Z"
</span>
,
"
<span
class="json-key"
>
endsAt
</span>
":
<span
class="json-string"
>
"2000-02-01T01:00:00.000Z"
</span>
,
"
<span
class="json-key"
>
createdBy
</span>
":
<span
class="json-string"
>
""
</span>
,
"
<span
class="json-key"
>
comment
</span>
":
<span
class="json-string"
>
"SilencePreview test"
</span>
}
</pre>
</div>
`;

View File

@@ -0,0 +1,81 @@
import React from "react";
import { shallow, mount } from "enzyme";
import { AlertStore } from "Stores/AlertStore";
import { SilenceFormStore } from "Stores/SilenceFormStore";
import { SilenceModal } from ".";
let alertStore;
let silenceFormStore;
beforeEach(() => {
alertStore = new AlertStore([]);
silenceFormStore = new SilenceFormStore();
});
const ShallowSilenceModal = () => {
return shallow(
<SilenceModal alertStore={alertStore} silenceFormStore={silenceFormStore} />
);
};
const MountedSilenceModal = () => {
return mount(
<SilenceModal alertStore={alertStore} silenceFormStore={silenceFormStore} />
);
};
describe("<SilenceModal />", () => {
it("only renders FontAwesomeIcon when modal is not shown", () => {
const tree = ShallowSilenceModal();
expect(tree.text()).toBe("<FontAwesomeIcon />");
});
it("renders the modal when it is shown", () => {
const tree = ShallowSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.text()).toBe("<FontAwesomeIcon /><SilenceModalContent />");
});
it("hides the modal when toggle() is called twice", () => {
const tree = ShallowSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
toggle.simulate("click");
expect(tree.text()).toBe("<FontAwesomeIcon />");
});
it("hides the modal when hide() is called", () => {
const tree = ShallowSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(tree.text()).toBe("<FontAwesomeIcon /><SilenceModalContent />");
silenceFormStore.toggle.hide();
expect(tree.text()).toBe("<FontAwesomeIcon />");
});
it("'modal-open' class is appended to body node when modal is visible", () => {
const tree = MountedSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
expect(document.body.className.split(" ")).toContain("modal-open");
});
it("'modal-open' class is removed from body node after modal is hidden", () => {
const tree = MountedSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
toggle.simulate("click");
expect(document.body.className.split(" ")).not.toContain("modal-open");
});
it("'modal-open' class is removed from body node after modal is unmounted", () => {
const tree = MountedSilenceModal();
const toggle = tree.find(".nav-link");
toggle.simulate("click");
tree.unmount();
expect(document.body.className.split(" ")).not.toContain("modal-open");
});
});

View File

@@ -4,7 +4,7 @@ import uniqueId from "lodash.uniqueid";
import moment from "moment";
const NewEmptyMatcher = id => {
const NewEmptyMatcher = () => {
return {
id: uniqueId(),
name: "",
@@ -17,7 +17,7 @@ const NewEmptyMatcher = id => {
};
};
const ValueToObject = value => ({ label: value, value: value });
const MatcherValueToObject = value => ({ label: value, value: value });
class SilenceFormStore {
// this is used to store modal visibility toggle
@@ -75,16 +75,10 @@ class SilenceFormStore {
for (const [key, value] of Object.entries(
Object.assign({}, group.labels, group.shared.labels)
)) {
matchers.push({
id: uniqueId(),
name: key,
values: [ValueToObject(value)],
suggestions: {
names: [],
values: []
},
isRegex: false
});
const matcher = NewEmptyMatcher();
matcher.name = key;
matcher.values = [MatcherValueToObject(value)];
matchers.push(matcher);
}
// add matchers for all unique labels in this group
@@ -101,7 +95,9 @@ class SilenceFormStore {
matchers.push({
id: uniqueId(),
name: key,
values: [...values].sort().map(value => ValueToObject(value)),
values: [...values]
.sort()
.map(value => MatcherValueToObject(value)),
suggestions: {
names: [],
values: []
@@ -141,15 +137,6 @@ class SilenceFormStore {
this.verifyStarEnd();
},
incDuration(minutes) {
this.endsAt = moment(this.endsAt).add(minutes, "minutes");
this.verifyStarEnd();
},
decDuration(minutes) {
this.endsAt = moment(this.endsAt).subtract(minutes, "minutes");
this.verifyStarEnd();
},
get toAlertmanagerPayload() {
const payload = {
matchers: this.matchers.map(m => ({
@@ -194,8 +181,6 @@ class SilenceFormStore {
decStart: action.bound,
incEnd: action.bound,
decEnd: action.bound,
incDuration: action.bound,
decDuration: action.bound,
toAlertmanagerPayload: computed,
toDuration: computed
},
@@ -203,4 +188,4 @@ class SilenceFormStore {
);
}
export { SilenceFormStore };
export { SilenceFormStore, NewEmptyMatcher, MatcherValueToObject };

View File

@@ -114,6 +114,8 @@ describe("SilenceFormStore.data", () => {
it("toAlertmanagerPayload creates payload that matches snapshot", () => {
const group = MockGroup();
store.data.fillMatchersFromGroup(group);
// add empty matcher so we test empty string rendering
store.data.addEmptyMatcher();
store.data.startsAt = moment([2000, 1, 1, 0, 0, 0]);
store.data.endsAt = moment([2000, 1, 1, 1, 0, 0]);
store.data.createdBy = "me@example.com";
@@ -121,3 +123,100 @@ describe("SilenceFormStore.data", () => {
expect(store.data.toAlertmanagerPayload).toMatchSnapshot();
});
});
describe("SilenceFormStore.data startsAt & endsAt validation", () => {
it("toDuration returns correct duration for 5d 0h 1m", () => {
store.data.startsAt = moment([2000, 1, 1, 0, 0, 0]);
store.data.endsAt = moment([2000, 1, 6, 0, 1, 15]);
expect(store.data.toDuration).toMatchObject({
days: 5,
hours: 0,
minutes: 1
});
});
it("toDuration returns correct duration for 2h 15m", () => {
store.data.startsAt = moment([2000, 1, 1, 0, 0, 0]);
store.data.endsAt = moment([2000, 1, 1, 2, 15, 0]);
expect(store.data.toDuration).toMatchObject({
days: 0,
hours: 2,
minutes: 15
});
});
it("toDuration returns correct duration for 59m", () => {
store.data.startsAt = moment([2000, 1, 1, 0, 10, 0]);
store.data.endsAt = moment([2000, 1, 1, 1, 9, 0]);
expect(store.data.toDuration).toMatchObject({
days: 0,
hours: 0,
minutes: 59
});
});
it("verifyStarEnd() doesn't do anything if endsAt if after startsAt", () => {
const startsAt = moment([2063, 1, 1, 0, 0, 0]);
const endsAt = moment([2063, 1, 1, 1, 1, 0]);
store.data.startsAt = startsAt;
store.data.endsAt = endsAt;
store.data.verifyStarEnd();
expect(store.data.startsAt.toISOString()).toBe(startsAt.toISOString());
expect(store.data.endsAt.toISOString()).toBe(endsAt.toISOString());
});
it("verifyStarEnd() updates startsAt if it's before now()", () => {
const now = moment().second(0);
const startsAt = moment([2000, 1, 1, 0, 0, 1]);
const endsAt = moment([2063, 1, 1, 0, 0, 0]);
store.data.startsAt = startsAt;
store.data.endsAt = endsAt;
store.data.verifyStarEnd();
expect(store.data.startsAt.isSameOrAfter(now)).toBeTruthy();
expect(store.data.endsAt.toISOString()).toBe(endsAt.toISOString());
});
it("verifyStarEnd() updates endsAt if it's before startsAt", () => {
const startsAt = moment([2063, 1, 1, 0, 0, 1]);
const endsAt = moment([2063, 1, 1, 0, 0, 0]);
store.data.startsAt = startsAt;
store.data.endsAt = endsAt;
store.data.verifyStarEnd();
expect(store.data.startsAt.toISOString()).toBe(startsAt.toISOString());
expect(store.data.endsAt.toISOString()).toBe(
moment([2063, 1, 1, 0, 1, 1]).toISOString()
);
});
it("incStart(7) adds 7 minutes to startsAt", () => {
const startsAt = moment([2063, 1, 1, 0, 0, 1]);
store.data.startsAt = startsAt;
store.data.incStart(7);
const diffMS = store.data.startsAt.diff(startsAt);
expect(diffMS).toBe(7 * 60 * 1000);
});
it("decStart(14) subtracts 14 minutes from startsAt", () => {
const startsAt = moment([2063, 1, 1, 0, 0, 1]);
store.data.startsAt = startsAt;
store.data.decStart(14);
const diffMS = store.data.startsAt.diff(startsAt);
expect(diffMS).toBe(-14 * 60 * 1000);
});
it("incEnd(120) adds 120 minutes to endsAt", () => {
const endsAt = moment([2063, 1, 1, 0, 0, 1]);
store.data.endsAt = endsAt;
store.data.incEnd(120);
const diffMS = store.data.endsAt.diff(endsAt);
expect(diffMS).toBe(120 * 60 * 1000);
});
it("decEnd(1) subtracts 1 minute from endsAt", () => {
const endsAt = moment([2063, 1, 1, 0, 0, 1]);
store.data.endsAt = endsAt;
store.data.decEnd(1);
const diffMS = store.data.endsAt.diff(endsAt);
expect(diffMS).toBe(-1 * 60 * 1000);
});
});

View File

@@ -26,6 +26,11 @@ Object {
"name": "cluster",
"value": "(dev|prod)",
},
Object {
"isRegex": false,
"name": "",
"value": "",
},
],
"startsAt": "2000-02-01T00:00:00.000Z",
}