diff --git a/__tests__/components/MeetingBox.test.tsx b/__tests__/components/MeetingBox.test.tsx index 32bd7d65..c6b223ba 100644 --- a/__tests__/components/MeetingBox.test.tsx +++ b/__tests__/components/MeetingBox.test.tsx @@ -1,722 +1,471 @@ /** * MeetingBox Component Tests - * Tests the meeting management functionality including scheduling, accepting, and cancelling meetings */ import React from 'react'; -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import MeetingBox from '@/components/messageSystem/MeetingBox'; -// Create shared mocks for router -const mockPush = jest.fn(); -const mockRefresh = jest.fn(); - -// Mock the router -jest.mock('next/navigation', () => ({ - useRouter: () => ({ - push: mockPush, - refresh: mockRefresh - }) -})); - -// Mock meeting API services -jest.mock('@/services/meetingApiServices', () => ({ - fetchMeetings: jest.fn(), - createMeeting: jest.fn(), - updateMeeting: jest.fn() -})); - -// Mock session API services for cache invalidation -jest.mock('@/services/sessionApiServices', () => ({ - invalidateUsersCaches: jest.fn() -})); - -// Mock debounced API service -jest.mock('@/services/debouncedApiService', () => { - const mockService = { - makeRequest: jest.fn(), - invalidate: jest.fn() - }; - return { - debouncedApiService: mockService, - makeRequest: mockService.makeRequest, - invalidate: mockService.invalidate - }; -}); - -// Mock utility functions -jest.mock('@/utils/avatarUtils', () => ({ - processAvatarUrl: jest.fn((url) => url), - getFirstLetter: jest.fn((firstName, userId) => firstName?.[0] || 'U'), - createFallbackAvatar: jest.fn(() => 'data:image/svg+xml;base64,mock') -})); - -// Mock OptimizedAvatar component -jest.mock('@/components/ui/OptimizedAvatar', () => { - return function MockOptimizedAvatar({ userId, firstName, lastName }: any) { +// Mock child components +jest.mock('@/components/meetingSystem/CreateMeetingModal', () => { + return function MockCreateMeetingModal({ onClose, onCreate, receiverName }: any) { return ( -
- Avatar for {firstName} {lastName} ({userId}) +
+
Create Meeting Modal for {receiverName}
+ +
); }; }); -// Mock Alert component -jest.mock('@/components/ui/Alert', () => { - return function MockAlert({ type, title, message, isOpen, onClose }: any) { - if (!isOpen) return null; +jest.mock('@/components/meetingSystem/CancelMeetingModal', () => { + return function MockCancelMeetingModal({ meetingId, onClose, onCancel, userName }: any) { return ( -
- {title &&
{title}
} -
{message}
- +
+
Cancel Meeting Modal for {userName}
+ +
); }; }); -// Mock ConfirmationDialog component -jest.mock('@/components/ui/ConfirmationDialog', () => { - return function MockConfirmationDialog({ - isOpen, - onClose, - onConfirm, - title, - message, - type, - confirmText +jest.mock('@/components/meetingSystem/MeetingList', () => { + return function MockMeetingList({ + pendingRequests, + upcomingMeetings, + onScheduleMeeting, + onMeetingAction, + onCancelMeeting }: any) { - if (!isOpen) return null; return ( -
-
{title}
-
{message}
-
{type}
- - +
+
{pendingRequests.length}
+
{upcomingMeetings.length}
+ + + + {pendingRequests.map((meeting: any) => ( +
+ + +
+ ))} + + {upcomingMeetings.map((meeting: any) => ( +
+ +
+ ))}
); }; }); -// Mock CreateMeetingModal component -jest.mock('@/components/meetingSystem/CreateMeetingModal', () => { - return function MockCreateMeetingModal({ onClose, onCreate, receiverName }: any) { +jest.mock('@/components/meetingSystem/SavedNotesList', () => { + return function MockSavedNotesList({ notes }: any) { return ( -
-
Creating meeting with {receiverName}
- - +
+
{notes.length}
); }; }); -// 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}

-
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 && ( -
+