package web
import (
"strings"
"testing"
)
// The stylesheet is served, not inlined. It used to be 112,118 bytes of
// <style> in every response — 40% of a 309,584-byte timeline page,
// byte-identical each time and uncacheable because it had no URL.
func TestSheetIsLinkedNotInlined(t *testing.T) {
if strings.Contains(baseHTML, "<style>") {
t.Error("the page still inlines a stylesheet")
}
if !strings.Contains(baseHTML, `rel="stylesheet"`) {
t.Error("the page does not link a stylesheet")
}
if len(sheet) < 50_000 {
t.Errorf("the sheet is %d bytes; the rules did not come with it", len(sheet))
}
}
// Both themes have to survive being moved out of the page — the whole
// two-theme story lives in this one file, and chroma's half of it is
// generated at startup rather than written down.
func TestSheetCarriesBothThemes(t *testing.T) {
for _, want := range []string{
"prefers-color-scheme: dark",
`:root[data-theme="dark"]`,
"--fg:", "--bg:", "--accent:",
".chroma",
} {
if !strings.Contains(sheet, want) {
t.Errorf("the sheet is missing %q", want)
}
}
}
// The URL carries a content hash so it can be served immutable: a rule
// change mints a new URL rather than asking anyone to revalidate.
func TestSheetURLIsContentAddressed(t *testing.T) {
if !strings.HasPrefix(sheetURL, "/assets/quarry.") || !strings.HasSuffix(sheetURL, ".css") {
t.Fatalf("sheetURL = %q", sheetURL)
}
if h := sheetHash(); !strings.Contains(sheetURL, h) {
t.Errorf("sheetURL %q does not carry the hash %q", sheetURL, h)
}
// Same content, same hash — the URL must be stable across a
// restart or every deploy busts every reader's cache for nothing.
if sheetHash() != sheetHash() {
t.Error("the hash is not stable for identical content")
}
}
// No page may carry its own <style> element.
//
// The strict CSP is style-src 'self' with no nonce for style elements —
// style-src-attr covers the style="" attribute and nothing else — so a
// <style> block in a template is kept in the DOM, given no sheet, and
// applied to nothing. It fails silently: no console error a reader
// would notice, no failing test, just a page rendering unstyled.
//
// It happened. Chat and the inbox each carried one, and shipped
// unstyled from the day the CSP landed until somebody looked: the chat
// log rendered as a numbered list because .chatlog's list-style never
// arrived. Every test asserted the rules were in the HTML, which they
// were, and none asserted they were anywhere a browser would read them.
func TestNoTemplateCarriesItsOwnStyleElement(t *testing.T) {
if n := strings.Count(baseHTML, "<style"); n > 0 {
t.Errorf("base carries %d <style> elements; the CSP drops them", n)
}
for _, src := range []struct{ name, body string }{
{"chatLogSource", chatLogSource},
{"boardSource", boardSource},
{"exploreSource", exploreSource},
{"fleetAdminSource", fleetAdminSource},
{"runStepsSource", runStepsSource},
{"ticketSource", ticketSource},
} {
if strings.Contains(src.body, "<style") {
t.Errorf("%s carries a <style> element; put the rules in sheetLiteral, "+
"which is served, hashed and actually applied", src.name)
}
}
}
// The rules chat and the inbox need are IN the served sheet. Pinned by
// name because moving them there is what made them work at all.
func TestSheetCarriesChatAndInboxRules(t *testing.T) {
for _, want := range []string{
".chatlog{", ".chatmsg", ".chat-toolbar{", ".chat-quote{",
".composer{", ".chat-date{", ".chat-divider",
".inbox-bar{", ".attn-row", ".attn-done{",
} {
if !strings.Contains(sheet, want) {
t.Errorf("the served stylesheet is missing %q", want)
}
}
// And the log is not a numbered list.
if !strings.Contains(sheet, "list-style:none") {
t.Error("the chat log has no list-style reset; it will render as 1. 2. 3.")
}
}