Skip to content
Closed
Show file tree
Hide file tree
Changes from 74 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
9f13221
feat(models): add Message class skeleton
Oct 15, 2025
1369ab4
feat(models): add User class skeleton
Oct 15, 2025
8a8a597
feat(models): add Reaction class skeleton
Oct 15, 2025
d38ded7
feat(services): add MessageService class skeleton
Oct 15, 2025
072b6da
feat(services): add ModerationService class skeleton
Oct 15, 2025
2899fbb
feat(services): add ReactionService class skeleton
Oct 15, 2025
9cf23f6
feat(controllers): add MessageController class skeleton
Oct 15, 2025
64d8a6f
feat(utils): add Logger class skeleton
Oct 15, 2025
cbb7a82
feat(utils): add DatabaseConnector class skeleton
Oct 15, 2025
f285fdc
feat(app): add main application entry point skeleton
Oct 15, 2025
077a56a
feat(app/message): organize messaging system into React app structure
Oct 15, 2025
9097f64
test(components): add 80% coverage for MessageComposer
Oct 15, 2025
6f08d19
test(components): fix MessageItem test failures (batch 1/3)
Oct 15, 2025
1f4957f
test(components): fix MessageItem reaction emoji test (batch 2/3)
Oct 15, 2025
07dd540
test(components): complete MessageItem test coverage (batch 3/3)
Oct 15, 2025
738f97d
test(components): create MessageList test setup (batch 1/4)
Oct 15, 2025
f10b136
test(components): add MessageList core behavior tests (batch 2/4)
Oct 15, 2025
3059e5c
test: Add MessageList interaction tests - batch 3
Oct 15, 2025
460106b
test: Complete MessageList comprehensive test suite - batch 4
Oct 15, 2025
2afc2d4
test(models): initialize jest setup for reaction model
Oct 17, 2025
5dead20
test(models): add constructor initialization test
Oct 17, 2025
537caac
test(models): add toObject plain object mapping test
Oct 17, 2025
93dc0a9
test(models): add type validation test
Oct 17, 2025
a46ceed
test(models): add getEmoji method test
Oct 17, 2025
3be3782
test(models): finalize reaction model coverage (~85%)
Oct 17, 2025
28d1424
✅ Initialize Jest setup for User model
Oct 17, 2025
f064196
✅ Add valid User initialization test
Oct 17, 2025
ceda3dc
🧪 Add default value test for optional isActive field
Oct 17, 2025
27faffa
✅ Add toObject serialization tests
Oct 17, 2025
ece4331
🧪 Add method behavior tests for getIsActive and getDisplayName
Oct 17, 2025
4958054
🔧 Add edge cases and data integrity tests
Oct 17, 2025
adb3c53
✅ Initialize Jest setup for MessageService
Oct 17, 2025
e7affde
🔧 Add setup/cleanup and mock configuration
Oct 17, 2025
b9166fa
✅ Add success test for createMessage()
Oct 17, 2025
abd6b7f
🧪 Add error handling test for createMessage()
Oct 17, 2025
3dcabf6
✅ Add success test for getMessages() without filters
Oct 17, 2025
bdfe931
🧪 Add filters test for getMessages()
Oct 17, 2025
6e0524c
✅ Add getMessages error test and updateMessage success test
Oct 17, 2025
07f9ba0
✅ Commit: Added imports, global fetch mock, and initial MessageServic…
Oct 17, 2025
7542ab5
✅ Commit: Reset to initial imports and MessageService describe block …
Oct 17, 2025
a4c51c1
✅ Commit: Add mock data setup and beforeEach block
Oct 17, 2025
734fefa
✅ Commit: Add afterEach cleanup and start createMessage() test
Oct 17, 2025
9e6dfc8
✅ Commit: Complete first createMessage() success test with assertions
Oct 17, 2025
4edd75f
✅ Commit: Add createMessage() error handling test
Oct 17, 2025
42b1d48
✅ Commit: Start getMessages() tests with basic success test
Oct 17, 2025
8a02a90
✅ Commit: Complete getMessages() assertions and start filters test
Oct 17, 2025
536876f
✅ Commit: Complete filters test and add getMessages() error handling
Oct 17, 2025
258a3cd
✅ Commit: Start updateMessage() tests with success case
Oct 17, 2025
d955530
✅ Commit: Complete updateMessage() tests with headers and error handling
Oct 17, 2025
03d61ce
✅ Commit: Complete MessageService test suite with deleteMessage() tests
Oct 17, 2025
06df6c0
🐛 Fix: Update test URL expectation to match service implementation
Oct 17, 2025
ec53415
✅ Commit: Added imports, global fetch mock, and initial ModerationSer…
Oct 17, 2025
8ce7048
✅ Commit: Add mock data setup and beforeEach block
Oct 17, 2025
1dbd5e3
✅ Commit: Add afterEach cleanup and start checkContent() test
Oct 17, 2025
84cdb1f
✅ Commit: Complete first checkContent() success test with assertions
Oct 17, 2025
51a06b6
✅ Commit: Add checkContent() error handling test
Oct 17, 2025
34fc792
✅ Commit: Add filterMessage() tests for clean and banned word filtering
Oct 17, 2025
0a247fb
✅ Commit: Add loadBannedWords() success test
Oct 17, 2025
14b9539
✅ Commit: Add loadBannedWords() error handling and start quickValidat…
Oct 17, 2025
9465b2c
✅ Commit: Complete ModerationService test suite with quickValidation(…
Oct 17, 2025
b4da241
✅ Commit: Added imports, global fetch mock, and initial ReactionServi…
Oct 17, 2025
1f2f566
✅ Commit: Add mock data setup and beforeEach block
Oct 17, 2025
7627f22
✅ Commit: Add afterEach cleanup block
Oct 17, 2025
2dab4dc
✅ Commit: Add addReaction() test setup with mock request data
Oct 17, 2025
bf1dafd
✅ Commit: Complete first addReaction() success test with assertions
Oct 17, 2025
5499464
✅ Commit: Add addReaction() error handling test
Oct 17, 2025
05ec8d5
✅ Commit: Add removeReaction() success test with proper parameters
Oct 17, 2025
6bc66e3
✅ Commit: Add removeReaction() error handling test
Oct 17, 2025
67a4ae4
✅ Commit: Add getReactions() success test with Reaction array validation
Oct 17, 2025
679041e
✅ Commit: Add getReactionCounts() error handling test
Oct 17, 2025
ac1b98a
✅ Commit: Remove unnecessary messaging_system Python backend folder
Oct 17, 2025
e7c2cee
fix: fixes message.id truthiness within validMessages filter
RiceViz Oct 18, 2025
5a0b617
fix: updates test to use to timestamp instead of createdAt
RiceViz Oct 18, 2025
aab3c62
fix: updates jest in package.json
RiceViz Oct 18, 2025
3a00220
fix: revert Jest to ^29.4.5 to fix broken Vercel preview
dev-benson-03 Oct 20, 2025
0a77c4a
Merge branch 'main' into feat/dev_mssg_structure
dev-benson-03 Oct 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/**
* __tests__/components/MessageComposer.test.tsx
* Unit tests for MessageComposer component with ~80% coverage
*/

import '@testing-library/jest-dom';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MessageComposer } from '../../components/MessageComposer';
import { MessageService } from '../../services/MessageService';
import { ModerationService } from '../../services/ModerationService';

