Fix SetupGuard error handling and add retry UI

This commit is contained in:
2026-04-19 20:20:31 +02:00
parent c58eb240b1
commit d0991e0d40
3 changed files with 70 additions and 8 deletions

View File

@@ -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 />;
}

View File

@@ -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();
});
});
});