diff --git a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-04-Test-Selection.md b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-04-Test-Selection.md index 61140df..d853581 100644 --- a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-04-Test-Selection.md +++ b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-04-Test-Selection.md @@ -36,7 +36,7 @@ This phase implements the test selection panel and analysis configuration contro - [x] Search/filter narrows visible tests by name - [x] DNS resolution toggle defaults to off - [x] Decode-all toggle is functional -- [ ] All controls are keyboard accessible (Tab, Enter, Space) +- [x] All controls are keyboard accessible (Tab, Enter, Space) - [ ] Linting passes (`ruff check backend/`, `npx eslint src/`) - [ ] Run `/speckit.analyze` to verify consistency diff --git a/frontend/src/__tests__/AnalysisControls.test.tsx b/frontend/src/__tests__/AnalysisControls.test.tsx index df75117..a62cbd7 100644 --- a/frontend/src/__tests__/AnalysisControls.test.tsx +++ b/frontend/src/__tests__/AnalysisControls.test.tsx @@ -178,4 +178,42 @@ describe("AnalysisControls", () => { expect.objectContaining({ resolve: true, decodeAll: true }), ); }); + + it("updates toggles on Space key presses", async () => { + setupFetchMock(sampleTests); + const handleChange = vi.fn(); + + const AnalysisControlsHarness = () => { + const [config, setConfig] = useState({ + testIds: [], + resolve: false, + decodeAll: false, + }); + + const updateConfig = (next: AnalysisConfig) => { + setConfig(next); + handleChange(next); + }; + + return ; + }; + + const { container } = render(); + + await act(async () => { + await flushPromises(); + }); + + const resolveToggle = getToggle(container, "toggle-resolve"); + act(() => { + resolveToggle.dispatchEvent( + new KeyboardEvent("keydown", { key: " ", bubbles: true }), + ); + }); + + expect(resolveToggle.getAttribute("aria-checked")).toBe("true"); + expect(handleChange).toHaveBeenLastCalledWith( + expect.objectContaining({ resolve: true }), + ); + }); }); diff --git a/frontend/src/__tests__/TestSelector.test.tsx b/frontend/src/__tests__/TestSelector.test.tsx index 2c9785e..aa48709 100644 --- a/frontend/src/__tests__/TestSelector.test.tsx +++ b/frontend/src/__tests__/TestSelector.test.tsx @@ -164,6 +164,43 @@ describe("TestSelector", () => { }); }); + it("supports keyboard activation for select and deselect", async () => { + setupFetchMock(sampleTests); + + const TestSelectorHarness = () => { + const [selected, setSelected] = useState([]); + return ; + }; + + const { container } = render(); + + await act(async () => { + await flushPromises(); + }); + + const selectAllButton = getSelectAllButton(container); + act(() => { + selectAllButton.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", bubbles: true }), + ); + }); + + sampleTests.forEach((test) => { + expect(getCheckbox(container, test.id).checked).toBe(true); + }); + + const deselectAllButton = getDeselectAllButton(container); + act(() => { + deselectAllButton.dispatchEvent( + new KeyboardEvent("keydown", { key: " ", bubbles: true }), + ); + }); + + sampleTests.forEach((test) => { + expect(getCheckbox(container, test.id).checked).toBe(false); + }); + }); + it("filters tests by search text", async () => { setupFetchMock(sampleTests); diff --git a/frontend/src/components/TestSelector.tsx b/frontend/src/components/TestSelector.tsx index b411a4c..73d3e7a 100644 --- a/frontend/src/components/TestSelector.tsx +++ b/frontend/src/components/TestSelector.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, type KeyboardEvent } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, @@ -46,6 +46,16 @@ const buildGroups = (tests: TestInfo[]): TestGroup[] => { })); }; +const handleActionKeyDown = ( + event: KeyboardEvent, + onAction: () => void, +): void => { + if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") { + event.preventDefault(); + onAction(); + } +}; + export default function TestSelector({ selectedTestIds, onSelectionChange }: TestSelectorProps) { const [tests, setTests] = useState([]); const [searchText, setSearchText] = useState(""); @@ -152,6 +162,7 @@ export default function TestSelector({ selectedTestIds, onSelectionChange }: Tes type="button" className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info" onClick={handleSelectAll} + onKeyDown={(event) => handleActionKeyDown(event, handleSelectAll)} data-testid="select-all-tests" > @@ -161,6 +172,7 @@ export default function TestSelector({ selectedTestIds, onSelectionChange }: Tes type="button" className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info" onClick={handleDeselectAll} + onKeyDown={(event) => handleActionKeyDown(event, handleDeselectAll)} data-testid="deselect-all-tests" >