oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package export_test
2
3import (
4 "math"
5 "sort"
6 "testing"
7
8 "github.com/carlosframework/docs/internal/export"
9)
10
11// The whole point of PageOne is that it is the SAME layout the PDF
12// runs, not a second one that agrees with it by inspection. These
13// tests hold it to that: the geometry it reports has to be geometry
14// the exporter would really have drawn.
15
16func TestPageOneReportsALineForEachLineOfText(t *testing.T) {
17 js := `{"type":"doc","content":[
18 {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"A heading"}]},
19 {"type":"paragraph","content":[{"type":"text","text":"One line of body text."}]}
20 ]}`
21 // Three lines: the title (set as an h1 by PDF and PageOne alike),
22 // the heading, and the paragraph.
23 lines := export.PageOne(mustParse(t, "Notes", js))
24 if len(lines) != 3 {
25 t.Fatalf("got %d lines, want 3: %+v", len(lines), lines)
26 }
27 for i, l := range lines {
28 if l.Height <= 0 || l.Width <= 0 {
29 t.Errorf("line %d has no extent: %+v", i, l)
30 }
31 if l.Top < export.MarginPt {
32 t.Errorf("line %d starts above the top margin: %+v", i, l)
33 }
34 if l.Left < export.MarginPt {
35 t.Errorf("line %d starts left of the margin: %+v", i, l)
36 }
37 }
38}
39
40// A document with no content at all still has its title, and nothing
41// else — an empty document must not report a page full of phantom
42// lines.
43func TestPageOneOfAnEmptyDocumentIsJustItsTitle(t *testing.T) {
44 lines := export.PageOne(mustParse(t, "Untitled document", `{"type":"doc","content":[]}`))
45 if len(lines) != 1 {
46 t.Fatalf("got %d lines, want 1 (the title): %+v", len(lines), lines)
47 }
48}
49
50// An untitled, contentless document reports nothing, which is what
51// lets the list render its empty box rather than a one-bar preview.
52func TestPageOneOfATrulyEmptyDocumentIsEmpty(t *testing.T) {
53 lines := export.PageOne(mustParse(t, "", `{"type":"doc","content":[]}`))
54 if len(lines) != 0 {
55 t.Fatalf("got %d lines, want none: %+v", len(lines), lines)
56 }
57}
58
59// The bound that makes this affordable on a list page: a document of
60// any length costs one page of layout, not all of it.
61func TestPageOneStopsAtTheEndOfTheFirstPage(t *testing.T) {
62 js := `{"type":"doc","content":[`
63 for i := 0; i < 400; i++ {
64 if i > 0 {
65 js += ","
66 }
67 js += `{"type":"paragraph","content":[{"type":"text","text":"A paragraph."}]}`
68 }
69 js += `]}`
70
71 lines := export.PageOne(mustParse(t, "Long", js))
72 if len(lines) == 0 {
73 t.Fatal("no lines at all")
74 }
75 // 648pt of content box at 16.8pt a line is 38-ish; the exact
76 // number is the layout's business, but 400 paragraphs must not
77 // all be here and none may sit below the bottom margin.
78 if len(lines) > 60 {
79 t.Errorf("got %d lines — that is more than one page's worth", len(lines))
80 }
81 for i, l := range lines {
82 if l.Top+l.Height > export.PageHeightPt-export.MarginPt+0.01 {
83 t.Errorf("line %d runs past the bottom margin: %+v", i, l)
84 }
85 }
86}
87
88// A heading's line reports a bigger size than a body line's. This is
89// what lets a thumbnail draw a heading heavier without the drawing
90// code having to know what a heading is.
91func TestPageOneReportsTheSizeOfEachLine(t *testing.T) {
92 js := `{"type":"doc","content":[
93 {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Big"}]},
94 {"type":"paragraph","content":[{"type":"text","text":"Small"}]}
95 ]}`
96 lines := export.PageOne(mustParse(t, "", js))
97 if len(lines) != 2 {
98 t.Fatalf("got %d lines, want 2: %+v", len(lines), lines)
99 }
100 if lines[0].Size <= lines[1].Size {
101 t.Errorf("heading size %v is not bigger than body size %v", lines[0].Size, lines[1].Size)
102 }
103 if lines[1].Size != export.BodySizePt {
104 t.Errorf("body line size %v, want %v", lines[1].Size, float64(export.BodySizePt))
105 }
106}
107
108// A centred line is reported where it was actually placed, not at the
109// margin — the thumbnail shows alignment because the layout already
110// knows it.
111func TestPageOneReportsAlignment(t *testing.T) {
112 js := `{"type":"doc","content":[
113 {"type":"paragraph","content":[{"type":"text","text":"Left."}]},
114 {"type":"paragraph","attrs":{"align":"center"},"content":[{"type":"text","text":"Middle."}]}
115 ]}`
116 lines := export.PageOne(mustParse(t, "", js))
117 if len(lines) != 2 {
118 t.Fatalf("got %d lines, want 2: %+v", len(lines), lines)
119 }
120 if lines[0].Left != export.MarginPt {
121 t.Errorf("left-aligned line at %v, want the margin %v", lines[0].Left, float64(export.MarginPt))
122 }
123 if lines[1].Left <= lines[0].Left {
124 t.Errorf("centred line at %v is not indented past the left-aligned one at %v", lines[1].Left, lines[0].Left)
125 }
126}
127
128// A test named TestPageOneDoesNotDisturbPDFExport stood here. It
129// exported a document, ran PageOne, exported it again and compared the
130// bytes — and it could not fail. PDF and PageOne build separate layout
131// values sharing no state, so only a package-level global could ever
132// have reddened it; a deliberate `l.cursor += 100` inside drawLine's
133// sink block left it green while reddening the agreement test below.
134//
135// Deleted rather than repaired, because a check that cannot
136// distinguish "no" from "I can't see it" is worse than no check: two
137// comments and a commit message cited it as the thing holding the sink
138// additive, which it never was. The exporter's own suite covers its
139// output, and TestPageOneMatchesTheFirstPageOfThePDF covers the
140// direction that can actually drift.
141
142// The test PageOne's own comment promises, and the one that makes "the
143// same layout, not a second opinion" a mechanism rather than a claim:
144// every line PageOne reports has to be a line the PDF exporter really
145// drew, at the same place.
146//
147// It reads the PDF back rather than trusting the layout twice. A run's
148// text matrix gives the baseline it was drawn at and the x it started
149// from; PageOne reports the top of the line box and its left edge. The
150// baseline must fall inside that box, and the left edges must be equal.
151//
152// Deliberately no list in the fixture: a list marker is drawn at an x
153// to the LEFT of its item's text (in the list's own indent), so the
154// leftmost run on that line is the marker rather than the line itself,
155// and this test's "leftmost run starts where the line starts" would be
156// comparing the wrong two numbers. That is a real gap, named rather
157// than papered over — a list's own geometry is not held by anything
158// here, and the pretext that another test covered it was false.
159func TestPageOneMatchesTheFirstPageOfThePDF(t *testing.T) {
160 js := `{"type":"doc","content":[
161 {"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"A heading that is long enough to be interesting"}]},
162 {"type":"paragraph","content":[{"type":"text","text":"A paragraph long enough that it has to wrap onto a second line, which is the case where an estimator and a real layout would disagree about where the break falls."}]},
163 {"type":"paragraph","attrs":{"align":"center"},"content":[{"type":"text","text":"Centred."}]},
164 {"type":"paragraph","attrs":{"align":"right"},"content":[{"type":"text","text":"Right."}]}
165 ]}`
166 doc := mustParse(t, "Notes", js)
167
168 lines := export.PageOne(doc)
169 data, _ := mustPDF(t, doc)
170 f := readPDF(t, data)
171 pages := f.pages(t)
172 if len(pages) != 1 {
173 t.Fatalf("fixture spilled onto %d pages; it is meant to fit on one", len(pages))
174 }
175
176 // Group the drawn runs into lines by baseline, keeping the leftmost
177 // x on each.
178 type drawn struct{ baseline, left float64 }
179 var byBaseline []drawn
180 for _, r := range f.runs(t, pages[0]) {
181 baseline := float64(export.PageHeightPt) - r.y
182 found := false
183 for i := range byBaseline {
184 if math.Abs(byBaseline[i].baseline-baseline) < 0.01 {
185 if r.x < byBaseline[i].left {
186 byBaseline[i].left = r.x
187 }
188 found = true
189 break
190 }
191 }
192 if !found {
193 byBaseline = append(byBaseline, drawn{baseline: baseline, left: r.x})
194 }
195 }
196 sort.Slice(byBaseline, func(i, j int) bool { return byBaseline[i].baseline < byBaseline[j].baseline })
197
198 if len(byBaseline) != len(lines) {
199 t.Fatalf("PageOne reports %d lines, the PDF drew %d", len(lines), len(byBaseline))
200 }
201 for i, d := range byBaseline {
202 l := lines[i]
203 if d.baseline < l.Top || d.baseline > l.Top+l.Height {
204 t.Errorf("line %d: baseline %.2f is outside the reported box [%.2f, %.2f]",
205 i, d.baseline, l.Top, l.Top+l.Height)
206 }
207 if math.Abs(d.left-l.Left) > 0.01 {
208 t.Errorf("line %d: the PDF drew it at x=%.2f, PageOne reports Left=%.2f", i, d.left, l.Left)
209 }
210 }
211}
212