test: add 18 e2e tests for billing, checkout, supplier signup/dashboard, export
- Pricing page (EN/DE, plan cards, no-auth access) - Checkout success (auth required, renders for authed user) - Supplier signup wizard (step 1, plan cards, DE variant, success page) - Supplier dashboard (overview stats, boosts/credit packs, listing, leads tabs) - Business plan export (auth required, form renders) Also fixes: - E2e server init_db mock scope — before_serving was calling real init_db outside the patch context, overwriting the in-memory DB (fixes 3 pre-existing failures: markets_hub, markets_results, signup_page) - Add _seed_billing_data() for supplier + feature flags in e2e server - Mock RESEND_API_KEY="" in conftest + e2e server to prevent real emails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,33 @@ BASE = f"http://127.0.0.1:{PORT}"
|
||||
# Server / Browser Fixtures
|
||||
# =============================================================================
|
||||
|
||||
async def _seed_billing_data(conn):
|
||||
"""Seed a supplier + feature flags for e2e billing tests."""
|
||||
# Create user for supplier dashboard
|
||||
await conn.execute(
|
||||
"INSERT INTO users (id, email, created_at)"
|
||||
" VALUES (999, 'supplier@test.com', datetime('now'))"
|
||||
)
|
||||
# Create pro-tier supplier claimed by that user
|
||||
await conn.execute(
|
||||
"INSERT INTO suppliers"
|
||||
" (id, name, slug, tier, claimed_by, claimed_at,"
|
||||
" country_code, region, category, credit_balance,"
|
||||
" monthly_credits, contact_name, contact_email, created_at)"
|
||||
" VALUES (1, 'Test Supplier GmbH', 'test-supplier', 'pro', 999,"
|
||||
" datetime('now'), 'DE', 'Europe', 'construction', 50,"
|
||||
" 10, 'Test User', 'supplier@test.com', datetime('now'))"
|
||||
)
|
||||
# Enable feature flags needed for billing/signup/export tests
|
||||
for flag in ("supplier_signup", "markets", "payments",
|
||||
"planner_export", "lead_unlock"):
|
||||
await conn.execute(
|
||||
"INSERT OR REPLACE INTO feature_flags (name, enabled)"
|
||||
" VALUES (?, 1)", (flag,)
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
|
||||
def _run_server(ready_event):
|
||||
"""Run the Quart dev server in a subprocess with in-memory SQLite."""
|
||||
import aiosqlite
|
||||
@@ -59,16 +86,32 @@ def _run_server(ready_event):
|
||||
conn.row_factory = aiosqlite.Row
|
||||
await conn.execute("PRAGMA foreign_keys=ON")
|
||||
await conn.executescript(schema_ddl)
|
||||
await conn.commit()
|
||||
# Safety: ensure feature_flags table exists (may be missed by
|
||||
# executescript if an earlier DDL statement involving FTS5 fails)
|
||||
await conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS feature_flags"
|
||||
" (name TEXT PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 0,"
|
||||
" description TEXT,"
|
||||
" updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')))"
|
||||
)
|
||||
await _seed_billing_data(conn)
|
||||
core._db = conn
|
||||
|
||||
with patch.object(core, "init_db", new_callable=AsyncMock), \
|
||||
patch.object(core, "close_db", new_callable=AsyncMock):
|
||||
# Never send real emails from the e2e server
|
||||
core.config.RESEND_API_KEY = ""
|
||||
|
||||
# Patch init_db/close_db at the *app* module level (where before_serving
|
||||
# imports them via `from .core import init_db`). Patching core.init_db
|
||||
# alone won't affect the already-bound reference in app.py's globals.
|
||||
with patch("padelnomics.app.init_db", new_callable=AsyncMock), \
|
||||
patch("padelnomics.app.close_db", new_callable=AsyncMock), \
|
||||
patch("padelnomics.app.open_analytics_db"), \
|
||||
patch("padelnomics.app.close_analytics_db"):
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
|
||||
ready_event.set()
|
||||
await app.run_task(host="127.0.0.1", port=PORT)
|
||||
ready_event.set()
|
||||
await app.run_task(host="127.0.0.1", port=PORT)
|
||||
|
||||
asyncio.run(_serve())
|
||||
|
||||
@@ -511,3 +554,187 @@ def test_planner_tooltips_present(live_server, page):
|
||||
# After clicking returns tab, look for tooltip info spans
|
||||
ti_spans = page.locator(".ti")
|
||||
assert ti_spans.count() >= 1, "No tooltip spans (.ti) found on results tab"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# J. Pricing Page
|
||||
# =============================================================================
|
||||
|
||||
def test_pricing_page_loads_en(live_server, page):
|
||||
"""Pricing page renders in English."""
|
||||
resp = page.goto(live_server + "/billing/pricing")
|
||||
assert resp.ok, f"/billing/pricing returned {resp.status}"
|
||||
expect(page.locator("h1").first).to_be_visible()
|
||||
|
||||
|
||||
def test_pricing_page_loads_de(live_server, page):
|
||||
"""Pricing page renders in German when lang cookie is set."""
|
||||
page.context.add_cookies([{
|
||||
"name": "lang", "value": "de",
|
||||
"domain": "127.0.0.1", "path": "/"
|
||||
}])
|
||||
resp = page.goto(live_server + "/billing/pricing")
|
||||
assert resp.ok, f"/billing/pricing returned {resp.status}"
|
||||
expect(page.locator("h1").first).to_be_visible()
|
||||
|
||||
|
||||
def test_pricing_page_has_plan_cards(live_server, page):
|
||||
"""Pricing page shows at least 2 plan cards with CTAs."""
|
||||
page.goto(live_server + "/billing/pricing")
|
||||
page.wait_for_load_state("networkidle")
|
||||
cards = page.locator(".card-header")
|
||||
assert cards.count() >= 2, f"Expected >=2 plan cards, found {cards.count()}"
|
||||
# Each card should have a CTA button/link
|
||||
ctas = page.locator("a.btn, a.btn-outline")
|
||||
assert ctas.count() >= 2, f"Expected >=2 CTAs, found {ctas.count()}"
|
||||
|
||||
|
||||
def test_pricing_page_no_auth_required(live_server, page):
|
||||
"""Pricing page is accessible without authentication."""
|
||||
resp = page.goto(live_server + "/billing/pricing")
|
||||
assert resp.ok
|
||||
# Should NOT have redirected to login
|
||||
assert "login" not in page.url and "auth" not in page.url
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# K. Checkout Success
|
||||
# =============================================================================
|
||||
|
||||
def test_checkout_success_requires_auth(live_server, page):
|
||||
"""Checkout success page redirects unauthenticated users to login."""
|
||||
page.goto(live_server + "/billing/success", wait_until="networkidle")
|
||||
assert "login" in page.url or "auth" in page.url, (
|
||||
f"Expected redirect to login, got: {page.url}"
|
||||
)
|
||||
|
||||
|
||||
def test_checkout_success_renders_for_authed_user(live_server, page):
|
||||
"""Checkout success page renders for authenticated user."""
|
||||
dev_login(page, live_server, "checkout@example.com")
|
||||
resp = page.goto(live_server + "/billing/success")
|
||||
assert resp.ok
|
||||
expect(page.locator("h1").first).to_be_visible()
|
||||
# Should have a CTA button (back to dashboard or similar)
|
||||
expect(page.locator("a.btn").first).to_be_visible()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# L. Supplier Signup Wizard
|
||||
# =============================================================================
|
||||
|
||||
def test_supplier_signup_step1_loads(live_server, page):
|
||||
"""Supplier signup wizard step 1 renders."""
|
||||
resp = page.goto(live_server + "/en/suppliers/signup")
|
||||
assert resp.ok, f"/en/suppliers/signup returned {resp.status}"
|
||||
# Wait for HTMX to load step content
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("[data-step='1']").first).to_be_visible()
|
||||
|
||||
|
||||
def test_supplier_signup_step1_has_plan_cards(live_server, page):
|
||||
"""Step 1 shows plan cards for selecting a tier."""
|
||||
page.goto(live_server + "/en/suppliers/signup")
|
||||
page.wait_for_load_state("networkidle")
|
||||
plan_cards = page.locator(".s-plan-card")
|
||||
assert plan_cards.count() >= 2, (
|
||||
f"Expected >=2 plan cards, found {plan_cards.count()}"
|
||||
)
|
||||
# Should have a next button
|
||||
expect(page.locator(".s-btn-next").first).to_be_visible()
|
||||
|
||||
|
||||
def test_supplier_signup_step1_de_loads(live_server, page):
|
||||
"""German variant of supplier signup wizard loads."""
|
||||
resp = page.goto(live_server + "/de/suppliers/signup")
|
||||
assert resp.ok, f"/de/suppliers/signup returned {resp.status}"
|
||||
page.wait_for_load_state("networkidle")
|
||||
expect(page.locator("[data-step='1']").first).to_be_visible()
|
||||
|
||||
|
||||
def test_supplier_signup_success_page_loads(live_server, page):
|
||||
"""Supplier signup success page renders."""
|
||||
resp = page.goto(live_server + "/en/suppliers/signup/success")
|
||||
assert resp.ok, f"signup success returned {resp.status}"
|
||||
expect(page.locator("h1, h2").first).to_be_visible()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# M. Supplier Dashboard (seeded supplier data)
|
||||
# =============================================================================
|
||||
|
||||
def test_supplier_dashboard_loads(live_server, page):
|
||||
"""Dashboard loads for authenticated supplier."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
resp = page.goto(live_server + "/en/suppliers/dashboard")
|
||||
assert resp.ok, f"dashboard returned {resp.status}"
|
||||
expect(page.locator(".dash").first).to_be_visible()
|
||||
# Sidebar should show supplier name
|
||||
expect(page.locator(".dash-sidebar__name").first).to_be_visible()
|
||||
|
||||
|
||||
def test_supplier_dashboard_overview_tab(live_server, page):
|
||||
"""Overview tab renders with stats grid."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
page.goto(live_server + "/en/suppliers/dashboard")
|
||||
page.wait_for_load_state("networkidle")
|
||||
# Wait for HTMX to load overview content
|
||||
page.wait_for_timeout(1000)
|
||||
stats = page.locator(".ov-stat")
|
||||
assert stats.count() >= 2, f"Expected >=2 stat cards, found {stats.count()}"
|
||||
|
||||
|
||||
def test_supplier_dashboard_boosts_tab(live_server, page):
|
||||
"""Boosts tab renders with boost options."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
resp = page.goto(live_server + "/en/suppliers/dashboard/boosts")
|
||||
assert resp.ok, f"boosts tab returned {resp.status}"
|
||||
|
||||
|
||||
def test_supplier_dashboard_boosts_has_credit_packs(live_server, page):
|
||||
"""Boosts tab shows credit pack purchase options."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
page.goto(live_server + "/en/suppliers/dashboard/boosts")
|
||||
page.wait_for_load_state("networkidle")
|
||||
content = page.content()
|
||||
# Credit pack amounts should be visible (25, 50, 100, 250)
|
||||
assert "25" in content or "50" in content or "100" in content, (
|
||||
"No credit pack amounts found on boosts page"
|
||||
)
|
||||
|
||||
|
||||
def test_supplier_dashboard_listing_tab(live_server, page):
|
||||
"""Listing tab renders for authenticated supplier."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
resp = page.goto(live_server + "/en/suppliers/dashboard/listing")
|
||||
assert resp.ok, f"listing tab returned {resp.status}"
|
||||
|
||||
|
||||
def test_supplier_dashboard_leads_tab(live_server, page):
|
||||
"""Leads tab renders for pro-tier supplier."""
|
||||
dev_login(page, live_server, "supplier@test.com")
|
||||
resp = page.goto(live_server + "/en/suppliers/dashboard/leads")
|
||||
assert resp.ok, f"leads tab returned {resp.status}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# N. Business Plan Export
|
||||
# =============================================================================
|
||||
|
||||
def test_export_page_requires_auth(live_server, page):
|
||||
"""Export page redirects unauthenticated users to login."""
|
||||
page.goto(live_server + "/en/planner/export", wait_until="networkidle")
|
||||
assert "login" in page.url or "auth" in page.url, (
|
||||
f"Expected redirect to login, got: {page.url}"
|
||||
)
|
||||
|
||||
|
||||
def test_export_page_loads_for_authed_user(live_server, page):
|
||||
"""Export page renders for authenticated user with form."""
|
||||
dev_login(page, live_server, "export@example.com")
|
||||
resp = page.goto(live_server + "/en/planner/export")
|
||||
assert resp.ok, f"export page returned {resp.status}"
|
||||
expect(page.locator("h1").first).to_be_visible()
|
||||
# Should have the export form with scenario select and buy button
|
||||
expect(page.locator("#export-form").first).to_be_visible()
|
||||
expect(page.locator("#export-buy-btn").first).to_be_visible()
|
||||
|
||||
Reference in New Issue
Block a user