Files
padelnomics/web/tests/test_quote_wizard.py
Deeman 777333e918 refactor(tests): overhaul visual tests — single server, mock emails, fix init_db
- Consolidate 3 duplicate server processes into 1 session-scoped
  live_server fixture in conftest.py (port 5111, shared across all
  visual test modules). Reduces startup overhead from ~3× to 1×.

- Fix init_db mock: patch padelnomics.app.init_db (where it's used)
  instead of core.init_db (where it's defined). The before_serving
  hook imported init_db locally — patching core alone didn't prevent
  the real init_db from replacing the in-memory test DB.

- Keep patches active through app.run_task() so before_serving hooks
  can't replace the test DB during the server's lifetime.

- Force RESEND_API_KEY="" in the visual test server subprocess to
  prevent real email sends (dev mode: prints to stdout, returns "dev").

- Remove 4 screenshot-only no-op tests, replace with single
  test_capture_screenshots that grabs all pages in one pass.

- Fix test_planner_tab_switching: remove nonexistent "metrics" tab.

- Delete ~200 lines of duplicated boilerplate from 3 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:40:11 +01:00

191 lines
5.7 KiB
Python

"""
Playwright tests for the 9-step quote wizard flow.
Tests the full happy path, back navigation with data preservation,
and validation error handling.
Run explicitly with:
uv run pytest -m visual tests/test_quote_wizard.py -v
"""
from pathlib import Path
import pytest
from playwright.sync_api import expect
pytestmark = pytest.mark.visual
SCREENSHOTS_DIR = Path(__file__).parent / "screenshots"
SCREENSHOTS_DIR.mkdir(exist_ok=True)
@pytest.fixture
def page(browser):
"""Desktop page for quote wizard tests."""
pg = browser.new_page(viewport={"width": 1280, "height": 900})
yield pg
pg.close()
def _check_radio(page, name, value):
"""Click the label for a CSS-hidden radio/checkbox pill button."""
page.locator(f"label:has(input[name='{name}'][value='{value}'])").click()
def _fill_step_1(page):
_check_radio(page, "facility_type", "indoor")
def _fill_step_2(page):
page.select_option("select[name='country']", "DE")
def _fill_step_5(page):
_check_radio(page, "timeline", "3-6mo")
def _fill_step_6(page):
_check_radio(page, "financing_status", "self_funded")
_check_radio(page, "decision_process", "solo")
def _fill_step_7(page):
_check_radio(page, "stakeholder_type", "entrepreneur")
def _fill_step_8(page):
_check_radio(page, "services_needed", "installation")
def _fill_step_9(page):
page.fill("input[name='contact_name']", "Test User")
page.fill("input[name='contact_email']", "test@example.com")
page.fill("input[name='contact_phone']", "+49 123 456789")
page.locator("input[name='consent']").check()
def _click_next(page):
page.locator("button.q-btn-next").click()
page.wait_for_timeout(500)
def _click_back(page):
page.locator("button.q-btn-back").click()
page.wait_for_timeout(500)
def test_quote_wizard_full_flow(live_server, page):
"""Complete all 9 steps and submit — verify success page shown."""
page.goto(f"{live_server}/en/leads/quote")
page.wait_for_load_state("networkidle")
# Step 1: Your Project
expect(page.locator("h2.q-step-title")).to_contain_text("Your Project")
_fill_step_1(page)
_click_next(page)
# Step 2: Location
expect(page.locator("h2.q-step-title")).to_contain_text("Location")
_fill_step_2(page)
_click_next(page)
# Step 3: Build Context (optional)
expect(page.locator("h2.q-step-title")).to_contain_text("Build Context")
_click_next(page)
# Step 4: Project Phase (optional)
expect(page.locator("h2.q-step-title")).to_contain_text("Project Phase")
_click_next(page)
# Step 5: Timeline
expect(page.locator("h2.q-step-title")).to_contain_text("Timeline")
_fill_step_5(page)
_click_next(page)
# Step 6: Financing
expect(page.locator("h2.q-step-title")).to_contain_text("Financing")
_fill_step_6(page)
_click_next(page)
# Step 7: About You
expect(page.locator("h2.q-step-title")).to_contain_text("About You")
_fill_step_7(page)
_click_next(page)
# Step 8: Services Needed
expect(page.locator("h2.q-step-title")).to_contain_text("Services Needed")
_fill_step_8(page)
_click_next(page)
# Step 9: Contact Details
expect(page.locator("h2.q-step-title")).to_contain_text("Contact Details")
_fill_step_9(page)
# Submit
page.locator("button.q-btn-submit").click()
page.wait_for_load_state("networkidle")
body_text = page.locator("body").inner_text()
assert (
"matched" in body_text.lower()
or "check your email" in body_text.lower()
or "verify" in body_text.lower()
), f"Expected success or verification page, got: {body_text[:200]}"
page.screenshot(path=str(SCREENSHOTS_DIR / "quote_wizard_submitted.png"), full_page=True)
def test_quote_wizard_back_navigation(live_server, page):
"""Go forward 3 steps, go back, verify data preserved in form fields."""
page.goto(f"{live_server}/en/leads/quote")
page.wait_for_load_state("networkidle")
_fill_step_1(page)
_click_next(page)
_fill_step_2(page)
page.fill("input[name='city']", "Berlin")
_click_next(page)
_check_radio(page, "build_context", "new_standalone")
_click_next(page)
# Step 4: go back to step 3
expect(page.locator("h2.q-step-title")).to_contain_text("Project Phase")
_click_back(page)
expect(page.locator("h2.q-step-title")).to_contain_text("Build Context")
checked = page.locator("input[name='build_context'][value='new_standalone']").is_checked()
assert checked, "Build context 'new_standalone' should still be checked after going back"
_click_back(page)
expect(page.locator("h2.q-step-title")).to_contain_text("Location")
country_val = page.locator("select[name='country']").input_value()
assert country_val == "DE", f"Country should be DE, got {country_val}"
city_val = page.locator("input[name='city']").input_value()
assert city_val == "Berlin", f"City should be Berlin, got {city_val}"
def test_quote_wizard_validation_errors(live_server, page):
"""Skip a required field on step 1 — verify error shown on same step."""
page.goto(f"{live_server}/en/leads/quote")
page.wait_for_load_state("networkidle")
expect(page.locator("h2.q-step-title")).to_contain_text("Your Project")
_click_next(page)
# Should still be on step 1 with error hint
expect(page.locator("h2.q-step-title")).to_contain_text("Your Project")
expect(page.locator(".q-error-hint")).to_be_visible()
# Fix and proceed
_fill_step_1(page)
_click_next(page)
expect(page.locator("h2.q-step-title")).to_contain_text("Location")
# Skip country (required)
_click_next(page)
expect(page.locator("h2.q-step-title")).to_contain_text("Location")
expect(page.locator(".q-error-hint")).to_be_visible()