diff --git a/frontend/app/layout.test.tsx b/frontend/app/layout.test.tsx
new file mode 100644
index 00000000..eb1c4629
--- /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/);
+ });
+});
diff --git a/frontend/components/DisputeModal.f105-f108.test.tsx b/frontend/components/DisputeModal.f105-f108.test.tsx
new file mode 100644
index 00000000..bbe9e37f
--- /dev/null
+++ b/frontend/components/DisputeModal.f105-f108.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/MarketStatusBadge.f105-f108.test.tsx b/frontend/components/MarketStatusBadge.f105-f108.test.tsx
new file mode 100644
index 00000000..a198830b
--- /dev/null
+++ b/frontend/components/MarketStatusBadge.f105-f108.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();
+ });
+});
diff --git a/frontend/components/TransactionStatusModal.f105-f108.test.tsx b/frontend/components/TransactionStatusModal.f105-f108.test.tsx
new file mode 100644
index 00000000..d8f64bc8
--- /dev/null
+++ b/frontend/components/TransactionStatusModal.f105-f108.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");
+ });
+});
diff --git a/frontend/lib/format.test.ts b/frontend/lib/format.test.ts
new file mode 100644
index 00000000..b5dcec0b
--- /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");
+ });
+});