oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "bytes"
5 "mime/multipart"
6 "net/http"
7 "net/http/httptest"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12)
13
14// The sheet list is a list of sheets.
15//
16// It used to open with two stacked forms — a title field with a Create
17// button, then a dashed drop zone with a file input and four lines of
18// limits — so the sheets themselves started below both. These tests pin
19// the removal, because the easiest way to undo it is for somebody to
20// add "just one small form" back to the top of the page.
21
22// signedInListBody renders the list for a fresh signed-in member.
23// preview_test.go's listBody takes an app and cookies because the
24// thumbnail tests need sheets in it; these tests want the chrome around
25// an empty one, so this wraps the sign-in rather than duplicating it.
26func signedInListBody(t *testing.T) string {
27 t.Helper()
28 app, cookies := signIn(t)
29 return listBody(t, app, cookies)
30}
31
32// dropOverlay returns the sheet list's body up to the drop overlay, and
33// the overlay's own start tag. Everything before the overlay is what
34// the page actually renders; the overlay itself is hidden until a file
35// is dragged over the window.
36func dropOverlay(t *testing.T, body string) (inFlow, tag string) {
37 t.Helper()
38 start := strings.Index(body, "<form data-drop")
39 if start < 0 {
40 t.Fatal("the sheet list has no drop overlay — dropping a file on this page is the affordance it traded its drop box for")
41 }
42 end := strings.Index(body[start:], ">")
43 if end < 0 {
44 t.Fatal("the drop overlay's start tag never closes")
45 }
46 return body[:start], body[start : start+end+1]
47}
48
49func TestSheetListCarriesNoInlineForms(t *testing.T) {
50 body := signedInListBody(t)
51
52 // The title field is simply gone. A sheet is named by renaming it,
53 // on the sheet itself, so there is nothing left for this to be.
54 if strings.Contains(body, `name="title"`) {
55 t.Error(`the sheet list is carrying name="title" again — naming a sheet is a rename, in the top bar of the sheet`)
56 }
57
58 // The file input is back, but only inside the overlay. Anything of
59 // it appearing BEFORE the overlay's start tag is a box in the page
60 // again, which is the thing this change removed.
61 inFlow, _ := dropOverlay(t, body)
62 for _, gone := range []string{
63 `type="file"`,
64 `enctype="multipart/form-data"`,
65 "data-drop",
66 } {
67 if strings.Contains(inFlow, gone) {
68 t.Errorf("%s renders in the flow of the sheet list — the drop target is an overlay, not a box above the sheets", gone)
69 }
70 }
71}
72
73// The overlay is a real import form that happens to be invisible until
74// a drag brings it up. Both halves of that matter: hidden at rest, so
75// it costs the list no layout and no tab stop, and a genuine multipart
76// POST to /sheets/import, so a drop is the ordinary upload and there is
77// no second code path on the server.
78func TestTheDropOverlayIsHiddenAtRest(t *testing.T) {
79 _, tag := dropOverlay(t, signedInListBody(t))
80
81 for _, want := range []string{
82 "hidden",
83 `method="post"`,
84 `action="/sheets/import"`,
85 `enctype="multipart/form-data"`,
86 } {
87 if !strings.Contains(tag, want) {
88 t.Errorf("the drop overlay's start tag is missing %s: %s", want, tag)
89 }
90 }
91}
92
93// The overlay is nothing without drop.js: it is `hidden`, and only that
94// script ever takes the attribute off. A page that renders the markup
95// and does not load the script has an import affordance that can never
96// appear.
97func TestTheSheetListLoadsDropJS(t *testing.T) {
98 // By prefix, not by filename: assets are served fingerprinted, so
99 // the tag reads /static/drop.<hash>.js and matching the literal
100 // name passes only until the file next changes.
101 body := signedInListBody(t)
102 if !strings.Contains(body, `src="/static/drop.`) {
103 t.Error("the sheet list renders the drop overlay but never loads drop.js, so nothing will ever reveal it")
104 }
105}
106
107// The paint that keeps the overlay hidden.
108//
109// [rst-modal-overlay] sets display:flex, and an author declaration beats
110// the user agent's [hidden] { display: none } however weak its selector
111// looks. Drop the reinstating rule and the overlay covers the sheet list
112// permanently — worst with scripting off, where nothing would ever come
113// along to hide it again. The markup test above passes either way, so
114// this reads the stylesheet.
115func TestTheDropOverlayStaysHiddenInCSS(t *testing.T) {
116 css, err := os.ReadFile(filepath.Join("static", "sheets.css"))
117 if err != nil {
118 t.Fatalf("read sheets.css: %v", err)
119 }
120 if !strings.Contains(string(css), "[data-drop-page][hidden]") {
121 t.Error("sheets.css no longer reinstates display:none for the hidden drop overlay — it will cover the sheet list")
122 }
123
124 // The drop BOX's paint must exclude the overlay, which carries
125 // data-drop too. [data-drop][data-drop-over] is 0-2-0 against
126 // [rst-modal-overlay]'s 0-1-0, so without the exclusion the scrim is
127 // painted accent-soft and the sheet list disappears behind a flat
128 // blue screen mid-drag. A browser found that; nothing here could.
129 if !strings.Contains(string(css), "[data-drop]:not([data-drop-page])") {
130 t.Error("the drop box's paint no longer excludes the overlay — it will out-specify the modal scrim and cover the list")
131 }
132}
133
134// Create is a real form posting to /sheets, in the top bar. A link
135// would be wrong twice over: it makes a sheet, so it must not be a GET,
136// and it has to work with scripting off.
137func TestCreateLivesInTheTopBar(t *testing.T) {
138 body := signedInListBody(t)
139
140 bar := body[:strings.Index(body, `<main`)]
141 if !strings.Contains(bar, `action="/sheets"`) || !strings.Contains(bar, `type="submit"`) {
142 t.Error("the top bar has no Create form")
143 }
144 if !strings.Contains(bar, `href="/sheets/import"`) {
145 t.Error("the top bar has no Import link")
146 }
147 // Beside the account menu, not adrift somewhere else in the shell.
148 if strings.Index(bar, `action="/sheets"`) > strings.Index(bar, `action="/signout"`) {
149 t.Error("Create renders after the account menu, not beside it")
150 }
151}
152
153// The empty state is the one screen with nothing to do but say what to
154// do next, and it used to say "make one above" — which pointed at the
155// form this change removed.
156func TestEmptyStateDoesNotPointAtAFormThatIsGone(t *testing.T) {
157 body := signedInListBody(t)
158 if !strings.Contains(body, "No sheets yet") {
159 t.Fatal("no empty state on a list with no sheets")
160 }
161 if strings.Contains(body, "above and it opens") || strings.Contains(body, "Make one above") {
162 t.Error("the empty state still points at the removed form")
163 }
164 if !strings.Contains(body, `href="/sheets/import"`) {
165 t.Error("the empty state offers no way to import, which is the one route a person with no sheets wants")
166 }
167}
168
169// /sheets/import must reach the import page, not be parsed as a sheet
170// called "import". chi prefers a static segment over {sheetID} whatever
171// the registration order, so this passes today — it is pinned because a
172// route that only works by resolution order is one refactor from a 404
173// that looks like a missing sheet.
174func TestImportPathIsNotASheetID(t *testing.T) {
175 app, cookies := signIn(t)
176 rec := do(t, app, as(httptest.NewRequest(http.MethodGet, "/sheets/import", nil), cookies))
177 if rec.Code != http.StatusOK {
178 t.Fatalf("GET /sheets/import = %d, want 200", rec.Code)
179 }
180 if !strings.Contains(rec.Body.String(), `action="/sheets/import"`) {
181 t.Error("/sheets/import did not render the import form")
182 }
183}
184
185// A refused upload re-renders the import page, where the form is —
186// never the sheet list, which no longer has one to try again with.
187func TestRefusalRendersOnTheImportPage(t *testing.T) {
188 app, cookies := signIn(t)
189
190 // An empty multipart body: the shape a submit with nothing chosen
191 // actually arrives in. A urlencoded post would refuse too, but with
192 // "could not be read" rather than "choose a spreadsheet", which is
193 // a different sentence about a different mistake.
194 var buf bytes.Buffer
195 w := multipart.NewWriter(&buf)
196 if err := w.Close(); err != nil {
197 t.Fatalf("close writer: %v", err)
198 }
199 req := httptest.NewRequest(http.MethodPost, "/sheets/import", &buf)
200 req.Header.Set("Content-Type", w.FormDataContentType())
201
202 rec := do(t, app, as(req, cookies))
203 if rec.Code != http.StatusBadRequest {
204 t.Fatalf("import with no file = %d, want 400", rec.Code)
205 }
206 body := rec.Body.String()
207 if !strings.Contains(body, refusalNoFile) {
208 t.Error("the refusal is not on the page")
209 }
210 if !strings.Contains(body, `action="/sheets/import"`) {
211 t.Error("the refusal rendered somewhere without the form to retry from")
212 }
213 if strings.Contains(body, "All sheets</h1>") {
214 t.Error("the refusal rendered on the sheet list")
215 }
216}
217