Add EnableDisable tests

Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
Mcat12
2019-05-28 18:31:21 -07:00
parent 314407fba1
commit a6d9ade69e
3 changed files with 399 additions and 9 deletions
+34 -7
View File
@@ -35,6 +35,10 @@ import {
export interface EnableDisableProps extends WithNamespaces {
status: Status;
refresh: (data?: ApiStatus) => void;
onSetStatus: (
action: StatusAction,
time?: number
) => Promise<ApiSuccessResponse>;
}
export interface EnableDisableState {
@@ -44,7 +48,10 @@ export interface EnableDisableState {
customMultiplier: number;
}
class EnableDisable extends Component<EnableDisableProps, EnableDisableState> {
export class EnableDisable extends Component<
EnableDisableProps,
EnableDisableState
> {
state: EnableDisableState = {
processing: false,
customModalShown: false,
@@ -82,17 +89,26 @@ class EnableDisable extends Component<EnableDisableProps, EnableDisableState> {
}
// Only allow one status update at a time
this.setState({ processing: true });
this.toggleProcessing();
// Send the status change request
this.updateHandler = makeCancelable(api.setStatus(action, time));
this.updateHandler = makeCancelable(this.props.onSetStatus(action, time));
this.updateHandler.promise
// Refresh once we get a good response
.then(() =>
this.props.refresh({ status: this.getStatusFromAction(action) })
)
// Even if it failed, allow new status changes
.finally(() => this.setState({ processing: false }));
// Allow new status changes when finished
.then(this.toggleProcessing)
.catch(e => {
// Ignore canceled requests
if (e.isCanceled) {
return;
}
// Even if it failed, allow new status changes
this.toggleProcessing();
});
};
componentWillUnmount() {
@@ -108,6 +124,13 @@ class EnableDisable extends Component<EnableDisableProps, EnableDisableState> {
this.setState({ customModalShown: !this.state.customModalShown });
};
/**
* Toggle the processing flag
*/
toggleProcessing = () => {
this.setState(prevState => ({ processing: !prevState.processing }));
};
/**
* Submit the request to disable blocking for the custom time that the user
* input.
@@ -228,10 +251,14 @@ class EnableDisable extends Component<EnableDisableProps, EnableDisableState> {
export const TranslatedEnableDisable = withNamespaces("common")(EnableDisable);
export default () => (
export const EnableDisableContainer = () => (
<StatusContext.Consumer>
{({ status, refresh }) => (
<TranslatedEnableDisable status={status} refresh={refresh} />
<TranslatedEnableDisable
status={status}
refresh={refresh}
onSetStatus={api.setStatus}
/>
)}
</StatusContext.Consumer>
);
@@ -0,0 +1,363 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* EnableDisable component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import { mount, shallow, ShallowWrapper } from "enzyme";
import {
EnableDisable,
EnableDisableContainer,
EnableDisableProps,
EnableDisableState,
TranslatedEnableDisable
} from "../EnableDisable";
import React, { MouseEvent } from "react";
import { StatusContext, StatusContextType } from "../context/StatusContext";
import NavButton from "../NavButton";
import NavDropdown from "../NavDropdown";
import { Form, Input, Modal, ModalFooter, ModalHeader } from "reactstrap";
const tick = global.tick;
type EnableDisableWrapper = ShallowWrapper<
EnableDisableProps,
EnableDisableState,
EnableDisable
>;
describe("EnableDisableContainer", () => {
it("should use context", () => {
const context: StatusContextType = {
status: "unknown",
refresh: jest.fn()
};
// Using mount here as there are issues with passing functions via context
// when using shallow (the passed in value is not equal to the received
// value)
const wrapper = mount(
<StatusContext.Provider value={context}>
<EnableDisableContainer />
</StatusContext.Provider>
);
const props = wrapper.find(EnableDisable).props();
expect(props.status).toEqual(context.status);
expect(props.refresh).toEqual(context.refresh);
});
});
describe("EnableDisable", () => {
describe("API calls", () => {
/**
* Test clicking on a button and expecting it to call setStatus
*
* @param initialStatus The initial status
* @param buttonIndex The index of the button to click
* @param setStatusArgs The arguments that are expected for setStatus
* @param expectedStatus The expected status given to to refresh function
*/
const testCall = async (
initialStatus: Status,
buttonIndex: number,
setStatusArgs: Array<any>,
expectedStatus: any
) => {
const setStatus = jest.fn(() => Promise.resolve({ status: "success" }));
const refresh = jest.fn();
const wrapper = shallow(
<TranslatedEnableDisable
refresh={refresh}
status={initialStatus}
onSetStatus={setStatus}
/>
);
wrapper
.find(NavButton)
.at(buttonIndex)
.props()
.onClick({} as MouseEvent);
// Wait for the setStatus promise to resolve
await tick();
expect(setStatus).toHaveBeenCalledWith(...setStatusArgs);
expect(refresh).toHaveBeenCalledWith({ status: expectedStatus });
};
it("calls the API to enable when the enable button is clicked", async () => {
await testCall("disabled", 0, ["enable", undefined], "enabled");
});
it("calls the API to disable when the disable permanently button is clicked", async () => {
await testCall("enabled", 0, ["disable", undefined], "disabled");
});
it("calls the API to disable when the 10 second button is clicked", async () => {
await testCall("enabled", 1, ["disable", 10], "disabled");
});
it("calls the API to disable when the 30 second button is clicked", async () => {
await testCall("enabled", 2, ["disable", 30], "disabled");
});
it("calls the API to disable when the 5 minute button is clicked", async () => {
await testCall("enabled", 3, ["disable", 5 * 60], "disabled");
});
it("waits for the first call to finish before sending another", () => {
const setStatus = jest.fn(() => Promise.resolve({ status: "success" }));
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="disabled"
onSetStatus={setStatus}
/>
);
wrapper.setState({ processing: true });
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
expect(setStatus).not.toHaveBeenCalled();
});
it("cancels an in-flight request when unmounting", async () => {
const setStatus = jest.fn(() => Promise.resolve({ status: "success" }));
const refresh = jest.fn();
const wrapper = shallow(
<TranslatedEnableDisable
refresh={refresh}
status="disabled"
onSetStatus={setStatus}
/>
);
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
wrapper.unmount();
// Wait for the setStatus promise to resolve
await tick();
expect(refresh).not.toHaveBeenCalled();
});
it("resets processing flag if a setStatus request fails", async () => {
const setStatus = jest.fn(() => Promise.reject({ error: "test" }));
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="disabled"
onSetStatus={setStatus}
/>
);
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
expect(wrapper.state().processing).toBeTruthy();
// Wait for the setStatus promise to resolve
await tick();
expect(wrapper.state().processing).toBeFalsy();
});
it("calls the API with the custom time specified via modal, and closes the modal", () => {
const setStatus = jest.fn(() => Promise.resolve({ status: "success" }));
const event = { preventDefault: jest.fn() };
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={setStatus}
/>
);
wrapper.setState({ customModalShown: true });
wrapper
.find(Form)
.props()
.onSubmit(event);
expect(wrapper.find(Modal).props().isOpen).toBeFalsy();
expect(setStatus).toHaveBeenCalledWith("disable", 60 * 60);
});
});
describe("interactions", () => {
it("opens the custom time modal with the corresponding button is clicked", () => {
const wrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
expect(wrapper.find(Modal).props().isOpen).toBeFalsy();
wrapper
.find(NavButton)
.last()
.props()
.onClick({} as MouseEvent);
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
});
it("should close the modal when clicking outside", () => {
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
wrapper.setState({ customModalShown: true });
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
wrapper
.find(Modal)
.props()
.toggle();
expect(wrapper.find(Modal).props().isOpen).toBeFalsy();
});
it("should close the modal when the header close button is clicked", () => {
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
wrapper.setState({ customModalShown: true });
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
wrapper
.find(ModalHeader)
.props()
.toggle();
expect(wrapper.find(Modal).props().isOpen).toBeFalsy();
});
it("should close the modal when the cancel button is clicked", () => {
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
wrapper.setState({ customModalShown: true });
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
wrapper
.find(ModalFooter)
.childAt(1)
.props()
.onClick();
expect(wrapper.find(Modal).props().isOpen).toBeFalsy();
});
it("should allow custom disable times", () => {
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
wrapper.setState({ customModalShown: true });
// Check the default
let timeInput = wrapper.find(Input).first();
expect(timeInput).toHaveValue(60);
timeInput.props().onChange({ target: { value: "42" } });
timeInput = wrapper.find(Input).first();
expect(timeInput).toHaveValue(42);
});
it("should allow changing the custom disable time unit", () => {
const wrapper: EnableDisableWrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
wrapper.setState({ customModalShown: true });
// Check the default
let timeUnit = wrapper.find(Input).last();
expect(timeUnit).toHaveValue(60);
timeUnit.props().onChange({ target: { value: "1" } });
timeUnit = wrapper.find(Input).last();
expect(timeUnit).toHaveValue(1);
});
});
describe("rendering", () => {
it("renders null if status is unknown", () => {
const wrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="unknown"
onSetStatus={jest.fn()}
/>
);
expect(wrapper).toBeEmptyRender();
});
it("shows an enable button if status is disabled", () => {
const wrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="disabled"
onSetStatus={jest.fn()}
/>
);
expect(wrapper.find(NavButton).props().name).toEqual("Enable");
});
it("shows a dropdown with disable buttons if status is enabled", () => {
const wrapper = shallow(
<TranslatedEnableDisable
refresh={jest.fn()}
status="enabled"
onSetStatus={jest.fn()}
/>
);
expect(wrapper.find(NavDropdown)).toExist();
expect(wrapper.find(NavButton)).toHaveLength(5);
});
});
});
+2 -2
View File
@@ -17,7 +17,7 @@ import Versions from "./views/Versions";
import Networking from "./views/Networking";
import Login from "./views/Login";
import Logout from "./views/Logout";
import EnableDisable from "./components/common/EnableDisable";
import { EnableDisableContainer } from "./components/common/EnableDisable";
import Preferences from "./views/Preferences";
import i18next from "i18next";
import { ComponentType } from "react";
@@ -109,7 +109,7 @@ export const nav: Array<RouteData> = [
]
},
{
customComponent: EnableDisable,
customComponent: EnableDisableContainer,
fakeRoute: true,
auth: true
},