1package sheet23import (4 "bytes"5 "slices"6 "strings"7 "testing"8)910// roundTrip saves s's workbook and reads it back.11func roundTrip(t *testing.T, s *Sheet) *Sheet {12 t.Helper()13 var b bytes.Buffer14 if err := s.Write(&b); err != nil {15 t.Fatal(err)16 }17 got, err := Read(&b)18 if err != nil {19 t.Fatalf("%v\n%s", err, b.String())20 }21 return got22}2324func TestBorders(t *testing.T) {25 s := New()26 s.SetBorders(rng("B2:C3"), BorderOuter, LineThin)27 for _, c := range []struct {28 cell string29 want Borders30 }{31 {"B2", BordersOf(LineThin, LineNone, LineThin, LineNone)},32 {"C2", BordersOf(LineThin, LineNone, LineNone, LineThin)},33 {"B3", BordersOf(LineNone, LineThin, LineThin, LineNone)},34 {"C3", BordersOf(LineNone, LineThin, LineNone, LineThin)},35 } {36 if got := s.CellStyle(at(c.cell)).Borders; got != c.want {37 t.Errorf("outer: %s = %+v, want %+v", c.cell, got, c.want)38 }39 }40 // Neighbors see the shared edge.41 if s.EdgeAbove(at("B4")) != LineThin || s.EdgeLeft(at("D2")) != LineThin || s.EdgeAbove(at("B3")) != LineNone {42 t.Error("shared edges")43 }44 // Inner draws between the cells only; a later thick top clears the45 // neighbor's facing bottom, so the last change shows.46 s.SetBorders(rng("B2:C3"), BorderInner, LineDouble)47 if got := s.CellStyle(at("B2")).Borders; got != BordersOf(LineThin, LineDouble, LineThin, LineDouble) {48 t.Errorf("inner B2 = %+v", got)49 }50 s.SetBorders(rng("B1"), BorderBottom, LineThin)51 s.SetBorders(rng("B2"), BorderTop, LineThick)52 if got := s.CellStyle(at("B1")).Borders.Bottom(); got != LineNone {53 t.Errorf("facing bottom kept: %v", got)54 }55 if s.EdgeAbove(at("B2")) != LineThick {56 t.Error("top not thick")57 }58 // None removes every line of the range and the facing ones.59 s.SetBorders(rng("A1:D4"), BorderNone, LineThin)60 for a := range s.cells.all() {61 if b := s.CellStyle(a).Borders; !b.IsZero() {62 t.Errorf("%s still %+v", a, b)63 }64 }65 if s.cells.len() != 0 {66 t.Errorf("formatting-only cells left: %d", s.cells.len())67 }68 s.Undo()69 if s.CellStyle(at("C3")).Borders.Right() != LineThin {70 t.Error("undo none")71 }72 // Whole columns keep borders on the column.73 s.SetBorders(rng("E:F"), BorderAll, LineThin)74 if s.RowFormats()[0] != (Format{}) || s.EdgeLeft(at("F900")) != LineThin || s.EdgeAbove(at("E77")) != LineThin {75 t.Error("column borders")76 }77 if len(s.ColStyles()) != 2 {78 t.Errorf("column styles %v", s.ColStyles())79 }80 got := roundTrip(t, s)81 if got.CellStyle(at("C2")).Borders != s.CellStyle(at("C2")).Borders || got.EdgeLeft(at("F3")) != LineThin {82 t.Error("borders lost in the file")83 }84}8586func TestWrapFile(t *testing.T) {87 s := New()88 s.Set(at("A1"), "a long line of text")89 s.SetStyle(rng("A1"), func(st *Style) { st.Wrap = WrapOn })90 s.SetStyle(rng("B:B"), func(st *Style) { st.Wrap = WrapClip })91 var b bytes.Buffer92 s.Write(&b)93 if !strings.Contains(b.String(), `"wrap": "wrap"`) && !strings.Contains(b.String(), `"wrap":"wrap"`) {94 t.Errorf("file: %s", b.String())95 }96 got := roundTrip(t, s)97 if got.CellStyle(at("A1")).Wrap != WrapOn || got.CellStyle(at("B7")).Wrap != WrapClip {98 t.Error("wrap lost in the file")99 }100 if _, err := Read(strings.NewReader(`{"version": 2, "cells": {"A1": {"input": "x", "wrap": "sideways"}}}`)); err == nil {101 t.Error("unknown wrapping read")102 }103}104105func TestShapers(t *testing.T) {106 s := New()107 if s.Shaped() {108 t.Error("new sheet shaped")109 }110 s.Set(at("C5"), "words that wrap")111 s.SetStyle(rng("C5"), func(st *Style) { st.Wrap = WrapOn })112 if !s.Shaped() || !slices.Equal(s.WrappedIn(4), []int{2}) || s.WrappedIn(3) != nil {113 t.Errorf("wrapped in row 5: %v", s.WrappedIn(4))114 }115 s.SetBorders(rng("A7"), BorderBottom, LineThin)116 if !s.RuleAbove(7) || s.RuleAbove(6) || s.RuleAbove(4) {117 t.Error("rule above row 8 only")118 }119 // A number read from a file is indexed too.120 s.Set(at("A7"), "12.5")121 if got := roundTrip(t, s); !got.RuleAbove(7) || !slices.Equal(got.WrappedIn(4), []int{2}) {122 t.Error("the file's borders and wrapping aren't indexed")123 }124 s.ClearFormatting(rng("A1:Z99"))125 if s.Shaped() {126 t.Error("still shaped after clearing")127 }128 // A column that wraps wraps the text its cells hold.129 s.SetStyle(rng("D:D"), func(st *Style) { st.Wrap = WrapOn })130 s.Set(at("D3"), "x")131 if !slices.Equal(s.WrappedIn(2), []int{3}) || s.WrappedIn(3) != nil {132 t.Errorf("column wrap %v", s.WrappedIn(2))133 }134 // A merged cell doesn't grow its row.135 s.Merge(rng("D3:E3"), MergeAll)136 if s.WrappedIn(2) != nil {137 t.Error("merged cell wraps")138 }139}140141func TestRowHeights(t *testing.T) {142 s := New()143 s.SetRowHeight(2, 3, 4)144 if h, ok := s.RowHeight(3); !ok || h != 4 {145 t.Errorf("height = %d, %v", h, ok)146 }147 s.Seal() // the UI seals between actions, so heights join no other step148 s.InsertRows(0, 2)149 if _, ok := s.RowHeight(2); ok {150 t.Error("row 3 kept its height")151 }152 if h, _ := s.RowHeight(5); h != 4 {153 t.Error("heights didn't move")154 }155 s.Undo()156 if h, _ := s.RowHeight(2); h != 4 {157 t.Error("undo insert")158 }159 s.SetRowHeight(2, 2, 0)160 if _, ok := s.RowHeight(2); ok {161 t.Error("fit to data")162 }163 s.Undo()164 s.SetRowHeight(9, 9, 99)165 if h, _ := s.RowHeight(9); h != MaxRowHeight {166 t.Errorf("capped at %d", h)167 }168 // Whole columns set the rows up to the last holding a cell.169 s.Set(at("B12"), "x")170 s.SetRowHeight(0, MaxRows-1, 2)171 if len(s.Heights()) != 12 {172 t.Errorf("whole column heights %v", s.Heights())173 }174 s.Undo()175 s.Undo()176 if s.ColsWidth(0, 3) != 4*DefaultWidth {177 t.Error("ColsWidth")178 }179 s.SetColWidth(2, 3)180 if s.ColsWidth(0, 3) != 3*DefaultWidth+3 || s.ColsWidth(3, 9) != 7*DefaultWidth {181 t.Error("ColsWidth with a width")182 }183 s.Undo()184 got := roundTrip(t, s)185 if !mapsEqual(got.Heights(), s.Heights()) {186 t.Errorf("file heights %v, want %v", got.Heights(), s.Heights())187 }188 // Whole rows carry their heights when copied and moved.189 clip := s.Copy(rowRect(2, 3))190 s.Paste(clip, rowRect(20, 20), false)191 if h, _ := s.RowHeight(21); h != 4 {192 t.Errorf("pasted rows' heights %v", s.Heights())193 }194 s.Move(rowRect(20, 21), Addr{Row: 30})195 if _, ok := s.RowHeight(20); ok {196 t.Error("moved rows left their heights")197 }198 if h, _ := s.RowHeight(31); h != 4 {199 t.Errorf("moved rows' heights %v", s.Heights())200 }201}202203func mapsEqual(a, b map[int]int) bool {204 if len(a) != len(b) {205 return false206 }207 for k, v := range a {208 if b[k] != v {209 return false210 }211 }212 return true213}214215func TestMerge(t *testing.T) {216 s := New()217 s.Set(at("B2"), "kept")218 s.Set(at("C3"), "lost")219 if a, loses := s.MergeLoses(rng("B2:C3"), MergeAll); !loses || a != at("C3") {220 t.Errorf("MergeLoses = %v, %v", a, loses)221 }222 if _, loses := s.MergeLoses(rng("B3:C3"), MergeVertically); loses {223 t.Error("vertical merges of one row lose nothing")224 }225 if err := s.Merge(rng("B2:C3"), MergeAll); err != nil {226 t.Fatal(err)227 }228 if m, ok := s.MergeAt(at("C3")); !ok || m != rng("B2:C3") {229 t.Errorf("MergeAt = %v, %v", m, ok)230 }231 if !s.Cell(at("C3")).Blank() || s.Value(at("B2")).Str != "kept" {232 t.Error("merging keeps only the top-left value")233 }234 if s.Grow(rng("A1:B2")) != rng("A1:C3") {235 t.Errorf("Grow = %v", s.Grow(rng("A1:B2")))236 }237 s.Undo()238 if s.Value(at("C3")).Str != "lost" || len(s.Merges()) != 0 {239 t.Error("undo merge")240 }241 s.Redo()242 if err := s.Merge(rng("A1"), MergeAll); err != ErrMergeOne {243 t.Errorf("one cell: %v", err)244 }245 if err := s.Merge(rng("A1:B1048576"), MergeHorizontally); err != ErrMergeMany {246 t.Errorf("a million merges: %v", err)247 }248 // Merges follow inserted and deleted lines and are saved.249 s.InsertCols(0, 1)250 if got := s.Merges(); len(got) != 1 || got[0] != rng("C2:D3") {251 t.Errorf("after insert %v", got)252 }253 got := roundTrip(t, s)254 if !slices.Equal(got.Merges(), s.Merges()) {255 t.Errorf("file merges %v", got.Merges())256 }257 s.DeleteCols(3, 1)258 if got := s.Merges(); len(got) != 1 || got[0] != rng("C2:C3") {259 t.Errorf("after deleting a column %v", got)260 }261 s.DeleteRows(2, 1)262 if len(s.Merges()) != 0 {263 t.Errorf("merge of one cell left %v", s.Merges())264 }265 s.Undo()266 s.Undo()267 // Rows and columns merge one by one.268 s.Merge(rng("F1:G3"), MergeHorizontally)269 if len(s.MergesIn(rng("F1:G3"))) != 3 {270 t.Errorf("horizontal merges %v", s.Merges())271 }272 if n := s.Unmerge(rng("G2")); n != 1 || len(s.MergesIn(rng("F1:G3"))) != 2 {273 t.Errorf("unmerge %d, %v", n, s.Merges())274 }275}276277func TestMergeCopyMoveSort(t *testing.T) {278 s := New()279 s.Set(at("A1"), "b")280 s.Set(at("A2"), "a")281 s.Merge(rng("A1:B1"), MergeAll)282 // A copy carries the merges wholly inside it.283 s.Paste(s.Copy(rng("A1:B2")), rng("D5"), false)284 if m, ok := s.MergeAt(at("E5")); !ok || m != rng("D5:E5") {285 t.Errorf("pasted merge %v, %v", m, ok)286 }287 s.Move(rng("D5:E6"), at("D9"))288 if _, ok := s.MergeAt(at("D5")); ok {289 t.Error("moved merge left behind")290 }291 if m, _ := s.MergeAt(at("E9")); m != rng("D9:E9") {292 t.Errorf("moved merge %v", s.Merges())293 }294 // Sorting a range with merged cells is refused.295 s.SortRange(rng("A1:B2"), []SortKey{{Col: 0}})296 if s.Value(at("A1")).Str != "b" {297 t.Error("sorted over merged cells")298 }299 // A merge blocks a spill.300 s.Set(at("H1"), "=SEQUENCE(3)")301 s.Merge(rng("H2:I2"), MergeAll)302 if v := s.Value(at("H1")); v.Kind != Error {303 t.Errorf("spill over a merge = %v", v)304 }305 s.Unmerge(rng("H2"))306 if v := s.Value(at("H3")); v.Num != 3 {307 t.Errorf("spill after unmerging = %v", v)308 }309}