25da436bba
The test suite login as 'admin'/'changeme' but only 'shawn' was seeded.
Adding the admin user with SHA256('changeme') hash lets all 15 Playwright
tests reach the actual app pages instead of failing at login.
191 lines
6.1 KiB
Python
191 lines
6.1 KiB
Python
"""Fixtures for Playwright frontend E2E tests.
|
|
|
|
Architecture:
|
|
- Each test gets an isolated temp SQLite DB.
|
|
- A FastAPI uvicorn server runs on a random port in a background thread.
|
|
- CANTEEN_SKIP_AUTH=1 skips auth middleware so Playwright doesn't need real tokens.
|
|
- Playwright launches system Google Chrome (Ubuntu 26.04 can't install bundled browsers,
|
|
and Chrome 148 SIGTRAPs with certain --disable-features flags; ignore_default_args
|
|
workaround applied).
|
|
- Viewport: iPhone 14 (390x844), Geolocation: Orlando, FL.
|
|
"""
|
|
|
|
# Chrome 148 on Ubuntu 26.04 (kernel 7.0) SIGTRAPs when Playwright's default
|
|
# --disable-features and related flags are passed. Ignoring these defaults
|
|
# allows Chrome to launch cleanly with DevTools protocol.
|
|
CHROME_IGNORE_DEFAULTS = [
|
|
'--disable-field-trial-config',
|
|
'--disable-background-networking',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-breakpad',
|
|
'--disable-client-side-phishing-detection',
|
|
'--disable-default-apps',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-extensions',
|
|
'--disable-hang-monitor',
|
|
'--disable-ipc-flooding-protection',
|
|
'--disable-popup-blocking',
|
|
'--disable-prompt-on-repost',
|
|
'--disable-renderer-backgrounding',
|
|
'--disable-sync',
|
|
'--enable-automation',
|
|
]
|
|
|
|
import importlib
|
|
import os
|
|
import socket
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import uvicorn
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
# Ensure project root is on path
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
# Global env: skip auth for all tests
|
|
os.environ["CANTEEN_SKIP_AUTH"] = "1"
|
|
|
|
# ── device profiles ─────────────────────────────────────────────────────────
|
|
# Set CANTEEN_DEVICE=pixel_9a in env to run tests with Pixel 9a viewport.
|
|
DEVICE_PROFILES = {
|
|
"iphone_14": {
|
|
"viewport": {"width": 390, "height": 844},
|
|
"user_agent": None,
|
|
"device_scale_factor": None,
|
|
},
|
|
"pixel_9a": {
|
|
"viewport": {"width": 412, "height": 892},
|
|
"user_agent": (
|
|
"Mozilla/5.0 (Linux; Android 15; Pixel 9a) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Chrome/130.0.6723.58 Mobile Safari/537.36"
|
|
),
|
|
"device_scale_factor": 2.625,
|
|
},
|
|
}
|
|
|
|
|
|
def get_device_config() -> tuple:
|
|
"""Return device config dict + name based on CANTEEN_DEVICE env var."""
|
|
device = os.environ.get("CANTEEN_DEVICE", "iphone_14").lower().replace("-", "_")
|
|
if device in DEVICE_PROFILES:
|
|
return DEVICE_PROFILES[device], device
|
|
print(f"⚠ Unknown device '{device}', falling back to iphone_14")
|
|
return DEVICE_PROFILES["iphone_14"], "iphone_14"
|
|
|
|
|
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _find_free_port() -> int:
|
|
"""Find an available TCP port."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
# ── fixtures ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def browser():
|
|
"""Launch system Chromium once per test session."""
|
|
pw = sync_playwright().start()
|
|
browser = pw.chromium.launch(
|
|
executable_path="/usr/bin/google-chrome-stable",
|
|
headless=True,
|
|
args=["--no-sandbox", "--disable-gpu"],
|
|
ignore_default_args=CHROME_IGNORE_DEFAULTS,
|
|
)
|
|
yield browser
|
|
browser.close()
|
|
pw.stop()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_db_path():
|
|
"""Create an isolated temp SQLite DB for each test."""
|
|
fd, path = tempfile.mkstemp(suffix=".db", prefix="canteen_frontend_test_")
|
|
os.close(fd)
|
|
os.environ["CANTEEN_DB_PATH"] = path
|
|
yield path
|
|
# Cleanup DB and WAL/SHM/journal files
|
|
for suffix in ("", "-shm", "-wal", "-journal"):
|
|
p = Path(path + suffix)
|
|
if p.exists():
|
|
p.unlink()
|
|
|
|
|
|
@pytest.fixture
|
|
def live_server(test_db_path):
|
|
"""Start FastAPI + uvicorn on a random port in a background thread.
|
|
|
|
Returns the base URL (e.g. 'http://127.0.0.1:12345').
|
|
"""
|
|
port = _find_free_port()
|
|
os.environ["CANTEEN_PORT"] = str(port)
|
|
|
|
# Reload the server module so DB_PATH picks up the current
|
|
# CANTEEN_DB_PATH (module-level constant read at import time).
|
|
import server
|
|
importlib.reload(server)
|
|
app = server.app
|
|
|
|
t = threading.Thread(
|
|
target=uvicorn.run,
|
|
kwargs={
|
|
"app": "server:app",
|
|
"host": "127.0.0.1",
|
|
"port": port,
|
|
"log_level": "error",
|
|
},
|
|
daemon=True,
|
|
)
|
|
t.start()
|
|
|
|
base_url = f"http://127.0.0.1:{port}"
|
|
|
|
# Wait for server to be ready
|
|
deadline = time.time() + 10
|
|
import urllib.request
|
|
|
|
while time.time() < deadline:
|
|
try:
|
|
urllib.request.urlopen(f"{base_url}/", timeout=1)
|
|
break
|
|
except Exception:
|
|
time.sleep(0.1)
|
|
else:
|
|
raise RuntimeError(f"Server did not start on {base_url} within 10s")
|
|
|
|
yield base_url
|
|
# Thread is daemon, will exit when test process ends
|
|
|
|
|
|
@pytest.fixture
|
|
def page(browser, live_server):
|
|
"""Create a Playwright page pointed at the live server.
|
|
|
|
Mobile viewport driven by CANTEEN_DEVICE env var (default: iphone_14).
|
|
Options: iphone_14, pixel_9a
|
|
Orlando FL geolocation, geolocation permission auto-granted.
|
|
"""
|
|
device_config, device_name = get_device_config()
|
|
context = browser.new_context(
|
|
viewport=device_config["viewport"],
|
|
user_agent=device_config["user_agent"],
|
|
device_scale_factor=device_config["device_scale_factor"],
|
|
geolocation={"latitude": 28.3852, "longitude": -81.5639},
|
|
permissions=["geolocation"],
|
|
)
|
|
page = context.new_page()
|
|
page.goto(live_server)
|
|
yield page
|
|
context.close()
|