311 lines
18 KiB
Python
311 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import expect, sync_playwright
|
|
|
|
BASE_URL = "http://127.0.0.1:8000"
|
|
ARTIFACTS = Path("tests/artifacts")
|
|
ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def assert_no_horizontal_overflow(page) -> None:
|
|
scroll_width = page.evaluate("document.documentElement.scrollWidth")
|
|
viewport_width = page.evaluate("window.innerWidth")
|
|
assert scroll_width <= viewport_width + 1
|
|
|
|
|
|
# UX-001 expects an approximately 1/3 and 2/3 desktop split without horizontal overflow.
|
|
def assert_two_pane_layout(page, left_testid: str, right_testid: str) -> None:
|
|
left = page.locator(f'[data-testid="{left_testid}"]:visible').first
|
|
right = page.locator(f'[data-testid="{right_testid}"]:visible').first
|
|
expect(left).to_be_visible(timeout=15000)
|
|
expect(right).to_be_visible(timeout=15000)
|
|
left_box = left.bounding_box()
|
|
right_box = right.bounding_box()
|
|
assert left_box is not None
|
|
assert right_box is not None
|
|
assert left_box["x"] < right_box["x"]
|
|
assert abs(left_box["y"] - right_box["y"]) < 120
|
|
width_ratio = right_box["width"] / left_box["width"]
|
|
assert 1.4 <= width_ratio <= 2.6
|
|
assert_no_horizontal_overflow(page)
|
|
|
|
|
|
def assert_stacked_pane_layout(page, left_testid: str, right_testid: str) -> None:
|
|
left = page.locator(f'[data-testid="{left_testid}"]:visible').first
|
|
right = page.locator(f'[data-testid="{right_testid}"]:visible').first
|
|
expect(left).to_be_visible(timeout=15000)
|
|
expect(right).to_be_visible(timeout=15000)
|
|
left_box = left.bounding_box()
|
|
right_box = right.bounding_box()
|
|
assert left_box is not None
|
|
assert right_box is not None
|
|
assert abs(left_box["x"] - right_box["x"]) < 40
|
|
assert right_box["y"] > left_box["y"] + left_box["height"] * 0.5
|
|
assert_no_horizontal_overflow(page)
|
|
|
|
|
|
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="domcontentloaded", timeout=30000)
|
|
expect(page).to_have_title("NiceGUI")
|
|
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"{BASE_URL}/*", timeout=15000)
|
|
workspace_url = page.url
|
|
workspace_id = workspace_url.removeprefix(f"{BASE_URL}/")
|
|
assert workspace_id
|
|
workspace_cookie = None
|
|
for _ in range(5):
|
|
cookies = page.context.cookies()
|
|
workspace_cookie = next((cookie for cookie in cookies if cookie["name"] == "workspace_id"), None)
|
|
if workspace_cookie is not None:
|
|
break
|
|
page.wait_for_timeout(200)
|
|
assert workspace_cookie is not None
|
|
assert workspace_cookie["value"] == workspace_id
|
|
expect(page.locator("text=Vault Dashboard").first).to_be_visible(timeout=10000)
|
|
expect(page.locator("text=Overview").first).to_be_visible(timeout=10000)
|
|
expect(page.locator("text=Live quote source:").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Alert Status").first).to_be_visible(timeout=15000)
|
|
assert_two_pane_layout(page, "overview-left-pane", "overview-right-pane")
|
|
page.screenshot(path=str(ARTIFACTS / "overview.png"), full_page=True)
|
|
|
|
page.set_viewport_size({"width": 900, "height": 1000})
|
|
page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000)
|
|
assert_stacked_pane_layout(page, "overview-left-pane", "overview-right-pane")
|
|
page.set_viewport_size({"width": 1440, "height": 1000})
|
|
|
|
page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
|
|
page.wait_for_url(workspace_url, timeout=15000)
|
|
expect(page.locator("text=Alert Status").first).to_be_visible(timeout=15000)
|
|
|
|
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
|
|
expect(page.locator("text=Backtests").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Scenario Form").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Scenario Summary").first).to_be_visible(timeout=15000)
|
|
assert_two_pane_layout(page, "backtests-left-pane", "backtests-right-pane")
|
|
backtests_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Historical scenario starts undercollateralized:" in backtests_text
|
|
assert "loan_amount must be less than initial collateral value" not in backtests_text
|
|
assert "RuntimeError" not in backtests_text
|
|
assert "Server error" not in backtests_text
|
|
assert "Traceback" not in backtests_text
|
|
page.screenshot(path=str(ARTIFACTS / "backtests.png"), full_page=True)
|
|
|
|
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
|
|
expect(page.locator("text=Event Comparison").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Comparison Form").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Scenario Summary").first).to_be_visible(timeout=15000)
|
|
assert_two_pane_layout(page, "event-comparison-left-pane", "event-comparison-right-pane")
|
|
event_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "GLD January 2024 Selloff" in event_text
|
|
assert "Protective Put ATM" in event_text
|
|
assert "Historical scenario starts undercollateralized:" in event_text
|
|
assert "loan_amount must be less than initial collateral value" not in event_text
|
|
assert "RuntimeError" not in event_text
|
|
assert "Server error" not in event_text
|
|
assert "Traceback" not in event_text
|
|
page.screenshot(path=str(ARTIFACTS / "event-comparison.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)
|
|
assert_two_pane_layout(page, "options-left-pane", "options-right-pane")
|
|
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)
|
|
|
|
page.goto(f"{workspace_url}/settings", wait_until="domcontentloaded", timeout=30000)
|
|
assert page.url.endswith("/settings")
|
|
expect(page.locator("text=Settings").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Collateral entry basis").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Entry price ($/oz)").first).to_be_visible(timeout=15000)
|
|
assert_two_pane_layout(page, "settings-left-pane", "settings-right-pane")
|
|
|
|
page.get_by_label("Collateral entry basis").click()
|
|
page.get_by_text("Gold weight + entry price", exact=True).click()
|
|
page.get_by_label("Entry price ($/oz)").fill("4400")
|
|
page.get_by_label("Gold weight (oz)").fill("220")
|
|
page.get_by_label("Loan amount ($)").fill("222000")
|
|
page.get_by_label("Margin call LTV threshold").fill("0.8")
|
|
budget_input = page.get_by_label("Monthly hedge budget ($)")
|
|
budget_input.fill("12345")
|
|
page.get_by_role("button", name="Save settings").click()
|
|
expect(page.locator("text=Settings saved successfully").first).to_be_visible(timeout=15000)
|
|
page.reload(wait_until="domcontentloaded", timeout=30000)
|
|
expect(page.get_by_label("Monthly hedge budget ($)")).to_have_value("12345")
|
|
expect(page.get_by_label("Entry price ($/oz)")).to_have_value("4400")
|
|
expect(page.get_by_label("Loan amount ($)")).to_have_value("222000")
|
|
expect(page.get_by_label("Margin call LTV threshold")).to_have_value("0.8")
|
|
settings_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "RuntimeError" not in settings_text
|
|
assert "Server error" not in settings_text
|
|
page.screenshot(path=str(ARTIFACTS / "settings.png"), full_page=True)
|
|
|
|
page.goto(f"{workspace_url}/backtests", wait_until="networkidle", timeout=30000)
|
|
expect(page.get_by_label("Underlying units")).to_have_value("2200")
|
|
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
|
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
|
assert_two_pane_layout(page, "backtests-left-pane", "backtests-right-pane")
|
|
backtests_workspace_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Scenario Summary" in backtests_workspace_text
|
|
assert "Scenario validation failed" in backtests_workspace_text
|
|
assert "$220,000" in backtests_workspace_text
|
|
assert "Historical scenario starts undercollateralized:" in backtests_workspace_text
|
|
|
|
page.get_by_label("Underlying units").fill("3000")
|
|
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
|
page.get_by_label("Template").click()
|
|
page.get_by_text("Protective Put 95%", exact=True).click()
|
|
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
|
page.get_by_role("button", name="Run backtest").click()
|
|
expect(page.locator("text=Daily Results").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Scenario Results").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Template: Protective Put 95%").first).to_be_visible(timeout=15000)
|
|
rerun_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Margin call days hedged" in rerun_text
|
|
assert "Historical scenario starts undercollateralized:" not in rerun_text
|
|
assert "RuntimeError" not in rerun_text
|
|
assert "Server error" not in rerun_text
|
|
|
|
page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000)
|
|
expect(page.get_by_label("Underlying units")).to_have_value("2200")
|
|
expect(page.get_by_label("Loan amount")).to_have_value("222000")
|
|
expect(page.get_by_label("Margin call LTV")).to_have_value("0.8")
|
|
assert_two_pane_layout(page, "event-comparison-left-pane", "event-comparison-right-pane")
|
|
event_workspace_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "$222,000" in event_workspace_text
|
|
assert "2,200" in event_workspace_text
|
|
assert "80.0%" in event_workspace_text
|
|
assert "Scenario validation failed" in event_workspace_text
|
|
assert "Historical scenario starts undercollateralized:" in event_workspace_text
|
|
|
|
page.get_by_label("Underlying units").fill("3000")
|
|
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
|
event_before_run_text = page.locator("body").inner_text(timeout=15000)
|
|
assert (
|
|
"Protective Put ATM, Protective Put 95%, Protective Put 90%, Laddered Puts 50/50 ATM + 95%"
|
|
in event_before_run_text
|
|
)
|
|
page.get_by_role("button", name="Run comparison").click()
|
|
expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Scenario Results").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Strategy Drilldown").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Portfolio Value Paths").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Selected strategy: Protective Put ATM").first).to_be_visible(timeout=15000)
|
|
rerun_event_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Baseline series shows the unhedged collateral value path" in rerun_event_text
|
|
assert "Templates compared" in rerun_event_text and "4" in rerun_event_text
|
|
assert "Worst LTV point" in rerun_event_text
|
|
assert "Margin threshold breach dates" in rerun_event_text
|
|
assert "Daily path details" in rerun_event_text
|
|
assert "Historical scenario starts undercollateralized:" not in rerun_event_text
|
|
|
|
page.get_by_label("Strategy drilldown").click()
|
|
page.get_by_text("#4 — Protective Put 90%", exact=True).click()
|
|
expect(page.locator("text=Selected strategy: Protective Put 90%").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Rank #4 · Breached margin threshold").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=2024-01-08 · 82.6%").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=$17,939").first).to_be_visible(timeout=15000)
|
|
|
|
page.get_by_label("Event preset").click()
|
|
page.get_by_text("GLD January 2024 Drawdown", exact=True).click()
|
|
expect(page.locator("text=Results out of date").first).to_be_visible(timeout=15000)
|
|
page.get_by_role("button", name="Run comparison").click()
|
|
expect(page.locator("text=Ranked Results").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Portfolio Value Paths").first).to_be_visible(timeout=15000)
|
|
rerun_event_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "GLD January 2024 Drawdown" in rerun_event_text
|
|
assert "Laddered Puts 33/33/33 ATM + 95% + 90%" in rerun_event_text
|
|
assert "Templates compared" in rerun_event_text and "3" in rerun_event_text
|
|
assert "RuntimeError" not in rerun_event_text
|
|
assert "Server error" not in rerun_event_text
|
|
|
|
page.goto(workspace_url, wait_until="domcontentloaded", timeout=30000)
|
|
overview_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Hedge Analysis" in overview_text
|
|
assert "Options Chain" in overview_text
|
|
assert "Backtests" in overview_text
|
|
assert "Event Comparison" in overview_text
|
|
assert "Live quote source:" in overview_text
|
|
assert "GLD share quote converted to ozt-equivalent spot" in overview_text
|
|
assert "configured entry price fallback" not in overview_text
|
|
assert "Collateral Spot Price" in overview_text
|
|
assert "$1,261.36" in overview_text
|
|
assert "$968,000" in overview_text
|
|
assert "$222,000" in overview_text
|
|
expect(page.get_by_role("link", name="Hedge Analysis")).to_have_attribute("href", f"/{workspace_id}/hedge")
|
|
expect(page.get_by_role("link", name="Backtests")).to_have_attribute("href", f"/{workspace_id}/backtests")
|
|
expect(page.get_by_role("link", name="Event Comparison")).to_have_attribute(
|
|
"href", f"/{workspace_id}/event-comparison"
|
|
)
|
|
|
|
second_context = browser.new_context(viewport={"width": 1440, "height": 1000})
|
|
second_page = second_context.new_page()
|
|
second_page.goto(BASE_URL, wait_until="domcontentloaded", timeout=30000)
|
|
expect(second_page.locator("text=Create a private workspace URL").first).to_be_visible(timeout=10000)
|
|
second_page.get_by_role("button", name="Get started").click()
|
|
second_page.wait_for_url(f"{BASE_URL}/*", timeout=15000)
|
|
second_workspace_url = second_page.url
|
|
assert second_workspace_url != workspace_url
|
|
second_page.goto(f"{second_workspace_url}/settings", wait_until="domcontentloaded", timeout=30000)
|
|
settings_loaded = False
|
|
for _ in range(3):
|
|
try:
|
|
expect(second_page).to_have_url(f"{second_workspace_url}/settings")
|
|
expect(second_page.locator("text=Settings").first).to_be_visible(timeout=15000)
|
|
expect(second_page.get_by_label("Monthly hedge budget ($)")).to_have_value("8000", timeout=15000)
|
|
settings_loaded = True
|
|
break
|
|
except AssertionError:
|
|
second_page.reload(wait_until="domcontentloaded", timeout=30000)
|
|
assert settings_loaded
|
|
second_page.close()
|
|
second_context.close()
|
|
|
|
page.goto(f"{workspace_url}/hedge", wait_until="domcontentloaded", timeout=30000)
|
|
expect(page.locator("text=Hedge Analysis").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Strategy selector").first).to_be_visible(timeout=15000)
|
|
expect(page.locator("text=Strategy Controls").first).to_be_visible(timeout=15000)
|
|
assert_two_pane_layout(page, "hedge-left-pane", "hedge-right-pane")
|
|
hedge_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Scenario Summary" in hedge_text
|
|
assert "RuntimeError" not in hedge_text
|
|
assert "Server error" not in hedge_text
|
|
|
|
slider = page.locator(".q-slider").first
|
|
slider_box = slider.bounding_box()
|
|
assert slider_box is not None
|
|
page.mouse.click(slider_box["x"] + slider_box["width"] * 0.1, slider_box["y"] + slider_box["height"] / 2)
|
|
expect(page.locator("text=Scenario move: -20%").first).to_be_visible(timeout=15000)
|
|
hedge_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "Start value" in hedge_text
|
|
assert "Start price" in hedge_text
|
|
assert "Weight" in hedge_text
|
|
assert "Loan amount" in hedge_text
|
|
assert "Monthly hedge budget" in hedge_text
|
|
assert "$4,400.00/oz" not in hedge_text
|
|
assert "220 oz" in hedge_text
|
|
assert "$222,000" in hedge_text
|
|
assert "80.0%" in hedge_text
|
|
assert "$12,345" in hedge_text
|
|
assert "converted collateral spot" in hedge_text
|
|
assert "Start value" in hedge_text
|
|
assert "Start price" in hedge_text
|
|
assert "Scenario spot" in hedge_text
|
|
assert "Unhedged equity" in hedge_text
|
|
assert "Hedged equity" in hedge_text
|
|
page.screenshot(path=str(ARTIFACTS / "hedge.png"), full_page=True)
|
|
|
|
page.goto(f"{workspace_url}/hedge", wait_until="domcontentloaded", timeout=30000)
|
|
hedge_spot_text = page.locator("body").inner_text(timeout=15000)
|
|
assert "converted collateral spot" in hedge_spot_text or "configured entry price" in hedge_spot_text
|
|
|
|
browser.close()
|