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(); e.stopPropagation();
downloadMedia(displayImage!, "image"); 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}> <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" /> <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 */} {/* Download button */}
<button <button
onClick={() => downloadMedia(nodeData.audioFile!, "audio")} 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" title="Download audio"
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <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; let outputStarted = false;
if (effectiveAudioData) { if (effectiveAudioData) {
updateProgress('processing', 'Detecting supported audio codec...', 8); updateProgress('processing', 'Detecting supported audio codec...', 11);
// Detect the best supported audio codec for MP4 // Detect the best supported audio codec for MP4
// Try common codecs in order of preference: aac, mp3 (no opus - Twitter doesn't support it) // 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) * @returns Object with displayedText (current partial text) and isComplete (true when done)
*/ */
export function useTypewriter(text: string, speed: number = 50): UseTypewriterResult { export function useTypewriter(text: string, speed: number = 50): UseTypewriterResult {
const safeSpeed = Math.max(16, speed);
const [displayedText, setDisplayedText] = useState(""); const [displayedText, setDisplayedText] = useState("");
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
@ -38,10 +39,10 @@ export function useTypewriter(text: string, speed: number = 50): UseTypewriterRe
} }
return nextIndex; return nextIndex;
}); });
}, speed); }, safeSpeed);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [text, speed, currentIndex]); }, [text, safeSpeed, currentIndex]);
return { return {
displayedText, displayedText,

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

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

Loading…
Cancel
Save