MAESTRO: Make decode-all toggle stateful

This commit is contained in:
Mariusz Banach
2026-02-18 01:28:46 +01:00
parent 196689f041
commit 997056ad4f
3 changed files with 44 additions and 13 deletions

View File

@@ -35,7 +35,7 @@ This phase implements the test selection panel and analysis configuration contro
- [x] Select All / Deselect All buttons work correctly - [x] Select All / Deselect All buttons work correctly
- [x] Search/filter narrows visible tests by name - [x] Search/filter narrows visible tests by name
- [x] DNS resolution toggle defaults to off - [x] DNS resolution toggle defaults to off
- [ ] Decode-all toggle is functional - [x] Decode-all toggle is functional
- [ ] All controls are keyboard accessible (Tab, Enter, Space) - [ ] All controls are keyboard accessible (Tab, Enter, Space)
- [ ] Linting passes (`ruff check backend/`, `npx eslint src/`) - [ ] Linting passes (`ruff check backend/`, `npx eslint src/`)
- [ ] Run `/speckit.analyze` to verify consistency - [ ] Run `/speckit.analyze` to verify consistency

View File

@@ -110,6 +110,27 @@ describe("AnalysisControls", () => {
); );
}); });
it("updates toggles without a controlled config", async () => {
setupFetchMock(sampleTests);
const handleChange = vi.fn();
const { container } = render(<AnalysisControls onChange={handleChange} />);
await act(async () => {
await flushPromises();
});
const decodeToggle = getToggle(container, "toggle-decode-all");
act(() => {
decodeToggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(decodeToggle.getAttribute("aria-checked")).toBe("true");
expect(handleChange).toHaveBeenLastCalledWith(
expect.objectContaining({ decodeAll: true }),
);
});
it("updates toggles on click and keyboard", async () => { it("updates toggles on click and keyboard", async () => {
setupFetchMock(sampleTests); setupFetchMock(sampleTests);
const handleChange = vi.fn(); const handleChange = vi.fn();

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import type { KeyboardEvent } from "react"; import { useState, type KeyboardEvent } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faToggleOff, faToggleOff,
@@ -34,19 +34,29 @@ const handleToggleKeyDown = (
}; };
export default function AnalysisControls({ export default function AnalysisControls({
config = defaultConfig, config,
onChange, onChange,
}: AnalysisControlsProps) { }: AnalysisControlsProps) {
const [internalConfig, setInternalConfig] = useState<AnalysisConfig>(defaultConfig);
const resolvedConfig = config ?? internalConfig;
const commitConfig = (nextConfig: AnalysisConfig) => {
if (!config) {
setInternalConfig(nextConfig);
}
onChange(nextConfig);
};
const updateTests = (nextTestIds: number[]) => { const updateTests = (nextTestIds: number[]) => {
onChange({ ...config, testIds: nextTestIds }); commitConfig({ ...resolvedConfig, testIds: nextTestIds });
}; };
const toggleResolve = () => { const toggleResolve = () => {
onChange({ ...config, resolve: !config.resolve }); commitConfig({ ...resolvedConfig, resolve: !resolvedConfig.resolve });
}; };
const toggleDecodeAll = () => { const toggleDecodeAll = () => {
onChange({ ...config, decodeAll: !config.decodeAll }); commitConfig({ ...resolvedConfig, decodeAll: !resolvedConfig.decodeAll });
}; };
return ( return (
@@ -70,15 +80,15 @@ export default function AnalysisControls({
<button <button
type="button" type="button"
role="switch" role="switch"
aria-checked={config.resolve} aria-checked={resolvedConfig.resolve}
aria-label="Toggle DNS resolution" aria-label="Toggle DNS resolution"
data-testid="toggle-resolve" data-testid="toggle-resolve"
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/60 px-3 py-2 text-xs 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" className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/60 px-3 py-2 text-xs 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={toggleResolve} onClick={toggleResolve}
onKeyDown={(event) => handleToggleKeyDown(event, toggleResolve)} onKeyDown={(event) => handleToggleKeyDown(event, toggleResolve)}
> >
<FontAwesomeIcon icon={config.resolve ? faToggleOn : faToggleOff} /> <FontAwesomeIcon icon={resolvedConfig.resolve ? faToggleOn : faToggleOff} />
{config.resolve ? "On" : "Off"} {resolvedConfig.resolve ? "On" : "Off"}
</button> </button>
</div> </div>
<div className="flex items-center justify-between gap-4 rounded-xl border border-info/10 bg-background/40 p-4"> <div className="flex items-center justify-between gap-4 rounded-xl border border-info/10 bg-background/40 p-4">
@@ -94,20 +104,20 @@ export default function AnalysisControls({
<button <button
type="button" type="button"
role="switch" role="switch"
aria-checked={config.decodeAll} aria-checked={resolvedConfig.decodeAll}
aria-label="Toggle decode all" aria-label="Toggle decode all"
data-testid="toggle-decode-all" data-testid="toggle-decode-all"
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/60 px-3 py-2 text-xs 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" className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/60 px-3 py-2 text-xs 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={toggleDecodeAll} onClick={toggleDecodeAll}
onKeyDown={(event) => handleToggleKeyDown(event, toggleDecodeAll)} onKeyDown={(event) => handleToggleKeyDown(event, toggleDecodeAll)}
> >
<FontAwesomeIcon icon={config.decodeAll ? faToggleOn : faToggleOff} /> <FontAwesomeIcon icon={resolvedConfig.decodeAll ? faToggleOn : faToggleOff} />
{config.decodeAll ? "On" : "Off"} {resolvedConfig.decodeAll ? "On" : "Off"}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<TestSelector selectedTestIds={config.testIds} onSelectionChange={updateTests} /> <TestSelector selectedTestIds={resolvedConfig.testIds} onSelectionChange={updateTests} />
</section> </section>
); );
} }