diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index e586cd7..6e280c2 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -41,6 +41,11 @@ jobs: - name: Lint (client + server) run: npm run lint + - name: Dead code (knip) + # Blocking since 2026-07-10 (repo is knip-clean): new unused files, + # exports, or dependencies fail CI instead of rotting silently. + run: npm run knip + - name: Typecheck (client) run: npm run typecheck diff --git a/client/lib/billDrafts.ts b/client/lib/billDrafts.ts index e1bc155..247f353 100644 --- a/client/lib/billDrafts.ts +++ b/client/lib/billDrafts.ts @@ -84,6 +84,3 @@ export function makeBillDraft( snowball_exempt: !!data.snowball_exempt, }; } - -/** The pre-filled draft object produced by makeBillDraft (fed to BillModal's `initialBill`). */ -export type BillDraft = ReturnType; diff --git a/client/lib/money.ts b/client/lib/money.ts index c313f28..885016f 100644 --- a/client/lib/money.ts +++ b/client/lib/money.ts @@ -23,7 +23,11 @@ export const asCents = (n: number): Cents => n as Cents; export const asDollars = (n: number): Dollars => n as Dollars; /** Convert cents → dollars (the only way to cross the unit boundary). */ export const centsToDollars = (c: Cents): Dollars => (c / 100) as Dollars; -/** Convert dollars → cents, rounding to the nearest cent. */ +/** + * Convert dollars → cents, rounding to the nearest cent. + * @public API symmetry with centsToDollars — the sanctioned crossing in this + * direction, kept exported so no caller reinvents an ad-hoc `* 100`. + */ export const dollarsToCents = (d: Dollars): Cents => Math.round(d * 100) as Cents; // A money value as it may still arrive untyped from a form field or legacy code: @@ -51,7 +55,6 @@ const CURRENCY_LOCALES: Record = { CHF: 'de-CH', }; const CURRENCY_STORAGE_KEY = 'bt-currency'; -export const SUPPORTED_CURRENCIES = Object.keys(CURRENCY_LOCALES); function loadCurrency(): string { try { diff --git a/docs/advisory_non_bill_transaction_filters_us_ms_5000.json b/db/data/advisory_non_bill_transaction_filters_us_ms_5000.json similarity index 100% rename from docs/advisory_non_bill_transaction_filters_us_ms_5000.json rename to db/data/advisory_non_bill_transaction_filters_us_ms_5000.json diff --git a/docs/merchant_store_match_us_nems_online_5k_v0_2.json b/db/data/merchant_store_match_us_nems_online_5k_v0_2.json similarity index 100% rename from docs/merchant_store_match_us_nems_online_5k_v0_2.json rename to db/data/merchant_store_match_us_nems_online_5k_v0_2.json diff --git a/db/database.cts b/db/database.cts index 96f4898..7dd9caf 100644 --- a/db/database.cts +++ b/db/database.cts @@ -183,8 +183,7 @@ function runAdvisoryFiltersMigration(database) { if (filterCount.n === 0) { const jsonPath = path.join( __dirname, - '..', - 'docs', + 'data', 'advisory_non_bill_transaction_filters_us_ms_5000.json', ); const raw = fs.readFileSync(jsonPath, 'utf8'); @@ -240,8 +239,7 @@ function runMerchantStoreMatchMigration(database) { if (count.n === 0) { const jsonPath = path.join( __dirname, - '..', - 'docs', + 'data', 'merchant_store_match_us_nems_online_5k_v0_2.json', ); const raw = fs.readFileSync(jsonPath, 'utf8'); diff --git a/knip.json b/knip.json index ba4412a..2222466 100644 --- a/knip.json +++ b/knip.json @@ -1,23 +1,19 @@ { "$schema": "https://unpkg.com/knip@6/schema.json", "entry": [ - "server.cts", - "client/main.tsx", "workers/**/*.cts", "scripts/*.{cts,js}", "setup/*.cts", "db/migrations/*.cts", "tests/**/*.test.js", "client/**/*.test.ts", - "e2e/**/*.{js,spec.js}", - "eslint.config.mjs", - "vite.config.mjs" + "e2e/**/*.{js,spec.js}" ], "project": [ "client/**/*.{ts,tsx}", - "{routes,services,middleware,db,workers,utils,setup}/**/*.cts", - "server.cts" + "{routes,services,middleware,db,workers,utils,setup}/**/*.cts" ], - "ignore": ["client/components/ui/**", "types/**"], + "ignore": ["client/components/ui/**"], + "ignoreDependencies": ["tailwindcss", "tailwindcss-animate"], "ignoreExportsUsedInFile": true } diff --git a/package-lock.json b/package-lock.json index 55d1756..51dbc2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,6 @@ "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", - "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^1.0.0", "concurrently": "^10.0.3", "eslint": "^10.7.0", @@ -71,7 +70,6 @@ "jsdom": "^29.1.1", "knip": "^6.24.0", "lefthook": "^2.1.9", - "postcss": "^8.4.47", "prettier": "3.9.4", "tailwindcss": "^4.3.2", "typescript": "^6.0.3", @@ -5745,43 +5743,6 @@ "node": ">= 4.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", - "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.4", - "caniuse-lite": "^1.0.30001799", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -7542,20 +7503,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/framer-motion": { "version": "12.42.2", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", @@ -10083,13 +10030,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", diff --git a/package.json b/package.json index 4729d00..a4958aa 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", - "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^1.0.0", "concurrently": "^10.0.3", "eslint": "^10.7.0", @@ -94,7 +93,6 @@ "jsdom": "^29.1.1", "knip": "^6.24.0", "lefthook": "^2.1.9", - "postcss": "^8.4.47", "prettier": "3.9.4", "tailwindcss": "^4.3.2", "typescript": "^6.0.3", diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 6a9a85c..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,3 +0,0 @@ -// Tailwind v4 runs via @tailwindcss/vite (see vite.config.mjs); autoprefixing -// is built into v4, so PostCSS needs no plugins here anymore. -module.exports = { plugins: {} }; diff --git a/run-functional-test.js b/run-functional-test.js deleted file mode 100644 index e0ef62f..0000000 --- a/run-functional-test.js +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const BASE_URL = 'http://localhost:3033'; -const TEST_USER = 'admin'; -const TEST_PASS = 'admin123'; - -function runPlaywrightTest() { - const testScript = ` -const { chromium } = require('playwright'); - -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 - 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 }); - console.log('Login: PASS'); - - // 2. Create 20 bills - await page.goto('${BASE_URL}/bills'); - await page.waitForSelector('button:has-text("Add Bill")'); - - const bills = [ - { 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 (const bill of bills) { - 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)); - await page.fill('input[name="due_day"]', String(bill.dueDay)); - await page.click('button:has-text("Select category")'); - await page.waitForSelector('button:has-text("' + bill.category + '")'); - await page.click('button:has-text("' + bill.category + '")'); - if (bill.autopay) await page.click('label:has-text("Autopay")'); - if (bill.twoFA) await page.click('label:has-text("Two-factor")'); - await page.click('button:has-text("Save")'); - await page.waitForTimeout(500); - } - - const billCount = await page.locator('.bill-row').count(); - console.log('Bills created: ' + billCount); - - // 3. Test notes feature - await page.goto('${BASE_URL}/tracker'); - await page.waitForTimeout(3000); - - const billRows = await page.locator('.bill-row, .react-flow__node, .bill-card').all(); - console.log('Bills on tracker: ' + billRows.length); - - // Add notes to bills - let notesAdded = 0; - for (let i = 0; i < Math.min(20, billRows.length); i++) { - const row = billRows[i]; - await row.hover(); - await page.waitForTimeout(100); - const notesInput = await row.locator('input[placeholder*="notes"], input.notes-input').first(); - if (await notesInput.count() > 0) { - await notesInput.fill('Test note ' + (i + 1)); - await page.waitForTimeout(500); - await notesInput.blur(); - notesAdded++; - } - } - console.log('Notes added: ' + notesAdded); - - // Verify persistence - await page.reload(); - await page.waitForTimeout(2000); - const content = await page.content(); - let persisted = 0; - for (let i = 1; i <= notesAdded; i++) { - if (content.includes('Test note ' + i)) persisted++; - } - console.log('Notes persisted: ' + persisted); - - console.log('ALL TESTS COMPLETED'); - - } catch (error) { - console.error('Test error:', error.message); - } finally { - await browser.close(); - } -} - -runTests(); - `; - - fs.writeFileSync('/tmp/playwright-test.js', testScript); - - try { - const output = execSync('cd /home/kaspa/.openclaw/Projects/bill-tracker && npx playwright exec /tmp/playwright-test.js 2>&1', { - encoding: 'utf8', - maxBuffer: 1024 * 1024 * 10 - }); - console.log(output); - return output; - } catch (error) { - console.error('Error running playwright:', error.stderr || error.message); - return error.stderr || error.message; - } -} - -// Run the test -console.log('='.repeat(60)); -console.log('Bill Tracker Functional Test'); -console.log('Started:', new Date().toLocaleString()); -console.log('='.repeat(60)); -console.log(''); - -const output = runPlaywrightTest(); - -console.log(''); -console.log('='.repeat(60)); -console.log('Test Output:'); -console.log('='.repeat(60)); -console.log(output); - -// Save results to REVIEW.md -const timestamp = new Date().toLocaleString('en-US', { - timeZone: 'America/Chicago', - dateStyle: 'full', - timeStyle: 'long' -}); - -const newSection = ` -## Functional Testing Results - ${timestamp} - -### Test Run Output -\`\`\` -${output} -\`\`\` - -### 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. - ---- - -`; - -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'; -} - -const updatedContent = reviewContent.replace( - /## Functional Testing Results - .*?(?=##|$)/s, - '' -) + newSection; - -fs.writeFileSync(reviewPath, updatedContent, 'utf8'); -console.log('\n✅ Test results saved to REVIEW.md'); diff --git a/test-functional.js b/test-functional.js deleted file mode 100644 index 77bc58f..0000000 --- a/test-functional.js +++ /dev/null @@ -1,551 +0,0 @@ -// 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);