Skip to main content

E2E Test Plan

E2E Test Plan

Project: Bilko Version: 1.0 Date: 2026-02-25 Author: Ops Architect Status: Final Reviewers: Tech Lead, Alem Bašić

Document History

Version Date Author Changes
0.1 2026-02-23 Ops Architect Initial draft
1.0 2026-02-25 ALAI Documentation Team Finalized — approved for production use

1. Overview

This plan covers all Playwright E2E tests for Bilko. E2E tests validate critical user journeys through the real deployed application (staging environment). They are the final quality gate before production deploy.

Framework: Playwright Target: 12 E2E tests across 4 critical user flows Execution time: < 8 minutes (4 parallel workers, 3 browsers) Browsers: Chromium (primary), Firefox, Safari/WebKit Base URL: Vercel preview URL (PRs) / bilko.io (production smoke)


2. Test Environment

Configuration

// apps/e2e/playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './tests',
  timeout: 60000, // 60s per test
  retries: 1, // Retry flaky tests once
  workers: 4, // Parallel execution
  reporter: [['html'], ['github']],
  use: {
    baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
})

Test Data

Each E2E test run uses seeded test data:

  • Organization: Test Company d.o.o. (country: RS, currency: RSD)
  • User: [email protected] / demo123 (role: owner)
  • Customer: Acme Corp (pre-seeded)
  • Vendor: Office Supplies d.o.o. (pre-seeded)

Seed command: pnpm run test:e2e:seed (resets and re-seeds test database before E2E run)


3. E2E Test Suites

Suite 1: Invoice Flow (4 tests — P0)

File: apps/e2e/tests/invoice-flow.spec.ts

ID Test Priority Status
E2E-INV-01 Create invoice via 6-step wizard P0 Not implemented
E2E-INV-02 Preview invoice before sending P0 Not implemented
E2E-INV-03 Send invoice to customer via email P0 Not implemented
E2E-INV-04 Mark invoice as paid P0 Not implemented

E2E-INV-01: Create invoice via 6-step wizard

test('create invoice via 6-step wizard', async ({ page }) => {
  await page.goto('/invoices/new')

  // Step 1: Customer selection
  await page.selectOption('[data-testid="customer-select"]', { label: 'Acme Corp' })
  await page.click('[data-testid="next-btn"]')

  // Step 2: Invoice details
  await page.fill('[data-testid="invoice-date"]', '2026-02-23')
  await page.fill('[data-testid="due-date"]', '2026-03-23')
  await page.selectOption('[data-testid="currency-select"]', 'RSD')
  await page.click('[data-testid="next-btn"]')

  // Step 3: Line items
  await page.fill('[data-testid="item-0-description"]', 'Web Development')
  await page.fill('[data-testid="item-0-quantity"]', '10')
  await page.fill('[data-testid="item-0-unit-price"]', '5000')
  await page.selectOption('[data-testid="item-0-tax-rate"]', '20')
  await page.click('[data-testid="next-btn"]')

  // Step 4: Review — verify totals
  await expect(page.locator('[data-testid="subtotal"]')).toContainText('50,000.00 RSD')
  await expect(page.locator('[data-testid="tax-amount"]')).toContainText('10,000.00 RSD')
  await expect(page.locator('[data-testid="total-amount"]')).toContainText('60,000.00 RSD')

  // Create
  await page.click('[data-testid="create-invoice-btn"]')

  // Verify redirect and invoice number
  await expect(page).toHaveURL(/\/invoices\/[a-f0-9-]+$/)
  await expect(page.locator('h1')).toContainText('INV-')
  await expect(page.locator('[data-testid="invoice-status"]')).toContainText('Draft')
})

E2E-INV-04: Mark invoice as paid

test('mark invoice as paid', async ({ page }) => {
  // Create invoice first (or use pre-seeded sent invoice)
  await page.goto('/invoices')
  await page.click('[data-testid="invoice-row"]:first-child')

  await page.click('[data-testid="mark-paid-btn"]')
  await page.fill('[data-testid="payment-date"]', '2026-02-23')
  await page.click('[data-testid="confirm-payment-btn"]')

  await expect(page.locator('[data-testid="invoice-status"]')).toContainText('Paid')
  await expect(page.locator('[data-testid="paid-at"]')).toBeVisible()
})

Suite 2: Expense Flow (3 tests — P1)

File: apps/e2e/tests/expense-flow.spec.ts

ID Test Priority Status
E2E-EXP-01 Add expense with receipt photo upload P1 Not implemented
E2E-EXP-02 Approve expense as admin P1 Not implemented
E2E-EXP-03 Mark expense as paid P1 Not implemented

E2E-EXP-01: Add expense with receipt upload