// Mock the services to avoid network calls
jest.mock('../../services/MessageService');
jest.mock('../../services/ModerationService');

const MockedMessageService = MessageService as jest.MockedClass<typeof MessageService>;
const MockedModerationService = ModerationService as jest.MockedClass<typeof ModerationService>;

describe('MessageComposer', () => {
let mockMessageService: jest.Mocked<MessageService>;
let mockModerationService: jest.Mocked<ModerationService>;
let mockOnMessageCreated: jest.Mock;

beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();

// Setup service mocks
mockMessageService = {
createMessage: jest.fn(),
} as any;

mockModerationService = {
quickValidation: jest.fn(),
checkContent: jest.fn(),
} as any;

MockedMessageService.mockImplementation(() => mockMessageService);
MockedModerationService.mockImplementation(() => mockModerationService);

mockOnMessageCreated = jest.fn();
});

// Test: Component renders with required form elements
test('renders author and content fields with submit button', () => {
render(<MessageComposer />);

expect(screen.getByLabelText(/your name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/message/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /post message/i })).toBeInTheDocument();
expect(screen.getByText(/0\/5000 characters/i)).toBeInTheDocument();
});

// Test: Custom placeholder prop is applied
test('displays custom placeholder when provided', () => {
const customPlaceholder = 'Share your thoughts...';
render(<MessageComposer placeholder={customPlaceholder} />);

expect(screen.getByPlaceholderText(customPlaceholder)).toBeInTheDocument();
});

