Skip to content
quarry Artifact [f0c2feb642]
Login

Artifact [f0c2feb642]

Artifact f0c2feb642123c868b3ad99b6f026987fc5b763c4754030756c071cb742fccf3:


package web

import (
	"context"
	"html/template"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/chat"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

// Principle 3: the plain link is the whole feature. The preview rides
// ON the same anchors, armed once for the page, so a reader without
// script still navigates.
//
// The container carries the behaviour; an anchor carries only its
// token. Three handlers for the whole page, whatever it holds โ€” a chat
// log with two hundred references pays for them once (RFC 0011 ยง14,
// Principle 4).
func TestTheContainerCarriesTheHandlersNotTheAnchors(t *testing.T) {
	for _, ev := range []string{
		`data-on:mouseover__debounce.300ms="`,
		`data-on:focusin="`,
		`data-on:click="`,
	} {
		if n := strings.Count(refDelegation, ev); n != 1 {
			t.Fatalf("the delegation run declares %s %d times, want 1:\n%s", ev, n, refDelegation)
		}
	}
	if n := strings.Count(refDelegation, "data-on:"); n != 3 {
		t.Fatalf("the delegation run carries %d handlers, want 3:\n%s", n, refDelegation)
	}
	if n := strings.Count(baseHTML, refDelegation); n != 1 {
		t.Fatalf("the layout arms the preview %d times, want 1", n)
	}
	// The whole page's preview script, once. Armed per anchor this was
	// three ~380-byte expressions on every reference on the page.
	if len(refDelegation) > 2048 {
		t.Fatalf("the delegation run is %d bytes", len(refDelegation))
	}
	for _, want := range []string{
		// The event is resolved to the anchor it reached, not to the
		// element it landed on.
		`closest('a[data-ref]')`,
		// The token is read off the anchor and encoded at event time;
		// nothing author-written is ever interpolated into the
		// attribute.
		`encodeURIComponent(a.dataset.ref)`,
		`/ref.frag?ref=`,
		// The repository is the page's own, stated on the container.
		`document.body.dataset.refRepo`,
		// Hover and focus are for pointers that hover; the tap path
		// cancels the navigation only where they do not.
		`matchMedia('(hover: hover)')`,
		`evt.preventDefault()`,
		// The card is parked at its link, not centred by the UA.
		`anchorName`,
		`getBoundingClientRect`,
		`showPopover`,
		`refpop`,
	} {
		if !strings.Contains(refDelegation, want) {
			t.Fatalf("the delegation run misses %q:\n%s", want, refDelegation)
		}
	}
	// The rows this file draws are references without being prose, so
	// they carry the one attribute a reference in prose does โ€” and no
	// handlers of their own.
	quote := string(chatQuoteHTML(ChatMessage{Ref: 412, RefUser: "dan", RefLine: "a line"}, "demo"))
	if !strings.Contains(quote, `data-quote-seq="412"`) || !strings.Contains(quote, `data-ref="#412"`) {
		t.Fatalf("the reply-quote is not armed like a reference:\n%s", quote)
	}
	if strings.Contains(quote, "data-on:") {
		t.Fatalf("the reply-quote arms itself:\n%s", quote)
	}
	row := string(chatRefsHTML(chatRefsRow{Count: 2, Seq: 7}, "demo"))
	if !strings.Contains(row, `data-ref="#7"`) || strings.Contains(row, "data-on:") {
		t.Fatalf("the backlink row is not armed like a reference:\n%s", row)
	}
}

// The shared popover is positioned against the link that opened it,
// with the measured fallback only where CSS anchoring is unknown.
func TestPopoverIsAnchoredNotCentred(t *testing.T) {
	rule := baseHTML[strings.Index(baseHTML, "#refpop { position: fixed"):]
	rule = rule[:strings.Index(rule, "\n}")]
	for _, want := range []string{
		"#refpop { position: fixed; inset: auto",
		"position-anchor: " + refAnchorName,
		"top: anchor(bottom)",
	} {
		if !strings.Contains(rule, want) {
			t.Fatalf("#refpop is not anchored to its link, missing %q:\n%s", want, rule)
		}
	}
}

// One popover element for the whole page: a per-link popover is a
// per-link element, and the page grows one for every reference in it.
func TestBaseLayoutHasOneSharedPopover(t *testing.T) {
	if n := strings.Count(baseHTML, `id="refpop"`); n != 1 {
		t.Fatalf("base layout declares %d #refpop elements, want 1", n)
	}
	if !strings.Contains(baseHTML, `<div id="refpop" popover`) {
		t.Fatalf("the shared preview is not a native popover:\n%s", baseHTML)
	}
}

// Each card says what it is, structurally, and offers the page it
// previews. The copy is not asserted; the hooks are.
func TestRefCardCarriesKindAndOpenLink(t *testing.T) {
	for _, tt := range []struct{ kind, href string }{
		{"chat", "/r/demo/chat/412"},
		{"checkin", "/r/demo/checkin/abc"},
		{"ticket", "/r/demo/ticket/xy9"},
		{"landing", "/r/demo/landing/ab"},
		{"forum", "/r/demo/forum/ab"},
		{"wiki", "/r/demo/wiki/Home"},
		{"person", "/people/alice"},
	} {
		got := string(refCardHTML(refCard{Kind: tt.kind, Href: tt.href, Title: "t"}))
		if !strings.Contains(got, `data-kind="`+tt.kind+`"`) {
			t.Fatalf("%s card lost its data-kind:\n%s", tt.kind, got)
		}
		if !strings.Contains(got, `href="`+tt.href+`"`) {
			t.Fatalf("%s card lost its open link:\n%s", tt.kind, got)
		}
	}
	// An aged-out chat reference says so as a state, not as prose the
	// caller has to parse.
	got := string(refCardHTML(refCard{Kind: "chat", Href: "/r/demo/chat/1", Expired: true}))
	if !strings.Contains(got, `data-expired`) {
		t.Fatalf("expired card carries no state hook:\n%s", got)
	}
	// A person card never carries an address (RFC 0004 keeps those
	// behind `e`), so there is nothing shaped like one to leak.
	person := string(refCardHTML(refCard{Kind: "person", Href: "/people/alice", Title: "alice"}))
	if strings.Contains(person, "mailto:") || strings.Contains(person, "@") {
		t.Fatalf("person card leaked an address:\n%s", person)
	}
}

// The reply-quote: a message posted with Ref shows who it answers and
// the first line of what they said, linked to the permalink.
func TestReplyRendersItsQuote(t *testing.T) {
	got := string(chatMsgHTML(ChatMessage{
		Seq: 9, Kind: "msg", User: "bob", Text: "agreed", Ref: 4,
		RefUser: "alice", RefLine: "the first line",
	}, "demo", "", false))
	for _, want := range []string{
		`class="chat-quote"`,
		`data-quote-seq="4"`,
		`href="/r/demo/chat/4"`,
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("reply quote missing %q:\n%s", want, got)
		}
	}
}

