From 6a6175fd004e4c51e7797e269b36e80af2360e46 Mon Sep 17 00:00:00 2001 From: Ify Justin Date: Mon, 20 Jul 2026 11:21:05 +0000 Subject: [PATCH] feat: add catch-all 404 NotFound route - Add src/pages/NotFound.tsx with clear 404 message, the unrecognised path, quick-nav links back to main sections, and a go-back button - Register }> as the last route in App.tsx inside the existing ErrorBoundary/ Suspense layout - Add test/NotFound.test.tsx with 8 tests covering unknown path rendering and known routes being unaffected Closes #19 --- src/App.tsx | 2 + src/pages/NotFound.tsx | 114 ++++++++++++++++++++++++++++++++++++++++ test/NotFound.test.tsx | 116 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 src/pages/NotFound.tsx create mode 100644 test/NotFound.test.tsx diff --git a/src/App.tsx b/src/App.tsx index f675c83..256243f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ const SetupPage = lazy(() => import("./pages/SetupPage")); const BatchMultiCall = lazy(() => import("./pages/BatchMultiCall")); const SubInvocationPage = lazy(() => import("./pages/SubInvocationPage")); const RateLimitDashboard = lazy(() => import("./pages/RateLimitDashboard")); +const NotFound = lazy(() => import("./pages/NotFound")); function Fallback() { return

Loading…

; @@ -45,6 +46,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..d4988e2 --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,114 @@ +import { Link, useLocation } from "react-router-dom"; + +const NAV_LINKS: { label: string; to: string; description: string }[] = [ + { label: "Home", to: "/", description: "Recent contract events" }, + { label: "Search", to: "/search", description: "Find contracts, wallets & events" }, + { label: "Graph", to: "/graph", description: "Contract relationship graph" }, + { label: "XDR Inspector", to: "/xdr", description: "Decode XDR envelopes" }, + { label: "Sandbox", to: "/sandbox", description: "Prototype against live contracts" }, +]; + +export default function NotFound() { + const { pathname } = useLocation(); + + return ( +
+ {/* Status code */} + + + {/* Heading */} +

Page not found

+ + {/* Path that was requested */} +

+ + {pathname} + {" "} + doesn't match any known route. +

+ + {/* Quick-nav back into the app */} + + + {/* Go back button */} + +
+ ); +} diff --git a/test/NotFound.test.tsx b/test/NotFound.test.tsx new file mode 100644 index 0000000..b0559c7 --- /dev/null +++ b/test/NotFound.test.tsx @@ -0,0 +1,116 @@ +/** + * Issue #19 — Add a catch-all 404 route + * + * Tests: + * 1. Navigating to an unknown path renders the NotFound component. + * 2. The NotFound page displays the requested path. + * 3. The NotFound page provides at least one working link back into the app. + * 4. Known routes (e.g. "/search") are NOT swallowed by the catch-all. + */ + +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import NotFound from "../src/pages/NotFound"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeQC() { + return new QueryClient({ defaultOptions: { queries: { retry: false } } }); +} + +/** + * Render the full route tree (mirroring App.tsx) at a given initial path. + * Keeps the test scope narrow: only the routes we need to exercise here. + */ +function renderAt(initialPath: string) { + const qc = makeQC(); + return render( + + + + {/* Known route — a lightweight stub so we can verify it wins */} + SearchPage} /> + {/* Catch-all — must come last */} + } /> + + + , + ); +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("NotFound — catch-all 404 route", () => { + it("renders '404' heading at an unknown path", () => { + renderAt("/this/does/not/exist"); + expect(screen.getByText("404")).toBeDefined(); + }); + + it("displays the unrecognised path in the message", () => { + renderAt("/totally/unknown"); + // The component renders the pathname in a block + expect(screen.getByText("/totally/unknown")).toBeDefined(); + }); + + it("shows 'Page not found' heading", () => { + renderAt("/bad-path"); + expect(screen.getByRole("heading", { name: /page not found/i })).toBeDefined(); + }); + + it("renders at least one link back to the home page", () => { + renderAt("/nonexistent"); + // The nav list includes a "Home" link pointing to "/" + const homeLink = screen.getByRole("link", { name: /home/i }); + expect(homeLink).toBeDefined(); + expect((homeLink as HTMLAnchorElement).getAttribute("href")).toBe("/"); + }); + + it("renders all five quick-nav links", () => { + renderAt("/nonexistent"); + const links = ["Home", "Search", "Graph", "XDR Inspector", "Sandbox"]; + links.forEach((label) => { + expect(screen.getByRole("link", { name: new RegExp(label, "i") })).toBeDefined(); + }); + }); + + it("renders the go-back button", () => { + renderAt("/nonexistent"); + expect(screen.getByRole("button", { name: /go back/i })).toBeDefined(); + }); +}); + +describe("Known routes — unaffected by catch-all", () => { + it("renders SearchPage at /search, not the NotFound page", () => { + renderAt("/search"); + // Known route content should appear + expect(screen.getByText("SearchPage")).toBeDefined(); + // 404 heading must NOT be visible + expect(screen.queryByText("404")).toBeNull(); + }); + + it("does not show NotFound at the root path", () => { + const qc = makeQC(); + render( + + + + HomePage} /> + } /> + + + , + ); + expect(screen.getByText("HomePage")).toBeDefined(); + expect(screen.queryByText("404")).toBeNull(); + }); +});