Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions __tests__/KanbanBoard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @jest-environment jsdom
*/
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, onClick, ...props }) => <div onClick={onClick} {...props}>{children}</div>,
},
AnimatePresence: ({ children }) => <>{children}</>,
}));

jest.mock('@/components/ui/badge', () => ({
Badge: ({ children, className }) => <span className={className}>{children}</span>,
}));

jest.mock('@/components/ui/avatar', () => ({
Avatar: ({ children, className }) => <div className={className}>{children}</div>,
AvatarImage: () => null,
AvatarFallback: ({ children, className }) => <span className={className}>{children}</span>,
}));

jest.mock('@/components/workspace/TaskDetailDialog', () => {
return function MockDialog({ open, onOpenChange }) {
return open ? <div data-testid="task-detail-dialog" onClick={() => onOpenChange(false)} /> : null;
};
});

jest.mock('date-fns', () => ({
format: jest.fn(() => '1 Jan 2026'),
formatDistanceToNow: jest.fn(() => '2 days ago'),
isToday: jest.fn(() => false),
isYesterday: jest.fn(() => false),
}));

const { render, screen, fireEvent } = require('@testing-library/react');
const React = require('react');
const KanbanBoard = require('@/components/workspace/KanbanBoard').default;

const makeTask = (overrides = {}) => ({
_id: 'task-' + Math.random().toString(36).slice(2, 8),
id: 'task-' + Math.random().toString(36).slice(2, 8),
title: 'Test Task',
status: 'Ready',
assignedTo: 'user-1',
assignedToName: 'User One',
...overrides,
});

const makeStep = (tasks = []) => ({
_id: 'step-1',
id: 'step-1',
title: 'Backlog',
tasks,
});

const defaultProps = (overrides = {}) => ({
steps: [],
onUpdateTask: jest.fn(),
users: [{ uid: 'user-1', displayName: 'User One', email: 'u@t.com' }],
isOwner: true,
currentUser: { uid: 'user-1' },
...overrides,
});

// F1: renders 5 columns
test('F1: renders all 5 Kanban columns', () => {
render(<KanbanBoard {...defaultProps()} />);
expect(screen.getByText('Ready')).toBeTruthy();
expect(screen.getByText('Active')).toBeTruthy();
expect(screen.getByText('In Progress')).toBeTruthy();
expect(screen.getByText('Done')).toBeTruthy();
expect(screen.getByText('PR Raised')).toBeTruthy();
});

// F2: maps Pending → Ready (legacy fallback)
test('F2: maps Pending status to Ready column', () => {
const task = makeTask({ status: 'Pending' });
const step = makeStep([task]);
render(<KanbanBoard {...defaultProps({ steps: [step] })} />);
const readyColumn = screen.getByText('Ready').closest('div.flex.flex-col');
expect(readyColumn.textContent).toContain('Test Task');
});

// F3: maps Completed → Done
test('F3: maps Completed status to Done column', () => {
const task = makeTask({ status: 'Completed', title: 'Completed Task' });
const step = makeStep([task]);
render(<KanbanBoard {...defaultProps({ steps: [step] })} />);
const doneColumn = screen.getByText('Done').closest('div.flex.flex-col');
expect(doneColumn.textContent).toContain('Completed Task');
});

// F4: clicking Ready task as assignee calls onUpdateTask with Active
test('F4: clicking Ready task as assignee auto-updates to Active', () => {
const task = makeTask({ status: 'Ready', _id: 't1', id: 't1' });
const step = makeStep([task]);
const onUpdateTask = jest.fn();
render(<KanbanBoard {...defaultProps({ steps: [step], onUpdateTask })} />);
fireEvent.click(screen.getByText('Test Task'));
expect(onUpdateTask).toHaveBeenCalledWith('step-1', 't1', { status: 'Active' });
});

// F5: clicking Ready task as non-assignee does NOT call onUpdateTask
test('F5: clicking Ready task as non-assignee does not auto-update', () => {
const task = makeTask({ status: 'Ready', _id: 't2', id: 't2', assignedTo: 'other-user' });
const step = makeStep([task]);
const onUpdateTask = jest.fn();
render(<KanbanBoard {...defaultProps({ steps: [step], onUpdateTask, currentUser: { uid: 'user-1' } })} />);
fireEvent.click(screen.getByText('Test Task'));
expect(onUpdateTask).not.toHaveBeenCalled();
});

