refactor: move Playwright tests to tests/e2e/ with proper conftest

- Move conftest_playwright.py to tests/e2e/conftest.py for proper pytest discovery
- Move test_playwright_server.py to tests/e2e/
- Server fixture starts FastAPI with uvicorn for isolated E2E testing
This commit is contained in:
Bu5hm4nn
2026-04-04 18:30:40 +02:00
parent d835544e58
commit 2de5966a4e
2 changed files with 0 additions and 4 deletions

125
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,125 @@
"""Pytest configuration for Playwright tests.
This conftest creates module-scoped fixtures that start the FastAPI server
before running Playwright tests and stop it after all tests complete.
"""
from __future__ import annotations
import logging
import os
import socket
import sys
import threading
import time
from collections.abc import Generator
from pathlib import Path
import pytest
import uvicorn
# Suppress NiceGUI banner noise
logging.getLogger("nicegui").setLevel(logging.WARNING)
def find_free_port() -> int:
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
s.listen(1)
port = s.getsockname()[1]
return port
class ServerManager:
"""Manages a NiceGUI/FastAPI server for testing."""
_instance: "ServerManager | None" = None
_lock = threading.Lock()
def __init__(self, port: int) -> None:
self.port = port
self.url = f"http://localhost:{port}"
self._thread: threading.Thread | None = None
self._server: uvicorn.Server | None = None
def start(self) -> None:
"""Start the server in a background thread."""
self._thread = threading.Thread(target=self._run_server, daemon=True)
self._thread.start()
# Wait for server to be ready
if not self._wait_for_connection(timeout=30):
raise RuntimeError("Server did not start within 30 seconds")
def _run_server(self) -> None:
"""Run the FastAPI server with uvicorn."""
# Ensure project root is on sys.path
project_root = str(Path(__file__).parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
os.environ["APP_ENV"] = "test"
os.environ["NICEGUI_STORAGE_SECRET"] = "test-secret-key"
# Import after environment is set
from app.main import app
# Configure uvicorn
config = uvicorn.Config(
app,
host="127.0.0.1",
port=self.port,
log_level="warning",
access_log=False,
)
self._server = uvicorn.Server(config)
self._server.run()
def _wait_for_connection(self, timeout: float = 10.0) -> bool:
"""Wait for server to accept connections."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", self.port), timeout=1):
return True
except OSError:
time.sleep(0.1)
return False
def stop(self) -> None:
"""Stop the server."""
if self._server:
self._server.should_exit = True
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5)
@classmethod
def get_or_create(cls, port: int) -> "ServerManager":
"""Get existing instance or create new one."""
with cls._lock:
if cls._instance is None:
cls._instance = cls(port)
return cls._instance
@pytest.fixture(scope="module")
def server_url() -> Generator[str, None, None]:
"""Start the server once per module and yield its URL."""
port = find_free_port()
server = ServerManager(port)
# Start server
server.start()
yield server.url
# Cleanup
server.stop()
ServerManager._instance = None
@pytest.fixture(scope="module")
def base_url(server_url: str) -> str:
"""Alias for server_url for naming consistency with Playwright conventions."""
return server_url

View File

@@ -0,0 +1,129 @@
"""Playwright tests for end-to-end validation of the vault dashboard.
This test file verifies that:
1. The application starts and serves pages correctly
2. Workspace creation and navigation work
3. Backtest page loads and executes scenarios
4. The full user flow works without runtime errors
"""
from __future__ import annotations
from pathlib import Path
from playwright.sync_api import expect
ARTIFACTS = Path("tests/artifacts")
ARTIFACTS.mkdir(parents=True, exist_ok=True)
def assert_no_horizontal_overflow(page) -> None:
"""Verify the page doesn't have horizontal scrollbar."""
scroll_width = page.evaluate("document.documentElement.scrollWidth")
viewport_width = page.evaluate("window.innerWidth")
assert scroll_width <= viewport_width + 1
def test_server_starts_and_serves_homepage(server_url: str) -> None:
"""Verify the server starts and serves the homepage."""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 1000})
page.goto(server_url, wait_until="domcontentloaded", timeout=30000)
expect(page).to_have_title("NiceGUI")
expect(page.locator("text=Create a private workspace URL").first).to_be_visible(timeout=10000)
browser.close()
def test_workspace_creation_and_navigation(server_url: str) -> None:
"""Test creating a workspace and navigating between pages."""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 1000})
# Create workspace
page.goto(server_url, wait_until="domcontentloaded", timeout=30000)
expect(page.locator("text=Create a private workspace URL").first).to_be_visible(timeout=10000)
page.get_by_role("button", name="Get started").click()
page.wait_for_url(f"{server_url}/*", timeout=15000)
workspace_url = page.url
workspace_id = workspace_url.removeprefix(f"{server_url}/")
assert workspace_id, "Should have workspace ID in URL"
# Verify overview page loads
expect(page.locator("text=Vault Dashboard").first).to_be_visible(timeout=10000)
expect(page.locator("text=Overview").first).to_be_visible(timeout=10000)
browser.close()
def test_backtest_with_synthetic_data(server_url: str) -> None:
"""E2E test: Backtest scenario execution with synthetic fixture data.
This test verifies:
1. Synthetic data source can be selected
2. Fixture-supported dates (2024-01-02 to 2024-01-08) work
3. Run button triggers execution and results display
"""
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1440, "height": 1000})
# Create workspace
page.goto(server_url, wait_until="domcontentloaded", timeout=30000)
expect(page.locator("text=Create a private workspace URL").first).to_be_visible(timeout=10000)
page.get_by_role("button", name="Get started").click()
page.wait_for_url(f"{server_url}/*", timeout=15000)
workspace_url = page.url
# Navigate to backtests
page.goto(f"{workspace_url}/backtests", wait_until="domcontentloaded", timeout=30000)
expect(page.locator("text=Scenario Configuration")).to_be_visible(timeout=10000)
# Select Synthetic data source
data_source_select = page.locator("[data-testid=data-source-select]")
expect(data_source_select).to_be_visible(timeout=5000)
data_source_select.click()
page.get_by_text("Synthetic", exact=True).click()
page.wait_for_timeout(500)
# Fill in fixture-supported dates
start_date_input = page.get_by_label("Start date")
end_date_input = page.get_by_label("End date")
start_date_input.fill("2024-01-02")
end_date_input.fill("2024-01-08")
start_date_input.press("Tab")
page.wait_for_timeout(1000)
# Fill scenario parameters
page.get_by_label("Underlying units").fill("1000")
page.get_by_label("Loan amount").fill("68000")
page.get_by_label("Margin call LTV").fill("0.75")
# Run backtest
run_button = page.get_by_role("button", name="Run backtest")
expect(run_button).to_be_enabled(timeout=5000)
run_button.click()
# Wait for results
scenario_results = page.locator("text=Scenario Results")
expect(scenario_results).to_be_visible(timeout=30000)
# Verify results
result_text = page.locator("body").inner_text(timeout=10000)
assert "Start value" in result_text
assert "RuntimeError" not in result_text
assert "Traceback" not in result_text
page.screenshot(path=str(ARTIFACTS / "backtest_synthetic_results.png"), full_page=True)
browser.close()