Files
kanban/backend/column_max_time_test.go
egutierrez 9f4fd85db3 feat(kanban): tiempo maximo por columna con borde rojo (issue 0089)
Adds column-level max time limit. Cards whose time_in_column_ms exceeds
the limit show a red border + red halo. Columns marked as Done never
trigger the visual regardless of the limit (per spec).

Backend:
- Migration 011_column_max_time.sql adds columns.max_time_minutes
  INTEGER NOT NULL DEFAULT 0 (0 = no limit). Aditiva, idempotente.
- Column struct + ColumnPatch + UpdateColumn handle the new field;
  negatives clamp to 0; listing query includes it.
- handleUpdateColumn (PATCH /api/columns/:id) accepts max_time_minutes
  in the JSON body.

Frontend:
- Column TS interface + UpdateColumnInput updated.
- KanbanColumn context menu: new entry "Tiempo maximo" using
  window.prompt for low-friction config; shows current value when >0.
- KanbanCard receives columnOverdue prop calculated from the column
  state and card.time_in_column_ms; renders red border (var
  --mantine-color-red-6) with 2px width + 2px red halo when overdue.
- data-card-id, data-column-overdue, data-locked attributes on the card
  paper element so e2e tests / scripts can query state.

Tests: TestColumnMaxTimeMinutes_Defaults + _Update verify the schema
default, the clamp on negative input, and that updating max_time leaves
other fields untouched.

Visual regression of the red border kept out of automated e2e because
it requires either clock control or real cards aged > N minutes; will be
verified manually after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:57:14 +02:00

83 lines
1.9 KiB
Go

package main
import (
"os"
"path/filepath"
"testing"
)
// Issue 0089: tiempo maximo por columna.
func openTestDB(t *testing.T) *DB {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "test.db")
db, err := openDB(path)
if err != nil {
t.Fatalf("openDB: %v", err)
}
t.Cleanup(func() {
db.Close()
_ = os.Remove(path)
})
return db
}
func TestColumnMaxTimeMinutes_Defaults(t *testing.T) {
db := openTestDB(t)
c, err := db.CreateColumn("col1")
if err != nil {
t.Fatalf("CreateColumn: %v", err)
}
if c.MaxTimeMinutes != 0 {
t.Fatalf("new column max_time_minutes = %d, want 0", c.MaxTimeMinutes)
}
cols, err := db.ListColumns()
if err != nil {
t.Fatalf("ListColumns: %v", err)
}
if len(cols) == 0 || cols[0].MaxTimeMinutes != 0 {
t.Fatalf("listed col max_time_minutes = %d, want 0", cols[0].MaxTimeMinutes)
}
}
func TestColumnMaxTimeMinutes_Update(t *testing.T) {
db := openTestDB(t)
c, _ := db.CreateColumn("c")
v := 30
if err := db.UpdateColumn(c.ID, ColumnPatch{MaxTimeMinutes: &v}); err != nil {
t.Fatalf("UpdateColumn set 30: %v", err)
}
cols, _ := db.ListColumns()
if cols[0].MaxTimeMinutes != 30 {
t.Fatalf("after set max=30 got %d", cols[0].MaxTimeMinutes)
}
// Negative clamps to 0.
neg := -5
if err := db.UpdateColumn(c.ID, ColumnPatch{MaxTimeMinutes: &neg}); err != nil {
t.Fatalf("UpdateColumn neg: %v", err)
}
cols, _ = db.ListColumns()
if cols[0].MaxTimeMinutes != 0 {
t.Fatalf("negative should clamp to 0, got %d", cols[0].MaxTimeMinutes)
}
// Other fields untouched.
w := 555
if err := db.UpdateColumn(c.ID, ColumnPatch{Width: &w}); err != nil {
t.Fatalf("UpdateColumn width: %v", err)
}
cols, _ = db.ListColumns()
if cols[0].MaxTimeMinutes != 0 {
t.Fatalf("max_time should still be 0 after width update, got %d", cols[0].MaxTimeMinutes)
}
if cols[0].Width != 555 {
t.Fatalf("width = %d, want 555", cols[0].Width)
}
}