From 855bfb203f698b883d8e45b598f402306b1f0c41 Mon Sep 17 00:00:00 2001 From: uchechithelmaonye-cpu Date: Mon, 19 Oct 2026 06:59:49 +0000 Subject: [PATCH 1/4] test(frontend): cover Draw, No Contest and admin-cancel badges Closes #1443 --- .../components/MarketStatusBadge.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 frontend/components/MarketStatusBadge.test.tsx diff --git a/frontend/components/MarketStatusBadge.test.tsx b/frontend/components/MarketStatusBadge.test.tsx new file mode 100644 index 000000000..a198830b6 --- /dev/null +++ b/frontend/components/MarketStatusBadge.test.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { MarketStatusBadge } from "./MarketStatusBadge"; + +describe("MarketStatusBadge outcome labels (F-105)", () => { + it("shows a distinct Draw badge with refund copy", () => { + render(); + + expect(screen.getByText("Draw")).toBeInTheDocument(); + expect(screen.getByText(/full refund/i)).toBeInTheDocument(); + expect(screen.queryByText("Cancelled")).not.toBeInTheDocument(); + }); + + it("shows a distinct No Contest badge with refund copy", () => { + render(); + + expect(screen.getByText("No Contest")).toBeInTheDocument(); + expect(screen.getByText(/full refund/i)).toBeInTheDocument(); + }); + + it("shows a distinct Admin cancelled badge", () => { + render(); + + expect(screen.getByText("Admin cancelled")).toBeInTheDocument(); + }); + + it("keeps the generic Cancelled label when no reason is given", () => { + render(); + + expect(screen.getByText("Cancelled")).toBeInTheDocument(); + }); +}); From 65cb61b97362aa4762215fe2690754355b1faa49 Mon Sep 17 00:00:00 2001 From: uchechithelmaonye-cpu Date: Mon, 19 Oct 2026 07:28:59 +0000 Subject: [PATCH 2/4] test(frontend): cover dialog semantics for dispute and transaction modals Closes #1444 --- frontend/components/DisputeModal.test.tsx | 39 +++++++++++++++++++ .../TransactionStatusModal.test.tsx | 37 ++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 frontend/components/DisputeModal.test.tsx create mode 100644 frontend/components/TransactionStatusModal.test.tsx diff --git a/frontend/components/DisputeModal.test.tsx b/frontend/components/DisputeModal.test.tsx new file mode 100644 index 000000000..bbe9e37f9 --- /dev/null +++ b/frontend/components/DisputeModal.test.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { DisputeModal } from "./DisputeModal"; +import { Market } from "@/lib/api"; + +const market = { + id: "market-123", + status: "Resolved", +} as unknown as Market; + +describe("DisputeModal accessibility (F-106)", () => { + it("renders as a labelled modal dialog", () => { + render(); + + const dialog = screen.getByRole("dialog", { name: /dispute/i }); + expect(dialog).toHaveAttribute("aria-modal", "true"); + }); + + it("moves focus into the dialog on open", () => { + render(); + + expect(screen.getByRole("dialog")).toContainElement(document.activeElement as HTMLElement); + }); + + it("closes on Escape and returns focus to the trigger", () => { + const onClose = jest.fn(); + render(); + + fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" }); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("announces status changes through an aria-live region", () => { + render(); + + expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite"); + }); +}); diff --git a/frontend/components/TransactionStatusModal.test.tsx b/frontend/components/TransactionStatusModal.test.tsx new file mode 100644 index 000000000..d8f64bc82 --- /dev/null +++ b/frontend/components/TransactionStatusModal.test.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { TransactionStatusModal } from "./TransactionStatusModal"; + +describe("TransactionStatusModal accessibility (F-106)", () => { + it("renders as a labelled modal dialog", () => { + render(); + + const dialog = screen.getByRole("dialog", { name: /transaction/i }); + expect(dialog).toHaveAttribute("aria-modal", "true"); + }); + + it("ignores Escape while a transaction is in flight and announces it", () => { + const onClose = jest.fn(); + render(); + + fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" }); + + expect(onClose).not.toHaveBeenCalled(); + expect(screen.getByRole("status")).toHaveTextContent(/in progress/i); + }); + + it("closes on Escape once the transaction has settled", () => { + const onClose = jest.fn(); + render(); + + fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" }); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("announces status changes through an aria-live region", () => { + render(); + + expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite"); + }); +}); From 060d431abacec6695fe5012de179f34a5e84841c Mon Sep 17 00:00:00 2001 From: uchechithelmaonye-cpu Date: Mon, 19 Oct 2026 07:29:18 +0000 Subject: [PATCH 3/4] test(frontend): cover skip link and main landmark in root layout Closes #1445 --- frontend/app/layout.test.tsx | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/app/layout.test.tsx diff --git a/frontend/app/layout.test.tsx b/frontend/app/layout.test.tsx new file mode 100644 index 000000000..eb1c4629e --- /dev/null +++ b/frontend/app/layout.test.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import RootLayout from "./layout"; + +jest.mock("@/app/globals.css", () => ({})); + +describe("RootLayout skip link and focus (F-107)", () => { + it("renders a skip link as the first focusable element", () => { + render(

