1<!DOCTYPE html>
2<html lang="en" data-theme="dark">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>WealthPulse - Self-Test Suite</title>
7 <link rel="stylesheet" href="style.css">
8 <style>
9 .test-container {
10 max-width: 900px;
11 margin: 40px auto;
12 padding: 24px;
13 }
14 .test-header {
15 display: flex;
16 justify-content: space-between;
17 align-items: center;
18 margin-bottom: 24px;
19 }
20 .test-summary-card {
21 background: var(--bg-card);
22 border: 1px solid var(--border-color);
23 border-radius: var(--radius-lg);
24 padding: 20px;
25 margin-bottom: 24px;
26 display: flex;
27 gap: 24px;
28 }
29 .test-metric {
30 display: flex;
31 flex-direction: column;
32 }
33 .test-metric-val {
34 font-size: 1.8rem;
35 font-weight: 800;
36 }
37 .test-metric-lbl {
38 font-size: 0.85rem;
39 color: var(--text-secondary);
40 }
41 .test-suite {
42 display: flex;
43 flex-direction: column;
44 gap: 12px;
45 }
46 .test-card {
47 background: var(--bg-card);
48 border: 1px solid var(--border-color);
49 border-radius: var(--radius-md);
50 padding: 16px 20px;
51 display: flex;
52 flex-direction: column;
53 gap: 8px;
54 }
55 .test-card.pass {
56 border-left: 4px solid var(--income-green);
57 }
58 .test-card.fail {
59 border-left: 4px solid var(--expense-rose);
60 }
61 .test-title-row {
62 display: flex;
63 justify-content: space-between;
64 align-items: center;
65 }
66 .test-name {
67 font-weight: 700;
68 font-size: 1rem;
69 }
70 .test-badge {
71 font-size: 0.75rem;
72 font-weight: 700;
73 padding: 4px 10px;
74 border-radius: var(--radius-full);
75 text-transform: uppercase;
76 }
77 .badge-pass {
78 background: var(--income-green-bg);
79 color: var(--income-green);
80 }
81 .badge-fail {
82 background: var(--expense-rose-bg);
83 color: var(--expense-rose);
84 }
85 .test-details {
86 font-size: 0.85rem;
87 color: var(--text-secondary);
88 font-family: monospace;
89 white-space: pre-wrap;
90 background: var(--bg-secondary);
91 padding: 8px 12px;
92 border-radius: var(--radius-sm);
93 }
94 </style>
95</head>
96<body>
97 <div class="test-container">
98 <div class="test-header">
99 <div>
100 <h1>🧪 Core Self-Test Suite</h1>
101 <p style="color: var(--text-secondary);">Automated unit & logic verification for WealthPulse</p>
102 </div>
103 <div style="display: flex; gap: 10px;">
104 <a href="index.html" class="btn btn-secondary" style="text-decoration: none;">← Back to Dashboard</a>
105 <button id="btn-run-tests" class="btn btn-primary">Run Tests Again</button>
106 </div>
107 </div>
108
109 <div class="test-summary-card">
110 <div class="test-metric">
111 <span id="total-tests" class="test-metric-val">0</span>
112 <span class="test-metric-lbl">Total Tests</span>
113 </div>
114 <div class="test-metric">
115 <span id="passed-tests" class="test-metric-val" style="color: var(--income-green);">0</span>
116 <span class="test-metric-lbl">Passed</span>
117 </div>
118 <div class="test-metric">
119 <span id="failed-tests" class="test-metric-val" style="color: var(--expense-rose);">0</span>
120 <span class="test-metric-lbl">Failed</span>
121 </div>
122 <div class="test-metric">
123 <span id="test-time" class="test-metric-val">0ms</span>
124 <span class="test-metric-lbl">Duration</span>
125 </div>
126 </div>
127
128 <div id="test-suite-container" class="test-suite">
129
130 </div>
131 </div>
132
133 <script src="data.js"></script>
134 <script>
135 (function () {
136 'use strict';
137
138 const resultsContainer = document.getElementById('test-suite-container');
139 const totalEl = document.getElementById('total-tests');
140 const passedEl = document.getElementById('passed-tests');
141 const failedEl = document.getElementById('failed-tests');
142 const timeEl = document.getElementById('test-time');
143 const btnRun = document.getElementById('btn-run-tests');
144
145 function assert(condition, message) {
146 if (!condition) {
147 throw new Error(message || 'Assertion failed');
148 }
149 }
150
151 function assertEqual(actual, expected, message) {
152 if (actual !== expected) {
153 throw new Error(`${message || 'Equal assertion failed'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
154 }
155 }
156
157 function assertDeepEqual(actual, expected, message) {
158 const aStr = JSON.stringify(actual);
159 const eStr = JSON.stringify(expected);
160 if (aStr !== eStr) {
161 throw new Error(`${message || 'Deep equal assertion failed'}: expected ${eStr}, got ${aStr}`);
162 }
163 }
164
165 const tests = [
166 {
167 name: 'Data Service Initialization',
168 fn: () => {
169 const service = new FinanceData.FinanceDataService();
170 const txs = service.loadTransactions();
171 assert(Array.isArray(txs), 'Transactions should be an array');
172 assert(txs.length > 0, 'Sample transactions should not be empty');
173 }
174 },
175 {
176 name: 'Transaction Validation & Sanitization',
177 fn: () => {
178 const service = new FinanceData.FinanceDataService();
179 const invalid = service.validateTransaction({ amount: -10, description: '' });
180 assertEqual(invalid.isValid, false, 'Invalid transaction should fail validation');
181 assert(invalid.errors.length >= 2, 'Should report multiple validation errors');
182
183 const valid = service.validateTransaction({
184 date: '2026-07-28',
185 description: ' Coffee Shop ',
186 amount: 15.5,
187 category: 'Food & Dining',
188 type: 'expense'
189 });
190 assertEqual(valid.isValid, true, 'Valid transaction should pass');
191 assertEqual(valid.sanitized.description, 'Coffee Shop', 'Description should be trimmed');
192 assertEqual(valid.sanitized.amount, 15.5, 'Amount should be parsed to float');
193 }
194 },
195 {
196 name: 'Add, Update, and Delete Transaction Lifecycle',
197 fn: () => {
198 const service = new FinanceData.FinanceDataService();
199 let txs = [];
200 const newTx = {
201 date: '2026-07-28',
202 description: 'Test Laptop',
203 amount: 1200,
204 category: 'Shopping',
205 type: 'expense'
206 };
207
208 txs = service.addTransaction(txs, newTx);
209 assertEqual(txs.length, 1, 'Transaction array length should be 1');
210 const createdId = txs[0].id;
211
212 txs = service.updateTransaction(txs, createdId, { amount: 1300, description: 'Test Laptop Pro' });
213 assertEqual(txs[0].amount, 1300, 'Updated amount should be 1300');
214 assertEqual(txs[0].description, 'Test Laptop Pro', 'Updated description should match');
215
216 txs = service.deleteTransaction(txs, createdId);
217 assertEqual(txs.length, 0, 'Transaction array length should be 0 after delete');
218 }
219 },
220 {
221 name: 'Summary Calculations (Income, Expense, Savings Rate)',
222 fn: () => {
223 const service = new FinanceData.FinanceDataService();
224 const sampleTxs = [
225 { type: 'income', amount: 4000 },
226 { type: 'expense', amount: 1000 },
227 { type: 'expense', amount: 500 }
228 ];
229
230 const summary = service.calculateSummary(sampleTxs);
231 assertEqual(summary.totalIncome, 4000, 'Total income calculation');
232 assertEqual(summary.totalExpense, 1500, 'Total expense calculation');
233 assertEqual(summary.netBalance, 2500, 'Net balance calculation');
234 assertEqual(summary.savingsRate, 62.5, 'Savings rate percentage calculation');
235 }
236 },
237 {
238 name: 'Category Spending Aggregation',
239 fn: () => {
240 const service = new FinanceData.FinanceDataService();
241 const sampleTxs = [
242 { date: '2026-07-01', category: 'Food & Dining', amount: 50, type: 'expense' },
243 { date: '2026-07-02', category: 'Food & Dining', amount: 30, type: 'expense' },
244 { date: '2026-07-03', category: 'Housing', amount: 1200, type: 'expense' },
245 { date: '2026-07-04', category: 'Income', amount: 3000, type: 'income' }
246 ];
247
248 const catSpending = service.calculateCategorySpending(sampleTxs);
249 assertEqual(catSpending['Food & Dining'], 80, 'Food & Dining total should be 80');
250 assertEqual(catSpending['Housing'], 1200, 'Housing total should be 1200');
251 assertEqual(catSpending['Income'], 0, 'Income category spending should be 0');
252 }
253 },
254 {
255 name: 'Spending Over Time Timeline Grouping',
256 fn: () => {
257 const service = new FinanceData.FinanceDataService();
258 const sampleTxs = [
259 { date: '2026-07-01', amount: 100, type: 'income' },
260 { date: '2026-07-01', amount: 40, type: 'expense' },
261 { date: '2026-07-02', amount: 60, type: 'expense' }
262 ];
263
264 const daily = service.calculateSpendingOverTime(sampleTxs, 'daily');
265 assertDeepEqual(daily.labels, ['2026-07-01', '2026-07-02'], 'Timeline labels order');
266 assertDeepEqual(daily.incomeValues, [100, 0], 'Daily income values');
267 assertDeepEqual(daily.expenseValues, [40, 60], 'Daily expense values');
268 }
269 },
270 {
271 name: 'Filter Transactions (Search, Category, Type, Date Range)',
272 fn: () => {
273 const service = new FinanceData.FinanceDataService();
274 const sampleTxs = [
275 { date: '2026-07-01', description: 'Supermarket Groceries', category: 'Food & Dining', type: 'expense', amount: 120 },
276 { date: '2026-07-15', description: 'Electric Bill', category: 'Utilities', type: 'expense', amount: 90 },
277 { date: '2026-07-20', description: 'Salary Bonus', category: 'Income', type: 'income', amount: 500 }
278 ];
279
280 const searchRes = service.filterTransactions(sampleTxs, { search: 'electric' });
281 assertEqual(searchRes.length, 1, 'Search query filter');
282 assertEqual(searchRes[0].category, 'Utilities', 'Search matching transaction');
283
284 const catRes = service.filterTransactions(sampleTxs, { category: 'Food & Dining' });
285 assertEqual(catRes.length, 1, 'Category filter');
286
287 const dateRes = service.filterTransactions(sampleTxs, { startDate: '2026-07-10', endDate: '2026-07-25' });
288 assertEqual(dateRes.length, 2, 'Date range filter');
289 }
290 },
291 {
292 name: 'CSV Export and Parsing Pipeline',
293 fn: () => {
294 const service = new FinanceData.FinanceDataService();
295 const sampleTxs = [
296 { id: 'tx-100', date: '2026-07-28', description: 'Gadget Store, "Special"', amount: 299.99, category: 'Shopping', type: 'expense', notes: 'Gift' }
297 ];
298
299 const csvString = service.exportToCSV(sampleTxs);
300 assert(csvString.includes('Gadget Store, ""Special""'), 'CSV escaping double quotes');
301
302 const parsed = service.parseCSV(csvString);
303 assertEqual(parsed.transactions.length, 1, 'Parsed CSV row count');
304 assertEqual(parsed.transactions[0].amount, 299.99, 'Parsed CSV transaction amount');
305 assertEqual(parsed.transactions[0].description, 'Gadget Store, "Special"', 'Parsed CSV quote unescaping');
306 }
307 },
308 {
309 name: 'Currency & Date Formatting Helpers',
310 fn: () => {
311 const service = new FinanceData.FinanceDataService();
312 assertEqual(service.formatCurrency(1234.5), '$1,234.50', 'Format positive currency');
313 assertEqual(service.formatCurrency(-45.2), '-$45.20', 'Format negative currency');
314 assertEqual(service.formatDate('2026-07-28'), 'Jul 28, 2026', 'Format ISO date string');
315 }
316 }
317 ];
318
319 function runSuite() {
320 resultsContainer.innerHTML = '';
321 let passed = 0;
322 let failed = 0;
323 const startTime = performance.now();
324
325 tests.forEach(test => {
326 const card = document.createElement('div');
327 card.className = 'test-card';
328
329 try {
330 test.fn();
331 passed++;
332 card.classList.add('pass');
333 card.innerHTML = `
334 <div class="test-title-row">
335 <span class="test-name">${test.name}</span>
336 <span class="test-badge badge-pass">PASS</span>
337 </div>
338 `;
339 } catch (err) {
340 failed++;
341 card.classList.add('fail');
342 card.innerHTML = `
343 <div class="test-title-row">
344 <span class="test-name">${test.name}</span>
345 <span class="test-badge badge-fail">FAIL</span>
346 </div>
347 <div class="test-details">${err.stack || err.message}</div>
348 `;
349 }
350
351 resultsContainer.appendChild(card);
352 });
353
354 const duration = Math.round(performance.now() - startTime);
355
356 totalEl.textContent = tests.length;
357 passedEl.textContent = passed;
358 failedEl.textContent = failed;
359 timeEl.textContent = duration + 'ms';
360 }
361
362 btnRun.addEventListener('click', runSuite);
363 runSuite();
364 })();
365 </script>
366</body>
367</html>
368
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.