diff --git a/app/pages/settings.py b/app/pages/settings.py index d77edf2..3974eb9 100644 --- a/app/pages/settings.py +++ b/app/pages/settings.py @@ -70,6 +70,15 @@ def settings_page(workspace_id: str) -> None: return str(int(parsed)) return str(parsed) + def as_positive_int(value: object) -> int | None: + try: + parsed = float(value) + except (TypeError, ValueError): + return None + if parsed < 1 or not parsed.is_integer(): + return None + return int(parsed) + def last_saved_status_text(config: PortfolioConfig) -> str: return save_status_text(config).replace("Saved:", "Last saved:", 1) @@ -78,6 +87,10 @@ def settings_page(workspace_id: str) -> None: if parsed_loan_amount is None: raise ValueError("Loan amount must be zero or greater") + parsed_refresh_interval = as_positive_int(refresh_interval.value) + if parsed_refresh_interval is None: + raise ValueError("Refresh interval must be a whole number of seconds") + return PortfolioConfig( gold_value=as_positive_float(gold_value.value), entry_price=as_positive_float(entry_price.value), @@ -89,7 +102,7 @@ def settings_page(workspace_id: str) -> None: ltv_warning=float(ltv_warning.value), primary_source=str(primary_source.value), fallback_source=str(fallback_source.value), - refresh_interval=int(refresh_interval.value), + refresh_interval=parsed_refresh_interval, volatility_spike=float(vol_alert.value), spot_drawdown=float(price_alert.value), email_alerts=bool(email_alerts.value), diff --git a/tests/test_settings_validation_playwright.py b/tests/test_settings_validation_playwright.py index 50eab94..d0e63da 100644 --- a/tests/test_settings_validation_playwright.py +++ b/tests/test_settings_validation_playwright.py @@ -20,8 +20,15 @@ def test_settings_reject_invalid_loan_amount_without_silent_zero_fallback() -> N expect(page.locator("text=Settings").first).to_be_visible(timeout=15000) expect(page.locator("text=Last saved:").first).to_be_visible(timeout=15000) - page.get_by_label("Refresh interval (seconds)").fill("7") + refresh_interval_input = page.get_by_label("Refresh interval (seconds)") + refresh_interval_input.fill("7") expect(page.locator("text=Unsaved changes — Last saved:").first).to_be_visible(timeout=15000) + refresh_interval_input.fill("5.5") + refresh_interval_input.press("Tab") + expect(page.locator("text=INVALID").first).to_be_visible(timeout=15000) + expect(page.locator("text=Refresh interval must be a whole number of seconds").first).to_be_visible( + timeout=15000 + ) page.reload(wait_until="domcontentloaded", timeout=30000) expect(page.get_by_label("Refresh interval (seconds)")).to_have_value("5") expect(page.locator("text=Last saved:").first).to_be_visible(timeout=15000)