import { fireEvent, render, screen } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { MediaFile } from "@/features/media/types/media" import { TransitionPreview } from "../../components/transition-preview" import { Transition } from "../../types/transitions" // Mock dependencies vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string, fallback?: string) => fallback || key, }), })) vi.mock("@/features/browser", () => ({ ApplyButton: ({ resource, onClick }: any) => ( ), })) vi.mock("@/features/browser/components/layout/add-media-button", () => ({ AddMediaButton: ({ resource }: any) => , })) vi.mock("@/features/browser/components/layout/favorite-button", () => ({ FavoriteButton: ({ file }: any) => , })) vi.mock("../../hooks/use-transitions", () => ({ useTransitions: () => ({ transitions: [ { id: "fade", type: "fade", name: "Fade", labels: { ru: "Затухание", en: "Fade" }, category: "basic", complexity: "basic", duration: { default: 1.0, min: 0.5, max: 3.0 }, ffmpegCommand: () => "fade", }, { id: "zoom", type: "zoom", name: "Zoom", labels: { ru: "Увеличение", en: "Zoom" }, category: "advanced", complexity: "intermediate", duration: { default: 1.5, min: 0.5, max: 3.0 }, ffmpegCommand: () => "zoompan", }, ] as Transition[], }), })) describe("TransitionPreview", () => { const mockSourceVideo: MediaFile = { id: "source", path: "/path/to/source.mp4", name: "source.mp4", size: 1000, type: "video", duration: 10, metadata: { width: 1920, height: 1080, fps: 30, codec: "h264", bitrate: 5000000, }, } const mockTargetVideo: MediaFile = { id: "target", path: "/path/to/target.mp4", name: "target.mp4", size: 1000, type: "video", duration: 10, metadata: { width: 1920, height: 1080, fps: 30, codec: "h264", bitrate: 5000000, }, } const mockTransition: Transition = { id: "fade", type: "fade", name: "Fade", labels: { ru: "Затухание", en: "Fade" }, category: "basic", complexity: "basic", duration: { default: 1.0, min: 0.5, max: 3.0 }, ffmpegCommand: () => "fade", } const defaultProps = { sourceVideo: mockSourceVideo, targetVideo: mockTargetVideo, transitionType: "fade", onClick: vi.fn(), size: 120, } beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() // Mock HTMLVideoElement methods globally HTMLVideoElement.prototype.play = vi.fn().mockResolvedValue(undefined) HTMLVideoElement.prototype.pause = vi.fn() HTMLVideoElement.prototype.load = vi.fn() // Mock currentTime and other properties Object.defineProperty(HTMLVideoElement.prototype, "currentTime", { value: 0, writable: true, configurable: true, }) }) afterEach(() => { vi.runOnlyPendingTimers() vi.useRealTimers() }) describe("Component Rendering", () => { it("should render with basic props", () => { render() // Check that video elements are rendered expect(screen.getByTestId("source-video")).toBeInTheDocument() expect(screen.getByTestId("target-video")).toBeInTheDocument() // Check that control buttons are rendered expect(screen.getByTestId("favorite-button")).toBeInTheDocument() expect(screen.getByTestId("apply-button")).toBeInTheDocument() }) it("should render with transition prop", () => { render() // Should show add media button when transition is provided expect(screen.getByTestId("add-media-button")).toBeInTheDocument() }) it("should render with custom preview dimensions", () => { render() const container = screen.getByTestId("source-video").closest("div")?.parentElement expect(container).toHaveStyle({ width: "200px", height: "150px" }) }) it("should display transition name", () => { render() expect(screen.getByText("Затухание")).toBeInTheDocument() }) it("should display category indicator", () => { render() const categoryIndicator = screen.getByText("BSC") expect(categoryIndicator).toBeInTheDocument() // The indicator should be within a div that has styling const styledParent = categoryIndicator.closest('div[class*="bg-"]') expect(styledParent).toBeTruthy() }) it("should display duration indicator", () => { render() expect(screen.getByText("1.0s")).toBeInTheDocument() }) }) describe("Video Error Handling", () => { it("should display error message when video fails to load", async () => { render() const sourceVideo = screen.getByTestId("source-video") fireEvent.error(sourceVideo) // Error message should appear immediately expect(screen.getByText("timeline.player.videoLoadError")).toBeInTheDocument() }) it("should not start transition when video has error", async () => { render() const sourceVideo = screen.getByTestId("source-video") fireEvent.error(sourceVideo) // Error message should appear immediately expect(screen.getByText("timeline.player.videoLoadError")).toBeInTheDocument() // Try to trigger hover on the error state container const errorContainer = screen.getByText("timeline.player.videoLoadError").closest("div")?.parentElement if (errorContainer) { fireEvent.mouseEnter(errorContainer) } expect(HTMLVideoElement.prototype.play).not.toHaveBeenCalled() }) }) describe("Hover Interactions", () => { it("should respond to mouse enter and leave events", async () => { const { container } = render() // Find the preview container with onMouseEnter/onMouseLeave handlers const previewContainer = container.querySelector('[style*="width"][style*="height"]') expect(previewContainer).toBeTruthy() // Hover functionality exists but is complex to test due to refs and timers // The component handles hover states internally fireEvent.mouseEnter(previewContainer!) fireEvent.mouseLeave(previewContainer!) // Verify the component rendered without errors expect(screen.getByTestId("source-video")).toBeInTheDocument() expect(screen.getByTestId("target-video")).toBeInTheDocument() }) }) describe("Click Interactions", () => { it("should call onClick when preview is clicked", () => { const onClick = vi.fn() const { container } = render() // Click on the actual preview container div that has onClick const previewDiv = container.querySelector(".cursor-pointer.rounded-xs") expect(previewDiv).toBeTruthy() fireEvent.click(previewDiv!) expect(onClick).toHaveBeenCalled() }) }) describe("Transition Effects", () => { it("should apply fade transition effect", async () => { const { container } = render() const sourceVideo = screen.getByTestId("source-video") const targetVideo = screen.getByTestId("target-video") // Trigger transition const previewContainer = container.querySelector(".cursor-pointer.rounded-xs") fireEvent.mouseEnter(previewContainer!) // Advance through transition await vi.advanceTimersByTimeAsync(500) // Check that videos have opacity styles applied const sourceContainer = sourceVideo.parentElement const targetContainer = targetVideo.parentElement expect(sourceContainer).toBeTruthy() expect(targetContainer).toBeTruthy() }) it("should apply zoom transition effect", async () => { const { container } = render() const sourceVideo = screen.getByTestId("source-video") const targetVideo = screen.getByTestId("target-video") // Trigger transition const previewContainer = container.querySelector(".cursor-pointer.rounded-xs") fireEvent.mouseEnter(previewContainer!) // Advance through transition await vi.advanceTimersByTimeAsync(500) // Check that videos have transform styles applied const sourceStyle = sourceVideo.style const targetStyle = targetVideo.style // Zoom effect should apply scale transform expect(sourceStyle.transform || targetStyle.transform).toBeTruthy() }) it("should apply slide transition effect", async () => { const { container } = render() const sourceVideo = screen.getByTestId("source-video") const targetVideo = screen.getByTestId("target-video") // Trigger transition const previewContainer = container.querySelector(".cursor-pointer.rounded-xs") fireEvent.mouseEnter(previewContainer!) // Advance through transition await vi.advanceTimersByTimeAsync(500) // Check that videos have transform styles applied const sourceStyle = sourceVideo.style const targetStyle = targetVideo.style // Slide effect should apply translate transform expect(sourceStyle.transform || targetStyle.transform).toBeTruthy() }) }) describe("Transition Types", () => { const transitionTypes = [ "scale", "rotate", "flip", "push", "squeeze", "diagonal", "spiral", "fold", "wave", "shutter", "bounce", "swirl", "dissolve", ] transitionTypes.forEach((type) => { it(`should render ${type} transition without errors`, () => { const { container } = render() // Verify the component renders correctly with the transition type expect(screen.getByTestId("source-video")).toBeInTheDocument() expect(screen.getByTestId("target-video")).toBeInTheDocument() // Find the preview container const previewContainer = container.querySelector('[style*="width"][style*="height"]') expect(previewContainer).toBeTruthy() // Verify hover handlers are attached fireEvent.mouseEnter(previewContainer!) fireEvent.mouseLeave(previewContainer!) }) }) }) describe("Category Indicators", () => { it("should show correct indicator for basic category", () => { render() expect(screen.getByText("BSC")).toBeInTheDocument() }) it("should show correct indicator for advanced category", () => { const advancedTransition = { ...mockTransition, category: "advanced" as const } render() expect(screen.getByText("ADV")).toBeInTheDocument() }) it("should show correct indicator for creative category", () => { const creativeTransition = { ...mockTransition, category: "creative" as const } render() expect(screen.getByText("CRE")).toBeInTheDocument() }) it("should show correct indicator for 3d category", () => { const threeDTransition = { ...mockTransition, category: "3d" as const } render() expect(screen.getByText("3D")).toBeInTheDocument() }) it("should show correct indicator for artistic category", () => { const artisticTransition = { ...mockTransition, category: "artistic" as const } render() expect(screen.getByText("ART")).toBeInTheDocument() }) it("should show correct indicator for cinematic category", () => { const cinematicTransition = { ...mockTransition, category: "cinematic" as const } render() expect(screen.getByText("CIN")).toBeInTheDocument() }) it("should show UNK for unknown category", () => { const unknownTransition = { ...mockTransition, category: "unknown" as any } render() expect(screen.getByText("UNK")).toBeInTheDocument() }) }) describe("Transition Lookup", () => { it("should find transition by type when transition prop is not provided", () => { render() // Should display the zoom transition name from the mocked transitions expect(screen.getByText("Увеличение")).toBeInTheDocument() }) it("should use transition prop over looking up by type", () => { const customTransition = { ...mockTransition, id: "custom", labels: { ru: "Пользовательский", en: "Custom" }, } render() // Should display the custom transition name, not zoom expect(screen.getByText("Пользовательский")).toBeInTheDocument() expect(screen.queryByText("Увеличение")).not.toBeInTheDocument() }) }) describe("Video Reset Behavior", () => { it("should reset video styles on mouse leave", async () => { render() const sourceVideo = screen.getByTestId("source-video") const targetVideo = screen.getByTestId("target-video") const previewContainer = screen.getByTestId("source-video").closest(".cursor-pointer") // Start transition fireEvent.mouseEnter(previewContainer!) // Wait for transition to start and advance timers to apply zoom effect await vi.advanceTimersByTimeAsync(1100) // 1000ms delay + 100ms for effect // Leave hover fireEvent.mouseLeave(previewContainer!) // Wait a bit for reset await vi.advanceTimersByTimeAsync(100) // After mouse leave, videos should be reset to initial state expect(sourceVideo.style.transform).toBe("scale(1)") expect(sourceVideo.style.opacity).toBe("1") expect(targetVideo.style.opacity).toBe("0") }) }) })