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 |
|
| 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 |
|
| 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 |
|
| 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.
|
// MsgTestsDone reports the result of running tests.
|
||||||
type MsgTestsDone struct {
|
type MsgTestsDone struct {
|
||||||
|
Kind TestKind // which test suite was executed
|
||||||
Passed bool
|
Passed bool
|
||||||
Output []string // lines of test output
|
Output []string // lines of test output
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -11,9 +11,21 @@ const (
|
|||||||
ScreenAgentActions // actions for a selected agent
|
ScreenAgentActions // actions for a selected agent
|
||||||
ScreenLogs // tail log output
|
ScreenLogs // tail log output
|
||||||
ScreenServer // server-wide process management
|
ScreenServer // server-wide process management
|
||||||
|
ScreenTests // test type selection menu
|
||||||
ScreenTestOutput // test run output
|
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.
|
// Model is the complete TUI state — pure data.
|
||||||
type Model struct {
|
type Model struct {
|
||||||
Screen Screen
|
Screen Screen
|
||||||
@@ -33,6 +45,9 @@ type Model struct {
|
|||||||
LauncherMemory string
|
LauncherMemory string
|
||||||
LauncherCPU string
|
LauncherCPU string
|
||||||
LauncherLogSize 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.
|
// AgentView is a pre-formatted projection of an agent for display.
|
||||||
@@ -62,10 +77,21 @@ func MainMenuOptions() []MenuOption {
|
|||||||
return []MenuOption{
|
return []MenuOption{
|
||||||
{Label: "Agents", Desc: "Gestionar agentes"},
|
{Label: "Agents", Desc: "Gestionar agentes"},
|
||||||
{Label: "Server", Desc: "Gestionar launcher unificado"},
|
{Label: "Server", Desc: "Gestionar launcher unificado"},
|
||||||
|
{Label: "Tests", Desc: "Ejecutar tests"},
|
||||||
{Label: "Quit", Desc: "Salir"},
|
{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.
|
// ServerMenuOptions returns the available server-wide actions.
|
||||||
func ServerMenuOptions(running bool) []MenuOption {
|
func ServerMenuOptions(running bool) []MenuOption {
|
||||||
if running {
|
if running {
|
||||||
@@ -75,13 +101,13 @@ func ServerMenuOptions(running bool) []MenuOption {
|
|||||||
{Label: "Kill", Desc: "SIGKILL forzado"},
|
{Label: "Kill", Desc: "SIGKILL forzado"},
|
||||||
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
||||||
{Label: "Logs", Desc: "Ver log del launcher"},
|
{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{
|
return []MenuOption{
|
||||||
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
||||||
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
{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"
|
IntentKillLauncher IntentKind = "kill_launcher"
|
||||||
IntentRebuildRestart IntentKind = "rebuild_restart"
|
IntentRebuildRestart IntentKind = "rebuild_restart"
|
||||||
IntentRunTests IntentKind = "run_tests"
|
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.
|
// 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.LogLines = m.Output
|
||||||
model.LogScroll = 0
|
model.LogScroll = 0
|
||||||
model.Cursor = 0
|
model.Cursor = 0
|
||||||
|
model.LastTestKind = m.Kind
|
||||||
|
label := testKindLabel(m.Kind)
|
||||||
if m.Passed {
|
if m.Passed {
|
||||||
model.StatusMsg = "Tests PASSED"
|
model.StatusMsg = label + " PASSED"
|
||||||
} else {
|
} else {
|
||||||
model.StatusMsg = "Tests FAILED"
|
model.StatusMsg = label + " FAILED"
|
||||||
}
|
}
|
||||||
return model, nil
|
return model, nil
|
||||||
|
|
||||||
@@ -143,6 +149,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
|||||||
return updateLogs(model, key)
|
return updateLogs(model, key)
|
||||||
case ScreenServer:
|
case ScreenServer:
|
||||||
return updateServerScreen(model, key)
|
return updateServerScreen(model, key)
|
||||||
|
case ScreenTests:
|
||||||
|
return updateTestsScreen(model, key)
|
||||||
case ScreenTestOutput:
|
case ScreenTestOutput:
|
||||||
return updateTestOutput(model, key)
|
return updateTestOutput(model, key)
|
||||||
}
|
}
|
||||||
@@ -167,6 +175,11 @@ func updateMainScreen(model Model, key KeyMsg) (Model, []Intent) {
|
|||||||
model.Cursor = 0
|
model.Cursor = 0
|
||||||
model.StatusMsg = ""
|
model.StatusMsg = ""
|
||||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||||
|
case "Tests":
|
||||||
|
model.Screen = ScreenTests
|
||||||
|
model.Cursor = 0
|
||||||
|
model.StatusMsg = ""
|
||||||
|
return model, nil
|
||||||
case "Quit":
|
case "Quit":
|
||||||
return model, []Intent{{Kind: IntentQuit}}
|
return model, []Intent{{Kind: IntentQuit}}
|
||||||
}
|
}
|
||||||
@@ -304,9 +317,11 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
|||||||
case "Rebuild & Restart":
|
case "Rebuild & Restart":
|
||||||
model.StatusMsg = "Building & restarting..."
|
model.StatusMsg = "Building & restarting..."
|
||||||
return model, []Intent{{Kind: IntentRebuildRestart}}
|
return model, []Intent{{Kind: IntentRebuildRestart}}
|
||||||
case "Run Tests":
|
case "Tests":
|
||||||
model.StatusMsg = "Running tests..."
|
model.Screen = ScreenTests
|
||||||
return model, []Intent{{Kind: IntentRunTests}}
|
model.Cursor = 0
|
||||||
|
model.StatusMsg = ""
|
||||||
|
return model, nil
|
||||||
case "Logs":
|
case "Logs":
|
||||||
model.Screen = ScreenLogs
|
model.Screen = ScreenLogs
|
||||||
model.LogLines = nil
|
model.LogLines = nil
|
||||||
@@ -321,7 +336,7 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
|||||||
func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||||
switch key.Str {
|
switch key.Str {
|
||||||
case "0":
|
case "0":
|
||||||
model.Screen = ScreenServer
|
model.Screen = ScreenTests
|
||||||
model.Cursor = 0
|
model.Cursor = 0
|
||||||
model.LogLines = nil
|
model.LogLines = nil
|
||||||
model.LogScroll = 0
|
model.LogScroll = 0
|
||||||
@@ -332,12 +347,91 @@ func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
|||||||
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
|
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
|
||||||
model.LogScroll = min(model.LogScroll+1, maxScroll)
|
model.LogScroll = min(model.LogScroll+1, maxScroll)
|
||||||
case "r":
|
case "r":
|
||||||
|
intent := testKindIntent(model.LastTestKind)
|
||||||
|
if intent == "" {
|
||||||
|
intent = IntentRunGoTests
|
||||||
|
}
|
||||||
model.StatusMsg = "Running tests..."
|
model.StatusMsg = "Running tests..."
|
||||||
return model, []Intent{{Kind: IntentRunTests}}
|
model.LogLines = nil
|
||||||
|
model.LogScroll = 0
|
||||||
|
return model, []Intent{{Kind: intent}}
|
||||||
}
|
}
|
||||||
return model, nil
|
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 ─────────────────────────────────────────────────────────
|
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func visibleLogLines(m Model) int {
|
func visibleLogLines(m Model) int {
|
||||||
|
|||||||
+38
-1
@@ -18,6 +18,8 @@ func View(model Model) string {
|
|||||||
return viewLogs(model)
|
return viewLogs(model)
|
||||||
case ScreenServer:
|
case ScreenServer:
|
||||||
return viewServer(model)
|
return viewServer(model)
|
||||||
|
case ScreenTests:
|
||||||
|
return viewTests(model)
|
||||||
case ScreenTestOutput:
|
case ScreenTestOutput:
|
||||||
return viewTestOutput(model)
|
return viewTestOutput(model)
|
||||||
default:
|
default:
|
||||||
@@ -238,10 +240,45 @@ func viewServer(m Model) string {
|
|||||||
return b.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 {
|
func viewTestOutput(m Model) string {
|
||||||
var b strings.Builder
|
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")
|
b.WriteString(" " + strings.Repeat("─", 60) + "\n")
|
||||||
|
|
||||||
if m.StatusMsg != "" {
|
if m.StatusMsg != "" {
|
||||||
|
|||||||
+78
-5
@@ -59,7 +59,19 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
|||||||
return a.rebuildRestart()
|
return a.rebuildRestart()
|
||||||
|
|
||||||
case puretui.IntentRunTests:
|
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:
|
case puretui.IntentTick:
|
||||||
return a.tick()
|
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 {
|
return func() tea.Msg {
|
||||||
goBin, err := exec.LookPath("go")
|
goBin, err := exec.LookPath("go")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to known location
|
|
||||||
goBin = "/usr/local/go/bin/go"
|
goBin = "/usr/local/go/bin/go"
|
||||||
}
|
}
|
||||||
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||||
@@ -252,9 +263,71 @@ func (a *Adapter) runTests() tea.Cmd {
|
|||||||
output = "Error: " + err.Error()
|
output = "Error: " + err.Error()
|
||||||
}
|
}
|
||||||
lines := strings.Split(output, "\n")
|
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