package services // Pure-Go validator tests for LayoutSpec. No DB required. import ( "encoding/json" "errors" "testing" ) func TestDefaultLayoutSpec_IsValid(t *testing.T) { if err := DefaultLayoutSpec().Validate(); err != nil { t.Fatalf("DefaultLayoutSpec invalid: %v", err) } } func TestLayoutSpec_RejectsEmpty(t *testing.T) { s := LayoutSpec{ Density: CardDensityRoomy, GridColumns: GridAuto, } err := s.Validate() if !errors.Is(err, ErrInvalidInput) { t.Fatalf("empty facts: got %v, want ErrInvalidInput", err) } } func TestLayoutSpec_RequiresTitleRowFirst(t *testing.T) { s := LayoutSpec{ Facts: []LayoutFact{ {Key: FactTypeChip, Visible: true}, {Key: FactTitleRow, Visible: true}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("title-row not first: got %v, want ErrInvalidInput", err) } } func TestLayoutSpec_AllowsTitleRowAfterHiddenLeading(t *testing.T) { // type-chip is hidden so it doesn't count; title-row is the first VISIBLE. s := LayoutSpec{ Facts: []LayoutFact{ {Key: FactTypeChip, Visible: false}, {Key: FactTitleRow, Visible: true}, {Key: FactStatusChip, Visible: true}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); err != nil { t.Fatalf("title-row first-visible should be ok; got %v", err) } } func TestLayoutSpec_RejectsDuplicateKey(t *testing.T) { s := LayoutSpec{ Facts: []LayoutFact{ {Key: FactTitleRow, Visible: true}, {Key: FactTypeChip, Visible: true}, {Key: FactTypeChip, Visible: true}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("duplicate key: got %v, want ErrInvalidInput", err) } } func TestLayoutSpec_RejectsUnknownKey(t *testing.T) { s := LayoutSpec{ Facts: []LayoutFact{ {Key: FactTitleRow, Visible: true}, {Key: "made-up-fact", Visible: true}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("unknown key: got %v, want ErrInvalidInput", err) } } func TestLayoutSpec_CountBoundsAndPlacement(t *testing.T) { bad := 7 s := LayoutSpec{ Facts: []LayoutFact{ {Key: FactTitleRow, Visible: true}, {Key: FactNextEvents, Visible: true, Count: &bad}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("count out-of-range: got %v, want ErrInvalidInput", err) } // count on a key that doesn't accept it. good := 3 s = LayoutSpec{ Facts: []LayoutFact{ {Key: FactTitleRow, Visible: true}, {Key: FactStatusChip, Visible: true, Count: &good}, }, Density: CardDensityRoomy, GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("count on wrong key: got %v, want ErrInvalidInput", err) } } func TestLayoutSpec_DensityAndGrid(t *testing.T) { s := LayoutSpec{ Facts: []LayoutFact{{Key: FactTitleRow, Visible: true}}, Density: "spacious", // invalid GridColumns: GridAuto, } if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("invalid density: got %v, want ErrInvalidInput", err) } s.Density = CardDensityRoomy s.GridColumns = "5" // invalid if err := s.Validate(); !errors.Is(err, ErrInvalidInput) { t.Fatalf("invalid grid: got %v, want ErrInvalidInput", err) } } func TestParseLayoutSpec_RoundTrip(t *testing.T) { in := DefaultLayoutSpec() bytes := mustJSON(t, in) out, err := ParseLayoutSpec(bytes) if err != nil { t.Fatalf("parse: %v", err) } if len(out.Facts) != len(in.Facts) { t.Errorf("facts len = %d, want %d", len(out.Facts), len(in.Facts)) } if out.Density != in.Density || out.GridColumns != in.GridColumns { t.Errorf("density/grid drift: out=(%s, %s) in=(%s, %s)", out.Density, out.GridColumns, in.Density, in.GridColumns) } } func mustJSON(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } return b }