Skip to content
quarry Artifact [3e82936c62]
Login

Artifact [3e82936c62]

Artifact 3e82936c6292d14e1e980c03d2f396d75161440a51f502a2863fae3be92bb11d:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"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 Datastar hooks
// ride ON the same anchor, so a reader without script still navigates.
func TestReferenceLinkKeepsItsHrefAndGainsHooks(t *testing.T) {
	in := `<p>see <a class="ref" data-ref="#412" data-ref-kind="chat" href="/r/demo/chat/412">#412</a></p>`
	got := string(enhanceRefs(in, "demo"))
	for _, want := range []string{
		`href="/r/demo/chat/412"`,
		`data-ref="#412"`,
		`data-on:mouseenter__debounce.300ms=`,
		`data-on:focus=`,
		`data-on:click=`,
		`evt.preventDefault()`,
		`/r/demo/ref.frag?ref=`,
		// Hover and focus previews are for hover-capable pointers; the
		// tap path is for everything else. One expression, gated.
		`matchMedia(&#39;(hover: hover)&#39;)`,
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("enhanced reference missing %q:\n%s", want, got)
		}
	}
	// The anchor is not rewritten into something else.
	if !strings.HasPrefix(got, "<p>see <a ") {
		t.Fatalf("the reference stopped being an anchor:\n%s", got)
	}
}

// A link that is not a reference is left alone: enhancement is opt-in
// by the resolver's own marker, never by shape.
func TestEnhanceRefsLeavesOrdinaryLinksAlone(t *testing.T) {
	in := `<a href="/r/demo/ticket/x">a ticket</a>`
	if got := string(enhanceRefs(in, "demo")); got != in {
		t.Fatalf("an ordinary link was enhanced:\n%s", got)
	}
}

// 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="chatquote"`,
		`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.
	for _, m := range refAnchor.FindAllStringSubmatch(body, -1) {
		if !strings.Contains(m[1], `href="/`) {
			t.Fatalf("a reference has no navigable href: %s", m[0])
		}
	}
}