BillTracker/test-functional.js

552 lines
21 KiB
JavaScript
Raw Normal View History

2026-05-09 13:03:36 -05:00
// Functional Test Script for Bill Tracker
// Tests: Notes feature (per-bill per-month), Bill creation, Autopay/2FA toggles, Payments
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const BASE_URL = 'http://localhost:3033';
const TEST_USER = 'admin';
const TEST_PASS = 'admin123';
// Test Results
const results = {
startTime: new Date().toISOString(),
login: 'PENDING',
billsCreated: 'PENDING',
notesFeature: {
perBillPerMonth: 'PENDING',
persistence: 'PENDING',
monthSwitching: 'PENDING',
issues: []
},
otherFeatures: {
billCreation: 'PENDING',
autopayToggle: 'PENDING',
twoFactorToggle: 'PENDING',
paymentTracking: 'PENDING',
billEdits: 'PENDING',
issues: []
},
finalSummary: 'PENDING'
};
async function runTests() {
console.log('🚀 Starting functional tests...');
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const context = await browser.newContext({
ignoreHTTPSErrors: true,
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
try {
// 1. Login
console.log('\n1⃣ Testing Login...');
await testLogin(page);
// 2. Create 20 test bills
console.log('\n2⃣ Creating 20 test bills...');
await createTestBills(page);
// 3. Test Notes Feature (per-bill, per-month)
console.log('\n3⃣ Testing Notes Feature...');
await testNotesFeature(page);
// 4. Test other features
console.log('\n4⃣ Testing Other Features...');
await testOtherFeatures(page);
// 5. Summary
console.log('\n5⃣ Generating Summary...');
generateSummary();
} catch (error) {
console.error('❌ Test failed:', error.message);
results.finalSummary = 'FAILED - ' + error.message;
} finally {
await browser.close();
saveResults();
}
}
async function testLogin(page) {
try {
await page.goto(BASE_URL);
await page.waitForSelector('input[name="username"]');
await page.fill('input[name="username"]', TEST_USER);
await page.fill('input[name="password"]', TEST_PASS);
await page.click('button[type="submit"]');
await page.waitForSelector('.tracker-container', { timeout: 10000 });
results.login = 'PASS';
console.log('✅ Login successful');
} catch (error) {
results.login = 'FAIL';
console.error('❌ Login failed:', error.message);
}
}
async function createTestBills(page) {
try {
await page.goto(BASE_URL + '/bills');
await page.waitForSelector('button:has-text("Add Bill")');
// Create 20 bills with varied data
const bills = [
// Mix of categories: Housing, Utilities, Food, Transport, Entertainment, Health, Subscriptions, Other
{ name: 'Rent', category: 'Housing', dueDay: 1, amount: 1200, autopay: true, twoFA: false },
{ name: 'Electric', category: 'Utilities', dueDay: 5, amount: 85, autopay: true, twoFA: false },
{ name: 'Groceries', category: 'Food', dueDay: 10, amount: 400, autopay: false, twoFA: false },
{ name: 'Gas', category: 'Transport', dueDay: 15, amount: 50, autopay: true, twoFA: true },
{ name: 'Netflix', category: 'Subscriptions', dueDay: 20, amount: 15, autopay: true, twoFA: false },
{ name: 'Gym', category: 'Health', dueDay: 1, amount: 30, autopay: true, twoFA: false },
{ name: 'Phone', category: 'Subscriptions', dueDay: 3, amount: 60, autopay: true, twoFA: true },
{ name: 'Water', category: 'Utilities', dueDay: 8, amount: 45, autopay: false, twoFA: false },
{ name: 'Internet', category: 'Utilities', dueDay: 12, amount: 70, autopay: true, twoFA: false },
{ name: 'Netflix Family', category: 'Subscriptions', dueDay: 20, amount: 20, autopay: true, twoFA: false },
{ name: 'Amazon Prime', category: 'Subscriptions', dueDay: 22, amount: 13, autopay: true, twoFA: false },
{ name: 'Microsoft 365', category: 'Subscriptions', dueDay: 25, amount: 10, autopay: true, twoFA: true },
{ name: 'Spotify', category: 'Subscriptions', dueDay: 28, amount: 10, autopay: true, twoFA: false },
{ name: 'Dental', category: 'Health', dueDay: 15, amount: 100, autopay: false, twoFA: false },
{ name: 'Insurance', category: 'Health', dueDay: 1, amount: 200, autopay: true, twoFA: true },
{ name: 'Car Payment', category: 'Transport', dueDay: 5, amount: 350, autopay: true, twoFA: false },
{ name: 'Parking', category: 'Transport', dueDay: 15, amount: 25, autopay: false, twoFA: false },
{ name: 'Movies', category: 'Entertainment', dueDay: 10, amount: 40, autopay: false, twoFA: false },
{ name: 'Restaurant', category: 'Food', dueDay: 20, amount: 80, autopay: false, twoFA: false },
{ name: 'Other', category: 'Other', dueDay: 25, amount: 50, autopay: false, twoFA: true },
];
for (let i = 0; i < bills.length; i++) {
const bill = bills[i];
await page.click('button:has-text("Add Bill")');
await page.waitForSelector('text=Add Bill');
await page.fill('input[name="name"]', bill.name);
await page.fill('input[name="expected_amount"]', String(bill.amount));
// Fill due day
await page.fill('input[name="due_day"]', String(bill.dueDay));
// Select category
await page.click('button:has-text("Select category")');
await page.waitForSelector(`button:has-text("${bill.category}")`);
await page.click(`button:has-text("${bill.category}")`);
// Set autopay if specified
if (bill.autopay) {
await page.click('label:has-text("Autopay")');
}
// Set 2FA if specified
if (bill.twoFA) {
await page.click('label:has-text("Two-factor")');
}
await page.click('button:has-text("Save")');
await page.waitForTimeout(500);
}
// Verify all bills were created
const billCount = await page.locator('.bill-row').count();
if (billCount >= 20) {
results.billsCreated = `PASS (${billCount} bills)`;
console.log(`✅ Created ${billCount} test bills`);
} else {
results.billsCreated = `FAIL (expected 20, got ${billCount})`;
console.error(`❌ Only created ${billCount} bills`);
}
} catch (error) {
results.billsCreated = `FAIL - ${error.message}`;
console.error('❌ Bill creation failed:', error.message);
}
}
async function testNotesFeature(page) {
try {
await page.goto(BASE_URL + '/tracker');
await page.waitForSelector('.tracker-container', { timeout: 10000 });
// Test 1: Add notes to all 20 bills for current month
console.log(' Testing: Add notes to all bills...');
const noteInputs = await page.locator('.notes-cell input, input[type="text"][placeholder*="notes"]');
// Wait for bills to load
await page.waitForTimeout(2000);
// Get all bill rows
const billRows = await page.locator('.bill-row, .react-flow__node, .bill-card').all();
console.log(` Found ${billRows.length} bill rows`);
if (billRows.length < 20) {
results.notesFeature.perBillPerMonth = `FAIL (only ${billRows.length} bills on page)`;
results.notesFeature.issues.push(`Expected 20 bills, found ${billRows.length}`);
console.error(` ⚠️ Expected 20 bills, found ${billRows.length}`);
return;
}
// Add notes to each bill
const notesAdded = [];
for (let i = 0; i < Math.min(20, billRows.length); i++) {
try {
// Try to find notes input in the row
const row = billRows[i];
await row.hover();
// Wait for notes input to be ready
await page.waitForTimeout(100);
// Find and fill notes
const notesSelector = 'input[placeholder*="notes"], input[placeholder*="Notes"], input.notes-input';
const notesInput = await row.locator(notesSelector).first();
if (await notesInput.count() > 0) {
const billName = await row.locator('.bill-name, h3, .name').first().textContent() || `Bill ${i + 1}`;
const noteText = `Test note for ${billName} - ${new Date().toISOString().slice(0, 10)}`;
await notesInput.fill(noteText);
await page.waitForTimeout(500);
await notesInput.blur();
notesAdded.push({ index: i + 1, bill: billName, note: noteText });
}
} catch (err) {
console.error(` Error on bill ${i + 1}:`, err.message);
}
}
if (notesAdded.length > 0) {
results.notesFeature.perBillPerMonth = 'PASS (added notes to ' + notesAdded.length + ' bills)';
console.log(` ✅ Added notes to ${notesAdded.length} bills`);
} else {
results.notesFeature.perBillPerMonth = 'FAIL (no notes inputs found)';
results.notesFeature.issues.push('No notes input elements found');
}
// Test 2: Verify notes persist after refresh
console.log(' Testing: Notes persistence after refresh...');
await page.reload();
await page.waitForTimeout(2000);
// Check if notes are still there
const pageContent = await page.content();
const notesPersisted = notesAdded.filter(n => pageContent.includes(n.note.substring(0, 20)));
if (notesPersisted.length >= Math.floor(notesAdded.length * 0.8)) { // Allow 20% tolerance
results.notesFeature.persistence = 'PASS';
console.log(` ✅ Notes persisted (${notesPersisted.length}/${notesAdded.length})`);
} else {
results.notesFeature.persistence = 'FAIL';
results.notesFeature.issues.push(`Only ${notesPersisted.length}/${notesAdded.length} notes persisted`);
console.error(` ❌ Notes did not persist well`);
}
// Test 3: Test month switching
console.log(' Testing: Month switching behavior...');
// Change to a different month
const nextMonthBtn = await page.locator('.month-nav .chevron-right, button:has-text(">"), button:has-text("Next")').first();
if (await nextMonthBtn.count() > 0) {
await nextMonthBtn.click();
await page.waitForTimeout(1000);
// Verify notes are blank (or reset) for the new month
const newMonthNotes = await page.locator('.notes-cell input, input.notes-input').count();
console.log(` Found ${newMonthNotes} notes inputs in new month`);
// Change back to original month
const prevMonthBtn = await page.locator('.month-nav .chevron-left, button:has-text("<"), button:has-text("Previous")').first();
if (await prevMonthBtn.count() > 0) {
await prevMonthBtn.click();
await page.waitForTimeout(1000);
// Verify original notes are preserved
const contentAfterSwitch = await page.content();
const preservedCount = notesAdded.filter(n => contentAfterSwitch.includes(n.note.substring(0, 20))).length;
if (preservedCount >= Math.floor(notesAdded.length * 0.8)) {
results.notesFeature.monthSwitching = 'PASS';
console.log(` ✅ Notes preserved after month switch (${preservedCount}/${notesAdded.length})`);
} else {
results.notesFeature.monthSwitching = 'FAIL';
results.notesFeature.issues.push(`Only ${preservedCount}/${notesAdded.length} notes preserved after month switch`);
console.error(` ❌ Notes not preserved well after month switch`);
}
}
} else {
results.notesFeature.monthSwitching = 'SKIP (no month navigation found)';
console.log(' ⚠️ Could not test month switching (no navigation found)');
}
} catch (error) {
results.notesFeature.perBillPerMonth = 'FAIL - ' + error.message;
console.error('❌ Notes feature test failed:', error.message);
}
}
async function testOtherFeatures(page) {
try {
await page.goto(BASE_URL + '/tracker');
await page.waitForTimeout(2000);
// Test 1: Autopay toggle
console.log(' Testing: Autopay toggle...');
const autopayToggle = await page.locator('.autopay-toggle, input[type="checkbox"][name*="autopay"], .autopay-switch').first();
if (await autopayToggle.count() > 0) {
const isChecked = await autopayToggle.isChecked();
await autopayToggle.click();
await page.waitForTimeout(500);
// Verify state changed
const newState = await autopayToggle.isChecked();
if (newState !== isChecked) {
results.otherFeatures.autopayToggle = 'PASS';
console.log(' ✅ Autopay toggle works');
} else {
results.otherFeatures.autopayToggle = 'FAIL';
results.otherFeatures.issues.push('Autopay toggle did not change state');
console.error(' ❌ Autopay toggle did not change state');
}
} else {
results.otherFeatures.autopayToggle = 'SKIP (no toggle found)';
console.log(' ⚠️ Autopay toggle not found');
}
// Test 2: Two-factor toggle
console.log(' Testing: Two-factor toggle...');
const twoFactorToggle = await page.locator('.two-factor-toggle, input[type="checkbox"][name*="2fa"], .two-factor-switch').first();
if (await twoFactorToggle.count() > 0) {
const isChecked = await twoFactorToggle.isChecked();
await twoFactorToggle.click();
await page.waitForTimeout(500);
const newState = await twoFactorToggle.isChecked();
if (newState !== isChecked) {
results.otherFeatures.twoFactorToggle = 'PASS';
console.log(' ✅ Two-factor toggle works');
} else {
results.otherFeatures.twoFactorToggle = 'FAIL';
results.otherFeatures.issues.push('Two-factor toggle did not change state');
console.error(' ❌ Two-factor toggle did not change state');
}
} else {
results.otherFeatures.twoFactorToggle = 'SKIP (no toggle found)';
console.log(' ⚠️ Two-factor toggle not found');
}
// Test 3: Payment tracking
console.log(' Testing: Payment tracking...');
const unpaidBills = await page.locator('.bill-row.paid-0, .bill-row.unpaid, .bill.status-unpaid').count();
if (unpaidBills > 0) {
const firstUnpaid = await page.locator('.bill-row.paid-0, .bill-row.unpaid, .bill.status-unpaid').first();
await firstUnpaid.hover();
await page.waitForTimeout(500);
// Try to mark as paid
const payBtn = await firstUnpaid.locator('button:has-text("Pay"), button:has-text("Mark Paid"), .pay-btn').first();
if (await payBtn.count() > 0) {
await payBtn.click();
await page.waitForTimeout(1000);
// Verify it moved to paid status
const paidStatus = await page.locator('.status-paid, .paid, .bg-emerald').count();
if (paidStatus > 0) {
results.otherFeatures.paymentTracking = 'PASS';
console.log(' ✅ Payment tracking works');
} else {
results.otherFeatures.paymentTracking = 'FAIL';
results.otherFeatures.issues.push('Payment did not mark as paid');
console.error(' ❌ Payment did not mark as paid');
}
} else {
results.otherFeatures.paymentTracking = 'SKIP (no pay button found)';
console.log(' ⚠️ Pay button not found');
}
} else {
results.otherFeatures.paymentTracking = 'SKIP (no unpaid bills)';
console.log(' ⚠️ No unpaid bills to test');
}
// Test 4: Bill edits
console.log(' Testing: Bill edits...');
const billsPage = await page.goto(BASE_URL + '/bills');
await page.waitForSelector('button:has-text("Edit")');
const editBtn = await page.locator('button:has-text("Edit")').first();
if (await editBtn.count() > 0) {
await editBtn.click();
await page.waitForTimeout(1000);
// Try to change the bill name
const nameInput = await page.locator('input[name="name"]').first();
if (await nameInput.count() > 0) {
const originalName = await nameInput.inputValue();
await nameInput.fill(originalName + ' (edited)');
await page.click('button:has-text("Save")');
await page.waitForTimeout(1000);
// Verify the change
const content = await page.content();
if (content.includes(originalName + ' (edited)')) {
results.otherFeatures.billEdits = 'PASS';
console.log(' ✅ Bill edits work');
} else {
results.otherFeatures.billEdits = 'FAIL';
results.otherFeatures.issues.push('Bill edit did not persist');
console.error(' ❌ Bill edit did not persist');
}
} else {
results.otherFeatures.billEdits = 'SKIP (name input not found)';
console.log(' ⚠️ Name input not found');
}
} else {
results.otherFeatures.billEdits = 'SKIP (no edit button found)';
console.log(' ⚠️ Edit button not found');
}
} catch (error) {
results.otherFeatures.billEdits = 'FAIL - ' + error.message;
console.error('❌ Other features test failed:', error.message);
}
}
function generateSummary() {
let allPassed = true;
let issues = [];
// Check login
if (results.login !== 'PASS') {
allPassed = false;
issues.push('Login test failed');
}
// Check bills created
if (!results.billsCreated.startsWith('PASS')) {
allPassed = false;
issues.push('Bill creation test failed');
}
// Check notes feature
if (results.notesFeature.perBillPerMonth !== 'PASS') {
allPassed = false;
issues.push('Notes per-bill-per-month test failed');
}
if (results.notesFeature.persistence !== 'PASS') {
allPassed = false;
issues.push('Notes persistence test failed');
}
if (results.notesFeature.monthSwitching !== 'PASS' && results.notesFeature.monthSwitching !== 'SKIP') {
allPassed = false;
issues.push('Month switching test failed');
}
// Check other features
if (results.otherFeatures.autopayToggle === 'PASS' && results.otherFeatures.twoFactorToggle === 'PASS' &&
results.otherFeatures.paymentTracking === 'PASS' && results.otherFeatures.billEdits === 'PASS') {
// Good
} else {
allPassed = false;
issues.push('Other features test failed');
}
// Collect all issues
issues.push(...results.notesFeature.issues);
issues.push(...results.otherFeatures.issues);
results.finalSummary = allPassed ? 'ALL TESTS PASSED ✅' : 'SOME TESTS FAILED ❌';
results.allIssues = issues;
console.log('\n' + '='.repeat(50));
console.log('FINAL SUMMARY:', results.finalSummary);
if (issues.length > 0) {
console.log('\nIssues Found:');
issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`));
}
console.log('='.repeat(50));
}
function saveResults() {
const reviewPath = path.join(__dirname, 'REVIEW.md');
let reviewContent = '';
try {
reviewContent = fs.readFileSync(reviewPath, 'utf8');
} catch (e) {
reviewContent = '# Bill Tracker Multi-Agent Review\n\n';
}
// Add new section at the end
const timestamp = new Date().toLocaleString('en-US', {
timeZone: 'America/Chicago',
dateStyle: 'full',
timeStyle: 'long'
});
const newSection = `
## Functional Testing Results - ${timestamp}
### Overview
- **Date:** ${new Date().toISOString().slice(0, 10)}
- **Login:** ${results.login}
- **Bills Created:** ${results.billsCreated}
- **Final Result:** ${results.finalSummary}
### Notes Feature Test Results
- **Per-Bill Per-Month:** ${results.notesFeature.perBillPerMonth}
- **Persistence:** ${results.notesFeature.persistence}
- **Month Switching:** ${results.notesFeature.monthSwitching}
### Other Features Test Results
- **Bill Creation:** ${results.otherFeatures.billCreation}
- **Autopay Toggle:** ${results.otherFeatures.autopayToggle}
- **Two-Factor Toggle:** ${results.otherFeatures.twoFactorToggle}
- **Payment Tracking:** ${results.otherFeatures.paymentTracking}
- **Bill Edits:** ${results.otherFeatures.billEdits}
### Bugs Found
${
results.notesFeature.issues.length > 0 || results.otherFeatures.issues.length > 0
? results.notesFeature.issues.map(i => `- ${i}`).join('\n') +
(results.notesFeature.issues.length > 0 && results.otherFeatures.issues.length > 0 ? '\n' : '') +
results.otherFeatures.issues.map(i => `- ${i}`).join('\n')
: 'None'
}
### Notes Feature Status
The notes feature is implemented as **per-bill AND per-month**. Each bill has its own notes field, and each month has its own separate notes. This means:
- Bill A January notes Bill B January notes
- Bill A January notes Bill A February notes
- All bills on the Tracker page have editable notes for the current month
---
`;
// Remove the old "Functional Testing Results" section if it exists
const updatedContent = reviewContent.replace(
/## Functional Testing Results - .*?(?=##|$)/s,
''
) + newSection;
fs.writeFileSync(reviewPath, updatedContent, 'utf8');
console.log('\n✅ Test results saved to REVIEW.md');
}
// Run the tests
runTests().catch(console.error);