Fix SetupGuard error handling and add retry UI
This commit is contained in:
@@ -89,7 +89,9 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr
|
||||
|
||||
---
|
||||
|
||||
### TASK-005 — `SetupGuard` redirects to `/setup` when the backend is temporarily unreachable
|
||||
### TASK-005 — `SetupGuard` redirects to `/setup` when the backend is temporarily unreachable (done)
|
||||
|
||||
**Where fixed:** `frontend/src/components/SetupGuard.tsx`
|
||||
|
||||
**Where found:** `frontend/src/components/SetupGuard.tsx`. When `useSetup` returns `{ loading: false, status: null }` due to a network error, the guard treats this the same as "setup not completed" and redirects to `/setup`.
|
||||
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
*/
|
||||
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { Spinner, makeStyles } from "@fluentui/react-components";
|
||||
import {
|
||||
Button,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
Spinner,
|
||||
makeStyles,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import { useSetup } from "../hooks/useSetup";
|
||||
|
||||
/**
|
||||
@@ -22,7 +29,7 @@ interface SetupGuardProps {
|
||||
/**
|
||||
* Render `children` only when setup has been completed.
|
||||
*
|
||||
* Redirects to `/setup` if setup is still pending.
|
||||
* Redirects to `/setup` only when setup status is known and incomplete.
|
||||
*/
|
||||
const useStyles = makeStyles({
|
||||
loadingWrapper: {
|
||||
@@ -31,11 +38,30 @@ const useStyles = makeStyles({
|
||||
alignItems: "center",
|
||||
minHeight: "100vh",
|
||||
},
|
||||
errorContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "100vh",
|
||||
padding: tokens.spacingHorizontalM,
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
},
|
||||
errorCard: {
|
||||
width: "100%",
|
||||
maxWidth: "520px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalM,
|
||||
padding: tokens.spacingVerticalL,
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
borderRadius: tokens.borderRadiusXLarge,
|
||||
boxShadow: tokens.shadow8,
|
||||
},
|
||||
});
|
||||
|
||||
export function SetupGuard({ children }: SetupGuardProps): React.JSX.Element {
|
||||
const styles = useStyles();
|
||||
const { status, loading } = useSetup();
|
||||
const { status, loading, error, refresh } = useSetup();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -45,6 +71,21 @@ export function SetupGuard({ children }: SetupGuardProps): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (error && status === null) {
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorCard}>
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
<Button appearance="primary" onClick={() => void refresh()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status?.completed) {
|
||||
return <Navigate to="/setup" replace />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter, Routes, Route } from "react-router-dom";
|
||||
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||
import { SetupGuard } from "../SetupGuard";
|
||||
@@ -65,13 +65,32 @@ describe("SetupGuard", () => {
|
||||
expect(screen.queryByTestId("protected-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("redirects to /setup when the API call fails", async () => {
|
||||
// Task 0.3: a failed check must redirect to /setup, not allow through.
|
||||
it("renders an error card when the setup status check fails", async () => {
|
||||
mockedGetSetupStatus.mockRejectedValue(new Error("Network error"));
|
||||
renderGuard();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("setup-page")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/network error/i)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("protected-content")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("setup-page")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("retries setup status fetch when Retry is clicked", async () => {
|
||||
mockedGetSetupStatus.mockRejectedValueOnce(new Error("Network error"));
|
||||
mockedGetSetupStatus.mockResolvedValueOnce({ completed: true });
|
||||
|
||||
renderGuard();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /retry/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("protected-content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user