diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 9a46b3a..b583f26 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -338,6 +338,9 @@ const MatchCard = ({ match }) => { const navigate = useNavigate(); const { partner, event, videoExchange, ratings } = match; + // Can rate when video exchange is complete and user hasn't rated yet + const canRate = videoExchange?.sentByMe && videoExchange?.receivedFromPartner && !ratings?.ratedByMe; + return (
@@ -374,14 +377,25 @@ const MatchCard = ({ match }) => {
- {/* Action */} - + {/* Actions */} +
+ + {canRate && ( + + )} +
); diff --git a/frontend/src/pages/__tests__/DashboardPage.test.jsx b/frontend/src/pages/__tests__/DashboardPage.test.jsx new file mode 100644 index 0000000..e694105 --- /dev/null +++ b/frontend/src/pages/__tests__/DashboardPage.test.jsx @@ -0,0 +1,476 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { BrowserRouter } from 'react-router-dom'; + +// Mock the modules +vi.mock('../../contexts/AuthContext', () => ({ + useAuth: () => ({ + user: { id: 1, username: 'testuser', firstName: 'Test' }, + }), +})); + +vi.mock('../../services/api', () => ({ + dashboardAPI: { + getData: vi.fn(), + }, + matchesAPI: { + acceptMatch: vi.fn(), + deleteMatch: vi.fn(), + getMatches: vi.fn().mockResolvedValue({ data: [] }), + }, +})); + +vi.mock('../../services/socket', () => ({ + connectSocket: vi.fn(), + disconnectSocket: vi.fn(), + getSocket: vi.fn(() => ({ + on: vi.fn(), + off: vi.fn(), + })), +})); + +import DashboardPage from '../DashboardPage'; +import { dashboardAPI, matchesAPI } from '../../services/api'; + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); + +const renderWithRouter = (component) => { + return render({component}); +}; + +describe('DashboardPage', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Loading State', () => { + it('should show loading spinner while fetching data', async () => { + dashboardAPI.getData.mockImplementation(() => new Promise(() => {})); + + renderWithRouter(); + + expect(screen.getByText('Loading dashboard...')).toBeInTheDocument(); + }); + }); + + describe('Empty States', () => { + it('should show empty state when no events', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('No active events')).toBeInTheDocument(); + }); + }); + + it('should show empty state when no matches', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('No active matches')).toBeInTheDocument(); + }); + }); + }); + + describe('Active Events', () => { + it('should display event cards', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [ + { + id: 1, + slug: 'test-event', + name: 'Test Dance Event', + location: 'Warsaw', + startDate: '2025-12-01', + endDate: '2025-12-03', + participantsCount: 50, + myHeats: [], + }, + ], + activeMatches: [], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Test Dance Event')).toBeInTheDocument(); + expect(screen.getByText('Warsaw')).toBeInTheDocument(); + expect(screen.getByText('50 participants')).toBeInTheDocument(); + }); + }); + + it('should navigate to event chat when clicking Enter Chat', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [ + { + id: 1, + slug: 'test-event', + name: 'Test Event', + location: 'Warsaw', + startDate: '2025-12-01', + endDate: '2025-12-03', + participantsCount: 50, + myHeats: [], + }, + ], + activeMatches: [], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Enter Chat')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Enter Chat')); + expect(mockNavigate).toHaveBeenCalledWith('/events/test-event/chat'); + }); + }); + + describe('Active Matches', () => { + it('should display match cards with partner info', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { + id: 2, + username: 'sarah_dancer', + firstName: 'Sarah', + lastName: 'Martinez', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: false, receivedFromPartner: false }, + ratings: { ratedByMe: false, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Sarah Martinez')).toBeInTheDocument(); + expect(screen.getByText('@sarah_dancer')).toBeInTheDocument(); + expect(screen.getByText('Test Event')).toBeInTheDocument(); + }); + }); + + it('should show Rate button when video exchange is complete and not rated', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { + id: 2, + username: 'sarah_dancer', + firstName: 'Sarah', + lastName: 'Martinez', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: true, receivedFromPartner: true }, + ratings: { ratedByMe: false, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Rate')).toBeInTheDocument(); + }); + }); + + it('should NOT show Rate button when already rated', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { + id: 2, + username: 'sarah_dancer', + firstName: 'Sarah', + lastName: 'Martinez', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: true, receivedFromPartner: true }, + ratings: { ratedByMe: true, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Chat')).toBeInTheDocument(); + }); + expect(screen.queryByText('Rate')).not.toBeInTheDocument(); + }); + + it('should NOT show Rate button when video exchange incomplete', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { + id: 2, + username: 'sarah_dancer', + firstName: 'Sarah', + lastName: 'Martinez', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: true, receivedFromPartner: false }, + ratings: { ratedByMe: false, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Chat')).toBeInTheDocument(); + }); + expect(screen.queryByText('Rate')).not.toBeInTheDocument(); + }); + + it('should navigate to rate page when clicking Rate button', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { + id: 2, + username: 'sarah_dancer', + firstName: 'Sarah', + lastName: 'Martinez', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: true, receivedFromPartner: true }, + ratings: { ratedByMe: false, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Rate')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Rate')); + expect(mockNavigate).toHaveBeenCalledWith('/matches/match-123/rate'); + }); + }); + + describe('Match Requests', () => { + it('should display incoming requests', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [], + matchRequests: { + incoming: [ + { + id: 1, + slug: 'req-123', + requester: { + id: 3, + username: 'john_lead', + firstName: 'John', + lastName: 'Lead', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + requesterHeats: [], + createdAt: '2025-11-21T10:00:00Z', + }, + ], + outgoing: [], + }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('John Lead')).toBeInTheDocument(); + expect(screen.getByText('Incoming (1)')).toBeInTheDocument(); + }); + }); + + it('should display outgoing requests', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [], + matchRequests: { + incoming: [], + outgoing: [ + { + id: 1, + slug: 'req-456', + recipient: { + id: 4, + username: 'anna_follow', + firstName: 'Anna', + lastName: 'Follow', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + createdAt: '2025-11-21T10:00:00Z', + }, + ], + }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('Anna Follow')).toBeInTheDocument(); + expect(screen.getByText('Outgoing (1)')).toBeInTheDocument(); + expect(screen.getByText('Waiting for response...')).toBeInTheDocument(); + }); + }); + + it('should accept incoming request', async () => { + matchesAPI.acceptMatch.mockResolvedValue({ success: true }); + dashboardAPI.getData + .mockResolvedValueOnce({ + activeEvents: [], + activeMatches: [], + matchRequests: { + incoming: [ + { + id: 1, + slug: 'req-123', + requester: { + id: 3, + username: 'john_lead', + firstName: 'John', + lastName: 'Lead', + avatar: null, + }, + event: { id: 1, name: 'Test Event' }, + requesterHeats: [], + createdAt: '2025-11-21T10:00:00Z', + }, + ], + outgoing: [], + }, + }) + .mockResolvedValueOnce({ + activeEvents: [], + activeMatches: [], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + expect(screen.getByText('John Lead')).toBeInTheDocument(); + }); + + // Find the accept button (green button with checkmark) + const acceptButtons = screen.getAllByRole('button'); + const acceptButton = acceptButtons.find(btn => btn.title === 'Accept'); + expect(acceptButton).toBeInTheDocument(); + + fireEvent.click(acceptButton); + + await waitFor(() => { + expect(matchesAPI.acceptMatch).toHaveBeenCalledWith('req-123'); + }); + }); + }); + + describe('Video Status Display', () => { + it('should show sent status when video sent by me', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { id: 2, username: 'partner', firstName: null, lastName: null, avatar: null }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: true, receivedFromPartner: false }, + ratings: { ratedByMe: false, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + // Check that "Sent" text exists with check icon (green color) + const sentTexts = screen.getAllByText('Sent'); + expect(sentTexts.length).toBeGreaterThan(0); + }); + }); + }); + + describe('Rating Status Display', () => { + it('should show rating status indicators', async () => { + dashboardAPI.getData.mockResolvedValue({ + activeEvents: [], + activeMatches: [ + { + id: 1, + slug: 'match-123', + partner: { id: 2, username: 'partner', firstName: null, lastName: null, avatar: null }, + event: { id: 1, name: 'Test Event' }, + videoExchange: { sentByMe: false, receivedFromPartner: false }, + ratings: { ratedByMe: true, ratedByPartner: false }, + }, + ], + matchRequests: { incoming: [], outgoing: [] }, + }); + + renderWithRouter(); + + await waitFor(() => { + // Check for "You" and "Partner" rating indicators + expect(screen.getByText('You')).toBeInTheDocument(); + expect(screen.getByText('Partner')).toBeInTheDocument(); + }); + }); + }); +});