E2E Test Plan
E2E Test Plan
Project: Bilko Version:
0.11.0 Date: 2026-02-2325 Author: Ops Architect Status:DraftFinal 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
- Retry failed E2E test once (
retries: 1in playwright.config.ts) - If still fails: mark as
flakyin GitHub Issues, do NOT ignore - Fix flaky tests within the same sprint they appear
- Common causes: race conditions (use
await expect()notawait 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 },
});
});
Related Documents
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Ops Architect | 2026-02-23 | |
| Reviewer | Tech Lead | ||
| Approver | Alem Bašić |