mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 14:47:32 +00:00
@@ -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
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user