/**
* Comprehensive tests for use-timeline-ai-analysis hook
*/
import { act, renderHook, waitFor } from "@testing-library/react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { useTimelineAIAnalysis } from "../../hooks/use-timeline-ai-analysis"
import type { TimelineClip } from "../../types/timeline"
import { MockTimelineProvider } from "../test-providers"
// Mock timeline-machine
vi.mock("../../services/timeline-machine", () => ({
timelineMachine: {},
}))
// Mock AI engines
const mockSceneEngine = {
initialize: vi.fn().mockResolvedValue(undefined),
process: vi.fn().mockResolvedValue({
scenes: [
{
id: "scene-1",
type: "dialogue",
startTime: 0,
duration: 15, // Больше 10 секунд - будет предложение о нарезке
confidence: 0.9,
},
{
id: "scene-2",
type: "action",
startTime: 15,
duration: 3, // Меньше 5 секунд - будет предложение о скорости
confidence: 0.85,
},
],
totalScenes: 2,
averageSceneLength: 9,
keyMoments: [
{
id: "moment-1",
timestamp: 5,
type: "emotional_peak",
description: "Эмоциональный пик",
score: 0.9,
},
],
}),
}
const mockOrchestrator = {
initialize: vi.fn().mockResolvedValue(undefined),
analyzeContent: vi.fn().mockResolvedValue({
id: "analysis-1",
mediaFile: {},
insights: {
summary: "Видео содержит диалог и экшн-сцены",
duration: 18,
dominantColors: ["#ff0000", "#00ff00"],
},
keyMoments: [
{
id: "moment-2",
timestamp: 10,
type: "climax",
description: "Кульминация",
score: 0.95,
},
],
qualityMetrics: {
overall: 45,
sharpness: 70,
brightness: 50,
contrast: 60,
},
contentType: "video",
timestamp: new Date().toISOString(),
}),
}
vi.mock("../../../ai-content-intelligence/engines/scene-analysis/services/scene-analysis-engine", () => ({
SceneAnalysisEngine: vi.fn(() => mockSceneEngine),
}))
vi.mock("../../../ai-content-intelligence/shared/services/ai-intelligence-orchestrator", () => ({
AIIntelligenceOrchestrator: vi.fn(() => mockOrchestrator),
}))
// Mock useTimeline
const mockSend = vi.fn()
const mockProject = {
id: "project-1",
name: "Test Project",
sections: [
{
id: "section-1",
name: "Section 1",
tracks: [
{
id: "track-1",
clips: [
{
id: "clip-1",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip,
],
},
],
},
],
globalTracks: [],
}
vi.mock("../../hooks/use-timeline", () => ({
useTimeline: vi.fn(() => ({
project: mockProject,
uiState: {},
send: mockSend,
})),
}))
describe("useTimelineAIAnalysis", () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllTimers()
})
it("должен инициализироваться с начальным состоянием", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
expect(result.current.state).toEqual({
isAnalyzing: false,
analysisProgress: 0,
currentAnalysis: null,
sceneAnalysis: null,
insights: null,
keyMoments: [],
error: null,
lastAnalyzedClipId: null,
})
expect(result.current.suggestions).toEqual([])
expect(result.current.enableAutoAnalysis).toBe(true)
})
it("должен инициализировать AI движки при монтировании", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
renderHook(() => useTimelineAIAnalysis(), { wrapper })
await waitFor(() => {
expect(mockSceneEngine.initialize).toHaveBeenCalled()
})
})
it("должен анализировать клип", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
expect(mockSceneEngine.process).toHaveBeenCalledWith({
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
},
})
// Оркестратор больше не используется
expect(result.current.state.sceneAnalysis).toBeDefined()
expect(result.current.state.currentAnalysis).toBeDefined()
expect(result.current.state.isAnalyzing).toBe(false)
expect(result.current.state.analysisProgress).toBe(100)
})
it("должен генерировать предложения после анализа", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
expect(result.current.suggestions.length).toBeGreaterThan(0)
// Проверяем предложения по типам
const markerSuggestions = result.current.suggestions.filter((s) => s.type === "marker")
const speedSuggestions = result.current.suggestions.filter((s) => s.type === "speed")
const colorSuggestions = result.current.suggestions.filter((s) => s.type === "color")
// Проверяем что есть предложения
expect(markerSuggestions.length).toBeGreaterThan(0)
expect(speedSuggestions.length).toBeGreaterThan(0)
expect(colorSuggestions.length).toBeGreaterThan(0)
})
it("должен анализировать весь timeline", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
await act(async () => {
await result.current.analyzeTimeline()
})
expect(mockSceneEngine.process).toHaveBeenCalled()
// Оркестратор больше не используется
expect(result.current.state.isAnalyzing).toBe(false)
})
it("должен очищать анализ", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Сначала анализируем
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
expect(result.current.state.currentAnalysis).toBeDefined()
expect(result.current.suggestions.length).toBeGreaterThan(0)
// Очищаем
act(() => {
result.current.clearAnalysis()
})
expect(result.current.state).toEqual({
isAnalyzing: false,
analysisProgress: 0,
currentAnalysis: null,
sceneAnalysis: null,
insights: null,
keyMoments: [],
error: null,
lastAnalyzedClipId: null,
})
expect(result.current.suggestions).toEqual([])
})
it("должен применять предложение о нарезке", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Настраиваем мок для генерации предложения о нарезке
mockSceneEngine.process.mockResolvedValueOnce({
scenes: [
{
id: "scene-1",
type: "dialogue",
startTime: 0,
duration: 5,
confidence: 0.9,
},
{
id: "scene-2",
type: "action",
startTime: 5,
duration: 15, // Больше 10 секунд - будет предложение о нарезке
confidence: 0.85,
},
],
totalScenes: 2,
averageSceneLength: 10,
keyMoments: [],
})
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
// Находим предложение о нарезке
const cutSuggestion = result.current.suggestions.find((s) => s.type === "cut")
expect(cutSuggestion).toBeDefined()
if (cutSuggestion) {
await act(async () => {
await result.current.applySuggestion(cutSuggestion)
})
expect(mockSend).toHaveBeenCalledWith({
type: "SPLIT_CLIP",
clipId: cutSuggestion.clipId,
splitTime: cutSuggestion.timestamp,
})
}
})
it("должен применять предложение о маркере", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Сначала анализируем клип чтобы получить предложения
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
// Находим предложение о маркере
const markerSuggestion = result.current.suggestions.find((s) => s.type === "marker")
expect(markerSuggestion).toBeDefined()
if (markerSuggestion) {
await act(async () => {
await result.current.applySuggestion(markerSuggestion)
})
expect(mockSend).toHaveBeenCalledWith({
type: "ADD_MARKER",
marker: expect.objectContaining({
type: "note",
timecode: markerSuggestion.timestamp,
name: markerSuggestion.title,
description: markerSuggestion.description,
color: "#3b82f6",
}),
})
}
})
it("должен применять предложение о скорости", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Сначала анализируем клип чтобы получить предложения
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
// Находим предложение о скорости
const speedSuggestion = result.current.suggestions.find((s) => s.type === "speed")
expect(speedSuggestion).toBeDefined()
if (speedSuggestion) {
await act(async () => {
await result.current.applySuggestion(speedSuggestion)
})
expect(mockSend).toHaveBeenCalledWith({
type: "UPDATE_CLIP",
clipId: speedSuggestion.clipId,
updates: {
playbackRate: speedSuggestion.actionData.speed,
},
})
}
})
it("должен отклонять предложения", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Сначала анализируем клип чтобы получить предложения
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
const suggestionsCount = result.current.suggestions.length
expect(suggestionsCount).toBeGreaterThan(0)
const firstSuggestion = result.current.suggestions[0]
act(() => {
result.current.dismissSuggestion(firstSuggestion.id)
})
expect(result.current.suggestions.length).toBe(suggestionsCount - 1)
expect(result.current.suggestions.find((s) => s.id === firstSuggestion.id)).toBeUndefined()
})
it("должен генерировать маркеры из анализа", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Сначала анализируем
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
// Генерируем маркеры
await act(async () => {
await result.current.generateMarkersFromAnalysis()
})
// Проверяем, что были созданы маркеры для ключевых моментов
expect(mockSend).toHaveBeenCalledWith({
type: "ADD_MARKER",
marker: expect.objectContaining({
type: "chapter",
timecode: 5,
name: "Эмоциональный пик",
}),
})
// Проверяем, что были созданы маркеры для смены сцен
expect(mockSend).toHaveBeenCalledWith({
type: "ADD_MARKER",
marker: expect.objectContaining({
type: "section",
timecode: 15,
name: "Сцена 2",
}),
})
})
it("должен находить ключевые моменты в клипе", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
const keyMoments = await act(async () => {
return await result.current.findKeyMoments(testClip)
})
expect(keyMoments).toHaveLength(1)
expect(keyMoments[0]).toEqual({
id: "moment-1",
timestamp: 5,
type: "emotional_peak",
description: "Эмоциональный пик",
score: 0.9,
})
})
it("должен обрабатывать ошибки при анализе", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
mockSceneEngine.process.mockRejectedValueOnce(new Error("Ошибка анализа"))
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(testClip)
})
expect(result.current.state.error).toBe("Ошибка анализа")
expect(result.current.state.isAnalyzing).toBe(false)
})
it("должен переключать автоматический анализ", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
expect(result.current.enableAutoAnalysis).toBe(true)
act(() => {
result.current.setEnableAutoAnalysis(false)
})
expect(result.current.enableAutoAnalysis).toBe(false)
})
it("должен автоматически анализировать новые клипы", async () => {
vi.useFakeTimers()
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
// Убеждаемся, что автоанализ включен
expect(result.current.enableAutoAnalysis).toBe(true)
// Ждем таймер (автоанализ срабатывает через 1 секунду)
await act(async () => {
await vi.advanceTimersByTimeAsync(1100)
})
// Проверяем что анализ был запущен
expect(mockSceneEngine.process).toHaveBeenCalledWith({
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
},
})
vi.useRealTimers()
})
it("не должен анализировать клип без медиафайла", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const clipWithoutMedia: TimelineClip = {
id: "clip-no-media",
name: "Empty Clip",
} as TimelineClip
await act(async () => {
await result.current.analyzeClip(clipWithoutMedia)
})
expect(mockSceneEngine.process).not.toHaveBeenCalled()
// Оркестратор больше не используется
})
it("не должен запускать новый анализ пока идет текущий", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useTimelineAIAnalysis(), { wrapper })
const testClip: TimelineClip = {
id: "clip-test",
name: "Test Clip",
mediaFile: {
path: "/test/video.mp4",
name: "video.mp4",
duration: 20,
size: 1000000,
format: "mp4",
},
} as TimelineClip
// Задерживаем выполнение анализа
mockSceneEngine.process.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 1000)))
// Запускаем первый анализ
act(() => {
void result.current.analyzeClip(testClip)
})
expect(result.current.state.isAnalyzing).toBe(true)
// Пытаемся запустить второй анализ
await act(async () => {
await result.current.analyzeClip(testClip)
})
// Проверяем, что process был вызван только один раз
expect(mockSceneEngine.process).toHaveBeenCalledTimes(1)
})
})