/**
* Tests for AudioClip component
*/
import { fireEvent, render, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { AudioClip } from "@/features/timeline/components/clip/audio-clip"
import { TimelineClip, TimelineTrack, TrackType } from "../../../types"
// Mock hooks
vi.mock("../../../hooks", () => ({
useClips: vi.fn(() => ({
updateClip: vi.fn(),
})),
}))
// Mock components
vi.mock("../../../components/audio-effects-editor", () => ({
AudioEffectsEditor: ({ onApply, onClose }: { onApply: (effects: any[]) => void; onClose: () => void }) => (
),
}))
vi.mock("../track/waveform", () => ({
default: ({ audioUrl, className }: { audioUrl?: string | null; className?: string }) => (
Waveform
),
}))
// Mock lucide-react icons to include data-testid
vi.mock("lucide-react", async () => {
const actual = await vi.importActual("lucide-react")
return {
...actual,
Music: (props: any) => (
),
Sparkles: (props: any) => (
),
Copy: (props: any) => (
),
Scissors: (props: any) => (
),
Trash2: (props: any) => (
),
Volume2: (props: any) => (
),
}
})
// Mock data
const mockAudioClip: TimelineClip = {
id: "clip-1",
trackId: "track-1",
mediaId: "media-1",
name: "Test Audio Clip",
startTime: 0,
duration: 30,
trimStart: 0,
trimEnd: 30,
mediaStartTime: 0,
mediaEndTime: 30,
volume: 0.8,
speed: 1,
opacity: 1,
isReversed: false,
isSelected: false,
isLocked: false,
effects: [],
filters: [],
transitions: [],
position: {
x: 0,
y: 0,
width: 1,
height: 1,
rotation: 0,
scaleX: 1,
scaleY: 1,
},
mediaFile: {
id: "media-1",
path: "/path/to/audio.mp3",
name: "audio.mp3",
size: 5000000,
duration: 30,
hasAudio: true,
hasVideo: false,
},
}
const mockMusicTrack: TimelineTrack = {
id: "track-1",
name: "Music Track",
type: "music" as TrackType,
order: 0,
isLocked: false,
height: 60,
clips: [],
}
const mockVoiceoverTrack: TimelineTrack = {
...mockMusicTrack,
type: "voiceover" as TrackType,
name: "Voiceover Track",
}
const mockSfxTrack: TimelineTrack = {
...mockMusicTrack,
type: "sfx" as TrackType,
name: "SFX Track",
}
const mockAmbientTrack: TimelineTrack = {
...mockMusicTrack,
type: "ambient" as TrackType,
name: "Ambient Track",
}
const mockAudioTrack: TimelineTrack = {
...mockMusicTrack,
type: "audio" as TrackType,
name: "Audio Track",
}
describe("AudioClip", () => {
const mockOnUpdate = vi.fn()
const mockOnRemove = vi.fn()
const mockUpdateClip = vi.fn()
beforeEach(async () => {
vi.clearAllMocks()
const { useClips } = await import("../../../hooks")
vi.mocked(useClips).mockReturnValue({
updateClip: mockUpdateClip,
} as any)
})
describe("Rendering", () => {
it("should render audio clip with correct name and icon", () => {
render()
expect(screen.getByText("Test Audio Clip")).toBeInTheDocument()
expect(screen.getByTestId("music-icon")).toBeInTheDocument()
})
it("should handle audio rendering", () => {
render()
// The component should render the main content area
expect(screen.getByText("Test Audio Clip")).toBeInTheDocument()
// Should show volume and duration
expect(screen.getByText("80%")).toBeInTheDocument()
expect(screen.getByText("30s")).toBeInTheDocument()
})
it("should show effects indicator when effects are present", () => {
const clipWithEffects = {
...mockAudioClip,
effects: [
{ id: "1", effectId: "reverb", isEnabled: true, order: 0 },
{ id: "2", effectId: "echo", isEnabled: true, order: 1 },
],
}
render(
,
)
expect(screen.getByTitle("Эффекты применены")).toBeInTheDocument()
})
})
describe("Track Type Colors", () => {
it("should apply music track color", () => {
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("bg-pink-500")
})
it("should apply voiceover track color", () => {
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("bg-cyan-500")
})
it("should apply sfx track color", () => {
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("bg-red-500")
})
it("should apply ambient track color", () => {
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("bg-gray-500")
})
it("should apply default audio track color", () => {
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("bg-green-500")
})
})
describe("Selection", () => {
it("should call onUpdate when clicked", () => {
const { container } = render(
,
)
fireEvent.click(container.firstChild!)
expect(mockOnUpdate).toHaveBeenCalledWith({ isSelected: true })
})
it("should toggle selection state", () => {
const selectedClip = { ...mockAudioClip, isSelected: true }
const { container } = render(
,
)
fireEvent.click(container.firstChild!)
expect(mockOnUpdate).toHaveBeenCalledWith({ isSelected: false })
})
it("should show selection ring when selected", () => {
const selectedClip = { ...mockAudioClip, isSelected: true }
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("ring-2")
})
})
describe("Hover Effects", () => {
it("should show action buttons on hover", () => {
render()
expect(screen.queryByTitle("Эффекты")).not.toBeInTheDocument()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
expect(screen.getByTitle("Эффекты")).toBeInTheDocument()
expect(screen.getByTitle("Копировать")).toBeInTheDocument()
expect(screen.getByTitle("Разделить")).toBeInTheDocument()
expect(screen.getByTitle("Удалить")).toBeInTheDocument()
})
it("should hide action buttons on mouse leave", () => {
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
fireEvent.mouseLeave(clipElement.parentElement!)
expect(screen.queryByTitle("Эффекты")).not.toBeInTheDocument()
})
it("should not show buttons when clip is locked", () => {
const lockedClip = { ...mockAudioClip, isLocked: true }
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
expect(screen.queryByTitle("Эффекты")).not.toBeInTheDocument()
})
it("should change color on hover", () => {
const { container } = render(
,
)
const clipElement = container.firstChild as Element
expect(clipElement).toHaveClass("bg-pink-500")
expect(clipElement).not.toHaveClass("bg-pink-600")
fireEvent.mouseEnter(clipElement)
expect(clipElement).toHaveClass("bg-pink-600")
fireEvent.mouseLeave(clipElement)
expect(clipElement).not.toHaveClass("bg-pink-600")
})
})
describe("Action Buttons", () => {
it("should handle effects button click", () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
const effectsButton = screen.getByTitle("Эффекты")
fireEvent.click(effectsButton)
// Test should pass without errors - effects modal logic is complex
expect(effectsButton).toBeInTheDocument()
consoleSpy.mockRestore()
})
it("should handle copy button click", () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
const copyButton = screen.getByTitle("Копировать")
fireEvent.click(copyButton)
expect(consoleSpy).toHaveBeenCalledWith("Copy audio clip:", "clip-1")
consoleSpy.mockRestore()
})
it("should handle split button click", () => {
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {})
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
const splitButton = screen.getByTitle("Разделить")
fireEvent.click(splitButton)
expect(consoleSpy).toHaveBeenCalledWith("Split audio clip:", "clip-1")
consoleSpy.mockRestore()
})
it("should handle remove button click", () => {
render()
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
const removeButton = screen.getByTitle("Удалить")
fireEvent.click(removeButton)
expect(mockOnRemove).toHaveBeenCalled()
})
})
describe("Effects Editor", () => {
it("should handle effects button click", () => {
const { container } = render(
,
)
const clipElement = screen.getByText("Test Audio Clip").closest("div")!
fireEvent.mouseEnter(clipElement.parentElement!)
const effectsButton = screen.getByTitle("Эффекты")
expect(effectsButton).toBeInTheDocument()
// The effects editor should exist in the DOM but might not be visible initially
expect(container.querySelector('[data-testid="audio-effects-editor"]')).toBeDefined()
})
})
describe("Visual States", () => {
it("should show opacity when locked", () => {
const lockedClip = { ...mockAudioClip, isLocked: true }
const { container } = render(
,
)
expect(container.firstChild).toHaveClass("opacity-60")
})
it("should show volume icon", () => {
render()
expect(screen.getByTestId("volume-icon")).toBeInTheDocument()
})
})
describe("Edge Cases", () => {
it("should handle clip without name", () => {
const clipWithoutName = { ...mockAudioClip, name: "" }
render(
,
)
expect(screen.getByTestId("music-icon")).toBeInTheDocument()
})
it("should handle clip without media file", () => {
const clipWithoutMedia = { ...mockAudioClip, mediaFile: undefined }
render(
,
)
// Should show fallback waveform instead of the Waveform component
expect(screen.queryByTestId("waveform")).not.toBeInTheDocument()
// Should still show the audio clip
expect(screen.getByText("Test Audio Clip")).toBeInTheDocument()
})
it("should show fallback waveform when no audio URL", () => {
const clipWithoutPath = {
...mockAudioClip,
mediaFile: { ...mockAudioClip.mediaFile!, path: "" },
}
const { container } = render(
,
)
// Should show fallback waveform (array of divs)
expect(screen.queryByTestId("waveform")).not.toBeInTheDocument()
// Should show animated bars for fallback
const fallbackBars = container.querySelectorAll(".animate-pulse")
expect(fallbackBars.length).toBe(20) // Array.from({ length: 20 })
})
it("should handle zero volume", () => {
const zeroVolumeClip = { ...mockAudioClip, volume: 0 }
render()
expect(screen.getByText("0%")).toBeInTheDocument()
})
it("should handle missing callbacks", () => {
const { container } = render()
expect(() => fireEvent.click(container.firstChild!)).not.toThrow()
})
})
})