// Test: Submit button is disabled when fields are empty (testing HTML5 validation behavior)
test('disables submit button when fields are empty', async () => {
render(<MessageComposer />);

const submitButton = screen.getByRole('button', { name: /post message/i });
expect(submitButton).toBeDisabled();

// Verify that moderation service isn't called when button is disabled
expect(mockModerationService.quickValidation).not.toHaveBeenCalled();
});

// Test: Quick validation failure blocks submission
test('shows error when quick validation fails', async () => {
mockModerationService.quickValidation.mockReturnValue(false);

render(<MessageComposer />);

await userEvent.type(screen.getByLabelText(/your name/i), 'John Doe');
await userEvent.type(screen.getByLabelText(/message/i), 'Invalid content');

fireEvent.click(screen.getByRole('button', { name: /post message/i }));

expect(screen.getByText('Message content is invalid')).toBeInTheDocument();
expect(mockModerationService.quickValidation).toHaveBeenCalledWith('Invalid content');
expect(mockModerationService.checkContent).not.toHaveBeenCalled();
});

// Test: Moderation rejection (corresponds to moderation step in sequence diagram)
test('handles moderation rejection with appropriate error message', async () => {
mockModerationService.quickValidation.mockReturnValue(true);
mockModerationService.checkContent.mockResolvedValue({
isApproved: false,
flaggedWords: ['bad'],
severity: 'high' as const,
suggestedAction: 'review' as const
});

render(<MessageComposer />);

await userEvent.type(screen.getByLabelText(/your name/i), 'John Doe');
await userEvent.type(screen.getByLabelText(/message/i), 'Bad content here');

fireEvent.click(screen.getByRole('button', { name: /post message/i }));

await waitFor(() => {
expect(screen.getByText('Message rejected: review')).toBeInTheDocument();
});

expect(mockModerationService.checkContent).toHaveBeenCalledWith('Bad content here');
expect(mockMessageService.createMessage).not.toHaveBeenCalled();
});

// Test: Successful message creation (corresponds to API call and acknowledgment in sequence diagram)
test('creates message successfully and calls onMessageCreated', async () => {
const mockCreatedMessage = {
id: 1,
author: 'John Doe',
content: 'Hello world',
timestamp: '2025-01-01T00:00:00Z'
};

mockModerationService.quickValidation.mockReturnValue(true);
mockModerationService.checkContent.mockResolvedValue({
isApproved: true,
flaggedWords: [],
severity: 'low' as const,
suggestedAction: 'approve' as const
});
mockMessageService.createMessage.mockResolvedValue(mockCreatedMessage as any);

render(<MessageComposer onMessageCreated={mockOnMessageCreated} />);

const authorInput = screen.getByLabelText(/your name/i);
const contentInput = screen.getByLabelText(/message/i);

await userEvent.type(authorInput, 'John Doe');
await userEvent.type(contentInput, 'Hello world');

fireEvent.click(screen.getByRole('button', { name: /post message/i }));

await waitFor(() => {
expect(mockOnMessageCreated).toHaveBeenCalledWith(mockCreatedMessage);
});

// Form should be reset after successful submission
expect(authorInput).toHaveValue('');
expect(contentInput).toHaveValue('');
expect(screen.getByText(/0\/5000 characters/i)).toBeInTheDocument();
});

