feat(web): Executions table gets real columns and status icons

No cell stacks two facts on separate lines with <br> any more (Run/Repo,
Branch/Commit, Trigger/Actor, Duration/Started, VM/IP-Uptime each get their
own column, 8 -> 13 total). The card is already .card--wide with its own
.table-wrap, so the extra width scrolls horizontally rather than the page.

Status pills carry a check/x/play icon for the three outcomes that are a
real verdict or an in-progress state (success/failure/running); queued,
cancelled, skipped stay plain text. Every pill in the column -- including
the VM-only orphan/running pills on unmatched-VM rows -- is forced to the
same width with left-aligned content instead of a ragged column.
This commit is contained in:
2026-07-25 19:33:27 +02:00
parent c598d0658e
commit 7f2b03029a
5 changed files with 214 additions and 26 deletions
+99
View File
@@ -270,6 +270,102 @@ def test_status_badges(
assert f">{status}<" in resp.text
@pytest.mark.parametrize(
("status", "icon"),
[
("success", "i-check"),
("failure", "i-x"),
],
)
def test_status_badge_carries_a_check_or_x_icon(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, status: str, icon: str
) -> None:
"""A pass/fail outcome should read without needing the badge colour."""
_stub_view(
monkeypatch,
ExecutionsView(
runs=[_execution(status=status)], unmatched_vms=[], source="admin", error=None
),
)
app = create_app(_make_config(tmp_path))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
assert f'href="#{icon}"' in resp.text
def test_status_badge_carries_a_play_icon_for_running(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""In progress is its own state, not a verdict -- but still not bare text."""
_stub_view(
monkeypatch,
ExecutionsView(
runs=[_execution(status="running")], unmatched_vms=[], source="admin", error=None
),
)
app = create_app(_make_config(tmp_path))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
# The card's own header icon is also #i-play -- count, don't just look
# for presence, or this assertion is trivially true either way.
assert resp.text.count('href="#i-play"') == 2
@pytest.mark.parametrize("status", ["queued", "cancelled", "unknown"])
def test_status_badge_has_no_icon_for_non_terminal_states(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, status: str
) -> None:
"""queued/cancelled/unknown are neither a verdict nor in progress -- plain text."""
_stub_view(
monkeypatch,
ExecutionsView(
runs=[_execution(status=status)], unmatched_vms=[], source="admin", error=None
),
)
app = create_app(_make_config(tmp_path))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
assert '<td><span class="badge badge-' in resp.text
assert "#i-check" not in resp.text
assert "#i-x" not in resp.text
assert "#i-x" not in resp.text
def test_no_cell_stacks_two_facts_on_separate_lines(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Run/Repo, Branch/Commit, Trigger/Actor, Duration/Started and VM/IP each
get their own column now -- no cell relies on a <br> to show two facts."""
vm = _vm("Clone_412-1_20260725_115000_linux")
_stub_view(
monkeypatch,
ExecutionsView(
runs=[_execution(vms=[vm])], unmatched_vms=[], source="admin", error=None
),
)
app = create_app(_make_config(tmp_path))
app.dependency_overrides[get_backend] = lambda: _FakeBackend([])
client = TestClient(app)
resp = client.get("/executions")
assert resp.status_code == 200
assert "<br>" not in resp.text
for header in ("Repo", "Trigger", "Actor", "Started", "IP / Uptime"):
assert f"<th>{header}</th>" in resp.text
assert "Simone" in resp.text # actor, now its own column
assert vm.ip in resp.text
def test_empty_state_when_no_runs_and_no_vms(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -305,6 +401,9 @@ def test_unmatched_vm_keeps_its_row_and_remove_button(
assert vm.name in resp.text
assert "370-1" in resp.text # job id parsed out of the clone name
assert "Remove" in resp.text
# Same word, same column as a run's "running" status pill -- same icon.
# The card's own header icon is also #i-play, hence count == 2 not 1.
assert resp.text.count('href="#i-play"') == 2
def test_end_to_end_against_a_stubbed_gitea(
+20
View File
@@ -15,6 +15,7 @@ from pathlib import Path
from ci_orchestrator.operations.vms import ParsedClone
from ci_orchestrator.web.filters import (
clone_job,
cpu_short,
duration,
humanbytes,
localtime,
@@ -23,6 +24,25 @@ from ci_orchestrator.web.filters import (
thousands,
)
# ── cpu_short ────────────────────────────────────────────────────────────
def test_cpu_short_strips_intel_trademark_noise_and_clock_tail() -> None:
assert cpu_short("Intel(R) Core(TM) i9-13900K CPU @ 3.00GHz") == "Intel Core i9-13900K"
def test_cpu_short_strips_amd_core_count_tail() -> None:
assert cpu_short("AMD Ryzen 9 5950X 16-Core Processor") == "AMD Ryzen 9 5950X"
def test_cpu_short_passes_through_an_already_short_name() -> None:
assert cpu_short("Apple M2 Pro") == "Apple M2 Pro"
def test_cpu_short_of_empty_stays_empty_so_the_template_can_hide_the_line() -> None:
assert cpu_short("") == ""
assert cpu_short(None) == ""
# ── humanbytes ───────────────────────────────────────────────────────────