/**
* Тесты для PersonsPanel компонента
*/
import { fireEvent, render, screen } from "@testing-library/react"
import { describe, expect, it, vi } from "vitest"
// Mock для хука useTimelinePersons
vi.mock("@/features/timeline/hooks/use-timeline-persons", () => ({
useTimelinePersons: vi.fn(),
}))
import type { PersonProfile } from "@/features/person-identification/types/person"
import type { TimelinePersonAppearance } from "@/features/timeline/hooks/use-timeline-persons"
import { useTimelinePersons } from "@/features/timeline/hooks/use-timeline-persons"
import { PersonsPanel } from "../persons-panel"
// Mock для UI компонентов
vi.mock("@/components/ui/badge", () => ({
Badge: ({ children, onClick, className, variant }: any) => (
{children}
),
}))
vi.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, disabled, className, variant, size }: any) => (
),
}))
vi.mock("@/components/ui/card", () => ({
Card: ({ children, className }: any) => (
{children}
),
CardContent: ({ children, className }: any) => (
{children}
),
CardHeader: ({ children, className }: any) => (
{children}
),
CardTitle: ({ children, className }: any) => (
{children}
),
}))
vi.mock("@/components/ui/input", () => ({
Input: ({ placeholder, value, onChange, className }: any) => (
),
}))
vi.mock("@/components/ui/scroll-area", () => ({
ScrollArea: ({ children, className }: any) => (
{children}
),
}))
vi.mock("@/components/ui/slider", () => ({
Slider: ({ value, onValueChange, min, max, step, className }: any) => (
onValueChange([Number.parseFloat(e.target.value)])}
min={min}
max={max}
step={step}
className={className}
data-testid="slider"
/>
),
}))
vi.mock("@/components/ui/switch", () => ({
Switch: ({ checked, onCheckedChange }: any) => (
onCheckedChange(e.target.checked)} data-testid="switch" />
),
}))
vi.mock("@/components/ui/tooltip", () => ({
Tooltip: ({ children }: any) => {children}
,
TooltipTrigger: ({ children }: any) => {children}
,
TooltipContent: ({ children }: any) => {children}
,
}))
vi.mock("lucide-react", () => ({
Eye: ({ className }: any) => (
Eye
),
EyeOff: ({ className }: any) => (
EyeOff
),
Filter: ({ className }: any) => (
Filter
),
Search: ({ className }: any) => (
Search
),
Settings: ({ className }: any) => (
Settings
),
Users: ({ className }: any) => (
Users
),
}))
// Получаем замоканную версию хука
describe("PersonsPanel", () => {
const mockPersons: PersonProfile[] = [
{
id: "person-1",
name: "Иван Петров",
isVerified: true,
faceEmbeddings: [],
appearances: [],
totalScreenTime: 120,
firstSeen: { seconds: 0 },
lastSeen: { seconds: 120 },
tags: ["актер", "главный"],
thumbnails: [
{
id: "thumb-1",
imageUrl: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAg",
width: 64,
height: 64,
sourceClipId: "clip-1",
sourceTimestamp: { seconds: 10 },
quality: 0.9,
isPrimary: true,
isGenerated: false,
},
],
privacy: {
blurFace: false,
hideFromSearch: false,
anonymize: false,
blurIntensity: 5,
blurTracking: true,
},
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
{
id: "person-2",
name: "Анна Сидорова",
isVerified: false,
faceEmbeddings: [],
appearances: [],
totalScreenTime: 60,
firstSeen: { seconds: 30 },
lastSeen: { seconds: 90 },
tags: ["актриса", "второстепенный"],
thumbnails: [],
privacy: {
blurFace: false,
hideFromSearch: false,
anonymize: false,
blurIntensity: 5,
blurTracking: true,
},
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
{
id: "person-3",
name: "",
isVerified: false,
faceEmbeddings: [],
appearances: [],
totalScreenTime: 30,
firstSeen: { seconds: 60 },
lastSeen: { seconds: 90 },
tags: [],
thumbnails: [],
privacy: {
blurFace: false,
hideFromSearch: false,
anonymize: false,
blurIntensity: 5,
blurTracking: true,
},
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
]
const mockAppearances: TimelinePersonAppearance[] = [
{
id: "app-1",
personId: "person-1",
clipId: "clip-1",
startTime: 10,
endTime: 20,
confidence: 0.95,
detectedAt: new Date("2024-01-01T00:00:00Z"),
},
{
id: "app-2",
personId: "person-2",
clipId: "clip-1",
startTime: 15,
endTime: 25,
confidence: 0.75,
detectedAt: new Date("2024-01-01T00:00:00Z"),
},
{
id: "app-3",
personId: "person-1",
clipId: "clip-2",
startTime: 5,
endTime: 15,
confidence: 0.85,
detectedAt: new Date("2024-01-01T00:00:00Z"),
},
]
const mockHookReturn = {
persons: mockPersons,
state: {
isAnalyzing: false,
analysisProgress: 0,
appearances: mockAppearances,
error: null,
},
analyzeTimelineForPersons: vi.fn(),
clearPersonsAnalysis: vi.fn(),
showPersonDetail: vi.fn(),
enablePersonDetection: true,
setEnablePersonDetection: vi.fn(),
confidenceThreshold: 0.7,
setConfidenceThreshold: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useTimelinePersons).mockReturnValue(mockHookReturn)
})
describe("Базовое отображение", () => {
it("отображает компонент", () => {
render()
expect(screen.getByTestId("card")).toBeInTheDocument()
expect(screen.getByTestId("card-header")).toBeInTheDocument()
expect(screen.getByTestId("card-content")).toBeInTheDocument()
})
it("отображает заголовок с количеством персон", () => {
render()
expect(screen.getByTestId("card-title")).toHaveTextContent("Персоны (3)")
// Один в заголовке + 2 для персон без аватара
expect(screen.getAllByTestId("users-icon")).toHaveLength(3)
})
it("отображает кнопки управления", () => {
render()
const buttons = screen.getAllByTestId("button")
expect(buttons).toHaveLength(3) // Settings, Analyze, Clear
expect(screen.getByTestId("settings-icon")).toBeInTheDocument()
expect(screen.getByTestId("eye-icon")).toBeInTheDocument()
expect(screen.getByTestId("eye-off-icon")).toBeInTheDocument()
})
it("отображает поле поиска", () => {
render()
const searchInput = screen.getByTestId("input")
expect(searchInput).toHaveAttribute("placeholder", "Поиск персон...")
expect(screen.getByTestId("search-icon")).toBeInTheDocument()
})
it("отображает список персон", () => {
render()
expect(screen.getByText("Иван Петров")).toBeInTheDocument()
expect(screen.getByText("Анна Сидорова")).toBeInTheDocument()
expect(screen.getByText("Безымянная персона")).toBeInTheDocument()
})
it("отображает статистику появлений", () => {
render()
expect(screen.getByText("Появлений: 3")).toBeInTheDocument()
expect(screen.getByText("Средняя уверенность: 85%")).toBeInTheDocument()
})
})
describe("Действия кнопок", () => {
it("вызывает анализ Timeline при клике на кнопку анализа", () => {
render()
const analyzeButton = screen.getAllByTestId("button")[1]
fireEvent.click(analyzeButton)
expect(mockHookReturn.analyzeTimelineForPersons).toHaveBeenCalled()
})
it("очищает анализ при клике на кнопку очистки", () => {
render()
const clearButton = screen.getAllByTestId("button")[2]
fireEvent.click(clearButton)
expect(mockHookReturn.clearPersonsAnalysis).toHaveBeenCalled()
})
it("показывает детали персоны при клике на персону", () => {
render()
const personElement = screen.getByText("Иван Петров").closest("div")
fireEvent.click(personElement!)
expect(mockHookReturn.showPersonDetail).toHaveBeenCalledWith("person-1")
})
})
describe("Настройки", () => {
it("показывает настройки при клике на кнопку настроек", () => {
render()
const settingsButton = screen.getAllByTestId("button")[0]
fireEvent.click(settingsButton)
expect(screen.getByTestId("switch")).toBeInTheDocument()
expect(screen.getByTestId("slider")).toBeInTheDocument()
expect(screen.getByText("Автообнаружение")).toBeInTheDocument()
expect(screen.getByText("Уверенность")).toBeInTheDocument()
})
it("переключает автообнаружение", () => {
render()
const settingsButton = screen.getAllByTestId("button")[0]
fireEvent.click(settingsButton)
const switchElement = screen.getByTestId("switch")
expect(switchElement).toBeChecked() // Проверяем, что переключатель включен по умолчанию
// Просто проверяем, что переключатель реагирует на изменения
fireEvent.change(switchElement, { target: { checked: false } })
expect(switchElement).not.toBeChecked()
})
it("изменяет порог уверенности", () => {
render()
const settingsButton = screen.getAllByTestId("button")[0]
fireEvent.click(settingsButton)
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "0.8" } })
expect(mockHookReturn.setConfidenceThreshold).toHaveBeenCalledWith(0.8)
})
})
describe("Поиск персон", () => {
it("фильтрует персон по имени", () => {
render()
const searchInput = screen.getByTestId("input")
fireEvent.change(searchInput, { target: { value: "Иван" } })
expect(screen.getByText("Иван Петров")).toBeInTheDocument()
expect(screen.queryByText("Анна Сидорова")).not.toBeInTheDocument()
})
it("показывает сообщение когда персоны не найдены", () => {
render()
const searchInput = screen.getByTestId("input")
fireEvent.change(searchInput, {
target: { value: "Несуществующая персона" },
})
expect(screen.getByText("Персоны не найдены по заданным критериям.")).toBeInTheDocument()
})
})
describe("Фильтр по тегам", () => {
it("отображает доступные теги", () => {
render()
expect(screen.getAllByText("актер")).toHaveLength(2) // В фильтре и в персоне
expect(screen.getAllByText("главный")).toHaveLength(2) // В фильтре и в персоне
expect(screen.getAllByText("актриса")).toHaveLength(2) // В фильтре и в персоне
expect(screen.getAllByText("второстепенный")).toHaveLength(2) // В фильтре и в персоне
})
it("фильтрует персон по выбранным тегам", () => {
render()
const actorTags = screen.getAllByText("актер")
const filterTag = actorTags[0] // Первый тег - в фильтре
fireEvent.click(filterTag)
expect(screen.getByText("Иван Петров")).toBeInTheDocument()
expect(screen.queryByText("Анна Сидорова")).not.toBeInTheDocument()
})
})
describe("Отображение персон", () => {
it("отображает аватар персоны если есть thumbnail", () => {
render()
const avatar = screen.getByAltText("Иван Петров")
expect(avatar).toHaveAttribute("src", "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAg")
})
it("отображает статистику появлений для каждой персоны", () => {
render()
expect(screen.getByText("2 появлений")).toBeInTheDocument() // person-1
expect(screen.getByText("1 появлений")).toBeInTheDocument() // person-2
expect(screen.getByText("0 появлений")).toBeInTheDocument() // person-3
})
it("отображает теги персон", () => {
render()
const tagBadges = screen.getAllByTestId("badge")
// Теги отображаются как в фильтре по тегам, так и в каждой персоне
expect(tagBadges.length).toBeGreaterThan(0)
// Проверим, что есть нужные теги
expect(screen.getAllByText("актер")).toHaveLength(2)
expect(screen.getAllByText("главный")).toHaveLength(2)
expect(screen.getAllByText("актриса")).toHaveLength(2)
expect(screen.getAllByText("второстепенный")).toHaveLength(2)
})
})
describe("Состояния", () => {
it("отображает прогресс анализа", () => {
mockHookReturn.state.isAnalyzing = true
mockHookReturn.state.analysisProgress = 45
vi.mocked(useTimelinePersons).mockReturnValue(mockHookReturn)
render()
expect(screen.getByText("Анализ...")).toBeInTheDocument()
expect(screen.getByText("45%")).toBeInTheDocument()
})
it("отображает ошибку если есть", () => {
mockHookReturn.state.error = "Ошибка анализа"
vi.mocked(useTimelinePersons).mockReturnValue(mockHookReturn)
render()
expect(screen.getByText("Ошибка анализа")).toBeInTheDocument()
})
it("показывает сообщение когда нет персон", () => {
mockHookReturn.persons = []
mockHookReturn.state.appearances = []
vi.mocked(useTimelinePersons).mockReturnValue(mockHookReturn)
render()
expect(screen.getByText("Персоны не обнаружены. Запустите анализ Timeline.")).toBeInTheDocument()
})
})
describe("Пропсы компонента", () => {
it("применяет переданный className", () => {
render()
expect(screen.getByTestId("card")).toHaveClass("custom-class")
})
})
})