Coverage for tests/test_diagram.py: 100%
70 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-05 04:36 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-05 04:36 +0000
1import plotly.graph_objects as go
2import pytest
4from sankey_cashflow import AppSettings, RowLabels, Transactions, build_line_figure, build_sankey_figure, fetch_data
7def _process(make_args, **overrides):
8 args_kwargs = {'all_time': True, 'hover': 'Category'}
9 args_kwargs.update(overrides)
10 settings = AppSettings(make_args(**args_kwargs))
11 src_target, df = fetch_data(settings)
12 labels = RowLabels(src_target)
13 txn = Transactions(df, labels, settings)
14 txn.process()
15 return txn, labels, settings
18@pytest.fixture
19def processed_transactions(make_args):
20 return _process(make_args)
23class TestBuildSankeyFigure:
25 def test_returns_figure(self, processed_transactions):
26 txn, labels, settings = processed_transactions
27 fig = build_sankey_figure(txn, labels, settings)
28 assert isinstance(fig, go.Figure)
30 def test_links_match_grouped_data_length(self, processed_transactions):
31 txn, labels, settings = processed_transactions
32 fig = build_sankey_figure(txn, labels, settings)
33 assert len(fig.data[0].link.value) == len(txn.grouped_data)
35 def test_title_uses_transactions_title(self, processed_transactions):
36 txn, labels, settings = processed_transactions
37 fig = build_sankey_figure(txn, labels, settings)
38 assert fig.layout.title.text == txn.title
40 def test_safe_to_call_more_than_once(self, processed_transactions):
41 # Regression test: the original script mutated grouped_data in place (remapped Source/Target
42 # to integer indices), so a second call would silently produce a broken figure.
43 txn, labels, settings = processed_transactions
44 fig1 = build_sankey_figure(txn, labels, settings)
45 fig2 = build_sankey_figure(txn, labels, settings)
46 assert list(fig1.data[0].link.value) == list(fig2.data[0].link.value)
47 assert list(fig1.data[0].node.label) == list(fig2.data[0].node.label)
49 def test_hover_customdata_present_when_enabled(self, processed_transactions):
50 txn, labels, settings = processed_transactions
51 fig = build_sankey_figure(txn, labels, settings)
52 assert fig.data[0].node.customdata is not None
54 def test_no_hover_customdata_when_disabled(self, make_args):
55 # AppSettings only disables hover for the string 'none' (or 'no'/'false') - omitting the
56 # flag entirely defaults to hover='Category' (enabled).
57 txn, labels, settings = _process(make_args, hover='none')
58 assert settings.hover is None
59 fig = build_sankey_figure(txn, labels, settings)
60 assert fig.data[0].node.customdata is None
63class TestBuildLineFigure:
65 def test_returns_figure_with_traces(self, processed_transactions):
66 txn, _, settings = processed_transactions
67 settings.chart_resolution = 'week'
68 fig = build_line_figure(txn, settings)
69 assert isinstance(fig, go.Figure)
70 assert len(fig.data) > 0
72 def test_excludes_income_and_uncategorized(self, processed_transactions):
73 txn, _, settings = processed_transactions
74 settings.chart_resolution = 'month'
75 fig = build_line_figure(txn, settings)
76 trace_names = [t.name for t in fig.data]
77 assert 'Income' not in trace_names
78 assert 'Uncategorized' not in trace_names
80 def test_excludes_x_prefixed_classifications(self, processed_transactions):
81 txn, _, settings = processed_transactions
82 settings.chart_resolution = 'month'
83 fig = build_line_figure(txn, settings)
84 trace_names = [t.name for t in fig.data]
85 assert any(name.startswith('x') for name in txn.processed_data['Classification'].unique())
86 assert not any(name.startswith('x') for name in trace_names)
88 def test_does_not_mutate_processed_data(self, processed_transactions):
89 txn, _, settings = processed_transactions
90 settings.chart_resolution = 'month'
91 original_columns = list(txn.processed_data.columns)
92 build_line_figure(txn, settings)
93 assert list(txn.processed_data.columns) == original_columns