// 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 }