);
};
});
-// Mock CancelMeetingModal component
-jest.mock('@/components/meetingSystem/CancelMeetingModal', () => {
- return function MockCancelMeetingModal({ meetingId, onClose, onCancel, userName }: any) {
- return (
-
-
Cancelling meeting for {userName}
-
-
+jest.mock('@/components/ui/Alert', () => {
+ return function MockAlert({ isOpen, type, message, onClose }: any) {
+ return isOpen ? (
+
+
{message}
+
- );
+ ) : null;
};
});
-// Mock fetch for API calls
-global.fetch = jest.fn();
+jest.mock('@/components/ui/ConfirmationDialog', () => {
+ return function MockConfirmationDialog({ isOpen, title, onConfirm, onClose }: any) {
+ return isOpen ? (
+
+
{title}
+
+
+
+ ) : null;
+ };
+});
-// Mock URL.createObjectURL for notes download
-global.URL.createObjectURL = jest.fn(() => 'mock-blob-url');
-global.URL.revokeObjectURL = jest.fn();
+// Mock API Services
+const mockFetchMeetings = jest.fn();
+const mockCreateMeeting = jest.fn();
+const mockUpdateMeeting = jest.fn();
+const mockCancelMeetingWithReason = jest.fn();
+const mockFetchAllUserMeetingNotes = jest.fn();
+const mockFilterMeetingsByType = jest.fn();
+const mockCheckMeetingLimit = jest.fn();
+const mockCanCancelMeeting = jest.fn();
-describe('MeetingBox Component', () => {
- const mockProps = {
- chatRoomId: '6873cdd898892472c9621cf1',
- userId: '6873cc50ac4e1d6e1cddf33f',
- onClose: jest.fn(),
- onMeetingUpdate: jest.fn()
- };
+jest.mock('@/services/meetingApiServices', () => ({
+ fetchMeetings: (...args: any[]) => mockFetchMeetings(...args),
+ createMeeting: (...args: any[]) => mockCreateMeeting(...args),
+ updateMeeting: (...args: any[]) => mockUpdateMeeting(...args),
+ cancelMeetingWithReason: (...args: any[]) => mockCancelMeetingWithReason(...args),
+ fetchMeetingCancellation: jest.fn(),
+ acknowledgeMeetingCancellation: jest.fn(),
+ checkMeetingNotesExist: jest.fn().mockResolvedValue(false),
+ fetchAllUserMeetingNotes: (...args: any[]) => mockFetchAllUserMeetingNotes(...args),
+ downloadMeetingNotesFile: jest.fn(),
+ filterMeetingsByType: (...args: any[]) => mockFilterMeetingsByType(...args),
+ checkMeetingLimit: (...args: any[]) => mockCheckMeetingLimit(...args),
+ canCancelMeeting: (...args: any[]) => mockCanCancelMeeting(...args),
+}));
- const mockMeetings = [
- {
- _id: 'meeting-1',
- senderId: 'other-user-id', // Other user is sender
- receiverId: '6873cc50ac4e1d6e1cddf33f', // Test user is receiver
- description: 'Test meeting 1',
- meetingTime: '2025-07-20T10:00:00Z',
- state: 'pending',
- acceptStatus: false,
- meetingLink: null
- },
- {
- _id: 'meeting-2',
- senderId: 'other-user-id',
- receiverId: '6873cc50ac4e1d6e1cddf33f',
- description: 'Test meeting 2',
- meetingTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Tomorrow
- state: 'accepted',
- acceptStatus: true,
- meetingLink: 'https://meet.example.com/room123'
- },
- {
- _id: 'meeting-3',
- senderId: '6873cc50ac4e1d6e1cddf33f',
- receiverId: 'other-user-id',
- description: 'Past meeting',
- meetingTime: '2025-07-10T10:00:00Z',
- state: 'completed',
- acceptStatus: true,
- meetingLink: null
- }
- ];
-
- const mockChatRoom = {
- _id: '6873cdd898892472c9621cf1',
- participants: ['6873cc50ac4e1d6e1cddf33f', 'other-user-id']
- };
+const mockFetchChatRoom = jest.fn();
+const mockFetchUserProfile = jest.fn();
- const mockUserProfile = {
- _id: 'other-user-id',
- firstName: 'John',
- lastName: 'Doe',
- avatar: 'https://example.com/avatar.jpg'
- };
+jest.mock('@/services/chatApiServices', () => ({
+ fetchChatRoom: (...args: any[]) => mockFetchChatRoom(...args),
+ fetchUserProfile: (...args: any[]) => mockFetchUserProfile(...args),
+}));
+
+jest.mock('@/services/sessionApiServices', () => ({
+ invalidateUsersCaches: jest.fn(),
+}));
+
+jest.mock('@/services/debouncedApiService', () => ({
+ debouncedApiService: {
+ fetchUserProfile: jest.fn(),
+ makeRequest: jest.fn(),
+ },
+}));
- // Get the mocked functions
- const mockFetchMeetings = require('@/services/meetingApiServices').fetchMeetings as jest.MockedFunction
;
- const mockCreateMeeting = require('@/services/meetingApiServices').createMeeting as jest.MockedFunction;
- const mockUpdateMeeting = require('@/services/meetingApiServices').updateMeeting as jest.MockedFunction;
- const mockInvalidateUsersCaches = require('@/services/sessionApiServices').invalidateUsersCaches as jest.MockedFunction;
- const mockMakeRequest = require('@/services/debouncedApiService').debouncedApiService.makeRequest as jest.MockedFunction;
- const mockInvalidate = require('@/services/debouncedApiService').debouncedApiService.invalidate as jest.MockedFunction;
- const mockRouter = require('next/navigation').useRouter();
- const mockPush = mockRouter.push as jest.MockedFunction;
+// Mock data
+const mockChatRoom = {
+ _id: 'chat-room-1',
+ participants: ['user1', 'user2'],
+ messages: []
+};
+
+const mockUserProfile = {
+ _id: 'user2',
+ firstName: 'John',
+ lastName: 'Doe',
+ avatar: '/avatar.png'
+};
+
+const mockMeetings = [
+ {
+ _id: 'meeting-1',
+ description: 'Frontend Development Discussion',
+ meetingTime: new Date('2025-07-20T14:00:00Z').toISOString(),
+ state: 'pending',
+ senderId: 'user2',
+ receiverId: 'user1',
+ createdAt: new Date('2025-07-18T10:00:00Z').toISOString()
+ },
+ {
+ _id: 'meeting-2',
+ description: 'Project Planning',
+ meetingTime: new Date('2025-07-22T16:00:00Z').toISOString(),
+ state: 'accepted',
+ senderId: 'user1',
+ receiverId: 'user2',
+ createdAt: new Date('2025-07-17T10:00:00Z').toISOString()
+ }
+];
+
+const mockFilteredMeetings = {
+ pendingRequests: [mockMeetings[0]],
+ upcomingMeetings: [mockMeetings[1]],
+ pastMeetings: [],
+ cancelledMeetings: []
+};
+
+const mockSavedNotes = [
+ {
+ _id: 'note-1',
+ meetingId: 'meeting-3',
+ title: 'Meeting Notes 1',
+ content: 'Content of the first note',
+ tags: ['development'],
+ wordCount: 100,
+ lastModified: new Date('2025-07-15T15:00:00Z').toISOString(),
+ createdAt: new Date('2025-07-15T15:00:00Z').toISOString(),
+ isPrivate: false
+ }
+];
+
+// Default props
+const defaultProps = {
+ chatRoomId: 'chat-room-1',
+ userId: 'user1',
+ onClose: jest.fn(),
+ onMeetingUpdate: jest.fn()
+};
+describe('MeetingBox Component', () => {
beforeEach(() => {
jest.clearAllMocks();
- // Mock chat room fetch
- (global.fetch as jest.Mock).mockImplementation((url: string) => {
- if (url.includes('/api/chatrooms')) {
- return Promise.resolve({
- ok: true,
- json: () => Promise.resolve({
- success: true,
- chatRooms: [mockChatRoom]
- })
- });
- }
-
- if (url.includes('/api/users/profile')) {
- return Promise.resolve({
- ok: true,
- json: () => Promise.resolve({
- success: true,
- user: mockUserProfile
- })
- });
- }
-
- if (url.includes('/api/meeting/cancel')) {
- return Promise.resolve({
- ok: true,
- json: () => Promise.resolve({
- meeting: { ...mockMeetings[0], state: 'cancelled' }
- })
- });
- }
-
- if (url.includes('/api/meeting-notes')) {
- return Promise.resolve({
- ok: true,
- json: () => Promise.resolve({
- _id: 'notes-1',
- content: 'Test meeting notes content',
- title: 'Meeting Notes',
- userName: 'Test User',
- createdAt: '2025-07-10T10:00:00Z'
- })
- });
- }
-
- return Promise.resolve({
- ok: false,
- json: () => Promise.resolve({ error: 'Not found' })
- });
- });
-
- // Mock API services
+ // Setup default mocks
+ mockFetchChatRoom.mockResolvedValue(mockChatRoom);
+ mockFetchUserProfile.mockResolvedValue(mockUserProfile);
mockFetchMeetings.mockResolvedValue(mockMeetings);
- mockCreateMeeting.mockResolvedValue({
- _id: 'new-meeting-id',
- senderId: '6873cc50ac4e1d6e1cddf33f',
- receiverId: 'other-user-id',
- description: 'New test meeting',
- meetingTime: '2025-07-30T15:00:00Z',
- state: 'pending'
- });
- mockUpdateMeeting.mockResolvedValue({
- ...mockMeetings[0],
- state: 'accepted'
- });
-
- // Mock debounced API service
- mockMakeRequest.mockImplementation((key: string, fn: () => any) => fn());
+ mockFilterMeetingsByType.mockReturnValue(mockFilteredMeetings);
+ mockFetchAllUserMeetingNotes.mockResolvedValue(mockSavedNotes);
+ mockCanCancelMeeting.mockReturnValue({ canCancel: true, reason: null });
+ mockCheckMeetingLimit.mockReturnValue(0);
});
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- describe('Initial Rendering', () => {
- it('should render the meeting box with header and schedule button', async () => {
- render();
+ describe('Rendering', () => {
+ it('should render loading state initially', async () => {
+ mockFetchMeetings.mockImplementation(() => new Promise(() => {})); // Never resolve
- await waitFor(() => {
- expect(screen.getByText('Meetings')).toBeInTheDocument();
- expect(screen.getByText('New')).toBeInTheDocument();
- });
- });
-
- it('should show loading state initially', () => {
- render();
+ render();
expect(screen.getByText('Loading meetings...')).toBeInTheDocument();
});
- it('should fetch chat room data and meetings on mount', async () => {
- render();
+ it('should render the main meeting interface after loading', async () => {
+ render();
await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledWith(
- expect.stringContaining('/api/chatrooms?chatRoomId=6873cdd898892472c9621cf1')
- );
- expect(mockFetchMeetings).toHaveBeenCalledWith(
- '6873cc50ac4e1d6e1cddf33f',
- 'other-user-id'
- );
+ expect(screen.getByText('Meetings')).toBeInTheDocument();
});
- });
- });
-
- describe('Meeting Display and Categorization', () => {
- it('should categorize and display meetings correctly', async () => {
- render();
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- // Should show different meeting categories
- expect(screen.getByText('Pending Requests (1)')).toBeInTheDocument();
- expect(screen.getByText('Upcoming Meetings (1)')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
+ expect(screen.getByTestId('meeting-list')).toBeInTheDocument();
});
- it('should show empty state when no meetings exist', async () => {
- mockFetchMeetings.mockResolvedValueOnce([]);
-
- render();
+ it('should render saved notes section', async () => {
+ render();
await waitFor(() => {
- expect(screen.getByText('No meetings scheduled')).toBeInTheDocument();
- expect(screen.getByText('Schedule Meeting')).toBeInTheDocument();
+ expect(screen.getByText(/saved meeting notes/i)).toBeInTheDocument();
});
});
});
- describe('Meeting Creation', () => {
- it('should open create meeting modal when schedule button is clicked', async () => {
- // Use meetings with no active count to allow modal opening
- mockFetchMeetings.mockResolvedValueOnce([mockMeetings[2]]); // Only past meeting
-
- render();
+ describe('Data Fetching', () => {
+ it('should fetch chat room and meetings on mount', async () => {
+ render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- const scheduleButton = screen.getByText('New');
-
- act(() => {
- fireEvent.click(scheduleButton);
+ expect(mockFetchChatRoom).toHaveBeenCalledWith(defaultProps.chatRoomId);
});
+ // Give more time for fetchMeetings to be called
await waitFor(() => {
- expect(screen.getByTestId('create-meeting-modal')).toBeInTheDocument();
- });
+ expect(mockFetchMeetings).toHaveBeenCalled();
+ }, { timeout: 3000 });
});
- it('should create a new meeting successfully', async () => {
- // Use meetings with no active count to allow modal opening
- mockFetchMeetings.mockResolvedValueOnce([mockMeetings[2]]); // Only past meeting
-
- render();
+ it('should fetch saved notes when component loads', async () => {
+ render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- // Open create modal
- const scheduleButton = screen.getByText('New');
-
- act(() => {
- fireEvent.click(scheduleButton);
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('create-meeting-modal')).toBeInTheDocument();
- });
-
- // Submit meeting creation
- const createButton = screen.getByTestId('create-meeting-submit');
- fireEvent.click(createButton);
-
- await waitFor(() => {
- expect(mockCreateMeeting).toHaveBeenCalledWith({
- senderId: '6873cc50ac4e1d6e1cddf33f',
- receiverId: 'other-user-id',
- description: 'Test meeting description',
- meetingTime: new Date('2025-07-15T10:00')
- });
+ expect(mockFetchAllUserMeetingNotes).toHaveBeenCalled();
});
});
+ });
- it('should show error when meeting limit is reached', async () => {
- // Mock meetings with 2 active meetings
- const activeMeetings = [
- { ...mockMeetings[0], state: 'pending' },
- { ...mockMeetings[1], state: 'accepted', meetingTime: '2025-07-30T10:00:00Z' }
- ];
- mockFetchMeetings.mockResolvedValueOnce(activeMeetings);
-
- render();
+ describe('Meeting Management', () => {
+ it('should open create meeting modal when new button is clicked', async () => {
+ render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
});
-
- const scheduleButton = screen.getByText('New');
- fireEvent.click(scheduleButton);
- await waitFor(() => {
- expect(screen.getByTestId('alert')).toBeInTheDocument();
- expect(screen.getByText('Meeting Limit Reached')).toBeInTheDocument();
- });
+ fireEvent.click(screen.getByRole('button', { name: /new/i }));
+
+ expect(screen.getByTestId('create-meeting-modal')).toBeInTheDocument();
});
- });
- describe('Meeting Actions', () => {
- it('should accept a meeting request', async () => {
- render();
+ it('should handle meeting creation', async () => {
+ const newMeeting = {
+ _id: 'new-meeting-id',
+ description: 'Test Meeting',
+ meetingTime: new Date('2025-07-20T14:00:00Z').toISOString(),
+ state: 'pending',
+ senderId: 'user1',
+ receiverId: 'user2'
+ };
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
+ mockCreateMeeting.mockResolvedValue(newMeeting);
- // Find and click accept button for pending meeting
- const acceptButton = screen.getByText('Accept');
- fireEvent.click(acceptButton);
+ render();
- // Confirm the action
await waitFor(() => {
- expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /new/i }));
});
- const confirmButton = screen.getByTestId('confirmation-confirm');
- fireEvent.click(confirmButton);
+ fireEvent.click(screen.getByText('Create Meeting'));
await waitFor(() => {
- expect(mockUpdateMeeting).toHaveBeenCalledWith('meeting-1', 'accept');
+ expect(mockCreateMeeting).toHaveBeenCalledWith({
+ senderId: 'user1',
+ receiverId: 'user2',
+ description: 'Test Meeting',
+ meetingTime: expect.any(Date)
+ });
});
});
- it('should decline a meeting request', async () => {
- render();
+ it('should handle meeting acceptance with confirmation', async () => {
+ const updatedMeeting = {
+ ...mockMeetings[0],
+ state: 'accepted'
+ };
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- const declineButton = screen.getByText('Decline');
- fireEvent.click(declineButton);
-
- await waitFor(() => {
- expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
- expect(screen.getByText('Decline Meeting')).toBeInTheDocument();
- });
+ mockUpdateMeeting.mockResolvedValue(updatedMeeting);
- const confirmButton = screen.getByTestId('confirmation-confirm');
- fireEvent.click(confirmButton);
+ render();
await waitFor(() => {
- expect(mockUpdateMeeting).toHaveBeenCalledWith('meeting-1', 'reject');
+ expect(screen.getByTestId('meeting-list')).toBeInTheDocument();
});
- });
-
- it('should cancel a meeting with reason', async () => {
- render();
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- // Find cancel button (should be on upcoming meeting)
- const cancelButton = screen.getByText('Cancel');
- fireEvent.click(cancelButton);
+ fireEvent.click(screen.getByText('Accept'));
- await waitFor(() => {
- expect(screen.getByTestId('cancel-meeting-modal')).toBeInTheDocument();
- });
+ // Should show confirmation dialog
+ expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
+ expect(screen.getByText('Accept Meeting')).toBeInTheDocument();
- const submitCancelButton = screen.getByTestId('cancel-meeting-submit');
- fireEvent.click(submitCancelButton);
+ fireEvent.click(screen.getByTestId('confirm-button'));
await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledWith('/api/meeting/cancel',
- expect.objectContaining({
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: expect.stringContaining('meeting-2')
- })
- );
+ expect(mockUpdateMeeting).toHaveBeenCalledWith('meeting-1', 'accept');
});
});
- });
- describe('Meeting Navigation', () => {
- it('should navigate to meeting room when join button is clicked', async () => {
- render();
+ it('should handle meeting cancellation', async () => {
+ const cancelledMeeting = {
+ ...mockMeetings[1],
+ state: 'cancelled'
+ };
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- const joinButton = screen.getByText('Join Meeting');
- expect(joinButton).toBeInTheDocument();
-
- fireEvent.click(joinButton);
+ mockCancelMeetingWithReason.mockResolvedValue(cancelledMeeting);
- expect(mockPush).toHaveBeenCalledWith('/meeting/meeting-2');
- });
- });
-
- describe('Meeting Notes Functionality', () => {
- it('should download meeting notes for completed meetings', async () => {
- render();
+ render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ expect(screen.getByTestId('meeting-list')).toBeInTheDocument();
});
-
- // Click on past meetings to see download option
- const pastMeetingsButton = screen.getByText('Past Meetings (1)');
- fireEvent.click(pastMeetingsButton);
- await waitFor(() => {
- const downloadButton = screen.getByText('Download Notes');
- fireEvent.click(downloadButton);
- });
-
- // Verify notes API was called
- await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledWith(
- expect.stringContaining('/api/meeting-notes?meetingId=meeting-3')
- );
- });
- });
- });
-
- describe('View Toggles', () => {
- it('should toggle past meetings view', async () => {
- render();
+ fireEvent.click(screen.getByText('Cancel'));
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- const pastMeetingsButton = screen.getByText('Past Meetings (1)');
- fireEvent.click(pastMeetingsButton);
+ expect(screen.getByTestId('cancel-meeting-modal')).toBeInTheDocument();
- // Should show past meetings section
- expect(screen.getByText('Past meeting')).toBeInTheDocument();
- });
-
- it('should toggle cancelled meetings view', async () => {
- // Add a cancelled meeting to the mock data
- const meetingsWithCancelled = [
- ...mockMeetings,
- {
- _id: 'meeting-4',
- senderId: '6873cc50ac4e1d6e1cddf33f',
- receiverId: 'other-user-id',
- description: 'Cancelled meeting',
- meetingTime: '2025-07-18T10:00:00Z',
- state: 'cancelled',
- acceptStatus: false,
- meetingLink: null
- }
- ];
- mockFetchMeetings.mockResolvedValueOnce(meetingsWithCancelled);
-
- render();
+ fireEvent.click(screen.getByText('Cancel Meeting'));
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ expect(mockCancelMeetingWithReason).toHaveBeenCalledWith('meeting-2', 'user1', 'Test cancellation reason');
});
-
- const cancelledMeetingsButton = screen.getByText('Cancelled Meetings (1)');
- fireEvent.click(cancelledMeetingsButton);
-
- expect(screen.getByText('Cancelled meeting')).toBeInTheDocument();
});
});
describe('Error Handling', () => {
- it('should handle meeting creation errors', async () => {
- mockCreateMeeting.mockRejectedValueOnce(new Error('Failed to create meeting'));
- // Use meetings with no active count to allow modal opening
- mockFetchMeetings.mockResolvedValueOnce([mockMeetings[2]]); // Only past meeting
+ it('should show error alert when meeting creation fails', async () => {
+ mockCreateMeeting.mockRejectedValue(new Error('Failed to create meeting'));
- render();
+ render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /new/i }));
});
-
- const scheduleButton = screen.getByText('New');
- act(() => {
- fireEvent.click(scheduleButton);
- });
-
- await waitFor(() => {
- expect(screen.getByTestId('create-meeting-modal')).toBeInTheDocument();
- });
-
- const createButton = screen.getByTestId('create-meeting-submit');
- fireEvent.click(createButton);
+ fireEvent.click(screen.getByText('Create Meeting'));
await waitFor(() => {
- expect(screen.getByTestId('alert')).toBeInTheDocument();
+ expect(screen.getByTestId('alert-error')).toBeInTheDocument();
expect(screen.getByText('Failed to create meeting')).toBeInTheDocument();
});
});
+ });
- it('should handle meeting update errors', async () => {
- mockUpdateMeeting.mockRejectedValueOnce(new Error('Update failed'));
-
- render();
-
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
- });
-
- const acceptButton = screen.getByText('Accept');
- fireEvent.click(acceptButton);
+ describe('Saved Notes', () => {
+ it('should toggle saved notes section', async () => {
+ render();
await waitFor(() => {
- expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
+ expect(screen.getByText(/saved meeting notes/i)).toBeInTheDocument();
});
- const confirmButton = screen.getByTestId('confirmation-confirm');
- fireEvent.click(confirmButton);
+ const toggleButton = screen.getByText(/saved meeting notes/i);
+ fireEvent.click(toggleButton);
await waitFor(() => {
- expect(screen.getByTestId('alert')).toBeInTheDocument();
- expect(screen.getByText('Failed to accept meeting')).toBeInTheDocument();
+ expect(screen.getByTestId('saved-notes-list')).toBeInTheDocument();
});
});
});
- describe('Component Cleanup', () => {
- it('should call onMeetingUpdate when meetings change', async () => {
- render();
+ describe('Callback Functions', () => {
+ it('should call onMeetingUpdate when meetings are loaded', async () => {
+ render();
await waitFor(() => {
- expect(mockProps.onMeetingUpdate).toHaveBeenCalled();
+ expect(defaultProps.onMeetingUpdate).toHaveBeenCalled();
});
});
});
- describe('Alert and Confirmation Management', () => {
- it('should close alert when close button is clicked', async () => {
- mockCreateMeeting.mockRejectedValueOnce(new Error('Test error'));
- // Use meetings with no active count to allow modal opening
- mockFetchMeetings.mockResolvedValueOnce([mockMeetings[2]]); // Only past meeting
-
- render();
-
- await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ describe('Edge Cases', () => {
+ it('should handle empty meetings list', async () => {
+ mockFetchMeetings.mockResolvedValue([]);
+ mockFilterMeetingsByType.mockReturnValue({
+ pendingRequests: [],
+ upcomingMeetings: [],
+ pastMeetings: [],
+ cancelledMeetings: []
});
-
- // Trigger an error to show alert
- const scheduleButton = screen.getByText('New');
- act(() => {
- fireEvent.click(scheduleButton);
- });
+ render();
await waitFor(() => {
- expect(screen.getByTestId('create-meeting-modal')).toBeInTheDocument();
+ expect(screen.getByTestId('meeting-list')).toBeInTheDocument();
+ expect(screen.getByTestId('pending-requests-count')).toHaveTextContent('0');
+ expect(screen.getByTestId('upcoming-meetings-count')).toHaveTextContent('0');
});
+ });
- const createButton = screen.getByTestId('create-meeting-submit');
- fireEvent.click(createButton);
+ it('should handle meeting limit restriction', async () => {
+ mockCheckMeetingLimit.mockReturnValue(2); // At limit
- await waitFor(() => {
- expect(screen.getByTestId('alert')).toBeInTheDocument();
- });
-
- const closeAlertButton = screen.getByTestId('alert-close');
- fireEvent.click(closeAlertButton);
+ render();
await waitFor(() => {
- expect(screen.queryByTestId('alert')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /new/i }));
});
- });
-
- it('should close confirmation dialog when cancel is clicked', async () => {
- render();
await waitFor(() => {
- expect(screen.queryByText('Loading meetings...')).not.toBeInTheDocument();
+ expect(screen.getByTestId('alert-warning')).toBeInTheDocument();
+ // The exact text from the component
+ expect(screen.getByText(/maximum of 2 active meetings/i)).toBeInTheDocument();
});
+ });
- const acceptButton = screen.getByText('Accept');
- fireEvent.click(acceptButton);
-
- await waitFor(() => {
- expect(screen.getByTestId('confirmation-dialog')).toBeInTheDocument();
+ it('should handle cancellation when meeting cannot be cancelled', async () => {
+ mockCanCancelMeeting.mockReturnValue({
+ canCancel: false,
+ reason: 'Meeting is too close to start time'
});
- const cancelButton = screen.getByTestId('confirmation-cancel');
- fireEvent.click(cancelButton);
+ render();
await waitFor(() => {
- expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument();
+ fireEvent.click(screen.getByText('Cancel'));
});
+
+ expect(screen.getByTestId('alert-warning')).toBeInTheDocument();
+ expect(screen.getByText('Meeting is too close to start time')).toBeInTheDocument();
});
});
-});
+});
\ No newline at end of file
diff --git a/__tests__/components/SessionBox.test.tsx b/__tests__/components/SessionBox.test.tsx
index b0d6880a..6e978068 100644
--- a/__tests__/components/SessionBox.test.tsx
+++ b/__tests__/components/SessionBox.test.tsx
@@ -6,6 +6,7 @@ import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import SessionBox from '@/components/messageSystem/SessionBox';
+import { useSessionActions } from '@/hooks/useSessionActions';
// Mock Next.js router
const mockPush = jest.fn();
@@ -87,6 +88,11 @@ jest.mock('@/utils/avatarUtils', () => ({
processAvatarUrl: jest.fn((url) => url || '/default-avatar.png'),
}));
+// Mock the useSessionActions hook
+jest.mock('@/hooks/useSessionActions', () => ({
+ useSessionActions: jest.fn(),
+}));
+
// Mock data
const mockUser = {
_id: '6873cc50ac4e1d6e1cddf33f',
@@ -110,8 +116,8 @@ const mockSession = {
_id: 'session-id-1',
user1Id: mockUser,
user2Id: mockOtherUser,
- skill1Id: { _id: 'skill1', name: 'React' },
- skill2Id: { _id: 'skill2', name: 'Node.js' },
+ skill1Id: { _id: 'skill1', skillName: 'React' },
+ skill2Id: { _id: 'skill2', skillName: 'Node.js' },
descriptionOfService1: 'I can teach React basics',
descriptionOfService2: 'I want to learn Node.js',
startDate: '2025-01-15',
@@ -197,9 +203,33 @@ describe('SessionBox Component', () => {
onSessionUpdate: jest.fn()
};
+ // Default mock implementation for useSessionActions
+ const defaultMockSessionActions = {
+ sessions: [],
+ counterOffers: {},
+ loading: false,
+ processingSession: null,
+ pendingSessionCount: 0,
+ activeSessionCount: 0,
+ fetchSessions: jest.fn(),
+ handleAcceptReject: jest.fn(),
+ handleDeleteSession: jest.fn(),
+ handleCounterOfferResponse: jest.fn(),
+ handleRequestCompletion: jest.fn(),
+ handleCompletionResponse: jest.fn(),
+ handleRatingSubmit: jest.fn(),
+ setLoading: jest.fn(),
+ setSessions: jest.fn(),
+ setCounterOffers: jest.fn(),
+ setPendingSessionCount: jest.fn(),
+ setActiveSessionCount: jest.fn()
+ };
+
beforeEach(() => {
jest.clearAllMocks();
global.fetch = createMockFetch({});
+ // Reset to default mock implementation
+ (useSessionActions as jest.MockedFunction).mockReturnValue(defaultMockSessionActions);
});
afterEach(() => {
@@ -210,6 +240,12 @@ describe('SessionBox Component', () => {
it('should show New Session button', async () => {
global.fetch = createMockFetch({ sessions: [] });
+ // Mock the hook to return empty sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: []
+ });
+
render();
await waitFor(() => {
@@ -220,6 +256,12 @@ describe('SessionBox Component', () => {
it('should show empty state when no sessions exist', async () => {
global.fetch = createMockFetch({ sessions: [] });
+ // Mock the hook to return empty sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: []
+ });
+
render();
await waitFor(() => {
@@ -236,11 +278,19 @@ describe('SessionBox Component', () => {
counterOffers: []
});
+ // Mock the hook to return the session
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: [mockSession],
+ pendingSessionCount: 1,
+ activeSessionCount: 1
+ });
+
render();
await waitFor(() => {
- expect(screen.getByText('I can teach React basics')).toBeInTheDocument();
- expect(screen.getByText('I want to learn Node.js')).toBeInTheDocument();
+ expect(screen.getByText('React')).toBeInTheDocument();
+ expect(screen.getByText('Node.js')).toBeInTheDocument();
expect(screen.getByText('pending')).toBeInTheDocument();
});
});
@@ -257,6 +307,14 @@ describe('SessionBox Component', () => {
counterOffers: []
});
+ // Mock the hook to return multiple sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: multiplePendingSessions,
+ pendingSessionCount: 3,
+ activeSessionCount: 3
+ });
+
render();
await waitFor(() => {
@@ -269,6 +327,12 @@ describe('SessionBox Component', () => {
it('should open create session modal when New Session button is clicked', async () => {
global.fetch = createMockFetch({ sessions: [] });
+ // Mock the hook to return empty sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: []
+ });
+
render();
await waitFor(() => {
@@ -291,6 +355,14 @@ describe('SessionBox Component', () => {
counterOffers: []
});
+ // Mock the hook to return 3 active sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: multiplePendingSessions,
+ pendingSessionCount: 3,
+ activeSessionCount: 3
+ });
+
render();
await waitFor(() => {
@@ -322,7 +394,7 @@ describe('SessionBox Component', () => {
render();
await waitFor(() => {
- expect(screen.getByText('Failed to load user information')).toBeInTheDocument();
+ expect(screen.getByText('Failed to load user')).toBeInTheDocument();
});
});
@@ -351,6 +423,12 @@ describe('SessionBox Component', () => {
it('should close create session modal when close button is clicked', async () => {
global.fetch = createMockFetch({ sessions: [] });
+ // Mock the hook to return empty sessions
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: []
+ });
+
render();
await waitFor(() => {
@@ -374,6 +452,14 @@ describe('SessionBox Component', () => {
counterOffers: []
});
+ // Mock the hook to return the session
+ (useSessionActions as jest.MockedFunction).mockReturnValue({
+ ...defaultMockSessionActions,
+ sessions: [mockSession],
+ pendingSessionCount: 1,
+ activeSessionCount: 1
+ });
+
render();
await waitFor(() => {
diff --git a/__tests__/pages/SessionPage.test..tsx b/__tests__/pages/SessionPage.test..tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/__tests__/pages/SessionWorkspace.test.tsx b/__tests__/pages/SessionWorkspace.test.tsx
new file mode 100644
index 00000000..86cdfa3d
--- /dev/null
+++ b/__tests__/pages/SessionWorkspace.test.tsx
@@ -0,0 +1,603 @@
+/**
+ * SessionWorkspace Page Component Tests
+ */
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { useParams, useRouter } from 'next/navigation';
+import SessionWorkspace from '@/app/session/[sessionId]/page';
+
+// Mock Next.js navigation
+const mockPush = jest.fn();
+const mockBack = jest.fn();
+
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: mockPush,
+ back: mockBack,
+ replace: jest.fn(),
+ prefetch: jest.fn(),
+ forward: jest.fn(),
+ refresh: jest.fn(),
+ }),
+ useParams: jest.fn(),
+}));
+
+// Mock AuthContext
+const mockUser = {
+ _id: 'user1',
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john.doe@example.com',
+ title: 'Developer'
+};
+
+jest.mock('@/lib/context/AuthContext', () => ({
+ useAuth: () => ({
+ user: mockUser,
+ loading: false,
+ logout: jest.fn(),
+ }),
+}));
+
+// Mock tab components
+jest.mock('@/components/sessionTabs/OverviewTab', () => {
+ return function MockOverviewTab({
+ session,
+ setActiveTab,
+ onSessionUpdate,
+ showAlert
+ }: any) {
+ return (
+
+
Overview Tab
+
{session?.status}
+
+
+
+
+ );
+ };
+});
+
+jest.mock('@/components/sessionTabs/SubmitWorkTab', () => {
+ return function MockSubmitWorkTab({ session, showAlert }: any) {
+ return (
+
+
Submit Work Tab
+
+
+ );
+ };
+});
+
+jest.mock('@/components/sessionTabs/ViewWorksTab', () => {
+ return function MockViewWorksTab({ onWorkUpdate, showAlert }: any) {
+ return (
+
+
View Works Tab
+
+
+
+ );
+ };
+});
+
+jest.mock('@/components/sessionTabs/ProgressTab', () => {
+ return function MockProgressTab({ showAlert }: any) {
+ return (
+
+
Progress Tab
+
+
+ );
+ };
+});
+
+jest.mock('@/components/sessionTabs/ReportTab', () => {
+ return function MockReportTab({ showAlert }: any) {
+ return (
+
+
Report Tab
+
+
+ );
+ };
+});
+
+// Mock UI Components
+jest.mock('@/components/ui/Alert', () => {
+ return function MockAlert({ isOpen, type, message, title, onClose }: any) {
+ return isOpen ? (
+
+ {title &&
{title}
}
+
{message}
+
+
+ ) : null;
+ };
+});
+
+jest.mock('@/components/ui/ConfirmationDialog', () => {
+ return function MockConfirmationDialog({
+ isOpen,
+ title,
+ message,
+ onConfirm,
+ onClose,
+ confirmText
+ }: any) {
+ return isOpen ? (
+
+
{title}
+
{message}
+
+
+
+ ) : null;
+ };
+});
+
+// Mock data
+const mockActiveSession = {
+ _id: 'session-123',
+ status: 'active',
+ startDate: '2025-07-15T10:00:00.000Z',
+ expectedEndDate: '2025-07-25T10:00:00.000Z',
+ user1Id: {
+ _id: 'user1',
+ firstName: 'John',
+ lastName: 'Doe',
+ name: 'John Doe'
+ },
+ user2Id: {
+ _id: 'user2',
+ firstName: 'Jane',
+ lastName: 'Smith',
+ name: 'Jane Smith'
+ },
+ skill1Id: {
+ _id: 'skill1',
+ name: 'Frontend Development'
+ },
+ skill2Id: {
+ _id: 'skill2',
+ name: 'Backend Development'
+ },
+ descriptionOfService1: 'I will help with React and TypeScript development',
+ descriptionOfService2: 'I will help with Node.js and database design'
+};
+
+const mockCompletedSession = {
+ ...mockActiveSession,
+ status: 'completed'
+};
+
+const mockCancelledSession = {
+ ...mockActiveSession,
+ status: 'canceled'
+};
+
+const mockWorks = [
+ {
+ _id: 'work-1',
+ submitUser: { _id: 'user1', firstName: 'John', lastName: 'Doe' },
+ receiveUser: { _id: 'user2', firstName: 'Jane', lastName: 'Smith' },
+ title: 'Frontend Component',
+ description: 'React component for user dashboard',
+ acceptanceStatus: 'accepted',
+ submittedAt: '2025-07-16T10:00:00.000Z'
+ },
+ {
+ _id: 'work-2',
+ submitUser: { _id: 'user2', firstName: 'Jane', lastName: 'Smith' },
+ receiveUser: { _id: 'user1', firstName: 'John', lastName: 'Doe' },
+ title: 'API Endpoint',
+ description: 'User authentication API',
+ acceptanceStatus: 'pending',
+ submittedAt: '2025-07-17T10:00:00.000Z'
+ }
+];
+
+const mockProgress = [
+ {
+ _id: 'progress-1',
+ userId: 'user1',
+ status: 'in_progress',
+ completionPercentage: 75,
+ notes: 'Working on the final components',
+ dueDate: '2025-07-22T10:00:00.000Z'
+ },
+ {
+ _id: 'progress-2',
+ userId: 'user2',
+ status: 'in_progress',
+ completionPercentage: 60,
+ notes: 'Database schema complete',
+ dueDate: '2025-07-23T10:00:00.000Z'
+ }
+];
+
+const mockOtherUserDetails = {
+ _id: 'user2',
+ firstName: 'Jane',
+ lastName: 'Smith',
+ email: 'jane.smith@example.com',
+ title: 'Backend Developer'
+};
+
+// Global fetch mock
+const mockFetch = jest.fn();
+global.fetch = mockFetch;
+
+describe('SessionWorkspace Page', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Mock useParams to return sessionId
+ (useParams as jest.Mock).mockReturnValue({ sessionId: 'session-123' });
+
+ // Setup default fetch responses
+ mockFetch.mockImplementation((url: string) => {
+ if (url.includes('/api/session/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ session: mockActiveSession
+ })
+ });
+ }
+
+ if (url.includes('/api/work/session/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ works: mockWorks
+ })
+ });
+ }
+
+ if (url.includes('/api/session-progress/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ progress: mockProgress
+ })
+ });
+ }
+
+ if (url.includes('/api/users/profile?id=user2')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ user: mockOtherUserDetails
+ })
+ });
+ }
+
+ if (url.includes('/api/notification')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ success: true })
+ });
+ }
+
+ // Default fallback
+ return Promise.resolve({
+ ok: false,
+ json: () => Promise.resolve({ success: false, message: 'Not found' })
+ });
+ });
+ });
+
+ describe('Rendering', () => {
+ it('should render session not found when session is null', async () => {
+ mockFetch.mockImplementation((url: string) => {
+ if (url.includes('/api/session/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: false,
+ session: null
+ })
+ });
+ }
+ return Promise.reject(new Error('Unhandled fetch'));
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Session Not Found')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Go Back')).toBeInTheDocument();
+ });
+ });
+
+
+
+ describe('Tab Navigation', () => {
+ beforeEach(async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('overview-tab')).toBeInTheDocument();
+ });
+ });
+
+ it('should render all tab buttons', () => {
+ expect(screen.getAllByText('Overview')).toHaveLength(2); // Desktop and mobile versions
+ expect(screen.getAllByText('Submit Work')).toHaveLength(2);
+ expect(screen.getAllByText('View Works')).toHaveLength(2);
+ expect(screen.getAllByText('Progress')).toHaveLength(2);
+ expect(screen.getAllByText('Report Issue')).toHaveLength(2);
+ });
+
+ it('should switch to submit work tab', async () => {
+ // Use getByRole to target the button specifically
+ const submitWorkButtons = screen.getAllByRole('button');
+ const submitWorkTab = submitWorkButtons.find(button =>
+ button.textContent?.includes('Submit Work')
+ );
+
+ if (submitWorkTab) {
+ fireEvent.click(submitWorkTab);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('submit-work-tab')).toBeInTheDocument();
+ });
+ }
+ });
+
+ it('should switch to view works tab', async () => {
+ const viewWorksButtons = screen.getAllByRole('button');
+ const viewWorksTab = viewWorksButtons.find(button =>
+ button.textContent?.includes('View Works')
+ );
+
+ if (viewWorksTab) {
+ fireEvent.click(viewWorksTab);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('view-works-tab')).toBeInTheDocument();
+ });
+ }
+ });
+
+ it('should switch to progress tab', async () => {
+ const progressButtons = screen.getAllByRole('button');
+ const progressTab = progressButtons.find(button =>
+ button.textContent?.includes('Progress') && !button.textContent?.includes('Update')
+ );
+
+ if (progressTab) {
+ fireEvent.click(progressTab);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('progress-tab')).toBeInTheDocument();
+ });
+ }
+ });
+
+ it('should switch to report tab', async () => {
+ const reportButtons = screen.getAllByRole('button');
+ const reportTab = reportButtons.find(button =>
+ button.textContent?.includes('Report Issue')
+ );
+
+ if (reportTab) {
+ fireEvent.click(reportTab);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('report-tab')).toBeInTheDocument();
+ });
+ }
+ });
+ });
+
+ describe('Data Fetching', () => {
+ it('should fetch session data on mount', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith('/api/session/session-123');
+ });
+ });
+
+ it('should fetch works and progress when user is available', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith('/api/work/session/session-123');
+ expect(mockFetch).toHaveBeenCalledWith('/api/session-progress/session-123');
+ });
+ });
+
+ it('should fetch other user details when session is loaded', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith('/api/users/profile?id=user2');
+ });
+ });
+
+ it('should handle session refresh', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('overview-tab')).toBeInTheDocument();
+ });
+
+ const refreshButton = screen.getByText('Refresh Session');
+ fireEvent.click(refreshButton);
+
+ // Should call fetch again for session data
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith('/api/session/session-123');
+ });
+ });
+ });
+
+ describe('Alert System', () => {
+ beforeEach(async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('overview-tab')).toBeInTheDocument();
+ });
+ });
+
+ it('should show alert from tab component', async () => {
+ const alertButton = screen.getByText('Test Alert');
+ fireEvent.click(alertButton);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('alert-success')).toBeInTheDocument();
+ expect(screen.getByText('Test alert from overview')).toBeInTheDocument();
+ });
+ });
+
+ it('should close alert when close button is clicked', async () => {
+ const alertButton = screen.getByText('Test Alert');
+ fireEvent.click(alertButton);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('alert-success')).toBeInTheDocument();
+ });
+
+ const closeButton = screen.getByText('Close Alert');
+ fireEvent.click(closeButton);
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('alert-success')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should handle session fetch error gracefully', async () => {
+ mockFetch.mockRejectedValue(new Error('Network error'));
+
+ render();
+
+ // Should still render but may show loading state or handle error
+ await waitFor(() => {
+ // Component should not crash - check if main content exists
+ expect(document.body).toBeInTheDocument();
+ }, { timeout: 2000 });
+ });
+
+ it('should handle works fetch error gracefully', async () => {
+ mockFetch.mockImplementation((url: string) => {
+ if (url.includes('/api/work/session/session-123')) {
+ return Promise.reject(new Error('Works fetch failed'));
+ }
+ if (url.includes('/api/session/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ session: mockActiveSession
+ })
+ });
+ }
+ return mockFetch(url);
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Session with Jane Smith')).toBeInTheDocument();
+ });
+ });
+
+ it('should handle progress fetch error gracefully', async () => {
+ mockFetch.mockImplementation((url: string) => {
+ if (url.includes('/api/session-progress/session-123')) {
+ return Promise.reject(new Error('Progress fetch failed'));
+ }
+ if (url.includes('/api/session/session-123')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ session: mockActiveSession
+ })
+ });
+ }
+ return mockFetch(url);
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Session with Jane Smith')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Responsive Design', () => {
+ beforeEach(async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('overview-tab')).toBeInTheDocument();
+ });
+ });
+
+ it('should render mobile-friendly header', () => {
+ expect(screen.getByText('Session with Jane Smith')).toBeInTheDocument();
+ expect(screen.getByText('Started:')).toBeInTheDocument();
+ });
+
+ it('should have scrollable tab navigation', () => {
+ // Get the tab navigation container specifically
+ const allNavs = screen.getAllByRole('navigation');
+ const tabNavigation = allNavs.find(nav =>
+ nav.className.includes('mb-4')
+ );
+ expect(tabNavigation).toBeInTheDocument();
+ });
+ });
+
+
+
+ describe('User Display Names', () => {
+ beforeEach(async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('overview-tab')).toBeInTheDocument();
+ });
+ });
+
+ it('should display correct other user name in header', () => {
+ expect(screen.getByText('Session with Jane Smith')).toBeInTheDocument();
+ });
+
+ it('should display session start date', () => {
+ expect(screen.getByText('Started:')).toBeInTheDocument();
+ expect(screen.getByText('Jul 15, 2025')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/app/meeting/[id]/page.tsx b/src/app/meeting/[id]/page.tsx
index fd7200e0..8ac63f49 100644
--- a/src/app/meeting/[id]/page.tsx
+++ b/src/app/meeting/[id]/page.tsx
@@ -106,10 +106,10 @@ export default function MeetingPage() {
if (authLoading || loading) {
return (
-
+
-
-
Loading meeting...
+
+
Loading meeting...
);
@@ -117,16 +117,16 @@ export default function MeetingPage() {
if (error || !meeting || !isAuthorized) {
return (
-
-
+
+
-
-
Meeting Not Available
-
+
+
Meeting Not Available
+
{error || 'You are not authorized to access this meeting.'}
-
@@ -143,28 +143,28 @@ export default function MeetingPage() {
const isTooEarly = timeUntilMeeting > 30 * 60 * 1000; // More than 30 minutes before
return (
-
-
+
+
-
-
Meeting Not Available
+
+
Meeting Not Available
{meeting.state !== 'accepted' && (
-
+
This meeting has not been accepted yet.
)}
{meeting.state === 'accepted' && !meeting.meetingLink && (
-
+
Meeting link is not available yet.
)}
{meeting.state === 'accepted' && meeting.meetingLink && isTooEarly && (
-
-
Meeting starts in:
-
+
+
Meeting starts in:
+
{(() => {
const minutesUntil = Math.floor(timeUntilMeeting / (1000 * 60));
if (minutesUntil >= 60) {
@@ -174,34 +174,34 @@ export default function MeetingPage() {
return `${minutesUntil} minutes`;
})()}
-
+
You can join 30 minutes before the scheduled time.
)}
{meeting.state === 'accepted' && meeting.meetingLink && isPastMeeting && (
-
+
This meeting has ended.
)}
-
-
Meeting Details
-
+
+
Meeting Details
+
With: {otherUser?.firstName} {otherUser?.lastName}
-
+
Scheduled: {meetingTime.toLocaleString()}
-
+
Description: {meeting.description}
-
router.push('/dashboard')} className="w-full bg-[var(--primary)] hover:bg-[var(--primary)]/90">
-
+ router.push('/dashboard')} className="w-full bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-sm md:text-base">
+
Return to Dashboard
diff --git a/src/app/user/notification/page.tsx b/src/app/user/notification/page.tsx
index b8328a8e..694c93f7 100644
--- a/src/app/user/notification/page.tsx
+++ b/src/app/user/notification/page.tsx
@@ -215,7 +215,7 @@ const NotificationPage = () => {
-
+
{user && (
Hi {user.firstName}, here are your notifications:
@@ -223,23 +223,23 @@ const NotificationPage = () => {
)}
{/*UnReaded Notifications Badge*/}
-
-
-
Notifications
+
+
+
Notifications
{unreadNotifications.length > 0 && (
-
+
{unreadNotifications.length} unread
)}
-
+
{notifications.length > 0 && (
-
+
diff --git a/src/components/meetingSystem/DailyMeeting.tsx b/src/components/meetingSystem/DailyMeeting.tsx
index c74ab422..7cecc9eb 100644
--- a/src/components/meetingSystem/DailyMeeting.tsx
+++ b/src/components/meetingSystem/DailyMeeting.tsx
@@ -26,11 +26,9 @@ import {
Users,
Maximize,
Minimize,
- StickyNote,
- HelpCircle
+ StickyNote
} from 'lucide-react';
import { MeetingNotesSidebar } from './MeetingNotesSidebar';
-import { MediaDeviceTips } from './MediaDeviceTips';
interface DailyMeetingProps {
roomUrl: string;
@@ -59,8 +57,22 @@ const videoStyles = `
}
.daily-camera-box {
- min-width: 128px;
- min-height: 96px;
+ min-width: 100px;
+ min-height: 75px;
+ aspect-ratio: 4/3;
+ }
+
+ @media (min-width: 768px) {
+ .daily-camera-box {
+ min-width: 128px;
+ min-height: 96px;
+ }
+ }
+
+ @media (max-width: 767px) {
+ .daily-camera-box {
+ flex-shrink: 0;
+ }
}
`;
@@ -96,7 +108,6 @@ function DailyMeetingInner({
// UI State
const [isFullscreen, setIsFullscreen] = useState(false);
const [showNotes, setShowNotes] = useState(false);
- const [showTips, setShowTips] = useState(false);
const videoContainerRef = useRef
(null);
// Meeting control functions with enhanced error handling
@@ -224,9 +235,9 @@ function DailyMeetingInner({
const screenShareParticipant = screens[0];
return (
-
+
{/* Main screen share area */}
-
+
{/* Camera videos sidebar */}
-
+
{participantIds.map(id => (
))}
@@ -277,37 +288,30 @@ function DailyMeetingInner({
{/* Header */}
-
-
-
-
+
+
+
+
{otherUserName ? `Meeting with ${otherUserName}` : 'Meeting Room'}
{meetingDescription && (
-
{meetingDescription}
+
{meetingDescription}
)}
-
-
+
+
{participantIds.length} participant{participantIds.length !== 1 ? 's' : ''}
-
-
setShowTips(true)}
- title="Camera & Microphone Tips"
- >
-
-
+
- {isFullscreen ? : }
+ {isFullscreen ? : }
@@ -317,17 +321,17 @@ function DailyMeetingInner({
{/* Video Area */}
{screens.length > 0 ? (
renderScreenShare()
) : (
{participantIds.map(renderParticipant)}
@@ -336,52 +340,52 @@ function DailyMeetingInner({
{/* Controls */}
-
-
+
+
- {!localParticipant?.audio ? : }
+ {!localParticipant?.audio ? : }
- {!localParticipant?.video ? : }
+ {!localParticipant?.video ? : }
-
+
setShowNotes(!showNotes)}
- className="w-12 h-12 rounded-full p-0"
+ className="w-10 h-10 md:w-12 md:h-12 rounded-full p-0"
title="Toggle Notes"
>
-
+
-
+
@@ -394,12 +398,6 @@ function DailyMeetingInner({
isVisible={showNotes}
onToggle={() => setShowNotes(!showNotes)}
/>
-
- {/* Media Device Tips Modal */}
-
setShowTips(false)}
- />
);
}
@@ -427,14 +425,14 @@ function ParticipantTile({
{/* Participant info overlay */}
-
+
{participant.user_name || 'Anonymous'} {isLocal && '(You)'}
{/* Audio muted indicator */}
{!participant.audio && (
-
-
+
+
)}
@@ -442,10 +440,10 @@ function ParticipantTile({
{!participant.video && (
-
-
+
+
-
Camera off
+
Camera off
)}
@@ -462,7 +460,7 @@ function CameraTile({ participantId }: { participantId: string }) {
if (!participant) return null;
return (
-
@@ -470,22 +468,22 @@ function CameraTile({ participantId }: { participantId: string }) {
{/* Participant name */}
-
+
{participant.user_name || 'Anonymous'} {isLocal && '(You)'}
{/* Audio muted indicator */}
{!participant.audio && (
-
-
+
+
)}
{/* Video off indicator */}
{!participant.video && (
-
)}
@@ -589,17 +587,17 @@ export default function DailyMeeting({
if (joinError) {
return (
-
-
-
-
+
+
+
+
-
Unable to Join Meeting
-
{joinError}
+
Unable to Join Meeting
+
{joinError}
-
-
💡 Quick Solutions:
-
+
+
💡 Quick Solutions:
+
- • Close other browser tabs using your camera/microphone
- • Close video calling apps (Zoom, Teams, Skype, etc.)
- • Refresh this page and allow camera permissions
@@ -615,7 +613,7 @@ export default function DailyMeeting({
setIsJoining(true);
window.location.reload();
}}
- className="w-full bg-blue-600 hover:bg-blue-700"
+ className="w-full bg-blue-600 hover:bg-blue-700 text-sm md:text-base"
>
Try Again
@@ -623,7 +621,7 @@ export default function DailyMeeting({
Return to Dashboard
diff --git a/src/components/meetingSystem/MediaDeviceTips.tsx b/src/components/meetingSystem/MediaDeviceTips.tsx
deleted file mode 100644
index 5b9a1674..00000000
--- a/src/components/meetingSystem/MediaDeviceTips.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { AlertTriangle, X, Camera, Mic, Monitor, Smartphone } from 'lucide-react';
-
-interface MediaDeviceTipsProps {
- isVisible: boolean;
- onClose: () => void;
-}
-
-export function MediaDeviceTips({ isVisible, onClose }: MediaDeviceTipsProps) {
- if (!isVisible) return null;
-
- return (
-
-
-
- {/* Header */}
-
-
-
-
Camera & Microphone Tips
-
-
-
-
-
-
- {/* Content */}
-
-
-
⚠️ Multiple Browser Limitation
-
- Only one browser tab can access your camera and microphone at the same time.
- This is a security feature built into web browsers.
-
-
-
-
-
If you can't access camera/microphone:
-
-
-
-
-
-
Close Other Browser Tabs
-
Close any other tabs that might be using your camera (video calls, other meetings)
-
-
-
-
-
-
-
Close Video Applications
-
Close apps like Zoom, Teams, Skype, or any camera software
-
-
-
-
-
-
-
Check Browser Permissions
-
Make sure you've allowed camera and microphone access for this website
-
-
-
-
-
-
-
Try Different Browser
-
If issues persist, try opening the meeting in a different browser or incognito mode
-
-
-
-
-
-
-
💡 Pro Tips:
-
- - • Use one browser for video calls only
- - • Keep other video apps closed during meetings
- - • Refresh the page if you encounter permission issues
- - • Use headphones to prevent echo
-
-
-
-
- {/* Actions */}
-
-
- Got it!
-
- window.location.reload()}
- className="flex-1"
- >
- Refresh Page
-
-
-
-
-
- );
-}
diff --git a/src/components/meetingSystem/MediaDeviceWarning.tsx b/src/components/meetingSystem/MediaDeviceWarning.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/components/messageSystem/MeetingBox.tsx b/src/components/messageSystem/MeetingBox.tsx
index bf57c842..127ea1e1 100644
--- a/src/components/messageSystem/MeetingBox.tsx
+++ b/src/components/messageSystem/MeetingBox.tsx
@@ -1,6 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Calendar, Plus, FileText, ChevronDown, ChevronRight } from 'lucide-react';
-import { motion, AnimatePresence } from 'framer-motion';
import CreateMeetingModal from '@/components/meetingSystem/CreateMeetingModal';
import CancelMeetingModal from '@/components/meetingSystem/CancelMeetingModal';
import MeetingList from '@/components/meetingSystem/MeetingList';
@@ -75,80 +74,6 @@ interface MeetingBoxProps {
export default function MeetingBox({ chatRoomId, userId, onClose, onMeetingUpdate }: MeetingBoxProps) {
const [meetings, setMeetings] = useState([]);
const [loading, setLoading] = useState(true);
-
- // Animation variants
- const containerVariants = {
- hidden: {
- opacity: 0,
- y: 20,
- scale: 0.95
- },
- visible: {
- opacity: 1,
- y: 0,
- scale: 1,
- transition: {
- duration: 0.4,
- ease: "easeOut",
- staggerChildren: 0.1
- }
- },
- exit: {
- opacity: 0,
- y: -20,
- scale: 0.95,
- transition: {
- duration: 0.3,
- ease: "easeIn"
- }
- }
- };
-
- const itemVariants = {
- hidden: {
- opacity: 0,
- x: -20
- },
- visible: {
- opacity: 1,
- x: 0,
- transition: {
- duration: 0.3,
- ease: "easeOut"
- }
- }
- };
-
- const headerVariants = {
- hidden: {
- opacity: 0,
- y: -10
- },
- visible: {
- opacity: 1,
- y: 0,
- transition: {
- duration: 0.4,
- ease: "easeOut",
- delay: 0.1
- }
- }
- };
-
- const buttonHover = {
- scale: 1.05,
- transition: {
- duration: 0.2,
- ease: "easeInOut"
- }
- };
-
- const buttonTap = {
- scale: 0.95,
- transition: {
- duration: 0.1
- }
- };
const [userProfiles, setUserProfiles] = useState({});
const [showCreateModal, setShowCreateModal] = useState(false);
const [showCancelModal, setShowCancelModal] = useState(false);
@@ -714,167 +639,45 @@ ${formattedContent}
if (loading && meetings.length === 0) {
return (
-
+
{/* Background pulse effect */}
-
+
-
-
+
+
Loading meetings...
-
+
-
+
);
}
return (
<>
- {/* Animated Background Elements */}
-
- {/* Floating circles */}
-
-
-
-
-
-
+
{/* Header */}
-
-
-
+
+
+
-
+
Meetings
-
-
+
-
+
New
-
-
+
+
{/* Meetings List */}
-
+
setShowPastMeetings(!showPastMeetings)}
onToggleCancelledMeetings={() => setShowCancelledMeetings(!showCancelledMeetings)}
/>
-
+
{/* Saved Meeting Notes - Collapsible */}
-
-
-
+
+
setShowSavedNotes(!showSavedNotes)}
className="w-full bg-gray-50 hover:bg-gray-100 px-4 py-3 flex items-center justify-between text-sm font-medium text-gray-700 transition-colors"
- whileHover={{ backgroundColor: "rgb(243 244 246)" }}
- whileTap={{ scale: 0.98 }}
>
-
+
-
+
Saved Meeting Notes ({savedNotes.length})
-
+
{showSavedNotes ? (
) : (
)}
-
-
-
- {showSavedNotes && (
-
-
-
- )}
-
-
-
-
+
+
+ {showSavedNotes && (
+
+
+
+ )}
+
+
+
{/* Modals */}
-
- {showCreateModal && (
-
- setShowCreateModal(false)}
- onCreate={handleCreateMeeting}
- receiverName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'this user'}
- />
-
- )}
-
- {showCancelModal && meetingToCancel && (
-
- {
- setShowCancelModal(false);
- setMeetingToCancel(null);
- }}
- onCancel={handleCancelMeeting}
- userName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'User'}
- />
-
- )}
-
+ {showCreateModal && (
+
+ setShowCreateModal(false)}
+ onCreate={handleCreateMeeting}
+ receiverName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'this user'}
+ />
+
+ )}
+
+ {showCancelModal && meetingToCancel && (
+
+ {
+ setShowCancelModal(false);
+ setMeetingToCancel(null);
+ }}
+ onCancel={handleCancelMeeting}
+ userName={otherUserId ? userProfiles[otherUserId]?.firstName || 'User' : 'User'}
+ />
+
+ )}
{/* Alert Component */}
{/* Notes View Modal */}
-
- {showNotesModal && selectedNote && (
-
- {
- setShowNotesModal(false);
- setSelectedNote(null);
- }}
- onDownload={handleDownloadNotes}
- />
-
- )}
-
+ {showNotesModal && selectedNote && (
+
+ {
+ setShowNotesModal(false);
+ setSelectedNote(null);
+ }}
+ onDownload={handleDownloadNotes}
+ />
+
+ )}
>
);
}
\ No newline at end of file
diff --git a/src/components/notificationSystem/Notification.tsx b/src/components/notificationSystem/Notification.tsx
index 1cb362b5..89ac5511 100644
--- a/src/components/notificationSystem/Notification.tsx
+++ b/src/components/notificationSystem/Notification.tsx
@@ -24,51 +24,53 @@ const Notification: React.FC = ({ notification, onMarkAsRead
return (
-
+
-
-
+
+
{typeName}
-
{notification.description}
+
{notification.description}
{formattedDate}
-
+
{!notification.isRead && (
-
- Mark Read
+
+ Mark Read
+ Read
)}
{notification.targetDestination && (
-
+
View
)}
diff --git a/src/components/sessionSystem/CreateSessionModal.tsx b/src/components/sessionSystem/CreateSessionModal.tsx
index 6fa77c6f..b58b6145 100644
--- a/src/components/sessionSystem/CreateSessionModal.tsx
+++ b/src/components/sessionSystem/CreateSessionModal.tsx
@@ -308,7 +308,7 @@ export default function CreateSessionModal({
onChange={(e) => setSelectedOtherSkill(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
-
+
{otherUserSkills.map((skill) => (