- 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>
581 lines
22 KiB
Python
581 lines
22 KiB
Python
"""
|
|
Comprehensive E2E flow tests using Playwright.
|
|
|
|
Covers all major user flows: public pages, planner, auth, directory, quote wizard,
|
|
billing, supplier dashboard, and cross-cutting checks (translations, footer, language switcher).
|
|
|
|
Skipped by default (requires `playwright install chromium`).
|
|
Run explicitly with:
|
|
uv run pytest -m visual tests/test_e2e_flows.py -v
|
|
"""
|
|
import pytest
|
|
from playwright.sync_api import expect
|
|
|
|
pytestmark = pytest.mark.visual
|
|
|
|
|
|
@pytest.fixture
|
|
def page(browser):
|
|
"""Wide desktop page for E2E tests (1440px for sidebar tests)."""
|
|
pg = browser.new_page(viewport={"width": 1440, "height": 900})
|
|
yield pg
|
|
pg.close()
|
|
|
|
|
|
def dev_login(page, base, email="test@example.com"):
|
|
"""Instantly authenticate via dev-login endpoint."""
|
|
page.goto(f"{base}/auth/dev-login?email={email}")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
|
|
# =============================================================================
|
|
# A. Public Pages — smoke test every public GET returns 200
|
|
# =============================================================================
|
|
|
|
def test_root_redirects_to_lang(live_server, page):
|
|
"""GET / should redirect to /<lang>/."""
|
|
resp = page.goto(live_server + "/")
|
|
assert resp.ok, f"/ returned {resp.status}"
|
|
assert "/en/" in page.url or "/de/" in page.url, f"No lang in URL: {page.url}"
|
|
|
|
|
|
@pytest.mark.parametrize("path", [
|
|
"/en/",
|
|
"/de/",
|
|
"/en/features",
|
|
"/en/terms",
|
|
"/en/privacy",
|
|
"/en/about",
|
|
"/en/imprint",
|
|
"/en/suppliers",
|
|
"/de/features",
|
|
"/de/about",
|
|
"/de/suppliers",
|
|
])
|
|
def test_public_page_200(live_server, page, path):
|
|
"""Every public page should return 200 and contain meaningful content."""
|
|
resp = page.goto(live_server + path)
|
|
assert resp.ok, f"{path} returned {resp.status}"
|
|
heading = page.locator("h1, h2").first
|
|
expect(heading).to_be_visible()
|
|
|
|
|
|
def test_robots_txt(live_server, page):
|
|
resp = page.goto(live_server + "/robots.txt")
|
|
assert resp.ok
|
|
assert "User-agent" in page.content()
|
|
|
|
|
|
def test_sitemap_xml(live_server, page):
|
|
resp = page.goto(live_server + "/sitemap.xml")
|
|
assert resp.ok
|
|
assert "<urlset" in page.content() or "<sitemapindex" in page.content()
|
|
|
|
|
|
def test_health_endpoint(live_server, page):
|
|
resp = page.goto(live_server + "/health")
|
|
assert resp.ok
|
|
|
|
|
|
# =============================================================================
|
|
# B. Planner Flow
|
|
# =============================================================================
|
|
|
|
def test_planner_en_loads(live_server, page):
|
|
"""Planner page renders wizard and tab bar."""
|
|
resp = page.goto(live_server + "/en/planner/")
|
|
assert resp.ok
|
|
expect(page.locator("#nav")).to_be_visible()
|
|
expect(page.locator("#planner-form")).to_be_visible()
|
|
|
|
|
|
def test_planner_de_loads(live_server, page):
|
|
"""German planner renders with German UI strings."""
|
|
resp = page.goto(live_server + "/de/planner/")
|
|
assert resp.ok
|
|
content = page.content()
|
|
assert "Investition" in content or "Annahmen" in content or "Anlage" in content
|
|
|
|
|
|
def test_planner_calculate_htmx(live_server, page):
|
|
"""Clicking a result tab fires HTMX and shows the results panel."""
|
|
page.goto(live_server + "/en/planner/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
capex_btn = page.locator("button[data-tab='capex'], [data-tab='capex']").first
|
|
capex_btn.click()
|
|
page.wait_for_timeout(1000)
|
|
|
|
tab_content = page.locator("#tab-content")
|
|
expect(tab_content).to_be_visible()
|
|
assert len(tab_content.inner_html()) > 100
|
|
|
|
|
|
def test_planner_tab_switching(live_server, page):
|
|
"""Clicking all result tabs renders different content each time."""
|
|
page.goto(live_server + "/en/planner/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
tabs = ["capex", "operating", "cashflow", "returns"]
|
|
seen_contents = set()
|
|
|
|
for tab_id in tabs:
|
|
btn = page.locator(f"button[data-tab='{tab_id}'], [data-tab='{tab_id}']").first
|
|
if btn.count() == 0:
|
|
btn = page.get_by_role("button", name=tab_id, exact=False).first
|
|
btn.click()
|
|
page.wait_for_timeout(600)
|
|
html = page.locator("#tab-content").inner_html()
|
|
seen_contents.add(html[:100])
|
|
|
|
assert len(seen_contents) >= 3, "Tab content didn't change across tabs"
|
|
|
|
|
|
def test_planner_chart_data_present(live_server, page):
|
|
"""Result tabs embed chart data as JSON script tags."""
|
|
page.goto(live_server + "/en/planner/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
chart_scripts = page.locator("script[type='application/json']")
|
|
assert chart_scripts.count() >= 1, "No chart JSON script tags found"
|
|
|
|
|
|
def test_planner_quote_sidebar_visible_wide(live_server, browser):
|
|
"""Quote sidebar should be visible on wide viewport (>1400px)."""
|
|
pg = browser.new_page(viewport={"width": 1600, "height": 900})
|
|
pg.goto(live_server + "/en/planner/")
|
|
pg.wait_for_load_state("networkidle")
|
|
|
|
sidebar = pg.locator(".quote-sidebar")
|
|
if sidebar.count() > 0:
|
|
display = pg.evaluate(
|
|
"getComputedStyle(document.querySelector('.quote-sidebar')).display"
|
|
)
|
|
assert display != "none", f"Quote sidebar hidden on wide viewport: display={display}"
|
|
pg.close()
|
|
|
|
|
|
# =============================================================================
|
|
# C. Auth Flow
|
|
# =============================================================================
|
|
|
|
def test_login_page_loads(live_server, page):
|
|
resp = page.goto(live_server + "/auth/login")
|
|
assert resp.ok
|
|
expect(page.locator("form").first).to_be_visible()
|
|
expect(page.locator("input[type='email']")).to_be_visible()
|
|
|
|
|
|
def test_signup_page_loads(live_server, page):
|
|
resp = page.goto(live_server + "/auth/signup")
|
|
assert resp.ok
|
|
expect(page.locator("form").first).to_be_visible()
|
|
|
|
|
|
def test_dev_login_redirects_to_dashboard(live_server, page):
|
|
"""Dev login should create session and redirect to dashboard."""
|
|
page.goto(live_server + "/auth/dev-login?email=devtest@example.com")
|
|
page.wait_for_load_state("networkidle")
|
|
assert "/dashboard" in page.url, f"Expected dashboard redirect, got: {page.url}"
|
|
|
|
|
|
def test_authenticated_dashboard_loads(live_server, page):
|
|
"""After dev-login, dashboard should be accessible."""
|
|
dev_login(page, live_server, "dash@example.com")
|
|
assert "/dashboard" in page.url
|
|
resp_status = page.evaluate("() => document.readyState")
|
|
assert resp_status == "complete"
|
|
|
|
|
|
def test_dashboard_quote_link_goes_to_wizard(live_server, page):
|
|
"""Dashboard 'request quote' button should link to the quote wizard."""
|
|
dev_login(page, live_server, "dashquote@example.com")
|
|
href = page.locator("a", has_text="Quote").first.get_attribute("href")
|
|
assert "/leads/quote" in href, f"Expected quote wizard link, got: {href}"
|
|
assert "suppliers" not in href, f"Link should not point to suppliers page: {href}"
|
|
|
|
|
|
def test_unauthenticated_dashboard_redirects(live_server, page):
|
|
"""Without auth, /dashboard/ should redirect to login."""
|
|
page.goto(live_server + "/dashboard/", wait_until="networkidle")
|
|
assert "login" in page.url or "auth" in page.url, (
|
|
f"Expected redirect to login, got: {page.url}"
|
|
)
|
|
|
|
|
|
def test_magic_link_sent_page(live_server, page):
|
|
"""Magic link sent page renders with the email address shown."""
|
|
resp = page.goto(live_server + "/auth/magic-link-sent?email=test@example.com")
|
|
assert resp.ok
|
|
expect(page.get_by_text("test@example.com")).to_be_visible()
|
|
|
|
|
|
# =============================================================================
|
|
# D. Directory Flow
|
|
# =============================================================================
|
|
|
|
def test_directory_en_loads(live_server, page):
|
|
resp = page.goto(live_server + "/en/directory/")
|
|
assert resp.ok
|
|
expect(page.locator("h1, h2").first).to_be_visible()
|
|
|
|
|
|
def test_directory_de_loads(live_server, page):
|
|
resp = page.goto(live_server + "/de/directory/")
|
|
assert resp.ok
|
|
content = page.content()
|
|
assert "Lieferanten" in content or "Anbieter" in content or "Kategorie" in content or resp.ok
|
|
|
|
|
|
def test_directory_search_htmx(live_server, page):
|
|
"""Typing in directory search fires HTMX and returns results partial."""
|
|
page.goto(live_server + "/en/directory/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
search = page.locator("input[type='search'], input[name='q'], input[type='text']").first
|
|
if search.count() == 0:
|
|
pytest.skip("No search input found")
|
|
|
|
search.fill("padel")
|
|
page.wait_for_timeout(600)
|
|
results = page.locator("#supplier-results, #results, [id*='result']").first
|
|
expect(results).to_be_visible()
|
|
|
|
|
|
# =============================================================================
|
|
# E. Quote Flow (key steps)
|
|
# =============================================================================
|
|
|
|
def test_quote_step1_loads(live_server, page):
|
|
"""Quote wizard step 1 renders the form."""
|
|
resp = page.goto(live_server + "/en/leads/quote")
|
|
assert resp.ok
|
|
expect(page.locator("form").first).to_be_visible()
|
|
|
|
|
|
def test_quote_step1_de_loads(live_server, page):
|
|
resp = page.goto(live_server + "/de/leads/quote")
|
|
assert resp.ok
|
|
content = page.content()
|
|
assert "Anlage" in content or "Platz" in content or "Projekt" in content or resp.ok
|
|
|
|
|
|
def test_quote_verify_url_includes_lang(live_server, page):
|
|
"""Verify the leads/verify route exists at /<lang>/leads/verify."""
|
|
resp = page.goto(live_server + "/en/leads/verify?token=invalid")
|
|
assert resp.status != 404, "Verify endpoint returned 404 — lang prefix missing?"
|
|
|
|
|
|
# =============================================================================
|
|
# F. Authenticated Flows (planner scenarios, leads forms)
|
|
# =============================================================================
|
|
|
|
def test_authenticated_planner_loads(live_server, page):
|
|
"""Authenticated user can access the planner."""
|
|
dev_login(page, live_server, "planneruser@example.com")
|
|
resp = page.goto(live_server + "/en/planner/")
|
|
assert resp.ok
|
|
expect(page.locator("#planner-form")).to_be_visible()
|
|
|
|
|
|
def test_planner_scenarios_list(live_server, page):
|
|
"""Authenticated user can access scenarios list."""
|
|
dev_login(page, live_server, "scenuser@example.com")
|
|
resp = page.goto(live_server + "/en/planner/scenarios")
|
|
assert resp.ok
|
|
|
|
|
|
# =============================================================================
|
|
# G. Cross-Cutting Checks
|
|
# =============================================================================
|
|
|
|
def test_footer_present_on_public_pages(live_server, page):
|
|
"""Footer should be present on all public pages."""
|
|
for path in ["/en/", "/en/features", "/en/about"]:
|
|
page.goto(live_server + path)
|
|
expect(page.locator("footer")).to_be_visible()
|
|
|
|
|
|
def test_footer_has_four_column_layout(live_server, page):
|
|
"""Footer grid should have 4 link columns."""
|
|
page.goto(live_server + "/en/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
footer_cols = page.evaluate("""
|
|
(() => {
|
|
const footer = document.querySelector('footer');
|
|
if (!footer) return 0;
|
|
const grid = footer.querySelector('[class*="grid"]');
|
|
if (!grid) return 0;
|
|
return grid.children.length;
|
|
})()
|
|
""")
|
|
assert footer_cols >= 4, f"Expected 4 footer columns, found {footer_cols}"
|
|
|
|
|
|
def test_language_switcher_en_to_de(live_server, page):
|
|
"""Language switcher should navigate from /en/ to /de/."""
|
|
page.goto(live_server + "/en/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
de_link = page.locator("a[href='/de/']").first
|
|
if de_link.count() == 0:
|
|
pytest.skip("No DE language switcher link found")
|
|
|
|
href = de_link.get_attribute("href")
|
|
assert href and "/de" in href, f"DE link has unexpected href: {href}"
|
|
|
|
page.goto(live_server + "/de/")
|
|
page.wait_for_load_state("networkidle")
|
|
assert "/de/" in page.url, f"Language switch failed: {page.url}"
|
|
|
|
|
|
def test_no_missing_translations_en(live_server, page):
|
|
"""EN pages should not contain untranslated markers."""
|
|
for path in ["/en/", "/en/features", "/en/about"]:
|
|
page.goto(live_server + path)
|
|
content = page.locator("body").inner_text()
|
|
assert "t.auth_" not in content, f"Untranslated key in {path}"
|
|
assert "{{" not in content, f"Jinja template not rendered in {path}"
|
|
|
|
|
|
def test_no_missing_translations_de(live_server, page):
|
|
"""DE pages should contain German text, not raw English keys."""
|
|
page.goto(live_server + "/de/")
|
|
content = page.locator("body").inner_text()
|
|
assert "{{" not in content, "Jinja template not rendered in /de/"
|
|
assert "t.auth_" not in content, "Untranslated key in /de/"
|
|
|
|
|
|
def test_auth_login_page_german(live_server, page):
|
|
"""Auth login should render in German when lang cookie is 'de'."""
|
|
page.context.add_cookies([{
|
|
"name": "lang", "value": "de",
|
|
"domain": "127.0.0.1", "path": "/"
|
|
}])
|
|
page.goto(live_server + "/auth/login")
|
|
content = page.content()
|
|
assert "Anmelden" in content or "E-Mail" in content or "Weiter" in content
|
|
|
|
|
|
def test_404_for_nonexistent_page(live_server, page):
|
|
"""Non-existent pages should return 404."""
|
|
resp = page.goto(live_server + "/en/this-page-does-not-exist-xyz")
|
|
assert resp.status == 404, f"Expected 404, got {resp.status}"
|
|
|
|
|
|
def test_legacy_redirect_terms(live_server, page):
|
|
"""Legacy /terms should redirect to /en/terms."""
|
|
resp = page.goto(live_server + "/terms")
|
|
assert resp.ok or resp.status in (301, 302)
|
|
|
|
|
|
# =============================================================================
|
|
# H. Markets
|
|
# =============================================================================
|
|
|
|
def test_markets_hub_loads(live_server, page):
|
|
"""Markets hub should load normally."""
|
|
resp = page.goto(live_server + "/en/markets")
|
|
assert resp.ok
|
|
expect(page.locator("h1, h2").first).to_be_visible()
|
|
|
|
|
|
def test_markets_results_partial_loads(live_server, page):
|
|
"""Markets results HTMX partial should return 200."""
|
|
resp = page.goto(live_server + "/en/markets/results")
|
|
assert resp.ok
|
|
|
|
|
|
# =============================================================================
|
|
# I. Tooltip Presence
|
|
# =============================================================================
|
|
|
|
def test_planner_tooltips_present(live_server, page):
|
|
"""Result tabs should contain tooltip spans for complex financial terms."""
|
|
page.goto(live_server + "/en/planner/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
returns_btn = page.locator("button[data-tab='returns'], [data-tab='returns']").first
|
|
if returns_btn.count() > 0:
|
|
returns_btn.click()
|
|
page.wait_for_timeout(600)
|
|
|
|
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()
|