Reproducible Jest + Playwright tests for the EA Tool CRUD operations
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.
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.
| Test | What it does | Asserts |
|---|---|---|
| E2E-1.1 | Fill create form (email, name, company, country) and submit | Redirect to /customers/:id, ID extracted from URL |
| E2E-1.2 | Click submit without filling email | Stays on /customers/new, validation visible (message or :invalid) |
| E2E-1.3 | Click Cancel on create form | URL no longer contains /new |
| E2E-1.4 | Navigate to created customer detail | 13+ tabs rendered, includes Overview, Licenses, Quotes, Invoices |
| E2E-1.5 | Click Edit, change company name, save | Edit form loads, save succeeds, stays on detail page |
| Test | What it does | Asserts |
|---|---|---|
| E2E-2.1 | Navigate to /quotes/new | Type selector shows New Quote, Renewal, Upsell |
| E2E-2.2 | Enter wizard, search "hex" in customer field | Search input exists, dropdown appears |
| E2E-2.3 | Change duration to 2 years, currency to USD | Selects open, options clickable (uses force: true for overlay) |
| E2E-2.4 | Click "Add item", pick product + license type | "Add item" button exists, product picker works |
| E2E-2.5 | Check Save Draft button state | Disabled without customer/product (expected), clicks if enabled |
| E2E-2.6 | Check Save & Generate button state | Same pattern as 2.5 — documents state |
| E2E-2.7 | Navigate to existing quote detail via list | Selects customer, clicks row, checks action buttons |
| E2E-2.8 | Click "Mark as Sent" + confirm dialog | Dialog appears, status updates (skips if not available) |
| E2E-2.9 | Click "Reject" + confirm dialog | Dialog appears, status updates (skips if not available) |
| E2E-2.10 | Open actions dropdown, click "Duplicate" | Menu opens, duplicate item exists (skips if not available) |
| E2E-3.1 | Load empty wizard, check validation | "Customer must be selected" + "At least one product" visible, or buttons disabled |
| E2E-3.2 | Check Save Draft without customer | Button is disabled |
| Test | What it does | Asserts |
|---|---|---|
| E2E-5.1 | Click "Add Domain", fill domain + reason, confirm | Domain appears in the list after creation |
| E2E-5.2 | Type created domain in search | Filtered results contain the domain |
| E2E-5.3 | Click trash icon, confirm "Remove" dialog | Domain no longer in list after deletion |
| Test | What it does | Asserts |
|---|---|---|
| E2E-4.1 | Select customer, tick 2 checkboxes, bulk delete | Delete dialog appears, confirms (skips if <2 rows) |
| E2E-6.1 | Select customer on invoices page | Page loads without error |
| E2E-7.1 | Enter "INVALIDCOUPON" in wizard, click Apply | Error elements appear (red text or toast) |
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:
| Problem | Original script | Jest 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 | HiddencreatedCustomerId 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. |
Fullnpx jest ... customers.test.cjs runs just the customer file. -t "E2E-1.2" runs a single test by name. Standard Jest filtering. |
Tests bypass real Supabase auth entirely using scripts/playwright-helpers.cjs:
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.
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.
X-Test-As header is ignored in production.
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'); }); });
waitForLoadingDone(page) after navigation — the app has spinners and lazy-loaded data.CustomerSelector component is a text <input>, not a [role="combobox"]. Results appear as button[data-dropdown-item] elements. Use the selectCustomer() helper.{ force: true } on clicks when needed.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.
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.
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.
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.