oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3package slidestest
4
5// The image drive (plan task 6): add an image block, describe it, pick
6// a real PNG off disk, and watch it go browser → app → object store →
7// back onto the stage as an <img> the server rendered. Then reload, and
8// find it still there.
9//
10// It runs against the store development binds (blobs.Dir), which has no
11// presigning, so the PUT is the same-origin one this app accepts —
12// exactly the path scripts/run-dev.sh takes. The presigned half is
13// covered without a browser in assets_test.go, because a real bucket is
14// not something a test can have.
15
16import (
17 "os"
18 "path/filepath"
19 "strings"
20 "testing"
21
22 "github.com/chromedp/chromedp"
23)
24
25// TestBrowserUploadsAnImage is the whole §2.7 path in a real browser.
26func TestBrowserUploadsAnImage(t *testing.T) {
27 rig, mailer := newRigWithStore(t)
28 browserSignIn(t, rig, mailer)
29
30 // A real file on disk: chromedp hands the <input type="file"> a
31 // path, the same as a person picking one.
32 png := filepath.Join(t.TempDir(), "dot.png")
33 if err := os.WriteFile(png, onePixelPNG, 0o600); err != nil {
34 t.Fatalf("write the test PNG: %v", err)
35 }
36
37 var deck string
38 rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
39
40 const bound = `document.querySelector('[slides-stage] [data-block="heading"]')?.getAttribute('contenteditable') === 'true'`
41 const saved = `document.querySelector('[estilo-save-status]').dataset.state === 'saved'`
42 rig.Run(
43 chromedp.Navigate(rig.Origin+deck+"/edit"),
44 chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
45 waitFor(bound),
46 )
47 // With a store bound the page says so and the picker is live.
48 rig.Run(
49 waitFor(`document.querySelector('[slides-editor]').dataset.store === '1'`),
50 waitFor(`document.querySelector('[slides-asset-input]').disabled === false`),
51 )
52
53 // An image block, from the inspector's own Add block menu. The
54 // menu is a <details>, so it is opened before its button is
55 // pressed — the same two moves a person makes.
56 rig.Run(
57 chromedp.Evaluate(`(() => {
58 document.querySelector('[slides-add-block]').open = true
59 document.querySelector('[data-add-block="image"]').click()
60 return true
61 })()`, nil),
62 waitFor(`document.querySelector('[slides-stage] [data-block="image"]') !== null`),
63 waitFor(saved),
64 )
65 // An empty frame until there is something in it (§2.4's
66 // data-missing-asset), which is what the person is about to fix.
67 rig.Run(waitFor(`document.querySelector('[slides-stage] [data-block="image"]').dataset.missingAsset === 'true'`))
68 rig.Screen("[slides-editor]", "an empty image block")
69
70 // Describe it first: the file input refuses to move bytes for an
71 // image with no alt text and no decorative mark, and says so.
72 rig.Run(
73 chromedp.SetUploadFiles(`[slides-asset-input]`, []string{png}, chromedp.ByQuery),
74 waitFor(`document.querySelector('[slides-asset-status]').dataset.state === 'error'`),
75 waitFor(`document.querySelector('[slides-asset-status]').textContent.trim().length > 0`),
76 )
77 var refused string
78 rig.Run(chromedp.Text(`[slides-asset-status]`, &refused, chromedp.ByQuery))
79 if refused == "" || strings.HasPrefix(refused, "slides.") {
80 t.Fatalf("the refusal reads %q, want a resolved catalog sentence", refused)
81 }
82
83 rig.Run(
84 setValue(`#rst-block-alt`, "A single red dot"),
85 waitFor(saved),
86 waitFor(`document.querySelector('[slides-stage] [data-block="image"]') !== null`),
87 )
88
89 // Now the upload. The stage's <img> is the SERVER's rendering,
90 // re-fetched after the op saved — the browser never draws a slide.
91 rig.Run(
92 chromedp.SetUploadFiles(`[slides-asset-input]`, []string{png}, chromedp.ByQuery),
93 waitFor(`document.querySelector('[slides-stage] [data-block="image"] img') !== null`),
94 waitFor(saved),
95 )
96 rig.Screen("[slides-editor]", "the image on the stage")
97
98 var src, alt string
99 rig.Run(
100 chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "src", &src, nil, chromedp.ByQuery),
101 chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "alt", &alt, nil, chromedp.ByQuery),
102 )
103 wantPrefix := deck + "/assets/"
104 if !strings.HasPrefix(src, wantPrefix) {
105 t.Errorf("the image src is %q, want it under %q", src, wantPrefix)
106 }
107 if alt != "A single red dot" {
108 t.Errorf("alt = %q, want the description that was typed", alt)
109 }
110 // The ref in the src is the content hash the store keyed the bytes
111 // under, not the deck's own id for the asset.
112 if ref := strings.TrimPrefix(src, wantPrefix); ref != hashOf(onePixelPNG) {
113 t.Errorf("the src names %q, want the content hash %s", ref, hashOf(onePixelPNG))
114 }
115 // And the browser really fetched it: an <img> that 404'd would have
116 // failed the rig, and a decoded one has a natural width.
117 var decoded bool
118 rig.Run(chromedp.Evaluate(`(() => {
119 const i = document.querySelector('[slides-stage] [data-block="image"] img')
120 return !!i && i.complete && i.naturalWidth > 0
121 })()`, &decoded))
122 if !decoded {
123 t.Error("the browser did not decode the image it was served")
124 }
125
126 // The status line says it finished, in the catalog's words.
127 var done string
128 rig.Run(chromedp.Text(`[slides-asset-status]`, &done, chromedp.ByQuery))
129 if done == "" || strings.HasPrefix(done, "slides.") {
130 t.Errorf("the status line reads %q after a finished upload", done)
131 }
132
133 // Nothing is unused while the block points at it.
134 var unused int
135 rig.Run(chromedp.Evaluate(`document.querySelectorAll('[slides-unused-list] > li').length`, &unused))
136 if unused != 0 {
137 t.Errorf("%d unused images while one block uses the only one", unused)
138 }
139
140 // A reload: the deck's own document carries it, so the server
141 // renders the same picture without the bundle having to.
142 rig.Run(
143 chromedp.Reload(),
144 chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
145 waitFor(bound),
146 waitFor(`document.querySelector('[slides-stage] [data-block="image"] img') !== null`),
147 )
148 rig.Screen("[slides-editor]", "the image after a reload")
149 var again string
150 rig.Run(chromedp.AttributeValue(`[slides-stage] [data-block="image"] img`, "src", &again, nil, chromedp.ByQuery))
151 if again != src {
152 t.Errorf("after a reload the image src is %q, want %q", again, src)
153 }
154
155 // Delete the block and the registration stays (§2.7), which is
156 // what the Unused images list is for — and Remove clears it.
157 rig.Run(
158 chromedp.Click(`[slides-stage] [data-block="image"]`, chromedp.ByQuery),
159 waitFor(`document.querySelector('[slides-inspector-form][data-section="block"]').checkVisibility() === true`),
160 chromedp.Click(`[data-command="block-delete"]`, chromedp.ByQuery),
161 waitFor(`document.querySelector('[slides-stage] [data-block="image"]') === null`),
162 waitFor(saved),
163 waitFor(`document.querySelectorAll('[slides-unused-list] > li').length === 1`),
164 )
165 rig.Screen("[slides-editor]", "one unused image")
166 rig.Run(
167 chromedp.Click(`[slides-unused-list] [data-remove-asset]`, chromedp.ByQuery),
168 waitFor(`document.querySelectorAll('[slides-unused-list] > li').length === 0`),
169 waitFor(`document.querySelector('[slides-unused-none]').checkVisibility() === true`),
170 waitFor(saved),
171 )
172}
173