mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user