|
5 | 5 | "testing" |
6 | 6 |
|
7 | 7 | "github.com/ether/etherpad-go/lib/sheet" |
| 8 | + "github.com/xuri/excelize/v2" |
8 | 9 | ) |
9 | 10 |
|
10 | 11 | func TestImportExportRoundTrip(t *testing.T) { |
@@ -38,3 +39,117 @@ func TestImportExportRoundTrip(t *testing.T) { |
38 | 39 | t.Fatalf("B1 = %q", sh.GetCell(sheet.CellRef{Row: 0, Col: 1}).Raw) |
39 | 40 | } |
40 | 41 | } |
| 42 | + |
| 43 | +func TestRoundTripStylesDimsAndFreeze(t *testing.T) { |
| 44 | + wb := sheet.NewWorkbook() |
| 45 | + s := wb.AddSheet("Data", "Data") |
| 46 | + props := map[string]string{ |
| 47 | + "bold": "1", "italic": "1", "underline": "1", |
| 48 | + "color": "#ff0000", "bg": "#00ff00", "align": "center", |
| 49 | + "border": "all", "wrap": "1", |
| 50 | + "fontFamily": "Arial", "fontSize": "14", |
| 51 | + "numFmt": "currency:2", |
| 52 | + } |
| 53 | + styleId := wb.Styles.Put(sheet.Style{Props: props}) |
| 54 | + s.SetCell(sheet.CellRef{Row: 0, Col: 0}, sheet.Cell{Raw: "42", StyleId: styleId}) |
| 55 | + // styled but empty cell must survive too |
| 56 | + s.SetCell(sheet.CellRef{Row: 1, Col: 1}, sheet.Cell{StyleId: styleId}) |
| 57 | + s.ColWidths[2] = 150 |
| 58 | + s.RowHeights[3] = 40 |
| 59 | + s.FrozenRows, s.FrozenCols = 1, 1 |
| 60 | + |
| 61 | + data, err := Export(wb) |
| 62 | + if err != nil { |
| 63 | + t.Fatalf("Export: %v", err) |
| 64 | + } |
| 65 | + snap, err := Import(bytes.NewReader(data)) |
| 66 | + if err != nil { |
| 67 | + t.Fatalf("Import: %v", err) |
| 68 | + } |
| 69 | + got := sheet.WorkbookFromSnapshot(snap) |
| 70 | + sh := got.SheetByID("Data") |
| 71 | + if sh == nil { |
| 72 | + t.Fatal("sheet Data missing") |
| 73 | + } |
| 74 | + |
| 75 | + for _, ref := range []sheet.CellRef{{Row: 0, Col: 0}, {Row: 1, Col: 1}} { |
| 76 | + c := sh.GetCell(ref) |
| 77 | + if c.StyleId == 0 { |
| 78 | + t.Fatalf("cell %v lost its style", ref) |
| 79 | + } |
| 80 | + st, _ := got.Styles.Get(c.StyleId) |
| 81 | + for k, want := range props { |
| 82 | + if st.Props[k] != want { |
| 83 | + t.Errorf("cell %v prop %s = %q, want %q", ref, k, st.Props[k], want) |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + if px := sh.ColWidths[2]; px < 148 || px > 152 { // unit conversion rounds |
| 88 | + t.Errorf("col 2 width = %d, want ~150", px) |
| 89 | + } |
| 90 | + if px := sh.RowHeights[3]; px < 38 || px > 42 { |
| 91 | + t.Errorf("row 3 height = %d, want ~40", px) |
| 92 | + } |
| 93 | + if sh.FrozenRows != 1 || sh.FrozenCols != 1 { |
| 94 | + t.Errorf("freeze = %d/%d, want 1/1", sh.FrozenRows, sh.FrozenCols) |
| 95 | + } |
| 96 | + if len(sh.ColWidths) != 1 || len(sh.RowHeights) != 1 { |
| 97 | + t.Errorf("dims not sparse: cols=%v rows=%v", sh.ColWidths, sh.RowHeights) |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +// TestImportForeignFile builds a file the way Excel would (builtin numFmt ids, |
| 102 | +// style on a value cell) rather than via our own Export, so mapping gaps can't |
| 103 | +// hide behind a symmetric round-trip. |
| 104 | +func TestImportForeignFile(t *testing.T) { |
| 105 | + f := excelize.NewFile() |
| 106 | + styleId, err := f.NewStyle(&excelize.Style{ |
| 107 | + Font: &excelize.Font{Bold: true, Color: "FF0000"}, |
| 108 | + NumFmt: 10, // builtin "0.00%" |
| 109 | + }) |
| 110 | + if err != nil { |
| 111 | + t.Fatalf("NewStyle: %v", err) |
| 112 | + } |
| 113 | + if err := f.SetCellValue("Sheet1", "A1", 0.5); err != nil { |
| 114 | + t.Fatalf("SetCellValue: %v", err) |
| 115 | + } |
| 116 | + if err := f.SetCellStyle("Sheet1", "A1", "A1", styleId); err != nil { |
| 117 | + t.Fatalf("SetCellStyle: %v", err) |
| 118 | + } |
| 119 | + buf, err := f.WriteToBuffer() |
| 120 | + if err != nil { |
| 121 | + t.Fatalf("WriteToBuffer: %v", err) |
| 122 | + } |
| 123 | + |
| 124 | + snap, err := Import(bytes.NewReader(buf.Bytes())) |
| 125 | + if err != nil { |
| 126 | + t.Fatalf("Import: %v", err) |
| 127 | + } |
| 128 | + wb := sheet.WorkbookFromSnapshot(snap) |
| 129 | + c := wb.SheetByID("Sheet1").GetCell(sheet.CellRef{Row: 0, Col: 0}) |
| 130 | + if c.StyleId == 0 { |
| 131 | + t.Fatal("A1 has no style") |
| 132 | + } |
| 133 | + st, _ := wb.Styles.Get(c.StyleId) |
| 134 | + if st.Props["bold"] != "1" || st.Props["color"] != "#ff0000" || st.Props["numFmt"] != "percent:2" { |
| 135 | + t.Fatalf("props = %v", st.Props) |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +func TestNumFmtCodeMapping(t *testing.T) { |
| 140 | + cases := map[string]string{ |
| 141 | + "@": "text", "m/d/yyyy": "date", "0.00%": "percent:2", |
| 142 | + "$#,##0.00": "currency:2", "#,##0": "number:0", "General": "", |
| 143 | + } |
| 144 | + for code, want := range cases { |
| 145 | + if got := codeToNumFmt(code); got != want { |
| 146 | + t.Errorf("codeToNumFmt(%q) = %q, want %q", code, got, want) |
| 147 | + } |
| 148 | + } |
| 149 | + // symbolic -> code -> symbolic is stable |
| 150 | + for _, nf := range []string{"text", "date", "number:2", "currency:1", "percent:0"} { |
| 151 | + if got := codeToNumFmt(numFmtToCode(nf)); got != nf { |
| 152 | + t.Errorf("roundtrip %q -> %q", nf, got) |
| 153 | + } |
| 154 | + } |
| 155 | +} |
0 commit comments