// The original says how many answered it, and the count links to the
// permalink where they are.
func TestOriginalShowsItsReplyCount(t *testing.T) {
	got := string(chatMsgHTML(ChatMessage{
		Seq: 4, Kind: "msg", User: "alice", Text: "hi", Replies: 3,
	}, "demo", "", false))
	if !strings.Contains(got, `data-replies="3"`) {
		t.Fatalf("reply count hook missing:\n%s", got)
	}
	if !strings.Contains(got, `href="/r/demo/chat/4"`) {
		t.Fatalf("reply count does not link to the permalink:\n%s", got)
	}
}

// A message with no replies grows no count element at all: "0 replies"
// is noise on every line of a busy channel.
func TestNoRepliesRendersNoCount(t *testing.T) {
	got := string(chatMsgHTML(ChatMessage{Seq: 4, Kind: "msg", User: "alice", Text: "hi"}, "demo", "", false))
	if strings.Contains(got, "data-replies") {
		t.Fatalf("a message with no replies grew a count:\n%s", got)
	}
}

// The fragment route: one card, wrapped so the shared popover comes
// back still being a popover.
func TestRefFragServesTheCard(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	r := ops.NewRegistry()
	fc := &webChatStub{msgs: []chat.Message{
		{Seq: 1, Kind: chat.KindMsg, User: "dan", Text: "hello", At: "2026-08-16T12:00:00Z"},
	}}
	if err := api.RegisterAll(r, api.Deps{Version: "t",
		Repos:   api.NewRepos(map[string]*api.Repo{"test": {Index: ix, Code: "c1"}}),
		Chat:    fc,
		Forum:   &webForumReadStub{},
		CanRead: func(context.Context, string) bool { return true }}); err != nil {
		t.Fatal(err)
	}
	h := HandlerWith(r, nil, newFakeChatLive(), nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/ref.frag?ref=%231", nil))
	body := rec.Body.String()
	for _, want := range []string{
		"event: datastar-patch-elements",
		`<div id="refpop" popover>`,
		`data-kind="chat"`,
		`href="/r/test/chat/1"`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("ref.frag missing %q:\n%s", want, body)
		}
	}
	if rec.Header().Get("ETag") == "" {
		t.Fatalf("ref.frag served no ETag:\n%v", rec.Header())
	}
	// A repeat hover on an unchanged reference costs a 304, not a redraw.
	rec2 := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/r/test/ref.frag?ref=%231", nil)
	req.Header.Set("If-None-Match", rec.Header().Get("ETag"))
	h.ServeHTTP(rec2, req)
	if rec2.Code != http.StatusNotModified {
		t.Fatalf("unchanged reference re-rendered: %d", rec2.Code)
	}
}

