E2E Test Suite Documentation

Reproducible Jest + Playwright tests for the EA Tool CRUD operations

Quick Start

Prerequisites: The Vite dev server must be running on port 3000 before running tests.
Run npm run dev in a separate terminal first.
# Run the full E2E suite
npm run test:e2e

# With verbose output (shows each test name + timing)
npm run test:e2e:verbose

# Run a single test file
npx jest --config tests/jest.e2e.config.cjs tests/e2e/customers.test.cjs

# Run tests matching a pattern
npx jest --config tests/jest.e2e.config.cjs -t "Protected Leads"

Tests run headless by default (~100s total). Screenshots are saved to /tmp/ea-screenshots/ after every test.

File Structure

tests/ ├── jest.e2e.config.cjs # Jest config (60s timeout, sequential) ├── e2e-crud.cjs # Original script runner (legacy) └── e2e/ ├── setup.cjs # Shared: createBrowser, screenshot, helpers ├── customers.test.cjs # E2E-1: Customer CRUD (5 tests) ├── quotes.test.cjs # E2E-2/3: Quote lifecycle + validation (12 tests) ├── protected-leads.test.cjs # E2E-5: Protected leads CRUD (3 tests) ├── invoices-coupons.test.cjs# E2E-6/7: Invoices + coupon code (2 tests) └── bulk-delete.test.cjs # E2E-4: Quote bulk delete (1 test)

setup.cjs — Shared helpers

Every test file imports from setup.cjs which provides: createBrowser() for isolated Playwright sessions, screenshot() / screenshotFull() for evidence capture, waitForLoadingDone() to wait for spinners + network, selectCustomer() to pick a customer via the dropdown, and uniqueId() for timestamp-based test data.

What's Tested (23 scenarios)

customers.test.cjs — Customer CRUD

TestWhat it doesAsserts
E2E-1.1Fill create form (email, name, company, country) and submitRedirect to /customers/:id, ID extracted from URL
E2E-1.2Click submit without filling emailStays on /customers/new, validation visible (message or :invalid)
E2E-1.3Click Cancel on create formURL no longer contains /new
E2E-1.4Navigate to created customer detail13+ tabs rendered, includes Overview, Licenses, Quotes, Invoices
E2E-1.5Click Edit, change company name, saveEdit form loads, save succeeds, stays on detail page

quotes.test.cjs — Quote Lifecycle + Validation

TestWhat it doesAsserts
E2E-2.1Navigate to /quotes/newType selector shows New Quote, Renewal, Upsell
E2E-2.2Enter wizard, search "hex" in customer fieldSearch input exists, dropdown appears
E2E-2.3Change duration to 2 years, currency to USDSelects open, options clickable (uses force: true for overlay)
E2E-2.4Click "Add item", pick product + license type"Add item" button exists, product picker works
E2E-2.5Check Save Draft button stateDisabled without customer/product (expected), clicks if enabled
E2E-2.6Check Save & Generate button stateSame pattern as 2.5 — documents state
E2E-2.7Navigate to existing quote detail via listSelects customer, clicks row, checks action buttons
E2E-2.8Click "Mark as Sent" + confirm dialogDialog appears, status updates (skips if not available)
E2E-2.9Click "Reject" + confirm dialogDialog appears, status updates (skips if not available)
E2E-2.10Open actions dropdown, click "Duplicate"Menu opens, duplicate item exists (skips if not available)
E2E-3.1Load empty wizard, check validation"Customer must be selected" + "At least one product" visible, or buttons disabled
E2E-3.2Check Save Draft without customerButton is disabled

protected-leads.test.cjs — Protected Leads CRUD

TestWhat it doesAsserts
E2E-5.1Click "Add Domain", fill domain + reason, confirmDomain appears in the list after creation
E2E-5.2Type created domain in searchFiltered results contain the domain
E2E-5.3Click trash icon, confirm "Remove" dialogDomain no longer in list after deletion

invoices-coupons.test.cjs + bulk-delete.test.cjs

TestWhat it doesAsserts
E2E-4.1Select customer, tick 2 checkboxes, bulk deleteDelete dialog appears, confirms (skips if <2 rows)
E2E-6.1Select customer on invoices pagePage loads without error
E2E-7.1Enter "INVALIDCOUPON" in wizard, click ApplyError elements appear (red text or toast)

Reproducibility

The previous test runner (e2e-crud.cjs) was a single sequential script. It worked, but had several issues that made results unreliable across runs. The Jest suite fixes each of them:

