diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b6aec2..08447caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **A leaked `serve.token` is rotated on Windows, not just Unix.** After SBS-953, a world-readable token was replaced on Unix, but Windows still tightened the DACL and reused the same secret. The ACL is now inspected before tightening; if anyone other than the current user, SYSTEM, or Administrators can read the file, the token is replaced. Closes SBS-1043. ### Fixed +- **Frontend tests now catch accessibility regressions automatically.** A shared axe assertion checks representative quota cards, mini charts, and update banners in the existing Frontend CI job. Color contrast remains outside jsdom coverage because it requires a rendered browser. Fixes #222. - **`usage --all-accounts` now fetches every configured Codex and Claude account.** Account fetches run with bounded concurrency, preserve configured order, and report failures independently. Text and JSON identify each configured account while the default output remains unchanged. Fixes #274. - **The detached Settings window now reopens where you left it.** Its saved size and position are restored and clamped on screen instead of being overwritten by a second frontend resize on every open. Closes #275. - **`serve --refresh-interval` now controls response caching.** Successful usage and cost responses are cached separately by provider selection for the requested TTL. A zero interval disables caching, and provider failures are retried on the next request. Fixes #273. diff --git a/apps/desktop-tauri/package.json b/apps/desktop-tauri/package.json index 302c2f82..b31d5832 100644 --- a/apps/desktop-tauri/package.json +++ b/apps/desktop-tauri/package.json @@ -34,6 +34,7 @@ "jsdom": "25.0.1", "typescript": "5.6.3", "vite": "6.4.3", - "vitest": "3.2.6" + "vitest": "3.2.6", + "vitest-axe": "^0.1.0" } } diff --git a/apps/desktop-tauri/pnpm-lock.yaml b/apps/desktop-tauri/pnpm-lock.yaml index 649b7886..e870f3f3 100644 --- a/apps/desktop-tauri/pnpm-lock.yaml +++ b/apps/desktop-tauri/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: vitest: specifier: 3.2.6 version: 3.2.6(jsdom@25.0.1) + vitest-axe: + specifier: ^0.1.0 + version: 0.1.0(vitest@3.2.6(jsdom@25.0.1)) packages: @@ -688,6 +691,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + baseline-browser-mapping@2.10.43: resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} engines: {node: '>=6.0.0'} @@ -717,6 +724,10 @@ packages: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -926,6 +937,9 @@ packages: engines: {node: '>=6'} hasBin: true + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -1174,6 +1188,11 @@ packages: yaml: optional: true + vitest-axe@0.1.0: + resolution: {integrity: sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==} + peerDependencies: + vitest: '>=0.16.0' + vitest@3.2.6: resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1771,6 +1790,8 @@ snapshots: asynckit@0.4.0: {} + axe-core@4.13.0: {} + baseline-browser-mapping@2.10.43: {} browserslist@4.28.6: @@ -1803,6 +1824,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + check-error@2.1.3: {} color-convert@2.0.1: @@ -2024,6 +2047,8 @@ snapshots: json5@2.2.3: {} + lodash-es@4.18.1: {} + lodash@4.18.1: {} loose-envify@1.4.0: @@ -2245,6 +2270,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + vitest-axe@0.1.0(vitest@3.2.6(jsdom@25.0.1)): + dependencies: + aria-query: 5.3.2 + axe-core: 4.13.0 + chalk: 5.6.2 + dom-accessibility-api: 0.5.16 + lodash-es: 4.18.1 + redent: 3.0.0 + vitest: 3.2.6(jsdom@25.0.1) + vitest@3.2.6(jsdom@25.0.1): dependencies: '@types/chai': 5.2.3 diff --git a/apps/desktop-tauri/src/components/MiniBarChart.test.tsx b/apps/desktop-tauri/src/components/MiniBarChart.test.tsx index 177e3d9e..3590a831 100644 --- a/apps/desktop-tauri/src/components/MiniBarChart.test.tsx +++ b/apps/desktop-tauri/src/components/MiniBarChart.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { SimpleBarChart, StackedBarChart } from "./MiniBarChart"; +import { expectNoAccessibilityViolations } from "../test/accessibility"; vi.mock("../hooks/useLocale", () => ({ useLocale: () => ({ t: () => "No data" }), @@ -18,4 +19,18 @@ describe("MiniBarChart empty state", () => { expect(screen.getByRole("img", { name: "No data" })).toBeTruthy(); }); + + it("has no detectable accessibility violations", async () => { + const { container } = render( + , + ); + + await expectNoAccessibilityViolations(container); + }); }); diff --git a/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx b/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx index 944b4c4b..b136e7d5 100644 --- a/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx +++ b/apps/desktop-tauri/src/components/PlanStatusCard.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import PlanStatusCard from "./PlanStatusCard"; import type { ProviderUsageSnapshot, RateWindowSnapshot } from "../types/bridge"; +import { expectNoAccessibilityViolations } from "../test/accessibility"; vi.mock("../hooks/useLocale", () => ({ useLocale: () => ({ @@ -67,6 +68,19 @@ function provider( } describe("PlanStatusCard", () => { + it("has no detectable accessibility violations", async () => { + const { container } = render( + , + ); + + await expectNoAccessibilityViolations(container); + }); + it("includes displayed quota windows in the card button name", () => { render( ({ useLocale: () => ({ t: (key: string) => key }), @@ -81,4 +82,12 @@ describe("UpdateBanner accessibility", () => { ); expect(screen.getByRole("alert").textContent).toContain("network failed"); }); + + it("has no detectable accessibility violations", async () => { + const { container } = render( + , + ); + + await expectNoAccessibilityViolations(container); + }); }); diff --git a/apps/desktop-tauri/src/test/accessibility.ts b/apps/desktop-tauri/src/test/accessibility.ts new file mode 100644 index 00000000..7e609ec6 --- /dev/null +++ b/apps/desktop-tauri/src/test/accessibility.ts @@ -0,0 +1,17 @@ +import { axe } from "vitest-axe"; +import type { AxeMatchers } from "vitest-axe"; +import { expect } from "vitest"; + +export async function expectNoAccessibilityViolations( + container: Element, +): Promise { + const results = await axe(container, { + rules: { + // jsdom does not implement the canvas API axe uses for contrast. + "color-contrast": { enabled: false }, + }, + }); + ( + expect(results) as unknown as ReturnType & AxeMatchers + ).toHaveNoViolations(); +} diff --git a/apps/desktop-tauri/src/test/setup.ts b/apps/desktop-tauri/src/test/setup.ts index f149f27a..50b4fcf4 100644 --- a/apps/desktop-tauri/src/test/setup.ts +++ b/apps/desktop-tauri/src/test/setup.ts @@ -1 +1,6 @@ import "@testing-library/jest-dom/vitest"; +import "vitest-axe/extend-expect"; +import * as axeMatchers from "vitest-axe/matchers"; +import { expect } from "vitest"; + +expect.extend(axeMatchers);