Files
goext/excelext/mapper_test.go
T
Mikescher 53aa8c05b0
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m52s
v0.0.639 remove langext.Ptr from templates
2026-05-08 10:28:40 +02:00

304 lines
8.1 KiB
Go

package excelext
import (
"bytes"
"errors"
"testing"
"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, new(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", new(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, new(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)
}
}