feat: add more roles (tech_lead, scrum_master, product_owner, designer, qa)
- Registration form: added 5 new role options to dropdown - Sidebar: new roles get proper nav access via ALL_ROLES/LEADER_ROLES - Dashboard: isLeader check expanded to include new leadership roles - Shared/Pages: role badge colors added for all new roles - Invite modal: expanded role dropdown
This commit is contained in:
54
src/__tests__/App.test.tsx
Normal file
54
src/__tests__/App.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import App from '../App';
|
||||
import * as api from '../api';
|
||||
|
||||
// Mock the API module
|
||||
vi.mock('../api', () => ({
|
||||
apiFetchTasks: vi.fn(),
|
||||
apiFetchUsers: vi.fn(),
|
||||
apiCreateTask: vi.fn(),
|
||||
apiUpdateTask: vi.fn(),
|
||||
apiAddActivity: vi.fn(),
|
||||
apiLogin: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('App Component', () => {
|
||||
it('renders login page when no user is logged in', () => {
|
||||
render(<App />);
|
||||
expect(screen.getByRole('button', { name: /sig\s*n\s*in/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders main content after login', async () => {
|
||||
const mockUser = { id: 'u1', name: 'Test User', email: 'test@example.com', role: 'admin', dept: 'Engineering' };
|
||||
const mockTasks = [{ id: 't1', title: 'Task 1', status: 'todo' }];
|
||||
const mockUsers = [mockUser];
|
||||
|
||||
// Mock API responses
|
||||
(api.apiLogin as any).mockResolvedValue(mockUser);
|
||||
(api.apiFetchTasks as any).mockResolvedValue(mockTasks);
|
||||
(api.apiFetchUsers as any).mockResolvedValue(mockUsers);
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Simulate login
|
||||
const emailInput = screen.getByLabelText(/email/i);
|
||||
const passwordInput = screen.getByLabelText(/password/i);
|
||||
const loginButton = screen.getByRole('button', { name: /sig\s*n\s*in/i });
|
||||
|
||||
await userEvent.type(emailInput, 'test@example.com');
|
||||
await userEvent.type(passwordInput, 'password');
|
||||
await userEvent.click(loginButton);
|
||||
|
||||
// Wait for data loading
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check if main content is rendered (e.g., Sidebar, Calendar)
|
||||
expect(screen.getByText('Calendar')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test User')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
67
src/__tests__/Kanban.test.tsx
Normal file
67
src/__tests__/Kanban.test.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { KanbanBoard } from '../Kanban';
|
||||
import type { Task, User } from '../data';
|
||||
|
||||
// Mock Shared components
|
||||
vi.mock('../Shared', () => ({
|
||||
Avatar: ({ userId }: any) => <div data-testid="avatar">{userId}</div>,
|
||||
PriorityBadge: ({ level }: any) => <div>{level}</div>,
|
||||
StatusBadge: ({ status }: any) => <div>{status}</div>,
|
||||
ProgressBar: () => <div>Progress</div>
|
||||
}));
|
||||
|
||||
describe('KanbanBoard Component', () => {
|
||||
const mockUser: User = { id: 'u1', name: 'Test User', email: 'test@example.com', role: 'emp', dept: 'dev', avatar: 'TU', color: '#000' };
|
||||
const mockUsers = [mockUser];
|
||||
|
||||
const mockTasks: Task[] = [
|
||||
{ id: 't1', title: 'Task 1', description: 'Desc', status: 'todo', priority: 'medium', assignee: 'u1', reporter: 'u1', dueDate: '2023-12-31', tags: [], subtasks: [], comments: [], activity: [], dependencies: [] },
|
||||
{ id: 't2', title: 'Task 2', description: 'Desc', status: 'inprogress', priority: 'high', assignee: 'u1', reporter: 'u1', dueDate: '2023-12-31', tags: [], subtasks: [], comments: [], activity: [], dependencies: [] },
|
||||
];
|
||||
|
||||
const mockOnTaskClick = vi.fn();
|
||||
const mockOnAddTask = vi.fn();
|
||||
const mockOnMoveTask = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders columns and tasks', () => {
|
||||
render(<KanbanBoard tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onAddTask={mockOnAddTask} onMoveTask={mockOnMoveTask} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
expect(screen.getByText('To Do')).toBeInTheDocument();
|
||||
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
||||
expect(screen.getByText('Review')).toBeInTheDocument();
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Task 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Task 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters tasks by search query', () => {
|
||||
render(<KanbanBoard tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onAddTask={mockOnAddTask} onMoveTask={mockOnMoveTask} filterUser={null} searchQuery="Task 1" users={mockUsers} />);
|
||||
|
||||
expect(screen.getByText('Task 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Task 2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onAddTask when + button clicked', () => {
|
||||
render(<KanbanBoard tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onAddTask={mockOnAddTask} onMoveTask={mockOnMoveTask} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
// There are multiple + buttons (one per column)
|
||||
const addButtons = screen.getAllByText('+');
|
||||
fireEvent.click(addButtons[0]); // Click first column (Todo) add button
|
||||
|
||||
expect(mockOnAddTask).toHaveBeenCalledWith('todo');
|
||||
});
|
||||
|
||||
it('calls onTaskClick when task clicked', () => {
|
||||
render(<KanbanBoard tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onAddTask={mockOnAddTask} onMoveTask={mockOnMoveTask} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Task 1'));
|
||||
expect(mockOnTaskClick).toHaveBeenCalledWith(mockTasks[0]);
|
||||
});
|
||||
});
|
||||
57
src/__tests__/ListView.test.tsx
Normal file
57
src/__tests__/ListView.test.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ListView } from '../ListView';
|
||||
import type { Task, User } from '../data';
|
||||
|
||||
describe('ListView Component', () => {
|
||||
const mockUser: User = { id: 'u1', name: 'Test User', email: 'test@example.com', role: 'emp', dept: 'dev', avatar: 'TU', color: '#000' };
|
||||
const mockUsers = [mockUser];
|
||||
|
||||
// Test data
|
||||
const mockTasks: Task[] = [
|
||||
{ id: 't1', title: 'Task 1', description: 'Desc', status: 'todo', priority: 'medium', assignee: 'u1', reporter: 'u1', dueDate: '2023-12-31', tags: [], subtasks: [], comments: [], activity: [], dependencies: [] },
|
||||
{ id: 't2', title: 'Task 2', description: 'Desc', status: 'done', priority: 'high', assignee: 'u1', reporter: 'u1', dueDate: '2023-12-31', tags: [], subtasks: [], comments: [], activity: [], dependencies: [] },
|
||||
];
|
||||
|
||||
const mockOnTaskClick = vi.fn();
|
||||
const mockOnToggleDone = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders tasks in list', () => {
|
||||
render(<ListView tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onToggleDone={mockOnToggleDone} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
expect(screen.getByText('Task 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Task 2')).toBeInTheDocument();
|
||||
// Check for headers (using getAllByText because buttons also have these labels)
|
||||
expect(screen.getAllByText('Title').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Assignee').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Status').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('filters tasks by search', () => {
|
||||
render(<ListView tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onToggleDone={mockOnToggleDone} filterUser={null} searchQuery="Task 1" users={mockUsers} />);
|
||||
|
||||
expect(screen.getByText('Task 1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Task 2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles task click', () => {
|
||||
render(<ListView tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onToggleDone={mockOnToggleDone} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Task 1'));
|
||||
expect(mockOnTaskClick).toHaveBeenCalledWith(mockTasks[0]);
|
||||
});
|
||||
|
||||
it('handles toggle done', () => {
|
||||
render(<ListView tasks={mockTasks} currentUser={mockUser} onTaskClick={mockOnTaskClick} onToggleDone={mockOnToggleDone} filterUser={null} searchQuery="" users={mockUsers} />);
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
fireEvent.click(checkboxes[0]);
|
||||
|
||||
expect(mockOnToggleDone).toHaveBeenCalledWith('t1');
|
||||
});
|
||||
});
|
||||
107
src/__tests__/Login.test.tsx
Normal file
107
src/__tests__/Login.test.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { LoginPage } from '../Login';
|
||||
import * as api from '../api';
|
||||
|
||||
// Mock API
|
||||
vi.mock('../api', () => ({
|
||||
apiLogin: vi.fn(),
|
||||
apiRegister: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('LoginPage Component', () => {
|
||||
const mockOnLogin = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockOnLogin.mockClear();
|
||||
});
|
||||
|
||||
it('renders login form by default', () => {
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
expect(screen.getByText(/scrum-manager/i)).toBeInTheDocument(); // Logo check
|
||||
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/no account\? register here/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
it('switches to register mode', async () => {
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
await userEvent.click(screen.getByText(/no account\? register here/i));
|
||||
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /create account/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/already have an account\? sign in/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles successful login', async () => {
|
||||
const mockUser = { id: 'u1', name: 'Test', email: 'test@example.com', role: 'emp', dept: 'dev' };
|
||||
(api.apiLogin as any).mockResolvedValue(mockUser);
|
||||
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
|
||||
await userEvent.type(screen.getByLabelText(/password/i), 'password');
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.apiLogin).toHaveBeenCalledWith('test@example.com', 'password');
|
||||
expect(mockOnLogin).toHaveBeenCalledWith(mockUser);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles login failure', async () => {
|
||||
(api.apiLogin as any).mockRejectedValue(new Error('Invalid credentials'));
|
||||
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'wrong@example.com');
|
||||
await userEvent.type(screen.getByLabelText(/password/i), 'wrongpass');
|
||||
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(mockOnLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles successful registration', async () => {
|
||||
const mockUser = { id: 'u2', name: 'New User', email: 'new@example.com', role: 'employee', dept: 'dev' };
|
||||
(api.apiRegister as any).mockResolvedValue(mockUser);
|
||||
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
await userEvent.click(screen.getByText(/no account\? register here/i));
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/name/i), 'New User');
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'new@example.com');
|
||||
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
|
||||
await userEvent.type(screen.getByLabelText(/department/i), 'DevOps'); // "e.g. Backend..." placeholder, checking label
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /create account/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.apiRegister).toHaveBeenCalledWith({
|
||||
name: 'New User',
|
||||
email: 'new@example.com',
|
||||
password: 'password123',
|
||||
role: 'employee', // Default
|
||||
dept: 'DevOps'
|
||||
});
|
||||
expect(mockOnLogin).toHaveBeenCalledWith(mockUser);
|
||||
});
|
||||
});
|
||||
|
||||
it('validates registration inputs', async () => {
|
||||
render(<LoginPage onLogin={mockOnLogin} />);
|
||||
await userEvent.click(screen.getByText(/no account\? register here/i));
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /create account/i }));
|
||||
|
||||
expect(screen.getByText(/all fields are required/i)).toBeInTheDocument();
|
||||
expect(api.apiRegister).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
108
src/__tests__/TaskDrawer.test.tsx
Normal file
108
src/__tests__/TaskDrawer.test.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TaskDrawer } from '../TaskDrawer';
|
||||
import type { Task, User } from '../data';
|
||||
|
||||
// Mock Shared components to avoid testing their specific implementation here
|
||||
vi.mock('../Shared', () => ({
|
||||
Avatar: ({ userId }: any) => <div data-testid="avatar">{userId}</div>,
|
||||
Tag: ({ label }: any) => <div>{label}</div>,
|
||||
ProgressBar: () => <div>Progress</div>
|
||||
}));
|
||||
|
||||
describe('TaskDrawer Component', () => {
|
||||
const mockUser: User = { id: 'u1', name: 'Test User', email: 'test@example.com', role: 'emp', dept: 'dev', avatar: 'TU', color: '#000' };
|
||||
const mockUsers = [mockUser, { id: 'u2', name: 'Other User', email: 'other@example.com', role: 'emp', dept: 'dev', avatar: 'OU', color: '#fff' }];
|
||||
|
||||
const mockTask: Task = {
|
||||
id: 't1',
|
||||
title: 'Test Task',
|
||||
description: 'Test Description',
|
||||
status: 'todo',
|
||||
priority: 'medium',
|
||||
assignee: 'u1',
|
||||
reporter: 'u1',
|
||||
dueDate: '2023-12-31',
|
||||
tags: ['bug'],
|
||||
subtasks: [],
|
||||
comments: [],
|
||||
activity: [],
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
const mockOnUpdate = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
const mockOnAddDep = vi.fn();
|
||||
const mockOnToggleDep = vi.fn();
|
||||
const mockOnRemoveDep = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders task details correctly', () => {
|
||||
render(<TaskDrawer task={mockTask} currentUser={mockUser} onClose={mockOnClose} onUpdate={mockOnUpdate} onAddDependency={mockOnAddDep} onToggleDependency={mockOnToggleDep} onRemoveDependency={mockOnRemoveDep} users={mockUsers} />);
|
||||
|
||||
// screen.debug();
|
||||
|
||||
expect(screen.getByText('Test Task')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument();
|
||||
// Check for mocked avatar with userId
|
||||
const avatars = screen.getAllByTestId('avatar');
|
||||
expect(avatars.length).toBeGreaterThan(0);
|
||||
expect(avatars[0]).toHaveTextContent('u1');
|
||||
// expect(screen.getByText(/Test User/i)).toBeInTheDocument(); // Assignee name might be tricky in Select/Div
|
||||
expect(screen.getByText('bug')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates title/description calling onUpdate', async () => {
|
||||
// Title isn't editable in the drawer based on code, only via modal or just display?
|
||||
// Checking code: <h2 className="drawer-title">{task.title}</h2>. It's not an input.
|
||||
// But status, priority, assignee are selects.
|
||||
|
||||
render(<TaskDrawer task={mockTask} currentUser={mockUser} onClose={mockOnClose} onUpdate={mockOnUpdate} onAddDependency={mockOnAddDep} onToggleDependency={mockOnToggleDep} onRemoveDependency={mockOnRemoveDep} users={mockUsers} />);
|
||||
|
||||
// Update status
|
||||
const statusSelect = screen.getByRole('combobox', { name: /status/i });
|
||||
await userEvent.selectOptions(statusSelect, 'done');
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 't1',
|
||||
status: 'done'
|
||||
}));
|
||||
});
|
||||
|
||||
it('adds a subtask', async () => {
|
||||
render(<TaskDrawer task={mockTask} currentUser={mockUser} onClose={mockOnClose} onUpdate={mockOnUpdate} onAddDependency={mockOnAddDep} onToggleDependency={mockOnToggleDep} onRemoveDependency={mockOnRemoveDep} users={mockUsers} />);
|
||||
|
||||
const input = screen.getByPlaceholderText(/add a subtask/i);
|
||||
await userEvent.type(input, 'New Subtask{enter}');
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
subtasks: expect.arrayContaining([expect.objectContaining({ title: 'New Subtask', done: false })])
|
||||
}));
|
||||
});
|
||||
|
||||
it('adds a comment', async () => {
|
||||
render(<TaskDrawer task={mockTask} currentUser={mockUser} onClose={mockOnClose} onUpdate={mockOnUpdate} onAddDependency={mockOnAddDep} onToggleDependency={mockOnToggleDep} onRemoveDependency={mockOnRemoveDep} users={mockUsers} />);
|
||||
|
||||
const input = screen.getByPlaceholderText(/add a comment/i);
|
||||
await userEvent.type(input, 'This is a comment{enter}');
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
comments: expect.arrayContaining([expect.objectContaining({ text: 'This is a comment', userId: 'u1' })])
|
||||
}));
|
||||
});
|
||||
|
||||
it('adds a dependency', async () => {
|
||||
render(<TaskDrawer task={mockTask} currentUser={mockUser} onClose={mockOnClose} onUpdate={mockOnUpdate} onAddDependency={mockOnAddDep} onToggleDependency={mockOnToggleDep} onRemoveDependency={mockOnRemoveDep} users={mockUsers} />);
|
||||
|
||||
const descInput = screen.getByPlaceholderText(/describe the dependency/i);
|
||||
await userEvent.type(descInput, 'Blocked by API{enter}');
|
||||
|
||||
// Default select is "Anyone" (empty string).
|
||||
expect(mockOnAddDep).toHaveBeenCalledWith('t1', { dependsOnUserId: '', description: 'Blocked by API' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user