test('add expense with receipt photo upload', async ({ page }) => {
  await page.goto('/expenses/new')

  await page.fill('[data-testid="expense-amount"]', '1500')
  await page.selectOption('[data-testid="expense-currency"]', 'RSD')
  await page.selectOption('[data-testid="expense-category"]', 'office_supplies')
  await page.fill('[data-testid="expense-description"]', 'Printer paper')
  await page.fill('[data-testid="expense-date"]', '2026-02-23')

  // Upload receipt image
  const fileInput = page.locator('[data-testid="receipt-upload"]')
  await fileInput.setInputFiles('fixtures/test-receipt.jpg')
  await expect(page.locator('[data-testid="receipt-preview"]')).toBeVisible()

  await page.click('[data-testid="submit-expense-btn"]')

  await expect(page).toHaveURL(/\/expenses\/[a-f0-9-]+$/)
  await expect(page.locator('[data-testid="expense-status"]')).toContainText('Pending')
})

Suite 3: Report Flow (2 tests — P1)

File: apps/e2e/tests/report-flow.spec.ts

ID Test Priority Status
E2E-REP-01 Generate P&L report for date range P1 Not implemented
E2E-REP-02 Export P&L report to PDF P1 Not implemented

E2E-REP-01: Generate P&L report

test('generate P&L report for date range', async ({ page }) => {
  await page.goto('/reports/profit-loss')

  await page.fill('[data-testid="start-date"]', '2026-01-01')
  await page.fill('[data-testid="end-date"]', '2026-01-31')
  await page.click('[data-testid="generate-btn"]')

  await expect(page.locator('[data-testid="report-title"]')).toContainText('Profit & Loss')
  await expect(page.locator('[data-testid="total-revenue"]')).toBeVisible()
  await expect(page.locator('[data-testid="total-expenses"]')).toBeVisible()
  await expect(page.locator('[data-testid="net-profit"]')).toBeVisible()
})

Suite 4: Auth Flow (1 test — P1)

File: apps/e2e/tests/auth-flow.spec.ts

ID Test Priority Status
E2E-AUTH-01 Register → Login → 2FA → Logout P1 Not implemented

E2E-AUTH-01: Full auth flow

test('register, login with 2FA, and logout', async ({ page }) => {
  // Register new user
  await page.goto('/register')
  await page.fill('[data-testid="company-name"]', 'Test Firma d.o.o.')
  await page.fill('[data-testid="email"]', `test_${Date.now()}@bilko.io`)
  await page.fill('[data-testid="password"]', 'SecurePass123!')
  await page.click('[data-testid="register-btn"]')

  // Should redirect to dashboard after registration
  await expect(page).toHaveURL('/dashboard')

  // Logout
  await page.click('[data-testid="user-menu"]')
  await page.click('[data-testid="logout-btn"]')
  await expect(page).toHaveURL('/login')
})

Suite 5: Settings Flow (2 tests — P2)

File: apps/e2e/tests/settings-flow.spec.ts

ID Test Priority Status
E2E-SET-01 Update organization settings P2 Not implemented
E2E-SET-02 Invite user to organization P2 Not implemented

4. Test Execution

Running E2E Tests

# Start dev server (separate terminal)
pnpm run dev

# Run all E2E tests
pnpm run test:e2e

# Specific browser
pnpm run test:e2e -- --project=chromium
pnpm run test:e2e -- --project=firefox
pnpm run test:e2e -- --project=webkit

# Headed mode (see browser)
pnpm run test:e2e -- --headed

# Debug mode (pause on failure)
pnpm run test:e2e -- --debug

# Single test file
pnpm run test:e2e -- invoice-flow.spec.ts

# Generate HTML report
pnpm run test:e2e -- --reporter=html

On CI (GitHub Actions)

PLAYWRIGHT_BASE_URL=${{ vercel_preview_url }} pnpm run test:e2e

Artifacts uploaded on failure: screenshots, videos, traces.


5. Flaky Test Policy

  1. Retry failed E2E test once (retries: 1 in playwright.config.ts)
  2. If still fails: mark as flaky in GitHub Issues, do NOT ignore
  3. Fix flaky tests within the same sprint they appear
  4. Common causes: race conditions (use await expect() not await sleep()), timing, test data isolation

6. Accessibility Checks (PLANNED)

Add Axe accessibility checks to critical flows:

import { checkA11y } from 'axe-playwright'

test('invoice form is accessible', async ({ page }) => {
  await page.goto('/invoices/new')
  await checkA11y(page, '#invoice-form', {
    detailedReport: true,
    detailedReportOptions: { html: true },
  })
})


Approval

Role Name Date Signature
Author Ops Architect 2026-02-23
Reviewer Tech Lead
Approver Alem Bašić