Body

); + + const skipLink = screen.getByRole("link", { name: /skip to content/i }); + expect(skipLink).toHaveAttribute("href", "#main"); + }); + + it("targets a main landmark with the id used by the skip link", () => { + const { container } = render(

Body

); + + const main = container.querySelector("main"); + expect(main).not.toBeNull(); + expect(main).toHaveAttribute("id", "main"); + }); + + it("keeps the skip link visible when focused", () => { + render(

Body

); + + const skipLink = screen.getByRole("link", { name: /skip to content/i }); + expect(skipLink.className).toMatch(/sr-only/); + expect(skipLink.className).toMatch(/focus:not-sr-only/); + }); + + it("does not clip focus outlines on main", () => { + const { container } = render(

Body

); + + expect(container.querySelector("main")?.className).not.toMatch(/overflow-x-hidden/); + }); +}); From eb4ada1dea3b3d450d4a23392080516ea99dfa0d Mon Sep 17 00:00:00 2001 From: uchechithelmaonye-cpu Date: Mon, 19 Oct 2026 07:29:25 +0000 Subject: [PATCH 4/4] test(frontend): cover Intl-based XLM, date and countdown formatting Closes #1446 --- frontend/lib/format.test.ts | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 frontend/lib/format.test.ts diff --git a/frontend/lib/format.test.ts b/frontend/lib/format.test.ts new file mode 100644 index 000000000..b5dcec0bc --- /dev/null +++ b/frontend/lib/format.test.ts @@ -0,0 +1,42 @@ +import { formatXlm, formatDateTime, formatRelative } from "./format"; + +describe("formatXlm (F-108)", () => { + it("formats whole and fractional XLM with grouping and seven decimals trimmed to two", () => { + expect(formatXlm(1234.5, "en-US")).toBe("1,234.50 XLM"); + expect(formatXlm("0.1", "en-US")).toBe("0.10 XLM"); + }); + + it("uses the requested locale separators", () => { + expect(formatXlm(1234.5, "de-DE")).toBe("1.234,50 XLM"); + }); +}); + +describe("formatDateTime (F-108)", () => { + const iso = "2026-07-09T20:00:00Z"; + + it("renders the instant in the given zone with a zone label", () => { + const text = formatDateTime(iso, { locale: "en-US", timeZone: "America/New_York" }); + + expect(text).toContain("4:00 PM"); + expect(text).toMatch(/EDT|GMT-4/); + }); + + it("renders the same instant in UTC with a UTC label", () => { + const text = formatDateTime(iso, { locale: "en-US", timeZone: "UTC" }); + + expect(text).toContain("8:00 PM"); + expect(text).toMatch(/UTC|GMT/); + }); +}); + +describe("formatRelative (F-108)", () => { + const now = new Date("2026-07-09T12:00:00Z"); + + it("describes a future betting close in hours", () => { + expect(formatRelative("2026-07-09T20:00:00Z", { now, locale: "en-US" })).toBe("in 8 hours"); + }); + + it("describes a past close in minutes", () => { + expect(formatRelative("2026-07-09T11:30:00Z", { now, locale: "en-US" })).toBe("30 minutes ago"); + }); +});