package office
import (
"archive/zip"
"bytes"
"image"
"image/color"
"image/png"
"strings"
"testing"
)
const wNS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ` +
`xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ` +
`xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" ` +
`xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ` +
`xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"`
// testDocx zips a minimal package: the body XML plus any extra parts
func testDocx(t *testing.T, body string, parts map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
all := map[string]string{
"word/document.xml": `` + body +
``,
}
for k, v := range parts {
all[k] = v
}
for name, content := range all {
f, err := zw.Create(name)
if err != nil {
t.Fatalf("zip create: %v", err)
}
if _, err := f.Write([]byte(content)); err != nil {
t.Fatalf("zip write: %v", err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("zip close: %v", err)
}
return buf.Bytes()
}
func parseTestDocx(t *testing.T, body string, parts map[string]string) *Document {
t.Helper()
doc, err := ParseDocx(testDocx(t, body, parts))
if err != nil {
t.Fatalf("ParseDocx: %v", err)
}
return doc
}
func TestOnOff(t *testing.T) {
tests := []struct {
xml string
set, on bool
}{
{``, true, true},
{``, true, true},
{``, true, true},
{``, true, false},
{``, true, false},
{``, true, false},
}
for _, tc := range tests {
n, err := parseXMLTree([]byte(`` + tc.xml + ``))
if err != nil {
t.Fatalf("%s: %v", tc.xml, err)
}
got := onOff(n.first("b"))
if got.set != tc.set || got.v != tc.on {
t.Errorf("onOff(%s) = %+v, want set=%v on=%v", tc.xml, got, tc.set, tc.on)
}
}
if got := onOff(nil); got.set {
t.Errorf("onOff(nil) = %+v, want unset", got)
}
}
func TestFormatListNumber(t *testing.T) {
tests := []struct {
v int
fmt string
want string
}{
{3, "decimal", "3"},
{1, "lowerLetter", "a"},
{28, "lowerLetter", "bb"},
{2, "upperLetter", "B"},
{4, "lowerRoman", "iv"},
{1994, "upperRoman", "MCMXCIV"},
{7, "decimalZero", "07"},
{12, "decimalZero", "12"},
{5, "bullet", ""},
}
for _, tc := range tests {
if got := formatListNumber(tc.v, tc.fmt); got != tc.want {
t.Errorf("formatListNumber(%d, %s) = %q, want %q", tc.v, tc.fmt, got, tc.want)
}
}
}
func TestPictureTurns(t *testing.T) {
tests := []struct {
rot float64
turns int
ok bool
}{
{0, 0, true},
{5400000, 1, true},
{10800000, 2, true},
{16200000, 3, true},
{-5400000, 3, true},
{21600000, 0, true},
{5430000, 1, true}, // within a degree
{2700000, 0, false},
}
for _, tc := range tests {
turns, ok := pictureTurns(tc.rot)
if ok != tc.ok || (ok && turns != tc.turns) {
t.Errorf("pictureTurns(%v) = %d,%v want %d,%v", tc.rot, turns, ok, tc.turns, tc.ok)
}
}
}
func TestOrientInsets(t *testing.T) {
in := [4]float64{1, 2, 3, 4} // top right bottom left
tests := []struct {
name string
turns int
flipH, flipV bool
want [4]float64
}{
{"none", 0, false, false, [4]float64{1, 2, 3, 4}},
{"quarter", 1, false, false, [4]float64{4, 1, 2, 3}},
{"half", 2, false, false, [4]float64{3, 4, 1, 2}},
{"three quarters", 3, false, false, [4]float64{2, 3, 4, 1}},
{"mirror", 0, true, false, [4]float64{1, 4, 3, 2}},
{"flip", 0, false, true, [4]float64{3, 2, 1, 4}},
}
for _, tc := range tests {
if got := orientInsets(in, tc.turns, tc.flipH, tc.flipV); got != tc.want {
t.Errorf("%s: orientInsets = %v, want %v", tc.name, got, tc.want)
}
}
}
func TestOrientPixels(t *testing.T) {
// a 3x2 picture with a red top-left pixel
src := image.NewNRGBA(image.Rect(0, 0, 3, 2))
red := color.NRGBA{R: 255, A: 255}
src.SetNRGBA(0, 0, red)
tests := []struct {
name string
turns int
flipH, flipV bool
w, h, rx, ry int
}{
{"quarter turn", 1, false, false, 2, 3, 1, 0},
{"half turn", 2, false, false, 3, 2, 2, 1},
{"three quarters", 3, false, false, 2, 3, 0, 2},
{"mirror", 0, true, false, 3, 2, 2, 0},
{"flip", 0, false, true, 3, 2, 0, 1},
}
for _, tc := range tests {
out := orientPixels(src, tc.turns, tc.flipH, tc.flipV)
if out.Bounds().Dx() != tc.w || out.Bounds().Dy() != tc.h {
t.Errorf("%s: size %v, want %dx%d", tc.name, out.Bounds().Size(), tc.w, tc.h)
continue
}
if out.NRGBAAt(tc.rx, tc.ry) != red {
t.Errorf("%s: red pixel not at (%d,%d)", tc.name, tc.rx, tc.ry)
}
}
}
func TestDocxReaderRunAndParagraphRules(t *testing.T) {
tests := []struct {
name, body string
want []string
dont []string
}{
{
name: "explicit off toggle",
body: `plain`,
want: []string{`>plain
`},
dont: []string{"font-weight:700"},
},
{
name: "spacing before and after",
body: `x`,
want: []string{"padding-top:12pt;margin-bottom:6pt;"},
},
{
name: "exact line height",
body: `x`,
want: []string{`data-lsexact="18pt"`},
},
{
name: "a leading space inside the text collapses as usual",
body: `one two`,
dont: []string{"pre-wrap"},
},
{
name: "a leading space at the start is kept",
body: ` lead`,
want: []string{"white-space:pre-wrap"},
},
{
name: "right tab stop with leader",
body: `a1`,
want: []string{`data-tabs="right:451.3:dot"`, `` + "\t" + `1`},
},
{
name: "page break splits the paragraph",
body: `beforeafter`,
want: []string{`>beforeafter`},
},
{
name: "non-breaking and soft hyphens",
body: `
abc`,
want: []string{"a\u2011b\u00adc"},
},
}
for _, tc := range tests {
doc := parseTestDocx(t, tc.body, nil)
for _, w := range tc.want {
if !strings.Contains(doc.HTML, w) {
t.Errorf("%s: want %q in %s", tc.name, w, doc.HTML)
}
}
for _, d := range tc.dont {
if strings.Contains(doc.HTML, d) {
t.Errorf("%s: did not want %q in %s", tc.name, d, doc.HTML)
}
}
}
}
func TestDocxReaderFootnotes(t *testing.T) {
body := `
text`
notes := `
` +
`` +
` the note`
doc := parseTestDocx(t, body, map[string]string{"word/footnotes.xml": notes})
if !strings.Contains(doc.HTML, `
1`) {
t.Errorf("footnote reference not imported: %s", doc.HTML)
}
if strings.Count(doc.HTML, "
` +
``
picture := func(rot string) string {
return `` +
`` +
`` +
``
}
tests := []struct {
name, rot, want string
}{
{"upright", ``, "width:40pt;height:20pt;object-fit:fill;object-view-box:inset(10% 0% 0% 0%)"},
{"quarter turn", `rot="5400000"`, "width:20pt;height:40pt;object-fit:fill;object-view-box:inset(0% 10% 0% 0%)"},
}
for _, tc := range tests {
zipData := testDocx(t, picture(tc.rot), map[string]string{
"word/_rels/document.xml.rels": rels,
})
// the media part is binary: add it by rebuilding with the bytes
doc := parseDocxWithMedia(t, zipData, "word/media/p.png", pngBuf.Bytes())
if !strings.Contains(doc.HTML, tc.want) {
t.Errorf("%s: want %q in %.400s", tc.name, tc.want, doc.HTML)
}
}
}
// parseDocxWithMedia adds one binary part to a test package and parses it
func parseDocxWithMedia(t *testing.T, zipData []byte, name string, data []byte) *Document {
t.Helper()
zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
t.Fatalf("zip: %v", err)
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for _, f := range zr.File {
rc, err := f.Open()
if err != nil {
t.Fatalf("zip open: %v", err)
}
w, _ := zw.Create(f.Name)
var b bytes.Buffer
if _, err := b.ReadFrom(rc); err != nil {
t.Fatalf("zip read: %v", err)
}
rc.Close()
w.Write(b.Bytes())
}
w, _ := zw.Create(name)
w.Write(data)
if err := zw.Close(); err != nil {
t.Fatalf("zip close: %v", err)
}
doc, err := ParseDocx(buf.Bytes())
if err != nil {
t.Fatalf("ParseDocx: %v", err)
}
return doc
}
func TestDocxRichRoundTrip(t *testing.T) {
// the editor's own model survives docx -> model unchanged
tests := []struct {
name, html string
}{
{"spacing and indents", `x
`},
{"exact line height", `x
`},
{"line spacing", `x
`},
{"keep with next", `x
`},
{"tab stops", `a` + "\t" + `1
`},
{"footnote reference", `a1
`},
{"page field", `1
`},
{"shaded paragraph", `x
`},
{"trailing line break", `x
`},
{"horizontal rule", `a
b
`},
{"bordered paragraph", `x
`},
}
for _, tc := range tests {
src := &Document{HTML: tc.html, Footnotes: []Footnote{{ID: "1", HTML: "note
"}}}
data, err := BuildDocx(src)
if err != nil {
t.Fatalf("%s: BuildDocx: %v", tc.name, err)
}
back, err := ParseDocx(data)
if err != nil {
t.Fatalf("%s: ParseDocx: %v", tc.name, err)
}
if !strings.Contains(back.HTML, tc.html) {
t.Errorf("%s: want %s, got %s", tc.name, tc.html, back.HTML)
}
}
}
func TestDocxWriterEditorBoxes(t *testing.T) {
tests := []struct {
name, html string
want []string
}{
{
// docs.css: 3px rule, 12px padding, 8pt + 2pt either side
name: "blockquote",
html: `quoted
`,
want: []string{``,
``},
},
{
// a 1px frame with 10px 12px inside: the half point w:space
// cannot hold goes to the spacing outside
name: "code block",
html: `code
`,
want: []string{``,
``,
``, `w:ascii="Consolas"`},
},
{
name: "an indent blockquote from the browser keeps no rule",
html: `x
`,
want: []string{`| H |
`,
want: []string{``, ``},
},
{
name: "imported table stays a Word table",
html: ``,
want: []string{``},
},
{
name: "rule",
html: `
`,
want: []string{``, `w:line="20" w:lineRule="exact"`},
},
}
for _, tc := range tests {
data, err := BuildDocx(&Document{HTML: tc.html})
if err != nil {
t.Fatalf("%s: BuildDocx: %v", tc.name, err)
}
body := string(zipPart(t, data, "word/document.xml"))
for _, w := range tc.want {
if !strings.Contains(body, w) {
t.Errorf("%s: want %s in %s", tc.name, w, body)
}
}
}
// the indent blockquote draws no rule
data, _ := BuildDocx(&Document{HTML: tests[2].html})
if body := string(zipPart(t, data, "word/document.xml")); strings.Contains(body, "") {
t.Errorf("border:none blockquote got a rule: %s", body)
}
}
func TestDocxPlainHeaderStaysPlain(t *testing.T) {
data, err := BuildDocx(&Document{HTML: "x
", Header: "Top & tail", Footer: "Bottom"})
if err != nil {
t.Fatalf("BuildDocx: %v", err)
}
back, err := ParseDocx(data)
if err != nil {
t.Fatalf("ParseDocx: %v", err)
}
if back.Header != "Top & tail" || back.HeaderHTML != "" {
t.Errorf("header = %q / %q, want plain text", back.Header, back.HeaderHTML)
}
if back.Footer != "Bottom" || back.FooterHTML != "" || back.PageNumbers {
t.Errorf("footer = %q / %q pn=%v, want plain text", back.Footer, back.FooterHTML, back.PageNumbers)
}
}