258 lines
12 KiB
Python
258 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Playwright E2E Test: Profile Address Persistence (Stairwell / Floor / Door)
|
||
|
||
This test:
|
||
1. Logs in as the configured test user (via LoginModal on the landing page).
|
||
2. Navigates to the Profile page (/profile).
|
||
3. Enters edit mode, fills every address field (city, zip, street, house number,
|
||
stairwell, floor, door), and saves.
|
||
4. Reloads the page and verifies that the stairwell, floor, and door values
|
||
persisted after the page refresh.
|
||
|
||
Requirements:
|
||
- playwright (Python) + dotenv
|
||
- .env.test file at the project root with TEST_USER_EMAIL and TEST_USER_PASS
|
||
|
||
Usage:
|
||
python tests/active/e2e_profile_address.py
|
||
|
||
To see the browser visually, change HEADLESS=False below.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from dotenv import load_dotenv
|
||
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
|
||
|
||
|
||
# ── Configuration ──────────────────────────────────────────────────────────
|
||
BASE_URL = "https://app.servicefinder.hu"
|
||
HEADLESS = True # Set to False for visual debugging
|
||
SLOW_MO = 0 # ms delay between actions (useful for debugging)
|
||
ACTION_TIMEOUT = 15_000 # ms – default timeout for element lookups
|
||
|
||
# ── Load .env.test from project root ──────────────────────────────────────
|
||
ENV_PATH = Path(__file__).resolve().parents[1] / ".env.test"
|
||
if not ENV_PATH.exists():
|
||
raise FileNotFoundError(
|
||
f".env.test not found at {ENV_PATH}. "
|
||
"Create it with TEST_USER_EMAIL and TEST_USER_PASS."
|
||
)
|
||
|
||
load_dotenv(dotenv_path=ENV_PATH, override=True)
|
||
|
||
TEST_EMAIL = os.getenv("TEST_USER_EMAIL")
|
||
TEST_PASS = os.getenv("TEST_USER_PASS")
|
||
|
||
if not TEST_EMAIL or not TEST_PASS:
|
||
raise ValueError(
|
||
"TEST_USER_EMAIL and/or TEST_USER_PASS are missing from .env.test. "
|
||
f"Loaded from: {ENV_PATH}"
|
||
)
|
||
|
||
# ── Test data ──────────────────────────────────────────────────────────────
|
||
ADDRESS_DATA = {
|
||
"address_city": "Dunakeszi",
|
||
"address_zip": "2120",
|
||
"address_street_name": "Határ",
|
||
"address_street_type": "utca",
|
||
"address_house_number": "8",
|
||
"address_stairwell": "A",
|
||
"address_floor": "3",
|
||
"address_door": "5",
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# Helper functions
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
def log(msg: str) -> None:
|
||
"""Simple stdout logger."""
|
||
print(f"[E2E] {msg}")
|
||
|
||
|
||
def fill_input(page, placeholder: str, value: str) -> None:
|
||
"""Locate an <input> by its placeholder and fill it."""
|
||
locator = page.locator(f'input[placeholder="{placeholder}"]')
|
||
locator.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||
locator.fill(value)
|
||
log(f" Filled '{placeholder}' → '{value}'")
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# Main test flow
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
def main() -> None:
|
||
with sync_playwright() as pw:
|
||
# ── 1. Launch browser (local or remote via BROWSERLESS_URL) ──
|
||
browserless_url = os.getenv("BROWSERLESS_URL")
|
||
if browserless_url:
|
||
# Connect to a remote Browserless / Chrome container
|
||
browser = pw.chromium.connect_over_cdp(browserless_url)
|
||
log(f"Connected to remote browser at {browserless_url}")
|
||
else:
|
||
# Local fallback
|
||
browser = pw.chromium.launch(
|
||
headless=os.getenv("HEADLESS", "True").lower() == "true",
|
||
slow_mo=SLOW_MO,
|
||
)
|
||
log(f"Browser launched locally (headless={os.getenv('HEADLESS', 'True')})")
|
||
context = browser.new_context(
|
||
viewport={"width": 1280, "height": 800},
|
||
locale="hu-HU",
|
||
)
|
||
page = context.new_page()
|
||
log(f"Browser launched (headless={HEADLESS})")
|
||
|
||
# ── 2. Open landing page & login ─────────────────────────────
|
||
log("Navigating to landing page …")
|
||
page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
|
||
|
||
# Click the "Garázs Nyitása" button to open the LoginModal
|
||
login_trigger = page.locator("button:has-text('Garázs Nyitása')")
|
||
login_trigger.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||
login_trigger.click()
|
||
log("Login modal opened")
|
||
|
||
# Fill email & password
|
||
page.fill("#login-email", TEST_EMAIL)
|
||
page.fill("#login-password", TEST_PASS)
|
||
log("Login credentials entered")
|
||
|
||
# Click the submit button (the first .btn-premium inside the visible form)
|
||
submit_btn = page.locator("form button[type='submit']").first
|
||
submit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||
submit_btn.click()
|
||
log("Login submitted – waiting for navigation …")
|
||
|
||
# Wait for redirect away from landing page (to /dashboard or /complete-kyc)
|
||
try:
|
||
page.wait_for_url(
|
||
lambda url: "/dashboard" in url or "/complete-kyc" in url,
|
||
timeout=15_000,
|
||
)
|
||
except PwTimeout:
|
||
# If still on landing, maybe login failed – dump state
|
||
log(f"URL after login attempt: {page.url}")
|
||
page.screenshot(path="/tmp/e2e_login_fail.png")
|
||
raise AssertionError(
|
||
"Login failed – did not redirect to dashboard. "
|
||
"Check credentials or UI state."
|
||
)
|
||
|
||
log(f"Login successful – current URL: {page.url}")
|
||
|
||
# ── 3. Navigate to Profile page ──────────────────────────────
|
||
log("Navigating to /profile …")
|
||
page.goto(f"{BASE_URL}/profile", wait_until="networkidle", timeout=20_000)
|
||
|
||
# Wait for the profile page to finish loading (spinner disappears)
|
||
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
|
||
# Also wait until the loading spinner is gone
|
||
page.wait_for_function(
|
||
"() => !document.querySelector('.animate-spin')",
|
||
timeout=15_000,
|
||
)
|
||
log("Profile page loaded")
|
||
|
||
# ── 4. Enter edit mode ───────────────────────────────────────
|
||
edit_btn = page.locator("button:has-text('Szerkesztés')")
|
||
edit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||
edit_btn.click()
|
||
log("Edit mode activated")
|
||
|
||
# ── 5. Fill in all address fields ────────────────────────────
|
||
log("Filling address form …")
|
||
fill_input(page, "Irányítószám", ADDRESS_DATA["address_zip"])
|
||
fill_input(page, "Város", ADDRESS_DATA["address_city"])
|
||
fill_input(page, "Utca", ADDRESS_DATA["address_street_name"])
|
||
fill_input(page, "Közterület jellege", ADDRESS_DATA["address_street_type"])
|
||
fill_input(page, "Házszám", ADDRESS_DATA["address_house_number"])
|
||
fill_input(page, "Lépcsőház", ADDRESS_DATA["address_stairwell"])
|
||
fill_input(page, "Emelet", ADDRESS_DATA["address_floor"])
|
||
fill_input(page, "Ajtó", ADDRESS_DATA["address_door"])
|
||
|
||
# ── 6. Save ──────────────────────────────────────────────────
|
||
save_btn = page.locator("button:has-text('Mentés')")
|
||
save_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||
save_btn.click()
|
||
log("Save clicked – waiting for confirmation …")
|
||
|
||
# Wait for the success message to appear
|
||
success_locator = page.locator("text=A profil adatok és okmányok sikeresen frissítve!")
|
||
try:
|
||
success_locator.wait_for(state="visible", timeout=10_000)
|
||
log("Save succeeded – success message visible")
|
||
except PwTimeout:
|
||
# Check for error message
|
||
error_locator = page.locator(".text-red-400")
|
||
if error_locator.is_visible():
|
||
error_text = error_locator.text_content()
|
||
log(f"Save FAILED with error: {error_text}")
|
||
page.screenshot(path="/tmp/e2e_save_fail.png")
|
||
raise AssertionError(f"Save failed: {error_text}")
|
||
|
||
page.screenshot(path="/tmp/e2e_save_timeout.png")
|
||
raise AssertionError(
|
||
"Save timeout – neither success nor error message appeared."
|
||
)
|
||
|
||
# ── 7. Reload and verify persistence ─────────────────────────
|
||
log("Reloading page to verify persistence …")
|
||
page.reload(wait_until="networkidle")
|
||
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
|
||
page.wait_for_function(
|
||
"() => !document.querySelector('.animate-spin')",
|
||
timeout=15_000,
|
||
)
|
||
log("Page reloaded")
|
||
|
||
# Read the display text in the read-only address section
|
||
# The read-only view shows: Lépcsőház: …, Emelet: …, Ajtó: …
|
||
page_body = page.locator("body").inner_text()
|
||
log("── Read-only address block ──")
|
||
|
||
# Use a regex to extract values
|
||
stairwell_match = re.search(r"Lépcsőház:\s*(\S+)", page_body)
|
||
floor_match = re.search(r"Emelet:\s*(\S+)", page_body)
|
||
door_match = re.search(r"Ajtó:\s*(\S+)", page_body)
|
||
|
||
stairwell_val = stairwell_match.group(1) if stairwell_match else ""
|
||
floor_val = floor_match.group(1) if floor_match else ""
|
||
door_val = door_match.group(1) if door_match else ""
|
||
|
||
log(f" Stairwell (Lépcsőház): '{stairwell_val}'")
|
||
log(f" Floor (Emelet): '{floor_val}'")
|
||
log(f" Door (Ajtó): '{door_val}'")
|
||
|
||
# ── 8. Assertion: all three values must be present ───────────
|
||
errors = []
|
||
if not stairwell_val or stairwell_val in ("—", "null", "None"):
|
||
errors.append(f"stairwell (expected '{ADDRESS_DATA['address_stairwell']}', got '{stairwell_val}')")
|
||
if not floor_val or floor_val in ("—", "null", "None"):
|
||
errors.append(f"floor (expected '{ADDRESS_DATA['address_floor']}', got '{floor_val}')")
|
||
if not door_val or door_val in ("—", "null", "None"):
|
||
errors.append(f"door (expected '{ADDRESS_DATA['address_door']}', got '{door_val}')")
|
||
|
||
if errors:
|
||
page.screenshot(path="/tmp/e2e_persistence_fail.png")
|
||
raise AssertionError(
|
||
"Hibás backend mentés! Az emelet/ajtó adatok elvesztek az oldal "
|
||
f"frissítése után.\n Missing/empty fields: {', '.join(errors)}"
|
||
)
|
||
|
||
log("Sikeres E2E teszt: Az adatok megmaradtak a backend frissítés után is!")
|
||
log("ALL CHECKS PASSED.\n")
|
||
|
||
browser.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|