E2E Test Plan
E2E Test Plan
Project: Bilko Version: 1.
01 Date: 2026-02-2505-21 Author: Ops Architect / ALAI Documentation Team Status:FinalActive 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.1 | 2026-05-21 | ALAI Documentation Team | Clarified critical E2E vs real-demo smoke vs full-demo rehearsal; documented non-destructive demo policy |
1. Overview
This plan covers allBilko's Playwright E2Ebrowser tests for Bilko.tests. E2E tests validate critical user journeys through the reala deployed applicationapplication, (stagingbut environment). Theythey are not intended to cover every behavior in the finalproduct. qualityFinancial gatecorrectness, beforeAPI productionbehavior, deploy.RBAC, and tenant isolation must be primarily proven by unit/integration/contract tests.
Framework: Playwright
Target: 12small critical E2E testssuite across+ 4non-destructive criticalreal-demo usersmoke flows+ scheduled full regression/demo rehearsal
Execution time: < 8 minutes (4for parallelcritical workers,gate; 3< browsers)1 minute for real-demo smoke
Browsers: Chromium (primary),primary Firefox,for Safari/deploy gates; Firefox/WebKit on scheduled/risk-based runs
Base URL: Vercelstaging/resettable previewenvironment URLfor (PRs)critical/destructive E2E; https://bilko-demo.alai.no bilko.iofor (productionnon-destructive smoke)demo smoke
Policy: do not use the current full mixed Playwright suite as the deploy gate until it is partitioned. Specs that register users, create/delete data, torture rate limits, encode expected failures, or depend on one language must not run against public real demo as a blocking gate.
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
EachUse E2Edifferent testdata runpolicies usesper seeded test data:environment:
Organization:RealTestpublicCompany d.o.o.(country: RS, currency: RSD)User:demo:demo@bilko./iors;demo123Demo2026!(role:read-only/non-destructiveowner)smoke only. No registration, no deletes, no rate-limit tests, no invoice/expense creation unless the tenant is explicitly resettable.Customer:CriticalE2E:AcmestagingCorp(pre-seeded)seeded resettable organization, user, customer, vendor, invoices, expenses, and reports. Tests may create/update data because the environment is disposable.Vendor:Full-demoOfficerehearsal:Suppliesdedicatedd.o.o.resettable(pre-seeded)demo tenant/environment with a scripted business story and automatic reset before each rehearsal.
SeedSeed/reset command:commands implemented pnpmmust runbe test:e2e:seed(resetsfor and re-seeds test databasestaging/full-demo before destructive E2E run)is enabled as a gate.
3. E2E Test Suites
Suite 0: Real Demo Smoke (non-destructive — P0)
File: apps/e2e/tests/real-demo-smoke.spec.ts
| ID | Test | Priority | Status |
|---|---|---|---|
| E2E-SMOKE-REAL | API health + login + protected page smoke | P0 | Implemented |
Command:
cd apps/e2e
npm run test:real-demo-smoke
Default targets:
- Frontend:
https://bilko-demo.alai.no - API:
https://bilko-demo-api.alai.no
Coverage:
- API
/api/v1/health - API login with demo credentials
- refresh cookie/session validation
/dashboard,/invoices,/settings- unexpected browser console errors
- optional video evidence via
E2E_VIDEO_DIR
Non-goals:
- registration
- invoice/expense creation
- deletes/status mutations
- PDF correctness
- multi-tenant isolation
- rate-limit torture
- historical expected-fail regressions
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
#cd Start dev server (separate terminal)
pnpm run devapps/e2e
# RunNon-destructive allpublic E2Edemo testssmoke pnpm— deploy/demo health gate
npm run test:e2ereal-demo-smoke
# All Playwright specs — developer/regression use only until partitioned
npm test
# Specific browser
pnpmnpm run test:e2etest -- --project=chromium
pnpmnpm run test:e2etest -- --project=firefox
pnpmnpm run test:e2etest -- --project=webkit
# HeadedHeaded/debug modemodes
(see browser)
pnpmnpm run test:e2e -- --headed
#npm Debug mode (pause on failure)
pnpm run test:e2etest -- --debug
# Single test file
pnpmnpm run test:e2etest -- invoice-flow.tests/invoices.spec.ts --project=chromium
# Generate HTML report
pnpmnpm run test:e2etest -- --reporter=html
When running against real demo with video evidence:
E2E_REAL_DEMO_SMOKE=1 \
E2E_BASE_URL=https://bilko-demo.alai.no \
E2E_API_URL=https://bilko-demo-api.alai.no \
E2E_VIDEO_DIR=/tmp/alai/bilko-real-demo-smoke-video-$(date +%Y%m%d-%H%M%S)/videos \
npm test -- tests/real-demo-smoke.spec.ts --project=chromium --no-deps
On CICI/CD
Recommended Actions)gates:
PLAYWRIGHT_BASE_URL=# Staging/resettable environment: critical E2E only
# Add this script after Playwright specs are tagged/partitioned.
E2E_BASE_URL=${STAGING_WEB_URL} E2E_API_URL=${STAGING_API_URL} vercel_preview_url }} pnpmnpm run test:e2ee2e:critical
# Post-deploy/public demo: non-destructive smoke only — implemented now.
E2E_BASE_URL=https://bilko-demo.alai.no E2E_API_URL=https://bilko-demo-api.alai.no npm run test:real-demo-smoke
ArtifactsArtifacts: uploadedresult on failure:JSON, screenshots, videos,traces/videos traces.where enabled. Evidence should be stored outside the repo, e.g. /tmp/alai/<run-id>/ or CI artifacts.
5. Flaky Test Policy
- Retry failed critical E2E test once (
retries: 1inplaywright.config.ts)Playwright config where enabled) - If still fails: mark as
flakyin Mission Control/GitHub Issues, do NOT ignore - Fix flaky critical tests within the same sprint they appear
- Quarantine only non-blocking nightly/regression tests; never silently skip deploy-gate tests
- Common causes: race conditions (use
await expect()notawait sleep()), timing, localization assumptions, shared auth state, and 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ć |