feat: add testing infrastructure and improve component feedback

- Added React testing setup with JSDOM
- Added component tests for AddTransactionForm and TransactionTable
- Improved error handling and success messages in components
- Updated test configuration and dependencies
- Added CSS for error and success states
This commit is contained in:
GitHub Copilot
2025-05-05 17:41:39 +00:00
parent d3855aa7e4
commit 7e5ed585f7
19 changed files with 1299 additions and 1096 deletions

View File

@@ -7,6 +7,7 @@ import {
currentAccountId as currentAccountIdStore,
transactionSaved,
transactionToEdit as transactionToEditStore,
triggerRefresh,
} from '../stores/transactionStore';
import type { Transaction } from '../types';
@@ -22,50 +23,52 @@ export default function AddTransactionForm() {
const [editingId, setEditingId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const isEditMode = !!editingId;
// --- Effects ---
// Effect to set default date on mount
useEffect(() => {
// Only set default date if not editing
if (!transactionToEdit) {
setDate(new Date().toISOString().split('T')[0]);
}
}, [transactionToEdit]); // Rerun if edit mode changes
}, [transactionToEdit]);
// Effect to populate form when editing
useEffect(() => {
if (transactionToEdit) {
setEditingId(transactionToEdit.id);
// Format date correctly for input type="date"
try {
const dateObj = new Date(transactionToEdit.date);
// Check if date is valid before formatting
if (!Number.isNaN(dateObj.getTime())) {
// Directly format the date object (usually interpreted as UTC midnight)
// into the YYYY-MM-DD format required by the input.
// No timezone adjustment needed here.
setDate(dateObj.toISOString().split('T')[0]);
} else {
console.warn('Invalid date received for editing:', transactionToEdit.date);
setDate(''); // Set to empty if invalid
setDate('');
}
} catch (e) {
console.error('Error parsing date for editing:', e);
setDate(''); // Set to empty on error
setDate('');
}
setDescription(transactionToEdit.description);
setAmount(transactionToEdit.amount.toString());
setError(null); // Clear errors when starting edit
} else {
// Reset form if transactionToEdit becomes null (e.g., after saving or cancelling)
// but only if not already resetting via handleCancel or handleSubmit
if (!isLoading) {
resetForm();
}
setError(null);
setSuccessMessage(null);
} else if (!isLoading) {
resetForm();
}
}, [transactionToEdit, isLoading]); // Add isLoading dependency
}, [transactionToEdit, isLoading]);
// Clear success message after 5 seconds
useEffect(() => {
if (successMessage) {
const timer = setTimeout(() => {
setSuccessMessage(null);
}, 5000);
return () => clearTimeout(timer);
}
}, [successMessage]);
// --- Helper Functions ---
const resetForm = () => {
@@ -74,7 +77,7 @@ export default function AddTransactionForm() {
setDescription('');
setAmount('');
setError(null);
// Don't reset isLoading here, it's handled in submit/cancel
setSuccessMessage(null);
};
const validateForm = (): string[] => {
@@ -96,7 +99,7 @@ export default function AddTransactionForm() {
errors.push('Date is required');
} else {
try {
const dateObj = new Date(`${date}T00:00:00`); // Treat input as local date
const dateObj = new Date(`${date}T00:00:00`);
if (Number.isNaN(dateObj.getTime())) {
errors.push('Invalid date format');
}
@@ -111,6 +114,7 @@ export default function AddTransactionForm() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccessMessage(null);
if (isLoading || !currentAccountId) {
if (!currentAccountId) setError('No account selected.');
@@ -126,11 +130,9 @@ export default function AddTransactionForm() {
setIsLoading(true);
try {
// Ensure date is sent in a consistent format (e.g., YYYY-MM-DD)
// The API should handle parsing this.
const transactionData = {
accountId: currentAccountId,
date: date, // Send as YYYY-MM-DD string
date: date,
description: description.trim(),
amount: Number.parseFloat(amount),
};
@@ -150,17 +152,39 @@ export default function AddTransactionForm() {
const errorData = await response.json();
errorMsg = errorData.error || errorMsg;
} catch (jsonError) {
// Ignore if response is not JSON
errorMsg = `${response.status}: ${response.statusText}`;
}
throw new Error(errorMsg);
}
const savedTransaction: Transaction = await response.json();
transactionSaved(savedTransaction); // Call store action instead of prop callback
resetForm(); // Reset form on success
// First notify about the saved transaction
transactionSaved(savedTransaction);
// Then explicitly trigger a refresh to ensure balance updates
triggerRefresh();
// Set success message before clearing form
setSuccessMessage(
isEditMode ? 'Transaction updated successfully' : 'Transaction created successfully',
);
// Only reset the form after the success message is shown
setTimeout(() => {
resetForm();
// Optionally collapse the form after success
const addTransactionSection = document.getElementById('add-transaction-section');
const toggleAddTxnBtn = document.getElementById('toggle-add-txn-btn');
if (addTransactionSection?.classList.contains('expanded')) {
addTransactionSection.classList.replace('expanded', 'collapsed');
toggleAddTxnBtn?.setAttribute('aria-expanded', 'false');
}
}, 2000);
} catch (err) {
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred';
setError(errorMessage);
setSuccessMessage(null);
} finally {
setIsLoading(false);
}
@@ -168,14 +192,23 @@ export default function AddTransactionForm() {
const handleCancel = () => {
resetForm();
cancelEditingTransaction(); // Call store action instead of prop callback
cancelEditingTransaction();
};
// --- JSX ---
return (
<form id="add-transaction-form-react" onSubmit={handleSubmit} noValidate>
<form id="add-transaction-form-react" role="form" onSubmit={handleSubmit} noValidate>
<h4>{isEditMode ? 'Edit Transaction' : 'New Transaction'}</h4>
{error && <div className="error-message">{error}</div>}
{error && (
<div className="error-message" data-testid="error-message">
{error}
</div>
)}
{successMessage && (
<div className="success-message" data-testid="success-message">
{successMessage}
</div>
)}
<div className="form-group">
<label htmlFor="txn-date-react">Date</label>