package slidestest
import (
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
)
// bundleLink finds the editor bundle's script tag on a page: the one
// committed file under internal/slides/static, served through the
// fingerprinted asset handler.
var bundleLink = regexp.MustCompile(`/static/editor\.[0-9a-f]{16}(\.[0-9a-f]{16})?\.js`)
// TestEditorPageLinksTheCommittedBundle: GET /d/{sqid}/edit is the one
// screen that needs the bundle (spec §8), and the page carries
// everything the bundle reads back — the deck, the seq, the current
// slide, the person — as data rather than as script. The strip and the
// stage are server-rendered too, so the first paint is right before a
// byte of JS has run.
func TestEditorPageLinksTheCommittedBundle(t *testing.T) {
a := newApp(t)
cookies := signedIn(t, a, allowedAddress)
path := sampleDeck(t, a, cookies)
sqid := strings.TrimPrefix(path, "/d/")
page := mustGet(t, a, path+"/edit", http.StatusOK, cookies).Body.String()
src := bundleLink.FindString(page)
if src == "" {
t.Fatalf("the editor page links no committed bundle:\n%s", page)
}
if !strings.Contains(page, `type="module"`) {
t.Errorf("the bundle is an ES module and must be loaded as one:\n%s", page)
}
js := mustGet(t, a, src, http.StatusOK, nil)
if ct := js.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") {
t.Errorf("GET %s: Content-Type = %q, want text/javascript", src, ct)
}
if !strings.Contains(js.Header().Get("Cache-Control"), "immutable") {
t.Errorf("GET %s: Cache-Control = %q, want immutable", src, js.Header().Get("Cache-Control"))
}
for _, css := range []string{"editor", "slides"} {
if !regexp.MustCompile(`/static/` + css + `\.[0-9a-f]{16}\.css`).MatchString(page) {
t.Errorf("the editor page links no fingerprinted %s.css:\n%s", css, page)
}
}
for _, attr := range []string{
`data-deck="` + sqid + `"`,
`data-seq="`,
`data-slide="`,
`data-client-name="`,
`data-client-colour="`,
} {
if !strings.Contains(page, attr) {
t.Errorf("the editor page carries no %s:\n%s", attr, page)
}
}
if strings.Contains(page, `data-slide=""`) {
t.Errorf("the sample deck has slides, so data-slide must name the first one:\n%s", page)
}
// The strip: one
per slide, each an of the server's SVG
// of it (thumbs.go), the current one saying so in words.
// The the bundle clones for a new slide is an item too,
// with no slide id; the eight with one are the deck's.
if n := strings.Count(page, `
"} {
if !strings.Contains(page, want) {
t.Errorf("the editor page lacks %s:\n%s", want, page)
}
}
if !strings.Contains(page, `href="`+path+`"`) {
t.Errorf("the noscript fallback does not link the viewer:\n%s", page)
}
// Every shortcut has a visible menu equivalent.
for _, want := range []string{"New slide", "Duplicate slide", `action="` + path + `/present"`} {
if !strings.Contains(page, want) {
t.Errorf("the editor's menu lacks %s:\n%s", want, page)
}
}
}
// TestEditorRequiresMembership: the editor is deckFor's audience — the
// owner and, on a team deck, every member — and 404 to anybody else, so
// the sqid of a private deck stays unconfirmed. The viewer offers the
// Edit button to exactly the people the editor lets in.
func TestEditorRequiresMembership(t *testing.T) {
a := newSharedApp(t)
mine := signedIn(t, a, allowedAddress)
theirs := signedIn(t, a, otherAddress)
path := createDeck(t, a, mine, "Board update")
mustGet(t, a, path+"/edit", http.StatusNotFound, theirs)
mustGet(t, a, path+"/edit", http.StatusOK, mine)
owner := mustGet(t, a, path, http.StatusOK, mine).Body.String()
if !strings.Contains(owner, `href="`+path+`/edit"`) {
t.Errorf("the owner's viewer offers no Edit:\n%s", owner)
}
if rec := post(t, a, path+"/settings", url.Values{
"title": {"Board update"}, "theme": {"ink"}, "rung": {"team"},
}, mine...); rec.Code != http.StatusSeeOther {
t.Fatalf("raising the rung: status %d\n%s", rec.Code, rec.Body)
}
mustGet(t, a, path+"/edit", http.StatusOK, theirs)
member := mustGet(t, a, path, http.StatusOK, theirs).Body.String()
if !strings.Contains(member, `href="`+path+`/edit"`) {
t.Errorf("a member's viewer offers no Edit:\n%s", member)
}
// Signed out, the editor is behind the gate like every other
// signed-in screen: a redirect to sign in, never the page.
if rec := get(t, a, path+"/edit"); rec.Code != http.StatusSeeOther && rec.Code != http.StatusFound {
t.Errorf("GET %s/edit signed out: status %d, want a redirect to /signin", path, rec.Code)
}
}
// TestEditorOfAnEmptyDeck: a deck with no slides is a real state (the
// viewer's own rule), so the editor opens on it with no current slide
// and a way to make the first one, rather than a 404 or a 500.
func TestEditorOfAnEmptyDeck(t *testing.T) {
a := newApp(t)
cookies := signedIn(t, a, allowedAddress)
path := createDeck(t, a, cookies, "Blank")
sqid := strings.TrimPrefix(path, "/d/")
// A new deck opens with one slide; an agent takes it away, which
// is the only way a deck ends up with none.
token := mintAgent(t, a, cookies, "claude")
before := mustGet(t, a, path+"/edit", http.StatusOK, cookies).Body.String()
m := regexp.MustCompile(`data-slide="([a-z2-7]{12})"`).FindStringSubmatch(before)
if m == nil {
t.Fatalf("a new deck's editor names no current slide:\n%s", before)
}
apiJSON(t, a, http.MethodPost, "/api/v1/decks/"+sqid+"/ops", token,
`{"ops":[{"op":"remove_slide","id":"`+m[1]+`"}]}`, http.StatusOK)
page := mustGet(t, a, path+"/edit", http.StatusOK, cookies).Body.String()
if !strings.Contains(page, `data-slide=""`) {
t.Errorf("an empty deck's editor names a current slide:\n%s", page)
}
if !strings.Contains(page, "No slides yet") {
t.Errorf("an empty deck's editor does not say so:\n%s", page)
}
if !strings.Contains(page, "New slide") {
t.Errorf("an empty deck's editor offers no way to add a slide:\n%s", page)
}
}
// TestEditorSlideHTMLServesTheThumbnailViewport: ?vp=thumb is the strip's
// rendering and is the desktop one — a thumbnail is a desktop slide
// scaled by CSS, never a third rendering.
func TestEditorSlideHTMLServesTheThumbnailViewport(t *testing.T) {
a := newApp(t)
cookies := signedIn(t, a, allowedAddress)
path := sampleDeck(t, a, cookies)
page := mustGet(t, a, path+"/edit", http.StatusOK, cookies).Body.String()
m := regexp.MustCompile(`data-slide="([a-z2-7]{12})"`).FindStringSubmatch(page)
if m == nil {
t.Fatalf("no current slide id on the editor page:\n%s", page)
}
thumb := mustGet(t, a, path+"/slides/"+m[1]+".html?vp=thumb", http.StatusOK, cookies).Body.String()
if !strings.HasPrefix(thumb, " that
// names its id, or a wrapping