PHP Classes

File: extension/core/structure_test.go

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   extension/core/structure_test.go   Download  
File: extension/core/structure_test.go
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Easy-Excel
Export and read XLSX files with a PHP Go extension
Author: By
Last change:
Date: 5 days ago
Size: 18,121 bytes
 

Contents

Class file image Download
package core import ( "archive/zip" "io" "path/filepath" "strings" "testing" "github.com/xuri/excelize/v2" "github.com/xiidea/easy-excel/extension/compat" ) func fillRows(t *testing.T, w *Workbook, sheet string, from, to int) { t.Helper() rows := make([][]compat.Cell, 0, to-from+1) for r := from; r <= to; r++ { rows = append(rows, mustCells(t, "a", int64(r), 1.5)) } if err := w.WriteRows(sheet, from, 1, rows); err != nil { t.Fatal(err) } } func reopen(t *testing.T, path string) *excelize.File { t.Helper() f, err := excelize.OpenFile(path) if err != nil { t.Fatal(err) } t.Cleanup(func() { f.Close() }) return f } func cellStyle(t *testing.T, f *excelize.File, sheet, cell string) *excelize.Style { t.Helper() id, err := f.GetCellStyle(sheet, cell) if err != nil { t.Fatal(err) } style, err := f.GetStyle(id) if err != nil { t.Fatal(err) } return style } func TestStyledHeaderStaysStreaming(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "styled.xlsx") // style + widths + panes + height queued before rows: the report pattern if err := w.ApplyStyle("Worksheet", "A1:C1", `{"font":{"bold":true},"fill":{"fillType":"solid","startColor":{"rgb":"FFFF00"}}}`); err != nil { t.Fatal(err) } if err := w.SetColWidth("Worksheet", 1, 2, 23.5); err != nil { t.Fatal(err) } if err := w.FreezePanes("Worksheet", "A2"); err != nil { t.Fatal(err) } if err := w.SetRowHeight("Worksheet", 1, 30); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 50) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("styled header forced a degrade; should have streamed inline") } f := reopen(t, path) if s := cellStyle(t, f, "Worksheet", "B1"); s.Font == nil || !s.Font.Bold { t.Errorf("B1 should be bold: %+v", s.Font) } else if s.Fill.Pattern != 1 || len(s.Fill.Color) == 0 || s.Fill.Color[0] != "FFFF00" { t.Errorf("B1 fill wrong: %+v", s.Fill) } if s := cellStyle(t, f, "Worksheet", "B2"); s.Font != nil && s.Font.Bold { t.Error("B2 should not be bold") } if width, _ := f.GetColWidth("Worksheet", "B"); width != 23.5 { t.Errorf("col B width = %v, want 23.5", width) } if panes, _ := f.GetPanes("Worksheet"); !panes.Freeze || panes.YSplit != 1 { t.Errorf("panes wrong: %+v", panes) } if h, _ := f.GetRowHeight("Worksheet", 1); h != 30 { t.Errorf("row 1 height = %v, want 30", h) } // value integrity under styled streaming if v, _ := f.GetCellValue("Worksheet", "B50"); v != "50" { t.Errorf("B50 = %q, want 50", v) } } func TestNumberFormatStreamsInline(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "numfmt.xlsx") if err := w.SetNumberFormat("Worksheet", "C1:C10", "0.00%"); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 10) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("number format on future rows should not degrade") } f := reopen(t, path) if v, _ := f.GetCellValue("Worksheet", "C5"); v != "150.00%" { t.Errorf("C5 formatted = %q, want 150.00%%", v) } } func TestFullColumnFormatStreams(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "colfmt.xlsx") if err := w.SetNumberFormat("Worksheet", "B", "#,##0.00"); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 20) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("full-column format should ride the stream") } f := reopen(t, path) if v, _ := f.GetCellValue("Worksheet", "B7"); v != "7.00" { t.Errorf("B7 formatted = %q, want 7.00", v) } } func TestStyleMergeLayersComponents(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "merge.xlsx") if err := w.ApplyStyle("Worksheet", "A1:C1", `{"font":{"bold":true}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "B1", `{"font":{"italic":true}}`); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 3) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) if s := cellStyle(t, f, "Worksheet", "B1"); !s.Font.Bold || !s.Font.Italic { t.Errorf("B1 should layer bold+italic: %+v", s.Font) } if s := cellStyle(t, f, "Worksheet", "A1"); !s.Font.Bold || s.Font.Italic { t.Errorf("A1 should be bold only: %+v", s.Font) } } func TestStyleAfterWriteDegradesAtSaveOnly(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "late.xlsx") fillRows(t, w, "Worksheet", 1, 30) // rows already streamed: style queues, degrade deferred to save if err := w.ApplyStyle("Worksheet", "A1:A30", `{"font":{"bold":true}}`); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("styling already-written rows should defer the degrade to save") } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if !w.Degraded() { t.Fatal("save should have degraded to apply the late style") } f := reopen(t, path) if s := cellStyle(t, f, "Worksheet", "A15"); !s.Font.Bold { t.Error("late style lost") } if v, _ := f.GetCellValue("Worksheet", "B15"); v != "15" { t.Errorf("B15 = %q, want 15 (degrade must keep data)", v) } } func TestMergeCellsWhileStreaming(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "merged.xlsx") if err := w.MergeCells("Worksheet", "A1:C1"); err != nil { // before sw exists t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 5) if err := w.MergeCells("Worksheet", "A4:B5"); err != nil { // sw active t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("merges should not degrade a streaming sheet") } f := reopen(t, path) merged, err := f.GetMergeCells("Worksheet") if err != nil { t.Fatal(err) } got := map[string]bool{} for _, m := range merged { got[m.GetStartAxis()+":"+m.GetEndAxis()] = true } if !got["A1:C1"] || !got["A4:B5"] { t.Errorf("merges missing: %v", got) } } func TestDeferredOpsApplyAtSave(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "deferred.xlsx") fillRows(t, w, "Worksheet", 1, 10) if err := w.AutoFilter("Worksheet", "A1:C10"); err != nil { t.Fatal(err) } if err := w.SetHyperlink("Worksheet", "A2", "https://example.com/x", "docs"); err != nil { t.Fatal(err) } if err := w.SetComment("Worksheet", "B3", "QA", "verify me"); err != nil { t.Fatal(err) } if err := w.SetComment("Worksheet", "B3", "QA", "verified"); err != nil { // replace t.Fatal(err) } if err := w.SetColAutoSize("Worksheet", 1, 1); err != nil { t.Fatal(err) } if err := w.SetPageSetup("Worksheet", "landscape", 9, 1, -1); err != nil { t.Fatal(err) } if err := w.SetDefinedName("data", "Worksheet!$A$1:$C$10", ""); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) if ok, link, _ := f.GetCellHyperLink("Worksheet", "A2"); !ok || link != "https://example.com/x" { t.Errorf("hyperlink: ok=%v link=%q", ok, link) } comments, err := f.GetComments("Worksheet") if err != nil { t.Fatal(err) } if len(comments) != 1 || comments[0].Author != "QA" || !strings.Contains(commentText(comments[0]), "verified") { t.Errorf("comment wrong: %+v", comments) } // col A's widest value is "a" (1 rune) ? approximated width 1+2 if width, _ := f.GetColWidth("Worksheet", "A"); width != 3 { t.Errorf("autosize width = %v, want 3", width) } layout, err := f.GetPageLayout("Worksheet") if err != nil { t.Fatal(err) } if layout.Orientation == nil || *layout.Orientation != "landscape" { t.Errorf("orientation: %+v", layout.Orientation) } names := f.GetDefinedName() found := false for _, n := range names { if n.Name == "data" && strings.Contains(n.RefersTo, "$A$1:$C$10") { found = true } } if !found { t.Errorf("defined name missing: %+v", names) } assertSheetXMLContains(t, path, `<autoFilter ref="$A$1:$C$10"`) } func commentText(c excelize.Comment) string { if c.Text != "" { return c.Text } var sb strings.Builder for _, p := range c.Paragraph { sb.WriteString(p.Text) } return sb.String() } // assertSheetXMLContains greps the first worksheet part of a saved file // (excelize has no GetAutoFilter accessor). func assertSheetXMLContains(t *testing.T, path, needle string) { t.Helper() zr, err := zip.OpenReader(path) if err != nil { t.Fatal(err) } defer zr.Close() for _, zf := range zr.File { if !strings.HasPrefix(zf.Name, "xl/worksheets/") { continue } r, err := zf.Open() if err != nil { t.Fatal(err) } data, err := io.ReadAll(r) r.Close() if err != nil { t.Fatal(err) } if strings.Contains(string(data), needle) { return } } t.Errorf("no worksheet XML contains %q", needle) } func TestStructureOpsOnLoadedFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "src.xlsx") w, err := New(testEnv()) if err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 5) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } w.Close() // loaded files are random-access: ops apply immediately w2, err := Open(path, "", testEnv()) if err != nil { t.Fatal(err) } defer w2.Close() if err := w2.ApplyStyle("Worksheet", "A1", `{"font":{"bold":true}}`); err != nil { t.Fatal(err) } if err := w2.SetColWidth("Worksheet", 2, 2, 40); err != nil { t.Fatal(err) } out := filepath.Join(dir, "out.xlsx") if err := w2.SaveXlsx(out, ""); err != nil { t.Fatal(err) } f := reopen(t, out) if s := cellStyle(t, f, "Worksheet", "A1"); !s.Font.Bold { t.Error("style on loaded file lost") } if width, _ := f.GetColWidth("Worksheet", "B"); width != 40 { t.Errorf("width on loaded file = %v", width) } } func TestBroadLateStyleKeepsNarrowEarlierStyles(t *testing.T) { // the ERP-report pattern: write data, apply column number formats, then a // broad alignment over everything ? the broad rect must not clobber the // narrower formats in the replay path (PhpSpreadsheet layers per cell) w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "layered.xlsx") fillRows(t, w, "Worksheet", 1, 15) // streamed first ? styles replay at save if err := w.SetNumberFormat("Worksheet", "C8:C15", "0.00%"); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "A1:C15", `{"alignment":{"vertical":"center"}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "A8:A15", `{"font":{"bold":true}}`); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) if v, _ := f.GetCellValue("Worksheet", "C8"); v != "150.00%" { t.Errorf("C8 formatted = %q: broad style clobbered the number format", v) } s := cellStyle(t, f, "Worksheet", "C8") if s.Alignment == nil || s.Alignment.Vertical != "center" { t.Errorf("C8 lost the broad alignment: %+v", s.Alignment) } if s = cellStyle(t, f, "Worksheet", "A9"); s.Font == nil || !s.Font.Bold || s.Alignment == nil || s.Alignment.Vertical != "center" { t.Errorf("A9 should layer bold over alignment: font=%+v align=%+v", s.Font, s.Alignment) } } func TestNestedRectLayeringSurvivesBroadStyle(t *testing.T) { // the title-block pattern: bold A1:C2 ? fill A1:C3 ? broad vcenter A1:C10 // applied after rows streamed ? the nested intersections must be applied // smallest-last so A1 keeps bold + fill + alignment w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "nested.xlsx") fillRows(t, w, "Worksheet", 1, 10) if err := w.ApplyStyle("Worksheet", "A1:C2", `{"font":{"bold":true},"alignment":{"horizontal":"center"}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "A1:C3", `{"fill":{"fillType":"solid","startColor":{"rgb":"F9F9F9"}}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "A1:C10", `{"alignment":{"vertical":"center"}}`); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) s := cellStyle(t, f, "Worksheet", "A1") if s.Font == nil || !s.Font.Bold { t.Errorf("A1 lost bold: %+v", s.Font) } if s.Alignment == nil || s.Alignment.Horizontal != "center" || s.Alignment.Vertical != "center" { t.Errorf("A1 alignment wrong: %+v", s.Alignment) } if s.Fill.Pattern != 1 { t.Errorf("A1 lost fill: %+v", s.Fill) } if s = cellStyle(t, f, "Worksheet", "A3"); (s.Font != nil && s.Font.Bold) || s.Fill.Pattern != 1 { t.Errorf("A3 should have fill but not bold: font=%+v fill=%+v", s.Font, s.Fill) } if s = cellStyle(t, f, "Worksheet", "A5"); s.Fill.Pattern == 1 || s.Alignment == nil || s.Alignment.Vertical != "center" { t.Errorf("A5 should have only the broad alignment: fill=%+v align=%+v", s.Fill, s.Alignment) } } func TestCrossOverlapLayering(t *testing.T) { // the subtotal pattern: a column number format crosses a bolded total // row; their intersection must carry both, plus the broad alignment w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "cross.xlsx") fillRows(t, w, "Worksheet", 1, 15) if err := w.ApplyStyle("Worksheet", "A13:G13", `{"font":{"bold":true}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "C8:C15", `{"numberFormat":{"formatCode":"#,##0.00"}}`); err != nil { t.Fatal(err) } if err := w.ApplyStyle("Worksheet", "A1:G15", `{"alignment":{"vertical":"center"}}`); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) if v, _ := f.GetCellValue("Worksheet", "C13"); v != "1.50" { t.Errorf("C13 formatted = %q: cross overlap lost the number format", v) } s := cellStyle(t, f, "Worksheet", "C13") if s.Font == nil || !s.Font.Bold { t.Errorf("C13 lost bold: %+v", s.Font) } if s.Alignment == nil || s.Alignment.Vertical != "center" { t.Errorf("C13 lost alignment: %+v", s.Alignment) } if v, _ := f.GetCellValue("Worksheet", "C9"); v != "1.50" { t.Errorf("C9 formatted = %q", v) } if s = cellStyle(t, f, "Worksheet", "C9"); s.Font != nil && s.Font.Bold { t.Error("C9 should not be bold") } } func TestAutoSizeSkipsMergedCells(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "autosize.xlsx") if err := w.MergeCells("Worksheet", "A1:C1"); err != nil { t.Fatal(err) } if err := w.WriteRows("Worksheet", 1, 1, [][]compat.Cell{ mustCells(t, "a very long merged title that must not stretch column A"), mustCells(t, "abc"), mustCells(t, "abcdef"), }); err != nil { t.Fatal(err) } if err := w.SetColAutoSize("Worksheet", 1, 1); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } f := reopen(t, path) // widest non-merged value is "abcdef" (6 runes) ? width 8 if width, _ := f.GetColWidth("Worksheet", "A"); width != 8 { t.Errorf("col A width = %v, want 8 (merged title must be skipped)", width) } } func TestFormattedReadSeesQueuedStyles(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() if err := w.SetNumberFormat("Worksheet", "C1:C5", "0.0"); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 5) // reading degrades and must replay the queued format first v, err := w.GetCell("Worksheet", "C2", GetFormatted) if err != nil { t.Fatal(err) } if v != "1.5" { t.Errorf("formatted C2 = %v, want 1.5", v) } } func TestDefaultDimensionsStayStreaming(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "defaultdims.xlsx") // before any row: the streaming-safe report pattern if err := w.SetDefaultRowHeight("Worksheet", 20); err != nil { t.Fatal(err) } if err := w.SetDefaultColWidth("Worksheet", 14); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 50) if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } if w.Degraded() { t.Fatal("pre-stream default dimensions forced a degrade; they must stay free") } f := reopen(t, path) props, err := f.GetSheetProps("Worksheet") if err != nil { t.Fatal(err) } if props.DefaultRowHeight == nil || *props.DefaultRowHeight != 20 { t.Errorf("defaultRowHeight = %v, want 20", derefF(props.DefaultRowHeight)) } if props.CustomHeight == nil || !*props.CustomHeight { t.Error("customHeight should be set alongside defaultRowHeight") } if props.DefaultColWidth == nil || *props.DefaultColWidth != 14 { t.Errorf("defaultColWidth = %v, want 14", derefF(props.DefaultColWidth)) } if v, _ := f.GetCellValue("Worksheet", "B50"); v != "50" { t.Errorf("B50 = %q, want 50", v) } } func TestDefaultDimensionsMidStreamReplay(t *testing.T) { w, err := New(testEnv()) if err != nil { t.Fatal(err) } defer w.Close() path := filepath.Join(t.TempDir(), "defaultdims-mid.xlsx") if err := w.SetDefaultRowHeight("Worksheet", 20); err != nil { t.Fatal(err) } fillRows(t, w, "Worksheet", 1, 50) // after rows the preamble is snapshotted; the update must replay at save // (correctness over speed, like per-row heights on written rows) if err := w.SetDefaultRowHeight("Worksheet", 22); err != nil { t.Fatal(err) } if err := w.SaveXlsx(path, ""); err != nil { t.Fatal(err) } props, err := reopen(t, path).GetSheetProps("Worksheet") if err != nil { t.Fatal(err) } if props.DefaultRowHeight == nil || *props.DefaultRowHeight != 22 { t.Errorf("mid-stream defaultRowHeight = %v, want 22", derefF(props.DefaultRowHeight)) } } func derefF(f *float64) float64 { if f == nil { return -1 } return *f }