From 78de8782c4f780842fb624a2241f9961ad3443f0 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 26 Mar 2026 10:24:52 +0100 Subject: [PATCH] fix(UX-001): address layout review findings --- app/pages/backtests.py | 118 ++++++++++++------ app/pages/common.py | 9 +- app/pages/event_comparison.py | 105 ++++++++++++---- docs/roadmap/ROADMAP.yaml | 4 +- .../UX-001-full-width-two-pane-layout.yaml | 24 ---- .../UX-001-full-width-two-pane-layout.yaml | 21 ++++ tests/test_e2e_playwright.py | 55 +++++++- 7 files changed, 238 insertions(+), 98 deletions(-) delete mode 100644 docs/roadmap/backlog/UX-001-full-width-two-pane-layout.yaml create mode 100644 docs/roadmap/done/UX-001-full-width-two-pane-layout.yaml diff --git a/app/pages/backtests.py b/app/pages/backtests.py index 1d34229..5814180 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from datetime import date, datetime from fastapi.responses import RedirectResponse @@ -10,6 +11,8 @@ from app.models.workspace import get_workspace_repository from app.pages.common import dashboard_page, split_page_panes from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService +logger = logging.getLogger(__name__) + def _chart_options(result: BacktestPageRunResult) -> dict: template_result = result.run_result.template_results[0] @@ -133,18 +136,23 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: except ValueError as exc: raise ValueError(f"{field_name} must be in YYYY-MM-DD format") from exc - def render_seeded_summary(*, entry_spot: float | None = None) -> None: + def derive_entry_spot() -> tuple[float | None, str | None]: + try: + resolved_entry_spot = service.derive_entry_spot( + str(symbol_input.value or "GLD"), + parse_iso_date(start_input.value, "Start date"), + parse_iso_date(end_input.value, "End date"), + ) + except (ValueError, KeyError) as exc: + return None, str(exc) + return resolved_entry_spot, None + + def render_seeded_summary(*, entry_spot: float | None = None, entry_spot_error: str | None = None) -> None: seeded_summary.clear() resolved_entry_spot = entry_spot - if resolved_entry_spot is None: - try: - resolved_entry_spot = service.derive_entry_spot( - str(symbol_input.value or "GLD"), - parse_iso_date(start_input.value, "Start date"), - parse_iso_date(end_input.value, "End date"), - ) - except (ValueError, KeyError): - resolved_entry_spot = None + resolved_error = entry_spot_error + if resolved_entry_spot is None and resolved_error is None: + resolved_entry_spot, resolved_error = derive_entry_spot() with seeded_summary: ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"): @@ -159,7 +167,7 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ), ( "Entry spot", - f"${resolved_entry_spot:,.2f}" if resolved_entry_spot is not None else "Pending", + f"${resolved_entry_spot:,.2f}" if resolved_entry_spot is not None else "Unavailable", ), ] for label, value in cards: @@ -168,15 +176,34 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ): ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400") ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100") + if resolved_error: + ui.label(resolved_error).classes("text-sm text-amber-700 dark:text-amber-300") - def render_result_validation(message: str) -> None: + def render_result_state(title: str, message: str, *, tone: str = "info") -> None: + tone_classes = { + "info": "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/30", + "warning": "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30", + "error": "border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/30", + } + text_classes = { + "info": "text-sky-800 dark:text-sky-200", + "warning": "text-amber-800 dark:text-amber-200", + "error": "text-rose-800 dark:text-rose-200", + } result_panel.clear() with result_panel: with ui.card().classes( - "w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30" + f"w-full rounded-2xl border shadow-sm {tone_classes.get(tone, tone_classes['info'])}" ): - ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") - ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200") + ui.label(title).classes("text-lg font-semibold text-slate-900 dark:text-slate-100") + ui.label(message).classes(f"text-sm {text_classes.get(tone, text_classes['info'])}") + + def mark_results_stale() -> None: + render_result_state( + "Results out of date", + "Inputs changed. Run backtest again to refresh charts and daily results for the current scenario.", + tone="info", + ) def render_result(result: BacktestPageRunResult) -> None: result_panel.clear() @@ -263,20 +290,23 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ).classes("w-full") def refresh_workspace_seeded_units() -> None: - if not workspace_id or config is None or config.gold_value is None: - return - try: - entry_spot = service.derive_entry_spot( - str(symbol_input.value or "GLD"), - parse_iso_date(start_input.value, "Start date"), - parse_iso_date(end_input.value, "End date"), - ) - except (ValueError, KeyError): - render_seeded_summary(entry_spot=None) - return - units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD") - entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}") - render_seeded_summary(entry_spot=entry_spot) + entry_spot, entry_error = derive_entry_spot() + if ( + workspace_id + and config is not None + and config.gold_value is not None + and entry_spot is not None + ): + units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD") + entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}") + else: + entry_spot_hint.set_text("Entry spot unavailable until the scenario dates are valid.") + render_seeded_summary(entry_spot=entry_spot, entry_spot_error=entry_error) + mark_results_stale() + + def on_form_change() -> None: + render_seeded_summary() + mark_results_stale() def run_backtest() -> None: validation_label.set_text("") @@ -291,25 +321,39 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: margin_call_ltv=float(ltv_input.value or 0.0), ) except (ValueError, KeyError) as exc: - render_seeded_summary(entry_spot=None) + render_seeded_summary(entry_spot=None, entry_spot_error=None) validation_label.set_text(str(exc)) - render_result_validation(str(exc)) + render_result_state("Scenario validation failed", str(exc), tone="warning") return except Exception: - render_seeded_summary(entry_spot=None) + render_seeded_summary(entry_spot=None, entry_spot_error=None) message = "Backtest failed. Please verify the scenario inputs and try again." + logger.exception( + "Backtest page run failed for workspace=%s symbol=%s start=%s end=%s template=%s units=%s loan=%s margin_call_ltv=%s", + workspace_id, + symbol_input.value, + start_input.value, + end_input.value, + template_select.value, + units_input.value, + loan_input.value, + ltv_input.value, + ) validation_label.set_text(message) - render_result_validation(message) + render_result_state("Backtest failed", message, tone="error") return render_result(result) if workspace_id: start_input.on_value_change(lambda _event: refresh_workspace_seeded_units()) end_input.on_value_change(lambda _event: refresh_workspace_seeded_units()) - template_select.on_value_change(lambda _event: render_seeded_summary()) - units_input.on_value_change(lambda _event: render_seeded_summary()) - loan_input.on_value_change(lambda _event: render_seeded_summary()) - ltv_input.on_value_change(lambda _event: render_seeded_summary()) + else: + start_input.on_value_change(lambda _event: on_form_change()) + end_input.on_value_change(lambda _event: on_form_change()) + template_select.on_value_change(lambda _event: on_form_change()) + units_input.on_value_change(lambda _event: on_form_change()) + loan_input.on_value_change(lambda _event: on_form_change()) + ltv_input.on_value_change(lambda _event: on_form_change()) run_button.on_click(lambda: run_backtest()) render_seeded_summary(entry_spot=default_entry_spot) run_backtest() diff --git a/app/pages/common.py b/app/pages/common.py index 0c490a8..87c0bc0 100644 --- a/app/pages/common.py +++ b/app/pages/common.py @@ -124,13 +124,10 @@ def strategy_metrics( def split_page_panes(*, left_testid: str, right_testid: str) -> tuple[ui.column, ui.column]: + """Render responsive page panes with a desktop 1:2 flex split and stable test hooks.""" with ui.row().classes("w-full items-start gap-6 max-lg:flex-col lg:flex-nowrap"): - left = ui.column().classes("min-w-0 w-full gap-6 lg:w-1/3 lg:flex-none").props( - f"data-testid={left_testid}" - ) - right = ui.column().classes("min-w-0 w-full gap-6 lg:w-2/3 lg:flex-none").props( - f"data-testid={right_testid}" - ) + left = ui.column().classes("min-w-0 w-full gap-6 lg:flex-[1_1_0%]").props(f"data-testid={left_testid}") + right = ui.column().classes("min-w-0 w-full gap-6 lg:flex-[2_1_0%]").props(f"data-testid={right_testid}") return left, right diff --git a/app/pages/event_comparison.py b/app/pages/event_comparison.py index 0fd2952..a7d5a45 100644 --- a/app/pages/event_comparison.py +++ b/app/pages/event_comparison.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from fastapi.responses import RedirectResponse from nicegui import ui @@ -8,6 +10,8 @@ from app.models.workspace import get_workspace_repository from app.pages.common import dashboard_page, split_page_panes from app.services.event_comparison_ui import EventComparisonPageService +logger = logging.getLogger(__name__) + def _chart_options(dates: tuple[str, ...], series: tuple[dict[str, object], ...]) -> dict: return { @@ -124,13 +128,15 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: with right_pane: result_panel = ui.column().classes("w-full gap-6") + syncing_controls = {"value": False} + def selected_template_slugs() -> tuple[str, ...]: raw_value = template_select.value or [] if isinstance(raw_value, str): return (raw_value,) if raw_value else () return tuple(str(item) for item in raw_value if item) - def render_selected_summary(entry_spot: float | None = None) -> None: + def render_selected_summary(entry_spot: float | None = None, entry_spot_error: str | None = None) -> None: selected_summary.clear() with selected_summary: ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") @@ -144,7 +150,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: ("Underlying units", f"{float(units_input.value or 0.0):,.0f}"), ("Loan amount", f"${float(loan_input.value or 0.0):,.0f}"), ("Margin call LTV", f"{float(ltv_input.value or 0.0):.1%}"), - ("Entry spot", f"${entry_spot:,.2f}" if entry_spot is not None else "Pending"), + ("Entry spot", f"${entry_spot:,.2f}" if entry_spot is not None else "Unavailable"), ] for label, value in cards: with ui.card().classes( @@ -152,18 +158,53 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: ): ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400") ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100") + if entry_spot_error: + ui.label(entry_spot_error).classes("text-sm text-amber-700 dark:text-amber-300") - def refresh_preset_details() -> None: + def render_result_state(title: str, message: str, *, tone: str = "info") -> None: + tone_classes = { + "info": "border-sky-200 bg-sky-50 dark:border-sky-900/60 dark:bg-sky-950/30", + "warning": "border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30", + "error": "border-rose-200 bg-rose-50 dark:border-rose-900/60 dark:bg-rose-950/30", + } + text_classes = { + "info": "text-sky-800 dark:text-sky-200", + "warning": "text-amber-800 dark:text-amber-200", + "error": "text-rose-800 dark:text-rose-200", + } + result_panel.clear() + with result_panel: + with ui.card().classes( + f"w-full rounded-2xl border shadow-sm {tone_classes.get(tone, tone_classes['info'])}" + ): + ui.label(title).classes("text-lg font-semibold text-slate-900 dark:text-slate-100") + ui.label(message).classes(f"text-sm {text_classes.get(tone, text_classes['info'])}") + + def mark_results_stale() -> None: + render_result_state( + "Results out of date", + "Inputs changed. Run comparison again to refresh rankings and portfolio paths for the current scenario.", + tone="info", + ) + + def refresh_preview(*, reset_templates: bool = False, reseed_units: bool = False) -> None: option = preset_lookup.get(str(preset_select.value or "")) if option is None: metadata_label.set_text("") scenario_label.set_text("") render_selected_summary(entry_spot=None) return - template_select.value = list(service.default_template_selection(str(option["slug"]))) + + if reset_templates: + syncing_controls["value"] = True + try: + template_select.value = list(service.default_template_selection(str(option["slug"]))) + finally: + syncing_controls["value"] = False + try: preview_units = float(units_input.value or 0.0) - if workspace_id and config is not None: + if workspace_id and config is not None and reseed_units: preview_scenario = service.preview_scenario( preset_slug=str(option["slug"]), template_slugs=selected_template_slugs(), @@ -176,7 +217,11 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: entry_spot=float(preview_scenario.initial_portfolio.entry_spot), symbol="GLD", ) - units_input.value = preview_units + syncing_controls["value"] = True + try: + units_input.value = preview_units + finally: + syncing_controls["value"] = False scenario = service.preview_scenario( preset_slug=str(option["slug"]), template_slugs=selected_template_slugs(), @@ -187,7 +232,7 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: except (ValueError, KeyError) as exc: metadata_label.set_text(f"Preset: {option['label']} — {option['description']}") scenario_label.set_text(str(exc)) - render_selected_summary(entry_spot=None) + render_selected_summary(entry_spot=None, entry_spot_error=str(exc)) return preset = service.event_preset_service.get_preset(str(option["slug"])) metadata_label.set_text(f"Preset: {option['label']} — {option['description']}") @@ -203,15 +248,6 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: ) render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot)) - def render_result_validation(message: str) -> None: - result_panel.clear() - with result_panel: - with ui.card().classes( - "w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30" - ): - ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") - ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200") - def render_report() -> None: validation_label.set_text("") result_panel.clear() @@ -225,12 +261,21 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: ) except (ValueError, KeyError) as exc: validation_label.set_text(str(exc)) - render_result_validation(str(exc)) + render_result_state("Scenario validation failed", str(exc), tone="warning") return except Exception: message = "Event comparison failed. Please verify the seeded inputs and try again." + logger.exception( + "Event comparison page run failed for workspace=%s preset=%s templates=%s units=%s loan=%s margin_call_ltv=%s", + workspace_id, + preset_select.value, + selected_template_slugs(), + units_input.value, + loan_input.value, + ltv_input.value, + ) validation_label.set_text(message) - render_result_validation(message) + render_result_state("Event comparison failed", message, tone="error") return preset = report.event_preset @@ -340,11 +385,23 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None: ) ).classes("h-96 w-full") - preset_select.on_value_change(lambda _: refresh_preset_details()) - template_select.on_value_change(lambda _: refresh_preset_details()) - units_input.on_value_change(lambda _: refresh_preset_details()) - loan_input.on_value_change(lambda _: refresh_preset_details()) - ltv_input.on_value_change(lambda _: refresh_preset_details()) + def on_preset_change() -> None: + if syncing_controls["value"]: + return + refresh_preview(reset_templates=True, reseed_units=True) + mark_results_stale() + + def on_preview_input_change() -> None: + if syncing_controls["value"]: + return + refresh_preview() + mark_results_stale() + + preset_select.on_value_change(lambda _: on_preset_change()) + template_select.on_value_change(lambda _: on_preview_input_change()) + units_input.on_value_change(lambda _: on_preview_input_change()) + loan_input.on_value_change(lambda _: on_preview_input_change()) + ltv_input.on_value_change(lambda _: on_preview_input_change()) run_button.on_click(lambda: render_report()) - refresh_preset_details() + refresh_preview(reset_templates=False, reseed_units=False) render_report() diff --git a/docs/roadmap/ROADMAP.yaml b/docs/roadmap/ROADMAP.yaml index 700685e..74e0cc4 100644 --- a/docs/roadmap/ROADMAP.yaml +++ b/docs/roadmap/ROADMAP.yaml @@ -11,7 +11,6 @@ notes: - One task lives in one YAML file and changes state by moving between status folders. - Priority ordering is maintained here so agents can parse one short file first. priority_queue: - - UX-001 - CORE-001D - BT-003B - PORT-003 @@ -24,6 +23,7 @@ priority_queue: - OPS-001 - BT-003 recently_completed: + - UX-001 - CORE-002 - CORE-002C - CORE-001D2B @@ -43,7 +43,6 @@ states: - BT-003B - BT-001C - CORE-001D - - UX-001 in_progress: [] done: - DATA-001 @@ -68,5 +67,6 @@ states: - CORE-002A - CORE-002B - CORE-002C + - UX-001 blocked: [] cancelled: [] diff --git a/docs/roadmap/backlog/UX-001-full-width-two-pane-layout.yaml b/docs/roadmap/backlog/UX-001-full-width-two-pane-layout.yaml deleted file mode 100644 index 8751dbd..0000000 --- a/docs/roadmap/backlog/UX-001-full-width-two-pane-layout.yaml +++ /dev/null @@ -1,24 +0,0 @@ -id: UX-001 -title: Full-Width Two-Pane Page Layout -status: backlog -priority: P1 -effort: M -depends_on: - - PORT-004 - - BT-001A - - BT-003A -tags: - - ui - - layout - - nicegui -summary: Use the full available browser width and standardize primary pages on a 1/3 control-summary pane and 2/3 charts-results pane. -acceptance_criteria: - - Dashboard pages use the full available browser width instead of the current centered max-width container. - - Scenario-heavy pages use a consistent desktop two-pane layout with an approximately 1/3 left pane and 2/3 right pane. - - Left pane contains controls and summary content; right pane contains charts, tables, and scenario results. - - Mobile and narrow widths still stack cleanly without clipping. - - Browser-visible tests cover the split-pane structure on representative pages. -technical_notes: - - Likely file targets include `app/pages/common.py`, `app/pages/hedge.py`, `app/pages/backtests.py`, `app/pages/event_comparison.py`, `app/pages/options.py`, and possibly `app/pages/overview.py` / `app/pages/settings.py` for consistent full-width layout. - - Prefer shared layout helpers in `app/pages/common.py` over page-specific one-off width classes. - - Add stable DOM hooks (for example `data-testid`) so layout structure can be asserted in browser tests. diff --git a/docs/roadmap/done/UX-001-full-width-two-pane-layout.yaml b/docs/roadmap/done/UX-001-full-width-two-pane-layout.yaml new file mode 100644 index 0000000..c98e19e --- /dev/null +++ b/docs/roadmap/done/UX-001-full-width-two-pane-layout.yaml @@ -0,0 +1,21 @@ +id: UX-001 +title: Full-Width Two-Pane Page Layout +status: done +priority: P1 +effort: M +depends_on: + - PORT-004 + - BT-001A + - BT-003A +tags: + - ui + - layout + - nicegui +summary: Use the full available browser width and standardize primary pages on a 1/3 control-summary pane and 2/3 charts-results pane. +completed_notes: + - Added a shared `split_page_panes(...)` helper in `app/pages/common.py` and removed the old centered max-width dashboard container. + - Applied the full-width two-pane layout to overview, hedge, backtests, event comparison, options, and settings pages. + - Fixed desktop pane overflow by switching the shared helper to a shrinkable 1:2 flex split instead of fixed width fractions plus gap. + - Historical scenario pages now mark prior right-pane output as stale when inputs change instead of silently showing old results beside new left-pane summaries. + - Event comparison now preserves manual template/unit edits across preview refreshes except when the preset itself changes and intentionally resets defaults. + - Added browser-visible Playwright coverage for desktop pane ratios, no-horizontal-overflow checks, stacked narrow-width behavior, and stale-result states. diff --git a/tests/test_e2e_playwright.py b/tests/test_e2e_playwright.py index e269c01..1099b82 100644 --- a/tests/test_e2e_playwright.py +++ b/tests/test_e2e_playwright.py @@ -9,6 +9,13 @@ 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 @@ -21,7 +28,22 @@ def assert_two_pane_layout(page, left_testid: str, right_testid: str) -> 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.6 <= width_ratio <= 2.4 + 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: @@ -37,8 +59,14 @@ def test_homepage_and_options_page_render() -> None: workspace_url = page.url workspace_id = workspace_url.removeprefix(f"{BASE_URL}/") assert workspace_id - cookies = page.context.cookies() - workspace_cookie = next(cookie for cookie in cookies if cookie["name"] == "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) @@ -47,6 +75,11 @@ def test_homepage_and_options_page_render() -> None: 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) @@ -122,13 +155,15 @@ def test_homepage_and_options_page_render() -> None: 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 Results" 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) @@ -148,9 +183,16 @@ def test_homepage_and_options_page_render() -> None: 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) @@ -162,9 +204,12 @@ def test_homepage_and_options_page_render() -> None: 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=GLD January 2024 Drawdown").first).to_be_visible(timeout=15000) + 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