import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { TimelinePreviewStrip, useTimelinePreviewStrip } from "@/features/timeline/components/timeline-preview-strip" // Mock the frame extraction hook vi.mock("@/features/video-compiler/hooks/use-frame-extraction", () => ({ useSmartTimelinePreviews: vi.fn(), })) // Mock the frame extraction service vi.mock("@/features/video-compiler/services/frame-extraction-service", () => ({ frameExtractionService: { createPreviewElement: vi.fn(), }, })) // Mock Skeleton component vi.mock("@/components/ui/skeleton", () => ({ Skeleton: ({ className, style }: any) =>
, })) // Mock cn utility vi.mock("@/lib/utils", () => ({ cn: (...args: any[]) => args.filter(Boolean).join(" "), })) describe("TimelinePreviewStrip", () => { const mockUseSmartTimelinePreviews = vi.fn() beforeEach(async () => { vi.clearAllMocks() // Setup default mock return values const { useSmartTimelinePreviews } = await import("@/features/video-compiler/hooks/use-frame-extraction") vi.mocked(useSmartTimelinePreviews).mockImplementation(mockUseSmartTimelinePreviews) mockUseSmartTimelinePreviews.mockReturnValue({ frames: [], isLoading: false, error: null, progress: 0, frameWidth: 80, }) // Mock the frame extraction service const { frameExtractionService } = await import("@/features/video-compiler/services/frame-extraction-service") vi.mocked(frameExtractionService.createPreviewElement).mockReturnValue({ src: "data:image/jpeg;base64,test-frame-data", onload: null, className: "", }) }) afterEach(() => { vi.resetAllMocks() }) const defaultProps = { videoPath: "/test/video.mp4", duration: 10, containerWidth: 800, scale: 80, } describe("initial rendering", () => { it("should render without error", () => { render() expect(mockUseSmartTimelinePreviews).toHaveBeenCalledWith("/test/video.mp4", 10, 800, { cacheResults: true, interval: 1.0, }) }) it("should apply custom height and className", () => { const { container } = render() const element = container.firstChild as HTMLElement expect(element).toHaveStyle({ height: "120px" }) expect(element).toHaveClass("custom-class") }) it("should handle null video path", () => { render() expect(mockUseSmartTimelinePreviews).toHaveBeenCalledWith(null, 10, 800, { cacheResults: true, interval: 1.0, }) }) }) describe("loading states", () => { it("should show skeletons during initial loading", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [], isLoading: true, error: null, progress: 25, frameWidth: 80, }) render() const skeletons = screen.getAllByTestId("skeleton") expect(skeletons).toHaveLength(10) // Math.min(10, Math.floor(800 / 80)) skeletons.forEach((skeleton) => { expect(skeleton).toHaveStyle({ width: "76px", height: "52px" }) // frameWidth - 4, height - 8 }) }) it("should show progress bar during loading", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [{ timestamp: 1, frameData: "test", isKeyframe: false }], isLoading: true, error: null, progress: 75, frameWidth: 80, }) const { container } = render() const progressBar = container.querySelector('[style*="width: 75%"]') expect(progressBar).toBeInTheDocument() }) it("should not show progress bar when not loading", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [{ timestamp: 1, frameData: "test", isKeyframe: false }], isLoading: false, error: null, progress: 100, frameWidth: 80, }) const { container } = render() const progressBar = container.querySelector('[style*="width: 100%"]') expect(progressBar).not.toBeInTheDocument() }) }) describe("error handling", () => { it("should display error message when loading fails", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [], isLoading: false, error: new Error("Failed to load frames"), progress: 0, frameWidth: 80, }) render() expect(screen.getByText("Ошибка загрузки превью")).toBeInTheDocument() }) it("should apply custom className to error state", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [], isLoading: false, error: new Error("Failed to load frames"), progress: 0, frameWidth: 80, }) const { container } = render() const errorElement = container.firstChild as HTMLElement expect(errorElement).toHaveClass("error-class") }) }) describe("frame rendering", () => { const mockFrames = [ { timestamp: 1, frameData: "frame1-data", isKeyframe: true }, { timestamp: 2, frameData: "frame2-data", isKeyframe: false }, { timestamp: 3, frameData: "frame3-data", isKeyframe: false }, ] beforeEach(() => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: mockFrames, isLoading: false, error: null, progress: 100, frameWidth: 80, }) }) it("should render all frames", () => { render() mockFrames.forEach((frame) => { expect(screen.getByAltText(`Frame at ${frame.timestamp}s`)).toBeInTheDocument() }) }) it("should position frames correctly", () => { const { container } = render() const frameContainer = container.querySelector('[style*="width: 800px"]') // duration * scale expect(frameContainer).toBeInTheDocument() }) it("should handle frame clicks", () => { const onFrameClick = vi.fn() render() const firstFrame = screen.getByAltText("Frame at 1s") fireEvent.click(firstFrame.closest('[title*="1.00s"]')!) expect(onFrameClick).toHaveBeenCalledWith(1) }) it("should show keyframe indicators", () => { render() const keyframeIndicator = screen.getByText("K") expect(keyframeIndicator).toBeInTheDocument() }) it("should show timestamps when enabled", () => { render() expect(screen.getByText("0:01.00")).toBeInTheDocument() expect(screen.getByText("0:02.00")).toBeInTheDocument() expect(screen.getByText("0:03.00")).toBeInTheDocument() }) it("should not show timestamps when disabled", () => { render() expect(screen.queryByText("0:01.00")).not.toBeInTheDocument() }) }) describe("scroll handling", () => { it("should update visible range based on scroll offset", () => { const { rerender } = render() // Change scroll offset rerender() const { container } = render() const frameContainer = container.querySelector('[style*="translateX(-160px)"]') expect(frameContainer).toBeInTheDocument() }) it("should render all frames regardless of visible range (current behavior)", () => { const allFrames = [ { timestamp: 0.5, frameData: "frame1", isKeyframe: false }, { timestamp: 5, frameData: "frame2", isKeyframe: false }, { timestamp: 15, frameData: "frame3", isKeyframe: false }, // Currently renders all frames ] mockUseSmartTimelinePreviews.mockReturnValue({ frames: allFrames, isLoading: false, error: null, progress: 100, frameWidth: 80, }) render() // Currently renders all frames (TODO: should be optimized to only render visible ones) expect(screen.getByAltText("Frame at 0.5s")).toBeInTheDocument() expect(screen.getByAltText("Frame at 5s")).toBeInTheDocument() expect(screen.getByAltText("Frame at 15s")).toBeInTheDocument() }) }) describe("PreviewFrame component", () => { it("should create preview element on mount", async () => { const { frameExtractionService } = await import("@/features/video-compiler/services/frame-extraction-service") mockUseSmartTimelinePreviews.mockReturnValue({ frames: [{ timestamp: 1, frameData: "test-data", isKeyframe: false }], isLoading: false, error: null, progress: 100, frameWidth: 80, }) render() expect(frameExtractionService.createPreviewElement).toHaveBeenCalledWith("test-data", 1) }) it("should show loading skeleton before image loads", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [{ timestamp: 1, frameData: "test-data", isKeyframe: false }], isLoading: false, error: null, progress: 100, frameWidth: 80, }) render() const skeleton = screen.getByTestId("skeleton") expect(skeleton).toBeInTheDocument() }) it("should show correct title for keyframes", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [{ timestamp: 1, frameData: "test-data", isKeyframe: true }], isLoading: false, error: null, progress: 100, frameWidth: 80, }) render() const frame = screen.getByAltText("Frame at 1s").closest('[title*="(Ключевой кадр)"]') expect(frame).toBeInTheDocument() }) }) describe("formatTimestamp function", () => { it("should format timestamps correctly", () => { mockUseSmartTimelinePreviews.mockReturnValue({ frames: [ { timestamp: 0, frameData: "test", isKeyframe: false }, { timestamp: 65.5, frameData: "test", isKeyframe: false }, { timestamp: 3661.25, frameData: "test", isKeyframe: false }, ], isLoading: false, error: null, progress: 100, frameWidth: 80, }) render() expect(screen.getByText("0:00.00")).toBeInTheDocument() expect(screen.getByText("1:05.50")).toBeInTheDocument() expect(screen.getByText("61:01.25")).toBeInTheDocument() }) }) }) describe("useTimelinePreviewStrip hook", () => { beforeEach(() => { // Mock ResizeObserver global.ResizeObserver = vi.fn().mockImplementation((callback) => ({ observe: vi.fn().mockImplementation((_element) => { // Simulate resize observation setTimeout(() => { callback([{ contentRect: { width: 1200 } }]) }, 0) }), unobserve: vi.fn(), disconnect: vi.fn(), })) }) afterEach(() => { vi.restoreAllMocks() }) it("should return container ref and width", () => { const TestComponent = () => { const { containerRef, containerWidth } = useTimelinePreviewStrip("/test.mp4", 10) return (
Container
{containerWidth}
) } render() const container = screen.getByTestId("container") expect(container).toBeInTheDocument() const width = screen.getByTestId("width") expect(width).toHaveTextContent("0") // Initial width }) it("should update container width on resize", async () => { const TestComponent = () => { const { containerRef, containerWidth } = useTimelinePreviewStrip("/test.mp4", 10) return (
Container
{containerWidth}
) } render() await waitFor(() => { const width = screen.getByTestId("width") expect(width).toHaveTextContent("1200") }) }) it("should disconnect observer on unmount", () => { const mockDisconnect = vi.fn() global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: mockDisconnect, })) const TestComponent = () => { const { containerRef } = useTimelinePreviewStrip("/test.mp4", 10) return (
Container
) } const { unmount } = render() unmount() expect(mockDisconnect).toHaveBeenCalled() }) it("should handle missing container ref", () => { const TestComponent = () => { const { containerWidth } = useTimelinePreviewStrip("/test.mp4", 10) return {containerWidth} } render() const width = screen.getByTestId("width") expect(width).toHaveTextContent("0") }) })