// Test: API error handling
test('displays API error when message creation fails', async () => {
mockModerationService.quickValidation.mockReturnValue(true);
mockModerationService.checkContent.mockResolvedValue({
isApproved: true,
flaggedWords: [],
severity: 'low' as const,
suggestedAction: 'approve' as const
});
mockMessageService.createMessage.mockRejectedValue(new Error('Network error'));

render(<MessageComposer />);

await userEvent.type(screen.getByLabelText(/your name/i), 'John Doe');
await userEvent.type(screen.getByLabelText(/message/i), 'Hello world');

fireEvent.click(screen.getByRole('button', { name: /post message/i }));

await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument();
});
});

// Test: Form state during submission (loading state)
test('disables form and shows loading state while submitting', async () => {
mockModerationService.quickValidation.mockReturnValue(true);
mockModerationService.checkContent.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({
isApproved: true,
flaggedWords: [],
severity: 'low' as const,
suggestedAction: 'approve' as const
}), 100))
);

render(<MessageComposer />);

await userEvent.type(screen.getByLabelText(/your name/i), 'John Doe');
await userEvent.type(screen.getByLabelText(/message/i), 'Hello world');

fireEvent.click(screen.getByRole('button', { name: /post message/i }));

// Should show loading state
expect(screen.getByText('Posting...')).toBeInTheDocument();
expect(screen.getByLabelText(/your name/i)).toBeDisabled();
expect(screen.getByLabelText(/message/i)).toBeDisabled();
expect(screen.getByRole('button', { name: /posting.../i })).toBeDisabled();

// Wait for submission to complete
await waitFor(() => {
expect(screen.getByText('Post Message')).toBeInTheDocument();
}, { timeout: 200 });
});

// Test: Error clearing behavior
test('clears error when user starts typing after validation error', async () => {
render(<MessageComposer />);

// Bypass HTML5 validation by directly triggering the form submit event
const form = screen.getByRole('button', { name: /post message/i }).closest('form');
fireEvent.submit(form!, { preventDefault: () => {} });
expect(screen.getByText('Please fill in all fields')).toBeInTheDocument();

// Start typing in author field with valid content
await userEvent.type(screen.getByLabelText(/your name/i), 'A');
expect(screen.queryByText('Please fill in all fields')).not.toBeInTheDocument();
}); // Test: Character count updates
test('updates character count as user types', async () => {
render(<MessageComposer />);

const contentInput = screen.getByLabelText(/message/i);
await userEvent.type(contentInput, 'Hello');

expect(screen.getByText(/5\/5000 characters/i)).toBeInTheDocument();
});

// Test: Button state management based on form validity
test('enables submit button only when both fields have content', async () => {
render(<MessageComposer />);

const submitButton = screen.getByRole('button', { name: /post message/i });
const authorInput = screen.getByLabelText(/your name/i);
const contentInput = screen.getByLabelText(/message/i);

// Initially disabled
expect(submitButton).toBeDisabled();

// Still disabled with only author
await userEvent.type(authorInput, 'A');
expect(submitButton).toBeDisabled();

// Enabled when both fields have valid content
await userEvent.type(contentInput, 'Hello');
expect(submitButton).toBeEnabled();

// Disabled again when content is cleared
await userEvent.clear(contentInput);
expect(submitButton).toBeDisabled();
});
});
Loading