import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { MarkerControls } from "../../../components/markers/marker-controls"
import type { ExtendedTimelineMarker } from "../../../types/markers"
import { MarkerColors } from "../../../types/markers"
// Мок для иконок lucide-react
vi.mock("lucide-react", () => ({
Bookmark: ({ className, "data-testid": dataTestId }: any) => (
),
CheckSquare: ({ className, "data-testid": dataTestId }: any) => (
),
ChevronLeft: ({ className, "data-testid": dataTestId }: any) => (
),
ChevronRight: ({ className, "data-testid": dataTestId }: any) => (
),
Download: ({ className, "data-testid": dataTestId }: any) => (
),
Filter: ({ className, "data-testid": dataTestId }: any) => (
),
Folder: ({ className, "data-testid": dataTestId }: any) => (
),
PlayCircle: ({ className, "data-testid": dataTestId }: any) => (
),
Plus: ({ className, "data-testid": dataTestId }: any) => (
),
RefreshCw: ({ className, "data-testid": dataTestId }: any) => (
),
Search: ({ className, "data-testid": dataTestId }: any) => (
),
StickyNote: ({ className, "data-testid": dataTestId }: any) => (
),
X: ({ className, "data-testid": dataTestId }: any) => (
),
}))
// Моки для хуков
const mockUseTimeline = vi.fn()
const mockUseTimelineMarkers = vi.fn()
vi.mock("../../../hooks/use-timeline", () => ({
useTimeline: () => mockUseTimeline(),
}))
vi.mock("../../../hooks/use-timeline-markers", () => ({
useTimelineMarkers: () => mockUseTimelineMarkers(),
}))
// Мок для UI компонентов
vi.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, disabled, ...props }: any) => (
),
}))
vi.mock("@/components/ui/badge", () => ({
Badge: ({ children, ...props }: any) => {children},
}))
vi.mock("@/components/ui/input", () => ({
Input: ({ onChange, onKeyDown, ...props }: any) => ,
}))
vi.mock("@/components/ui/popover", () => ({
Popover: ({ children, open }: any) => (
{children}
),
PopoverTrigger: ({ children, asChild }: any) => (
{asChild ? children :
{children}
}
),
PopoverContent: ({ children }: any) => {children}
,
}))
vi.mock("@/components/ui/dropdown-menu", () => ({
DropdownMenu: ({ children }: any) => {children}
,
DropdownMenuTrigger: ({ children, asChild }: any) => (
{asChild ? children :
{children}
}
),
DropdownMenuContent: ({ children }: any) => {children}
,
DropdownMenuItem: ({ children, onClick }: any) => (
{children}
),
DropdownMenuSeparator: () =>
,
}))
describe("MarkerControls", () => {
const mockMarkers: ExtendedTimelineMarker[] = [
{
id: "1",
time: 10,
name: "Chapter 1",
type: "chapter",
color: MarkerColors.chapter,
description: "First chapter",
},
{
id: "2",
time: 20,
name: "Note 1",
type: "note",
color: MarkerColors.note,
description: "Important note",
},
{
id: "3",
time: 30,
name: "Todo 1",
type: "note",
color: MarkerColors.todo,
},
]
const defaultMocks = {
currentTime: 15,
seek: vi.fn(),
markers: mockMarkers,
addMarker: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
mockUseTimeline.mockReturnValue({
currentTime: defaultMocks.currentTime,
seek: defaultMocks.seek,
})
mockUseTimelineMarkers.mockReturnValue({
markers: defaultMocks.markers,
addMarker: defaultMocks.addMarker,
})
})
it("рендерит основные элементы управления", () => {
render()
// Используем getAllByText и проверяем первый элемент (кнопка)
const addMarkerButtons = screen.getAllByText("Add Marker")
expect(addMarkerButtons[0]).toBeInTheDocument()
// Ищем кнопку Filter по data-icon
const filterButton = screen.getByRole("button", { name: /Filter/i })
expect(filterButton).toBeInTheDocument()
expect(screen.getByText("3 / 3")).toBeInTheDocument() // счетчик маркеров
})
it("открывает попап добавления маркера при клике", async () => {
render()
const addButton = screen.getAllByText("Add Marker")[0]
fireEvent.click(addButton)
await waitFor(() => {
expect(screen.getByText("Create a marker at current time")).toBeInTheDocument()
expect(screen.getByPlaceholderText("Marker name")).toBeInTheDocument()
})
})
it("добавляет новый маркер с корректными данными", async () => {
const user = userEvent.setup()
render()
// Открываем попап
fireEvent.click(screen.getAllByText("Add Marker")[0])
// Вводим имя маркера
const input = screen.getByPlaceholderText("Marker name")
await user.type(input, "New Chapter")
// Нажимаем кнопку добавления
const submitButton = screen.getAllByText("Add Marker")[2] // Третья кнопка в попапе
fireEvent.click(submitButton)
expect(defaultMocks.addMarker).toHaveBeenCalledWith({
time: 15,
name: "New Chapter",
type: "note",
color: "#f59e0b",
})
})
it("добавляет маркер при нажатии Enter", async () => {
const user = userEvent.setup()
render()
fireEvent.click(screen.getAllByText("Add Marker")[0])
const input = screen.getByPlaceholderText("Marker name")
await user.type(input, "Quick Marker")
fireEvent.keyDown(input, { key: "Enter" })
expect(defaultMocks.addMarker).toHaveBeenCalledWith({
time: 15,
name: "Quick Marker",
type: "note",
color: "#f59e0b",
})
})
it("не добавляет маркер с пустым именем", async () => {
render()
fireEvent.click(screen.getAllByText("Add Marker")[0])
const submitButton = screen.getAllByText("Add Marker")[2]
expect(submitButton).toBeDisabled()
})
it("позволяет выбрать тип маркера", async () => {
const user = userEvent.setup()
render()
fireEvent.click(screen.getAllByText("Add Marker")[0])
// Открываем выбор типа
const typeSelector = screen.getByTestId("dropdown-trigger")
fireEvent.click(typeSelector)
// Выбираем тип "Chapter" - находим первый элемент dropdown-item с текстом Chapter
const dropdownItems = screen.getAllByTestId("dropdown-item")
const chapterOption = dropdownItems.find((item) => item.textContent?.includes("Chapter"))
fireEvent.click(chapterOption!)
// Проверяем что тип изменился
await user.type(screen.getByPlaceholderText("Marker name"), "Test Chapter")
fireEvent.click(screen.getAllByText("Add Marker")[2])
expect(defaultMocks.addMarker).toHaveBeenCalledWith({
time: 15,
name: "Test Chapter",
type: "chapter",
color: "#3b82f6",
})
})
it("навигация между маркерами работает корректно", () => {
render()
// Находим кнопки по их содержимому
const buttons = screen.getAllByRole("button")
const prevButton = buttons.find((btn) => btn.querySelector('[data-icon="ChevronLeft"]'))
const nextButton = buttons.find((btn) => btn.querySelector('[data-icon="ChevronRight"]'))
fireEvent.click(prevButton!)
expect(defaultMocks.seek).toHaveBeenCalledWith(10) // предыдущий маркер на 10с
fireEvent.click(nextButton!)
expect(defaultMocks.seek).toHaveBeenCalledWith(20) // следующий маркер на 20с
})
it("отключает навигацию когда нет маркеров", () => {
mockUseTimelineMarkers.mockReturnValue({
markers: [],
addMarker: defaultMocks.addMarker,
})
render()
const buttons = screen.getAllByRole("button")
const prevButton = buttons.find((btn) => btn.querySelector('[data-icon="ChevronLeft"]'))
const nextButton = buttons.find((btn) => btn.querySelector('[data-icon="ChevronRight"]'))
expect(prevButton).toBeDisabled()
expect(nextButton).toBeDisabled()
})
it("открывает фильтр и показывает счетчик активных фильтров", async () => {
render()
const filterButton = screen.getByRole("button", { name: /Filter/i })
fireEvent.click(filterButton)
await waitFor(() => {
expect(screen.getByText("Filter Markers")).toBeInTheDocument()
expect(screen.getByPlaceholderText("Search markers...")).toBeInTheDocument()
})
})
it("фильтрует маркеры по поиску", async () => {
const user = userEvent.setup()
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
const searchInput = screen.getByPlaceholderText("Search markers...")
await user.type(searchInput, "Chapter")
// Проверяем, что фильтрация обновила счетчик
await waitFor(() => {
expect(screen.getByText("1 / 3")).toBeInTheDocument() // 1 маркер с "Chapter" из 3
})
})
it("фильтрует маркеры по типу", async () => {
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
// Находим чекбокс для типа "Chapter"
const chapterCheckbox = screen.getAllByRole("checkbox")[0]
fireEvent.click(chapterCheckbox)
// Проверяем, что фильтрация обновила счетчик
await waitFor(() => {
expect(screen.getByText("1 / 3")).toBeInTheDocument() // 1 chapter маркер из 3
})
})
it("очищает поиск при клике на X", async () => {
const user = userEvent.setup()
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
const searchInput = screen.getByPlaceholderText("Search markers...")
await user.type(searchInput, "test")
// Находим кнопку очистки по иконке X
const buttons = screen.getAllByRole("button")
const clearButton = buttons.find((btn) => btn.querySelector('[data-icon="X"]'))
fireEvent.click(clearButton!)
// Проверяем, что поле поиска очищено
expect(searchInput).toHaveValue("")
})
it("показывает количество активных фильтров", async () => {
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
// Добавляем фильтр по типу
const chapterCheckbox = screen.getAllByRole("checkbox")[0]
fireEvent.click(chapterCheckbox)
// Проверяем что badge показывает количество фильтров
await waitFor(() => {
const badge = screen.getByText("1")
expect(badge).toBeInTheDocument()
})
})
it("очищает все фильтры при клике на Clear all", async () => {
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
// Устанавливаем фильтр
const chapterCheckbox = screen.getAllByRole("checkbox")[0]
fireEvent.click(chapterCheckbox)
// Нажимаем Clear all
const clearAllButton = screen.getByText("Clear all")
fireEvent.click(clearAllButton)
// Проверяем, что фильтры очищены (нет активных фильтров)
await waitFor(() => {
expect(screen.getByText("3 / 3")).toBeInTheDocument() // все маркеры видимы
})
})
it("обновляет счетчик маркеров при фильтрации", async () => {
const user = userEvent.setup()
render()
// Фильтруем по поиску
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
const searchInput = screen.getByPlaceholderText("Search markers...")
await user.type(searchInput, "Chapter")
// Проверяем обновленный счетчик
await waitFor(() => {
expect(screen.getByText("1 / 3")).toBeInTheDocument()
})
})
it("сохраняет состояние фильтров между открытиями попапа", async () => {
const user = userEvent.setup()
render()
// Открываем фильтр и вводим поиск
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
const searchInput = screen.getByPlaceholderText("Search markers...")
await user.type(searchInput, "test")
// Закрываем попап (симулируем изменение состояния)
fireEvent.click(document.body)
// Открываем снова
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
// Проверяем что значение сохранилось
const input = screen.getByPlaceholderText("Search markers...")
expect(input.value).toBe("test")
})
it("комбинирует фильтры по типу и поиску", async () => {
const user = userEvent.setup()
render()
fireEvent.click(screen.getByRole("button", { name: /Filter/i }))
// Выбираем тип
const chapterCheckbox = screen.getAllByRole("checkbox")[0]
fireEvent.click(chapterCheckbox)
// Вводим поиск
const searchInput = screen.getByPlaceholderText("Search markers...")
await user.type(searchInput, "test")
// Проверяем, что badge показывает 2 активных фильтра (тип + поиск)
await waitFor(() => {
const badge = screen.getByText("2")
expect(badge).toBeInTheDocument()
})
})
it("отображает правильный цвет для каждого типа маркера", () => {
render()
fireEvent.click(screen.getAllByText("Add Marker")[0])
// Открываем dropdown с типами
const typeSelector = screen.getByTestId("dropdown-trigger")
fireEvent.click(typeSelector)
// Проверяем что каждый тип имеет свой цвет
const dropdownItems = screen.getAllByTestId("dropdown-item")
expect(dropdownItems).toHaveLength(7) // Все типы маркеров
dropdownItems.forEach((item) => {
const colorIndicator = item.querySelector('[style*="background-color"]')
expect(colorIndicator).toBeInTheDocument()
})
})
})