import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { toast } from "sonner" import { beforeEach, describe, expect, it, vi } from "vitest" // Mock react-i18next vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string, defaultValue?: any) => { if (typeof defaultValue === "string") return defaultValue return key }, i18n: { language: "ru", }, }), })) // Mock dependencies vi.mock("@/features/modals/services", () => ({ useModal: vi.fn(() => ({ modalData: null, closeModal: vi.fn(), })), })) vi.mock("@/features/timeline/hooks/use-timeline", () => ({ useTimeline: vi.fn(() => ({ project: null, send: vi.fn(), })), })) vi.mock("@/features/app-state/hooks/use-media-files", () => ({ useMediaFiles: vi.fn(() => ({ mediaFiles: [], })), })) vi.mock("@/features/ai-chat/services/whisper-service", () => ({ WhisperService: { getInstance: vi.fn(() => ({ loadApiKey: vi.fn().mockResolvedValue(false), hasApiKey: vi.fn(() => false), isLocalWhisperAvailable: vi.fn().mockResolvedValue(true), extractAudioForTranscription: vi.fn().mockResolvedValue("/tmp/audio.wav"), transcribeWithOpenAI: vi.fn(), transcribeWithLocalModel: vi.fn(), convertToSRT: vi.fn(), recommendModel: vi.fn(() => "whisper-base"), })), }, })) vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), }, })) import { WhisperService } from "@/features/ai-chat/services/whisper-service" import { useMediaFiles } from "@/features/app-state/hooks/use-media-files" import { useModal } from "@/features/modals/services" import { useTimeline } from "@/features/timeline/hooks/use-timeline" import { SubtitleAIToolsModal } from "../subtitle-ai-tools-modal" const mockedUseModal = vi.mocked(useModal) const mockedUseTimeline = vi.mocked(useTimeline) const mockedUseMediaFiles = vi.mocked(useMediaFiles) const mockedWhisperService = vi.mocked(WhisperService.getInstance) describe("SubtitleAIToolsModal", () => { const mockProject = { sections: [ { tracks: [ { id: "video-track-1", type: "video", clips: [ { id: "clip-1", type: "video", mediaFile: { path: "/path/to/video.mp4", name: "video.mp4", }, duration: 120, }, ], }, { id: "audio-track-1", type: "audio", clips: [ { id: "clip-2", type: "audio", mediaFile: { path: "/path/to/audio.mp3", name: "audio.mp3", }, duration: 180, }, ], }, ], }, ], globalTracks: [], } const mockSend = vi.fn() const mockCloseModal = vi.fn() beforeEach(() => { vi.clearAllMocks() ;(mockedUseModal as any).mockReturnValue({ modalData: null, closeModal: mockCloseModal, }) mockedUseTimeline.mockReturnValue({ project: mockProject as any, send: mockSend, } as any) // Extract media files from mock project const mediaFiles = [] for (const section of mockProject.sections) { for (const track of section.tracks) { for (const clip of track.clips) { if (clip.mediaFile) { mediaFiles.push({ id: clip.id, path: clip.mediaFile.path, name: clip.mediaFile.name, duration: clip.duration, media_type: track.type === "video" ? "Video" : "Audio", }) } } } } mockedUseMediaFiles.mockReturnValue({ mediaFiles: mediaFiles as any, } as any) }) it("should render the modal with description", () => { render() expect(screen.getByText("Используйте AI для автоматического создания субтитров из аудио")).toBeTruthy() }) it("should display available media files", async () => { render() const fileSelect = screen.getAllByRole("combobox")[0] fireEvent.click(fileSelect) // Wait for the dropdown to open and check for files await waitFor(() => { expect(screen.getAllByText("video.mp4").length).toBeGreaterThan(0) expect(screen.getAllByText("audio.mp3").length).toBeGreaterThan(0) }) }) it("should display language options", () => { render() const languageSelect = screen.getAllByRole("combobox")[1] fireEvent.click(languageSelect) // Use getAllByText since there might be duplicates in the portal expect(screen.getAllByText("Автоопределение").length).toBeGreaterThan(0) expect(screen.getAllByText("Русский").length).toBeGreaterThan(0) expect(screen.getAllByText("English").length).toBeGreaterThan(0) }) it("should display model selection options", async () => { render() const modelSelect = screen.getAllByRole("combobox")[2] fireEvent.click(modelSelect) await waitFor(() => { expect(screen.getAllByText("OpenAI Whisper (облачный)").length).toBeGreaterThan(0) expect(screen.getAllByText("Локальная Tiny (39 MB)").length).toBeGreaterThan(0) expect(screen.getAllByText("Локальная Large v3 (1.5 GB)").length).toBeGreaterThan(0) }) }) it("should disable start button when no file is selected", () => { render() const startButton = screen.getByText("Начать транскрипцию") expect(startButton).toBeDisabled() }) it("should show no media message when project has no files", () => { mockedUseTimeline.mockReturnValue({ project: { sections: [], globalTracks: [] } as any, send: mockSend, } as any) mockedUseMediaFiles.mockReturnValue({ mediaFiles: [], } as any) render() expect(screen.getByText("Добавьте видео или аудио файлы в проект для транскрипции")).toBeTruthy() }) it("should start transcription with selected video file", async () => { const mockTranscriptionResult = { segments: [ { id: 1, seek: 0, start: 0, end: 3, text: "Test subtitle", tokens: [], temperature: 0, avg_logprob: 0, compression_ratio: 1, no_speech_prob: 0, }, ], } const mockWhisper = { loadApiKey: vi.fn().mockResolvedValue(false), hasApiKey: vi.fn(() => false), isLocalWhisperAvailable: vi.fn().mockResolvedValue(true), extractAudioForTranscription: vi.fn().mockResolvedValue("/tmp/audio.wav"), transcribeWithLocalModel: vi.fn().mockResolvedValue(mockTranscriptionResult), convertToSRT: vi.fn(() => "1\n00:00:00,000 --> 00:00:03,000\nTest subtitle"), recommendModel: vi.fn(() => "whisper-base"), } mockedWhisperService.mockReturnValue(mockWhisper as any) render() // Select video file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("video.mp4")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(toast.info).toHaveBeenCalledWith("Извлечение аудио из видео...") expect(mockWhisper.extractAudioForTranscription).toHaveBeenCalledWith("/path/to/video.mp4") }) await waitFor(() => { expect(toast.info).toHaveBeenCalledWith("Транскрипция с помощью локальной модели...") expect(mockWhisper.transcribeWithLocalModel).toHaveBeenCalledWith( "/tmp/audio.wav", "whisper-base", expect.objectContaining({ language: undefined, outputFormat: "json", }), ) }) await waitFor(() => { expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ type: "ADD_TRACK", track: expect.objectContaining({ type: "subtitle", }), }), ) }) await waitFor(() => { expect(toast.success).toHaveBeenCalledWith( "Транскрипция завершена", expect.objectContaining({ description: "Добавлено {{count}} субтитров", }), ) expect(mockCloseModal).toHaveBeenCalled() }) }) it("should use OpenAI API when API key is available", async () => { const mockTranscriptionResult = { text: "Test subtitle", segments: [], } const mockWhisper = { loadApiKey: vi.fn().mockResolvedValue(true), hasApiKey: vi.fn(() => true), isLocalWhisperAvailable: vi.fn().mockResolvedValue(false), transcribeWithOpenAI: vi.fn().mockResolvedValue(mockTranscriptionResult), convertToSRT: vi.fn(() => ""), } mockedWhisperService.mockReturnValue(mockWhisper as any) render() // Select audio file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("audio.mp3")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(toast.info).toHaveBeenCalledWith("Транскрипция с помощью OpenAI Whisper...") expect(mockWhisper.transcribeWithOpenAI).toHaveBeenCalledWith( "/path/to/audio.mp3", expect.objectContaining({ language: undefined, response_format: "verbose_json", timestamp_granularities: ["segment"], }), ) }) }) it("should handle transcription errors", async () => { const mockWhisper = { loadApiKey: vi.fn().mockResolvedValue(false), hasApiKey: vi.fn(() => false), isLocalWhisperAvailable: vi.fn().mockResolvedValue(true), transcribeWithLocalModel: vi.fn().mockRejectedValue(new Error("Transcription failed")), recommendModel: vi.fn(() => "whisper-base"), } mockedWhisperService.mockReturnValue(mockWhisper as any) render() // Select file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("audio.mp3")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(toast.error).toHaveBeenCalledWith( "Ошибка транскрипции", expect.objectContaining({ description: "Transcription failed", }), ) }) }) it("should show local model note when local model is selected", async () => { render() const modelSelect = screen.getAllByRole("combobox")[2] fireEvent.click(modelSelect) await waitFor(() => { const baseOptions = screen.getAllByText("Локальная Base (74 MB)") fireEvent.click(baseOptions[baseOptions.length - 1]) // Click the last one (in dropdown) }) await waitFor(() => { expect(screen.getByText("Локальные модели работают без интернета, но могут быть медленнее")).toBeTruthy() }) }) it("should disable start button when transcribing", async () => { render() // Select file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("audio.mp3")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(startButton).toBeDisabled() }) }) it("should handle no available transcription method", async () => { const mockWhisper = { loadApiKey: vi.fn().mockResolvedValue(false), hasApiKey: vi.fn(() => false), isLocalWhisperAvailable: vi.fn().mockResolvedValue(false), } mockedWhisperService.mockReturnValue(mockWhisper as any) render() // Select file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("audio.mp3")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(toast.error).toHaveBeenCalledWith( "Метод транскрипции недоступен", expect.objectContaining({ description: "Установите API ключ OpenAI или локальную модель Whisper", }), ) }) }) it("should handle duplicate media files", async () => { const projectWithDuplicates = { sections: [ { tracks: [ { id: "track-1", type: "video", clips: [ { id: "clip-1", type: "video", mediaFile: { path: "/path/to/video.mp4", name: "video.mp4" }, duration: 120, }, { id: "clip-2", type: "video", mediaFile: { path: "/path/to/video.mp4", name: "video.mp4" }, duration: 120, }, ], }, ], }, ], globalTracks: [], } mockedUseTimeline.mockReturnValue({ project: projectWithDuplicates as any, send: mockSend, } as any) // Extract media files from projectWithDuplicates const mediaFilesMap = new Map() for (const section of projectWithDuplicates.sections) { for (const track of section.tracks) { for (const clip of track.clips) { if (clip.mediaFile && !mediaFilesMap.has(clip.mediaFile.path)) { mediaFilesMap.set(clip.mediaFile.path, { id: clip.id, path: clip.mediaFile.path, name: clip.mediaFile.name, duration: clip.duration, media_type: track.type === "video" ? "Video" : "Audio", }) } } } } const mediaFiles = Array.from(mediaFilesMap.values()) mockedUseMediaFiles.mockReturnValue({ mediaFiles: mediaFiles as any, } as any) render() const fileSelect = screen.getAllByRole("combobox")[0] fireEvent.click(fileSelect) // Should show only one instance of video.mp4 in the dropdown await waitFor(() => { // The file appears in the select value and in the dropdown, so we expect 2 const videoOptions = screen.getAllByText("video.mp4") expect(videoOptions.length).toBeGreaterThan(0) }) }) it("should use selected language for transcription", async () => { const mockWhisper = { loadApiKey: vi.fn().mockResolvedValue(false), hasApiKey: vi.fn(() => false), isLocalWhisperAvailable: vi.fn().mockResolvedValue(true), transcribeWithLocalModel: vi.fn().mockResolvedValue({ text: "Test" }), recommendModel: vi.fn(() => "whisper-base"), } mockedWhisperService.mockReturnValue(mockWhisper as any) render() // Select file fireEvent.click(screen.getByText("Выберите файл...")) fireEvent.click(screen.getByText("audio.mp3")) // Select language const languageSelect = screen.getAllByRole("combobox")[1] fireEvent.click(languageSelect) fireEvent.click(screen.getByText("Русский")) // Start transcription const startButton = screen.getByText("Начать транскрипцию") fireEvent.click(startButton) await waitFor(() => { expect(mockWhisper.transcribeWithLocalModel).toHaveBeenCalledWith( "/path/to/audio.mp3", "whisper-base", expect.objectContaining({ language: "ru", outputFormat: "json", }), ) }) }) })