diff --git a/src/components/settings/__tests__/about-section-update-badge.test.tsx b/src/components/settings/__tests__/about-section-update-badge.test.tsx index f40e79798..b9cc3520e 100644 --- a/src/components/settings/__tests__/about-section-update-badge.test.tsx +++ b/src/components/settings/__tests__/about-section-update-badge.test.tsx @@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { I18nProvider } from "@/lib/i18n/context"; -import { AboutSection } from "../about-section"; +import { AboutSection, UpdateBadge } from "../about-section"; /** * v1.4.36 W4f — AboutSection update-badge contract. @@ -100,3 +100,37 @@ describe(" — update badge", () => { expect(html).toContain("Sources & docs"); }); }); + +describe(" — a11y contract", () => { + it("renders an anchor with aria-label, focus-visible ring, and 44 px hit target when a URL is known", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain('aria-label="A newer version is available: v1.4.99"'); + expect(html).toContain("min-h-11"); + expect(html).toContain("min-w-11"); + expect(html).toContain("focus-visible:ring-ring"); + expect(html).toContain("focus-visible:ring-2"); + expect(html).toContain("focus-visible:ring-offset-2"); + expect(html).toContain("text-primary"); + expect(html).toContain('href="https://example.test/releases/v1.4.99"'); + }); + + it("falls back to a span with role=img + aria-label when no URL is known", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain('role="img"'); + expect(html).toContain('aria-label="A newer version is available: v1.4.99"'); + expect(html).toContain("min-h-11"); + expect(html).toContain("min-w-11"); + }); +}); diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx index 2f9a8a866..25de2ec98 100644 --- a/src/components/settings/about-section.tsx +++ b/src/components/settings/about-section.tsx @@ -67,6 +67,51 @@ function writeLastCheckedISO(iso: string): void { } } +/** + * v1.4.36 QA H7 — keyboard- and touch-accessible update badge. + * + * Renders as a 44 px-square hit target (anchor when a release URL is + * known, span fallback otherwise) with a focus-visible ring, semantic + * `text-primary` colour, and a real `aria-label` describing the + * available version. Exported so the SSR test can pin the contract + * without driving the parent's auto-check effect. + */ +export function UpdateBadge({ + latestTag, + htmlUrl, + ariaLabel, +}: { + latestTag: string; + htmlUrl: string | null; + ariaLabel: string; +}) { + if (htmlUrl) { + return ( + + + {latestTag} + + ); + } + return ( + + + + ); +} + export function AboutSection() { const { t } = useTranslations(); const fmt = useFormatters(); @@ -185,35 +230,15 @@ export function AboutSection() { the link copy on mobile). No bouncing, no badge counter, no manual recheck button — the badge is the entire surface. */} - {updateResult?.status === "newer_available" && - (updateResult.html_url ? ( - - - - ) : ( - - - - ))} + {updateResult?.status === "newer_available" && ( + + )}