import { render } from "@testing-library/react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { LevelMeter } from "../level-meter"
// Mock AudioContext и related APIs
const mockAudioContext = {
createAnalyser: vi.fn(() => ({
fftSize: 256,
smoothingTimeConstant: 0.8,
frequencyBinCount: 128,
connect: vi.fn(),
disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn((dataArray: Float32Array) => {
// Simulate audio data with some variation
for (let i = 0; i < dataArray.length; i++) {
dataArray[i] = Math.sin(i * 0.1) * 0.5 // Generate test sine wave data
}
}),
})),
} as unknown as AudioContext
const mockAudioNode = {
connect: vi.fn(),
disconnect: vi.fn(),
} as unknown as AudioNode
// Mock requestAnimationFrame and cancelAnimationFrame
let rafId = 0
const mockRaf = vi.fn((_callback) => {
// Don't execute immediately to prevent infinite loop
rafId++
return rafId
})
const mockCancelRaf = vi.fn()
beforeEach(() => {
global.requestAnimationFrame = mockRaf
global.cancelAnimationFrame = mockCancelRaf
rafId = 0
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("LevelMeter", () => {
it("should render without crashing", () => {
const { container } = render()
expect(container.firstChild).toBeInTheDocument()
})
it("should render with default props", () => {
const { container } = render()
// Should render with default 2 channels
const meters = container.querySelectorAll(".relative.w-4")
expect(meters).toHaveLength(2)
// Should have vertical orientation by default
expect(container.querySelector(".flex-row")).toBeInTheDocument()
})
it("should render mono meter (1 channel)", () => {
const { container } = render()
const meters = container.querySelectorAll(".relative.w-4")
expect(meters).toHaveLength(1)
})
it("should render horizontal orientation", () => {
const { container } = render()
// Should have horizontal orientation
expect(container.querySelector(".flex-col")).toBeInTheDocument()
// Should render horizontal meters
const meters = container.querySelectorAll(".relative.h-4")
expect(meters).toHaveLength(2)
})
it("should apply custom className", () => {
const { container } = render()
expect(container.querySelector(".custom-class")).toBeInTheDocument()
})
it("should create analyser when audioContext and source are provided", () => {
render()
expect(mockAudioContext.createAnalyser).toHaveBeenCalled()
expect(mockAudioNode.connect).toHaveBeenCalled()
})
it("should set analyser properties correctly", () => {
const mockAnalyser = {
fftSize: 0,
smoothingTimeConstant: 0,
frequencyBinCount: 128,
connect: vi.fn(),
disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn(),
}
const mockContext = {
createAnalyser: vi.fn(() => mockAnalyser),
} as unknown as AudioContext
render()
expect(mockAnalyser.fftSize).toBe(256)
expect(mockAnalyser.smoothingTimeConstant).toBe(0.8)
})
it("should start animation loop when audioContext and source are provided", () => {
render()
expect(mockRaf).toHaveBeenCalled()
})
it("should cleanup on unmount", () => {
const { unmount } = render()
unmount()
expect(mockCancelRaf).toHaveBeenCalled()
expect(mockAudioNode.disconnect).toHaveBeenCalled()
})
it("should not create analyser without audioContext", () => {
render()
expect(mockAudioContext.createAnalyser).not.toHaveBeenCalled()
})
it("should not create analyser without source", () => {
render()
expect(mockAudioContext.createAnalyser).not.toHaveBeenCalled()
})
it("should render scale marks in vertical orientation", () => {
const { container } = render()
// Look for scale marks (dB indicators)
const scaleMarks = container.querySelectorAll(".h-px.bg-zinc-600")
expect(scaleMarks.length).toBeGreaterThan(0)
})
it("should render level bars", () => {
const { container } = render()
// Look for level bars
const levelBars = container.querySelectorAll("[style*='height']")
expect(levelBars.length).toBeGreaterThan(0)
})
it("should render peak indicators", () => {
const { container } = render()
// Look for peak indicators
const peakIndicators = container.querySelectorAll(".bg-white")
expect(peakIndicators.length).toBeGreaterThan(0)
})
it("should handle different channel counts", () => {
const { container } = render()
const meters = container.querySelectorAll(".relative.w-4")
expect(meters).toHaveLength(1)
})
it("should update levels with audio data", () => {
const mockAnalyser = {
fftSize: 256,
smoothingTimeConstant: 0.8,
frequencyBinCount: 128,
connect: vi.fn(),
disconnect: vi.fn(),
getFloatTimeDomainData: vi.fn((dataArray: Float32Array) => {
// Simulate louder audio signal
for (let i = 0; i < dataArray.length; i++) {
dataArray[i] = Math.sin(i * 0.1) * 0.8
}
}),
}
const mockContext = {
createAnalyser: vi.fn(() => mockAnalyser),
} as unknown as AudioContext
const { container } = render()
expect(mockAnalyser.getFloatTimeDomainData).toHaveBeenCalled()
})
it("should handle horizontal orientation correctly", () => {
const { container } = render(
,
)
// Check for horizontal layout
expect(container.querySelector(".flex-col")).toBeInTheDocument()
// Check for horizontal meters
const horizontalMeters = container.querySelectorAll(".relative.h-4")
expect(horizontalMeters).toHaveLength(2)
})
it("should apply correct color classes based on level", () => {
const { container } = render()
// Level bars should have color classes
const greenBars = container.querySelectorAll(".bg-green-500")
const yellowBars = container.querySelectorAll(".bg-yellow-500")
const redBars = container.querySelectorAll(".bg-red-500")
// At least one color should be present
expect(greenBars.length + yellowBars.length + redBars.length).toBeGreaterThan(0)
})
})