Skip to content

Commit 8dceb18

Browse files
authored
Merge pull request #220 from ITBooster-practice/feature/test-widgets
feat(web): добавлены тесты для widgets
2 parents 45d755c + ee2caaf commit 8dceb18

3 files changed

Lines changed: 373 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { cleanup, render, screen } from '@testing-library/react'
2+
import { afterEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { TaskPriorityBadge } from '@/entities/task/ui/task-priority-badge'
5+
import { TaskStatusBadge } from '@/entities/task/ui/task-status-badge'
6+
import { TaskTypeBadge } from '@/entities/task/ui/task-type-badge'
7+
8+
// ─── Мок: @repo/ui ───────────────────────────────────────────────
9+
// Tooltip рендерит контент сразу (без hover), чтобы тест мог проверить label.
10+
vi.mock('@repo/ui', () => ({
11+
Badge: ({
12+
children,
13+
className,
14+
}: React.PropsWithChildren<{ variant?: string; className?: string }>) => (
15+
<span className={className}>{children}</span>
16+
),
17+
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
18+
Tooltip: ({ children }: React.PropsWithChildren) => <>{children}</>,
19+
TooltipContent: ({ children }: React.PropsWithChildren) => (
20+
<div data-testid='tooltip-content'>{children}</div>
21+
),
22+
TooltipProvider: ({ children }: React.PropsWithChildren) => <>{children}</>,
23+
TooltipTrigger: ({
24+
children,
25+
}: React.PropsWithChildren<{ asChild?: boolean; className?: string }>) => (
26+
<div data-testid='tooltip-trigger'>{children}</div>
27+
),
28+
}))
29+
30+
vi.mock('@repo/ui/icons', () => ({
31+
AlertTriangle: () => <span data-testid='icon-alert' />,
32+
ArrowDown: () => <span data-testid='icon-arrow-down' />,
33+
ArrowUp: () => <span data-testid='icon-arrow-up' />,
34+
Minus: () => <span data-testid='icon-minus' />,
35+
}))
36+
37+
afterEach(cleanup)
38+
39+
// ─── TaskStatusBadge ─────────────────────────────────────────────
40+
describe('TaskStatusBadge', () => {
41+
it.each([
42+
['TODO', 'К выполнению'],
43+
['IN_PROGRESS', 'В работе'],
44+
['IN_REVIEW', 'Ревью'],
45+
['DONE', 'Готово'],
46+
] as const)('статус %s → отображает "%s"', (status, label) => {
47+
render(<TaskStatusBadge status={status} />)
48+
49+
expect(screen.getByText(label)).toBeDefined()
50+
51+
cleanup()
52+
})
53+
})
54+
55+
// ─── TaskTypeBadge ───────────────────────────────────────────────
56+
describe('TaskTypeBadge', () => {
57+
it.each(['Эпик', 'Стори', 'Баг', 'Тех. долг', 'Задача'] as const)(
58+
'тип "%s" → рендерит текст типа',
59+
(type) => {
60+
render(<TaskTypeBadge type={type} />)
61+
62+
expect(screen.getByText(type)).toBeDefined()
63+
64+
cleanup()
65+
},
66+
)
67+
})
68+
69+
// ─── TaskPriorityBadge ───────────────────────────────────────────
70+
describe('TaskPriorityBadge', () => {
71+
it.each([
72+
['LOW', 'icon-arrow-down', 'Низкий'],
73+
['MEDIUM', 'icon-minus', 'Средний'],
74+
['HIGH', 'icon-arrow-up', 'Высокий'],
75+
['CRITICAL', 'icon-alert', 'Критичный'],
76+
] as const)('приоритет %s → иконка %s и тултип "%s"', (priority, iconTestId, label) => {
77+
render(<TaskPriorityBadge priority={priority} />)
78+
79+
expect(screen.getByTestId(iconTestId)).toBeDefined()
80+
expect(screen.getByTestId('tooltip-content')).toBeDefined()
81+
expect(screen.getByText(label)).toBeDefined()
82+
83+
cleanup()
84+
})
85+
})
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { cleanup, render, screen } from '@testing-library/react'
2+
import { afterEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { Header } from '@/widgets/main-layout/ui/header/header'
5+
import { Layout } from '@/widgets/main-layout/ui/layout'
6+
import { MainLayout } from '@/widgets/main-layout/ui/main-layout'
7+
8+
// ─── Мок: sub-компоненты Header ──────────────────────────────────
9+
vi.mock('@/widgets/main-layout/ui/header/mobile-sidebar-trigger', () => ({
10+
MobileSidebarTrigger: () => <div data-testid='mobile-trigger' />,
11+
}))
12+
13+
vi.mock('@/widgets/main-layout/ui/header/profile-menu', () => ({
14+
ProfileMenu: () => <div data-testid='profile-menu' />,
15+
}))
16+
17+
vi.mock('@/widgets/main-layout/ui/header/sidebar-toggle', () => ({
18+
SidebarToggle: () => <div data-testid='sidebar-toggle' />,
19+
}))
20+
21+
// ─── Мок: sub-компоненты MainLayout ──────────────────────────────
22+
vi.mock('@/widgets/main-layout/ui/sidebar', () => ({
23+
Sidebar: () => <div data-testid='sidebar' />,
24+
}))
25+
26+
vi.mock('@/widgets/main-layout/ui/header', () => ({
27+
Header: () => <div data-testid='header' />,
28+
}))
29+
30+
vi.mock('@/widgets/main-layout/ui/layout', () => ({
31+
Layout: ({
32+
sidebar,
33+
header,
34+
children,
35+
}: {
36+
sidebar: React.ReactNode
37+
header: React.ReactNode
38+
children: React.ReactNode
39+
}) => (
40+
<div>
41+
<div data-testid='slot-sidebar'>{sidebar}</div>
42+
<div data-testid='slot-header'>{header}</div>
43+
<div data-testid='slot-children'>{children}</div>
44+
</div>
45+
),
46+
}))
47+
48+
// ─── Мок: @repo/ui ───────────────────────────────────────────────
49+
vi.mock('@repo/ui', () => ({
50+
Button: ({
51+
children,
52+
...props
53+
}: React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>) => (
54+
<button {...props}>{children}</button>
55+
),
56+
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
57+
}))
58+
59+
vi.mock('@repo/ui/icons', () => ({
60+
Bell: () => <span data-testid='icon-bell' />,
61+
}))
62+
63+
afterEach(cleanup)
64+
65+
// ─── Layout ──────────────────────────────────────────────────────
66+
describe('Layout', () => {
67+
it('рендерит sidebar, header и children слоты', () => {
68+
const { unmount } = render(
69+
<Layout
70+
sidebar={<div data-testid='test-sidebar' />}
71+
header={<div data-testid='test-header' />}
72+
>
73+
<div data-testid='test-content'>Контент</div>
74+
</Layout>,
75+
)
76+
77+
expect(screen.getByTestId('test-sidebar')).toBeDefined()
78+
expect(screen.getByTestId('test-header')).toBeDefined()
79+
expect(screen.getByTestId('test-content')).toBeDefined()
80+
81+
unmount()
82+
})
83+
})
84+
85+
// ─── Header ──────────────────────────────────────────────────────
86+
describe('Header', () => {
87+
it('рендерит поле поиска', () => {
88+
render(<Header />)
89+
90+
expect(screen.getByPlaceholderText('Поиск (пока не реализовано)')).toBeDefined()
91+
})
92+
93+
it('рендерит кнопку уведомлений', () => {
94+
render(<Header />)
95+
96+
expect(screen.getByRole('button', { name: 'Уведомления (заглушка)' })).toBeDefined()
97+
})
98+
99+
it('рендерит вложенные компоненты навигации', () => {
100+
render(<Header />)
101+
102+
expect(screen.getByTestId('mobile-trigger')).toBeDefined()
103+
expect(screen.getByTestId('profile-menu')).toBeDefined()
104+
})
105+
})
106+
107+
// ─── MainLayout ──────────────────────────────────────────────────
108+
describe('MainLayout', () => {
109+
it('рендерит children в нужном слоте', () => {
110+
render(
111+
<MainLayout>
112+
<div data-testid='page-content'>Страница</div>
113+
</MainLayout>,
114+
)
115+
116+
expect(screen.getByTestId('slot-children')).toBeDefined()
117+
expect(screen.getByTestId('page-content')).toBeDefined()
118+
})
119+
})
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { cleanup, render, screen } from '@testing-library/react'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { useSideBarStore } from '@/widgets/main-layout/model/sidebar'
5+
import { Sidebar } from '@/widgets/main-layout/ui/sidebar'
6+
7+
// ─── Мок: next/link ──────────────────────────────────────────────
8+
vi.mock('next/link', () => ({
9+
default: ({
10+
href,
11+
children,
12+
className,
13+
onClick,
14+
}: React.PropsWithChildren<{
15+
href: string
16+
className?: string
17+
onClick?: () => void
18+
}>) => (
19+
<a href={href} className={className} onClick={onClick}>
20+
{children}
21+
</a>
22+
),
23+
}))
24+
25+
// ─── Мок: usePathname, useParams ─────────────────────────────────
26+
const mockUsePathname = vi.fn()
27+
const mockUseParams = vi.fn()
28+
29+
vi.mock('next/navigation', () => ({
30+
usePathname: () => mockUsePathname(),
31+
useParams: () => mockUseParams(),
32+
}))
33+
34+
// ─── Мок: ThemeToggle ────────────────────────────────────────────
35+
vi.mock('@/features/theme', () => ({
36+
ThemeToggle: () => <button data-testid='theme-toggle' />,
37+
}))
38+
39+
// ─── Мок: useTeamsList ───────────────────────────────────────────
40+
const mockUseTeamsList = vi.fn()
41+
42+
vi.mock('@/shared/api/use-teams', () => ({
43+
useTeamsList: () => mockUseTeamsList(),
44+
}))
45+
46+
// ─── Мок: shared/config ──────────────────────────────────────────
47+
vi.mock('@/shared/config', () => ({
48+
getSidebarRouteId: () => null,
49+
ROUTES: { home: '/' },
50+
}))
51+
52+
// ─── Мок: shared/lib/projects ────────────────────────────────────
53+
vi.mock('@/shared/lib/projects', () => ({
54+
buildTeamProjectHref: (teamId: string, projectId: string) =>
55+
`/teams/${teamId}/projects/${projectId}`,
56+
projectCatalog: [],
57+
}))
58+
59+
// ─── Мок: model/sidebar ──────────────────────────────────────────
60+
vi.mock('@/widgets/main-layout/model/sidebar', async (importOriginal) => {
61+
const original =
62+
await importOriginal<typeof import('@/widgets/main-layout/model/sidebar')>()
63+
64+
return {
65+
...original,
66+
getSidebarSections: () => [
67+
{
68+
title: 'Работа',
69+
items: [
70+
{
71+
title: 'Проекты',
72+
href: '/teams/team-1/projects',
73+
routeId: 'team.projects',
74+
icon: () => <span />,
75+
},
76+
],
77+
},
78+
],
79+
sidebarWorkspace: { title: 'Tracker Task', subtitle: 'Product Team' },
80+
sidebarCurrentUser: { initials: 'AI', name: 'Алексей Иванов', role: 'Owner' },
81+
sidebarProjects: [],
82+
}
83+
})
84+
85+
// ─── Мок: SidebarMenuItem / SidebarProjectItem ───────────────────
86+
vi.mock('@/widgets/main-layout/ui/sidebar/sidebar-menu-item', () => ({
87+
SidebarMenuItem: ({
88+
title,
89+
isActive,
90+
}: {
91+
title: string
92+
isActive: boolean
93+
isOpen: boolean
94+
href: string
95+
onNavigate?: () => void
96+
}) => (
97+
<div data-testid='menu-item' data-active={String(isActive)}>
98+
{title}
99+
</div>
100+
),
101+
}))
102+
103+
vi.mock('@/widgets/main-layout/ui/sidebar/sidebar-project-item', () => ({
104+
SidebarProjectItem: ({
105+
title,
106+
}: {
107+
title: string
108+
href: string
109+
isActive: boolean
110+
isOpen: boolean
111+
}) => <div data-testid='project-item'>{title}</div>,
112+
}))
113+
114+
// ─── Мок: UI-компоненты ──────────────────────────────────────────
115+
vi.mock('@repo/ui', () => ({
116+
Avatar: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
117+
AvatarFallback: ({ children }: React.PropsWithChildren) => <span>{children}</span>,
118+
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
119+
}))
120+
121+
vi.mock('@repo/ui/icons', () => ({
122+
KanbanSquare: () => <span data-testid='kanban-icon' />,
123+
}))
124+
125+
describe('Sidebar', () => {
126+
beforeEach(() => {
127+
vi.clearAllMocks()
128+
mockUsePathname.mockReturnValue('/')
129+
mockUseParams.mockReturnValue({})
130+
mockUseTeamsList.mockReturnValue({ data: [] })
131+
useSideBarStore.setState({ isOpen: true })
132+
})
133+
134+
afterEach(cleanup)
135+
136+
it('развёрнутый (forceOpen=true) — показывает заголовок workspace', () => {
137+
render(<Sidebar forceOpen={true} />)
138+
139+
expect(screen.getByText('Tracker Task')).toBeDefined()
140+
})
141+
142+
it('свёрнутый (forceOpen=false) — скрывает заголовок workspace', () => {
143+
render(<Sidebar forceOpen={false} />)
144+
145+
expect(screen.queryByText('Tracker Task')).toBeNull()
146+
})
147+
148+
it('показывает имя текущей команды под заголовком когда sidebar открыт', () => {
149+
mockUseParams.mockReturnValue({ id: 'team-1' })
150+
mockUseTeamsList.mockReturnValue({
151+
data: [
152+
{
153+
id: 'team-1',
154+
name: 'Design Team',
155+
description: null,
156+
avatarUrl: null,
157+
membersCount: 5,
158+
currentUserRole: 'MEMBER',
159+
createdAt: '2024-01-01',
160+
updatedAt: '2024-01-01',
161+
},
162+
],
163+
})
164+
165+
render(<Sidebar forceOpen={true} />)
166+
167+
expect(screen.getByText('Design Team')).toBeDefined()
168+
})
169+
})

0 commit comments

Comments
 (0)