/** * 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) => ( Music ), Sparkles: (props: any) => ( Sparkles ), Copy: (props: any) => ( Copy ), Scissors: (props: any) => ( Scissors ), Trash2: (props: any) => ( Trash ), Volume2: (props: any) => ( Volume ), } }) // 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() }) }) })