package excelext import ( "bytes" "errors" "testing" "git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/tst" "github.com/xuri/excelize/v2" ) type testRow struct { Name string Age int Score float64 } func openBytes(t *testing.T, data []byte) *excelize.File { t.Helper() f, err := excelize.OpenReader(bytes.NewReader(data)) if err != nil { t.Fatalf("failed to open xlsx bytes: %v", err) } return f } func cellValue(t *testing.T, f *excelize.File, sheet, axis string) string { t.Helper() v, err := f.GetCellValue(sheet, axis) if err != nil { t.Fatalf("GetCellValue(%s, %s) failed: %v", sheet, axis, err) } return v } func TestNewExcelMapper(t *testing.T) { em, err := NewExcelMapper[testRow]() tst.AssertNoErr(t, err) if em == nil { t.Fatal("expected non-nil mapper") } tst.AssertEqual(t, em.SkipColumnHeader, false) tst.AssertEqual(t, len(em.colDefinitions), 0) tst.AssertEqual(t, len(em.wsHeader), 0) tst.AssertEqual(t, len(em.colFilter), 0) if em.StyleDate != nil || em.StyleHeader != nil { t.Errorf("expected styles to be nil before init") } } func TestInitNewFileAndStyles(t *testing.T) { em, _ := NewExcelMapper[testRow]() f, err := em.InitNewFile("Sheet-Foo") tst.AssertNoErr(t, err) if f == nil { t.Fatal("expected non-nil file") } sheets := f.GetSheetList() tst.AssertEqual(t, len(sheets), 1) tst.AssertEqual(t, sheets[0], "Sheet-Foo") if em.StyleDate == nil || em.StyleDatetime == nil || em.StyleEUR == nil || em.StylePercentage == nil || em.StyleHeader == nil || em.StyleWSHeader == nil { t.Errorf("expected all styles to be initialized") } } func TestAddColumn(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) em.AddColumn("Age", nil, langext.Ptr(12.0), func(r testRow) any { return r.Age }) tst.AssertEqual(t, len(em.colDefinitions), 2) tst.AssertEqual(t, em.colDefinitions[0].header, "Name") tst.AssertEqual(t, em.colDefinitions[1].header, "Age") if em.colDefinitions[1].width == nil || *em.colDefinitions[1].width != 12.0 { t.Errorf("expected width 12.0") } val, err := em.colDefinitions[0].fn(testRow{Name: "Alice"}) tst.AssertNoErr(t, err) tst.AssertEqual(t, val.(string), "Alice") } func TestAddColumnErr(t *testing.T) { em, _ := NewExcelMapper[testRow]() sentinel := errors.New("boom") em.AddColumnErr("X", nil, nil, func(r testRow) (any, error) { if r.Age < 0 { return nil, sentinel } return r.Age, nil }) tst.AssertEqual(t, len(em.colDefinitions), 1) v, err := em.colDefinitions[0].fn(testRow{Age: 5}) tst.AssertNoErr(t, err) tst.AssertEqual(t, v.(int), 5) _, err = em.colDefinitions[0].fn(testRow{Age: -1}) if !errors.Is(err, sentinel) { t.Errorf("expected sentinel error, got %v", err) } } func TestAddWorksheetHeader(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddWorksheetHeader("Title 1", nil) em.AddWorksheetHeader("Title 2", langext.Ptr(7)) tst.AssertEqual(t, len(em.wsHeader), 2) tst.AssertEqual(t, em.wsHeader[0].V1, "Title 1") tst.AssertEqual(t, em.wsHeader[1].V1, "Title 2") if em.wsHeader[1].V2 == nil || *em.wsHeader[1].V2 != 7 { t.Errorf("expected style ptr 7") } if em.wsHeader[0].V2 != nil { t.Errorf("expected nil style for first header") } } func TestAddFilter(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddFilter(func(v testRow) bool { return v.Age >= 18 }) em.AddFilter(func(v testRow) bool { return v.Score > 0 }) tst.AssertEqual(t, len(em.colFilter), 2) } func TestBuildBasic(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) em.AddColumn("Age", nil, nil, func(r testRow) any { return r.Age }) rows := []testRow{ {Name: "Alice", Age: 30}, {Name: "Bob", Age: 25}, } data, err := em.Build("Sheet1", rows) tst.AssertNoErr(t, err) if len(data) == 0 { t.Fatal("expected non-empty xlsx output") } f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A1"), "Name") tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B1"), "Age") tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A2"), "Alice") tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B2"), "30") tst.AssertEqual(t, cellValue(t, f, "Sheet1", "A3"), "Bob") tst.AssertEqual(t, cellValue(t, f, "Sheet1", "B3"), "25") } func TestBuildSkipColumnHeader(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.SkipColumnHeader = true em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) rows := []testRow{{Name: "Alice"}, {Name: "Bob"}} data, err := em.Build("Data", rows) tst.AssertNoErr(t, err) f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "Data", "A1"), "Alice") tst.AssertEqual(t, cellValue(t, f, "Data", "A2"), "Bob") } func TestBuildWithFilter(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) em.AddFilter(func(v testRow) bool { return v.Age >= 18 }) rows := []testRow{ {Name: "Alice", Age: 30}, {Name: "Charlie", Age: 12}, {Name: "Bob", Age: 25}, } data, err := em.Build("S", rows) tst.AssertNoErr(t, err) f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "Name") tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "Alice") tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "Bob") tst.AssertEqual(t, cellValue(t, f, "S", "A4"), "") } func TestBuildWithWorksheetHeader(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddWorksheetHeader("My Big Title", nil) em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) em.AddColumn("Age", nil, nil, func(r testRow) any { return r.Age }) rows := []testRow{{Name: "Alice", Age: 30}} data, err := em.Build("S", rows) tst.AssertNoErr(t, err) f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "My Big Title") tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "Name") tst.AssertEqual(t, cellValue(t, f, "S", "B3"), "Age") tst.AssertEqual(t, cellValue(t, f, "S", "A4"), "Alice") tst.AssertEqual(t, cellValue(t, f, "S", "B4"), "30") } func TestBuildHandlesNilPointer(t *testing.T) { type ptrRow struct { Name *string } em, _ := NewExcelMapper[ptrRow]() em.AddColumn("Name", nil, nil, func(r ptrRow) any { return r.Name }) name := "Alice" rows := []ptrRow{ {Name: &name}, {Name: nil}, } data, err := em.Build("S", rows) tst.AssertNoErr(t, err) f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "Alice") tst.AssertEqual(t, cellValue(t, f, "S", "A3"), "") } func TestBuildPropagatesColumnError(t *testing.T) { em, _ := NewExcelMapper[testRow]() sentinel := errors.New("col fail") em.AddColumnErr("Bad", nil, nil, func(r testRow) (any, error) { return nil, sentinel }) _, err := em.Build("S", []testRow{{Name: "X"}}) if err == nil { t.Fatal("expected error from column fn to propagate") } } func TestBuildEmptyData(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) data, err := em.Build("S", []testRow{}) tst.AssertNoErr(t, err) f := openBytes(t, data) defer f.Close() tst.AssertEqual(t, cellValue(t, f, "S", "A1"), "Name") tst.AssertEqual(t, cellValue(t, f, "S", "A2"), "") } func TestBuildSingleSheetWithExistingFile(t *testing.T) { em, _ := NewExcelMapper[testRow]() em.AddColumn("Name", nil, nil, func(r testRow) any { return r.Name }) f, err := em.InitNewFile("S1") tst.AssertNoErr(t, err) _, err = f.NewSheet("S2") tst.AssertNoErr(t, err) err = em.BuildSingleSheet(f, "S2", []testRow{{Name: "Bob"}}) tst.AssertNoErr(t, err) tst.AssertEqual(t, cellValue(t, f, "S2", "A1"), "Name") tst.AssertEqual(t, cellValue(t, f, "S2", "A2"), "Bob") } func TestBuildWithColumnWidthAndStyle(t *testing.T) { em, _ := NewExcelMapper[testRow]() f, err := em.InitNewFile("S") tst.AssertNoErr(t, err) em.AddColumn("Name", em.StyleHeader, langext.Ptr(20.5), func(r testRow) any { return r.Name }) err = em.BuildSingleSheet(f, "S", []testRow{{Name: "Alice"}}) tst.AssertNoErr(t, err) w, err := f.GetColWidth("S", "A") tst.AssertNoErr(t, err) if w < 20.0 || w > 21.0 { t.Errorf("expected column width near 20.5, got %v", w) } }