oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import (
4 "bytes"
5 "context"
6 "encoding/xml"
7 "errors"
8 "io"
9 "net/http/httptest"
10 "testing"
11 "time"
12
13 "amadan.net/oficina/slides/internal/slides/deck"
14 "amadan.net/oficina/slides/internal/slides/layout"
15 "amadan.net/oficina/slides/internal/slides/svg"
16)
17
18// sampleView builds the deck a person meets on their first sign-in —
19// every flow template, both layout modes, and the block types worth
20// knowing about — and opens it.
21//
22// The sample rather than a hand-built fixture, because it is the one
23// deck in the repo that goes through the core the way a real one does:
24// the ops are applied by the wasm module, the document comes back out
25// of it, and what these tests measure is therefore a view the app could
26// actually be asked to draw.
27func sampleView(t *testing.T) (*app, Deck, *deck.Deck) {
28 t.Helper()
29 d := testDB(t)
30 a, _, err := newApp(d, Config{Env: EnvDevelopment, Logger: quietLogger()})
31 if err != nil {
32 t.Fatalf("newApp: %v", err)
33 }
34 t.Cleanup(func() { a.Close() })
35 ctx := context.Background()
36
37 owner, _, err := ensureUser(ctx, d.G, "paul@example.test")
38 if err != nil {
39 t.Fatalf("ensureUser: %v", err)
40 }
41 if err := a.seedSampleDeck(ctx, owner); err != nil {
42 t.Fatalf("seedSampleDeck: %v", err)
43 }
44 var row Deck
45 if err := d.G.WithContext(ctx).Where("owner_id = ?", owner.ID).Take(&row).Error; err != nil {
46 t.Fatalf("load the sample deck: %v", err)
47 }
48 h, view, err := a.openDeck(ctx, row)
49 if err != nil {
50 t.Fatalf("openDeck: %v", err)
51 }
52 t.Cleanup(func() { h.Close(context.Background()) })
53 // Eight, not the nine the slice 3 plan says: the nine is
54 // internal/slides/layout's own fixture, which has a canvas slide
55 // AND a reflowing one. sample.go's last slide is both. The number
56 // is asserted so a slide quietly dropped from the sample does not
57 // quietly shrink what these tests cover.
58 if len(view.Slides) != 8 {
59 t.Fatalf("the sample deck has %d slides, want 8", len(view.Slides))
60 }
61 return a, row, view
62}
63
64// Every slide of the sample deck comes out as XML a parser will read.
65// This is the whole promise of internal/slides/svg said against real
66// content: every flow template, both layout modes and every
67// block type in §2.4, each parsed back rather than eyeballed.
68func TestEverySampleSlideIsWellFormedSVG(t *testing.T) {
69 a, row, view := sampleView(t)
70 r := httptest.NewRequest("GET", "/d/"+row.Sqid+"/slides/x.svg", nil)
71
72 for i, s := range view.Slides {
73 out, err := a.thumbnail(r, row, view, i, "/d/"+row.Sqid)
74 if err != nil {
75 t.Fatalf("slide %d (%s): %v", i, s.ID, err)
76 }
77 dec := xml.NewDecoder(bytes.NewReader(out))
78 var depth, elements int
79 for {
80 tok, err := dec.Token()
81 if errors.Is(err, io.EOF) {
82 break
83 }
84 if err != nil {
85 t.Fatalf("slide %d (%s, template %q) is not well-formed XML: %v\n%s",
86 i, s.ID, s.Template, err, out)
87 }
88 switch tok.(type) {
89 case xml.StartElement:
90 depth++
91 elements++
92 case xml.EndElement:
93 depth--
94 }
95 }
96 if depth != 0 {
97 t.Errorf("slide %d closes at depth %d", i, depth)
98 }
99 // A slide that drew nothing at all would still parse. Every
100 // one of the sample's slides has words on it.
101 if elements < 3 {
102 t.Errorf("slide %d (template %q) drew %d elements; it looks empty:\n%s",
103 i, s.Template, elements, out)
104 }
105 if !bytes.Contains(out, []byte(`role="img"`)) || !bytes.Contains(out, []byte("<title>")) {
106 t.Errorf("slide %d has no accessible name:\n%s", i, out)
107 }
108 }
109}
110
111// Every slide, measured and written, each inside 50ms on this machine.
112//
113// A generous bound on purpose: the number that matters is the order of
114// magnitude, because the editor strip draws one per slide after every
115// save and the export draws one per page. If a slide ever takes half a
116// second, something has started re-reading the font programs per run —
117// which is exactly the mistake this catches and nothing else would.
118func TestEverySampleSlideEncodesQuickly(t *testing.T) {
119 if testing.Short() {
120 t.Skip("timing test")
121 }
122 _, _, view := sampleView(t)
123
124 // Once through first: fonts.Parsed caches a parsed face, and
125 // timing the first call would be timing the cache being filled,
126 // which happens once per process and not per thumbnail.
127 for i := range view.Slides {
128 if _, _, err := layout.Layout(view, i); err != nil {
129 t.Fatalf("warm-up slide %d: %v", i, err)
130 }
131 }
132
133 const budget = 50 * time.Millisecond
134 for i, s := range view.Slides {
135 start := time.Now()
136 paint, _, err := layout.Layout(view, i)
137 if err != nil {
138 t.Fatalf("slide %d: %v", i, err)
139 }
140 out := svg.Encode(paint, svg.Options{Title: "Slide"})
141 took := time.Since(start)
142 if took > budget {
143 t.Errorf("slide %d (template %q) took %v to measure and write, want under %v",
144 i, s.Template, took, budget)
145 }
146 if len(out) == 0 {
147 t.Errorf("slide %d wrote nothing", i)
148 }
149 }
150}
151
152// ?w= is read and is in the tag, and is otherwise ignored: an SVG
153// scales, so the bytes are the same at every width. It is in the tag so
154// a raster twin can land later without every cache in between already
155// holding one size under a key that does not mention it.
156func TestTheRequestedWidthIsReadAndBounded(t *testing.T) {
157 for _, c := range []struct{ raw, want string }{
158 {"", ""},
159 {"180", "180"},
160 {" 900 ", "900"},
161 {"0", ""},
162 {"-4", ""},
163 {"9999999", ""},
164 {"wide", ""},
165 } {
166 if got := thumbWidth(c.raw); got != c.want {
167 t.Errorf("thumbWidth(%q) = %q, want %q", c.raw, got, c.want)
168 }
169 }
170}
171