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:
@@ -226,16 +226,45 @@ def sign_payload(payload_bytes: bytes, secret: str = "whsec_test_secret") -> str
|
|||||||
_VISUAL_PORT = 5111
|
_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):
|
def _run_visual_server(ready_event):
|
||||||
"""Run a Quart dev server in a subprocess for visual/E2E tests.
|
"""Run a Quart dev server in a subprocess for visual/E2E tests.
|
||||||
|
|
||||||
Forces RESEND_API_KEY="" so no real emails are sent.
|
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).
|
Runs migrations in-process to build the full schema (including FTS tables).
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["RESEND_API_KEY"] = ""
|
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():
|
async def _serve():
|
||||||
# Build schema DDL in-process (FTS5 virtual tables need this)
|
# 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
|
conn.row_factory = aiosqlite.Row
|
||||||
await conn.execute("PRAGMA foreign_keys=ON")
|
await conn.execute("PRAGMA foreign_keys=ON")
|
||||||
await conn.executescript(schema_ddl)
|
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
|
core._db = conn
|
||||||
|
|
||||||
# Patch init_db/close_db where they're USED (app.py imports them
|
# 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
|
# Patches must stay active through run_task() because before_serving
|
||||||
# hooks call init_db() which would replace our in-memory DB.
|
# hooks call init_db() which would replace our in-memory DB.
|
||||||
with patch("padelnomics.app.init_db", new_callable=AsyncMock), \
|
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 = create_app()
|
||||||
app.config["TESTING"] = True
|
app.config["TESTING"] = True
|
||||||
ready_event.set()
|
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 |
@@ -2,7 +2,7 @@
|
|||||||
Comprehensive E2E flow tests using Playwright.
|
Comprehensive E2E flow tests using Playwright.
|
||||||
|
|
||||||
Covers all major user flows: public pages, planner, auth, directory, quote wizard,
|
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`).
|
Skipped by default (requires `playwright install chromium`).
|
||||||
Run explicitly with:
|
Run explicitly with:
|
||||||
@@ -403,3 +403,178 @@ def test_planner_tooltips_present(live_server, page):
|
|||||||
|
|
||||||
ti_spans = page.locator(".ti")
|
ti_spans = page.locator(".ti")
|
||||||
assert ti_spans.count() >= 1, "No tooltip spans (.ti) found on results tab"
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user