275 lines
8.9 KiB
Go
275 lines
8.9 KiB
Go
package officekit
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// minimalWordDoc builds a tiny, realistic .docx — using Word's own
|
|
// "w:"-prefixed convention, not this package's unprefixed
|
|
// default-namespace one — containing one body paragraph and one table
|
|
// with a single cell's worth of text. This exercises the append/replace
|
|
// operations against content this package doesn't itself model (tables)
|
|
// without needing a real binary fixture file on disk.
|
|
func minimalWordDoc(t *testing.T) []byte {
|
|
t.Helper()
|
|
documentXML := xml.Header + `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
`<w:body>` +
|
|
`<w:p><w:r><w:t>Intro paragraph.</w:t></w:r></w:p>` +
|
|
`<w:tbl><w:tr><w:tc><w:p><w:r><w:t>cell one text</w:t></w:r></w:p></w:tc></w:tr></w:tbl>` +
|
|
`<w:sectPr><w:pgSz w:w="12240" w:h="15840"/></w:sectPr>` +
|
|
`</w:body></w:document>`
|
|
data, err := buildZip([]zipPart{
|
|
{Name: "[Content_Types].xml", Body: docxContentTypes},
|
|
{Name: "_rels/.rels", Body: docxRootRels},
|
|
{Name: docxDocumentPart, Body: documentXML},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildZip: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestBuildDocxReadRoundTrip(t *testing.T) {
|
|
data, err := BuildDocx([]Paragraph{
|
|
{Text: "Title", Style: StyleHeading1},
|
|
{Text: "First item", Style: StyleBullet},
|
|
{Text: "Second item", Style: StyleBullet},
|
|
{Text: "Styled body text", Bold: true, Italic: true, Underline: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildDocx: %v", err)
|
|
}
|
|
text, err := ReadDocxText(data)
|
|
if err != nil {
|
|
t.Fatalf("ReadDocxText: %v", err)
|
|
}
|
|
want := []string{"Title", "- First item", "- Second item", "Styled body text"}
|
|
got := strings.Split(text, "\n")
|
|
if len(got) != len(want) {
|
|
t.Fatalf("expected %d lines, got %d: %q", len(want), len(got), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("line %d: got %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDocxRunFormattingXML(t *testing.T) {
|
|
data, err := BuildDocx([]Paragraph{{Text: "x", Bold: true, Italic: true, Underline: true}})
|
|
if err != nil {
|
|
t.Fatalf("BuildDocx: %v", err)
|
|
}
|
|
raw, ok, err := readZipPart(data, docxDocumentPart)
|
|
if err != nil || !ok {
|
|
t.Fatalf("readZipPart: ok=%v err=%v", ok, err)
|
|
}
|
|
body := string(raw)
|
|
for _, want := range []string{"<b></b>", "<i></i>", `<u val="single">`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("expected document.xml to contain %s, got:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendDocxParagraphPreservesTable(t *testing.T) {
|
|
data := minimalWordDoc(t)
|
|
out, err := AppendDocxParagraph(data, Paragraph{Text: "Appended line."})
|
|
if err != nil {
|
|
t.Fatalf("AppendDocxParagraph: %v", err)
|
|
}
|
|
text, err := ReadDocxText(out)
|
|
if err != nil {
|
|
t.Fatalf("ReadDocxText: %v", err)
|
|
}
|
|
for _, want := range []string{"Intro paragraph.", "cell one text", "Appended line."} {
|
|
if !strings.Contains(text, want) {
|
|
t.Errorf("expected read-back text to contain %q, got:\n%s", want, text)
|
|
}
|
|
}
|
|
// The new paragraph must land before the trailing sectPr, not after
|
|
// it — content after a body's final sectPr is invalid OOXML.
|
|
raw, ok, err := readZipPart(out, docxDocumentPart)
|
|
if err != nil || !ok {
|
|
t.Fatalf("readZipPart: ok=%v err=%v", ok, err)
|
|
}
|
|
body := string(raw)
|
|
idxAppended := strings.Index(body, "Appended line.")
|
|
idxSectPr := strings.Index(body, "<w:sectPr")
|
|
if idxAppended < 0 || idxSectPr < 0 || idxAppended > idxSectPr {
|
|
t.Errorf("expected appended paragraph before <w:sectPr>, got appended@%d sectPr@%d:\n%s", idxAppended, idxSectPr, body)
|
|
}
|
|
}
|
|
|
|
func TestReplaceDocxTextInTable(t *testing.T) {
|
|
data := minimalWordDoc(t)
|
|
out, n, err := ReplaceDocxText(data, "cell one text", "updated cell text")
|
|
if err != nil {
|
|
t.Fatalf("ReplaceDocxText: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("expected 1 text run changed, got %d", n)
|
|
}
|
|
text, err := ReadDocxText(out)
|
|
if err != nil {
|
|
t.Fatalf("ReadDocxText: %v", err)
|
|
}
|
|
if strings.Contains(text, "cell one text") {
|
|
t.Errorf("old text still present after replace: %s", text)
|
|
}
|
|
if !strings.Contains(text, "updated cell text") {
|
|
t.Errorf("expected replaced text, got: %s", text)
|
|
}
|
|
if !strings.Contains(text, "Intro paragraph.") {
|
|
t.Errorf("unrelated paragraph lost: %s", text)
|
|
}
|
|
}
|
|
|
|
func TestReplaceDocxTextNotFound(t *testing.T) {
|
|
data := minimalWordDoc(t)
|
|
if _, _, err := ReplaceDocxText(data, "nope, not in this document", "x"); err == nil {
|
|
t.Fatal("expected an error for text that isn't present")
|
|
}
|
|
}
|
|
|
|
func TestSetXlsxCellsFormulaAndOrdering(t *testing.T) {
|
|
data, err := BuildXlsx([]Sheet{{Name: "Sheet1", Rows: [][]string{{"Label", "Value"}}}})
|
|
if err != nil {
|
|
t.Fatalf("BuildXlsx: %v", err)
|
|
}
|
|
|
|
// Written deliberately out of row order, including a formula, to
|
|
// exercise the sort-before-marshal fix.
|
|
out, err := SetXlsxCells(data, "", map[string]string{
|
|
"A5": "fifth",
|
|
"A2": "second",
|
|
"B2": "=A2",
|
|
"A1": "first",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetXlsxCells: %v", err)
|
|
}
|
|
|
|
rows, _, err := ReadXlsxCells(out, "")
|
|
if err != nil {
|
|
t.Fatalf("ReadXlsxCells: %v", err)
|
|
}
|
|
// Three sparse rows exist: 1 (from BuildXlsx + overwritten A1), 2, 5.
|
|
if len(rows) != 3 {
|
|
t.Fatalf("expected 3 rows, got %d: %v", len(rows), rows)
|
|
}
|
|
if rows[0][0] != "first" {
|
|
t.Errorf("row 1 col A: got %q, want %q", rows[0][0], "first")
|
|
}
|
|
if rows[1][0] != "second" {
|
|
t.Errorf("row 2 col A: got %q, want %q", rows[1][0], "second")
|
|
}
|
|
if len(rows[1]) < 2 || rows[1][1] != "=A2" {
|
|
t.Errorf("row 2 col B: got %v, want formula \"=A2\"", rows[1])
|
|
}
|
|
if rows[2][0] != "fifth" {
|
|
t.Errorf("row 5 col A: got %q, want %q", rows[2][0], "fifth")
|
|
}
|
|
|
|
// A real reader (Excel included) cares about the underlying
|
|
// element order, not just what this package's own reader derives —
|
|
// confirm the raw XML itself is in ascending row/column order.
|
|
raw, ok, err := readZipPart(out, xlsxSheetPart(1))
|
|
if err != nil || !ok {
|
|
t.Fatalf("readZipPart sheet1: ok=%v err=%v", ok, err)
|
|
}
|
|
body := string(raw)
|
|
idxA1 := strings.Index(body, `r="A1"`)
|
|
idxA2 := strings.Index(body, `r="A2"`)
|
|
idxB2 := strings.Index(body, `r="B2"`)
|
|
idxA5 := strings.Index(body, `r="A5"`)
|
|
if idxA1 < 0 || idxA2 < 0 || idxB2 < 0 || idxA5 < 0 {
|
|
t.Fatalf("expected every written cell ref present in raw XML:\n%s", body)
|
|
}
|
|
if !(idxA1 < idxA2 && idxA2 < idxB2 && idxB2 < idxA5) {
|
|
t.Errorf("expected ascending order A1 < A2 < B2 < A5 in raw XML, got offsets %d,%d,%d,%d:\n%s",
|
|
idxA1, idxA2, idxB2, idxA5, body)
|
|
}
|
|
}
|
|
|
|
// TestSetXlsxCellsManyNewRowsNoDataLoss is a regression test for a bug
|
|
// where SetXlsxCells indexed newly-created rows by a *pointer* taken
|
|
// at append time. Go's append reallocates a slice's backing array
|
|
// once its capacity is exceeded, which silently strands any
|
|
// previously-taken pointer into the old array — a write applied
|
|
// through it returns no error but never reaches the file. That bug
|
|
// only showed up when (a) more than one cell landed in the same
|
|
// brand-new row and (b) a slice reallocation happened to fall between
|
|
// those two writes, so it passed or failed depending on Go's
|
|
// (deliberately randomized) map iteration order. This test forces
|
|
// many reallocations (far more new rows than any small initial slice
|
|
// capacity) and writes two cells into every one of them, so a
|
|
// reintroduction of the pointer bug fails deterministically rather
|
|
// than intermittently.
|
|
func TestSetXlsxCellsManyNewRowsNoDataLoss(t *testing.T) {
|
|
data, err := BuildXlsx([]Sheet{{Name: "Sheet1", Rows: [][]string{{"seed"}}}})
|
|
if err != nil {
|
|
t.Fatalf("BuildXlsx: %v", err)
|
|
}
|
|
|
|
const numRows = 200
|
|
writes := make(map[string]string, numRows*2)
|
|
for r := 2; r <= numRows+1; r++ {
|
|
writes[fmt.Sprintf("A%d", r)] = fmt.Sprintf("label-%d", r)
|
|
writes[fmt.Sprintf("B%d", r)] = fmt.Sprintf("=A%d", r)
|
|
}
|
|
|
|
out, err := SetXlsxCells(data, "", writes)
|
|
if err != nil {
|
|
t.Fatalf("SetXlsxCells: %v", err)
|
|
}
|
|
|
|
rows, _, err := ReadXlsxCells(out, "")
|
|
if err != nil {
|
|
t.Fatalf("ReadXlsxCells: %v", err)
|
|
}
|
|
if len(rows) != numRows+1 { // +1 for the seeded row 1
|
|
t.Fatalf("expected %d rows, got %d", numRows+1, len(rows))
|
|
}
|
|
for r := 2; r <= numRows+1; r++ {
|
|
row := rows[r-1]
|
|
if len(row) != 2 {
|
|
t.Fatalf("row %d: expected 2 cells, got %d: %v", r, len(row), row)
|
|
}
|
|
wantA := fmt.Sprintf("label-%d", r)
|
|
wantB := fmt.Sprintf("=A%d", r)
|
|
if row[0] != wantA {
|
|
t.Errorf("row %d col A: got %q, want %q", r, row[0], wantA)
|
|
}
|
|
if row[1] != wantB {
|
|
t.Errorf("row %d col B: got %q, want %q", r, row[1], wantB)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPptxReadRoundTrip(t *testing.T) {
|
|
data, err := BuildPptx([]Slide{
|
|
{Title: "Welcome"},
|
|
{Title: "Agenda", Bullets: []string{"Point one", "Point two & three"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildPptx: %v", err)
|
|
}
|
|
slides, err := ReadPptxSlides(data)
|
|
if err != nil {
|
|
t.Fatalf("ReadPptxSlides: %v", err)
|
|
}
|
|
if len(slides) != 2 {
|
|
t.Fatalf("expected 2 slides, got %d", len(slides))
|
|
}
|
|
if slides[0].Title != "Welcome" {
|
|
t.Errorf("slide 1 title: got %q", slides[0].Title)
|
|
}
|
|
if slides[1].Title != "Agenda" || len(slides[1].Bullets) != 2 || slides[1].Bullets[1] != "Point two & three" {
|
|
t.Errorf("slide 2: got %+v", slides[1])
|
|
}
|
|
}
|