Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions frontend/app/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<RootLayout><p>Body</p></RootLayout>);

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(<RootLayout><p>Body</p></RootLayout>);

const main = container.querySelector("main");
expect(main).not.toBeNull();
expect(main).toHaveAttribute("id", "main");
});

it("keeps the skip link visible when focused", () => {
render(<RootLayout><p>Body</p></RootLayout>);

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(<RootLayout><p>Body</p></RootLayout>);

expect(container.querySelector("main")?.className).not.toMatch(/overflow-x-hidden/);
});
});
39 changes: 39 additions & 0 deletions frontend/components/DisputeModal.f105-f108.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DisputeModal market={market} onDisputed={jest.fn()} />);

const dialog = screen.getByRole("dialog", { name: /dispute/i });
expect(dialog).toHaveAttribute("aria-modal", "true");
});

it("moves focus into the dialog on open", () => {
render(<DisputeModal market={market} onDisputed={jest.fn()} />);

expect(screen.getByRole("dialog")).toContainElement(document.activeElement as HTMLElement);
});

it("closes on Escape and returns focus to the trigger", () => {
const onClose = jest.fn();
render(<DisputeModal market={market} onDisputed={jest.fn()} onClose={onClose} />);

fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });

expect(onClose).toHaveBeenCalledTimes(1);
});

it("announces status changes through an aria-live region", () => {
render(<DisputeModal market={market} onDisputed={jest.fn()} />);

expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
});
});
32 changes: 32 additions & 0 deletions frontend/components/MarketStatusBadge.f105-f108.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<MarketStatusBadge status="Cancelled" cancelReason="Draw" />);

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(<MarketStatusBadge status="Cancelled" cancelReason="NoContest" />);

expect(screen.getByText("No Contest")).toBeInTheDocument();
expect(screen.getByText(/full refund/i)).toBeInTheDocument();
});

it("shows a distinct Admin cancelled badge", () => {
render(<MarketStatusBadge status="Cancelled" cancelReason="AdminCancelled" />);

expect(screen.getByText("Admin cancelled")).toBeInTheDocument();
});

it("keeps the generic Cancelled label when no reason is given", () => {
render(<MarketStatusBadge status="Cancelled" />);

expect(screen.getByText("Cancelled")).toBeInTheDocument();
});
});
37 changes: 37 additions & 0 deletions frontend/components/TransactionStatusModal.f105-f108.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TransactionStatusModal status="pending" onClose={jest.fn()} />);

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(<TransactionStatusModal status="pending" onClose={onClose} />);

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(<TransactionStatusModal status="success" onClose={onClose} />);

fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });

expect(onClose).toHaveBeenCalledTimes(1);
});

it("announces status changes through an aria-live region", () => {
render(<TransactionStatusModal status="pending" onClose={jest.fn()} />);

expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
});
});
42 changes: 42 additions & 0 deletions frontend/lib/format.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading