diff --git a/NAVIDOCS_TESTING_SESSION.md b/NAVIDOCS_TESTING_SESSION.md new file mode 100644 index 0000000..66fa1e5 --- /dev/null +++ b/NAVIDOCS_TESTING_SESSION.md @@ -0,0 +1,1149 @@ +# NaviDocs - Testing Session (6 Haiku Agents) + +**Mission:** Comprehensive E2E testing with real user simulation using Playwright + +**Budget:** $5-8 (6 Haiku agents × 1-2 hours) +**Timeline:** 2-3 hours +**Repository:** https://github.com/dannystocker/navidocs + +--- + +## Prerequisites + +**Must be complete before running this session:** +- ✅ H-01 through H-15 build session complete +- ✅ 16 database tables created +- ✅ 5 backend APIs deployed (inventory, maintenance, cameras, contacts, expenses) +- ✅ 5 frontend components built +- ✅ Production deployment at https://digital-lab.ca/navidocs/app/ + +--- + +## Mission Overview + +**Spawn 6 Haiku agents to create and run E2E tests:** +- T-01: Test infrastructure setup (Playwright, fixtures, test data) +- T-02: Inventory flow test +- T-03: Maintenance flow test +- T-04: Camera flow test +- T-05: Contacts flow test +- T-06: Expenses flow test + +**Plus 3 agents for quality assurance:** +- T-07: Lighthouse performance audit +- T-08: API load testing +- T-09: Security scan (OWASP Top 10) + +--- + +## T-01: Test Infrastructure Setup (MUST RUN FIRST) + +**Agent Type:** Haiku +**Model:** haiku +**Priority:** P0 (all other tests depend on this) +**Duration:** 30-45 min + +**Task:** +```bash +# 1. Install Playwright +cd /workspace/navidocs +npm install -D @playwright/test +npx playwright install chromium + +# 2. Create test directories +mkdir -p tests/fixtures +mkdir -p tests/e2e +mkdir -p test-results + +# 3. Create test fixtures (mock images) +curl -o tests/fixtures/gps-photo.jpg "https://via.placeholder.com/800x600.jpg?text=Garmin+GPS+Chartplotter" +curl -o tests/fixtures/fuel-receipt.jpg "https://via.placeholder.com/600x800.jpg?text=Fuel+Receipt+%E2%82%AC450.00" +curl -o tests/fixtures/engine-manual.pdf "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" + +# 4. Create Playwright config +cat > playwright.config.js << 'EOF' +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30000, + retries: 2, + use: { + baseURL: 'http://localhost:5173', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + trace: 'on-first-retry', + }, + webServer: [ + { + command: 'cd server && npm start', + port: 3000, + timeout: 60000, + }, + { + command: 'cd client && npm run dev', + port: 5173, + timeout: 60000, + } + ], +}); +EOF + +# 5. Create test helper utilities +cat > tests/e2e/helpers.js << 'EOF' +export async function loginAsTestUser(page) { + await page.goto('/'); + await page.fill('input[type="email"]', 'test@example.com'); + await page.fill('input[type="password"]', 'TestPassword123!'); + await page.click('button[type="submit"]'); + await page.waitForURL(/\/home/); +} + +export async function loginAsCoOwner(page) { + await page.goto('/'); + await page.fill('input[type="email"]', 'co-owner@example.com'); + await page.fill('input[type="password"]', 'CoOwnerPass123!'); + await page.click('button[type="submit"]'); + await page.waitForURL(/\/home/); +} + +export async function createTestBoat(page) { + // Assumes already logged in + await page.goto('/boats/new'); + await page.fill('[name="name"]', 'Test Yacht'); + await page.fill('[name="make"]', 'Jeanneau'); + await page.fill('[name="model"]', 'Prestige 520'); + await page.fill('[name="year"]', '2020'); + await page.click('button[type="submit"]'); + await page.waitForURL(/\/boats\/.+/); + // Extract boat ID from URL + const url = page.url(); + return url.match(/\/boats\/([^\/]+)/)[1]; +} +EOF + +# 6. Seed test database +sqlite3 /workspace/navidocs/server/db/navidocs.db << 'EOF' +-- Create test users (if not exist) +INSERT OR IGNORE INTO users (id, email, name, password_hash, created_at, updated_at) +VALUES + ('test-user-123', 'test@example.com', 'Test Owner', '$2b$10$N9qo8uLOickgx2ZMRZoMye', 1699999999, 1699999999), + ('test-coowner-456', 'co-owner@example.com', 'Co-Owner', '$2b$10$N9qo8uLOickgx2ZMRZoMye', 1699999999, 1699999999); + +-- Create test organization +INSERT OR IGNORE INTO organizations (id, name, type, created_at, updated_at) +VALUES ('test-org-123', 'Test Organization', 'personal', 1699999999, 1699999999); + +-- Link users to organization +INSERT OR IGNORE INTO user_organizations (user_id, organization_id, role, joined_at) +VALUES + ('test-user-123', 'test-org-123', 'admin', 1699999999), + ('test-coowner-456', 'test-org-123', 'member', 1699999999); + +-- Create test boat +INSERT OR IGNORE INTO entities (id, organization_id, user_id, entity_type, name, make, model, year, created_at, updated_at) +VALUES ('test-boat-123', 'test-org-123', 'test-user-123', 'boat', 'Test Yacht', 'Jeanneau', 'Prestige 520', 2020, 1699999999, 1699999999); +EOF + +echo "✅ Test infrastructure ready" +echo "Test users: test@example.com / co-owner@example.com (password: see users table)" +echo "Test boat: test-boat-123" +``` + +**Signal completion:** Write `/tmp/T-01-SETUP-COMPLETE.json`: +```json +{ + "status": "complete", + "playwright_installed": true, + "fixtures_created": 3, + "test_users_seeded": 2, + "test_boat_id": "test-boat-123", + "confidence": 0.95 +} +``` + +--- + +## T-02: Inventory Flow Test + +**Agent Type:** Haiku +**Dependencies:** Wait for T-01 complete +**Duration:** 45-60 min + +**Task:** Create Playwright test for inventory upload workflow + +**Test file:** `tests/e2e/inventory-flow.spec.js` + +```javascript +import { test, expect } from '@playwright/test'; +import { loginAsTestUser } from './helpers.js'; + +test.describe('Inventory Management Flow', () => { + test('User uploads equipment photo and sees depreciation calculation', async ({ page }) => { + // 1. Login as test user + await loginAsTestUser(page); + + // 2. Navigate to boat inventory + await page.goto('/boats/test-boat-123'); + await page.click('a[href="/boats/test-boat-123/inventory"]'); + await expect(page.locator('h1')).toContainText('Inventory'); + + // 3. Upload equipment with photo + await page.click('[data-testid="add-equipment"]'); + await page.fill('[name="name"]', 'Garmin GPS Chartplotter'); + await page.fill('[name="category"]', 'electronics'); + await page.fill('[name="purchase_price"]', '2500'); + await page.fill('[name="purchase_date"]', '2020-01-15'); + + // Upload photo + await page.setInputFiles('input[type="file"]', 'tests/fixtures/gps-photo.jpg'); + await page.click('button[type="submit"]'); + + // 4. Verify item appears in inventory list + await page.waitForSelector('[data-testid="inventory-item"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="inventory-item"]')).toContainText('Garmin GPS'); + + // 5. Check depreciation calculation (4.8 years depreciation from 2020-01-15 to now) + await page.click('[data-testid="inventory-item"]:has-text("Garmin GPS")'); + const currentValue = await page.locator('[data-testid="current-value"]').textContent(); + + // Depreciation calculation: $2500 - (4.8 years × $150/year) ≈ $1,780 + // Accept range $1,700 - $2,100 + expect(currentValue).toMatch(/\$1,[7-9]\d{2}|\$2,0\d{2}/); + + // 6. Verify appears in ROI dashboard + await page.goto('/boats/test-boat-123/roi'); + const totalInventory = await page.locator('[data-testid="total-inventory"]').textContent(); + expect(totalInventory).toMatch(/\$1,[5-9]\d{2}|\$2,[0-2]\d{2}/); + + // 7. Test persistence - refresh page + await page.reload(); + await expect(page.locator('[data-testid="total-inventory"]')).toBeVisible(); + + // 8. Test multi-tenancy isolation + // Login as different org user should NOT see this inventory + await page.click('[data-testid="user-menu"]'); + await page.click('button:has-text("Logout")'); + // (Would need another test user from different org to verify) + }); + + test('User adds multiple items and sees total value', async ({ page }) => { + await loginAsTestUser(page); + await page.goto('/boats/test-boat-123/inventory'); + + // Add 3 items quickly + const items = [ + { name: 'Anchor Windlass', category: 'deck', price: '1800' }, + { name: 'Satellite Phone', category: 'electronics', price: '650' }, + { name: 'Life Raft', category: 'safety', price: '2200' } + ]; + + for (const item of items) { + await page.click('[data-testid="add-equipment"]'); + await page.fill('[name="name"]', item.name); + await page.fill('[name="category"]', item.category); + await page.fill('[name="purchase_price"]', item.price); + await page.fill('[name="purchase_date"]', '2024-11-01'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(500); // Wait for item to save + } + + // Verify count + const itemCount = await page.locator('[data-testid="inventory-item"]').count(); + expect(itemCount).toBeGreaterThanOrEqual(3); + + // Verify total value calculation + await page.goto('/boats/test-boat-123/roi'); + const totalValue = await page.locator('[data-testid="total-inventory"]').textContent(); + // $1800 + $650 + $2200 = $4,650 (plus previous items) + expect(totalValue).toMatch(/\$[4-9],\d{3}/); + }); +}); +``` + +**Run test:** +```bash +npx playwright test tests/e2e/inventory-flow.spec.js --reporter=html +``` + +**Signal completion:** Write `/tmp/T-02-INVENTORY-TEST-COMPLETE.json`: +```json +{ + "status": "complete", + "tests_passed": 2, + "tests_failed": 0, + "execution_time_ms": 18543, + "screenshots": 0, + "confidence": 0.95, + "report_path": "playwright-report/index.html" +} +``` + +--- + +## T-03: Maintenance Flow Test + +**Agent Type:** Haiku +**Dependencies:** Wait for T-01 complete +**Duration:** 45-60 min + +**Task:** Create Playwright test for maintenance logging and reminders + +**Test file:** `tests/e2e/maintenance-flow.spec.js` + +```javascript +import { test, expect } from '@playwright/test'; +import { loginAsTestUser } from './helpers.js'; + +test.describe('Maintenance Management Flow', () => { + test('User logs service and sets reminder', async ({ page }) => { + await loginAsTestUser(page); + + // 1. Navigate to maintenance + await page.goto('/boats/test-boat-123/maintenance'); + await expect(page.locator('h1')).toContainText('Maintenance'); + + // 2. Log new service record + await page.click('[data-testid="log-service"]'); + await page.fill('[name="service_type"]', 'Oil Change'); + await page.fill('[name="cost"]', '250'); + await page.fill('[name="provider"]', 'Marine Services Inc'); + await page.fill('[name="provider_phone"]', '+33494563412'); + await page.fill('[name="service_date"]', '2025-11-14'); + await page.fill('[name="notes"]', 'Changed oil and filter, engine running smoothly'); + await page.click('button[type="submit"]'); + + // 3. Verify service appears in history + await page.waitForSelector('[data-testid="maintenance-record"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="maintenance-record"]')).toContainText('Oil Change'); + await expect(page.locator('[data-testid="maintenance-record"]')).toContainText('€250'); + + // 4. Set reminder for next service (6 months) + await page.click('[data-testid="set-reminder"]'); + + // Calculate 6 months from now + const futureDate = new Date(); + futureDate.setMonth(futureDate.getMonth() + 6); + const reminderDate = futureDate.toISOString().split('T')[0]; + + await page.fill('[name="reminder_date"]', reminderDate); + await page.fill('[name="reminder_note"]', 'Schedule next oil change - 6 months overdue'); + await page.selectOption('[name="reminder_type"]', 'whatsapp'); + await page.click('button:has-text("Save Reminder")'); + + // 5. Verify reminder appears in calendar + await page.click('[data-testid="calendar-view"]'); + await page.waitForSelector('.calendar-event', { timeout: 5000 }); + + // Navigate to future month + for (let i = 0; i < 6; i++) { + await page.click('button[aria-label="Next month"]'); + } + + await expect(page.locator('.calendar-event:has-text("Oil Change")')).toBeVisible(); + + // 6. Verify notification queued + const notificationResponse = await page.request.get('/api/notifications', { + params: { type: 'reminder', boatId: 'test-boat-123' } + }); + expect(notificationResponse.ok()).toBeTruthy(); + const notifications = await notificationResponse.json(); + expect(notifications.length).toBeGreaterThan(0); + + // 7. Mark service as complete + await page.click('.calendar-event:has-text("Oil Change")'); + await page.click('button:has-text("Mark Complete")'); + await expect(page.locator('[data-testid="completed-badge"]')).toBeVisible(); + }); + + test('User searches service history and filters by provider', async ({ page }) => { + await loginAsTestUser(page); + await page.goto('/boats/test-boat-123/maintenance'); + + // Search service records + await page.fill('[data-testid="search-maintenance"]', 'oil'); + await page.waitForTimeout(500); // Debounce + + const results = await page.locator('[data-testid="maintenance-record"]').count(); + expect(results).toBeGreaterThan(0); + + // Filter by provider + await page.selectOption('[name="filter_provider"]', 'Marine Services Inc'); + await page.waitForTimeout(500); + + // Verify all results show that provider + const providerNames = await page.locator('[data-testid="provider-name"]').allTextContents(); + expect(providerNames.every(name => name === 'Marine Services Inc')).toBeTruthy(); + }); +}); +``` + +**Run test:** +```bash +npx playwright test tests/e2e/maintenance-flow.spec.js --reporter=html +``` + +**Signal completion:** Write `/tmp/T-03-MAINTENANCE-TEST-COMPLETE.json` + +--- + +## T-04: Camera Flow Test + +**Agent Type:** Haiku +**Dependencies:** Wait for T-01 complete +**Duration:** 45-60 min + +**Task:** Create Playwright test for camera integration and live streaming + +**Test file:** `tests/e2e/camera-flow.spec.js` + +```javascript +import { test, expect } from '@playwright/test'; +import { loginAsTestUser } from './helpers.js'; + +test.describe('Camera Integration Flow', () => { + test('User connects camera and receives motion alerts', async ({ page }) => { + await loginAsTestUser(page); + + // 1. Navigate to cameras + await page.goto('/boats/test-boat-123/cameras'); + await expect(page.locator('h1')).toContainText('Camera'); + + // 2. Add new camera + await page.click('[data-testid="add-camera"]'); + await page.fill('[name="camera_name"]', 'Stern Camera'); + await page.fill('[name="location"]', 'stern'); + await page.fill('[name="home_assistant_entity"]', 'camera.boat_stern'); + await page.fill('[name="rtsp_url"]', 'rtsp://192.168.1.100:554/stream'); + await page.click('button[type="submit"]'); + + // 3. Verify camera appears in list + await page.waitForSelector('[data-testid="camera-tile"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="camera-tile"]')).toContainText('Stern Camera'); + + // 4. Get webhook URL + await page.click('[data-testid="camera-tile"]:has-text("Stern Camera")'); + const webhookUrl = await page.locator('[data-testid="webhook-url"]').textContent(); + expect(webhookUrl).toContain('/api/cameras/webhook'); + + // 5. Simulate webhook from Home Assistant (motion detected) + const webhookResponse = await page.request.post('/api/cameras/webhook', { + data: { + event_type: 'motion', + camera_entity: 'camera.boat_stern', + timestamp: Date.now(), + snapshot_url: 'https://example.com/snapshot.jpg', + confidence: 0.92 + }, + headers: { + 'X-HA-Webhook-Secret': process.env.HOME_ASSISTANT_WEBHOOK_SECRET || 'test-secret' + } + }); + expect(webhookResponse.ok()).toBeTruthy(); + + // 6. Verify motion alert appears + await page.reload(); + await page.waitForSelector('[data-testid="motion-alert"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="motion-alert"]')).toContainText('Motion detected'); + + // 7. Test live stream viewer (mock stream) + await page.click('[data-testid="camera-tile"]:has-text("Stern Camera")'); + await page.click('button:has-text("View Live")'); + + // Check video player loaded + const videoPlayer = page.locator('video'); + await expect(videoPlayer).toBeVisible({ timeout: 10000 }); + + // Verify video source set (even if stream not available in test) + const videoSrc = await videoPlayer.getAttribute('src'); + expect(videoSrc).toBeTruthy(); + + // 8. Daily check workflow + await page.click('[data-testid="daily-check-tab"]'); + await page.click('button:has-text("Boat looks OK ✓")'); + await expect(page.locator('[data-testid="last-checked"]')).toContainText('Today'); + await expect(page.locator('[data-testid="check-streak"]')).toContainText('1 day'); + }); + + test('User views camera event history', async ({ page }) => { + await loginAsTestUser(page); + await page.goto('/boats/test-boat-123/cameras'); + + // Navigate to events tab + await page.click('[data-testid="events-tab"]'); + + // Verify events listed + await page.waitForSelector('[data-testid="camera-event"]', { timeout: 5000 }); + const eventCount = await page.locator('[data-testid="camera-event"]').count(); + expect(eventCount).toBeGreaterThan(0); + + // Filter by camera + await page.selectOption('[name="filter_camera"]', 'Stern Camera'); + await page.waitForTimeout(500); + + // Check snapshot image loads + const firstEventSnapshot = page.locator('[data-testid="event-snapshot"]').first(); + await expect(firstEventSnapshot).toBeVisible(); + }); +}); +``` + +**Run test:** +```bash +npx playwright test tests/e2e/camera-flow.spec.js --reporter=html +``` + +**Signal completion:** Write `/tmp/T-04-CAMERA-TEST-COMPLETE.json` + +--- + +## T-05: Contacts Flow Test + +**Agent Type:** Haiku +**Dependencies:** Wait for T-01 complete +**Duration:** 30-45 min + +**Task:** Create Playwright test for contact management + +**Test file:** `tests/e2e/contacts-flow.spec.js` + +```javascript +import { test, expect } from '@playwright/test'; +import { loginAsTestUser } from './helpers.js'; + +test.describe('Contact Management Flow', () => { + test('User adds contact and uses one-tap call', async ({ page }) => { + await loginAsTestUser(page); + + // 1. Navigate to contacts + await page.goto('/boats/test-boat-123/contacts'); + await expect(page.locator('h1')).toContainText('Contact'); + + // 2. Add marina contact + await page.click('[data-testid="add-contact"]'); + await page.fill('[name="name"]', 'Port Pin Rolland Marina'); + await page.selectOption('[name="category"]', 'marina'); + await page.fill('[name="phone"]', '+33494563412'); + await page.fill('[name="email"]', 'contact@portpinrolland.com'); + await page.fill('[name="address"]', 'Saint-Tropez, France'); + await page.fill('[name="notes"]', 'Main marina for berth. Ask for Jean-Pierre.'); + await page.click('button[type="submit"]'); + + // 3. Verify contact appears + await page.waitForSelector('[data-testid="contact-card"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="contact-card"]')).toContainText('Port Pin Rolland'); + + // 4. Test one-tap call link + const callLink = page.locator('a[href^="tel:"]'); + await expect(callLink).toHaveAttribute('href', 'tel:+33494563412'); + + // 5. Test one-tap email link + const emailLink = page.locator('a[href^="mailto:"]'); + await expect(emailLink).toHaveAttribute('href', 'mailto:contact@portpinrolland.com'); + + // 6. Search contacts + await page.fill('[data-testid="contact-search"]', 'marina'); + await page.waitForTimeout(300); + const searchResults = await page.locator('[data-testid="contact-card"]').count(); + expect(searchResults).toBeGreaterThanOrEqual(1); + + // 7. Export vCard + await page.click('[data-testid="contact-card"]:has-text("Port Pin Rolland")'); + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('button:has-text("Export vCard")') + ]); + expect(download.suggestedFilename()).toMatch(/port-pin-rolland.*\.vcf/); + + // 8. Log call + await page.click('button:has-text("Log Call")'); + await page.fill('[name="call_notes"]', 'Confirmed berth reservation for summer 2026'); + await page.fill('[name="call_duration"]', '8'); + await page.click('button:has-text("Save")'); + + await expect(page.locator('[data-testid="call-history"]')).toContainText('Confirmed berth'); + }); + + test('User filters contacts by category', async ({ page }) => { + await loginAsTestUser(page); + await page.goto('/boats/test-boat-123/contacts'); + + // Add contacts in different categories + const contacts = [ + { name: 'Marine Mechanic Pro', category: 'mechanic', phone: '+33612345678' }, + { name: 'Parts Supplier', category: 'vendor', phone: '+33687654321' } + ]; + + for (const contact of contacts) { + await page.click('[data-testid="add-contact"]'); + await page.fill('[name="name"]', contact.name); + await page.selectOption('[name="category"]', contact.category); + await page.fill('[name="phone"]', contact.phone); + await page.click('button[type="submit"]'); + await page.waitForTimeout(500); + } + + // Filter by mechanic + await page.click('[data-testid="category-tab-mechanic"]'); + await expect(page.locator('[data-testid="contact-card"]')).toContainText('Marine Mechanic'); + await expect(page.locator('[data-testid="contact-card"]')).not.toContainText('Parts Supplier'); + + // Filter by vendor + await page.click('[data-testid="category-tab-vendor"]'); + await expect(page.locator('[data-testid="contact-card"]')).toContainText('Parts Supplier'); + await expect(page.locator('[data-testid="contact-card"]')).not.toContainText('Marine Mechanic'); + }); +}); +``` + +**Run test:** +```bash +npx playwright test tests/e2e/contacts-flow.spec.js --reporter=html +``` + +**Signal completion:** Write `/tmp/T-05-CONTACTS-TEST-COMPLETE.json` + +--- + +## T-06: Expenses Flow Test + +**Agent Type:** Haiku +**Dependencies:** Wait for T-01 complete +**Duration:** 60-75 min (most complex test) + +**Task:** Create Playwright test for expense tracking and approval + +**Test file:** `tests/e2e/expenses-flow.spec.js` + +```javascript +import { test, expect } from '@playwright/test'; +import { loginAsTestUser, loginAsCoOwner } from './helpers.js'; + +test.describe('Expense Tracking Flow', () => { + test('User uploads receipt, OCR extracts data, co-owner approves', async ({ page, context }) => { + // 1. Login as primary owner + await loginAsTestUser(page); + + // 2. Navigate to expenses + await page.goto('/boats/test-boat-123/expenses'); + await expect(page.locator('h1')).toContainText('Expense'); + + // 3. Upload receipt + await page.click('[data-testid="upload-receipt"]'); + await page.setInputFiles('input[type="file"]', 'tests/fixtures/fuel-receipt.jpg'); + + // Wait for OCR processing (max 10 seconds) + await page.waitForSelector('[data-testid="ocr-result"]', { timeout: 10000 }); + + // 4. Verify OCR extracted amount + const extractedAmount = await page.locator('[data-testid="ocr-amount"]').inputValue(); + expect(extractedAmount).toBe('450.00'); // From fixture + + // 5. Complete expense details + await page.selectOption('[name="category"]', 'fuel'); + await page.fill('[name="vendor"]', 'Total Energies'); + await page.fill('[name="date"]', '2025-11-10'); + await page.fill('[name="notes"]', 'Full tank refuel before winter storage'); + await page.click('button[type="submit"]'); + + // 6. Verify expense appears in list + await page.waitForSelector('[data-testid="expense-item"]', { timeout: 5000 }); + await expect(page.locator('[data-testid="expense-item"]')).toContainText('€450.00'); + await expect(page.locator('[data-testid="approval-badge"]')).toContainText('Pending'); + + // 7. Logout primary owner + await page.click('[data-testid="user-menu"]'); + await page.click('button:has-text("Logout")'); + + // 8. Login as co-owner + await loginAsCoOwner(page); + + // 9. Navigate to expenses (co-owner perspective) + await page.goto('/boats/test-boat-123/expenses'); + + // 10. Find pending expense + await page.click('[data-testid="filter-status-pending"]'); + await expect(page.locator('[data-testid="expense-item"]')).toContainText('€450.00'); + + // 11. Co-owner approves expense + await page.click('[data-testid="expense-item"]:has-text("€450.00")'); + await page.click('button:has-text("Approve")'); + + // Verify approval badge updated + await expect(page.locator('[data-testid="approval-badge"]')).toContainText('Approved'); + + // 12. Verify annual spend updated + const annualTotal = await page.locator('[data-testid="annual-total"]').textContent(); + expect(annualTotal).toContain('€450'); + + // 13. Check category breakdown chart + await page.click('[data-testid="category-breakdown-tab"]'); + const fuelCategory = page.locator('[data-testid="category-fuel"]'); + await expect(fuelCategory).toContainText('€450.00'); + + // 14. Test CSV export + const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('button:has-text("Export CSV")') + ]); + expect(download.suggestedFilename()).toMatch(/expenses-\d{4}-\d{2}-\d{2}\.csv/); + }); + + test('User splits expense with co-owner', async ({ page }) => { + await loginAsTestUser(page); + await page.goto('/boats/test-boat-123/expenses'); + + // Add expense + await page.click('[data-testid="upload-receipt"]'); + await page.setInputFiles('input[type="file"]', 'tests/fixtures/fuel-receipt.jpg'); + await page.waitForSelector('[data-testid="ocr-result"]', { timeout: 10000 }); + + await page.selectOption('[name="category"]', 'maintenance'); + await page.fill('[name="vendor"]', 'Marine Services'); + await page.fill('[name="date"]', '2025-11-12'); + + // Enable expense splitting + await page.click('[name="split_expense"]'); + + // Select co-owner for 50/50 split + await page.click('[data-testid="add-split-participant"]'); + await page.selectOption('[name="split_user"]', 'co-owner@example.com'); + await page.fill('[name="split_percentage"]', '50'); + + await page.click('button[type="submit"]'); + + // Verify split shows correctly + await page.click('[data-testid="expense-item"]:has-text("Marine Services")'); + await expect(page.locator('[data-testid="split-details"]')).toContainText('50% (€225.00)'); + await expect(page.locator('[data-testid="split-details"]')).toContainText('co-owner@example.com: 50%'); + }); +}); +``` + +**Run test:** +```bash +npx playwright test tests/e2e/expenses-flow.spec.js --reporter=html +``` + +**Signal completion:** Write `/tmp/T-06-EXPENSE-TEST-COMPLETE.json` + +--- + +## T-07: Performance Testing + +**Agent Type:** Haiku +**Dependencies:** Wait for T-02 through T-06 complete +**Duration:** 30-45 min + +**Task:** Run Lighthouse audits and API load tests + +```bash +# 1. Install Lighthouse +npm install -g lighthouse + +# 2. Run Lighthouse on all key pages +pages=( + "http://localhost:5173/" + "http://localhost:5173/boats/test-boat-123/inventory" + "http://localhost:5173/boats/test-boat-123/maintenance" + "http://localhost:5173/boats/test-boat-123/cameras" + "http://localhost:5173/boats/test-boat-123/contacts" + "http://localhost:5173/boats/test-boat-123/expenses" +) + +for url in "${pages[@]}"; do + page_name=$(echo $url | sed 's/.*\///' | sed 's/http.*5173/home/') + lighthouse "$url" \ + --output=json \ + --output=html \ + --output-path="test-results/lighthouse-$page_name" \ + --chrome-flags="--headless --no-sandbox" +done + +# 3. Extract scores +for file in test-results/lighthouse-*.json; do + page=$(basename $file .json | sed 's/lighthouse-//') + performance=$(jq '.categories.performance.score * 100' $file) + accessibility=$(jq '.categories.accessibility.score * 100' $file) + echo "$page: Performance=$performance, Accessibility=$accessibility" +done + +# 4. API load testing (using Apache Bench) +echo "Testing API endpoints..." + +# Get JWT token first +JWT_TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"TestPassword123!"}' | jq -r '.token') + +# Test each endpoint (1000 requests, 10 concurrent) +endpoints=( + "/api/inventory/test-boat-123" + "/api/maintenance/test-boat-123" + "/api/cameras/test-boat-123" + "/api/contacts/test-boat-123" + "/api/expenses/test-boat-123" +) + +for endpoint in "${endpoints[@]}"; do + echo "Testing $endpoint..." + ab -n 1000 -c 10 \ + -H "Authorization: Bearer $JWT_TOKEN" \ + "http://localhost:3000$endpoint" \ + > "test-results/load-test-$(echo $endpoint | tr '/' '-').txt" +done + +# Extract p95 latency +for file in test-results/load-test-*.txt; do + endpoint=$(basename $file .txt | sed 's/load-test-//') + p95=$(grep "95%" $file | awk '{print $2}') + echo "$endpoint: p95 latency = $p95 ms" +done + +# 5. Bundle size check +cd /workspace/navidocs/client +npm run build +echo "Bundle sizes:" +du -sh dist/assets/*.js +gzip -c dist/assets/index.*.js | wc -c | awk '{print "Gzipped: " $1/1024 " KB"}' +``` + +**Target Metrics:** +- Lighthouse Performance: >90 +- Lighthouse Accessibility: >95 +- API p95 latency: <200ms +- Bundle size (gzipped): <500KB + +**Signal completion:** Write `/tmp/T-07-PERFORMANCE-COMPLETE.json`: +```json +{ + "status": "complete", + "lighthouse_scores": { + "home": { "performance": 94, "accessibility": 97 }, + "inventory": { "performance": 92, "accessibility": 95 }, + "maintenance": { "performance": 91, "accessibility": 96 }, + "cameras": { "performance": 88, "accessibility": 94 }, + "contacts": { "performance": 93, "accessibility": 97 }, + "expenses": { "performance": 90, "accessibility": 95 } + }, + "api_latency_p95": { + "inventory": 145, + "maintenance": 167, + "cameras": 189, + "contacts": 123, + "expenses": 198 + }, + "bundle_size_kb": 387, + "confidence": 0.95 +} +``` + +--- + +## T-08: Security Testing + +**Agent Type:** Haiku +**Dependencies:** Wait for T-02 through T-06 complete +**Duration:** 30-45 min + +**Task:** Run OWASP Top 10 security scans + +```bash +# 1. SQL Injection Tests +echo "Testing SQL injection vulnerabilities..." + +JWT_TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"TestPassword123!"}' | jq -r '.token') + +# Test SQL injection in query params +curl -X GET "http://localhost:3000/api/inventory/test-boat-123' OR '1'='1" \ + -H "Authorization: Bearer $JWT_TOKEN" \ + -w "\nStatus: %{http_code}\n" \ + -o /tmp/sql-injection-test.json + +# Should return 400 or 404, not 200 with data leak +if grep -q "id" /tmp/sql-injection-test.json; then + echo "❌ VULNERABILITY: SQL injection possible" +else + echo "✅ SQL injection blocked" +fi + +# 2. XSS Tests +echo "Testing XSS vulnerabilities..." + +# Try to inject script tag in contact name +curl -X POST http://localhost:3000/api/contacts \ + -H "Authorization: Bearer $JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "boatId": "test-boat-123", + "name": "", + "phone": "123456789", + "category": "vendor" + }' \ + -w "\nStatus: %{http_code}\n" \ + -o /tmp/xss-test.json + +# Should sanitize the script tag +if grep -q "