feat(frontend): add Rate Partner button and dashboard tests
- Add Rate button to MatchCard (shows when video exchange complete & not rated) - Add 15 comprehensive tests for DashboardPage component - Tests cover: loading, empty states, events, matches, requests, navigation
This commit is contained in:
@@ -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 (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
@@ -374,14 +377,25 @@ const MatchCard = ({ match }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
{/* Actions */}
|
||||
<div className="flex-shrink-0 flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => navigate(`/matches/${match.slug}/chat`)}
|
||||
className="flex-shrink-0 flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
Chat
|
||||
</button>
|
||||
{canRate && (
|
||||
<button
|
||||
onClick={() => navigate(`/matches/${match.slug}/rate`)}
|
||||
className="flex items-center gap-2 px-4 py-2 border border-amber-500 text-amber-600 rounded-md hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
<Star className="w-4 h-4" />
|
||||
Rate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
476
frontend/src/pages/__tests__/DashboardPage.test.jsx
Normal file
476
frontend/src/pages/__tests__/DashboardPage.test.jsx
Normal file
@@ -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(<BrowserRouter>{component}</BrowserRouter>);
|
||||
};
|
||||
|
||||
describe('DashboardPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading spinner while fetching data', async () => {
|
||||
dashboardAPI.getData.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
renderWithRouter(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
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(<DashboardPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Check for "You" and "Partner" rating indicators
|
||||
expect(screen.getByText('You')).toBeInTheDocument();
|
||||
expect(screen.getByText('Partner')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user