Files
fn_registry/.claude/commands/e2e-cpp.md
T
egutierrez 516db8efc0 feat(infra): auto-commit con 10 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 16:56:53 +02:00

9.0 KiB

/e2e-cpp — Crear/ejecutar tests e2e para apps C++

Genera y corre tests e2e con Dear ImGui Test Engine sobre las apps C++ del registry. Cada app gana un ejecutable <app>_tests que reabre la app dentro de un harness de testing y ejecuta scripts de UI (clicks, escritura, asserts) sobre los componentes ImGui.

Suite ya instalada en cpp/vendor/imgui_test_engine/. Integracion en framework: fn::run_app_test() (ver cpp/framework/app_base.h). Opt-in via -DFN_BUILD_TESTS=ON. Sin la opcion los builds normales de /compile no cambian.

Argumento

$ARGUMENTS — formato libre. Casos:

  • <app_name> — solo el nombre. Si la app ya tiene tests, los ejecuta. Si no, pide al usuario que describa el flujo a testear.
  • <app_name> <descripcion del flujo> — genera un test nuevo para ese flujo y lo ejecuta. Ej: chart_demo abrir cada tab y verificar que renderiza.
  • vacio — detectar app desde pwd (si estas en cpp/apps/<X>/ o projects/*/apps/<X>/); si no, listar apps disponibles.

Pasos

1. Resolver app y directorio

ROOT=$HOME/fn_registry
ARGS="$ARGUMENTS"
APP_ARG="${ARGS%% *}"   # primera palabra
FLOW_DESC="${ARGS#* }"  # resto (puede coincidir con APP_ARG si solo hay una palabra)
[ "$FLOW_DESC" = "$APP_ARG" ] && FLOW_DESC=""

# Detectar desde CWD si no hay arg
if [ -z "$APP_ARG" ]; then
  CWD="$(pwd)"
  case "$CWD" in
    "$ROOT"/cpp/apps/*|"$ROOT"/projects/*/apps/*)
      APP_ARG="$(basename "$CWD")" ;;
  esac
fi

if [ -z "$APP_ARG" ]; then
  echo "Apps C++ disponibles:"
  ls "$ROOT"/cpp/apps/ 2>/dev/null
  ls "$ROOT"/projects/*/apps/ 2>/dev/null
  echo "Uso: /e2e-cpp <app> [descripcion del flujo]"
  exit 1
fi

APP_DIR=""
for cand in "$ROOT/cpp/apps/$APP_ARG" "$ROOT"/projects/*/apps/"$APP_ARG"; do
  [ -d "$cand" ] && [ -f "$cand/CMakeLists.txt" ] && APP_DIR="$cand" && break
done
[ -z "$APP_DIR" ] && { echo "App C++ no encontrada: $APP_ARG"; exit 1; }
echo "App: $APP_ARG"
echo "Dir: $APP_DIR"

2. Inspeccionar la app

Lee:

  • $APP_DIR/main.cpp — identifica:
    • El nombre de la funcion principal de render (suele ser render() o static void render()).
    • El window title que aparece en ImGui::Begin("...") — sera el primer arg de ctx->SetRef("...") en los tests. Si tiene em-dash u otros UTF-8 no ASCII, anotar la secuencia de bytes (ej: \xe2\x80\x94 para ).
    • Los IDs/labels de los widgets candidatos: tabs (BeginTabItem), botones (Button), inputs (InputText), checkboxes, etc.
  • $APP_DIR/app.md — para entender el dominio y proposito.
  • $APP_DIR/CMakeLists.txt — para saber que .cpp del registry enlaza la app (los tests linkearan los mismos).

3. Decidir tests a escribir

Si $FLOW_DESC esta vacio: pregunta al usuario que flujo testear. Sugiere 2-3 candidatos basados en los widgets vistos en main.cpp. NO inventes flujos sin confirmacion.

Si $FLOW_DESC viene en el comando: convierte la descripcion en una secuencia de pasos atomicos del Test Context API. Ejemplos canonicos:

Descripcion humano Llamada Test Engine
"abrir tab X" ctx->ItemClick("##tabs/X") o el path real del TabBar
"escribir 'hola' en el input search" ctx->ItemInput("Search", "hola")
"click boton Aceptar" ctx->ItemClick("Aceptar")
"verificar que aparece el modal Y" IM_CHECK(ctx->WindowInfo("Y").ID != 0)
"checkbox Z marcado" IM_CHECK(ctx->ItemIsChecked("Z"))
"menu File > Open" ctx->MenuClick("File/Open")

Ver cpp/vendor/imgui_test_engine/imgui_te_context.h para el catalogo completo de helpers.

4. Preparar la app para tests (idempotente)

Si es la primera vez que la app gana tests, hay que:

a) Hacer la funcion render() linkable desde otra TU

// Antes:    static void render() { ... }
// Despues:  void render() { ... }

b) Excluir int main() con guarda FN_TEST_BUILD

#ifndef FN_TEST_BUILD
int main() {
    return fn::run_app({...}, render);
}
#endif

Verifica con grep -n "FN_TEST_BUILD\|^static void render" "$APP_DIR/main.cpp". Si ya esta, no toques nada.

5. Generar/extender el archivo de tests

$APP_DIR/tests/<app>_tests.cpp — un solo archivo por app, varias IM_REGISTER_TEST dentro de register_tests().

Plantilla:

// E2E tests para <app> — Dear ImGui Test Engine.
// Construido solo con -DFN_BUILD_TESTS=ON. Reusa el mismo main.cpp con
// FN_TEST_BUILD definido para excluir su int main().

#include "app_base.h"
#include "imgui.h"
#include "imgui_te_engine.h"
#include "imgui_te_context.h"

void render();   // definido en <app>/main.cpp

static void register_tests(ImGuiTestEngine* e) {
    ImGuiTest* t = nullptr;

    t = IM_REGISTER_TEST(e, "<app>", "<test_name>");
    t->TestFunc = [](ImGuiTestContext* ctx) {
        ctx->SetRef("<window_title_exacto>");
        // ... pasos del flujo
    };

    // mas tests aqui
}

int main() {
    fn::AppConfig cfg{};
    cfg.title  = "<app>_tests";
    cfg.width  = 1280;
    cfg.height = 800;
    return fn::run_app_test(cfg, render, register_tests);
}

Si el archivo ya existe: AGREGA un nuevo IM_REGISTER_TEST dentro de la funcion register_tests existente. NO sobreescribas tests previos.

6. Actualizar CMakeLists.txt (idempotente)

Si $APP_DIR/CMakeLists.txt no tiene aun el bloque de tests, agregar al final:

# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
if(FN_BUILD_TESTS)
    add_imgui_app(<app>_tests
        main.cpp
        tests/<app>_tests.cpp
        # mismos .cpp del registry que la app principal
        ${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp
        ...
    )
    target_compile_definitions(<app>_tests PRIVATE FN_TEST_BUILD)
endif()

Las fuentes deben replicar las del target principal (mismas funciones del registry). Si la app ya tiene un bloque if(FN_BUILD_TESTS), no lo dupliques.

7. Build

cd "$ROOT/cpp"
cmake -S . -B build/linux_tests -DFN_BUILD_TESTS=ON 2>&1 | tail -5
cmake --build build/linux_tests --target ${APP_ARG}_tests -j4 2>&1 | tail -20

Si el build falla:

  • Errores de compilacion en tests/...cpp → revisa nombres de widgets/paths con el codigo real de main.cpp.
  • "undefined reference to render" → falta quitar static o falta el #ifndef FN_TEST_BUILD en main.cpp.
  • "multiple definition of main" → falta el target_compile_definitions(... FN_TEST_BUILD) en CMakeLists.

8. Ejecutar (headless preferente — sin parpadeo)

fn::run_app_test crea la ventana GLFW oculta por defecto (GLFW_VISIBLE=FALSE, ver cpp/framework/app_base.cpp). El contexto GL real se crea igual, así que el render que ejercita el Test Engine es fiel, pero la ventana nunca se mapea en pantalla: cero parpadeo, no roba foco. Por eso los tests de frontend C++ corren headless por defecto, sin tocar el código de cada app.

Dos formas de lanzar, según el entorno:

cd "$ROOT/cpp/build/linux_tests"
TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)"
[ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; }

if [ -n "$DISPLAY" ] && command -v glxinfo >/dev/null 2>&1 \
   && glxinfo 2>/dev/null | grep -q "OpenGL core profile version"; then
  # Host con GL nativo (PC enmanuel, X11 + GPU): binario directo.
  # La ventana ya nace oculta -> sin parpadeo, y usa la GPU real (rapido).
  timeout 90 "$TEST_BIN" 2>&1
else
  # CI / WSL sin GLX 4.3 nativo: display virtual en RAM + software Mesa.
  timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \
      env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
      "$TEST_BIN" 2>&1
fi
EXIT=$?
echo "EXIT: $EXIT"

Ambas vías son headless. xvfb-run sigue siendo seguro en host con display (corre en su propio display virtual), así que si el sniff de GL falla puedes usar siempre la rama xvfb.

Para depurar un test a ojo (ver la UI mientras el engine la maneja), desactiva el headless con FN_HEADLESS=0:

FN_HEADLESS=0 timeout 90 "$TEST_BIN" 2>&1

9. Reportar

  • Si EXIT == 0 y la salida contiene Tests Result: OK → reporta N/M tests passed con la lista de tests ejecutados.
  • Si EXIT != 0 → muestra el bloque de log del test fallido (test engine imprime el path del widget que no encontro, el archivo y la linea del IM_CHECK que fallo). Sugiere correcciones (widget renombrado, path mal escrito, race entre frames — usar ctx->Yield()).

10. Despues de añadir tests

NO ejecutes fn index automaticamente — los tests no son funciones del registry, son artefactos de la app. Si el usuario los queria persistir, ya los tiene en <app_dir>/tests/.

Si la app es un sub-repo (lo normal segun ADR 0002), recordar al usuario que los archivos nuevos viven dentro del repo de la app y necesitan un commit alli (no en fn_registry).

Referencias

  • API de Test Context: cpp/vendor/imgui_test_engine/imgui_te_context.h
  • API del engine: cpp/vendor/imgui_test_engine/imgui_te_engine.h
  • Implementacion del harness: cpp/framework/app_base.cpp (funcion fn::run_app_test)
  • Ejemplo canonico: cpp/apps/chart_demo/tests/chart_demo_tests.cpp
  • Licencia del test engine: personal/open-source gratis (cpp/vendor/imgui_test_engine/LICENSE.txt)