test: add Playwright browser tests and document test loop
- add real browser test for overview and options pages - document engineering learnings in AGENTS.md - commit NiceGUI header layout fix - limit options initial expirations for faster first render
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ config/secrets.yaml
|
||||
data/cache/
|
||||
.idea/
|
||||
.vscode/
|
||||
.worktrees/
|
||||
tests/artifacts/
|
||||
secrets/
|
||||
|
||||
52
AGENTS.md
Normal file
52
AGENTS.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Engineering Rules
|
||||
|
||||
1. **Always close the test loop.**
|
||||
- Do not stop after code changes.
|
||||
- Run the app locally.
|
||||
- Run real tests against the running app.
|
||||
- For UI work, prefer real browser tests (Playwright) over assumptions.
|
||||
- Verify the exact route/page that changed.
|
||||
|
||||
2. **Local Docker first, deploy later.**
|
||||
- Use local Docker/OrbStack for fast feedback.
|
||||
- Only deploy after local behavior is verified.
|
||||
|
||||
3. **Browser-visible behavior beats log-only confidence.**
|
||||
- A route returning HTML is not sufficient.
|
||||
- Confirm page content renders and no runtime/500 error is visible.
|
||||
|
||||
4. **When using parallel worktrees, preserve shared domain models.**
|
||||
- New feature branches must not silently remove compatibility types used elsewhere.
|
||||
- In this project, `LombardPortfolio` is still required by strategy/core modules.
|
||||
|
||||
5. **Do not claim a feature is live unless the UI is wired to it.**
|
||||
- Backend service existence is not enough.
|
||||
- The rendered page must actually consume the live data path.
|
||||
|
||||
## Project Learnings
|
||||
|
||||
### NiceGUI layout constraint
|
||||
- `ui.header()` must be a top-level page layout element.
|
||||
- Do not nest `ui.header()` inside `ui.column()` or other containers.
|
||||
|
||||
### Options page performance lesson
|
||||
- Loading all expiries/chains before first paint can make the page appear broken.
|
||||
- Prefer rendering fast, then loading data incrementally.
|
||||
|
||||
### FastAPI/NiceGUI integration lesson
|
||||
- NiceGUI page handlers should not assume `request.app.state.*` is always the right access path for shared services.
|
||||
- Use an explicit runtime/service registry if needed.
|
||||
|
||||
### Docker development lesson
|
||||
- Avoid mounting the whole repo over `/app` when the image contains required runtime scripts.
|
||||
- Mount narrower paths in dev (`./app`, `./config`) to preserve image-managed files like entrypoints.
|
||||
|
||||
## Validation Checklist for UI Changes
|
||||
- [ ] Local Docker stack starts cleanly
|
||||
- [ ] `/health` returns OK
|
||||
- [ ] Changed page opens in browser automation
|
||||
- [ ] No visible 500/runtime error
|
||||
- [ ] Screenshot artifact captured
|
||||
- [ ] Relevant logs checked
|
||||
@@ -183,7 +183,7 @@ def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]:
|
||||
def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.column]:
|
||||
ui.colors(primary="#0f172a", secondary="#1e293b", accent="#0ea5e9")
|
||||
|
||||
with ui.column().classes("mx-auto w-full max-w-7xl gap-6 bg-slate-50 p-6 dark:bg-slate-950") as container:
|
||||
# Header must be at page level, not inside container
|
||||
with ui.header(elevated=False).classes(
|
||||
"items-center justify-between border-b border-slate-200 bg-white/90 px-6 py-4 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90"
|
||||
):
|
||||
@@ -202,6 +202,7 @@ def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.colum
|
||||
)
|
||||
ui.link(label, href).classes(link_classes)
|
||||
|
||||
with ui.column().classes("mx-auto w-full max-w-7xl gap-6 bg-slate-50 p-6 dark:bg-slate-950") as container:
|
||||
with ui.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||
with ui.column().classes("gap-1"):
|
||||
ui.label(title).classes("text-3xl font-bold text-slate-900 dark:text-slate-50")
|
||||
|
||||
@@ -84,6 +84,9 @@ class DataService:
|
||||
await self.cache.set_json(cache_key, options_chain)
|
||||
return options_chain
|
||||
|
||||
# Limit initial load to the nearest expirations so the page can render quickly.
|
||||
expirations = expirations[:3]
|
||||
|
||||
calls: list[dict[str, Any]] = []
|
||||
puts: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ black>=24.0.0
|
||||
ruff>=0.2.0
|
||||
mypy>=1.8.0
|
||||
httpx>=0.26.0
|
||||
playwright>=1.55.0
|
||||
|
||||
31
tests/test_e2e_playwright.py
Normal file
31
tests/test_e2e_playwright.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import Page, expect, sync_playwright
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
ARTIFACTS = Path("tests/artifacts")
|
||||
ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def test_homepage_and_options_page_render() -> None:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 1000})
|
||||
|
||||
page.goto(BASE_URL, wait_until="networkidle", timeout=30000)
|
||||
expect(page).to_have_title("NiceGUI")
|
||||
expect(page.locator("text=Vault Dashboard").first).to_be_visible(timeout=10000)
|
||||
expect(page.locator("text=Overview").first).to_be_visible(timeout=10000)
|
||||
page.screenshot(path=str(ARTIFACTS / "overview.png"), full_page=True)
|
||||
|
||||
page.goto(f"{BASE_URL}/options", wait_until="domcontentloaded", timeout=30000)
|
||||
expect(page.locator("text=Options Chain").first).to_be_visible(timeout=15000)
|
||||
expect(page.locator("text=Filters").first).to_be_visible(timeout=15000)
|
||||
body_text = page.locator("body").inner_text(timeout=15000)
|
||||
assert "Server error" not in body_text
|
||||
assert "RuntimeError" not in body_text
|
||||
page.screenshot(path=str(ARTIFACTS / "options.png"), full_page=True)
|
||||
|
||||
browser.close()
|
||||
Reference in New Issue
Block a user