Files
canteen-asset-tracker/tests/frontend/conftest.py
T
shawn 25da436bba Fix: seed admin/changeme user for frontend E2E tests
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.
2026-05-22 15:08:39 -04:00

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()