Skip to content

Commit fdaefe3

Browse files
committed
Add test suite and refactor client component architecture
Set up testing infrastructure with vitest, jsdom, and react-testing-library. Reorganize components into focused modules, split CardModal into composable tab components, and add comprehensive test coverage across components, hooks, and stores. Refactor API layer for improved type safety and add new endpoints for sprint-based card querying.
1 parent 7328be6 commit fdaefe3

63 files changed

Lines changed: 5749 additions & 3741 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎client/package.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@
2424
"zustand": "^5.0.12"
2525
},
2626
"devDependencies": {
27+
"@testing-library/jest-dom": "^6.9.1",
28+
"@testing-library/react": "^16.3.2",
29+
"@testing-library/user-event": "^14.6.1",
2730
"@types/react": "^18.3.5",
2831
"@types/react-dom": "^18.3.0",
2932
"@vitejs/plugin-react": "^4.3.1",
3033
"autoprefixer": "^10.4.20",
34+
"jsdom": "^27.0.1",
3135
"postcss": "^8.4.41",
3236
"tailwindcss": "^3.4.10",
3337
"typescript": "^5.5.3",

‎client/src/App.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,9 @@ export default function App() {
5050

5151
// Hydrate auth state on mount
5252
useEffect(() => {
53-
fetch('/api/auth/me', { credentials: 'include' })
54-
.then(res => res.json())
55-
.then(json => {
56-
if (json.data) setUser(json.data)
57-
else setUser(null)
58-
})
53+
api.auth
54+
.me()
55+
.then(user => setUser(user))
5956
.catch(() => setUser(null))
6057
}, [setUser])
6158

‎client/src/api/index.ts‎

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,41 @@ import axios from 'axios'
22
import toast from 'react-hot-toast'
33
import type {
44
ActivityItem,
5+
ActivityLog,
6+
AuthUser,
57
CalendarEvent,
68
CalendarHoliday,
79
CalendarRange,
810
CalendarVacation,
11+
CapacityEntry,
912
Card,
1013
CardLink,
1114
Comment,
15+
CycleTimeEntry,
1216
DashboardStats,
17+
Dependency,
18+
DependencyList,
1319
Epic,
1420
Feature,
21+
Label,
1522
Lane,
23+
LanePreset,
24+
Notification,
1625
Project,
26+
ProjectAccessEntry,
1727
ProjectSummary,
1828
RetroCategory,
1929
RetroItem,
2030
Retrospective,
31+
RoadmapEpic,
2132
Sprint,
2233
Task,
2334
TestCase,
2435
TestCaseSummary,
2536
TestRun,
2637
TestSuite,
38+
User,
39+
VelocityEntry,
2740
} from '../types'
2841

2942
type Priority = 'low' | 'medium' | 'high' | 'critical'
@@ -101,6 +114,7 @@ export const api = {
101114
},
102115
cards: {
103116
listByLane: (laneId: number) => unwrap<Card[]>(http.get(`/lanes/${laneId}/cards`)),
117+
listBySprint: (sprintId: number) => unwrap<Card[]>(http.get(`/sprints/${sprintId}/cards`)),
104118
get: (cardId: number) => unwrap<Card>(http.get(`/cards/${cardId}`)),
105119
create: (
106120
laneId: number,
@@ -123,6 +137,10 @@ export const api = {
123137
deleteTask: (taskId: number) => unwrap<{ id: number }>(http.delete(`/tasks/${taskId}`)),
124138
reorderTasks: (cardId: number, ids: number[]) => unwrap<Task[]>(http.post(`/cards/${cardId}/tasks/reorder`, { ids })),
125139
listProjectTasks: (projectId: number) => unwrap<(Task & { story_title: string })[]>(http.get(`/projects/${projectId}/tasks`)),
140+
searchStories: (projectId: number, q: string) =>
141+
unwrap<Pick<Card, 'id' | 'title' | 'priority' | 'story_points' | 'assignee' | 'swim_lane_id'>[]>(
142+
http.get(`/projects/${projectId}/stories/search`, { params: { q } })
143+
),
126144
},
127145
epics: {
128146
list: (projectId: number) => unwrap<Epic[]>(http.get(`/projects/${projectId}/epics`)),
@@ -152,10 +170,14 @@ export const api = {
152170
update: (id: number, data: Partial<Pick<Sprint, 'name' | 'goal' | 'start_date' | 'end_date' | 'status'>>) =>
153171
unwrap<Sprint>(http.patch(`/sprints/${id}`, data)),
154172
delete: (id: number) => unwrap<{ id: number }>(http.delete(`/sprints/${id}`)),
173+
complete: (id: number) => unwrap<Sprint>(http.post(`/sprints/${id}/complete`)),
174+
},
175+
backlog: {
176+
get: (projectId: number) => unwrap<Card[]>(http.get(`/projects/${projectId}/backlog`)),
155177
},
156178
comments: {
157179
list: (cardId: number) => unwrap<Comment[]>(http.get(`/cards/${cardId}/comments`)),
158-
create: (cardId: number, data: { author: string; body: string }) =>
180+
create: (cardId: number, data: { body: string }) =>
159181
unwrap<Comment>(http.post(`/cards/${cardId}/comments`, data)),
160182
delete: (commentId: number) => unwrap<{ id: number }>(http.delete(`/comments/${commentId}`)),
161183
},
@@ -259,4 +281,128 @@ export const api = {
259281
},
260282
remove: (attachmentId: number) => unwrap<{ deleted: boolean }>(http.delete(`/attachments/${attachmentId}`)),
261283
},
284+
activity: {
285+
getCardActivity: (cardId: number) => unwrap<ActivityLog[]>(http.get(`/cards/${cardId}/activity`)),
286+
},
287+
labels: {
288+
list: (projectId: number) => unwrap<Label[]>(http.get(`/projects/${projectId}/labels`)),
289+
create: (projectId: number, data: { name: string; color?: string }) =>
290+
unwrap<Label>(http.post(`/projects/${projectId}/labels`, data)),
291+
getCardLabels: (cardId: number) => unwrap<Label[]>(http.get(`/cards/${cardId}/labels`)),
292+
addCardLabel: (cardId: number, labelId: number) =>
293+
unwrap<{ card_id: number; label_id: number }>(http.post(`/cards/${cardId}/labels`, { label_id: labelId })),
294+
removeCardLabel: (cardId: number, labelId: number) =>
295+
unwrap<{ card_id: number; label_id: number }>(http.delete(`/cards/${cardId}/labels/${labelId}`)),
296+
},
297+
dependencies: {
298+
list: (cardId: number) => unwrap<DependencyList>(http.get(`/cards/${cardId}/dependencies`)),
299+
add: (cardId: number, data: { target_id: number; type: 'blocks' | 'blocked_by' }) =>
300+
unwrap<Dependency>(http.post(`/cards/${cardId}/dependencies`, data)),
301+
remove: (depId: number) => unwrap<{ id: number }>(http.delete(`/dependencies/${depId}`)),
302+
},
303+
auth: {
304+
login: (data: { email: string; password: string }) =>
305+
unwrap<AuthUser>(http.post('/auth/login', data)),
306+
logout: () => unwrap<{ ok: true }>(http.post('/auth/logout')),
307+
me: () => unwrap<AuthUser & { project_access: ProjectAccessEntry[] }>(http.get('/auth/me')),
308+
updateMe: (data: {
309+
display_name?: string
310+
current_password?: string
311+
new_password?: string
312+
country?: string | null
313+
state?: string | null
314+
city?: string | null
315+
home_country?: string | null
316+
home_state?: string | null
317+
home_city?: string | null
318+
timezone?: string | null
319+
job_title?: string | null
320+
department?: string | null
321+
phone?: string | null
322+
gender?: string | null
323+
reporting_manager_id?: number | null
324+
email_notifications?: boolean
325+
}) =>
326+
unwrap<AuthUser>(http.patch('/auth/me', data)),
327+
},
328+
users: {
329+
list: () => unwrap<User[]>(http.get('/users')),
330+
search: (q: string) => unwrap<User[]>(http.get('/users/search', { params: { q } })),
331+
create: (data: {
332+
email: string
333+
display_name: string
334+
password: string
335+
role?: string
336+
skills?: string[]
337+
country?: string | null
338+
state?: string | null
339+
city?: string | null
340+
home_country?: string | null
341+
home_state?: string | null
342+
home_city?: string | null
343+
timezone?: string | null
344+
job_title?: string | null
345+
department?: string | null
346+
phone?: string | null
347+
gender?: string | null
348+
reporting_manager_id?: number | null
349+
}) =>
350+
unwrap<User>(http.post('/users', data)),
351+
update: (id: number, data: {
352+
display_name?: string
353+
role?: string
354+
is_active?: boolean
355+
new_password?: string
356+
skills?: string[]
357+
country?: string | null
358+
state?: string | null
359+
city?: string | null
360+
home_country?: string | null
361+
home_state?: string | null
362+
home_city?: string | null
363+
timezone?: string | null
364+
job_title?: string | null
365+
department?: string | null
366+
phone?: string | null
367+
gender?: string | null
368+
reporting_manager_id?: number | null
369+
}) =>
370+
unwrap<User>(http.patch(`/users/${id}`, data)),
371+
delete: (id: number) => unwrap<{ id: number }>(http.delete(`/users/${id}`)),
372+
projectAccess: (userId: number) =>
373+
unwrap<{ project_id: number; project_name: string; role: 'project_admin' | 'contributor' | 'reader' | null }[]>(
374+
http.get(`/users/${userId}/project-access`)
375+
),
376+
},
377+
projectAccess: {
378+
list: (projectId: number) => unwrap<ProjectAccessEntry[]>(http.get(`/projects/${projectId}/access`)),
379+
grant: (projectId: number, data: { user_id: number; role: string; skills?: string[]; capacity?: number | null }) =>
380+
unwrap<ProjectAccessEntry>(http.post(`/projects/${projectId}/access`, data)),
381+
update: (projectId: number, userId: number, data: { role?: string; skills?: string[]; capacity?: number | null }) =>
382+
unwrap<ProjectAccessEntry>(http.patch(`/projects/${projectId}/access/${userId}`, data)),
383+
revoke: (projectId: number, userId: number) =>
384+
unwrap<{ user_id: number; project_id: number }>(http.delete(`/projects/${projectId}/access/${userId}`)),
385+
},
386+
notifications: {
387+
list: (unreadOnly?: boolean) =>
388+
unwrap<Notification[]>(http.get('/notifications', { params: unreadOnly ? { unread_only: 1 } : undefined })),
389+
markRead: (id: number) => unwrap<{ id: number }>(http.patch(`/notifications/${id}/read`)),
390+
markAllRead: () => unwrap<{ count: number }>(http.patch('/notifications/read-all')),
391+
},
392+
roadmap: {
393+
get: (projectId: number) => unwrap<RoadmapEpic[]>(http.get(`/projects/${projectId}/roadmap`)),
394+
},
395+
reports: {
396+
velocity: (projectId: number) => unwrap<VelocityEntry[]>(http.get(`/projects/${projectId}/velocity`)),
397+
cycleTime: (projectId: number) => unwrap<CycleTimeEntry[]>(http.get(`/projects/${projectId}/cycle-time`)),
398+
capacity: (projectId: number, sprintId: number) =>
399+
unwrap<CapacityEntry[]>(http.get(`/projects/${projectId}/capacity`, { params: { sprint_id: sprintId } })),
400+
exportUrl: (projectId: number, type: 'backlog' | 'sprint' | 'full', sprintId?: number) => {
401+
const base = `/api/projects/${projectId}/export/csv?type=${type}`
402+
return sprintId ? `${base}&sprint_id=${sprintId}` : base
403+
},
404+
},
405+
lanePresets: {
406+
list: () => unwrap<LanePreset[]>(http.get('/lane-presets')),
407+
},
262408
}

‎client/src/components/AddCardForm.tsx‎

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)