// Principle 3, asserted on a real page: every reference on the chat
// page is an anchor with a real href, so navigation never depends on
// the Datastar attributes sitting beside it.
func TestChatPageReferencesNavigateWithoutScript(t *testing.T) {
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	r := ops.NewRegistry()
	fc := &webChatStub{msgs: []chat.Message{
		{Seq: 1, Kind: chat.KindMsg, User: "dan", Text: "start", At: "2026-08-16T12:00:00Z"},
		{Seq: 2, Kind: chat.KindMsg, User: "ann", Text: "see #1", Ref: 1, At: "2026-08-16T12:00:01Z"},
	}}
	if err := api.RegisterAll(r, api.Deps{Version: "t",
		Repos:   api.NewRepos(map[string]*api.Repo{"test": {Index: ix, Code: "c1"}}),
		Chat:    fc,
		Forum:   &webForumReadStub{},
		CanRead: func(context.Context, string) bool { return true }}); err != nil {
		t.Fatal(err)
	}
	h := HandlerWith(r, nil, newFakeChatLive(), nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/chat", nil))
	body := rec.Body.String()
	// The reference minted by the resolver, and the quote block.
	if !strings.Contains(body, `data-ref="#1"`) {
		t.Fatalf("chat page did not link a #seq reference:\n%s", body)
	}
	if !strings.Contains(body, `data-quote-seq="1"`) {
		t.Fatalf("reply did not render its quote:\n%s", body)
	}
	if !strings.Contains(body, `data-replies="1"`) {
		t.Fatalf("original did not show its reply count:\n%s", body)
	}
	// Every marked reference carries a real destination: Principle 3
	// says the link is the feature and the card is the accelerator.
	for _, tag := range markedAnchors.FindAllString(body, -1) {
		if !strings.Contains(tag, `href="/`) {
			t.Fatalf("a reference has no navigable href: %s", tag)
		}
	}
}

// The backlink row on an artifact page: its own row, its own hook, and
// a link to the latest message โ€” visibly not one of the durable
// mention rows beside it.
func TestArtifactPagesShowTheChatRow(t *testing.T) {
	for _, tt := range []struct {
		name string
		tmpl *template.Template
		data pageData
	}{
		{"ticket", ticketPage, pageData{Repo: "demo", Ticket: ticketDetail{ID: "xy9"}}},
		{"checkin", checkinPage, pageData{Repo: "demo", Checkin: checkinDetail{Hash: "abc"}}},
		{"forum", forumThreadPage, pageData{Repo: "demo", ForumRoot: "abc"}},
	} {
		tt.data.ChatRefs = chatRefsRow{Count: 2, Seq: 7, At: "2026-08-29T10:00:00Z"}
		var buf strings.Builder
		if err := tt.tmpl.Execute(&buf, tt.data); err != nil {
			t.Fatalf("%s: %v", tt.name, err)
		}
		got := buf.String()
		if !strings.Contains(got, `data-chat-refs data-count="2" data-seq="7"`) {
			t.Fatalf("%s page has no chat row:\n%s", tt.name, got)
		}
		if !strings.Contains(got, `href="/r/demo/chat/7"`) {
			t.Fatalf("%s chat row does not link the latest message", tt.name)
		}
	}
}

// Nothing named it, nothing shown: an empty row on every artifact page
// is noise that teaches readers to skip the place the real one appears.
func TestNoChatRowWhenNothingNamedIt(t *testing.T) {
	var buf strings.Builder
	if err := ticketPage.Execute(&buf, pageData{Repo: "demo", Ticket: ticketDetail{ID: "xy9"}}); err != nil {
		t.Fatal(err)
	}
	if strings.Contains(buf.String(), "data-chat-refs") {
		t.Fatalf("an empty chat row was drawn")
	}
}

// markedAnchors finds the opening <a> tags markup marked as references.
var markedAnchors = regexp.MustCompile(`<a [^>]*\bdata-ref="[^"]*"[^>]*>`)