/** * Tests for PreviewGrid component */ import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { describe, expect, it, vi } from "vitest" import type { UnifiedContentAnalysis } from "../../../shared/types" import { PreviewGrid } from "../preview-grid" // Mock lucide-react icons vi.mock("lucide-react", async (importOriginal) => { const actual = (await importOriginal()) as any const MockIcon = ({ ...props }: any) =>
return { ...actual, Play: MockIcon, Pause: MockIcon, Download: MockIcon, Heart: MockIcon, Share2: MockIcon, Filter: MockIcon, Grid3X3: MockIcon, Grid2X2: MockIcon, List: MockIcon, Search: MockIcon, Clock: MockIcon, Eye: MockIcon, Star: MockIcon, MoreVertical: MockIcon, } }) // Mock analysis data const createMockAnalysis = (): UnifiedContentAnalysis => ({ mediaFile: { path: "/test/video.mp4", filename: "video.mp4", size: 1024 * 1024 * 100, format: "mp4", duration: 180, }, scenes: [ { id: "scene-1", startTime: 0, endTime: 60, duration: 60, type: "dialogue" as any, keyFrames: [ { timestamp: 30, thumbnailPath: "/thumbnails/scene-1-frame-1.jpg", composition: { ruleOfThirds: 0.8, balance: 0.7, leadingLines: true, depth: 0.6, colorHarmony: 0.9, }, isKeyMoment: true, score: 85, }, ], quality: { overall: 85, sharpness: 90, brightness: 80, contrast: 85, saturation: 75, stability: 95, noise: 10, }, content: { objects: [ { id: "obj-1", label: "person", confidence: 0.95, boundingBox: { x: 0, y: 0, width: 100, height: 100 }, frameNumbers: [1, 2, 3], }, ], faces: [], text: [], activities: [], }, transitions: [], }, { id: "scene-2", startTime: 60, endTime: 120, duration: 60, type: "action" as any, keyFrames: [ { timestamp: 90, thumbnailPath: "/thumbnails/scene-2-frame-1.jpg", composition: { ruleOfThirds: 0.6, balance: 0.8, leadingLines: false, depth: 0.9, colorHarmony: 0.7, }, isKeyMoment: false, score: 75, }, ], quality: { overall: 75, sharpness: 80, brightness: 70, contrast: 75, saturation: 80, stability: 85, noise: 15, }, content: { objects: [ { id: "obj-2", label: "car", confidence: 0.88, boundingBox: { x: 0, y: 0, width: 200, height: 150 }, frameNumbers: [61, 62, 63], }, ], faces: [], text: [], activities: [], }, transitions: [], }, ], keyMoments: [ { id: "moment-1", timestamp: 30, duration: 5, type: "emotional_peak" as any, score: 95, description: "Intense emotional dialogue", sceneId: "scene-1", }, { id: "moment-2", timestamp: 90, duration: 10, type: "action_peak" as any, score: 88, description: "High-speed chase begins", sceneId: "scene-2", }, ], contentType: "narrative" as any, genres: ["drama" as any, "action" as any], mood: { primary: "dramatic" as any, intensity: 0.8, }, targetAudience: { ageRange: { min: 16, max: 35 }, interests: ["action", "drama", "suspense"], demographics: { primary: "young adults", }, }, technicalSpecs: { resolution: { width: 1920, height: 1080, aspectRatio: "16:9" }, frameRate: 30, bitrate: 5000000, codec: "h264", audioChannels: 2, audioCodec: "aac", audioBitrate: 128000, duration: 180, }, qualityMetrics: { overall: 80, sharpness: 85, brightness: 75, contrast: 80, saturation: 78, stability: 90, noise: 12, }, detections: { objects: [], faces: [], text: [], audio: { speech: [], music: [], soundEffects: [], silence: [], }, scenes: [], }, insights: { summary: "A dramatic narrative with intense emotional moments and action sequences.", highlights: [ "Strong emotional dialogue at 0:30", "Effective use of pacing between dialogue and action", "High production quality throughout", ], suggestions: [], warnings: [], opportunities: [], }, }) describe("PreviewGrid", () => { it("should render empty state when no analysis provided", () => { render() expect(screen.getByText("Нет контента для отображения")).toBeInTheDocument() expect(screen.getByText("Загрузите и проанализируйте видео")).toBeInTheDocument() }) it("should render preview items when analysis provided", () => { const analysis = createMockAnalysis() render() // Should show scenes, moments, and highlights expect(screen.getByText("Сцена dialogue")).toBeInTheDocument() expect(screen.getByText("Сцена action")).toBeInTheDocument() expect(screen.getByText("emotional peak")).toBeInTheDocument() expect(screen.getByText("action peak")).toBeInTheDocument() expect(screen.getAllByText("Основной момент")).toHaveLength(3) // 3 highlights }) it("should filter items by type", async () => { const user = userEvent.setup() const analysis = createMockAnalysis() render() // Initially all items should be visible expect(screen.getByText("Сцена dialogue")).toBeInTheDocument() expect(screen.getByText("emotional peak")).toBeInTheDocument() // Filter by scenes only await user.click(screen.getByText("Сцены")) await waitFor(() => { expect(screen.getByText("Сцена dialogue")).toBeInTheDocument() expect(screen.getByText("Сцена action")).toBeInTheDocument() // Moments should be filtered out expect(screen.queryByText("emotional peak")).not.toBeInTheDocument() }) }) it("should search items by title and description", async () => { const user = userEvent.setup() const analysis = createMockAnalysis() render() const searchInput = screen.getByPlaceholderText("Поиск по контенту...") // Search for "emotional" await user.type(searchInput, "emotional") await waitFor(() => { expect(screen.getByText("emotional peak")).toBeInTheDocument() expect(screen.queryByText("action peak")).not.toBeInTheDocument() }) }) it("should switch between grid and list view modes", async () => { const user = userEvent.setup() const analysis = createMockAnalysis() render() // Should start in grid mode by default const gridContainer = screen.getByText("Сцена dialogue").closest(".grid") expect(gridContainer).toBeTruthy() // Switch to list view const listButton = screen .getAllByTestId("mock-icon") .find((icon) => icon.closest("button")?.getAttribute("class")?.includes("ghost")) if (listButton) { await user.click(listButton.closest("button")!) await waitFor(() => { const listContainer = screen.getByText("Сцена dialogue").closest(".space-y-2") expect(listContainer).toBeTruthy() }) } }) it("should handle item selection when enabled", async () => { const user = userEvent.setup() const onSelectionChange = vi.fn() const analysis = createMockAnalysis() render() // Should show checkboxes and select all option expect(screen.getAllByRole("checkbox")).toHaveLength(8) // 1 for select-all + 7 items // Click on first item to select it const firstItem = screen.getByText("Сцена dialogue").closest('[class*="cursor-pointer"]') if (firstItem) { await user.click(firstItem) expect(onSelectionChange).toHaveBeenCalled() } }) it("should call action handlers when provided", async () => { const user = userEvent.setup() const onItemPlay = vi.fn() const onItemDownload = vi.fn() const onItemStar = vi.fn() const onItemShare = vi.fn() const analysis = createMockAnalysis() render( , ) // Find and click on first item (card should be clickable) const firstCard = screen.getByText("Сцена dialogue").closest('[class*="cursor-pointer"]') expect(firstCard).toBeTruthy() // Click on the card to trigger onItemSelect (which isn't passed, so it should work) await user.click(firstCard!) // Check that action handlers are available (they should be functions) expect(onItemPlay).toBeInstanceOf(Function) expect(onItemDownload).toBeInstanceOf(Function) expect(onItemStar).toBeInstanceOf(Function) expect(onItemShare).toBeInstanceOf(Function) }) it("should handle pagination correctly", async () => { const user = userEvent.setup() const analysis = createMockAnalysis() render() // Should show pagination controls if more than 3 items const totalItems = 2 + 2 + 3 // 2 scenes + 2 moments + 3 highlights = 7 items if (totalItems > 3) { expect(screen.getByText(/Страница \d+ из \d+/)).toBeInTheDocument() // Should have next button const nextButton = screen.getByText("Вперед") expect(nextButton).toBeInTheDocument() // Click next button await user.click(nextButton) // Should show different content on page 2 await waitFor(() => { expect(screen.getByText(/Страница 2 из/)).toBeInTheDocument() }) } }) it("should sort items correctly", async () => { const user = userEvent.setup() const analysis = createMockAnalysis() render() const sortSelect = screen.getByDisplayValue("По времени") // Change to sort by score await user.selectOptions(sortSelect, "score") await waitFor(() => { expect(sortSelect).toHaveValue("score") }) }) it("should apply custom className", () => { const customClass = "custom-preview-grid" const analysis = createMockAnalysis() const { container } = render() const element = container.querySelector(".preview-grid") expect(element).toHaveClass(customClass) }) it("should display correct item counts and summary", () => { const analysis = createMockAnalysis() render() // Should show results summary expect(screen.getByText(/Показано \d+ из \d+ элементов/)).toBeInTheDocument() }) it("should handle thumbnail loading errors gracefully", () => { const analysis = createMockAnalysis() render() // Find image elements const images = screen.getAllByRole("img") expect(images.length).toBeGreaterThan(0) // Check that images have src attributes const firstImage = images[0] as HTMLImageElement expect(firstImage).toBeTruthy() expect(firstImage.src).toBeTruthy() // Test would verify onError handling, but in JSDOM it's hard to test // We just verify the image elements exist and have proper src expect(firstImage.alt).toBeTruthy() }) })