fix(tests): add sections J-N, force WAITLIST_MODE=false in visual server

- Add 18 new E2E tests from master: pricing, checkout, supplier signup,
  supplier dashboard, and business plan export (sections J-N)
- Force WAITLIST_MODE=false in visual server subprocess — the root .env
  sets WAITLIST_MODE=true, and since Config class attributes evaluate at
  import time (before fork), the subprocess inherits the parent's value.
  Patching both os.environ and core.config directly ensures feature pages
  render instead of waitlist templates.
- All 77 visual tests now pass in ~59 seconds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-23 21:08:09 +01:00
parent 777333e918
commit 611f938d52
5 changed files with 218 additions and 3 deletions

View File

@@ -226,16 +226,45 @@ def sign_payload(payload_bytes: bytes, secret: str = "whsec_test_secret") -> str
_VISUAL_PORT = 5111
async def _seed_visual_data(conn):
"""Seed a supplier + feature flags for E2E billing/dashboard tests."""
await conn.execute(
"INSERT INTO users (id, email, created_at)"
" VALUES (999, 'supplier@test.com', datetime('now'))"
)
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'))"
)
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_visual_server(ready_event):
"""Run a Quart dev server in a subprocess for visual/E2E tests.
Forces RESEND_API_KEY="" so no real emails are sent.
Forces WAITLIST_MODE=false so feature pages render (not waitlist templates).
Runs migrations in-process to build the full schema (including FTS tables).
"""
import asyncio
import os
os.environ["RESEND_API_KEY"] = ""
os.environ["WAITLIST_MODE"] = "false"
# Config class attributes are evaluated at import time (before fork),
# so we must also patch the live config object directly.
core.config.WAITLIST_MODE = False
async def _serve():
# Build schema DDL in-process (FTS5 virtual tables need this)
@@ -257,7 +286,16 @@ def _run_visual_server(ready_event):
conn.row_factory = aiosqlite.Row
await conn.execute("PRAGMA foreign_keys=ON")
await conn.executescript(schema_ddl)
await conn.commit()
# Ensure feature_flags table exists (may be missed if an FTS5
# CREATE VIRTUAL TABLE causes executescript to stop early)
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')))"
)
# Seed data needed by E2E tests (supplier dashboard, billing, etc.)
await _seed_visual_data(conn)
core._db = conn
# Patch init_db/close_db where they're USED (app.py imports them
@@ -266,7 +304,9 @@ def _run_visual_server(ready_event):
# Patches must stay active through run_task() because before_serving
# hooks call init_db() which would replace our in-memory DB.
with patch("padelnomics.app.init_db", new_callable=AsyncMock), \
patch("padelnomics.app.close_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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -2,7 +2,7 @@
Comprehensive E2E flow tests using Playwright.
Covers all major user flows: public pages, planner, auth, directory, quote wizard,
and cross-cutting checks (translations, footer, language switcher).
billing, supplier dashboard, and cross-cutting checks (translations, footer, language switcher).
Skipped by default (requires `playwright install chromium`).
Run explicitly with:
@@ -403,3 +403,178 @@ def test_planner_tooltips_present(live_server, page):
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()}"
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
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()
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}"
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()}"
)
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()
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")
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()
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()
expect(page.locator("#export-form").first).to_be_visible()
expect(page.locator("#export-buy-btn").first).to_be_visible()