/** * Tests for TrackHeightAdjuster component * * The closure bug has been fixed by using useRef to track dragging state. * All tests should now pass correctly. * * WHAT IS TESTED: * ✅ Component rendering and basic structure * ✅ CSS classes and styling * ✅ MouseDown event handling and state changes * ✅ Event listener setup and cleanup * ✅ Visual feedback (dragging state, indicators) * ✅ Props handling and component configuration * ✅ Mathematical logic validation (unit tests) * ✅ Component unmounting during drag * ✅ Accessibility features * ✅ MouseMove event processing * ✅ Height calculation during drag * ✅ Constraint enforcement (min/max height) * ✅ Multi-session dragging * ✅ Edge cases with extreme mouse values */ import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { TrackHeightAdjuster } from "../../components/track-height-adjuster" describe("TrackHeightAdjuster", () => { const mockOnHeightChange = vi.fn() const defaultProps = { trackId: "test-track", currentHeight: 100, onHeightChange: mockOnHeightChange, } beforeEach(() => { vi.clearAllMocks() }) afterEach(() => { // Очищаем все моки после каждого теста vi.clearAllMocks() vi.restoreAllMocks() }) describe("Component rendering", () => { it("should render with correct test id", () => { render() expect(screen.getByTestId("track-height-adjuster-test-track")).toBeInTheDocument() }) it("should render with custom className", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") expect(adjuster).toHaveClass("custom-class") }) it("should have correct base styling classes", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") expect(adjuster).toHaveClass("absolute", "bottom-0", "left-0", "right-0", "h-1") expect(adjuster).toHaveClass("cursor-row-resize", "bg-transparent") expect(adjuster).toHaveClass("hover:bg-primary/20", "active:bg-primary/30") expect(adjuster).toHaveClass("transition-colors", "duration-150", "group") }) it("should render visual indicator element", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") const indicator = adjuster.querySelector(".bg-primary.opacity-0") expect(indicator).toBeInTheDocument() }) it("should render expanded capture area with title", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") const captureArea = adjuster.querySelector("[title='Перетащите для изменения высоты трека']") expect(captureArea).toBeInTheDocument() expect(captureArea).toHaveClass("absolute", "-top-1", "-bottom-1", "left-0", "right-0") }) }) describe("Mouse interaction basics", () => { it("should handle mouseDown event", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Создаем мок событие с clientY const mouseDownEvent = new MouseEvent("mousedown", { bubbles: true, clientY: 100, }) // Мокаем preventDefault и stopPropagation const preventDefaultSpy = vi.spyOn(mouseDownEvent, "preventDefault") const stopPropagationSpy = vi.spyOn(mouseDownEvent, "stopPropagation") fireEvent(adjuster, mouseDownEvent) expect(preventDefaultSpy).toHaveBeenCalled() expect(stopPropagationSpy).toHaveBeenCalled() }) it("should add dragging class when mouseDown", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 100 }) expect(adjuster).toHaveClass("bg-primary/30") }) it("should set up document event listeners on mouseDown", () => { const addEventListenerSpy = vi.spyOn(document, "addEventListener") render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 100 }) expect(addEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function)) expect(addEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function)) }) }) describe("Height calculation and constraints", () => { it("should calculate new height correctly with positive delta", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Имитируем движение мыши вниз на 50px fireEvent( document, new MouseEvent("mousemove", { clientY: 150, }), ) // Ожидаем, что высота увеличится: 100 + 50 = 150 expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 150) }) it("should calculate new height correctly with negative delta", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Имитируем движение мыши вверх на 30px fireEvent( document, new MouseEvent("mousemove", { clientY: 70, }), ) // Ожидаем, что высота уменьшится: 100 - 30 = 70 expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 70) }) it("should enforce minimum height constraint (40px)", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Имитируем движение мыши вверх на 50px (50 - 50 = 0, должно быть ограничено до 40) fireEvent( document, new MouseEvent("mousemove", { clientY: 50, }), ) // Ожидаем минимальную высоту 40px expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 40) }) it("should enforce maximum height constraint (300px)", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Имитируем движение мыши вниз на 100px (250 + 100 = 350, должно быть ограничено до 300) fireEvent( document, new MouseEvent("mousemove", { clientY: 200, }), ) // Ожидаем максимальную высоту 300px expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 300) }) it("should allow height exactly at minimum boundary", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Без движения высота должна остаться 40 fireEvent( document, new MouseEvent("mousemove", { clientY: 100, }), ) expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 40) }) it("should allow height exactly at maximum boundary", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Без движения высота должна остаться 300 fireEvent( document, new MouseEvent("mousemove", { clientY: 100, }), ) expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 300) }) // Test the mathematical logic separately from the event handling it("should have correct height calculation logic (unit test)", () => { // Test the math that would be applied in handleMouseMove const startHeight = 100 const startY = 100 // Positive delta case const clientY1 = 150 const deltaY1 = clientY1 - startY const newHeight1 = Math.max(40, Math.min(300, startHeight + deltaY1)) expect(newHeight1).toBe(150) // Negative delta case const clientY2 = 70 const deltaY2 = clientY2 - startY const newHeight2 = Math.max(40, Math.min(300, startHeight + deltaY2)) expect(newHeight2).toBe(70) // Minimum constraint const clientY3 = 0 const deltaY3 = clientY3 - startY const newHeight3 = Math.max(40, Math.min(300, startHeight + deltaY3)) expect(newHeight3).toBe(40) // Maximum constraint const startHeight4 = 250 const clientY4 = 350 const deltaY4 = clientY4 - startY const newHeight4 = Math.max(40, Math.min(300, startHeight4 + deltaY4)) expect(newHeight4).toBe(300) }) }) describe("Drag lifecycle and cleanup", () => { it("should cleanup event listeners on mouseUp", async () => { const removeEventListenerSpy = vi.spyOn(document, "removeEventListener") render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) // Завершаем перетаскивание fireEvent(document, new MouseEvent("mouseup")) await waitFor(() => { expect(removeEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function)) expect(removeEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function)) }) }) it("should remove dragging class on mouseUp", async () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Начинаем перетаскивание fireEvent.mouseDown(adjuster, { clientY: 100 }) expect(adjuster).toHaveClass("bg-primary/30") // Завершаем перетаскивание fireEvent(document, new MouseEvent("mouseup")) await waitFor(() => { expect(adjuster).not.toHaveClass("bg-primary/30") }) }) it("should not call onHeightChange during mousemove if not dragging", () => { render() // Имитируем движение мыши без предварительного mouseDown fireEvent( document, new MouseEvent("mousemove", { clientY: 150, }), ) expect(mockOnHeightChange).not.toHaveBeenCalled() }) it("should handle multiple drag sessions correctly", async () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Первая сессия перетаскивания fireEvent.mouseDown(adjuster, { clientY: 100 }) fireEvent(document, new MouseEvent("mousemove", { clientY: 150 })) fireEvent(document, new MouseEvent("mouseup")) await waitFor(() => { expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 150) }) vi.clearAllMocks() // Вторая сессия перетаскивания fireEvent.mouseDown(adjuster, { clientY: 200 }) fireEvent(document, new MouseEvent("mousemove", { clientY: 180 })) fireEvent(document, new MouseEvent("mouseup")) await waitFor(() => { expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 80) }) }) }) describe("Visual states and styling", () => { it("should show visual indicator with correct opacity classes when dragging", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") const indicator = adjuster.querySelector(".bg-primary") // Изначально индикатор скрыт expect(indicator).toHaveClass("opacity-0") // После начала перетаскивания индикатор должен стать видимым fireEvent.mouseDown(adjuster, { clientY: 100 }) expect(indicator).toHaveClass("opacity-100") }) it("should have group hover effects in CSS classes", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") const indicator = adjuster.querySelector(".bg-primary") expect(indicator).toHaveClass("group-hover:opacity-100") expect(indicator).toHaveClass("transition-opacity", "duration-150") }) }) describe("Props and configuration", () => { it("should work with different trackId", () => { render() expect(screen.getByTestId("track-height-adjuster-different-track")).toBeInTheDocument() // Test that the component renders with correct ID const adjuster = screen.getByTestId("track-height-adjuster-different-track") expect(adjuster).toBeInTheDocument() }) it("should accept different initial heights as props", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") expect(adjuster).toBeInTheDocument() // Test that mouseDown works with different initial height fireEvent.mouseDown(adjuster, { clientY: 100 }) expect(adjuster).toHaveClass("bg-primary/30") // Should be in dragging state }) it("should pass correct trackId in component structure", () => { const customTrackId = "custom-track-123" render() const adjuster = screen.getByTestId(`track-height-adjuster-${customTrackId}`) expect(adjuster).toBeInTheDocument() expect(adjuster).toHaveAttribute("data-testid", `track-height-adjuster-${customTrackId}`) }) }) describe("Edge cases and error handling", () => { beforeEach(() => { vi.clearAllMocks() }) it("should handle rapid mouse movements correctly", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 100 }) // Множественные быстрые движения fireEvent(document, new MouseEvent("mousemove", { clientY: 110 })) fireEvent(document, new MouseEvent("mousemove", { clientY: 120 })) fireEvent(document, new MouseEvent("mousemove", { clientY: 130 })) fireEvent(document, new MouseEvent("mousemove", { clientY: 140 })) // Проверяем, что обработчик вызывался и последний вызов правильный expect(mockOnHeightChange).toHaveBeenCalled() expect(mockOnHeightChange).toHaveBeenLastCalledWith("test-track", 140) }) it("should handle extreme mouse position values", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 100 }) // Экстремально большое значение fireEvent(document, new MouseEvent("mousemove", { clientY: 10000 })) expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 300) // Максимум // Экстремально маленькое значение fireEvent(document, new MouseEvent("mousemove", { clientY: -10000 })) expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 40) // Минимум }) it("should handle zero clientY values", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 0 }) fireEvent(document, new MouseEvent("mousemove", { clientY: 0 })) expect(mockOnHeightChange).toHaveBeenCalledWith("test-track", 100) }) it("should handle component unmount during drag", async () => { const { unmount } = render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") fireEvent.mouseDown(adjuster, { clientY: 100 }) // Размонтируем компонент во время перетаскивания unmount() // Движение мыши после размонтирования не должно вызывать ошибок expect(() => { fireEvent(document, new MouseEvent("mousemove", { clientY: 150 })) }).not.toThrow() }) it("should validate boundary constraints in isolation", () => { // Test edge cases in the Math.max/Math.min logic const testCases = [ { input: -100, expected: 40 }, // Below minimum { input: 40, expected: 40 }, // At minimum { input: 100, expected: 100 }, // Normal value { input: 300, expected: 300 }, // At maximum { input: 500, expected: 300 }, // Above maximum { input: 0, expected: 40 }, // Zero value ] testCases.forEach(({ input, expected }) => { const result = Math.max(40, Math.min(300, input)) expect(result).toBe(expected) }) }) }) describe("Accessibility and UX", () => { it("should have correct cursor style", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") expect(adjuster).toHaveClass("cursor-row-resize") }) it("should have descriptive title on capture area", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") const captureArea = adjuster.querySelector("[title]") expect(captureArea).toHaveAttribute("title", "Перетащите для изменения высоты трека") }) it("should provide visual feedback during interaction", () => { render() const adjuster = screen.getByTestId("track-height-adjuster-test-track") // Проверяем наличие hover/active состояний в классах expect(adjuster).toHaveClass("hover:bg-primary/20") expect(adjuster).toHaveClass("active:bg-primary/30") expect(adjuster).toHaveClass("transition-colors") }) }) })