Files
kanban/e2e/main_test.go
egutierrez 7ce227ddea feat(kanban): deadlines en cards (context menu, badges, calendario, history)
- migration 009 + columna deadline TEXT en cards
- backend: CardPatch.HasDeadline, eventos deadline_set/deadline_cleared
- KanbanCard: menu derecho con DatePicker, badge countdown con colores por ratio (azul>=50%, amarillo<50%, rojo<10%, red.9 overdue)
- App.tsx: filtro "Con deadline", handleSetCardDeadline optimista, jump-to-card + highlight
- CalendarView: popover por dia con seq_num + titulo, click navega a card en tablero
- HistoryModal: render eventos deadline_set/deadline_cleared
- .gitignore: *.log

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:45:36 +02:00

179 lines
4.7 KiB
Go

// Tests e2e contra kanban server (puerto 8095) usando funciones del registry.
// Requiere kanban backend corriendo + Chrome accesible (WSL2 o Linux).
//
// Ejecucion:
// cd e2e && go test -v -tags fts5 ./...
// o: BASE_URL=http://localhost:5180 go test -v ./... (modo dev con Vite)
//
// Variables de entorno:
// BASE_URL — default http://localhost:8095
// KANBAN_USER — default e2e
// KANBAN_PASS — default e2etest
// HEADLESS — "1" para headless. Default "1"
//
// Reusa funciones del registry: chrome_launch, cdp_*. NO duplica logica.
package e2e
import (
"fmt"
"net/http"
"os"
"strings"
"testing"
"time"
"fn-registry/functions/browser"
)
const cdpPort = 9335
type ctx struct {
t *testing.T
conn *browser.CDPConn
chromePID int
baseURL string
}
func envOr(k, dflt string) string {
if v := os.Getenv(k); v != "" {
return v
}
return dflt
}
func setup(t *testing.T) *ctx {
t.Helper()
baseURL := envOr("BASE_URL", "http://localhost:8095")
// Verificar que el backend responde antes de lanzar Chrome.
resp, err := http.Get(baseURL + "/api/board")
if err != nil {
t.Skipf("backend no accesible en %s: %v", baseURL, err)
}
resp.Body.Close()
headless := envOr("HEADLESS", "1") == "1"
pid, err := browser.ChromeLaunch(browser.ChromeLaunchOpts{
Port: cdpPort,
UserDataDir: "/tmp/kanban-e2e-profile",
Headless: headless,
})
if err != nil {
t.Skipf("chrome_launch fallo: %v", err)
}
conn, err := browser.CdpConnect(cdpPort)
if err != nil {
_ = browser.CdpClose(nil, pid)
t.Fatalf("cdp_connect fallo: %v", err)
}
c := &ctx{t: t, conn: conn, chromePID: pid, baseURL: baseURL}
t.Cleanup(func() { _ = browser.CdpClose(c.conn, c.chromePID) })
return c
}
func (c *ctx) navigate(path string) {
c.t.Helper()
if err := browser.CdpNavigate(c.conn, c.baseURL+path); err != nil {
c.t.Fatalf("navigate %s: %v", path, err)
}
if err := browser.CdpWaitLoad(c.conn, 10*time.Second); err != nil {
c.t.Fatalf("wait_load %s: %v", path, err)
}
}
func (c *ctx) screenshot(name string) {
c.t.Helper()
dir := "screenshots"
_ = os.MkdirAll(dir, 0o755)
out := fmt.Sprintf("%s/%s.png", dir, name)
if err := browser.CdpScreenshot(c.conn, out, browser.CdpScreenshotOpts{Format: "png"}); err != nil {
c.t.Logf("screenshot fallo (%s): %v", name, err)
return
}
c.t.Logf("screenshot: %s", out)
}
func (c *ctx) eval(expr string) string {
c.t.Helper()
out, err := browser.CdpEvaluate(c.conn, expr)
if err != nil {
c.t.Fatalf("eval (%s): %v", expr, err)
}
return out
}
// --- Tests ---
func TestE2E_HomeLoads(t *testing.T) {
c := setup(t)
c.navigate("/")
// Login form o board (segun haya sesion previa). Busca cualquier rasgo visible.
if err := browser.CdpWaitElement(c.conn, "body", 5*time.Second); err != nil {
t.Fatalf("body no aparecio: %v", err)
}
html, err := browser.CdpGetHTML(c.conn)
if err != nil {
t.Fatalf("get_html: %v", err)
}
if !strings.Contains(strings.ToLower(html), "kanban") &&
!strings.Contains(strings.ToLower(html), "iniciar") &&
!strings.Contains(strings.ToLower(html), "login") {
t.Errorf("home no contiene rastros esperados (kanban/login). HTML[:200]=%s", html[:min(len(html), 200)])
}
c.screenshot("01_home")
}
func TestE2E_ApiBoardResponds(t *testing.T) {
baseURL := envOr("BASE_URL", "http://localhost:8095")
resp, err := http.Get(baseURL + "/api/board")
if err != nil {
t.Skipf("backend no accesible: %v", err)
}
defer resp.Body.Close()
// 401 (sin sesion) o 200 (sesion activa) — ambos validos.
if resp.StatusCode != 200 && resp.StatusCode != 401 {
t.Errorf("/api/board status inesperado: %d", resp.StatusCode)
}
}
func TestE2E_FlagsEndpoint_DoesNotExist(t *testing.T) {
// Smoke: endpoint /api/me devuelve 401 sin auth (no 5xx).
baseURL := envOr("BASE_URL", "http://localhost:8095")
resp, err := http.Get(baseURL + "/api/me")
if err != nil {
t.Skipf("backend no accesible: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
t.Errorf("/api/me devolvio 5xx: %d", resp.StatusCode)
}
}
func TestE2E_FrontendBundleHasNoConsoleErrors(t *testing.T) {
c := setup(t)
c.navigate("/")
if err := browser.CdpWaitElement(c.conn, "body", 5*time.Second); err != nil {
t.Fatalf("body: %v", err)
}
// Comprueba que no hay errores graves en el DOM.
val := c.eval(`document.querySelectorAll('script[src*="error"]').length`)
if !strings.Contains(val, "0") {
t.Errorf("scripts de error detectados: %s", val)
}
// Verifica que el bundle se cargo (algun script de assets).
val = c.eval(`document.querySelectorAll('script[src*="/assets/"]').length`)
if strings.Contains(val, "0") {
t.Errorf("bundle no cargado: %s", val)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}