// F6: clicking non-Ready task does NOT call onUpdateTask
test('F6: clicking In Progress task does not auto-update', () => {
const task = makeTask({ status: 'In Progress', _id: 't3', id: 't3' });
const step = makeStep([task]);
const onUpdateTask = jest.fn();
render(<KanbanBoard {...defaultProps({ steps: [step], onUpdateTask })} />);
fireEvent.click(screen.getByText('Test Task'));
expect(onUpdateTask).not.toHaveBeenCalled();
});

// F7: column count badges reflect task counts
test('F7: column badges show correct task counts', () => {
const tasks = [
makeTask({ status: 'Ready', title: 'R1', _id: 'r1', id: 'r1' }),
makeTask({ status: 'Done', title: 'D1', _id: 'd1', id: 'd1' }),
makeTask({ status: 'Done', title: 'D2', _id: 'd2', id: 'd2' }),
];
const step = makeStep(tasks);
render(<KanbanBoard {...defaultProps({ steps: [step] })} />);
const badges = screen.getAllByText(/^[0-9]+$/);
const counts = badges.map(b => parseInt(b.textContent, 10));
expect(counts).toContain(1); // Ready: 1
expect(counts).toContain(2); // Done: 2
});
137 changes: 137 additions & 0 deletions __tests__/taskSocketService.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
jest.mock('socket.io-client', () => {
const handlers = {};
const mockSocket = {
connected: false,
on: jest.fn((event, cb) => { handlers[event] = cb; }),
emit: jest.fn(),
disconnect: jest.fn(() => { mockSocket.connected = false; }),
_fire: (event, data) => handlers[event]?.(data),
_setConnected: (val) => { mockSocket.connected = val; },
};
return { io: jest.fn(() => mockSocket), __mockSocket: mockSocket };
});

jest.mock('@/lib/utils', () => ({ SOCKET_BASE_URL: 'http://localhost:3000' }));

const { io, __mockSocket } = require('socket.io-client');
const {
connectTaskSocket,
disconnectTaskSocket,
getTaskSocket,
joinProject,
leaveProject,
onTaskCreated,
onTaskUpdated,
onTaskDeleted,
onTaskAssigned,
} = require('@/services/taskSocketService');

beforeEach(() => {
jest.clearAllMocks();
__mockSocket.connected = false;
disconnectTaskSocket();
});

afterEach(() => {
disconnectTaskSocket();
});

// F8: connectTaskSocket creates socket connection
test('F8: connectTaskSocket creates socket with /tasks namespace', () => {
const sock = connectTaskSocket('user-1');
expect(io).toHaveBeenCalledWith('http://localhost:3000/tasks', expect.objectContaining({
query: { userId: 'user-1' },
transports: ['websocket', 'polling'],
}));
expect(sock).toBe(__mockSocket);
});

// F9: connectTaskSocket is idempotent when already connected
test('F9: connectTaskSocket returns existing socket when already connected', () => {
__mockSocket._setConnected(true);
const sock1 = connectTaskSocket('user-1');
const sock2 = connectTaskSocket('user-1');
expect(io).toHaveBeenCalledTimes(1);
expect(sock1).toBe(sock2);
});

// F10: onTaskCreated fires callback on task-created event
test('F10: onTaskCreated callback fires when task-created event received', () => {
connectTaskSocket('user-1');
const cb = jest.fn();
onTaskCreated(cb);
__mockSocket._fire('task-created', { projectId: 'p1', taskId: 't1' });
expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't1' });
});

// F11: onTaskUpdated fires callback
test('F11: onTaskUpdated callback fires on task-updated event', () => {
connectTaskSocket('user-1');
const cb = jest.fn();
onTaskUpdated(cb);
__mockSocket._fire('task-updated', { projectId: 'p1', taskId: 't2', changes: { status: 'Done' } });
expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't2', changes: { status: 'Done' } });
});

// F12: onTaskDeleted fires callback
test('F12: onTaskDeleted callback fires on task-deleted event', () => {
connectTaskSocket('user-1');
const cb = jest.fn();
onTaskDeleted(cb);
__mockSocket._fire('task-deleted', { projectId: 'p1', taskId: 't3' });
expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't3' });
});

