|
|
"""Tests for the summary cards component in the Folio app. |
|
|
|
|
|
This file contains both unit tests and integration tests for the summary cards component. |
|
|
""" |
|
|
|
|
|
import pytest |
|
|
|
|
|
from src.folio.app import create_app |
|
|
from src.folio.components.summary_cards import error_values, format_summary_card_values |
|
|
from src.folio.data_model import ExposureBreakdown, PortfolioSummary |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def test_summary(): |
|
|
"""Create a test portfolio summary with known values.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=10000.0, |
|
|
stock_beta_adjusted=12000.0, |
|
|
option_delta_exposure=2000.0, |
|
|
option_beta_adjusted=2400.0, |
|
|
total_exposure=12000.0, |
|
|
total_beta_adjusted=14400.0, |
|
|
description="Long exposure", |
|
|
formula="Sum of long positions", |
|
|
components={"Stocks": 10000.0, "Options": 2000.0}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=3000.0, |
|
|
stock_beta_adjusted=3600.0, |
|
|
option_delta_exposure=1000.0, |
|
|
option_beta_adjusted=1200.0, |
|
|
total_exposure=4000.0, |
|
|
total_beta_adjusted=4800.0, |
|
|
description="Short exposure", |
|
|
formula="Sum of short positions", |
|
|
components={"Stocks": 3000.0, "Options": 1000.0}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=1000.0, |
|
|
option_beta_adjusted=1200.0, |
|
|
total_exposure=1000.0, |
|
|
total_beta_adjusted=1200.0, |
|
|
description="Net options exposure", |
|
|
formula="Long options - Short options", |
|
|
components={"Long Options": 2000.0, "Short Options": 1000.0, "Net": 1000.0}, |
|
|
) |
|
|
|
|
|
|
|
|
summary = PortfolioSummary( |
|
|
net_market_exposure=8000.0, |
|
|
portfolio_beta=1.2, |
|
|
long_exposure=long_exposure, |
|
|
short_exposure=short_exposure, |
|
|
options_exposure=options_exposure, |
|
|
short_percentage=25.0, |
|
|
cash_like_value=2000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=20.0, |
|
|
portfolio_estimate_value=10000.0, |
|
|
) |
|
|
|
|
|
return summary |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_format_summary_card_values(test_summary): |
|
|
"""Test that the format_summary_card_values function returns the correct values.""" |
|
|
|
|
|
summary_dict = test_summary.to_dict() |
|
|
|
|
|
|
|
|
result = format_summary_card_values(summary_dict) |
|
|
|
|
|
|
|
|
expected_values = [ |
|
|
"$10,000.00", |
|
|
"$8,000.00", |
|
|
"80.0% of portfolio", |
|
|
"$19,200.00", |
|
|
"192.0% of portfolio", |
|
|
"$12,000.00", |
|
|
"120.0% of portfolio", |
|
|
"$4,000.00", |
|
|
"40.0% of portfolio", |
|
|
"$1,000.00", |
|
|
"10.0% of portfolio", |
|
|
"$2,000.00", |
|
|
"20.0% of portfolio", |
|
|
] |
|
|
|
|
|
|
|
|
for i, (actual, expected) in enumerate(zip(result, expected_values, strict=False)): |
|
|
assert actual == expected, ( |
|
|
f"Error at index {i}: expected '{expected}', got '{actual}'" |
|
|
) |
|
|
|
|
|
|
|
|
def test_format_summary_card_values_with_missing_keys(test_summary): |
|
|
"""Test that format_summary_card_values handles missing keys.""" |
|
|
|
|
|
summary_dict = test_summary.to_dict() |
|
|
|
|
|
|
|
|
del summary_dict["portfolio_estimate_value"] |
|
|
|
|
|
|
|
|
result = format_summary_card_values(summary_dict) |
|
|
|
|
|
|
|
|
expected_values = [ |
|
|
"$10,000.00", |
|
|
"$8,000.00", |
|
|
"80.0% of portfolio", |
|
|
"$19,200.00", |
|
|
"192.0% of portfolio", |
|
|
"$12,000.00", |
|
|
"120.0% of portfolio", |
|
|
"$4,000.00", |
|
|
"40.0% of portfolio", |
|
|
"$1,000.00", |
|
|
"10.0% of portfolio", |
|
|
"$2,000.00", |
|
|
"20.0% of portfolio", |
|
|
] |
|
|
|
|
|
|
|
|
for i, (actual, expected) in enumerate(zip(result, expected_values, strict=False)): |
|
|
assert actual == expected, ( |
|
|
f"Error at index {i}: expected '{expected}', got '{actual}'" |
|
|
) |
|
|
|
|
|
|
|
|
def test_format_summary_card_values_with_invalid_data(): |
|
|
"""Test that format_summary_card_values handles invalid data.""" |
|
|
|
|
|
result = format_summary_card_values(None) |
|
|
|
|
|
|
|
|
assert result[0] == "Error" |
|
|
assert result[1] == "Error" |
|
|
assert result[3] == "Error" |
|
|
|
|
|
|
|
|
result = format_summary_card_values({}) |
|
|
|
|
|
|
|
|
assert result[0] == "Error" |
|
|
assert result[1] == "Error" |
|
|
assert result[3] == "Error" |
|
|
|
|
|
|
|
|
def test_error_values(): |
|
|
"""Test that the error_values function returns the correct values.""" |
|
|
result = error_values() |
|
|
|
|
|
|
|
|
assert result[0] == "Error" |
|
|
assert result[1] == "Error" |
|
|
assert result[2] == "Data missing" |
|
|
assert result[3] == "Error" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_callback_registration(): |
|
|
"""Test that the summary cards callback is registered.""" |
|
|
|
|
|
app = create_app() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert app is not None, "App was not created successfully" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_summary_cards_user_expectations(): |
|
|
"""Test that summary cards meet user expectations. |
|
|
|
|
|
This test focuses on what users expect to see, not implementation details. |
|
|
It verifies that the summary cards display the expected information in a user-friendly way. |
|
|
""" |
|
|
|
|
|
app = create_app() |
|
|
|
|
|
|
|
|
layout = app.layout |
|
|
|
|
|
|
|
|
summary_card = None |
|
|
|
|
|
def find_summary_card(component): |
|
|
"""Recursively find the summary card component.""" |
|
|
nonlocal summary_card |
|
|
if hasattr(component, "id") and component.id == "summary-card": |
|
|
summary_card = component |
|
|
return True |
|
|
|
|
|
if hasattr(component, "children"): |
|
|
if isinstance(component.children, list): |
|
|
for child in component.children: |
|
|
if find_summary_card(child): |
|
|
return True |
|
|
elif component.children is not None: |
|
|
return find_summary_card(component.children) |
|
|
|
|
|
return False |
|
|
|
|
|
|
|
|
find_summary_card(layout) |
|
|
|
|
|
|
|
|
assert summary_card is not None, "Summary card not found in layout" |
|
|
|
|
|
|
|
|
|
|
|
layout_str = str(summary_card) |
|
|
|
|
|
|
|
|
assert "Portfolio Summary" in layout_str, ( |
|
|
"Portfolio Summary not found in summary cards" |
|
|
) |
|
|
assert "Total Portfolio Value" in layout_str, ( |
|
|
"Total Portfolio Value not found in summary cards" |
|
|
) |
|
|
assert "Net Exposure" in layout_str, "Net Exposure not found in summary cards" |
|
|
assert "Beta-Adjusted Net Exposure" in layout_str, ( |
|
|
"Beta-Adjusted Net Exposure not found in summary cards" |
|
|
) |
|
|
assert "Long Exposure" in layout_str, "Long Exposure not found in summary cards" |
|
|
assert "Short Exposure" in layout_str, "Short Exposure not found in summary cards" |
|
|
assert "Options Exposure" in layout_str, ( |
|
|
"Options Exposure not found in summary cards" |
|
|
) |
|
|
assert "Cash & Equivalents" in layout_str, ( |
|
|
"Cash & Equivalents not found in summary cards" |
|
|
) |
|
|
|
|
|
|
|
|
def test_summary_cards_rendered_and_callback_registered(): |
|
|
"""Test that summary cards are rendered in the layout and their callback is registered. |
|
|
|
|
|
This test verifies two critical behaviors: |
|
|
1. The summary cards components are present in the app layout |
|
|
2. The callback for updating the summary cards is properly registered |
|
|
""" |
|
|
|
|
|
app = create_app() |
|
|
|
|
|
|
|
|
layout = app.layout |
|
|
|
|
|
|
|
|
expected_ids = [ |
|
|
"summary-card", |
|
|
"portfolio-value", |
|
|
"total-value", |
|
|
"beta-adjusted-exposure", |
|
|
"beta-adjusted-percent", |
|
|
"long-exposure", |
|
|
"short-exposure", |
|
|
"options-exposure", |
|
|
"cash-like-value", |
|
|
] |
|
|
|
|
|
|
|
|
found_ids = set() |
|
|
|
|
|
def find_components(component): |
|
|
"""Recursively find all components with IDs in the layout.""" |
|
|
if hasattr(component, "id") and component.id is not None: |
|
|
found_ids.add(component.id) |
|
|
|
|
|
if hasattr(component, "children"): |
|
|
if isinstance(component.children, list): |
|
|
for child in component.children: |
|
|
find_components(child) |
|
|
elif component.children is not None: |
|
|
find_components(component.children) |
|
|
|
|
|
|
|
|
find_components(layout) |
|
|
|
|
|
|
|
|
for component_id in expected_ids: |
|
|
assert component_id in found_ids, ( |
|
|
f"Component {component_id} not found in layout" |
|
|
) |
|
|
|
|
|
|
|
|
callbacks = app.callback_map |
|
|
|
|
|
|
|
|
|
|
|
portfolio_value_callback_found = False |
|
|
for callback_id in callbacks.keys(): |
|
|
if "portfolio-value.children" in callback_id: |
|
|
portfolio_value_callback_found = True |
|
|
break |
|
|
|
|
|
|
|
|
assert portfolio_value_callback_found, ( |
|
|
"Callback for portfolio-value not registered - register_callbacks(app) is not called in create_app" |
|
|
) |
|
|
|