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 navigate = useNavigate();
|
||||||
const { partner, event, videoExchange, ratings } = match;
|
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 (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
<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">
|
<div className="flex items-start gap-4">
|
||||||
@@ -374,14 +377,25 @@ const MatchCard = ({ match }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action */}
|
{/* Actions */}
|
||||||
|
<div className="flex-shrink-0 flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/matches/${match.slug}/chat`)}
|
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" />
|
<MessageCircle className="w-4 h-4" />
|
||||||
Chat
|
Chat
|
||||||
</button>
|
</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>
|
||||||
</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