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:
Radosław Gierwiało
2025-11-21 21:21:58 +01:00
parent f3bd169dbf
commit 4187157b94
2 changed files with 498 additions and 8 deletions

View File

@@ -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>
);

View 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();
});
});
});
});