Files
stack/apps/web/src/components/speech/AudioVisualizer.test.tsx
Jason Woltje 74d6c1092e
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(#403): add audio playback component for TTS output
Implements AudioPlayer inline component with play/pause, progress bar,
speed control (0.5x-2x), download, and duration display. Adds
TextToSpeechButton "Read aloud" component that synthesizes text via
the speech API and integrates AudioPlayer for playback. Includes
useTextToSpeech hook with API integration, audio caching, and
playback state management. All 32 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 03:05:39 -06:00

71 lines
2.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { AudioVisualizer } from "./AudioVisualizer";
describe("AudioVisualizer", (): void => {
it("should render the visualizer container", (): void => {
render(<AudioVisualizer audioLevel={0} isActive={false} />);
const container = screen.getByTestId("audio-visualizer");
expect(container).toBeInTheDocument();
});
it("should render visualization bars", (): void => {
render(<AudioVisualizer audioLevel={0.5} isActive={true} />);
const bars = screen.getAllByTestId("visualizer-bar");
expect(bars.length).toBeGreaterThan(0);
});
it("should show inactive state when not active", (): void => {
render(<AudioVisualizer audioLevel={0} isActive={false} />);
const container = screen.getByTestId("audio-visualizer");
expect(container).toBeInTheDocument();
// Bars should be at minimum height when inactive
const bars = screen.getAllByTestId("visualizer-bar");
bars.forEach((bar) => {
const style = bar.getAttribute("style");
expect(style).toContain("height");
});
});
it("should reflect audio level in bar heights when active", (): void => {
render(<AudioVisualizer audioLevel={0.8} isActive={true} />);
const bars = screen.getAllByTestId("visualizer-bar");
// At least one bar should have non-minimal height
const hasActiveBars = bars.some((bar) => {
const style = bar.getAttribute("style") ?? "";
const heightMatch = /height:\s*(\d+)/.exec(style);
return heightMatch?.[1] ? parseInt(heightMatch[1], 10) > 4 : false;
});
expect(hasActiveBars).toBe(true);
});
it("should use calm colors (no aggressive reds)", (): void => {
render(<AudioVisualizer audioLevel={0.5} isActive={true} />);
const container = screen.getByTestId("audio-visualizer");
const allElements = container.querySelectorAll("*");
allElements.forEach((el) => {
const className = (el as HTMLElement).className;
expect(className).not.toMatch(/bg-red-|text-red-/);
});
});
it("should accept custom className", (): void => {
render(<AudioVisualizer audioLevel={0.5} isActive={true} className="custom-class" />);
const container = screen.getByTestId("audio-visualizer");
expect(container.className).toContain("custom-class");
});
it("should render with configurable bar count", (): void => {
render(<AudioVisualizer audioLevel={0.5} isActive={true} barCount={8} />);
const bars = screen.getAllByTestId("visualizer-bar");
expect(bars).toHaveLength(8);
});
});