Skip to main content

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.12026-05-21ALAI Documentation TeamClarified 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:Real Testpublic Company d.o.o. (country: RS, currency: RSD)
  • User:demo: demo@bilko.iors / demo123Demo2026!; (role:read-only/non-destructive owner)smoke only. No registration, no deletes, no rate-limit tests, no invoice/expense creation unless the tenant is explicitly resettable.
  • Customer:Critical Acmestaging CorpE2E: (pre-seeded)seeded resettable organization, user, customer, vendor, invoices, expenses, and reports. Tests may create/update data because the environment is disposable.
  • Vendor:Full-demo Officerehearsal: Suppliesdedicated d.o.o.resettable (pre-seeded)demo tenant/environment with a scripted business story and automatic reset before each rehearsal.

SeedSeed/reset command:commands pnpmmust runbe test:e2e:seedimplemented (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

IDTestPriorityStatus
E2E-SMOKE-REALAPI health + login + protected page smokeP0Implemented

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

(GitHub
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

  1. Retry failed critical E2E test once (retries: 1 in playwright.config.ts)Playwright config where enabled)
  2. If still fails: mark as flaky in Mission Control/GitHub Issues, do NOT ignore
  3. Fix flaky critical tests within the same sprint they appear
  4. Quarantine only non-blocking nightly/regression tests; never silently skip deploy-gate tests
  5. Common causes: race conditions (use await expect() not await 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 },
  })
})


Approval

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