Skip to content

Commit cf9ae49

Browse files
authored
Merge pull request #218 from ITBooster-practice/feature/team-settings-page-view
test(web): добавлены тесты
2 parents f991b0a + 916f951 commit cf9ae49

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { TEAM_ROLES, type Team, type TeamListItem, type TeamMember } from '@repo/types'
2+
3+
export const createTeamMemberFixture = (overrides?: Partial<TeamMember>): TeamMember => ({
4+
id: 'user-1',
5+
userId: 'u-1',
6+
name: 'Alice Member',
7+
email: 'alice@example.com',
8+
role: TEAM_ROLES.MEMBER,
9+
joinedAt: '2024-01-01',
10+
...overrides,
11+
})
12+
13+
export const createTeamFixture = (overrides?: Partial<Team>): Team => ({
14+
id: 'team-1',
15+
name: 'Alpha Team',
16+
description: null,
17+
avatarUrl: null,
18+
members: [],
19+
createdAt: '2024-01-01',
20+
updatedAt: '2024-01-01',
21+
...overrides,
22+
})
23+
24+
export const createTeamListItemFixture = (
25+
overrides?: Partial<TeamListItem>,
26+
): TeamListItem => ({
27+
id: 'team-1',
28+
name: 'Alpha Team',
29+
description: null,
30+
avatarUrl: null,
31+
membersCount: 3,
32+
currentUserRole: TEAM_ROLES.MEMBER,
33+
createdAt: '2024-01-01',
34+
updatedAt: '2024-01-01',
35+
...overrides,
36+
})
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { cleanup, render, screen } from '@testing-library/react'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { ProjectDetailPageView } from '@/views/projects/ui/project-detail-page-view'
5+
6+
// ─── Мок: next/link ──────────────────────────────────────────────
7+
vi.mock('next/link', () => ({
8+
default: ({
9+
href,
10+
children,
11+
className,
12+
}: React.PropsWithChildren<{ href: string; className?: string }>) => (
13+
<a href={href} className={className}>
14+
{children}
15+
</a>
16+
),
17+
}))
18+
19+
// ─── Мок: useParams ──────────────────────────────────────────────
20+
vi.mock('next/navigation', () => ({
21+
useParams: () => ({ id: 'team-1', projectId: 'my-project' }),
22+
}))
23+
24+
// ─── Мок: useTeamDetail ──────────────────────────────────────────
25+
vi.mock('@/shared/api/use-teams', () => ({
26+
useTeamDetail: () => ({ data: { id: 'team-1', name: 'Alpha Team' } }),
27+
}))
28+
29+
// ─── Мок: teamRoutes ─────────────────────────────────────────────
30+
vi.mock('@/shared/config', () => ({
31+
teamRoutes: {
32+
projects: (teamId: string) => `/teams/${teamId}/projects`,
33+
},
34+
}))
35+
36+
// ─── Мок: getProjectById ─────────────────────────────────────────
37+
const mockGetProjectById = vi.fn()
38+
39+
vi.mock('@/shared/lib/projects', () => ({
40+
getProjectById: (id: string) => mockGetProjectById(id),
41+
formatProjectNameFromId: (id: string) =>
42+
id
43+
.split('-')
44+
.map((part) => part[0]?.toUpperCase() + part.slice(1))
45+
.join(' '),
46+
}))
47+
48+
// ─── Мок: UI-компоненты ──────────────────────────────────────────
49+
vi.mock('@repo/ui', () => ({
50+
Avatar: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
51+
AvatarFallback: ({ children }: React.PropsWithChildren) => <span>{children}</span>,
52+
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
53+
}))
54+
55+
vi.mock('@repo/ui/icons', () => ({
56+
Activity: () => <span />,
57+
ChevronRight: () => <span />,
58+
KanbanSquare: () => <span />,
59+
Plus: () => <span />,
60+
Sparkles: () => <span />,
61+
SquareKanban: () => <span />,
62+
}))
63+
64+
describe('ProjectDetailPageView', () => {
65+
beforeEach(() => {
66+
vi.clearAllMocks()
67+
})
68+
69+
afterEach(cleanup)
70+
71+
it('breadcrumb — показывает имя команды как ссылку и название проекта', () => {
72+
mockGetProjectById.mockReturnValue({
73+
id: 'my-project',
74+
name: 'My Project',
75+
description: 'Описание',
76+
boards: [],
77+
recentTasks: [],
78+
})
79+
80+
render(<ProjectDetailPageView />)
81+
82+
const teamLink = screen.getByRole('link', { name: 'Alpha Team' })
83+
expect(teamLink).toBeDefined()
84+
expect(teamLink.getAttribute('href')).toBe('/teams/team-1/projects')
85+
expect(screen.getByRole('heading', { name: 'My Project' })).toBeDefined()
86+
})
87+
88+
it('доски — пустое состояние когда boards: []', () => {
89+
mockGetProjectById.mockReturnValue({
90+
id: 'my-project',
91+
name: 'My Project',
92+
description: 'Описание',
93+
boards: [],
94+
recentTasks: [],
95+
})
96+
97+
render(<ProjectDetailPageView />)
98+
99+
expect(
100+
screen.getByText('Пока нет досок. Создайте первую доску для проекта.'),
101+
).toBeDefined()
102+
})
103+
104+
it('последние задачи — рендерит ключ, заголовок и инициалы исполнителя', () => {
105+
mockGetProjectById.mockReturnValue({
106+
id: 'my-project',
107+
name: 'My Project',
108+
description: 'Описание',
109+
boards: [],
110+
recentTasks: [
111+
{ id: 'task-1', key: 'MP-1', title: 'Настроить CI', assigneeInitials: 'AB' },
112+
{ id: 'task-2', key: 'MP-2', title: 'Написать тесты', assigneeInitials: 'CD' },
113+
],
114+
})
115+
116+
render(<ProjectDetailPageView />)
117+
118+
expect(screen.getByText('MP-1')).toBeDefined()
119+
expect(screen.getByText('Настроить CI')).toBeDefined()
120+
expect(screen.getByText('AB')).toBeDefined()
121+
expect(screen.getByText('MP-2')).toBeDefined()
122+
expect(screen.getByText('Написать тесты')).toBeDefined()
123+
expect(screen.getByText('CD')).toBeDefined()
124+
})
125+
})
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { createTeamFixture, createTeamMemberFixture } from '@/test/mocks/teams.fixtures'
2+
import { cleanup, render, screen } from '@testing-library/react'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import { TEAM_ROLES } from '@repo/types'
6+
7+
import { TeamSettingsPageView } from '@/views/teams/ui/team-settings-page-view'
8+
9+
// ─── Мок: useParams ──────────────────────────────────────────────
10+
vi.mock('next/navigation', () => ({
11+
useParams: () => ({ id: 'team-1' }),
12+
}))
13+
14+
// ─── Мок: useTeamDetail ──────────────────────────────────────────
15+
const mockUseTeamDetail = vi.fn()
16+
17+
vi.mock('@/shared/api/use-teams', () => ({
18+
useTeamDetail: () => mockUseTeamDetail(),
19+
}))
20+
21+
// ─── Мок: UI-компоненты ──────────────────────────────────────────
22+
vi.mock('@repo/ui', () => ({
23+
Avatar: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
24+
AvatarFallback: ({ children }: React.PropsWithChildren) => <span>{children}</span>,
25+
Badge: ({
26+
children,
27+
}: React.PropsWithChildren<{ variant?: string; className?: string }>) => (
28+
<span>{children}</span>
29+
),
30+
Button: ({
31+
children,
32+
...props
33+
}: React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>) => (
34+
<button {...props}>{children}</button>
35+
),
36+
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
37+
ConfirmDialog: ({
38+
open,
39+
title,
40+
onConfirm,
41+
onOpenChange,
42+
}: {
43+
open: boolean
44+
title?: string
45+
onConfirm: () => void
46+
onOpenChange: (v: boolean) => void
47+
description?: string
48+
confirmLabel?: string
49+
pendingLabel?: string
50+
}) =>
51+
open ? (
52+
<div data-testid='confirm-dialog'>
53+
<p>{title}</p>
54+
<button onClick={onConfirm}>Подтвердить</button>
55+
<button onClick={() => onOpenChange(false)}>Закрыть</button>
56+
</div>
57+
) : null,
58+
DialogDrawer: ({
59+
children,
60+
}: React.PropsWithChildren<{ open: boolean; onOpenChange: (v: boolean) => void }>) => (
61+
<div>{children}</div>
62+
),
63+
DialogDrawerContent: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
64+
DialogDrawerDescription: ({ children }: React.PropsWithChildren) => <p>{children}</p>,
65+
DialogDrawerFooter: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
66+
DialogDrawerHeader: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
67+
DialogDrawerTitle: ({ children }: React.PropsWithChildren) => <h2>{children}</h2>,
68+
EmptyState: ({
69+
title,
70+
action,
71+
}: {
72+
title: string
73+
description?: string
74+
action?: React.ReactNode
75+
icon?: React.ReactNode
76+
className?: string
77+
}) => (
78+
<div data-testid='empty-state'>
79+
<p>{title}</p>
80+
{action}
81+
</div>
82+
),
83+
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
84+
Label: ({
85+
children,
86+
...props
87+
}: React.PropsWithChildren<React.LabelHTMLAttributes<HTMLLabelElement>>) => (
88+
<label {...props}>{children}</label>
89+
),
90+
Select: ({
91+
children,
92+
}: {
93+
children: React.ReactNode
94+
value?: string
95+
onValueChange?: (v: string) => void
96+
}) => <div>{children}</div>,
97+
SelectContent: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
98+
SelectItem: ({ children, value }: React.PropsWithChildren<{ value: string }>) => (
99+
<option value={value}>{children}</option>
100+
),
101+
SelectTrigger: ({
102+
children,
103+
}: React.PropsWithChildren<{ className?: string; id?: string }>) => (
104+
<div>{children}</div>
105+
),
106+
SelectValue: ({ placeholder }: { placeholder?: string }) => <span>{placeholder}</span>,
107+
VStack: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
108+
}))
109+
110+
vi.mock('@repo/ui/icons', () => ({
111+
Crown: () => <span />,
112+
Mail: () => <span />,
113+
Settings2: () => <span />,
114+
Shield: () => <span />,
115+
Trash2: () => <span />,
116+
UserPlus: () => <span />,
117+
Users: () => <span />,
118+
}))
119+
120+
describe('TeamSettingsPageView', () => {
121+
beforeEach(() => {
122+
vi.clearAllMocks()
123+
})
124+
125+
afterEach(cleanup)
126+
127+
it('loading — показывает "Загрузка участников..."', () => {
128+
mockUseTeamDetail.mockReturnValue({
129+
data: undefined,
130+
isPending: true,
131+
isError: false,
132+
refetch: vi.fn(),
133+
})
134+
135+
render(<TeamSettingsPageView />)
136+
137+
expect(screen.getByText('Загрузка участников...')).toBeDefined()
138+
})
139+
140+
it('error — показывает empty state "Не удалось загрузить команду"', () => {
141+
mockUseTeamDetail.mockReturnValue({
142+
data: undefined,
143+
isPending: false,
144+
isError: true,
145+
refetch: vi.fn(),
146+
})
147+
148+
render(<TeamSettingsPageView />)
149+
150+
expect(screen.getByTestId('empty-state')).toBeDefined()
151+
expect(screen.getByText('Не удалось загрузить команду')).toBeDefined()
152+
})
153+
154+
it('список участников — OWNER не имеет кнопки удаления, MEMBER — имеет', () => {
155+
mockUseTeamDetail.mockReturnValue({
156+
data: createTeamFixture({
157+
members: [
158+
createTeamMemberFixture({
159+
id: 'user-owner',
160+
name: 'Alice Owner',
161+
email: 'alice@example.com',
162+
role: TEAM_ROLES.OWNER,
163+
}),
164+
createTeamMemberFixture({
165+
id: 'user-member',
166+
name: 'Bob Member',
167+
email: 'bob@example.com',
168+
role: TEAM_ROLES.MEMBER,
169+
}),
170+
],
171+
}),
172+
isPending: false,
173+
isError: false,
174+
refetch: vi.fn(),
175+
})
176+
177+
render(<TeamSettingsPageView />)
178+
179+
expect(screen.getByText('Alice Owner')).toBeDefined()
180+
expect(screen.getByText('Bob Member')).toBeDefined()
181+
expect(screen.queryByRole('button', { name: 'Удалить Alice Owner' })).toBeNull()
182+
expect(screen.getByRole('button', { name: 'Удалить Bob Member' })).toBeDefined()
183+
})
184+
})

0 commit comments

Comments
 (0)