// F13: onTaskAssigned fires callback
test('F13: onTaskAssigned callback fires on task-assigned event', () => {
connectTaskSocket('user-1');
const cb = jest.fn();
onTaskAssigned(cb);
__mockSocket._fire('task-assigned', { projectId: 'p1', taskId: 't4' });
expect(cb).toHaveBeenCalledWith({ projectId: 'p1', taskId: 't4' });
});

// F14: unsubscribe removes callback
test('F14: unsubscribe function removes callback from listeners', () => {
connectTaskSocket('user-1');
const cb = jest.fn();
const unsub = onTaskCreated(cb);
unsub();
__mockSocket._fire('task-created', { projectId: 'p1' });
expect(cb).not.toHaveBeenCalled();
});

// F15: joinProject emits join-project event
test('F15: joinProject emits join-project on socket', () => {
connectTaskSocket('user-1');
joinProject('proj-123');
expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-123');
});

// F16: leaveProject emits leave-project event
test('F16: leaveProject emits leave-project on socket', () => {
connectTaskSocket('user-1');
joinProject('proj-123');
leaveProject('proj-123');
expect(__mockSocket.emit).toHaveBeenCalledWith('leave-project', 'proj-123');
});

// F17: disconnectTaskSocket clears socket and disconnects
test('F17: disconnectTaskSocket disconnects and clears state', () => {
connectTaskSocket('user-1');
joinProject('p1');
disconnectTaskSocket();
expect(__mockSocket.disconnect).toHaveBeenCalled();
expect(getTaskSocket()).toBeNull();
});

// F18: reconnect re-joins previously joined projects
test('F18: on reconnect, previously joined projects are re-joined', () => {
connectTaskSocket('user-1');
joinProject('proj-a');
joinProject('proj-b');
__mockSocket.emit.mockClear();
__mockSocket._fire('connect', {});
expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-a');
expect(__mockSocket.emit).toHaveBeenCalledWith('join-project', 'proj-b');
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#Mon Aug 03 21:46:29 IST 2026
gradle.version=8.9
Empty file.
1 change: 1 addition & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ module.exports = {
modules: 'commonjs'
}],
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
],
};
6 changes: 6 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
*/
module.exports = {
testEnvironment: 'node',
// Run tests sequentially to avoid ESM module loading race conditions
// with jose/jwks-rsa (firebase-admin transitive deps) under parallel workers.
maxWorkers: 1,
// Includes both `*.test.js` and legacy `*_test.js` suffixes.
testMatch: ['**/tests/**/*.test.js', '**/tests/**/*_test.js', '**/__tests__/**/*.test.js'],
// ESM-only transitive deps (jose, jwks-rsa via firebase-admin) must go
Expand All @@ -83,4 +86,7 @@ module.exports = {
transform: { '^.+\\.js$': 'babel-jest' },
transformIgnorePatterns: ['node_modules/(?!jose|jwks-rsa|@panva/asn1.js|firebase-admin|@firebase/)'],
clearMocks: true,
moduleNameMapper: {
'^octokit$': '<rootDir>/tests/__mocks__/octokit.js',
},
};
4 changes: 2 additions & 2 deletions backend/models/ProjectTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const mongoose = require('mongoose');

const projectTaskSchema = new mongoose.Schema(
{
displayId: { type: String, default: null },
displayId: { type: String },
title: { type: String, required: true },
description: { type: String, default: null },
status: { type: String, default: 'Ready' },
Expand All @@ -89,7 +89,7 @@ const projectTaskSchema = new mongoose.Schema(
assignedBy: { type: String, default: null },


commitCode: { type: String, default: null },
commitCode: { type: String },
commitMessage: { type: String, default: null },
commitUrl: { type: String, default: null },
commitAuthor: { type: String, default: null },
Expand Down
1 change: 0 additions & 1 deletion backend/models/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const teamSchema = new mongoose.Schema(
}
);

teamSchema.index({ inviteCode: 1 }, { unique: true });
teamSchema.index({ members: 1 });
teamSchema.index({ ownerId: 1 });

Expand Down
2 changes: 0 additions & 2 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ const userSchema = new mongoose.Schema(
}
);

userSchema.index({ uid: 1 }, { unique: true });
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ displayName: 'text', firstName: 'text', lastName: 'text' });

module.exports = mongoose.model('User', userSchema);
Loading
Loading