Files
sm-seo-plugin/classes/csv_test.go
Jakub Zych ed2d919aee fix: the CSV round trip keeps values that start with an apostrophe before a formula character (WR-21)
escapeCell left '=x as it was and unescapeCell then dropped its
apostrophe on import. A value of apostrophes followed by a formula
character now gets one more apostrophe on export, and the import drops
exactly one, so unescapeCell is the inverse of escapeCell.
2026-10-27 15:39:23 +01:00

122 lines
4.2 KiB
Go

package classes
import (
"errors"
"reflect"
"strings"
"testing"
)
// TestCSV: the export writes a BOM, the delimiter and escaped formula
// cells; the import strips the BOM, detects or takes the delimiter, reads
// quoting, unescapes formula cells and refuses what is not CSV.
func TestCSV(t *testing.T) {
header := []string{"source", "target", "note"}
rows := [][]string{
{"/a", "/b", "plain"},
{"/ł", "/ż", "zażółć; gęślą, jaźń"},
{"/q", "/r", `she said "hi"` + "\nnext line"},
{"/f", "/g", "=HYPERLINK(\"x\")"},
{"/p", "/s", "+1"},
{"/m", "/n", "-1"},
{"/at", "/x", "@SUM(A1)"},
{"/tab", "/x", "\tlead"},
{"/cr", "/x", "\rlead"},
{"/ok", "/x", "a=b"},
{"/ap", "/x", "'=x"},
{"/ap2", "/x", "''-)"},
{"/ap3", "/x", "'plain"},
}
t.Run("round trip with each delimiter", func(t *testing.T) {
for _, delim := range []string{",", ";", ""} {
out, err := WriteCSV(header, rows, delim)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(out, BOM) {
t.Fatalf("%q: no BOM", delim)
}
sep := ","
if delim == ";" {
sep = ";"
}
if !strings.HasPrefix(strings.TrimPrefix(out, BOM), strings.Join(header, sep)+"\n") {
t.Fatalf("%q: header line %q", delim, strings.SplitN(out, "\n", 2)[0])
}
gotHeader, got, err := ReadCSV(out, delim)
if err != nil || !reflect.DeepEqual(gotHeader, header) || len(got) != len(rows) {
t.Fatalf("%q: ReadCSV = %v, %d rows, %v", delim, gotHeader, len(got), err)
}
for i, row := range rows {
// Escaped cells keep their leading tab or CR: the apostrophe
// protects them from the import's trimming.
for j, col := range header {
if want := row[j]; got[i].Fields[col] != want {
t.Fatalf("%q row %d %s = %q, want %q", delim, i, col, got[i].Fields[col], want)
}
}
}
}
})
t.Run("formula cells are escaped on export", func(t *testing.T) {
out, _ := WriteCSV([]string{"note"}, [][]string{{"=1+1"}, {"+x"}, {"-x"}, {"@x"}, {"a=b"}, {""}}, ",")
lines := strings.Split(strings.TrimPrefix(out, BOM), "\n")
if lines[1] != "'=1+1" || lines[2] != "'+x" || lines[3] != "'-x" || lines[4] != "'@x" || lines[5] != "a=b" {
t.Fatalf("export = %q", lines)
}
for in, want := range map[string]string{"'=x": "=x", "'+x": "+x", "'abc": "'abc", "''=x": "'=x", "plain": "plain", "'": "'"} {
if got := unescapeCell(in); got != want {
t.Errorf("unescapeCell(%q) = %q, want %q", in, got, want)
}
}
})
t.Run("delimiter detection and the setting", func(t *testing.T) {
if CSVDelimiter(";") != ';' || CSVDelimiter(" ; ") != ';' || CSVDelimiter(",") != ',' || CSVDelimiter("") != ',' || CSVDelimiter("|") != ',' {
t.Fatal("CSVDelimiter")
}
h, rows, err := ReadCSV("Source;Target\n/a;/b\n", "")
if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Fields["target"] != "/b" {
t.Fatalf("detected ; = %v %+v %v", h, rows, err)
}
h, rows, err = ReadCSV(BOM+" SOURCE , Target \n/a,/b\n", "")
if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Line != 2 {
t.Fatalf("detected , = %v %+v %v", h, rows, err)
}
})
t.Run("quoting, blank rows and extra cells", func(t *testing.T) {
data := "source,target,note\n\"/a,1\",\"/b\"\"x\",\"two\nlines\"\n,,\n\n/c,/d,n,extra\n"
_, rows, err := ReadCSV(data, ",")
if err != nil || len(rows) != 2 {
t.Fatalf("rows = %+v, %v", rows, err)
}
if rows[0].Fields["source"] != "/a,1" || rows[0].Fields["target"] != `/b"x` || rows[0].Fields["note"] != "two\nlines" || rows[0].Line != 2 {
t.Fatalf("quoted row = %+v", rows[0])
}
if rows[1].Fields["source"] != "/c" || len(rows[1].Fields) != 3 || rows[1].Line != 6 {
t.Fatalf("row with an extra cell = %+v", rows[1])
}
})
t.Run("unreadable files", func(t *testing.T) {
for name, data := range map[string]string{
"empty": "",
"a BOM only": BOM,
"blank": " \n\t\n",
"a bare quote": "source,target\n\"/a,/b\n",
"the wrong delimiter": "source;target\n/a;/b\n",
} {
delim := ""
if name == "the wrong delimiter" {
delim = ","
}
if _, _, err := ReadCSV(data, delim); !errors.Is(err, ErrUnreadable) {
t.Errorf("%s: err = %v", name, err)
}
}
})
}