"""Tests for Rich dashboard display.""" from io import StringIO from rich.console import Console from gitea_dashboard.display import ( render_dashboard, sort_repos, ) from tests.helpers import make_repo as _make_repo def _make_console(): """Create a console that captures output for testing. highlight=False desactive le highlighting automatique de Rich qui fragmente les nombres et noms avec des codes ANSI. """ buf = StringIO() return Console(file=buf, force_terminal=True, width=120, highlight=False), buf class TestRenderDashboardTable: """Test the main repos table rendering.""" def test_basic_repo_displayed(self): """Repo name and issues count appear in output.""" console, buf = _make_console() repos = [ _make_repo( name="mon-projet", open_issues=3, latest_release={"tag_name": "v1.2.0", "published_at": "2026-03-08T10:00:00Z"}, ), ] render_dashboard(repos, console=console) output = buf.getvalue() assert "mon-projet" in output assert "v1.2.0" in output def test_repo_without_release_shows_dash(self): """Repo with no release shows a dash character.""" console, buf = _make_console() repos = [_make_repo(name="no-release", latest_release=None)] render_dashboard(repos, console=console) output = buf.getvalue() assert "no-release" in output # The dash character for "no release" assert "\u2014" in output or "β€”" in output def test_fork_indicator(self): """Fork repos show [F] indicator.""" console, buf = _make_console() repos = [_make_repo(name="forked-repo", is_fork=True)] render_dashboard(repos, console=console) output = buf.getvalue() assert "forked-repo" in output assert "[F]" in output def test_archive_indicator(self): """Archived repos show [A] indicator.""" console, buf = _make_console() repos = [_make_repo(name="old-repo", is_archived=True)] render_dashboard(repos, console=console) output = buf.getvalue() assert "[A]" in output def test_mirror_indicator(self): """Mirror repos show [M] indicator.""" console, buf = _make_console() repos = [_make_repo(name="mirror-repo", is_mirror=True)] render_dashboard(repos, console=console) output = buf.getvalue() assert "[M]" in output def test_multiple_indicators(self): """Repo with multiple flags shows all indicators.""" console, buf = _make_console() repos = [_make_repo(name="special", is_fork=True, is_archived=True)] render_dashboard(repos, console=console) output = buf.getvalue() assert "[F]" in output assert "[A]" in output def test_issues_zero(self): """Repos with 0 issues display 0.""" console, buf = _make_console() repos = [_make_repo(name="clean-repo", open_issues=0)] render_dashboard(repos, console=console) output = buf.getvalue() assert "0" in output def test_multiple_repos(self): """Multiple repos all appear in the output.""" console, buf = _make_console() repos = [ _make_repo(name="repo-alpha", open_issues=1), _make_repo(name="repo-beta", open_issues=5), ] render_dashboard(repos, console=console) output = buf.getvalue() assert "repo-alpha" in output assert "repo-beta" in output class TestRenderDashboardLastCommit: """Test the last commit column rendering.""" def test_last_commit_column_displayed(self): """Column 'Dernier commit' appears in the table.""" console, buf = _make_console() repos = [_make_repo(name="projet", last_commit_date="2026-03-10T14:30:00Z")] render_dashboard(repos, console=console) output = buf.getvalue() assert "Dernier commit" in output def test_last_commit_shows_relative_date(self): """Last commit date is shown as relative date.""" console, buf = _make_console() repos = [_make_repo(name="projet", last_commit_date="2026-03-10T14:30:00Z")] render_dashboard(repos, console=console) output = buf.getvalue() # Should show some relative date (il y a Xj, etc.) assert "il y a" in output or "aujourd'hui" in output def test_last_commit_none_shows_dash(self): """Repo without commit shows dash.""" console, buf = _make_console() repos = [_make_repo(name="vide", last_commit_date=None)] render_dashboard(repos, console=console) output = buf.getvalue() assert "\u2014" in output or "β€”" in output class TestRenderDashboardMilestones: """Test the milestones section rendering.""" def test_milestones_displayed(self): """Repos with milestones show milestone info.""" console, buf = _make_console() repos = [ _make_repo( name="mon-projet", milestones=[ { "title": "v2.0", "open_issues": 2, "closed_issues": 3, "due_on": "2026-04-01T00:00:00Z", }, ], ), ] render_dashboard(repos, console=console) output = buf.getvalue() assert "v2.0" in output assert "3/5" in output # closed/total assert "60%" in output def test_milestone_without_due_date(self): """Milestone without due_on omits the deadline.""" console, buf = _make_console() repos = [ _make_repo( name="projet", milestones=[ { "title": "backlog", "open_issues": 5, "closed_issues": 1, "due_on": None, }, ], ), ] render_dashboard(repos, console=console) output = buf.getvalue() assert "backlog" in output assert "1/6" in output def test_no_milestones_section_when_none(self): """When no repos have milestones, milestone section is absent.""" console, buf = _make_console() repos = [_make_repo(name="simple", milestones=[])] render_dashboard(repos, console=console) output = buf.getvalue() assert "Milestones" not in output class TestRenderDashboardEmpty: """Test empty repo list handling.""" def test_empty_list_shows_message(self): """Empty repo list shows informative message.""" console, buf = _make_console() render_dashboard([], console=console) output = buf.getvalue() assert "Aucun repo" in output class TestDescriptionColumn: """Test Description column in dashboard table.""" def test_description_column_displayed(self): """Table contains a Description column by default.""" console, buf = _make_console() repos = [_make_repo(name="test", description="My project")] render_dashboard(repos, console=console) output = buf.getvalue() assert "Description" in output assert "My project" in output def test_description_truncated_at_40(self): """Description longer than 40 chars is truncated with '...'.""" console, buf = _make_console() long_desc = "A" * 60 repos = [_make_repo(name="test", description=long_desc)] render_dashboard(repos, console=console) output = buf.getvalue() # Should contain first 40 chars + "..." assert "A" * 40 + "..." in output # Should NOT contain the full 60-char string assert "A" * 60 not in output def test_description_short_not_truncated(self): """Description of 20 chars is displayed as-is.""" console, buf = _make_console() repos = [_make_repo(name="test", description="Short description")] render_dashboard(repos, console=console) output = buf.getvalue() assert "Short description" in output def test_description_empty(self): """Empty description renders without crash.""" console, buf = _make_console() repos = [_make_repo(name="test", description="")] render_dashboard(repos, console=console) output = buf.getvalue() assert "test" in output def test_no_description_flag(self): """show_description=False hides the Description column.""" console, buf = _make_console() repos = [_make_repo(name="test", description="My project")] render_dashboard(repos, console=console, show_description=False) output = buf.getvalue() assert "Description" not in output assert "test" in output class TestRenderDashboardEdgeCases: """Test edge cases for dashboard rendering.""" def test_render_dashboard_unicode_description(self): """Repo with unicode description renders without crash.""" console, buf = _make_console() repos = [_make_repo(name="unicode", description="Projet πŸš€ avec accents eaiu δΈ­ζ–‡")] render_dashboard(repos, console=console) output = buf.getvalue() assert "unicode" in output def test_render_dashboard_control_chars_in_name(self): """Repo with control characters in name renders without crash.""" console, buf = _make_console() repos = [_make_repo(name="test\x00repo")] render_dashboard(repos, console=console) output = buf.getvalue() # Rich may strip or display the control char, but must not crash assert "test" in output class TestColorizeMilestoneDue: """Test _colorize_milestone_due function.""" def test_overdue(self): """Past due date returns 'red'.""" from gitea_dashboard.display import _colorize_milestone_due assert _colorize_milestone_due("2020-01-01T00:00:00Z") == "red" def test_soon(self): """Due date within 7 days returns 'yellow'.""" from datetime import datetime, timedelta, timezone from gitea_dashboard.display import _colorize_milestone_due soon = datetime.now(timezone.utc) + timedelta(days=3) assert _colorize_milestone_due(soon.isoformat()) == "yellow" def test_ok(self): """Due date more than 7 days away returns 'green'.""" from datetime import datetime, timedelta, timezone from gitea_dashboard.display import _colorize_milestone_due future = datetime.now(timezone.utc) + timedelta(days=15) assert _colorize_milestone_due(future.isoformat()) == "green" def test_no_due(self): """No due date returns empty string.""" from gitea_dashboard.display import _colorize_milestone_due assert _colorize_milestone_due(None) == "" class TestSortRepos: """Test sort_repos function.""" def test_sort_by_name(self): """Sorts alphabetically by name (case-insensitive).""" repos = [ _make_repo(name="Charlie"), _make_repo(name="alpha"), _make_repo(name="Bravo"), ] result = sort_repos(repos, "name") assert [r.name for r in result] == ["alpha", "Bravo", "Charlie"] def test_sort_by_issues(self): """Sorts by issues count descending.""" repos = [ _make_repo(name="low", open_issues=1), _make_repo(name="high", open_issues=10), _make_repo(name="mid", open_issues=5), ] result = sort_repos(repos, "issues") assert [r.name for r in result] == ["high", "mid", "low"] def test_sort_by_release(self): """Sorts by release date descending; repos without release last.""" repos = [ _make_repo(name="no-rel", latest_release=None), _make_repo( name="old", latest_release={"tag_name": "v1.0", "published_at": "2025-01-01T00:00:00Z"}, ), _make_repo( name="new", latest_release={"tag_name": "v2.0", "published_at": "2026-03-01T00:00:00Z"}, ), ] result = sort_repos(repos, "release") assert [r.name for r in result] == ["new", "old", "no-rel"] def test_sort_by_activity(self): """Sorts by last commit date descending; repos without commit last.""" repos = [ _make_repo(name="inactive", last_commit_date=None), _make_repo(name="old-commit", last_commit_date="2025-06-01T00:00:00Z"), _make_repo(name="recent", last_commit_date="2026-03-10T00:00:00Z"), ] result = sort_repos(repos, "activity") assert [r.name for r in result] == ["recent", "old-commit", "inactive"] class TestRenderMilestones: """Test the dedicated milestones table rendering.""" def test_render_milestones_basic(self): """Milestones table displays expected columns.""" from gitea_dashboard.collector import MilestoneData from gitea_dashboard.display import render_milestones console, buf = _make_console() milestones = [ MilestoneData( repo_name="my-repo", title="v1.0", open_issues=2, closed_issues=8, progress_pct=80, due_on="2026-04-01T00:00:00Z", state="open", ), ] render_milestones(milestones, console=console) output = buf.getvalue() assert "my-repo" in output assert "v1.0" in output assert "80" in output def test_render_milestones_empty(self): """Empty list shows informative message.""" from gitea_dashboard.display import render_milestones console, buf = _make_console() render_milestones([], console=console) output = buf.getvalue() assert "Aucune milestone" in output def test_render_milestones_progress_colors(self): """Progress coloring: green > 80%, yellow 50-80%, red < 50%.""" from gitea_dashboard.collector import MilestoneData from gitea_dashboard.display import render_milestones console, buf = _make_console() milestones = [ MilestoneData("repo", "high", 1, 9, 90, None, "open"), MilestoneData("repo", "mid", 3, 3, 50, None, "open"), MilestoneData("repo", "low", 8, 2, 20, None, "open"), ] render_milestones(milestones, console=console) output = buf.getvalue() # All three should appear without crash assert "high" in output assert "mid" in output assert "low" in output class TestParseColumns: """Test parse_columns function.""" def test_parse_columns_all_default(self): """None returns all columns.""" from gitea_dashboard.display import AVAILABLE_COLUMNS, parse_columns result = parse_columns(None) assert result == list(AVAILABLE_COLUMNS.keys()) def test_parse_columns_inclusion(self): """'name,issues' returns only those columns.""" from gitea_dashboard.display import parse_columns result = parse_columns("name,issues") assert result == ["name", "issues"] def test_parse_columns_exclusion(self): """'-description,-commit' returns all except those.""" from gitea_dashboard.display import AVAILABLE_COLUMNS, parse_columns result = parse_columns("-description,-commit") assert "description" not in result assert "commit" not in result assert len(result) == len(AVAILABLE_COLUMNS) - 2 def test_parse_columns_unknown_raises(self): """Unknown column raises ValueError.""" from gitea_dashboard.display import parse_columns with pytest.raises(ValueError, match="unknown"): parse_columns("unknown") def test_parse_columns_help(self): """'help' returns sentinel list.""" from gitea_dashboard.display import parse_columns result = parse_columns("help") assert result == ["__help__"] def test_parse_columns_no_desc_compat(self): """no_desc=True excludes description column.""" from gitea_dashboard.display import parse_columns result = parse_columns(None, no_desc=True) assert "description" not in result def test_render_dashboard_with_columns(self): """Only specified columns appear in the output.""" from gitea_dashboard.display import render_dashboard console, buf = _make_console() repos = [_make_repo(name="test", open_issues=5)] render_dashboard(repos, console=console, columns=["name", "issues"]) output = buf.getvalue() assert "test" in output assert "Description" not in output assert "Release" not in output