merge: issue/0023-dashboard-tests — seccion de tests en dashboard TUI
Nueva pantalla Tests en el menu principal del dashboard para ejecutar Go tests, E2E tests (headless/headed) y todos secuencialmente. Reemplaza el "Run Tests" del menu Server por navegacion a la nueva pantalla.
This commit is contained in:
@@ -30,3 +30,4 @@ afectados y notas de implementacion.
|
||||
| 22a | E2E: Infraestructura base | [0022a-e2e-infra.md](completed/0022a-e2e-infra.md) | completado |
|
||||
| 22b | E2E: Auth fixtures y helpers | [0022b-e2e-auth-helpers.md](completed/0022b-e2e-auth-helpers.md) | completado |
|
||||
| 22c | E2E: Tests de agentes + docs | [0022c-e2e-agent-tests.md](completed/0022c-e2e-agent-tests.md) | completado |
|
||||
| 23 | Seccion de tests en dashboard | [0023-dashboard-tests.md](completed/0023-dashboard-tests.md) | completado |
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# 0023 — Seccion de tests en el dashboard
|
||||
|
||||
## Objetivo
|
||||
|
||||
Añadir una opcion "Tests" al menu principal del dashboard TUI que permita ejecutar tests de Go (`go test`) y tests E2E (Playwright) de forma independiente, con salida en tiempo real y resumen de resultados.
|
||||
|
||||
## Contexto
|
||||
|
||||
- El dashboard actual (`cmd/dashboard/`) tiene un "Run Tests" en el menu Server que solo ejecuta `go test -tags goolm ./...`
|
||||
- Los tests E2E existen en `e2e/` y se ejecutan con `./dev-scripts/e2e/run.sh`
|
||||
- No hay forma de ejecutar E2E desde el dashboard ni de elegir que tipo de tests correr
|
||||
- El dashboard sigue el patron pure core (`pkg/tui/`) + impure shell (`shell/tui/adapter.go`)
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
pkg/tui/model.go — nuevo ScreenTests, TestKind, campos de estado
|
||||
pkg/tui/update.go — logica pura para pantalla Tests (navegacion, seleccion)
|
||||
pkg/tui/view.go — render de la pantalla Tests (menu + output)
|
||||
pkg/tui/messages.go — nuevos mensajes: MsgTestsRunning, MsgTestOutput (streaming)
|
||||
shell/tui/adapter.go — nuevos intents: IntentRunGoTests, IntentRunE2ETests, IntentRunAllTests
|
||||
```
|
||||
|
||||
### Patron pure core / impure shell
|
||||
|
||||
- `pkg/tui/` — tipos de pantalla, opciones de menu, logica de navegacion, formateo de output. Todo puro.
|
||||
- `shell/tui/` — ejecucion real de `go test` y `./dev-scripts/e2e/run.sh`. Impuro.
|
||||
- No se necesitan cambios en `agents/`, `tools/`, ni `shell/` fuera de `shell/tui/`.
|
||||
|
||||
## Tareas
|
||||
|
||||
### Fase 1: Menu principal — nueva opcion "Tests"
|
||||
|
||||
- [ ] **1.1** Añadir `ScreenTests` al enum de screens en `pkg/tui/model.go`
|
||||
- [ ] **1.2** Añadir opcion "Tests" al `MainMenuOptions()` (entre "Server" y "Quit")
|
||||
- [ ] **1.3** Manejar seleccion de "Tests" en `updateMainScreen` — navegar a `ScreenTests`
|
||||
|
||||
### Fase 2: Pantalla de tests — menu de seleccion
|
||||
|
||||
- [ ] **2.1** Crear `TestMenuOptions()` en `model.go` con las opciones:
|
||||
- "Go Tests" — `go test -tags goolm -count=1 ./...`
|
||||
- "E2E Tests" — `./dev-scripts/e2e/run.sh`
|
||||
- "E2E Tests (headed)" — `./dev-scripts/e2e/run.sh --headed`
|
||||
- "All Tests" — Go tests + E2E secuencial
|
||||
- [ ] **2.2** Crear `updateTestsScreen` en `update.go` — navegacion y seleccion de tipo de test
|
||||
- [ ] **2.3** Crear `viewTests` en `view.go` — menu con las opciones y ultimo resultado (PASSED/FAILED/no ejecutado)
|
||||
|
||||
### Fase 3: Ejecucion y output
|
||||
|
||||
- [ ] **3.1** Añadir intents nuevos: `IntentRunGoTests`, `IntentRunE2ETests`, `IntentRunAllTests`
|
||||
- [ ] **3.2** Refactorizar el `runTests()` actual del adapter para que sea `runGoTests()`, reutilizable
|
||||
- [ ] **3.3** Implementar `runE2ETests(headed bool)` en el adapter — ejecuta `./dev-scripts/e2e/run.sh [--headed]`
|
||||
- [ ] **3.4** Implementar `runAllTests()` — ejecuta Go tests primero, luego E2E, combina output
|
||||
- [ ] **3.5** Reutilizar `ScreenTestOutput` existente para mostrar resultados (ya tiene scroll y re-run)
|
||||
- [ ] **3.6** Adaptar `updateTestOutput` para que "r" re-ejecute el mismo tipo de test (no siempre Go)
|
||||
|
||||
### Fase 4: Estado y UX
|
||||
|
||||
- [ ] **4.1** Añadir campo `LastTestKind` al Model para saber que re-ejecutar con "r"
|
||||
- [ ] **4.2** Mostrar indicador "Running..." mientras se ejecutan los tests
|
||||
- [ ] **4.3** El boton "0" desde test output vuelve a `ScreenTests` (no a Server)
|
||||
|
||||
### Fase 5: Limpiar intent antiguo
|
||||
|
||||
- [ ] **5.1** Eliminar `IntentRunTests` del menu Server y reemplazar por navegacion a `ScreenTests`
|
||||
- [ ] **5.2** Mantener retrocompatibilidad: "Run Tests" en Server menu ahora navega a la pantalla Tests
|
||||
|
||||
### Fase 6: Tests
|
||||
|
||||
- [ ] **6.1** Tests unitarios para `TestMenuOptions()` — verifica opciones correctas
|
||||
- [ ] **6.2** Tests unitarios para `updateTestsScreen` — navegacion, seleccion, generacion de intents
|
||||
- [ ] **6.3** Tests unitarios para `viewTests` — render correcto con distintos estados
|
||||
- [ ] **6.4** Verificar que `go build -tags goolm ./...` compila
|
||||
|
||||
### Fase 7: Cleanup
|
||||
|
||||
- [ ] **7.1** Actualizar seccion del dashboard en `CLAUDE.md` si es necesario
|
||||
|
||||
---
|
||||
|
||||
## Ejemplo de uso
|
||||
|
||||
```
|
||||
Bot Server Dashboard
|
||||
────────────────────────────────────
|
||||
2 agents (2 running, 0 stopped, 0 disabled)
|
||||
|
||||
Agents Gestionar agentes
|
||||
Server Gestionar launcher unificado
|
||||
> Tests Ejecutar tests
|
||||
Quit Salir
|
||||
|
||||
[enter]
|
||||
|
||||
Tests
|
||||
────────────────────────────────────
|
||||
> Go Tests go test ./...
|
||||
E2E Tests Playwright headless
|
||||
E2E Tests (headed) Playwright con browser
|
||||
All Tests Go + E2E secuencial
|
||||
|
||||
Last run: Go Tests — PASSED
|
||||
|
||||
↑↓ navegar enter ejecutar 0 volver
|
||||
|
||||
[enter en "E2E Tests"]
|
||||
|
||||
Test Results — E2E Tests
|
||||
────────────────────────────────────────────────────────
|
||||
Running tests...
|
||||
|
||||
(output va apareciendo)
|
||||
|
||||
↑↓ scroll r re-ejecutar 0 volver
|
||||
```
|
||||
|
||||
## Decisiones de diseno
|
||||
|
||||
- **Menu separado en vez de submenu de Server**: los tests son una actividad frecuente e independiente del estado del servidor. Merecen acceso directo desde el menu principal.
|
||||
- **Reutilizar ScreenTestOutput**: ya existe toda la logica de scroll, re-run y visualizacion. Solo hay que parametrizar el tipo de test.
|
||||
- **E2E headed como opcion separada**: util para debugging, pero no es el caso comun. Opcion explicita evita flags ocultos.
|
||||
- **"All Tests" secuencial**: Go tests son rapidos, E2E lentos. Ejecutar Go primero permite fail-fast.
|
||||
|
||||
## Prerequisitos
|
||||
|
||||
- Dashboard funcional (ya existe)
|
||||
- E2E tests configurados (`e2e/.env` con credenciales) — si no estan configurados, el E2E fallara con mensaje claro
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **E2E sin configurar**: si `e2e/.env` no existe, el script fallara. Mitigacion: capturar el error y mostrar mensaje descriptivo en el output ("E2E not configured — run ./dev-scripts/e2e/install.sh").
|
||||
- **E2E headed sin display**: en servidores sin X/Wayland, `--headed` fallara. Mitigacion: el error de Playwright es claro, se muestra en el output.
|
||||
@@ -40,6 +40,7 @@ type MsgRebuildDone struct {
|
||||
|
||||
// MsgTestsDone reports the result of running tests.
|
||||
type MsgTestsDone struct {
|
||||
Kind TestKind // which test suite was executed
|
||||
Passed bool
|
||||
Output []string // lines of test output
|
||||
}
|
||||
|
||||
+28
-2
@@ -11,9 +11,21 @@ const (
|
||||
ScreenAgentActions // actions for a selected agent
|
||||
ScreenLogs // tail log output
|
||||
ScreenServer // server-wide process management
|
||||
ScreenTests // test type selection menu
|
||||
ScreenTestOutput // test run output
|
||||
)
|
||||
|
||||
// TestKind identifies which test suite to run.
|
||||
type TestKind int
|
||||
|
||||
const (
|
||||
TestKindNone TestKind = iota
|
||||
TestKindGo // go test -tags goolm -count=1 ./...
|
||||
TestKindE2E // ./dev-scripts/e2e/run.sh
|
||||
TestKindE2EHead // ./dev-scripts/e2e/run.sh --headed
|
||||
TestKindAll // Go tests + E2E sequential
|
||||
)
|
||||
|
||||
// Model is the complete TUI state — pure data.
|
||||
type Model struct {
|
||||
Screen Screen
|
||||
@@ -33,6 +45,9 @@ type Model struct {
|
||||
LauncherMemory string
|
||||
LauncherCPU string
|
||||
LauncherLogSize string
|
||||
|
||||
// Test state
|
||||
LastTestKind TestKind // which test to re-run with "r"
|
||||
}
|
||||
|
||||
// AgentView is a pre-formatted projection of an agent for display.
|
||||
@@ -62,10 +77,21 @@ func MainMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Agents", Desc: "Gestionar agentes"},
|
||||
{Label: "Server", Desc: "Gestionar launcher unificado"},
|
||||
{Label: "Tests", Desc: "Ejecutar tests"},
|
||||
{Label: "Quit", Desc: "Salir"},
|
||||
}
|
||||
}
|
||||
|
||||
// TestMenuOptions returns the available test types.
|
||||
func TestMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Go Tests", Desc: "go test ./..."},
|
||||
{Label: "E2E Tests", Desc: "Playwright headless"},
|
||||
{Label: "E2E Tests (headed)", Desc: "Playwright con browser"},
|
||||
{Label: "All Tests", Desc: "Go + E2E secuencial"},
|
||||
}
|
||||
}
|
||||
|
||||
// ServerMenuOptions returns the available server-wide actions.
|
||||
func ServerMenuOptions(running bool) []MenuOption {
|
||||
if running {
|
||||
@@ -75,13 +101,13 @@ func ServerMenuOptions(running bool) []MenuOption {
|
||||
{Label: "Kill", Desc: "SIGKILL forzado"},
|
||||
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
||||
{Label: "Logs", Desc: "Ver log del launcher"},
|
||||
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||
{Label: "Tests", Desc: "Ir a pantalla de tests"},
|
||||
}
|
||||
}
|
||||
return []MenuOption{
|
||||
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
||||
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
||||
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||
{Label: "Tests", Desc: "Ir a pantalla de tests"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── TestMenuOptions ─────────────────────────────────────────────────────
|
||||
|
||||
func TestTestMenuOptions_Count(t *testing.T) {
|
||||
opts := TestMenuOptions()
|
||||
if len(opts) != 4 {
|
||||
t.Fatalf("expected 4 test menu options, got %d", len(opts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestMenuOptions_Labels(t *testing.T) {
|
||||
opts := TestMenuOptions()
|
||||
expected := []string{"Go Tests", "E2E Tests", "E2E Tests (headed)", "All Tests"}
|
||||
for i, want := range expected {
|
||||
if opts[i].Label != want {
|
||||
t.Errorf("option[%d]: expected label %q, got %q", i, want, opts[i].Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainMenuOptions_IncludesTests(t *testing.T) {
|
||||
opts := MainMenuOptions()
|
||||
found := false
|
||||
for _, opt := range opts {
|
||||
if opt.Label == "Tests" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("MainMenuOptions should include 'Tests'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainMenuOptions_TestsBeforeQuit(t *testing.T) {
|
||||
opts := MainMenuOptions()
|
||||
testsIdx, quitIdx := -1, -1
|
||||
for i, opt := range opts {
|
||||
if opt.Label == "Tests" {
|
||||
testsIdx = i
|
||||
}
|
||||
if opt.Label == "Quit" {
|
||||
quitIdx = i
|
||||
}
|
||||
}
|
||||
if testsIdx < 0 || quitIdx < 0 {
|
||||
t.Fatal("expected both Tests and Quit in menu")
|
||||
}
|
||||
if testsIdx >= quitIdx {
|
||||
t.Errorf("Tests (index %d) should come before Quit (index %d)", testsIdx, quitIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMenuOptions_NoRunTests(t *testing.T) {
|
||||
for _, running := range []bool{true, false} {
|
||||
opts := ServerMenuOptions(running)
|
||||
for _, opt := range opts {
|
||||
if opt.Label == "Run Tests" {
|
||||
t.Errorf("ServerMenuOptions(running=%v) should not have 'Run Tests', found it", running)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMenuOptions_HasTests(t *testing.T) {
|
||||
for _, running := range []bool{true, false} {
|
||||
opts := ServerMenuOptions(running)
|
||||
found := false
|
||||
for _, opt := range opts {
|
||||
if opt.Label == "Tests" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("ServerMenuOptions(running=%v) should have 'Tests'", running)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── updateTestsScreen ───────────────────────────────────────────────────
|
||||
|
||||
func TestUpdateTestsScreen_Navigation(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 0, WindowHeight: 40}
|
||||
|
||||
m, _ = Update(m, KeyMsg{Str: "down"})
|
||||
if m.Cursor != 1 {
|
||||
t.Errorf("expected cursor 1 after down, got %d", m.Cursor)
|
||||
}
|
||||
|
||||
m, _ = Update(m, KeyMsg{Str: "up"})
|
||||
if m.Cursor != 0 {
|
||||
t.Errorf("expected cursor 0 after up, got %d", m.Cursor)
|
||||
}
|
||||
|
||||
// Can't go below 0
|
||||
m, _ = Update(m, KeyMsg{Str: "up"})
|
||||
if m.Cursor != 0 {
|
||||
t.Errorf("expected cursor 0 clamped, got %d", m.Cursor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestsScreen_Back(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 2}
|
||||
m, _ = Update(m, KeyMsg{Str: "0"})
|
||||
if m.Screen != ScreenMain {
|
||||
t.Errorf("expected ScreenMain, got %d", m.Screen)
|
||||
}
|
||||
if m.Cursor != 0 {
|
||||
t.Errorf("expected cursor reset to 0, got %d", m.Cursor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestsScreen_SelectGoTests(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 0}
|
||||
m, intents := Update(m, KeyMsg{Str: "enter"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunGoTests {
|
||||
t.Errorf("expected IntentRunGoTests, got %v", intents)
|
||||
}
|
||||
if m.LastTestKind != TestKindGo {
|
||||
t.Errorf("expected LastTestKind=TestKindGo, got %d", m.LastTestKind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestsScreen_SelectE2ETests(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 1}
|
||||
_, intents := Update(m, KeyMsg{Str: "enter"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunE2ETests {
|
||||
t.Errorf("expected IntentRunE2ETests, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestsScreen_SelectE2EHeaded(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 2}
|
||||
_, intents := Update(m, KeyMsg{Str: "enter"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunE2EHeadTests {
|
||||
t.Errorf("expected IntentRunE2EHeadTests, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestsScreen_SelectAllTests(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 3}
|
||||
_, intents := Update(m, KeyMsg{Str: "enter"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunAllTests {
|
||||
t.Errorf("expected IntentRunAllTests, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
// ── MsgTestsDone ────────────────────────────────────────────────────────
|
||||
|
||||
func TestMsgTestsDone_SetsKindAndStatus(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests}
|
||||
m, _ = Update(m, MsgTestsDone{Kind: TestKindE2E, Passed: true, Output: []string{"ok"}})
|
||||
if m.Screen != ScreenTestOutput {
|
||||
t.Errorf("expected ScreenTestOutput, got %d", m.Screen)
|
||||
}
|
||||
if m.LastTestKind != TestKindE2E {
|
||||
t.Errorf("expected LastTestKind=TestKindE2E, got %d", m.LastTestKind)
|
||||
}
|
||||
if !strings.Contains(m.StatusMsg, "PASSED") {
|
||||
t.Errorf("expected PASSED in status, got %q", m.StatusMsg)
|
||||
}
|
||||
if !strings.Contains(m.StatusMsg, "E2E") {
|
||||
t.Errorf("expected E2E in status, got %q", m.StatusMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgTestsDone_Failed(t *testing.T) {
|
||||
m := Model{}
|
||||
m, _ = Update(m, MsgTestsDone{Kind: TestKindGo, Passed: false, Output: []string{"FAIL"}})
|
||||
if !strings.Contains(m.StatusMsg, "FAILED") {
|
||||
t.Errorf("expected FAILED in status, got %q", m.StatusMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// ── updateTestOutput ────────────────────────────────────────────────────
|
||||
|
||||
func TestUpdateTestOutput_BackGoesToTests(t *testing.T) {
|
||||
m := Model{Screen: ScreenTestOutput, LastTestKind: TestKindGo}
|
||||
m, _ = Update(m, KeyMsg{Str: "0"})
|
||||
if m.Screen != ScreenTests {
|
||||
t.Errorf("expected ScreenTests, got %d", m.Screen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestOutput_RerunUsesLastKind(t *testing.T) {
|
||||
m := Model{Screen: ScreenTestOutput, LastTestKind: TestKindE2E, WindowHeight: 40}
|
||||
_, intents := Update(m, KeyMsg{Str: "r"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunE2ETests {
|
||||
t.Errorf("expected IntentRunE2ETests, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTestOutput_RerunDefaultsToGo(t *testing.T) {
|
||||
m := Model{Screen: ScreenTestOutput, LastTestKind: TestKindNone, WindowHeight: 40}
|
||||
_, intents := Update(m, KeyMsg{Str: "r"})
|
||||
if len(intents) != 1 || intents[0].Kind != IntentRunGoTests {
|
||||
t.Errorf("expected IntentRunGoTests as default, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
// ── viewTests ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestViewTests_ShowsOptions(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 0, WindowWidth: 80, WindowHeight: 40}
|
||||
out := View(m)
|
||||
if !strings.Contains(out, "Go Tests") {
|
||||
t.Error("expected 'Go Tests' in view")
|
||||
}
|
||||
if !strings.Contains(out, "E2E Tests") {
|
||||
t.Error("expected 'E2E Tests' in view")
|
||||
}
|
||||
if !strings.Contains(out, "All Tests") {
|
||||
t.Error("expected 'All Tests' in view")
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewTests_ShowsCursor(t *testing.T) {
|
||||
m := Model{Screen: ScreenTests, Cursor: 1, WindowWidth: 80, WindowHeight: 40}
|
||||
out := View(m)
|
||||
// Cursor on E2E Tests (index 1)
|
||||
lines := strings.Split(out, "\n")
|
||||
foundCursor := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "> ") && strings.Contains(line, "E2E Tests") && !strings.Contains(line, "(headed)") {
|
||||
foundCursor = true
|
||||
}
|
||||
}
|
||||
if !foundCursor {
|
||||
t.Error("expected cursor on E2E Tests")
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewTests_ShowsLastRun(t *testing.T) {
|
||||
m := Model{
|
||||
Screen: ScreenTests,
|
||||
Cursor: 0,
|
||||
WindowWidth: 80,
|
||||
WindowHeight: 40,
|
||||
LastTestKind: TestKindGo,
|
||||
StatusMsg: "Go Tests PASSED",
|
||||
}
|
||||
out := View(m)
|
||||
if !strings.Contains(out, "Last run:") {
|
||||
t.Error("expected 'Last run:' in view")
|
||||
}
|
||||
if !strings.Contains(out, "PASSED") {
|
||||
t.Error("expected PASSED in last run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewTestOutput_ShowsKindInTitle(t *testing.T) {
|
||||
m := Model{
|
||||
Screen: ScreenTestOutput,
|
||||
LastTestKind: TestKindE2E,
|
||||
StatusMsg: "E2E Tests PASSED",
|
||||
LogLines: []string{"ok"},
|
||||
WindowWidth: 80,
|
||||
WindowHeight: 40,
|
||||
}
|
||||
out := View(m)
|
||||
if !strings.Contains(out, "Test Results — E2E Tests") {
|
||||
t.Errorf("expected 'Test Results — E2E Tests' in view, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main menu Tests navigation ──────────────────────────────────────────
|
||||
|
||||
func TestMainMenu_TestsNavigation(t *testing.T) {
|
||||
m := Model{Screen: ScreenMain, Cursor: 2} // Tests is at index 2
|
||||
m, intents := Update(m, KeyMsg{Str: "enter"})
|
||||
if m.Screen != ScreenTests {
|
||||
t.Errorf("expected ScreenTests, got %d", m.Screen)
|
||||
}
|
||||
if len(intents) != 0 {
|
||||
t.Errorf("expected no intents for Tests nav, got %v", intents)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Server menu Tests navigation ────────────────────────────────────────
|
||||
|
||||
func TestServerMenu_TestsNavigation(t *testing.T) {
|
||||
// "Tests" is the last option in both running/stopped menus
|
||||
opts := ServerMenuOptions(false)
|
||||
testsIdx := -1
|
||||
for i, o := range opts {
|
||||
if o.Label == "Tests" {
|
||||
testsIdx = i
|
||||
}
|
||||
}
|
||||
if testsIdx < 0 {
|
||||
t.Fatal("Tests not found in server menu")
|
||||
}
|
||||
|
||||
m := Model{Screen: ScreenServer, Cursor: testsIdx}
|
||||
m, _ = Update(m, KeyMsg{Str: "enter"})
|
||||
if m.Screen != ScreenTests {
|
||||
t.Errorf("expected ScreenTests, got %d", m.Screen)
|
||||
}
|
||||
}
|
||||
|
||||
// ── testKindLabel ───────────────────────────────────────────────────────
|
||||
|
||||
func TestTestKindLabel(t *testing.T) {
|
||||
cases := []struct {
|
||||
kind TestKind
|
||||
want string
|
||||
}{
|
||||
{TestKindGo, "Go Tests"},
|
||||
{TestKindE2E, "E2E Tests"},
|
||||
{TestKindE2EHead, "E2E Tests (headed)"},
|
||||
{TestKindAll, "All Tests"},
|
||||
{TestKindNone, "Tests"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := testKindLabel(tc.kind)
|
||||
if got != tc.want {
|
||||
t.Errorf("testKindLabel(%d) = %q, want %q", tc.kind, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── testKindIntent ──────────────────────────────────────────────────────
|
||||
|
||||
func TestTestKindIntent(t *testing.T) {
|
||||
cases := []struct {
|
||||
kind TestKind
|
||||
want IntentKind
|
||||
}{
|
||||
{TestKindGo, IntentRunGoTests},
|
||||
{TestKindE2E, IntentRunE2ETests},
|
||||
{TestKindE2EHead, IntentRunE2EHeadTests},
|
||||
{TestKindAll, IntentRunAllTests},
|
||||
{TestKindNone, ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := testKindIntent(tc.kind)
|
||||
if got != tc.want {
|
||||
t.Errorf("testKindIntent(%d) = %q, want %q", tc.kind, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
-7
@@ -23,6 +23,10 @@ const (
|
||||
IntentKillLauncher IntentKind = "kill_launcher"
|
||||
IntentRebuildRestart IntentKind = "rebuild_restart"
|
||||
IntentRunTests IntentKind = "run_tests"
|
||||
IntentRunGoTests IntentKind = "run_go_tests"
|
||||
IntentRunE2ETests IntentKind = "run_e2e_tests"
|
||||
IntentRunE2EHeadTests IntentKind = "run_e2e_head_tests"
|
||||
IntentRunAllTests IntentKind = "run_all_tests"
|
||||
)
|
||||
|
||||
// Intent is pure data describing a side effect to execute.
|
||||
@@ -107,10 +111,12 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
|
||||
model.LogLines = m.Output
|
||||
model.LogScroll = 0
|
||||
model.Cursor = 0
|
||||
model.LastTestKind = m.Kind
|
||||
label := testKindLabel(m.Kind)
|
||||
if m.Passed {
|
||||
model.StatusMsg = "Tests PASSED"
|
||||
model.StatusMsg = label + " PASSED"
|
||||
} else {
|
||||
model.StatusMsg = "Tests FAILED"
|
||||
model.StatusMsg = label + " FAILED"
|
||||
}
|
||||
return model, nil
|
||||
|
||||
@@ -143,6 +149,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
||||
return updateLogs(model, key)
|
||||
case ScreenServer:
|
||||
return updateServerScreen(model, key)
|
||||
case ScreenTests:
|
||||
return updateTestsScreen(model, key)
|
||||
case ScreenTestOutput:
|
||||
return updateTestOutput(model, key)
|
||||
}
|
||||
@@ -167,6 +175,11 @@ func updateMainScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
case "Tests":
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, nil
|
||||
case "Quit":
|
||||
return model, []Intent{{Kind: IntentQuit}}
|
||||
}
|
||||
@@ -304,9 +317,11 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
case "Rebuild & Restart":
|
||||
model.StatusMsg = "Building & restarting..."
|
||||
return model, []Intent{{Kind: IntentRebuildRestart}}
|
||||
case "Run Tests":
|
||||
model.StatusMsg = "Running tests..."
|
||||
return model, []Intent{{Kind: IntentRunTests}}
|
||||
case "Tests":
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, nil
|
||||
case "Logs":
|
||||
model.Screen = ScreenLogs
|
||||
model.LogLines = nil
|
||||
@@ -321,7 +336,7 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenServer
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
@@ -332,12 +347,91 @@ func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
|
||||
model.LogScroll = min(model.LogScroll+1, maxScroll)
|
||||
case "r":
|
||||
intent := testKindIntent(model.LastTestKind)
|
||||
if intent == "" {
|
||||
intent = IntentRunGoTests
|
||||
}
|
||||
model.StatusMsg = "Running tests..."
|
||||
return model, []Intent{{Kind: IntentRunTests}}
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
return model, []Intent{{Kind: intent}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func updateTestsScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
opts := TestMenuOptions()
|
||||
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenMain
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
case "up", "k":
|
||||
model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1)
|
||||
case "down", "j":
|
||||
model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1)
|
||||
case "enter":
|
||||
if model.Cursor < len(opts) {
|
||||
return executeTestAction(model, opts[model.Cursor].Label)
|
||||
}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func executeTestAction(model Model, action string) (Model, []Intent) {
|
||||
model.StatusMsg = "Running tests..."
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
switch action {
|
||||
case "Go Tests":
|
||||
model.LastTestKind = TestKindGo
|
||||
return model, []Intent{{Kind: IntentRunGoTests}}
|
||||
case "E2E Tests":
|
||||
model.LastTestKind = TestKindE2E
|
||||
return model, []Intent{{Kind: IntentRunE2ETests}}
|
||||
case "E2E Tests (headed)":
|
||||
model.LastTestKind = TestKindE2EHead
|
||||
return model, []Intent{{Kind: IntentRunE2EHeadTests}}
|
||||
case "All Tests":
|
||||
model.LastTestKind = TestKindAll
|
||||
return model, []Intent{{Kind: IntentRunAllTests}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// testKindIntent maps a TestKind to its corresponding IntentKind.
|
||||
func testKindIntent(k TestKind) IntentKind {
|
||||
switch k {
|
||||
case TestKindGo:
|
||||
return IntentRunGoTests
|
||||
case TestKindE2E:
|
||||
return IntentRunE2ETests
|
||||
case TestKindE2EHead:
|
||||
return IntentRunE2EHeadTests
|
||||
case TestKindAll:
|
||||
return IntentRunAllTests
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// testKindLabel returns a human-readable label for a TestKind.
|
||||
func testKindLabel(k TestKind) string {
|
||||
switch k {
|
||||
case TestKindGo:
|
||||
return "Go Tests"
|
||||
case TestKindE2E:
|
||||
return "E2E Tests"
|
||||
case TestKindE2EHead:
|
||||
return "E2E Tests (headed)"
|
||||
case TestKindAll:
|
||||
return "All Tests"
|
||||
default:
|
||||
return "Tests"
|
||||
}
|
||||
}
|
||||
|
||||
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
func visibleLogLines(m Model) int {
|
||||
|
||||
+38
-1
@@ -18,6 +18,8 @@ func View(model Model) string {
|
||||
return viewLogs(model)
|
||||
case ScreenServer:
|
||||
return viewServer(model)
|
||||
case ScreenTests:
|
||||
return viewTests(model)
|
||||
case ScreenTestOutput:
|
||||
return viewTestOutput(model)
|
||||
default:
|
||||
@@ -238,10 +240,45 @@ func viewServer(m Model) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewTests(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Tests\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 44) + "\n")
|
||||
|
||||
for i, opt := range TestMenuOptions() {
|
||||
cursor := " "
|
||||
if i == m.Cursor {
|
||||
cursor = "> "
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s%-22s %s\n", cursor, opt.Label, opt.Desc))
|
||||
}
|
||||
|
||||
if m.LastTestKind != TestKindNone {
|
||||
b.WriteString(fmt.Sprintf("\n Last run: %s", testKindLabel(m.LastTestKind)))
|
||||
if m.StatusMsg != "" && (strings.HasSuffix(m.StatusMsg, "PASSED") || strings.HasSuffix(m.StatusMsg, "FAILED")) {
|
||||
// Extract result from status
|
||||
if strings.HasSuffix(m.StatusMsg, "PASSED") {
|
||||
b.WriteString(" — PASSED")
|
||||
} else {
|
||||
b.WriteString(" — FAILED")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n ↑↓ navegar enter ejecutar 0 volver\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewTestOutput(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Test Results\n")
|
||||
title := "Test Results"
|
||||
if m.LastTestKind != TestKindNone {
|
||||
title = "Test Results — " + testKindLabel(m.LastTestKind)
|
||||
}
|
||||
b.WriteString("\n " + title + "\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 60) + "\n")
|
||||
|
||||
if m.StatusMsg != "" {
|
||||
|
||||
+78
-5
@@ -59,7 +59,19 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
return a.rebuildRestart()
|
||||
|
||||
case puretui.IntentRunTests:
|
||||
return a.runTests()
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunGoTests:
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunE2ETests:
|
||||
return a.runE2ETests(false)
|
||||
|
||||
case puretui.IntentRunE2EHeadTests:
|
||||
return a.runE2ETests(true)
|
||||
|
||||
case puretui.IntentRunAllTests:
|
||||
return a.runAllTests()
|
||||
|
||||
case puretui.IntentTick:
|
||||
return a.tick()
|
||||
@@ -236,11 +248,10 @@ func (a *Adapter) loadLogs(id string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runTests() tea.Cmd {
|
||||
func (a *Adapter) runGoTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
// Fallback to known location
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
@@ -252,9 +263,71 @@ func (a *Adapter) runTests() tea.Cmd {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
passed := err == nil
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindGo, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
return puretui.MsgTestsDone{Passed: passed, Output: lines}
|
||||
func (a *Adapter) runE2ETests(headed bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
args := []string{"./dev-scripts/e2e/run.sh"}
|
||||
if headed {
|
||||
args = append(args, "--headed")
|
||||
}
|
||||
cmd := exec.Command("bash", args...)
|
||||
cmd.Env = a.mgr.BuildEnv()
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
output := strings.TrimSpace(string(out))
|
||||
if output == "" && err != nil {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
kind := puretui.TestKindE2E
|
||||
if headed {
|
||||
kind = puretui.TestKindE2EHead
|
||||
}
|
||||
return puretui.MsgTestsDone{Kind: kind, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runAllTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var allLines []string
|
||||
|
||||
// Go tests first
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
goCmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
goCmd.Env = a.mgr.BuildEnv()
|
||||
goOut, goErr := goCmd.CombinedOutput()
|
||||
|
||||
allLines = append(allLines, "═══ Go Tests ═══")
|
||||
goOutput := strings.TrimSpace(string(goOut))
|
||||
if goOutput == "" && goErr != nil {
|
||||
goOutput = "Error: " + goErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(goOutput, "\n")...)
|
||||
|
||||
if goErr != nil {
|
||||
allLines = append(allLines, "", "Go tests FAILED — skipping E2E")
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: false, Output: allLines}
|
||||
}
|
||||
|
||||
// E2E tests
|
||||
allLines = append(allLines, "", "═══ E2E Tests ═══")
|
||||
e2eCmd := exec.Command("bash", "./dev-scripts/e2e/run.sh")
|
||||
e2eCmd.Env = a.mgr.BuildEnv()
|
||||
e2eOut, e2eErr := e2eCmd.CombinedOutput()
|
||||
|
||||
e2eOutput := strings.TrimSpace(string(e2eOut))
|
||||
if e2eOutput == "" && e2eErr != nil {
|
||||
e2eOutput = "Error: " + e2eErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(e2eOutput, "\n")...)
|
||||
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: e2eErr == nil, Output: allLines}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user