chore: dead-code sweep — knip now clean and blocking in CI (Phase 5)

- delete root test-functional.js (imported uninstalled 'playwright' —
  could never run) and run-functional-test.js (legacy pre-Playwright)
- delete postcss.config.js + autoprefixer/postcss devDeps (tailwind v4's
  vite plugin owns the pipeline; the config had no plugins left)
- client/lib/money.ts: drop unused SUPPORTED_CURRENCIES; keep
  dollarsToCents exported + @public-tagged (API symmetry with
  centsToDollars — the sanctioned unit crossing)
- client/lib/billDrafts.ts: drop unused BillDraft type export
- move the two runtime seed JSONs (advisory filters, merchant-store
  matches; 7 MB) out of docs/ into db/data/ next to their loader;
  fresh-DB seed verified (5000+5000 rows); Dockerfile COPY . covers it
- knip.json: apply config hints, ignore the two CSS-side tailwind deps;
  npx knip exits 0 — added to CI as a BLOCKING step so unused
  files/exports/deps fail instead of rotting (this is how the WebAuthn
  ghost feature survived four QA cycles)

NOT deleted: client/public/img/doingmypart.jpg — flagged unused by the
code grep but actually referenced from HISTORY.md markdown that the
release-notes renderer serves (user catch). Lesson encoded in the QA
plan: asset-reference sweeps must include rendered markdown content.

Full ci green (252 server / 52 client / build / knip 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-10 18:15:04 -05:00
parent 10eaf73e90
commit a31a774f95
12 changed files with 16 additions and 821 deletions

View File

@ -41,6 +41,11 @@ jobs:
- name: Lint (client + server) - name: Lint (client + server)
run: npm run lint 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) - name: Typecheck (client)
run: npm run typecheck run: npm run typecheck

View File

@ -84,6 +84,3 @@ export function makeBillDraft(
snowball_exempt: !!data.snowball_exempt, snowball_exempt: !!data.snowball_exempt,
}; };
} }
/** The pre-filled draft object produced by makeBillDraft (fed to BillModal's `initialBill`). */
export type BillDraft = ReturnType<typeof makeBillDraft>;

View File

@ -23,7 +23,11 @@ export const asCents = (n: number): Cents => n as Cents;
export const asDollars = (n: number): Dollars => n as Dollars; export const asDollars = (n: number): Dollars => n as Dollars;
/** Convert cents → dollars (the only way to cross the unit boundary). */ /** Convert cents → dollars (the only way to cross the unit boundary). */
export const centsToDollars = (c: Cents): Dollars => (c / 100) as Dollars; 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; 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: // A money value as it may still arrive untyped from a form field or legacy code:
@ -51,7 +55,6 @@ const CURRENCY_LOCALES: Record<string, string> = {
CHF: 'de-CH', CHF: 'de-CH',
}; };
const CURRENCY_STORAGE_KEY = 'bt-currency'; const CURRENCY_STORAGE_KEY = 'bt-currency';
export const SUPPORTED_CURRENCIES = Object.keys(CURRENCY_LOCALES);
function loadCurrency(): string { function loadCurrency(): string {
try { try {

View File

@ -183,8 +183,7 @@ function runAdvisoryFiltersMigration(database) {
if (filterCount.n === 0) { if (filterCount.n === 0) {
const jsonPath = path.join( const jsonPath = path.join(
__dirname, __dirname,
'..', 'data',
'docs',
'advisory_non_bill_transaction_filters_us_ms_5000.json', 'advisory_non_bill_transaction_filters_us_ms_5000.json',
); );
const raw = fs.readFileSync(jsonPath, 'utf8'); const raw = fs.readFileSync(jsonPath, 'utf8');
@ -240,8 +239,7 @@ function runMerchantStoreMatchMigration(database) {
if (count.n === 0) { if (count.n === 0) {
const jsonPath = path.join( const jsonPath = path.join(
__dirname, __dirname,
'..', 'data',
'docs',
'merchant_store_match_us_nems_online_5k_v0_2.json', 'merchant_store_match_us_nems_online_5k_v0_2.json',
); );
const raw = fs.readFileSync(jsonPath, 'utf8'); const raw = fs.readFileSync(jsonPath, 'utf8');

View File

@ -1,23 +1,19 @@
{ {
"$schema": "https://unpkg.com/knip@6/schema.json", "$schema": "https://unpkg.com/knip@6/schema.json",
"entry": [ "entry": [
"server.cts",
"client/main.tsx",
"workers/**/*.cts", "workers/**/*.cts",
"scripts/*.{cts,js}", "scripts/*.{cts,js}",
"setup/*.cts", "setup/*.cts",
"db/migrations/*.cts", "db/migrations/*.cts",
"tests/**/*.test.js", "tests/**/*.test.js",
"client/**/*.test.ts", "client/**/*.test.ts",
"e2e/**/*.{js,spec.js}", "e2e/**/*.{js,spec.js}"
"eslint.config.mjs",
"vite.config.mjs"
], ],
"project": [ "project": [
"client/**/*.{ts,tsx}", "client/**/*.{ts,tsx}",
"{routes,services,middleware,db,workers,utils,setup}/**/*.cts", "{routes,services,middleware,db,workers,utils,setup}/**/*.cts"
"server.cts"
], ],
"ignore": ["client/components/ui/**", "types/**"], "ignore": ["client/components/ui/**"],
"ignoreDependencies": ["tailwindcss", "tailwindcss-animate"],
"ignoreExportsUsedInFile": true "ignoreExportsUsedInFile": true
} }

60
package-lock.json generated
View File

@ -60,7 +60,6 @@
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3", "@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^10.0.3", "concurrently": "^10.0.3",
"eslint": "^10.7.0", "eslint": "^10.7.0",
@ -71,7 +70,6 @@
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"knip": "^6.24.0", "knip": "^6.24.0",
"lefthook": "^2.1.9", "lefthook": "^2.1.9",
"postcss": "^8.4.47",
"prettier": "3.9.4", "prettier": "3.9.4",
"tailwindcss": "^4.3.2", "tailwindcss": "^4.3.2",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@ -5745,43 +5743,6 @@
"node": ">= 4.0.0" "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": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -7542,20 +7503,6 @@
"node": ">= 0.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": { "node_modules/framer-motion": {
"version": "12.42.2", "version": "12.42.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz",
@ -10083,13 +10030,6 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/prebuild-install": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",

View File

@ -83,7 +83,6 @@
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3", "@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"concurrently": "^10.0.3", "concurrently": "^10.0.3",
"eslint": "^10.7.0", "eslint": "^10.7.0",
@ -94,7 +93,6 @@
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"knip": "^6.24.0", "knip": "^6.24.0",
"lefthook": "^2.1.9", "lefthook": "^2.1.9",
"postcss": "^8.4.47",
"prettier": "3.9.4", "prettier": "3.9.4",
"tailwindcss": "^4.3.2", "tailwindcss": "^4.3.2",
"typescript": "^6.0.3", "typescript": "^6.0.3",

View File

@ -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: {} };

View File

@ -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');

View File

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