"""Integration test: full trajectory pipeline produces non-empty plot.""" import pytest from explorer import load_positions, load_party_map, select_trajectory_plot_data from explorer_helpers import compute_party_centroids def test_trajectory_pipeline_produces_traces(): """Regression: trajectories must produce colored traces, not empty charts.""" db_path = "data/motions.db" window_size = "annual" # Stage 1: load positions positions_by_window, _ = load_positions(db_path, window_size) assert len(positions_by_window) > 0, "Expected at least one window" total_mps = sum(len(v) for v in positions_by_window.values()) assert total_mps > 0, "Expected MPs in windows" # Stage 2: load party map party_map = load_party_map(db_path) assert len(party_map) > 0, "Expected party map entries" # Stage 3: compute centroids windows = list(positions_by_window.keys()) centroids, mp_positions = compute_party_centroids( positions_by_window, party_map, windows ) assert len(centroids) > 0, "Expected at least one party centroid" # Stage 4: select trajectory plot data (default party selection) # Use the same defaults as build_trajectories_tab: CDA, D66, VVD if available default_parties = [p for p in ["CDA", "D66", "VVD"] if p in centroids] if not default_parties: default_parties = list(centroids.keys())[:3] fig, trace_count, banner = select_trajectory_plot_data( positions_by_window, party_map, windows, selected_parties=default_parties, smooth_alpha=0.35, ) # Assertions assert trace_count > 0, ( f"Expected traces but got trace_count={trace_count}, banner={banner}" ) assert banner is None, f"Expected no fallback banner but got: {banner}" assert len(fig.data) == trace_count, ( f"fig.data ({len(fig.data)}) should equal trace_count ({trace_count})" ) # Verify traces have real coordinates (not all NaN) for trace in fig.data: assert len(trace.x) > 0, f"Trace {trace.name} has no x values" assert len(trace.y) > 0, f"Trace {trace.name} has no y values" # At least some values should be real (not NaN) import math real_x = sum( 1 for v in trace.x if not (v is None or (isinstance(v, float) and v != v)) ) # v != v is True only for NaN real_y = sum( 1 for v in trace.y if not (v is None or (isinstance(v, float) and v != v)) ) assert real_x > 0, f"Trace {trace.name} has all NaN x values" assert real_y > 0, f"Trace {trace.name} has all NaN y values" def test_trajectory_helper_skips_second_loop(): """Regression: when select_trajectory_plot_data succeeds, build_trajectories_tab should NOT add duplicate traces via the fallback loop. This test verifies that the helper produces clean output without relying on the second loop in build_trajectories_tab. """ db_path = "data/motions.db" window_size = "annual" positions_by_window, _ = load_positions(db_path, window_size) party_map = load_party_map(db_path) windows = list(positions_by_window.keys()) centroids, _ = compute_party_centroids(positions_by_window, party_map, windows) # Use 6 parties like the app's multiselect selected = list(centroids.keys())[:6] fig, trace_count, banner = select_trajectory_plot_data( positions_by_window, party_map, windows, selected_parties=selected, smooth_alpha=0.35, ) # Should produce exactly the number of selected parties (or fewer if some have all-NaN) assert trace_count <= len(selected), ( f"trace_count ({trace_count}) should not exceed selected ({len(selected)})" ) assert banner is None, "No fallback should be needed with valid data" assert len(fig.data) == trace_count