diff --git a/frontend/app/(protected)/documents/[id]/not-found.tsx b/frontend/app/(protected)/documents/[id]/not-found.tsx index fc62127a..0dc9b9b2 100644 --- a/frontend/app/(protected)/documents/[id]/not-found.tsx +++ b/frontend/app/(protected)/documents/[id]/not-found.tsx @@ -1,42 +1,16 @@ -"use client"; - -import Link from "next/link"; +import { NotFoundContent } from "@/components/NotFoundContent"; /** - * Scoped not-found for the document detail route. Distinct from a - * verification failure: a missing hash means no record was ever - * anchored, not that the verifier adjudicated and rejected it. - * - * This file is intentionally outside the [locale] boundary so the - * English-only copy is rendered consistently regardless of locale - * (the document ID itself is locale-invariant). + * Scoped not-found for the document detail route. A missing hash means no + * record was ever anchored, not that the verifier adjudicated and rejected it. + * This boundary has no locale parameter, so it uses the default English copy. */ export default function DocumentNotFound() { return ( -
-

No record anchored

-

- This document hash has no record anchored on the Stellar ledger. -

-

- This is different from a verification failure. A failed verification - means a record exists but did not match what you submitted; an - unanchored hash means no party has ever published this hash, so - there is nothing to verify against. -

-

- This is a 404 (record not found), not a 422 or 500. The verifier - did not adjudicate. -

- - Back to home - -
+ ); } diff --git a/frontend/app/[locale]/not-found.tsx b/frontend/app/[locale]/not-found.tsx index 5baddc44..c5058b5a 100644 --- a/frontend/app/[locale]/not-found.tsx +++ b/frontend/app/[locale]/not-found.tsx @@ -1,5 +1,5 @@ import { getTranslations, setRequestLocale } from "next-intl/server"; -import { Link } from "@/i18n/navigation"; +import { NotFoundContent } from "@/components/NotFoundContent"; export default async function NotFound({ params, @@ -13,18 +13,11 @@ export default async function NotFound({ const t = await getTranslations("notFound"); return ( -
-

404

-

{t("description")}

- - {t("home")} - -

- {t("docNotFoundDescription")} -

-
+ ); } diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx index 3d04afce..3e0466f6 100644 --- a/frontend/app/not-found.tsx +++ b/frontend/app/not-found.tsx @@ -1,29 +1,18 @@ -import Link from "next/link"; +import { NotFoundContent } from "@/components/NotFoundContent"; /** * Root fallback for any request that bypasses the locale boundary. * Lives outside app/[locale]/ because Next.js renders this when * notFound() is called from a route with no nearer not-found.tsx. * - * NOTE: this file MUST NOT emit or . The root - * app/layout.tsx already provides the document frame, and a - * second / here triggers a hydration error. Return - * only the inner content tree so Next.js mounts it inside the - * existing root layout. + * The root fallback uses the default English copy because no locale is + * available at this boundary. */ export default function GlobalNotFound() { return ( -
-

404

-

- The page you are looking for could not be found. -

- - Go back home - -
+ ); } diff --git a/frontend/components/NotFoundContent.tsx b/frontend/components/NotFoundContent.tsx new file mode 100644 index 00000000..537fe8ce --- /dev/null +++ b/frontend/components/NotFoundContent.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; + +interface NotFoundContentProps { + description: string; + homeLabel: string; + homeHref?: string; + detailDescription?: string; +} + +export function NotFoundContent({ + description, + homeLabel, + homeHref = "/", + detailDescription, +}: NotFoundContentProps) { + return ( +
+

404

+

{description}

+ + {homeLabel} + + {detailDescription ? ( +

{detailDescription}

+ ) : null} +
+ ); +} diff --git a/frontend/test-utils/not-found.test.tsx b/frontend/test-utils/not-found.test.tsx new file mode 100644 index 00000000..a077382a --- /dev/null +++ b/frontend/test-utils/not-found.test.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import GlobalNotFound from "@/app/not-found"; +import LocaleNotFound from "@/app/[locale]/not-found"; +import DocumentNotFound from "@/app/(protected)/documents/[id]/not-found"; + +jest.mock("next-intl/server", () => ({ + getTranslations: async () => (key: string) => + ({ + description: "The page you are looking for could not be found.", + home: "Go back home", + docNotFoundDescription: "The requested document was not found.", + })[key], + setRequestLocale: jest.fn(), +})); + +jest.mock("@/i18n/navigation", () => ({ + Link: ({ children, ...props }: React.AnchorHTMLAttributes) => ( + {children} + ), +})); + +describe("not-found boundaries", () => { + it("renders the root boundary with default-locale copy", () => { + render(); + + expect(screen.getByRole("heading", { name: "404" })).toBeInTheDocument(); + expect(screen.getByText("The page you are looking for could not be found.")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Go back home" })).toHaveAttribute("href", "/"); + }); + + it("renders the locale boundary with translated copy", async () => { + render(await LocaleNotFound({ params: Promise.resolve({ locale: "fr" }) })); + + expect(screen.getByText("The page you are looking for could not be found.")).toBeInTheDocument(); + expect(screen.getByText("The requested document was not found.")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Go back home" })).toHaveAttribute("href", "/fr"); + }); + + it("renders the document boundary with the same branded structure", () => { + render(); + + expect(screen.getByRole("heading", { name: "404" })).toBeInTheDocument(); + expect(screen.getByText("No record anchored")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Back to home" })).toHaveAttribute("href", "/"); + }); +});