7ce227ddea
- 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>
179 lines
4.7 KiB
Go
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
|
|
}
|