bb9c8ec1c3
- Replace Socket.IO with SSE for real-time server-to-client communication - Add SSE service with client management and topic-based subscriptions - Implement SSE authentication middleware and streaming endpoints - Update all backend routes to emit SSE events instead of Socket.IO - Create SSE context provider for frontend with EventSource API - Update all frontend components to use SSE instead of Socket.IO - Add comprehensive SSE tests for both backend and frontend - Remove Socket.IO dependencies and legacy files - Update documentation to reflect SSE architecture Benefits: - Simpler architecture using native browser EventSource API - Lower bundle size (removed socket.io-client dependency) - Better compatibility with reverse proxies and load balancers - Reduced resource usage for Raspberry Pi deployment - Standard HTTP-based real-time communication 🤖 Generated with [AI Assistant] Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { BrowserRouter } from 'react-router-dom';
|
|
import { AuthContext } from '../../context/AuthContext';
|
|
import { SSEContext } from '../../context/SSEContext';
|
|
import Events from '../Events';
|
|
import axios from 'axios';
|
|
|
|
// Mocks
|
|
const mockAuthContext = {
|
|
auth: { isAuthenticated: true, loading: false, user: { id: 'user123', name: 'Test User' } },
|
|
login: jest.fn(),
|
|
logout: jest.fn(),
|
|
};
|
|
|
|
const mockSSEContext = {
|
|
connected: true,
|
|
notifications: [],
|
|
on: jest.fn(),
|
|
off: jest.fn(),
|
|
subscribe: jest.fn().mockResolvedValue({ subscribed: [] }),
|
|
unsubscribe: jest.fn().mockResolvedValue({ unsubscribed: [] }),
|
|
clearNotification: jest.fn(),
|
|
clearAllNotifications: jest.fn(),
|
|
};
|
|
|
|
jest.mock('axios');
|
|
|
|
describe('Events Component', () => {
|
|
const mockEvents = [
|
|
{
|
|
_id: 'event1',
|
|
title: 'Community Cleanup Day',
|
|
description: 'Join us for a community cleanup event',
|
|
date: '2023-06-15T10:00:00.000Z',
|
|
location: 'Central Park',
|
|
participants: [],
|
|
participantsCount: 0,
|
|
status: 'upcoming',
|
|
createdAt: '2023-01-01T00:00:00.000Z',
|
|
},
|
|
{
|
|
_id: 'event2',
|
|
title: 'Street Maintenance Workshop',
|
|
description: 'Learn proper street maintenance techniques',
|
|
date: '2023-06-20T14:00:00.000Z',
|
|
location: 'Community Center',
|
|
participants: [
|
|
{ userId: 'user123', name: 'Test User', joinedAt: '2023-01-01T00:00:00.000Z' }
|
|
],
|
|
participantsCount: 1,
|
|
status: 'ongoing',
|
|
createdAt: '2023-01-02T00:00:00.000Z',
|
|
},
|
|
{
|
|
_id: 'event3',
|
|
title: 'Completed Event',
|
|
description: 'This event has already finished',
|
|
date: '2023-01-01T00:00:00.000Z',
|
|
location: 'City Hall',
|
|
participants: [
|
|
{ userId: 'user123', name: 'Test User', joinedAt: '2023-01-01T00:00:00.000Z' },
|
|
{ userId: 'user456', name: 'Other User', joinedAt: '2023-01-01T00:00:00.000Z' }
|
|
],
|
|
participantsCount: 2,
|
|
status: 'completed',
|
|
createdAt: '2022-12-01T00:00:00.000Z',
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Mock axios.get to return events
|
|
axios.get.mockResolvedValue({ data: mockEvents });
|
|
|
|
// Mock axios.post for event creation
|
|
axios.post.mockResolvedValue({ data: { ...mockEvents[0], _id: 'event4' } });
|
|
|
|
// Mock axios.put for event joining
|
|
axios.put.mockResolvedValue({ data: { ...mockEvents[1], participants: [...mockEvents[1].participants, { userId: 'user123', name: 'Test User', joinedAt: '2023-01-01T00:00:00.000Z' }] } });
|
|
});
|
|
|
|
const renderEvents = () => {
|
|
return render(
|
|
<BrowserRouter>
|
|
<AuthContext.Provider value={mockAuthContext}>
|
|
<SSEContext.Provider value={mockSSEContext}>
|
|
<Events />
|
|
</SSEContext.Provider>
|
|
</AuthContext.Provider>
|
|
</BrowserRouter>
|
|
);
|
|
};
|
|
|
|
it('renders events list correctly', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Community Cleanup Day')).toBeInTheDocument();
|
|
expect(screen.getByText('Street Maintenance Workshop')).toBeInTheDocument();
|
|
expect(screen.getByText('Completed Event')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows loading state initially', () => {
|
|
// Mock axios to delay response
|
|
axios.get.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ data: [] }), 100)));
|
|
|
|
renderEvents();
|
|
|
|
expect(screen.getByText('Loading events...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays event status correctly', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const upcomingEvent = screen.getByText('Community Cleanup Day').closest('[data-status="upcoming"]');
|
|
const ongoingEvent = screen.getByText('Street Maintenance Workshop').closest('[data-status="ongoing"]');
|
|
const completedEvent = screen.getByText('Completed Event').closest('[data-status="completed"]');
|
|
|
|
expect(upcomingEvent).toBeInTheDocument();
|
|
expect(ongoingEvent).toBeInTheDocument();
|
|
expect(completedEvent).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays participant count', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('0 participants')).toBeInTheDocument();
|
|
expect(screen.getByText('1 participant')).toBeInTheDocument();
|
|
expect(screen.getByText('2 participants')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays event dates correctly', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('June 15, 2023')).toBeInTheDocument();
|
|
expect(screen.getByText('June 20, 2023')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays event locations', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Central Park')).toBeInTheDocument();
|
|
expect(screen.getByText('Community Center')).toBeInTheDocument();
|
|
expect(screen.getByText('City Hall')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows event creation form', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Create New Event')).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText('Event title')).toBeInTheDocument();
|
|
expect(screen.getByPlaceholderText('Event description')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('validates event creation form', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const createButton = screen.getByText('Create Event');
|
|
const titleInput = screen.getByPlaceholderText('Event title');
|
|
|
|
expect(createButton).toBeInTheDocument();
|
|
expect(titleInput).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('creates new event successfully', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const createButton = screen.getByText('Create Event');
|
|
const titleInput = screen.getByPlaceholderText('Event title');
|
|
const descriptionInput = screen.getByPlaceholderText('Event description');
|
|
const dateInput = screen.getByDisplayValue('2023-06-15');
|
|
const locationInput = screen.getByPlaceholderText('Event location');
|
|
|
|
expect(createButton).toBeInTheDocument();
|
|
expect(titleInput).toBeInTheDocument();
|
|
expect(descriptionInput).toBeInTheDocument();
|
|
expect(dateInput).toBeInTheDocument();
|
|
expect(locationInput).toBeInTheDocument();
|
|
});
|
|
|
|
// Fill out form
|
|
fireEvent.change(titleInput, { target: { value: 'New Test Event' } });
|
|
fireEvent.change(descriptionInput, { target: { value: 'Test event description' } });
|
|
fireEvent.change(dateInput, { target: { value: '2023-07-01' } });
|
|
fireEvent.change(locationInput, { target: { value: 'Test Location' } });
|
|
|
|
// Submit form
|
|
fireEvent.click(createButton);
|
|
|
|
// Verify axios.post was called
|
|
await waitFor(() => {
|
|
expect(axios.post).toHaveBeenCalledWith('/api/events', {
|
|
title: 'New Test Event',
|
|
description: 'Test event description',
|
|
date: '2023-07-01',
|
|
location: 'Test Location'
|
|
});
|
|
});
|
|
});
|
|
|
|
it('handles event joining', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const joinButtons = screen.getAllByText('Join Event');
|
|
const firstJoinButton = joinButtons[0];
|
|
|
|
expect(firstJoinButton).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('updates participant count when joining event', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const joinButtons = screen.getAllByText('Join Event');
|
|
const firstJoinButton = joinButtons[0];
|
|
|
|
fireEvent.click(firstJoinButton);
|
|
|
|
expect(axios.put).toHaveBeenCalledWith('/api/events/event1/join');
|
|
});
|
|
});
|
|
|
|
it('shows error message when API fails', async () => {
|
|
// Mock axios.get to throw error
|
|
axios.get.mockRejectedValue(new Error('Network error'));
|
|
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Failed to load events/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays empty state when no events', async () => {
|
|
// Mock empty response
|
|
axios.get.mockResolvedValue({ data: [] });
|
|
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('No upcoming events')).toBeInTheDocument();
|
|
expect(screen.getByText('Be the first to create one!')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('filters events by status', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
// Look for filter buttons
|
|
const filterButtons = screen.getAllByRole('button');
|
|
const upcomingFilter = filterButtons.find(btn => btn.textContent.includes('Upcoming'));
|
|
const completedFilter = filterButtons.find(btn => btn.textContent.includes('Completed'));
|
|
|
|
expect(upcomingFilter).toBeInTheDocument();
|
|
expect(completedFilter).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('searches events', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const searchInput = screen.getByPlaceholderText('Search events...');
|
|
expect(searchInput).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows event details', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const eventCards = screen.getAllByTestId('event-card');
|
|
expect(eventCards.length).toBeGreaterThan(0);
|
|
|
|
// Check first event card
|
|
const firstCard = eventCards[0];
|
|
expect(firstCard).toHaveTextContent('Community Cleanup Day');
|
|
});
|
|
});
|
|
|
|
it('handles real-time updates', async () => {
|
|
const { on } = mockSSEContext;
|
|
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
// Simulate receiving a new event via SSE
|
|
const sseCallback = on.mock.calls[0][1];
|
|
const newEventData = {
|
|
type: 'new_event',
|
|
event: { ...mockEvents[0], _id: 'event5' }
|
|
};
|
|
|
|
sseCallback(newEventData);
|
|
|
|
// Verify new event appears in the list
|
|
expect(screen.getByText('Community Cleanup Day')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays user\'s joined events', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
// Look for "My Events" section
|
|
const myEventsSection = screen.getByText('My Events');
|
|
expect(myEventsSection).toBeInTheDocument();
|
|
|
|
// Should show events user has joined
|
|
expect(screen.getByText('Street Maintenance Workshop')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('handles event cancellation', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
const cancelButtons = screen.getAllByText('Cancel');
|
|
const firstCancelButton = cancelButtons[0];
|
|
|
|
expect(firstCancelButton).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows event statistics', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Total Events: 3')).toBeInTheDocument();
|
|
expect(screen.getByText('Upcoming: 1')).toBeInTheDocument();
|
|
expect(screen.getByText('Ongoing: 1')).toBeInTheDocument();
|
|
expect(screen.getByText('Completed: 1')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('handles pagination', async () => {
|
|
renderEvents();
|
|
|
|
await waitFor(() => {
|
|
// Look for pagination controls
|
|
expect(screen.getByText('Next')).toBeInTheDocument();
|
|
expect(screen.getByText('Previous')).toBeInTheDocument();
|
|
});
|
|
});
|
|
}); |