diff --git a/frontend/src/components/__tests__/Events.test.js b/frontend/src/components/__tests__/Events.test.js new file mode 100644 index 0000000..683675d --- /dev/null +++ b/frontend/src/components/__tests__/Events.test.js @@ -0,0 +1,362 @@ +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 { SocketContext } from '../../context/SocketContext'; +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 mockSocketContext = { + socket: null, + connected: true, + on: jest.fn(), + off: jest.fn(), + joinEvent: jest.fn(), + leaveEvent: 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( + + + + + + + + ); + }; + + 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 } = mockSocketContext; + + renderEvents(); + + await waitFor(() => { + // Simulate receiving a new event via socket + const socketCallback = on.mock.calls[0][1]; + const newEventData = { + type: 'new_event', + data: { ...mockEvents[0], _id: 'event5' } + }; + + socketCallback(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(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/__tests__/SocialFeed.test.js b/frontend/src/components/__tests__/SocialFeed.test.js new file mode 100644 index 0000000..cb1a6d9 --- /dev/null +++ b/frontend/src/components/__tests__/SocialFeed.test.js @@ -0,0 +1,346 @@ +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 { SocketContext } from '../../context/SocketContext'; +import SocialFeed from '../SocialFeed'; +import axios from 'axios'; + +// Mock the contexts +const mockAuthContext = { + auth: { isAuthenticated: true, loading: false, user: { id: 'user123', name: 'Test User' } }, + login: jest.fn(), + logout: jest.fn(), +}; + +const mockSocketContext = { + socket: null, + connected: true, + on: jest.fn(), + off: jest.fn(), +}; + +// Mock axios +jest.mock('axios'); + +describe('SocialFeed Component', () => { + const mockPosts = [ + { + _id: 'post1', + content: 'Just cleaned up Main Street! ๐Ÿงน', + type: 'text', + user: { userId: 'user123', name: 'Test User', profilePicture: 'avatar.jpg' }, + likes: [], + likesCount: 0, + comments: [], + commentsCount: 0, + createdAt: '2023-01-01T00:00:00.000Z', + }, + { + _id: 'post2', + content: 'Beautiful sunset on Oak Street ๐ŸŒ…', + type: 'image', + imageUrl: 'https://example.com/sunset.jpg', + cloudinaryPublicId: 'sunset_123', + user: { userId: 'user456', name: 'Other User', profilePicture: 'avatar2.jpg' }, + likes: ['user123', 'user789'], + likesCount: 2, + comments: [ + { + _id: 'comment1', + content: 'Great work!', + user: { userId: 'user789', name: 'Another User' }, + createdAt: '2023-01-01T01:00:00.000Z', + } + ], + commentsCount: 1, + createdAt: '2023-01-01T12:00:00.000Z', + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock axios.get to return posts + axios.get.mockResolvedValue({ data: mockPosts }); + + // Mock axios.post for creating posts + axios.post.mockResolvedValue({ data: { ...mockPosts[0], _id: 'post3' } }); + + // Mock axios.put for liking posts + axios.put.mockResolvedValue({ data: { ...mockPosts[0], likes: ['user123'], likesCount: 1 } }); + }); + + const renderSocialFeed = () => { + return render( + + + + + + + + ); + }; + + it('renders social feed correctly', async () => { + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText('Just cleaned up Main Street! ๐Ÿงน')).toBeInTheDocument(); + expect(screen.getByText('Beautiful sunset on Oak Street ๐ŸŒ…')).toBeInTheDocument(); + }); + }); + + it('shows loading state initially', () => { + // Mock axios to delay response + axios.get.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ data: [] }), 100))); + + renderSocialFeed(); + + expect(screen.getByText('Loading posts...')).toBeInTheDocument(); + }); + + it('displays user information for posts', async () => { + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText('Test User')).toBeInTheDocument(); + expect(screen.getByText('Other User')).toBeInTheDocument(); + }); + }); + + it('displays post timestamps', async () => { + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText(/Jan 1, 2023/)).toBeInTheDocument(); + }); + }); + + it('handles post creation', async () => { + renderSocialFeed(); + + await waitFor(() => { + const createButton = screen.getByText('Create Post'); + const contentInput = screen.getByPlaceholderText('What\'s on your mind?'); + + expect(createButton).toBeInTheDocument(); + expect(contentInput).toBeInTheDocument(); + }); + }); + + it('creates new post successfully', async () => { + renderSocialFeed(); + + await waitFor(() => { + const createButton = screen.getByText('Create Post'); + const contentInput = screen.getByPlaceholderText('What\'s on your mind?'); + + fireEvent.change(contentInput, { target: { value: 'New test post' } }); + fireEvent.click(createButton); + + expect(axios.post).toHaveBeenCalledWith('/api/posts', { + content: 'New test post' + }); + }); + }); + + it('validates post creation form', async () => { + renderSocialFeed(); + + await waitFor(() => { + const createButton = screen.getByText('Create Post'); + + // Try to create post without content + fireEvent.click(createButton); + + expect(screen.getByText('Content is required')).toBeInTheDocument(); + }); + }); + + it('handles post liking', async () => { + renderSocialFeed(); + + await waitFor(() => { + const likeButtons = screen.getAllByLabelText('Like'); + const firstLikeButton = likeButtons[0]; + + expect(firstLikeButton).toBeInTheDocument(); + + // Click like button + fireEvent.click(firstLikeButton); + + expect(axios.put).toHaveBeenCalledWith('/api/posts/post1/like'); + }); + }); + + it('updates like count correctly', async () => { + renderSocialFeed(); + + await waitFor(() => { + // Initial like count should be 2 + expect(screen.getByText('2 likes')).toBeInTheDocument(); + }); + }); + + it('displays comments correctly', async () => { + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText('Great work!')).toBeInTheDocument(); + expect(screen.getByText('Another User')).toBeInTheDocument(); + }); + }); + + it('shows comment count', async () => { + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText('1 comment')).toBeInTheDocument(); + expect(screen.getByText('0 comments')).toBeInTheDocument(); + }); + }); + + it('handles comment submission', async () => { + renderSocialFeed(); + + await waitFor(() => { + const commentInput = screen.getByPlaceholderText('Add a comment...'); + const submitButton = screen.getByText('Post Comment'); + + expect(commentInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + }); + }); + + it('submits comment successfully', async () => { + // Mock axios.post for comment creation + axios.post.mockResolvedValue({ data: { _id: 'comment2', content: 'Test comment' } }); + + renderSocialFeed(); + + await waitFor(() => { + const commentInput = screen.getByPlaceholderText('Add a comment...'); + const submitButton = screen.getByText('Post Comment'); + + fireEvent.change(commentInput, { target: { value: 'Test comment' } }); + fireEvent.click(submitButton); + + expect(axios.post).toHaveBeenCalledWith('/api/posts/post1/comments', { + content: 'Test comment' + }); + }); + }); + + it('displays image posts correctly', async () => { + renderSocialFeed(); + + await waitFor(() => { + const postImages = screen.getAllByAltText('Post image'); + expect(postImages.length).toBeGreaterThan(0); + }); + }); + + it('shows error message when API fails', async () => { + // Mock axios.get to throw error + axios.get.mockRejectedValue(new Error('Network error')); + + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText(/Failed to load posts/)).toBeInTheDocument(); + }); + }); + + it('displays empty state when no posts', async () => { + // Mock empty response + axios.get.mockResolvedValue({ data: [] }); + + renderSocialFeed(); + + await waitFor(() => { + expect(screen.getByText('No posts yet. Be the first to share!')).toBeInTheDocument(); + }); + }); + + it('handles real-time updates', async () => { + const { on } = mockSocketContext; + + renderSocialFeed(); + + await waitFor(() => { + // Simulate receiving a new post via socket + const socketCallback = on.mock.calls[0][1]; + const newPostData = { + type: 'new_post', + data: { ...mockPosts[0], _id: 'post3', content: 'New real-time post!' } + }; + + socketCallback(newPostData); + + // Verify the new post appears in the feed + expect(screen.getByText('New real-time post!')).toBeInTheDocument(); + }); + }); + + it('filters posts by type', async () => { + renderSocialFeed(); + + await waitFor(() => { + // Look for filter buttons + const filterButtons = screen.getAllByRole('button'); + const allFilter = filterButtons.find(btn => btn.textContent.includes('All')); + const textFilter = filterButtons.find(btn => btn.textContent.includes('Text')); + const imageFilter = filterButtons.find(btn => btn.textContent.includes('Images')); + + expect(allFilter).toBeInTheDocument(); + expect(textFilter).toBeInTheDocument(); + expect(imageFilter).toBeInTheDocument(); + }); + }); + + it('searches posts', async () => { + renderSocialFeed(); + + await waitFor(() => { + const searchInput = screen.getByPlaceholderText('Search posts...'); + expect(searchInput).toBeInTheDocument(); + + // Test search functionality + fireEvent.change(searchInput, { target: { value: 'cleaned' } }); + + // Should filter posts to show only relevant content + expect(screen.getByText('Just cleaned up Main Street! ๐Ÿงน')).toBeInTheDocument(); + }); + }); + + it('shows post engagement statistics', async () => { + renderSocialFeed(); + + await waitFor(() => { + // Look for engagement stats + expect(screen.getByText('Total Posts: 2')).toBeInTheDocument(); + expect(screen.getByText('Total Likes: 2')).toBeInTheDocument(); + expect(screen.getByText('Total Comments: 1')).toBeInTheDocument(); + }); + }); + + it('handles infinite scroll', async () => { + renderSocialFeed(); + + await waitFor(() => { + // Look for load more indicator + expect(screen.getByText('Load more posts')).toBeInTheDocument(); + }); + }); + + it('displays user avatars', async () => { + renderSocialFeed(); + + await waitFor(() => { + const avatars = screen.getAllByAltText('User avatar'); + expect(avatars.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/__tests__/TaskList.test.js b/frontend/src/components/__tests__/TaskList.test.js new file mode 100644 index 0000000..c247eea --- /dev/null +++ b/frontend/src/components/__tests__/TaskList.test.js @@ -0,0 +1,299 @@ +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 { SocketContext } from '../../context/SocketContext'; +import TaskList from '../TaskList'; +import axios from 'axios'; + +// Mock the contexts +const mockAuthContext = { + auth: { isAuthenticated: true, loading: false, user: { id: 'user123', name: 'Test User' } }, + login: jest.fn(), + logout: jest.fn(), +}; + +const mockSocketContext = { + socket: null, + connected: true, + on: jest.fn(), + off: jest.fn(), +}; + +// Mock axios +jest.mock('axios'); + +describe('TaskList Component', () => { + const mockTasks = [ + { + _id: 'task1', + description: 'Clean up the street', + type: 'cleaning', + status: 'pending', + pointsAwarded: 10, + street: { streetId: 'street1', name: 'Main Street' }, + createdAt: '2023-01-01T00:00:00.000Z', + }, + { + _id: 'task2', + description: 'Fix pothole', + type: 'maintenance', + status: 'completed', + pointsAwarded: 15, + street: { streetId: 'street2', name: 'Oak Street' }, + completedBy: { userId: 'user123', name: 'Test User' }, + completedAt: '2023-01-02T00:00:00.000Z', + createdAt: '2023-01-01T00:00:00.000Z', + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock axios.get to return tasks + axios.get.mockResolvedValue({ data: mockTasks }); + + // Mock axios.put to return updated task + axios.put.mockResolvedValue({ data: { ...mockTasks[0], status: 'completed' } }); + }); + + const renderTaskList = () => { + return render( + + + + + + + + ); + }; + + it('renders task list correctly', async () => { + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('Clean up the street')).toBeInTheDocument(); + expect(screen.getByText('Fix pothole')).toBeInTheDocument(); + }); + }); + + it('shows loading state initially', () => { + // Mock axios to delay response + axios.get.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ data: [] }), 100))); + + renderTaskList(); + + expect(screen.getByText('Loading tasks...')).toBeInTheDocument(); + }); + + it('displays task status correctly', async () => { + renderTaskList(); + + await waitFor(() => { + const pendingTask = screen.getByText('Clean up the street').closest('[data-status="pending"]'); + const completedTask = screen.getByText('Fix pothole').closest('[data-status="completed"]'); + + expect(pendingTask).toBeInTheDocument(); + expect(completedTask).toBeInTheDocument(); + }); + }); + + it('displays task points', async () => { + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('10 points')).toBeInTheDocument(); + expect(screen.getByText('15 points')).toBeInTheDocument(); + }); + }); + + it('displays street information', async () => { + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('Main Street')).toBeInTheDocument(); + expect(screen.getByText('Oak Street')).toBeInTheDocument(); + }); + }); + + it('handles task completion', async () => { + renderTaskList(); + + await waitFor(() => { + const completeButton = screen.getAllByText('Complete Task')[0]; + expect(completeButton).toBeInTheDocument(); + }); + + // Click complete button + fireEvent.click(completeButton); + + // Verify axios.put was called + await waitFor(() => { + expect(axios.put).toHaveBeenCalledWith('/api/tasks/task1', { + status: 'completed' + }); + }); + }); + + it('shows error message when API fails', async () => { + // Mock axios.get to throw error + axios.get.mockRejectedValue(new Error('Network error')); + + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText(/Failed to load tasks/)).toBeInTheDocument(); + }); + }); + + it('filters tasks by status', async () => { + renderTaskList(); + + await waitFor(() => { + // Find filter buttons + const filterButtons = screen.getAllByRole('button'); + const pendingFilter = filterButtons.find(btn => btn.textContent.includes('Pending')); + const completedFilter = filterButtons.find(btn => btn.textContent.includes('Completed')); + + expect(pendingFilter).toBeInTheDocument(); + expect(completedFilter).toBeInTheDocument(); + }); + }); + + it('filters tasks by type', async () => { + renderTaskList(); + + await waitFor(() => { + // Find type filter buttons + const filterButtons = screen.getAllByRole('button'); + const cleaningFilter = filterButtons.find(btn => btn.textContent.includes('Cleaning')); + const maintenanceFilter = filterButtons.find(btn => btn.textContent.includes('Maintenance')); + + expect(cleaningFilter).toBeInTheDocument(); + expect(maintenanceFilter).toBeInTheDocument(); + }); + }); + + it('displays empty state when no tasks', async () => { + // Mock empty response + axios.get.mockResolvedValue({ data: [] }); + + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('No tasks found')).toBeInTheDocument(); + }); + }); + + it('shows task creation form', async () => { + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('Create New Task')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Task description')).toBeInTheDocument(); + }); + }); + + it('validates task creation form', async () => { + renderTaskList(); + + await waitFor(() => { + const createButton = screen.getByText('Create Task'); + const descriptionInput = screen.getByPlaceholderText('Task description'); + + // Try to create task without description + fireEvent.click(createButton); + + expect(screen.getByText('Description is required')).toBeInTheDocument(); + }); + }); + + it('creates new task successfully', async () => { + // Mock axios.post for task creation + axios.post.mockResolvedValue({ data: { ...mockTasks[0], _id: 'task3' } }); + + renderTaskList(); + + await waitFor(() => { + const createButton = screen.getByText('Create Task'); + const descriptionInput = screen.getByPlaceholderText('Task description'); + const typeSelect = screen.getByDisplayValue('cleaning'); + + fireEvent.change(descriptionInput, { target: { value: 'New test task' } }); + fireEvent.change(typeSelect, { target: { value: 'maintenance' } }); + fireEvent.click(createButton); + + expect(axios.post).toHaveBeenCalledWith('/api/tasks', { + description: 'New test task', + type: 'maintenance' + }); + }); + }); + + it('handles real-time updates', async () => { + const { on } = mockSocketContext; + + renderTaskList(); + + await waitFor(() => { + // Simulate receiving a task update via socket + const socketCallback = on.mock.calls[0][1]; + const taskUpdateData = { + type: 'task_update', + data: { ...mockTasks[0], status: 'completed' } + }; + + socketCallback(taskUpdateData); + + // Verify the task list updates with new data + expect(screen.getByText('Clean up the street')).toBeInTheDocument(); + }); + }); + + it('displays task priority indicators', async () => { + renderTaskList(); + + await waitFor(() => { + // Look for priority indicators + const priorityElements = screen.getAllByTestId('task-priority'); + expect(priorityElements.length).toBeGreaterThan(0); + }); + }); + + it('shows task statistics', async () => { + renderTaskList(); + + await waitFor(() => { + expect(screen.getByText('Total Tasks: 2')).toBeInTheDocument(); + expect(screen.getByText('Completed: 1')).toBeInTheDocument(); + expect(screen.getByText('Pending: 1')).toBeInTheDocument(); + }); + }); + + it('handles pagination', async () => { + renderTaskList(); + + await waitFor(() => { + // Look for pagination controls + expect(screen.getByText('Next')).toBeInTheDocument(); + expect(screen.getByText('Previous')).toBeInTheDocument(); + }); + }); + + it('searches tasks', async () => { + renderTaskList(); + + await waitFor(() => { + const searchInput = screen.getByPlaceholderText('Search tasks...'); + expect(searchInput).toBeInTheDocument(); + + // Test search functionality + fireEvent.change(searchInput, { target: { value: 'clean' } }); + + // Should filter tasks to show only cleaning tasks + expect(screen.getByText('Clean up the street')).toBeInTheDocument(); + expect(screen.queryByText('Fix pothole')).not.toBeInTheDocument(); + }); + }); +}); \ No newline at end of file