6a318bf0c9
terminal_panel.cpp: - BeginChild con PushStyleColor(ChildBg, negro) + PushStyleColor(Text, gris claro) - PushStyleVar(WindowPadding, 8/6px) para padding terminal real - Input prompt siempre visible cuando readonly=false - Prefijo "$ " antes del InputText (TextUnformatted + SameLine) - BeginDisabled() cuando el shell esta cerrado (en vez de ocultar el widget) - Calculo de child_h reserva exactamente GetFrameHeightWithSpacing+6 para el prompt cpp/tests/e2e/test_terminal_panel_e2e.py (nuevo): - 4 asserts: PNG existe, no todo-blanco, region oscura >= 30%, pixels no-negros >= 0.3% - Lanza primitives_gallery --capture, busca el binario Linux o Windows.exe automaticamente - Skip graceful si no hay GL ni binario (WSL/CI headless) - 4/4 pasan en Linux con LIBGL_ALWAYS_SOFTWARE=1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
226 lines
7.9 KiB
Python
226 lines
7.9 KiB
Python
"""E2E tests for terminal_panel demos in primitives_gallery.
|
|
|
|
Lanza primitives_gallery en modo --capture, captura el demo "terminal_panel"
|
|
como PNG y verifica que la region del terminal tiene fondo oscuro (fix del
|
|
issue 0132: fondo negro + prompt input).
|
|
|
|
Uso desde la raiz del registry:
|
|
python/.venv/bin/python3 -m pytest cpp/tests/e2e/test_terminal_panel_e2e.py -v
|
|
|
|
Requisitos:
|
|
- primitives_gallery compilado (Linux o Windows .exe).
|
|
- WSL2 con interop habilitado para el path Windows.
|
|
- Pillow instalado en el venv del registry (python/.venv).
|
|
|
|
En entornos sin GL (CI headless), el binario sale != 0 y el test se skipea
|
|
automaticamente (SKIP, no FAIL).
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers de localizacion del binario
|
|
# ---------------------------------------------------------------------------
|
|
|
|
REGISTRY_ROOT = Path(__file__).resolve().parents[3] # fn_registry/
|
|
|
|
|
|
def _find_binary() -> Path | None:
|
|
"""Devuelve el primer primitives_gallery encontrado (Linux o Windows)."""
|
|
# Paths fijos conocidos primero.
|
|
candidates = [
|
|
REGISTRY_ROOT / "cpp" / "build" / "apps" / "primitives_gallery" / "primitives_gallery",
|
|
REGISTRY_ROOT / "cpp" / "build" / "linux" / "apps" / "primitives_gallery" / "primitives_gallery",
|
|
REGISTRY_ROOT / "cpp" / "build" / "windows" / "apps" / "primitives_gallery" / "primitives_gallery.exe",
|
|
# Desktop de Windows (deploy anterior)
|
|
Path("/mnt/c/Users/lucas/Desktop/apps/primitives_gallery/primitives_gallery.exe"),
|
|
]
|
|
for p in candidates:
|
|
if p.exists():
|
|
return p
|
|
# Busqueda amplia como fallback.
|
|
for pattern in ("primitives_gallery", "primitives_gallery.exe"):
|
|
for found in (REGISTRY_ROOT / "cpp" / "build").rglob(pattern):
|
|
if found.is_file():
|
|
return found
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture: captura PNG del demo terminal_panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(scope="module")
|
|
def terminal_png(tmp_path_factory) -> Path:
|
|
"""Lanza primitives_gallery --capture y devuelve el PNG generado."""
|
|
binary = _find_binary()
|
|
if binary is None:
|
|
pytest.skip("primitives_gallery binary not found — build it first")
|
|
|
|
out_dir = tmp_path_factory.mktemp("terminal_capture")
|
|
|
|
# En WSL, un .exe Windows necesita invocarse como proceso Windows.
|
|
# En Linux, se invoca directamente con LIBGL_ALWAYS_SOFTWARE=1.
|
|
env = os.environ.copy()
|
|
is_windows_exe = binary.suffix == ".exe"
|
|
|
|
if is_windows_exe:
|
|
# Convertir el out_dir a path Windows via wslpath.
|
|
wslpath_result = subprocess.run(
|
|
["wslpath", "-w", str(out_dir)],
|
|
capture_output=True, text=True
|
|
)
|
|
if wslpath_result.returncode != 0:
|
|
pytest.skip("wslpath not available — can't convert path for Windows exe")
|
|
win_out_dir = wslpath_result.stdout.strip()
|
|
cmd = [str(binary), "--capture", win_out_dir]
|
|
else:
|
|
env["LIBGL_ALWAYS_SOFTWARE"] = "1"
|
|
cmd = [str(binary), "--capture", str(out_dir)]
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
cwd=str(REGISTRY_ROOT),
|
|
timeout=60,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
# Sin GL o sin display — skip en lugar de FAIL.
|
|
pytest.skip(
|
|
f"primitives_gallery --capture exited {result.returncode} "
|
|
f"(no GL context?). stdout: {result.stdout[-200:]} "
|
|
f"stderr: {result.stderr[-200:]}"
|
|
)
|
|
|
|
png_path = out_dir / "terminal_panel.png"
|
|
if not png_path.exists():
|
|
pytest.skip(f"terminal_panel.png not generated in {out_dir}. "
|
|
f"stdout: {result.stdout[-300:]}")
|
|
|
|
return png_path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_terminal_panel_png_exists(terminal_png: Path):
|
|
"""El PNG del demo terminal_panel debe existir despues del capture."""
|
|
assert terminal_png.exists(), f"PNG not found: {terminal_png}"
|
|
assert terminal_png.stat().st_size > 1000, "PNG sospechosamente pequeño"
|
|
|
|
|
|
def test_terminal_panel_not_all_white(terminal_png: Path):
|
|
"""La imagen no debe ser completamente blanca (render vacio)."""
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
pytest.skip("Pillow not installed — run: pip install Pillow")
|
|
|
|
img = Image.open(terminal_png).convert("RGB")
|
|
px = img.load()
|
|
w, h = img.size
|
|
total = w * h
|
|
white_count = sum(
|
|
1
|
|
for y in range(h)
|
|
for x in range(w)
|
|
if px[x, y][0] > 240 and px[x, y][1] > 240 and px[x, y][2] > 240 # type: ignore[index]
|
|
)
|
|
white_ratio = white_count / total
|
|
|
|
assert white_ratio < 0.95, (
|
|
f"Image is {white_ratio:.1%} white — terminal render likely failed. "
|
|
f"({terminal_png})"
|
|
)
|
|
|
|
|
|
def test_terminal_panel_dark_background(terminal_png: Path):
|
|
"""La region central del terminal debe ser mayormente oscura (fondo negro fix 0132)."""
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
pytest.skip("Pillow not installed — run: pip install Pillow")
|
|
|
|
img = Image.open(terminal_png).convert("RGB")
|
|
w, h = img.size
|
|
|
|
# Recortar la region central-inferior (donde vive el scrollback del terminal).
|
|
# El demo header ocupa ~15% superior; el resto deberia ser el area del terminal.
|
|
# Ajustar: top=20%, bottom=85%, left=10%, right=90%.
|
|
left = int(w * 0.10)
|
|
right = int(w * 0.90)
|
|
top = int(h * 0.20)
|
|
bottom = int(h * 0.85)
|
|
|
|
region = img.crop((left, top, right, bottom))
|
|
rw, rh = region.size
|
|
total = rw * rh
|
|
|
|
if total == 0:
|
|
pytest.skip("Crop region empty — image too small?")
|
|
|
|
rpx = region.load()
|
|
# Pixel oscuro: todos los canales RGB < 60.
|
|
dark_count = sum(
|
|
1
|
|
for y in range(rh)
|
|
for x in range(rw)
|
|
if rpx[x, y][0] < 60 and rpx[x, y][1] < 60 and rpx[x, y][2] < 60 # type: ignore[index]
|
|
)
|
|
dark_ratio = dark_count / total
|
|
|
|
assert dark_ratio >= 0.30, (
|
|
f"Terminal region has only {dark_ratio:.1%} dark pixels (expected >= 30%). "
|
|
f"The black background fix (issue 0132) may not be active. "
|
|
f"Region: ({left},{top})-({right},{bottom}) in {w}x{h} image. "
|
|
f"({terminal_png})"
|
|
)
|
|
|
|
|
|
def test_terminal_panel_has_light_text_on_dark(terminal_png: Path):
|
|
"""Debe haber pixels claros (texto/toolbar) sobre fondo oscuro — render activo.
|
|
|
|
En modo --capture el PTY reader es async y puede no entregar output en los
|
|
primeros frames. Verificamos que al menos la toolbar (Clear/Copy/Reset) y el
|
|
borde del child tienen pixels no-negros (> 0.3% de la imagen total), lo que
|
|
confirma que el panel se renderizo.
|
|
"""
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
pytest.skip("Pillow not installed — run: pip install Pillow")
|
|
|
|
img = Image.open(terminal_png).convert("RGB")
|
|
pixels = img.load()
|
|
w, h = img.size
|
|
total = w * h
|
|
|
|
# Contar pixels con al menos un canal > 60 en toda la imagen.
|
|
# Incluye la toolbar (botones), bordes, prompt "$ " y cualquier output.
|
|
light_count = sum(
|
|
1
|
|
for y in range(h)
|
|
for x in range(w)
|
|
if max(pixels[x, y]) > 60 # type: ignore[index]
|
|
)
|
|
light_ratio = light_count / total
|
|
|
|
# Umbral conservador: > 0.3% — basta con que la toolbar sea visible.
|
|
# En modo interactivo con PTY output el ratio sera mucho mayor (> 5%).
|
|
assert light_ratio >= 0.003, (
|
|
f"Image has only {light_ratio:.2%} non-dark pixels — "
|
|
f"terminal panel may not be rendering at all. "
|
|
f"Check that fn_term::render is called and ImGui window is visible. "
|
|
f"({terminal_png})"
|
|
)
|