from __future__ import annotations from pathlib import Path from playwright.sync_api import expect, sync_playwright BASE_URL = "http://127.0.0.1:8100" 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 Configuration").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) # With dynamic default dates, fixture source may show validation error # The page should render without runtime errors 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) # Verify page structure renders without errors expect(page.locator("text=Scenario Configuration").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_workspace_text = page.locator("body").inner_text(timeout=15000) assert "Scenario Summary" in backtests_workspace_text # Page should render without runtime errors assert "RuntimeError" not in backtests_workspace_text assert "Server error" not in backtests_workspace_text assert "Traceback" not in backtests_workspace_text # Test backtest run by filling in fixture-supported dates page.get_by_label("Start date").fill("2024-01-02") page.get_by_label("End date").fill("2024-01-08") page.get_by_label("Underlying units").fill("2200") page.get_by_label("Loan amount").fill("222000") page.get_by_label("Margin call LTV").fill("0.8") # Click run and verify no runtime errors page.get_by_role("button", name="Run backtest").click() page.wait_for_timeout(2000) # Wait for response backtests_result_text = page.locator("body").inner_text(timeout=15000) # Should have results or validation error, but not runtime error assert "RuntimeError" not in backtests_result_text assert "Traceback" not in backtests_result_text page.goto(f"{workspace_url}/event-comparison", wait_until="networkidle", timeout=30000) # Event comparison now uses Initial portfolio value instead of Underlying units expect(page.get_by_label("Initial portfolio value")).to_be_visible(timeout=15000) 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) # Page should render without runtime errors assert "RuntimeError" not in event_workspace_text assert "Traceback" not in event_workspace_text # Fill in value and run comparison page.get_by_label("Initial portfolio value").fill("100000") page.get_by_role("button", name="Run comparison").click() # Check for results or validation error page.wait_for_timeout(2000) event_result_text = page.locator("body").inner_text(timeout=15000) # Should have results or error, but not runtime crash assert "RuntimeError" not in event_result_text assert "Traceback" not in event_result_text # Test preset selection page.get_by_label("Event preset").click() page.get_by_text("GLD January 2024 Drawdown", exact=True).click() page.wait_for_timeout(1000) event_preset_text = page.locator("body").inner_text(timeout=15000) assert "RuntimeError" not in event_preset_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(5): # Increase retry count try: expect(second_page).to_have_url(f"{second_workspace_url}/settings") expect(second_page.locator("text=Settings").first).to_be_visible(timeout=20000) expect(second_page.get_by_label("Monthly hedge budget ($)")).to_have_value("8000", timeout=20000) settings_loaded = True break except AssertionError: second_page.reload(wait_until="domcontentloaded", timeout=30000) assert settings_loaded, "Settings page failed to load after retries" 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() def test_backtest_page_loads_with_valid_databento_dates() -> None: """E2E test: Backtest page loads and validates Databento date range. Regression test for CORE-003: Ensures backtest page handles Databento dataset date constraints gracefully without 500 errors. """ with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1440, "height": 1000}) # Navigate to backtests page page.goto(f"{BASE_URL}/backtests", wait_until="domcontentloaded", timeout=30000) page.wait_for_load_state("networkidle", timeout=15000) # Verify page loaded without 500 error expect(page.locator("text=Scenario Configuration")).to_be_visible(timeout=10000) # Verify data source is set to Databento by default data_source = page.locator("[data-testid=data-source-select]") expect(data_source).to_be_visible() # Verify dataset is XNAS.BASIC dataset = page.locator("[data-testid=dataset-select]") expect(dataset).to_be_visible() # Verify date range hint shows correct minimum date for XNAS.BASIC date_hint = page.locator("text=XNAS.BASIC data available from 2024-07-01") expect(date_hint).to_be_visible(timeout=5000) # Verify start date input is set to valid date (2024-07-01 or later) start_input = page.locator("input[placeholder*='Start date']").first start_value = start_input.input_value() assert start_value >= "2024-07-01", f"Start date {start_value} should be >= 2024-07-01" # Verify no 500 error on page error_500 = page.locator("text=500").count() assert error_500 == 0, "Page should not show 500 error" page.screenshot(path=str(ARTIFACTS / "backtest_loads.png"), full_page=True) browser.close() def test_backtest_page_handles_invalid_dates_gracefully() -> None: """E2E test: Backtest page shows validation error for invalid dates. Regression test: Ensures user-friendly error instead of 500 when dates are before dataset availability. """ with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1440, "height": 1000}) page.goto(f"{BASE_URL}/backtests", wait_until="domcontentloaded", timeout=30000) page.wait_for_load_state("networkidle", timeout=15000) # Set invalid start date (before 2024-07-01) start_input = page.locator("input[placeholder*='Start date']").first start_input.fill("2024-03-01") start_input.press("Tab") # Trigger blur/validation page.wait_for_timeout(2000) # Should show validation error or prevent running, not crash # Check for validation message has_validation = ( page.locator("text=Invalid start date").is_visible() or page.locator("text=data available from").is_visible() or page.locator("text=warning").is_visible() ) # Verify no 500 error error_500 = page.locator("text=500").count() assert error_500 == 0, "Page should not show 500 error for invalid dates" page.screenshot(path=str(ARTIFACTS / "backtest_invalid_dates.png"), full_page=True) browser.close()