oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package deck_test
2
3import (
4 "testing"
5
6 "amadan.net/oficina/slides/internal/slides/deck"
7)
8
9// viewFixture is the core's own view JSON (core/src/view.rs's module
10// comment, verbatim in shape), widened to cover every corner Parse has
11// to survive: a flow slide and a canvas one, a block with marked runs,
12// a block type that carries no `text` key at all, a phone override, a
13// registered asset, and the two meta keys that arrive as null.
14//
15// A literal rather than a call into the wasm core: this package must
16// parse what the core emits, and the core's own tests already prove
17// what it emits. Coupling this test to the module would mean a change
18// in the parser could only be tested by also loading 2MB of wasm.
19const viewFixture = `{
20 "meta": {"title":"Quarter review","theme":"ink","accent":"#2f6f4f",
21 "fonts":{"heading":"Inter","body":"Inter"},"aspect":"16:9"},
22 "slides": [
23 {"id":"aaaaaaaaaaaa","layout":"flow","template":"title-body",
24 "background":{"kind":"theme","value":""},
25 "phone":"inherit","transition":"none",
26 "notes":[[{"t":"Open with the number, not the story."}]],
27 "blocks":[
28 {"id":"bbbbbbbbbbbb","type":"heading","props":{"level":1},
29 "region":"heading","box":null,"z":0,"phone":{},
30 "text":[[{"t":"Hello ","b":true},{"t":"world"}]]},
31 {"id":"cccccccccccc","type":"text","props":{"size":"m"},
32 "region":"body","box":null,"z":0,"phone":{},
33 "text":[[{"t":"First"}],[{"t":"Second","i":true}]]},
34 {"id":"dddddddddddd","type":"divider","props":{},
35 "region":"body","box":null,"z":0,"phone":{}}
36 ]},
37 {"id":"eeeeeeeeeeee","layout":"canvas","template":"blank",
38 "background":{"kind":"color","value":"#101014"},
39 "phone":"reflow","transition":"fade","notes":[],
40 "blocks":[
41 {"id":"ffffffffffff","type":"shape","props":{"kind":"rect"},
42 "region":null,"box":{"x":0.06,"y":0.3,"w":0.4,"h":0.5},"z":2,
43 "phone":{"hidden":true,"order":1,"size":"s",
44 "box":{"x":0,"y":0,"w":1,"h":0.4}},
45 "text":[[{"t":"label"}]]}
46 ]}
47 ],
48 "assets": {"a1":{"ref":"sha256:beef","name":"chart.png",
49 "width":1200,"height":800,"alt":"Revenue by month"}}
50}`
51
52func TestParseViewFixture(t *testing.T) {
53 d, err := deck.Parse([]byte(viewFixture))
54 if err != nil {
55 t.Fatalf("Parse: %v", err)
56 }
57
58 if d.Meta.Title != "Quarter review" || d.Meta.Theme != "ink" || d.Meta.Aspect != "16:9" {
59 t.Errorf("meta = %+v", d.Meta)
60 }
61 if d.Meta.Accent != "#2f6f4f" {
62 t.Errorf("meta.accent = %q, want %q", d.Meta.Accent, "#2f6f4f")
63 }
64 if d.Meta.Fonts == nil || d.Meta.Fonts.Heading != "Inter" {
65 t.Errorf("meta.fonts = %+v, want the Inter pair", d.Meta.Fonts)
66 }
67 if len(d.Slides) != 2 {
68 t.Fatalf("parsed %d slides, want 2", len(d.Slides))
69 }
70
71 flow := d.Slides[0]
72 if flow.ID != "aaaaaaaaaaaa" || flow.Layout != "flow" || flow.Template != "title-body" {
73 t.Errorf("flow slide = %+v", flow)
74 }
75 if flow.Background.Kind != "theme" {
76 t.Errorf("flow background = %+v", flow.Background)
77 }
78 if got := flow.Notes.Plain(); got != "Open with the number, not the story." {
79 t.Errorf("notes.Plain() = %q", got)
80 }
81 if len(flow.Blocks) != 3 {
82 t.Fatalf("flow slide has %d blocks, want 3", len(flow.Blocks))
83 }
84
85 heading := flow.Blocks[0]
86 if heading.Type != "heading" || heading.Region != "heading" {
87 t.Errorf("heading block = %+v", heading)
88 }
89 if heading.Box != nil {
90 t.Errorf("heading block box = %+v, want nil on a flow slide", heading.Box)
91 }
92 if lvl, _ := heading.Props["level"].(float64); lvl != 1 {
93 t.Errorf("heading props = %v, want level 1", heading.Props)
94 }
95 // Runs concatenate inside a paragraph; marks ride along without
96 // changing the plain text.
97 if got := heading.Text.Plain(); got != "Hello world" {
98 t.Errorf("heading.Text.Plain() = %q, want %q", got, "Hello world")
99 }
100 if !heading.Text[0][0].B || heading.Text[0][1].B {
101 t.Errorf("heading runs lost their marks: %+v", heading.Text[0])
102 }
103
104 // Paragraphs join with a newline, which is what the plain-text
105 // deck page and every later exporter render between them.
106 if got := flow.Blocks[1].Text.Plain(); got != "First\nSecond" {
107 t.Errorf("text block Plain() = %q, want %q", got, "First\nSecond")
108 }
109 // A type with no `text` key at all parses to no paragraphs rather
110 // than failing: the view omits the key for non-text-bearing types.
111 if got := flow.Blocks[2].Text; got != nil {
112 t.Errorf("divider carries text %+v, want none", got)
113 }
114
115 canvas := d.Slides[1]
116 if canvas.Layout != "canvas" || canvas.Phone != "reflow" || canvas.Transition != "fade" {
117 t.Errorf("canvas slide = %+v", canvas)
118 }
119 if canvas.Background.Kind != "color" || canvas.Background.Value != "#101014" {
120 t.Errorf("canvas background = %+v", canvas.Background)
121 }
122 shape := canvas.Blocks[0]
123 if shape.Region != "" {
124 t.Errorf("canvas block region = %q, want empty (the view sends null)", shape.Region)
125 }
126 if shape.Box == nil || shape.Box.X != 0.06 || shape.Box.H != 0.5 {
127 t.Errorf("canvas block box = %+v", shape.Box)
128 }
129 if shape.Z != 2 {
130 t.Errorf("canvas block z = %d, want 2", shape.Z)
131 }
132 if !shape.Phone.Hidden || shape.Phone.Size != "s" {
133 t.Errorf("phone override = %+v", shape.Phone)
134 }
135 if shape.Phone.Order == nil || *shape.Phone.Order != 1 {
136 t.Errorf("phone override order = %v, want 1", shape.Phone.Order)
137 }
138 if shape.Phone.Box == nil || shape.Phone.Box.W != 1 {
139 t.Errorf("phone override box = %+v", shape.Phone.Box)
140 }
141
142 asset, ok := d.Assets["a1"]
143 if !ok {
144 t.Fatalf("assets = %+v, want an a1", d.Assets)
145 }
146 if asset.Ref != "sha256:beef" || asset.Width != 1200 || asset.Alt != "Revenue by month" {
147 t.Errorf("asset = %+v", asset)
148 }
149}
150
151// A deck a peer left half-written still has to render: the core fills
152// most defaults itself, but `z` is the one number a browser peer can
153// write as a fraction, and a whole deck failing to parse over it would
154// take the viewer down with it.
155func TestParseToleratesFractionalZ(t *testing.T) {
156 d, err := deck.Parse([]byte(`{"meta":{},"slides":[{"id":"a","blocks":[
157 {"id":"b","type":"shape","z":2.5}]}],"assets":{}}`))
158 if err != nil {
159 t.Fatalf("Parse: %v", err)
160 }
161 if got := d.Slides[0].Blocks[0].Z; got != 2 {
162 t.Errorf("z = %d, want 2 (2.5 truncated)", got)
163 }
164}
165
166func TestParseRejectsRubbish(t *testing.T) {
167 if _, err := deck.Parse([]byte("not json")); err == nil {
168 t.Fatal("Parse accepted a non-JSON body")
169 }
170}
171