ProblemOriginal scriptJest suite
Browser lifecycle Fragile
One shared browser for all 23 tests. If any test corrupts the page state (stuck dialog, unexpected navigation), all subsequent tests fail.
Isolated
Each test file gets its own createBrowser() call. Tests within a file share a browser (for speed), but files are fully isolated. A crash in quotes.test.cjs can't break protected-leads.test.cjs.
State coupling Hidden
createdCustomerId from test 1.1 silently feeds into 1.4 and 1.5. If 1.1 fails, downstream tests degrade without clear errors — they use stale page state or null IDs.
Explicit
Every test that needs a customer ID has a fallback path: const id = createdCustomerId || '21950'. If the create test failed, the detail/edit tests still run against a known customer. The dependency is visible in the code.
Test data cleanup Accumulates
Creates customers like e2e-test-{ts}@test.com and domains like e2e-test-{ts}.com on every run. Never cleaned up. Dozens of ghost records pile up over time.
Unique + documented
Still uses timestamp-based names (necessary for isolation), but each domain includes jest in the name and the reason field says "safe to delete". The protected leads test attempts to delete its own domain after creation.
Soft failures Hidden
A test that returns early (e.g., "skipped: no checkbox rows") counts as PASS. 7 of 23 "passing" tests were actually skipped — invisible in the summary.
Visible
Skips are explicit: the test either returns early (Jest shows it as passed but the assertion count is low) or uses conditional assertions. The verbose output shows timing per test — a 50ms "pass" is clearly a skip vs a 5000ms real test.
Evidence On fail only
Passing tests logged ✓ [id] name with zero detail. No screenshots path, no timing, no "what was actually verified". Impossible to audit a green run.
Always
Every test takes screenshots at key steps (saved to /tmp/ea-screenshots/jest-*.png). Jest provides timing per test. Assertions document exactly what was checked: expect(tabTexts).toContain('Overview') is self-documenting.
Re-runnability Partial
Can only re-run the entire 23-test script. No way to re-run just the failing customer test.
Full
npx jest ... customers.test.cjs runs just the customer file. -t "E2E-1.2" runs a single test by name. Standard Jest filtering.

How Authentication Works

Tests bypass real Supabase auth entirely using scripts/playwright-helpers.cjs:

1. Client-side: Fake Supabase session

A fake JWT session is injected into localStorage under the key sb-auth-auth-token. The React app's ProtectedRoute component sees this and considers the user authenticated. No real token exchange happens.

2. Server-side: X-Test-As header

Every HTTP request is intercepted by Playwright's page.route('**/*') and an X-Test-As: admin@hex-rays.com header is added. The backend API recognizes this header and bypasses JWT validation, treating the request as coming from that user.

Note: This auth bypass only works in dev/test environments. The X-Test-As header is ignored in production.

Adding New Tests

Create a new .test.cjs file in tests/e2e/. It will be auto-discovered by Jest.

// tests/e2e/my-feature.test.cjs
const {
  BASE, createBrowser,
  screenshot, waitForLoadingDone,
} = require('./setup.cjs');

let browser, page;

beforeAll(async () => {
  ({ browser, page } = await createBrowser());
});

afterAll(async () => {
  await browser.close();
});

describe('My Feature', () => {
  test('does the thing', async () => {
    await page.goto(`${BASE}/my-page`, {
      waitUntil: 'networkidle',
      timeout: 15000,
    });
    await waitForLoadingDone(page);

    // Screenshot for evidence
    await screenshot(page, 'my-feature-loaded');

    // Assertions
    const heading = page.locator('h1');
    expect(await heading.textContent()).toContain('Expected');
  });
});

Tips

Known Limitations

Quote lifecycle tests depend on data availability

E2E-2.7 through 2.10 (Mark as Sent, Reject, Duplicate) require an existing quote in the right status. If no quotes exist for the selected customer, these tests skip gracefully. To fully exercise the lifecycle, you need a customer with quotes in generated or sent status.

Bulk delete needs multiple draft quotes

E2E-4.1 needs at least 2 rows with checkboxes. If the selected customer has no quotes (or none in draft status), the test skips.

Test data accumulates

Each run creates a customer (e2e-test-{ts}@test.com) and a protected domain. The domain is cleaned up in-test, but the customer persists. For long-running dev environments, periodically clean up customers with the e2e-test- prefix.

CORS errors in dev

The /api/retool/products endpoint has a CORS issue in dev (see BUG-001 on the main report). This causes console errors but doesn't block most tests.