import { fireEvent, render, screen } from "@testing-library/react"
import React from "react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { AudioEffectsEditorModal } from "@/features/timeline/components/audio-effects-editor-modal"
// Mock the modal service
vi.mock("@/features/modals/services", () => ({
useModal: vi.fn(),
}))
// Mock UI components
vi.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, variant, ...props }: any) => (
),
}))
vi.mock("@/components/ui/label", () => ({
Label: ({ children, ...props }: any) => ,
}))
vi.mock("@/components/ui/slider", () => ({
Slider: ({ value, onValueChange, min, max, step, ...props }: any) => (
onValueChange?.([Number.parseFloat(e.target.value)])}
min={min}
max={max}
step={step}
data-testid="slider"
{...props}
/>
),
}))
vi.mock("@/components/ui/switch", () => ({
Switch: ({ checked, onCheckedChange, ...props }: any) => (
),
}))
// Track active tab globally for tests
let activeTab = "basic"
vi.mock("@/components/ui/tabs", () => ({
Tabs: ({ children, value, onValueChange, ...props }: any) => {
// Update active tab when value changes
if (value) activeTab = value
return (
{React.Children.map(children, (child) =>
React.isValidElement(child) ? React.cloneElement(child as any, { onValueChange }) : child,
)}
)
},
TabsList: ({ children, onValueChange, ...props }: any) => (
{React.Children.map(children, (child) =>
React.isValidElement(child) ? React.cloneElement(child as any, { onValueChange }) : child,
)}
),
TabsTrigger: ({ children, value, onValueChange, ...props }: any) => (
),
TabsContent: ({ children, value, ...props }: any) => {
return activeTab === value ? {children}
: null
},
}))
describe("AudioEffectsEditorModal", () => {
const mockCloseModal = vi.fn()
const mockOnApplyEffects = vi.fn()
const mockUseModal = vi.fn()
beforeEach(async () => {
vi.clearAllMocks()
activeTab = "basic" // Reset tab state
const { useModal } = await import("@/features/modals/services")
vi.mocked(useModal).mockImplementation(mockUseModal)
mockUseModal.mockReturnValue({
modalData: {
clip: { id: "test-clip" },
track: { id: "test-track" },
onApplyEffects: mockOnApplyEffects,
activeEffects: {},
},
closeModal: mockCloseModal,
})
})
afterEach(() => {
vi.resetAllMocks()
})
describe("initial rendering", () => {
it("should render with default tab selected", () => {
render()
expect(screen.getByText("Базовые")).toBeInTheDocument()
expect(screen.getByText("Динамика")).toBeInTheDocument()
expect(screen.getByText("Пространство")).toBeInTheDocument()
expect(screen.getByText("Коррекция")).toBeInTheDocument()
})
it("should render basic effects in default tab", () => {
render()
expect(screen.getByText("Fade In")).toBeInTheDocument()
expect(screen.getByText("Fade Out")).toBeInTheDocument()
expect(screen.getByText("Эквалайзер")).toBeInTheDocument()
})
it("should render action buttons", () => {
render()
expect(screen.getByText("Отмена")).toBeInTheDocument()
expect(screen.getByText("Применить эффекты")).toBeInTheDocument()
})
it("should load active effects from modal data", () => {
mockUseModal.mockReturnValue({
modalData: {
activeEffects: {
"fade-in": {
id: "fade-in",
name: "Fade In",
type: "AudioFadeIn",
enabled: true,
params: { duration: 2.0 },
},
},
onApplyEffects: mockOnApplyEffects,
},
closeModal: mockCloseModal,
})
render()
const fadeInSwitch = screen.getAllByRole("switch")[0]
expect(fadeInSwitch).toHaveAttribute("aria-checked", "true")
})
})
describe("effect toggling", () => {
it("should toggle fade in effect", () => {
render()
const fadeInSwitch = screen.getAllByRole("switch")[0]
expect(fadeInSwitch).toHaveAttribute("aria-checked", "false")
fireEvent.click(fadeInSwitch)
expect(fadeInSwitch).toHaveAttribute("aria-checked", "true")
// Should show fade in controls
expect(screen.getByText("Длительность (сек)")).toBeInTheDocument()
})
it("should toggle fade out effect", () => {
render()
const fadeOutSwitch = screen.getAllByRole("switch")[1]
fireEvent.click(fadeOutSwitch)
expect(fadeOutSwitch).toHaveAttribute("aria-checked", "true")
expect(screen.getAllByText("Длительность (сек)")).toHaveLength(1)
})
it("should toggle equalizer effect", () => {
render()
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
expect(equalizerSwitch).toHaveAttribute("aria-checked", "true")
expect(screen.getByText("Низкие частоты (100Hz)")).toBeInTheDocument()
expect(screen.getByText("Средние частоты (1kHz)")).toBeInTheDocument()
expect(screen.getByText("Высокие частоты (10kHz)")).toBeInTheDocument()
})
it("should remove effect when toggled off", () => {
render()
const fadeInSwitch = screen.getAllByRole("switch")[0]
// Toggle on
fireEvent.click(fadeInSwitch)
expect(screen.getByText("Длительность (сек)")).toBeInTheDocument()
// Toggle off
fireEvent.click(fadeInSwitch)
expect(screen.queryByText("Длительность (сек)")).not.toBeInTheDocument()
})
})
describe("parameter adjustment", () => {
it("should update fade in duration", () => {
render()
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "3.5" } })
expect(slider).toHaveValue("3.5")
})
it("should update equalizer parameters", () => {
render()
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
const sliders = screen.getAllByTestId("slider")
// Low frequency
fireEvent.change(sliders[0], { target: { value: "5" } })
expect(sliders[0]).toHaveValue("5")
// Mid frequency
fireEvent.change(sliders[1], { target: { value: "-3" } })
expect(sliders[1]).toHaveValue("-3")
// High frequency
fireEvent.change(sliders[2], { target: { value: "2" } })
expect(sliders[2]).toHaveValue("2")
})
})
describe("tab navigation", () => {
it("should switch to dynamics tab", () => {
render()
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
expect(screen.getByText("Компрессор")).toBeInTheDocument()
expect(screen.getByText("Нормализация")).toBeInTheDocument()
})
it("should show compressor controls when enabled", () => {
render()
// Switch to dynamics tab
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
const compressorSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(compressorSwitch)
expect(screen.getByText("Порог (dB)")).toBeInTheDocument()
expect(screen.getByText("Степень сжатия")).toBeInTheDocument()
})
it("should update compressor attack and release parameters", () => {
render()
// Switch to dynamics tab
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
// Enable compressor
const compressorSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(compressorSwitch)
const sliders = screen.getAllByTestId("slider")
// Update threshold
fireEvent.change(sliders[0], { target: { value: "-30" } })
expect(sliders[0]).toHaveValue("-30")
// Update ratio
fireEvent.change(sliders[1], { target: { value: "8" } })
expect(sliders[1]).toHaveValue("8")
})
it("should show normalize controls and update parameters", () => {
render()
// Switch to dynamics tab
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
// Enable normalize
const normalizeSwitch = screen.getAllByRole("switch")[1]
fireEvent.click(normalizeSwitch)
expect(screen.getByText("Целевая громкость (LUFS)")).toBeInTheDocument()
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "-16" } })
expect(slider).toHaveValue("-16")
})
})
describe("spatial effects", () => {
it("should show reverb controls", () => {
render()
// Switch to spatial tab
const spatialTab = screen.getByText("Пространство")
fireEvent.click(spatialTab)
expect(screen.getByText("Реверберация")).toBeInTheDocument()
const reverbSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(reverbSwitch)
expect(screen.getByText("Размер помещения")).toBeInTheDocument()
expect(screen.getByText("Микс (Dry/Wet)")).toBeInTheDocument()
})
it("should update reverb damping parameter", () => {
render()
// Switch to spatial tab
const spatialTab = screen.getByText("Пространство")
fireEvent.click(spatialTab)
// Enable reverb
const reverbSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(reverbSwitch)
const sliders = screen.getAllByTestId("slider")
// Update room size
fireEvent.change(sliders[0], { target: { value: "0.8" } })
expect(sliders[0]).toHaveValue("0.8")
// Update wet mix
fireEvent.change(sliders[1], { target: { value: "0.5" } })
expect(sliders[1]).toHaveValue("0.5")
})
it("should show delay controls", () => {
render()
// Switch to spatial tab
const spatialTab = screen.getByText("Пространство")
fireEvent.click(spatialTab)
expect(screen.getByText("Задержка (Delay)")).toBeInTheDocument()
const delaySwitch = screen.getAllByRole("switch")[1]
fireEvent.click(delaySwitch)
expect(screen.getByText("Время задержки (сек)")).toBeInTheDocument()
expect(screen.getByText("Затухание")).toBeInTheDocument()
})
})
describe("correction effects", () => {
it("should show denoise controls", () => {
render()
// Switch to correction tab
const correctionTab = screen.getByText("Коррекция")
fireEvent.click(correctionTab)
expect(screen.getByText("Шумоподавление")).toBeInTheDocument()
const denoiseSwitch = screen.getByRole("switch")
fireEvent.click(denoiseSwitch)
expect(screen.getByText("Сила подавления")).toBeInTheDocument()
})
})
describe("applying effects", () => {
it("should apply selected effects", () => {
render()
// Enable fade in
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
// Enable equalizer
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
// Apply
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({
effectId: "fade-in",
enabled: true,
order: 0,
customParams: { duration: 1.0 },
}),
expect.objectContaining({
effectId: "equalizer",
enabled: true,
order: 1,
customParams: { gain_low: 0, gain_mid: 0, gain_high: 0 },
}),
])
expect(mockCloseModal).toHaveBeenCalled()
})
it("should apply effects with modified parameters", () => {
render()
// Enable fade in and modify duration
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "2.5" } })
// Apply
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({
effectId: "fade-in",
customParams: { duration: 2.5 },
}),
])
})
it("should not call onApplyEffects if not provided", () => {
mockUseModal.mockReturnValue({
modalData: {
clip: { id: "test-clip" },
onApplyEffects: undefined,
},
closeModal: mockCloseModal,
})
render()
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockCloseModal).toHaveBeenCalled()
})
})
describe("cancel functionality", () => {
it("should close modal on cancel", () => {
render()
const cancelButton = screen.getByText("Отмена")
fireEvent.click(cancelButton)
expect(mockCloseModal).toHaveBeenCalled()
expect(mockOnApplyEffects).not.toHaveBeenCalled()
})
})
describe("edge cases", () => {
it("should handle missing modal data", () => {
mockUseModal.mockReturnValue({
modalData: null,
closeModal: mockCloseModal,
})
render()
expect(screen.getByText("Fade In")).toBeInTheDocument()
})
it("should handle empty active effects", () => {
mockUseModal.mockReturnValue({
modalData: {
activeEffects: null,
onApplyEffects: mockOnApplyEffects,
},
closeModal: mockCloseModal,
})
render()
const switches = screen.getAllByRole("switch")
switches.forEach((switchEl) => {
expect(switchEl).toHaveAttribute("aria-checked", "false")
})
})
it("should maintain correct order when applying multiple effects", () => {
render()
// Enable fade in (order 0)
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
// Enable equalizer (order 1)
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
// Switch to dynamics tab and enable compressor (order 2)
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
const compressorSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(compressorSwitch)
// Apply
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({
effectId: "fade-in",
order: 0,
}),
expect.objectContaining({
effectId: "equalizer",
order: 1,
}),
expect.objectContaining({
effectId: "compressor",
order: 2,
}),
])
})
it("should preserve effects state when switching tabs", () => {
render()
// Enable fade in in basic tab
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
// Change fade in duration
const fadeInSlider = screen.getByTestId("slider")
fireEvent.change(fadeInSlider, { target: { value: "3" } })
// Switch to dynamics tab
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
// Enable compressor
const compressorSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(compressorSwitch)
// Switch back to basic tab
const basicTab = screen.getByText("Базовые")
fireEvent.click(basicTab)
// Verify fade in is still enabled with the correct duration
const basicSwitches = screen.getAllByRole("switch")
expect(basicSwitches[0]).toHaveAttribute("aria-checked", "true")
const sliders = screen.getAllByTestId("slider")
expect(sliders[0]).toHaveValue("3")
// Apply and verify both effects are included
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({
effectId: "fade-in",
customParams: { duration: 3 },
}),
expect.objectContaining({
effectId: "compressor",
}),
])
})
it("should apply effects from multiple tabs correctly", () => {
render()
// Enable effects from different tabs
// Basic tab: Enable fade in and equalizer
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
// Dynamics tab
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
const normalizeSwitch = screen.getAllByRole("switch")[1]
fireEvent.click(normalizeSwitch)
// Spatial tab
const spatialTab = screen.getByText("Пространство")
fireEvent.click(spatialTab)
const reverbSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(reverbSwitch)
// Correction tab
const correctionTab = screen.getByText("Коррекция")
fireEvent.click(correctionTab)
const denoiseSwitch = screen.getByRole("switch")
fireEvent.click(denoiseSwitch)
// Apply all effects
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({ effectId: "fade-in" }),
expect.objectContaining({ effectId: "equalizer" }),
expect.objectContaining({ effectId: "normalize" }),
expect.objectContaining({ effectId: "reverb" }),
expect.objectContaining({ effectId: "denoise" }),
])
// Verify all effects have correct order
const calledEffects = mockOnApplyEffects.mock.calls[0][0]
expect(calledEffects[0].order).toBe(0)
expect(calledEffects[1].order).toBe(1)
expect(calledEffects[2].order).toBe(2)
expect(calledEffects[3].order).toBe(3)
expect(calledEffects[4].order).toBe(4)
})
it("should apply all effect parameters correctly", () => {
render()
// Enable and configure fade in
const fadeInSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(fadeInSwitch)
const fadeInSlider = screen.getByTestId("slider")
fireEvent.change(fadeInSlider, { target: { value: "2.5" } })
// Enable and configure equalizer
const equalizerSwitch = screen.getAllByRole("switch")[2]
fireEvent.click(equalizerSwitch)
const eqSliders = screen.getAllByTestId("slider")
fireEvent.change(eqSliders[1], { target: { value: "-5" } }) // Low
fireEvent.change(eqSliders[2], { target: { value: "3" } }) // Mid
fireEvent.change(eqSliders[3], { target: { value: "7" } }) // High
// Switch to dynamics tab and configure compressor
const dynamicsTab = screen.getByText("Динамика")
fireEvent.click(dynamicsTab)
const compressorSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(compressorSwitch)
const compressorSliders = screen.getAllByTestId("slider")
fireEvent.change(compressorSliders[0], { target: { value: "-25" } }) // Threshold
fireEvent.change(compressorSliders[1], { target: { value: "6" } }) // Ratio
// Switch to spatial tab and configure reverb
const spatialTab = screen.getByText("Пространство")
fireEvent.click(spatialTab)
const reverbSwitch = screen.getAllByRole("switch")[0]
fireEvent.click(reverbSwitch)
const reverbSliders = screen.getAllByTestId("slider")
fireEvent.change(reverbSliders[0], { target: { value: "0.7" } }) // Room size
fireEvent.change(reverbSliders[1], { target: { value: "0.4" } }) // Wet
// Apply
const applyButton = screen.getByText("Применить эффекты")
fireEvent.click(applyButton)
expect(mockOnApplyEffects).toHaveBeenCalledWith([
expect.objectContaining({
effectId: "fade-in",
customParams: { duration: 2.5 },
}),
expect.objectContaining({
effectId: "equalizer",
customParams: { gain_low: -5, gain_mid: 3, gain_high: 7 },
}),
expect.objectContaining({
effectId: "compressor",
customParams: expect.objectContaining({
threshold: -25,
ratio: 6,
attack: 5, // Default values preserved
release: 50,
}),
}),
expect.objectContaining({
effectId: "reverb",
customParams: expect.objectContaining({
room_size: 0.7,
wet: 0.4,
damping: 0.5, // Default value preserved
}),
}),
])
})
})
})