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>ApexFinance — Core Logic Self Tests</title>
7 <style>
8 :root {
9 --bg: #0b0f17;
10 --card-bg: #111827;
11 --text: #f8fafc;
12 --muted: #94a3b8;
13 --pass: #10b981;
14 --fail: #f43f5e;
15 --border: rgba(255, 255, 255, 0.1);
16 }
17 body {
18 font-family: 'Inter', system-ui, -apple-system, sans-serif;
19 background: var(--bg);
20 color: var(--text);
21 max-width: 900px;
22 margin: 40px auto;
23 padding: 20px;
24 }
25 h1 { margin-bottom: 8px; font-size: 24px; }
26 .summary-box {
27 background: var(--card-bg);
28 padding: 16px 20px;
29 border-radius: 10px;
30 border: 1px solid var(--border);
31 display: flex;
32 gap: 24px;
33 margin-bottom: 24px;
34 }
35 .metric-val { font-size: 22px; font-weight: bold; }
36 .pass-val { color: var(--pass); }
37 .fail-val { color: var(--fail); }
38 .test-suite {
39 display: flex;
40 flex-direction: column;
41 gap: 10px;
42 }
43 .test-card {
44 background: var(--card-bg);
45 padding: 14px 18px;
46 border-radius: 8px;
47 border: 1px solid var(--border);
48 display: flex;
49 align-items: center;
50 justify-content: space-between;
51 }
52 .test-title { font-weight: 500; font-size: 14px; }
53 .test-badge {
54 padding: 4px 10px;
55 border-radius: 999px;
56 font-size: 12px;
57 font-weight: bold;
58 }
59 .badge-pass { background: rgba(16, 185, 129, 0.2); color: var(--pass); }
60 .badge-fail { background: rgba(244, 63, 94, 0.2); color: var(--fail); }
61 .error-msg { font-size: 12px; color: var(--fail); margin-top: 4px; }
62 .nav-btn {
63 display: inline-block;
64 margin-bottom: 20px;
65 padding: 8px 16px;
66 background: #6366f1;
67 color: #fff;
68 border-radius: 6px;
69 text-decoration: none;
70 font-size: 13px;
71 font-weight: 600;
72 }
73 </style>
74 <script src="data.js"></script>
75</head>
76<body>
77 <a href="index.html" class="nav-btn">← Back to Dashboard</a>
78 <h1>🧪 Core Logic Self-Test Runner</h1>
79 <p style="color: var(--muted); margin-bottom: 20px;">Automated assertion battery testing data calculations, validations, CRUD operations, and CSV features.</p>
80
81 <div class="summary-box">
82 <div>
83 <div style="font-size: 12px; color: var(--muted);">TOTAL TESTS</div>
84 <div id="total-count" class="metric-val">0</div>
85 </div>
86 <div>
87 <div style="font-size: 12px; color: var(--muted);">PASSED</div>
88 <div id="pass-count" class="metric-val pass-val">0</div>
89 </div>
90 <div>
91 <div style="font-size: 12px; color: var(--muted);">FAILED</div>
92 <div id="fail-count" class="metric-val fail-val">0</div>
93 </div>
94 </div>
95
96 <div id="test-list" class="test-suite"></div>
97
98 <script>
99 (function () {
100 const tests = [];
101 const results = [];
102
103 function test(description, fn) {
104 tests.push({ description, fn });
105 }
106
107 function assert(condition, message) {
108 if (!condition) {
109 throw new Error(message || 'Assertion failed');
110 }
111 }
112
113 function assertEqual(actual, expected, message) {
114 if (actual !== expected) {
115 throw new Error((message ? message + ': ' : '') + `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
116 }
117 }
118
119 const FD = window.FinanceData;
120
121
122 test('1. Default Categories & Seed Data', () => {
123 assert(Array.isArray(FD.DEFAULT_CATEGORIES), 'DEFAULT_CATEGORIES should be an array');
124 assert(FD.DEFAULT_CATEGORIES.length >= 5, 'Should have at least 5 default categories');
125 assert(Array.isArray(FD.DEFAULT_TRANSACTIONS), 'DEFAULT_TRANSACTIONS should be an array');
126 assert(FD.DEFAULT_TRANSACTIONS.length > 0, 'Should have seed transactions');
127 });
128
129 test('2. Transaction Validation Logic', () => {
130 const validTx = { date: '2026-07-28', description: 'Lunch', amount: 15.50, type: 'expense', category: 'food' };
131 const v1 = FD.validateTransaction(validTx);
132 assertEqual(v1.valid, true, 'Valid transaction should pass validation');
133
134 const invalidTx = { date: 'invalid-date', description: '', amount: -10, type: 'unknown', category: '' };
135 const v2 = FD.validateTransaction(invalidTx);
136 assertEqual(v2.valid, false, 'Invalid transaction should fail validation');
137 assert(v2.errors.length >= 3, 'Should list multiple validation errors');
138 });
139
140 test('3. Financial Summary Calculation', () => {
141 const sample = [
142 { amount: 1000, type: 'income' },
143 { amount: 250, type: 'expense' },
144 { amount: 150, type: 'expense' }
145 ];
146 const summary = FD.calculateSummary(sample);
147 assertEqual(summary.totalIncome, 1000, 'Total income math');
148 assertEqual(summary.totalExpenses, 400, 'Total expenses math');
149 assertEqual(summary.netSavings, 600, 'Net savings math');
150 assertEqual(summary.savingsRate, 60, 'Savings rate percentage');
151 });
152
153 test('4. Category Spending & Budget Thresholds', () => {
154 const testCats = [
155 { id: 'food', name: 'Food', budget: 100, icon: '🍔', color: '#000' }
156 ];
157 const sampleTx = [
158 { amount: 85, type: 'expense', category: 'food' }
159 ];
160 const catSpending = FD.calculateCategorySpending(sampleTx, testCats);
161 assertEqual(catSpending.length, 1);
162 assertEqual(catSpending[0].spent, 85);
163 assertEqual(catSpending[0].remaining, 15);
164 assertEqual(catSpending[0].percentage, 85);
165 assertEqual(catSpending[0].status, 'warning', '85% spent should trigger warning status');
166 });
167
168 test('5. Daily Spending Trends Generator', () => {
169 const sampleTx = [
170 { date: '2026-07-20', amount: 500, type: 'income' },
171 { date: '2026-07-20', amount: 100, type: 'expense' },
172 { date: '2026-07-21', amount: 50, type: 'expense' }
173 ];
174 const trends = FD.calculateDailySpendingTrends(sampleTx);
175 assert(Array.isArray(trends), 'Trends should be an array');
176 assert(trends.length >= 2, 'Should cover date range');
177 const day1 = trends.find(t => t.date === '2026-07-20');
178 assert(day1 !== undefined, 'Should contain date 2026-07-20');
179 assertEqual(day1.income, 500);
180 assertEqual(day1.expense, 100);
181 assertEqual(day1.net, 400);
182 });
183
184 test('6. Filtering & Sorting', () => {
185 const list = [
186 { id: '1', date: '2026-07-01', category: 'food', amount: 50, description: 'Apple Market' },
187 { id: '2', date: '2026-07-10', category: 'housing', amount: 1000, description: 'Rent' }
188 ];
189
190 const searchRes = FD.filterTransactions(list, { search: 'rent' });
191 assertEqual(searchRes.length, 1);
192 assertEqual(searchRes[0].id, '2');
193
194 const catRes = FD.filterTransactions(list, { category: 'food' });
195 assertEqual(catRes.length, 1);
196 assertEqual(catRes[0].id, '1');
197
198 const sortedDesc = FD.sortTransactions(list, 'amount', 'desc');
199 assertEqual(sortedDesc[0].id, '2');
200 });
201
202 test('7. Add, Update & Delete CRUD Operations', () => {
203 let list = [];
204 const newTx = { date: '2026-07-28', description: 'Test Item', amount: 42, type: 'expense', category: 'shopping' };
205
206
207 list = FD.addTransaction(list, newTx);
208 assertEqual(list.length, 1);
209 const createdId = list[0].id;
210 assert(createdId.startsWith('tx-'), 'Should generate unique transaction ID');
211
212
213 const updateData = { date: '2026-07-28', description: 'Updated Test Item', amount: 99, type: 'expense', category: 'shopping' };
214 list = FD.updateTransaction(list, createdId, updateData);
215 assertEqual(list[0].amount, 99);
216 assertEqual(list[0].description, 'Updated Test Item');
217
218
219 list = FD.deleteTransaction(list, createdId);
220 assertEqual(list.length, 0);
221 });
222
223 test('8. CSV Export & Import Roundtrip', () => {
224 const sample = [
225 { id: 'tx-100', date: '2026-07-28', type: 'expense', category: 'food', amount: 25.5, description: 'Dinner, Special', notes: 'Note with "quotes"' }
226 ];
227
228 const csvString = FD.generateCSV(sample);
229 assert(csvString.includes('Dinner, Special'), 'CSV string should include description');
230
231 const parsed = FD.parseCSV(csvString);
232 assertEqual(parsed.success, true, 'CSV import should succeed');
233 assertEqual(parsed.transactions.length, 1, 'Should parse 1 transaction');
234 assertEqual(parsed.transactions[0].amount, 25.5);
235 assertEqual(parsed.transactions[0].description, 'Dinner, Special');
236 });
237
238
239 let passCount = 0;
240 let failCount = 0;
241
242 const listElem = document.getElementById('test-list');
243
244 tests.forEach(t => {
245 const card = document.createElement('div');
246 card.className = 'test-card';
247
248 try {
249 t.fn();
250 passCount++;
251 card.innerHTML = `
252 <div>
253 <div class="test-title">${t.description}</div>
254 </div>
255 <span class="test-badge badge-pass">PASSED</span>
256 `;
257 results.push({ description: t.description, passed: true });
258 } catch (err) {
259 failCount++;
260 card.innerHTML = `
261 <div>
262 <div class="test-title">${t.description}</div>
263 <div class="error-msg">${err.message}</div>
264 </div>
265 <span class="test-badge badge-fail">FAILED</span>
266 `;
267 results.push({ description: t.description, passed: false, error: err.message });
268 }
269
270 listElem.appendChild(card);
271 });
272
273 document.getElementById('total-count').textContent = tests.length;
274 document.getElementById('pass-count').textContent = passCount;
275 document.getElementById('fail-count').textContent = failCount;
276
277 window.__TEST_RESULTS__ = {
278 total: tests.length,
279 passed: passCount,
280 failed: failCount,
281 results
282 };
283 })();
284 </script>
285</body>
286</html>
287
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.