Browse Source

fix: accessibility, progress monotonicity, speed guard, and flaky test

- AnnotationNode: add aria-label and focus-visible styles to download btn
- AudioInputNode: same accessibility fix for download button
- useStitchVideos: fix non-monotonic progress (8 -> 11 after 10)
- useTypewriter: guard speed with Math.max(16, speed) minimum
- loopEdge test: replace setTimeout race with vi.waitFor polling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
8124d8587d
  1. 3
      src/components/nodes/AnnotationNode.tsx
  2. 3
      src/components/nodes/AudioInputNode.tsx
  3. 2
      src/hooks/useStitchVideos.ts
  4. 5
      src/hooks/useTypewriter.ts
  5. 10
      src/store/__tests__/loopEdge.integration.test.ts

3
src/components/nodes/AnnotationNode.tsx

@ -136,7 +136,8 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
e.stopPropagation();
downloadMedia(displayImage!, "image");
}}
className="absolute top-2 right-10 w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
aria-label="Download image"
className="absolute top-2 right-10 w-6 h-6 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-white transition-opacity flex items-center justify-center"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />

3
src/components/nodes/AudioInputNode.tsx

@ -217,7 +217,8 @@ export function AudioInputNode({ id, data, selected }: NodeProps<AudioInputNodeT
{/* Download button */}
<button
onClick={() => downloadMedia(nodeData.audioFile!, "audio")}
className="absolute top-1 right-7 w-5 h-5 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
aria-label="Download audio"
className="absolute top-1 right-7 w-5 h-5 bg-black/60 hover:bg-black/80 text-white rounded text-xs opacity-0 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-white transition-opacity flex items-center justify-center"
title="Download audio"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>

2
src/hooks/useStitchVideos.ts

@ -295,7 +295,7 @@ export async function stitchVideosAsync(
let outputStarted = false;
if (effectiveAudioData) {
updateProgress('processing', 'Detecting supported audio codec...', 8);
updateProgress('processing', 'Detecting supported audio codec...', 11);
// Detect the best supported audio codec for MP4
// Try common codecs in order of preference: aac, mp3 (no opus - Twitter doesn't support it)

5
src/hooks/useTypewriter.ts

@ -13,6 +13,7 @@ interface UseTypewriterResult {
* @returns Object with displayedText (current partial text) and isComplete (true when done)
*/
export function useTypewriter(text: string, speed: number = 50): UseTypewriterResult {
const safeSpeed = Math.max(16, speed);
const [displayedText, setDisplayedText] = useState("");
const [currentIndex, setCurrentIndex] = useState(0);
@ -38,10 +39,10 @@ export function useTypewriter(text: string, speed: number = 50): UseTypewriterRe
}
return nextIndex;
});
}, speed);
}, safeSpeed);
return () => clearInterval(timer);
}, [text, speed, currentIndex]);
}, [text, safeSpeed, currentIndex]);
return {
displayedText,

10
src/store/__tests__/loopEdge.integration.test.ts

@ -214,10 +214,12 @@ describe("executeWorkflow with loop edges", () => {
// Start execution (don't await)
const executionPromise = useWorkflowStore.getState().executeWorkflow();
// Abort after a short delay
setTimeout(() => {
useWorkflowStore.getState().stopWorkflow();
}, 10);
// Poll until execution has actually started before stopping
await vi.waitFor(() => {
expect(useWorkflowStore.getState().isRunning).toBe(true);
}, { timeout: 1000 });
useWorkflowStore.getState().stopWorkflow();
await executionPromise;

Loading…
Cancel
Save