BillTracker/test-functional.js

552 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);