Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use separate confirmModals for reboot and shutdown; add tests for ActionBox and DebugLogBox #659

Merged
merged 1 commit into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions src/pages/Settings/ActionBox.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import type { FC, ReactElement } from "react";
import type { FC, ReactElement, ReactNode } from "react";

export type Props = {
name: string | ReactElement;
actionName: string;
action: () => void;
showChild: boolean;
children: ReactNode;
};

const ActionBox: FC<Props> = ({ name, action, actionName }) => {
/**
* displays a box with a title and a button which triggers an action (e.g. reboot)
* has a child component which is displayed if showChild is true
*/
const ActionBox: FC<Props> = ({
name,
action,
actionName,
showChild,
children,
}) => {
return (
<div className="box-border w-full transition-colors dark:text-white">
<article className="relative rounded bg-white p-5 shadow-xl dark:bg-gray-800">
<div className="flex justify-between">
<h4 className="flex w-1/2 items-center font-bold xl:w-2/3">
<span>{name}</span>
</h4>
<button className="bd-button w-1/2 py-1 xl:w-1/3" onClick={action}>
{actionName}
</button>
</div>
</article>
</div>
<>
{showChild && children}
<div className="box-border w-full transition-colors dark:text-white">
<article className="relative rounded bg-white p-5 shadow-xl dark:bg-gray-800">
<div className="flex justify-between">
<h4 className="flex w-1/2 items-center font-bold xl:w-2/3">
<span>{name}</span>
</h4>
<button className="bd-button w-1/2 py-1 xl:w-1/3" onClick={action}>
{actionName}
</button>
</div>
</article>
</div>
</>
);
};

Expand Down
3 changes: 3 additions & 0 deletions src/pages/Settings/DebugLogBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { toast } from "react-toastify";
import { checkError } from "utils/checkError";
import { instance } from "utils/interceptor";

/**
* Displays a button to generate a debug report which is downloaded as a file
*/
const DebugLogBox: FC = () => {
const { t } = useTranslation();
const { isGeneratingReport, setIsGeneratingReport } = useContext(AppContext);
Expand Down
3 changes: 3 additions & 0 deletions src/pages/Settings/I18nBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { FC } from "react";
import { useTranslation } from "react-i18next";
import I18nDropdown from "../../components/I18nDropdown";

/**
* Displays the current language and allows the user to change it.
*/
const I18nBox: FC = () => {
const { t } = useTranslation();

Expand Down
64 changes: 64 additions & 0 deletions src/pages/Settings/RebootModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC, useContext } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import ModalDialog from "../../layouts/ModalDialog";
import { AppContext } from "../../context/app-context";
import { instance } from "../../utils/interceptor";
import { MODAL_ROOT } from "../../utils";
import { HttpStatusCode } from "axios";

export type Props = {
confirmText: string;
confirmEndpoint?: string; // TODO #345 remove
onConfirm?: () => void;
onClose: () => void;
};

const btnClasses =
"w-full xl:w-1/2 text-center h-10 m-2 bg-yellow-500 hover:bg-yellow-400 rounded text-white";

const RebootModal: FC<Props> = ({
confirmText,
confirmEndpoint,
onConfirm,
onClose,
}) => {
const { t } = useTranslation();
const { setIsLoggedIn } = useContext(AppContext);
const navigate = useNavigate();

const shutdownHandler = async () => {
if (confirmEndpoint) {
const resp = await instance.post(confirmEndpoint);
if (resp.status === HttpStatusCode.Ok) {
setIsLoggedIn(false);
navigate("/login");
}
}
};

return createPortal(
<ModalDialog close={onClose}>
{confirmText}
<div className="flex flex-col p-3 xl:flex-row">
<button className={btnClasses} onClick={onClose}>
{t("settings.cancel")}
</button>
{onConfirm && (
<button className={btnClasses} onClick={onConfirm}>
{t("settings.confirm")}
</button>
)}
{!onConfirm && (
<button className={btnClasses} onClick={shutdownHandler}>
{t("settings.confirm")}
</button>
)}
</div>
</ModalDialog>,
MODAL_ROOT,
);
};

export default RebootModal;
64 changes: 64 additions & 0 deletions src/pages/Settings/ShutdownModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC, useContext } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import ModalDialog from "../../layouts/ModalDialog";
import { AppContext } from "../../context/app-context";
import { instance } from "../../utils/interceptor";
import { MODAL_ROOT } from "../../utils";
import { HttpStatusCode } from "axios";

export type Props = {
confirmText: string;
confirmEndpoint?: string; // TODO #345 remove
onConfirm?: () => void;
onClose: () => void;
};

const btnClasses =
"w-full xl:w-1/2 text-center h-10 m-2 bg-yellow-500 hover:bg-yellow-400 rounded text-white";

const ShutdownModal: FC<Props> = ({
confirmText,
confirmEndpoint,
onConfirm,
onClose,
}) => {
const { t } = useTranslation();
const { setIsLoggedIn } = useContext(AppContext);
const navigate = useNavigate();

const shutdownHandler = async () => {
if (confirmEndpoint) {
const resp = await instance.post(confirmEndpoint);
if (resp.status === HttpStatusCode.Ok) {
setIsLoggedIn(false);
navigate("/login");
}
}
};

return createPortal(
<ModalDialog close={onClose}>
{confirmText}
<div className="flex flex-col p-3 xl:flex-row">
<button className={btnClasses} onClick={onClose}>
{t("settings.cancel")}
</button>
{onConfirm && (
<button className={btnClasses} onClick={onConfirm}>
{t("settings.confirm")}
</button>
)}
{!onConfirm && (
<button className={btnClasses} onClick={shutdownHandler}>
{t("settings.confirm")}
</button>
)}
</div>
</ModalDialog>,
MODAL_ROOT,
);
};

export default ShutdownModal;
3 changes: 3 additions & 0 deletions src/pages/Settings/VersionBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ type Props = {
apiVersion: string;
};

/**
* Displays the versions of RaspiBlitz, the WebUI and the Blitz-API
*/
const VersionBox: FC<Props> = ({ platformVersion, apiVersion }) => {
const { t } = useTranslation();
return (
Expand Down
76 changes: 76 additions & 0 deletions src/pages/Settings/__tests__/ActionBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { render, fireEvent } from "test-utils";
import ActionBox from "../ActionBox";

describe("ActionBox component", () => {
it("renders the name prop", () => {
const { getByText } = render(
<ActionBox
name="Test Name"
action={() => {}}
actionName="Test Action"
showChild={false}
>
<div>Test Child</div>
</ActionBox>,
);
expect(getByText("Test Name")).toBeInTheDocument();
});

it("renders the action button with the correct text", () => {
const { getByText } = render(
<ActionBox
name="Test Name"
action={() => {}}
actionName="Test Action"
showChild={false}
>
<div>Test Child</div>
</ActionBox>,
);
expect(getByText("Test Action")).toBeInTheDocument();
});

it("calls the action prop when the button is clicked", () => {
const mockAction = vitest.fn();
const { getByText } = render(
<ActionBox
name="Test Name"
action={mockAction}
actionName="Test Action"
showChild={false}
>
<div>Test Child</div>
</ActionBox>,
);
fireEvent.click(getByText("Test Action"));
expect(mockAction).toHaveBeenCalled();
});

it("renders the child component when showChild is true", () => {
const { getByText } = render(
<ActionBox
name="Test Name"
action={() => {}}
actionName="Test Action"
showChild={true}
>
<div>Test Child</div>
</ActionBox>,
);
expect(getByText("Test Child")).toBeInTheDocument();
});

it("does not render the child component when showChild is false", () => {
const { queryByText } = render(
<ActionBox
name="Test Name"
action={() => {}}
actionName="Test Action"
showChild={false}
>
<div>Test Child</div>
</ActionBox>,
);
expect(queryByText("Test Child")).toBeNull();
});
});
51 changes: 51 additions & 0 deletions src/pages/Settings/__tests__/DebugLogBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { toast } from "react-toastify";
import { fireEvent, render, screen } from "test-utils";
import DebugLogBox from "../DebugLogBox";

vitest.mock("react-toastify", () => ({
toast: {
info: vitest.fn(),
error: vitest.fn(),
},
}));

describe("DebugLogBox", () => {
it("renders without crashing", () => {
render(<DebugLogBox />);
});

it("displays a button to generate a debug report", () => {
render(<DebugLogBox />);
const button = screen.getByRole("button", {
name: /settings.generate/i,
});
expect(button).toBeInTheDocument();
});

it("sets isGeneratingReport to true when the button is clicked", async () => {
const setIsGeneratingReport = vitest.fn();
render(<DebugLogBox />, {
providerOptions: {
appProps: {
setIsGeneratingReport,
},
},
});
const button = screen.getByRole("button", {
name: /settings.generate/i,
});
fireEvent.click(button);
expect(setIsGeneratingReport).toHaveBeenCalledWith(true);
});

it("displays a loading toast while the report is being generated", async () => {
render(<DebugLogBox />);
const button = screen.getByRole("button", {
name: /settings.generate/i,
});
fireEvent.click(button);
expect(toast.info).toHaveBeenCalledWith(expect.any(String), {
isLoading: true,
});
});
});
Loading