Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Failing after 37m50s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Failing after 42m10s
Conventional commits / commits (pull_request) Successful in 29s
Enable every plugin recommended preset and replace Biome with ESLint formatting. Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
import { MantineProvider } from "@mantine/core";
|
||
import { fireEvent, render, screen } from "@testing-library/react";
|
||
import { type FC, lazy, Suspense } from "react";
|
||
import { MemoryRouter, useLocation, useNavigate } from "react-router";
|
||
import { describe, expect, it, vi } from "vitest";
|
||
|
||
import { RouteErrorBoundary } from "./RouteErrorBoundary";
|
||
|
||
const session = { isAuthenticated: false };
|
||
vi.mock("@/features/auth", () => ({
|
||
useAuth: () => ((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||
{ user: session.isAuthenticated ? {} : null }
|
||
/* eslint-enable unicorn/no-null */)),
|
||
}));
|
||
|
||
const ThrowingRoute: FC = () => {
|
||
throw new Error("test route failure");
|
||
};
|
||
|
||
const HealthyRoute: FC = () => {
|
||
return (
|
||
<div>
|
||
{useLocation().pathname === "/healthy"
|
||
? (
|
||
"healthy route"
|
||
)
|
||
: (
|
||
<ThrowingRoute />
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const RouteHarness: FC = () => {
|
||
const navigate = useNavigate();
|
||
|
||
return (
|
||
<>
|
||
<button onClick={() => navigate("/healthy")} type="button">
|
||
go healthy
|
||
</button>
|
||
<RouteErrorBoundary>
|
||
<HealthyRoute />
|
||
</RouteErrorBoundary>
|
||
</>
|
||
);
|
||
};
|
||
|
||
function renderBoundary(path: string) {
|
||
return render(
|
||
<MantineProvider>
|
||
<MemoryRouter initialEntries={[path]}>
|
||
<RouteHarness />
|
||
</MemoryRouter>
|
||
</MantineProvider>,
|
||
);
|
||
}
|
||
|
||
describe("RouteErrorBoundary", () => {
|
||
it.each(["/accounts", "/login"])(
|
||
"recovers a transient failure through the fallback on %s itself",
|
||
async (path) => {
|
||
session.isAuthenticated = path === "/accounts";
|
||
let isFailing = true;
|
||
const RecoverableRoute: FC = () => {
|
||
if (isFailing) throw new Error("temporary failure");
|
||
return <div>recovered route</div>;
|
||
};
|
||
render(
|
||
<MantineProvider>
|
||
<MemoryRouter initialEntries={[path]}>
|
||
<RouteErrorBoundary>
|
||
<RecoverableRoute />
|
||
</RouteErrorBoundary>
|
||
</MemoryRouter>
|
||
</MantineProvider>,
|
||
);
|
||
const action = screen.getByRole("link", { name: "Продолжить" });
|
||
isFailing = false;
|
||
fireEvent.click(action);
|
||
expect(await screen.findByText("recovered route")).toBeInTheDocument();
|
||
},
|
||
);
|
||
|
||
it("keeps a persistent failure recoverable without an automatic reset loop", () => {
|
||
session.isAuthenticated = true;
|
||
renderBoundary("/accounts");
|
||
fireEvent.click(screen.getByRole("link", { name: "Продолжить" }));
|
||
expect(
|
||
screen.getByRole("heading", { name: "Не удалось открыть страницу" }),
|
||
).toBeInTheDocument();
|
||
expect(screen.queryByText("test route failure")).not.toBeInTheDocument();
|
||
expect(
|
||
screen.getByRole("link", { name: "Перезагрузить страницу" }),
|
||
).toHaveAttribute("href", "/accounts");
|
||
});
|
||
|
||
it("offers document navigation when React.lazy caches a rejected chunk", async () => {
|
||
session.isAuthenticated = false;
|
||
const load = vi.fn(() => Promise.reject(new Error("chunk failed")));
|
||
const Chunk = lazy(load);
|
||
render(
|
||
<MantineProvider>
|
||
<MemoryRouter initialEntries={["/login"]}>
|
||
<RouteErrorBoundary>
|
||
<Suspense fallback={<div>loading chunk</div>}>
|
||
<Chunk />
|
||
</Suspense>
|
||
</RouteErrorBoundary>
|
||
</MemoryRouter>
|
||
</MantineProvider>,
|
||
);
|
||
fireEvent.click(await screen.findByRole("link", { name: "Продолжить" }));
|
||
expect(load).toHaveBeenCalledTimes(1);
|
||
const reload = screen.getByRole("link", { name: "Перезагрузить страницу" });
|
||
expect(reload).toHaveAttribute("href", "/login");
|
||
// Cancel browser navigation in JSDOM only after proving React Router did not intercept it.
|
||
let isIntercepted = true;
|
||
const preventNavigation = (event: MouseEvent) => {
|
||
isIntercepted = event.defaultPrevented;
|
||
event.preventDefault();
|
||
};
|
||
document.addEventListener("click", preventNavigation, { once: true });
|
||
fireEvent.click(reload);
|
||
expect(isIntercepted).toBe(false);
|
||
});
|
||
|
||
it("offers login after a public route error without error details", () => {
|
||
session.isAuthenticated = false;
|
||
renderBoundary("/login");
|
||
expect(document.title).toBe("Не удалось открыть страницу — WealthLedger");
|
||
expect(screen.getByRole("link", { name: "Продолжить" })).toHaveAttribute(
|
||
"href",
|
||
"/login",
|
||
);
|
||
expect(screen.queryByText("test route failure")).not.toBeInTheDocument();
|
||
});
|
||
|
||
it("offers accounts after a protected route error and resets on navigation", async () => {
|
||
session.isAuthenticated = true;
|
||
renderBoundary("/accounts");
|
||
fireEvent.click(screen.getByRole("button", { name: "go healthy" }));
|
||
expect(await screen.findByText("healthy route")).toBeInTheDocument();
|
||
});
|
||
});
|