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/
|
data/cache/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.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]:
|
def dashboard_page(title: str, subtitle: str, current: str) -> Iterator[ui.column]:
|
||||||
ui.colors(primary="#0f172a", secondary="#1e293b", accent="#0ea5e9")
|
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(
|
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"
|
"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)
|
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.row().classes("w-full items-end justify-between gap-4 max-md:flex-col max-md:items-start"):
|
||||||
with ui.column().classes("gap-1"):
|
with ui.column().classes("gap-1"):
|
||||||
ui.label(title).classes("text-3xl font-bold text-slate-900 dark:text-slate-50")
|
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)
|
await self.cache.set_json(cache_key, options_chain)
|
||||||
return 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]] = []
|
calls: list[dict[str, Any]] = []
|
||||||
puts: list[dict[str, Any]] = []
|
puts: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ black>=24.0.0
|
|||||||
ruff>=0.2.0
|
ruff>=0.2.0
|
||||||
mypy>=1.8.0
|
mypy>=1.8.0
|
||||||
httpx>=0.26.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