Skip to content
quarry Artifact [78048a6077]
Login

Artifact [78048a6077]

Artifact 78048a607793ef436937a8ea35b38984031b7891bfb315b64bc71f83ea7b90c3:


package web

import (
	"fmt"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

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

// findServer is a repository of six trunk check-ins, three of whose
// comments mention "stylesheet": c1, c3 and c5.
func findServer(t *testing.T) *httptest.Server {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	var batch []index.Parsed
	for i := range 6 {
		comment := fmt.Sprintf("plain work %d", i)
		if i%2 == 1 {
			comment = fmt.Sprintf("the stylesheet, part %d", i)
		}
		art := &artifact.Artifact{Type: artifact.Manifest,
			Date: fmt.Sprintf("2026-09-17T10:%02d:00", i), User: "dan", Comment: comment}
		if i > 0 {
			art.Parents = []string{batch[i-1].Hash}
		}
		batch = append(batch, index.Parsed{Hash: fmt.Sprintf("c%d", i) + strings.Repeat("0", 39), Art: art})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
	t.Cleanup(srv.Close)
	return srv
}

// c= opens the timeline centred on one check-in, ringed and brought into
// view, with the bar that says so. A centred page is a place in the
// history, not the live head, so it does not stream.
func TestTimelineCentresOnACheckin(t *testing.T) {
	srv := activityServer(t)
	code, body := getPage(t, srv, "/r/alpha/timeline?c="+strings.Repeat("22", 6))
	if code != 200 {
		t.Fatalf("status %d", code)
	}
	if n := strings.Count(body, `data-hook="timeline-found"`); n != 1 {
		t.Errorf("FAIL: %d ringed cards, want 1", n)
	}
	if !strings.Contains(body, `data-hook="timeline-centre"`) {
		t.Error("FAIL: no centre bar")
	}
	if strings.Contains(body, "timeline.rev") {
		t.Error("FAIL: a centred page streams like the live head")
	}
}

// On the timeline the header box searches the timeline, and what it
// accepts decides where the page opens: words centre on the newest
// matching check-in, a hash prefix on that check-in, a date on the
// newest check-in that day.
func TestTimelineBoxFindsOnTheTimeline(t *testing.T) {
	srv := activityServer(t)
	_, body := getPage(t, srv, "/r/alpha/timeline")
	if !strings.Contains(body, `data-hook="timeline-find"`) || !strings.Contains(body, `action="/r/alpha/timeline"`) {
		t.Error("FAIL: the timeline's header box does not search the timeline")
	}
	for _, c := range []struct{ q, want string }{
		{"trunk", strings.Repeat("22", 6)},
		{"1111", strings.Repeat("11", 6)},
		{"2026-08-03", strings.Repeat("22", 6)},
	} {
		_, body := getPage(t, srv, "/r/alpha/timeline?q="+c.q)
		i := strings.Index(body, `data-hook="timeline-found"`)
		if i < 0 {
			t.Errorf("FAIL: q=%s rings nothing", c.q)
			continue
		}
		card := body[i:min(len(body), i+3000)]
		if !strings.Contains(card, c.want) {
			t.Errorf("FAIL: q=%s rings the wrong check-in", c.q)
		}
	}
}

// A search with no match, or a centre that names nothing, still opens
// the timeline and says what happened: never an empty page or a 404
// that loses the reader's filters.
func TestTimelineFindMissesSayWhatHappened(t *testing.T) {
	srv := activityServer(t)
	for _, path := range []string{"/r/alpha/timeline?q=nothinglikeit", "/r/alpha/timeline?c=ffffff"} {
		code, body := getPage(t, srv, path)
		if code != 200 {
			t.Errorf("FAIL: %s answers %d", path, code)
		}
		if !strings.Contains(body, `data-hook="timeline-centre"`) || strings.Contains(body, `data-hook="timeline-found"`) {
			t.Errorf("FAIL: %s does not say it found nothing", path)
		}
		if strings.Count(body, `<div class="tl-card"`) == 0 {
			t.Errorf("FAIL: %s shows no timeline", path)
		}
	}
}

// Stepping through matches is links, so it works with no script: from
// the newest of three matches "older" leads on and "newer" is not
// offered; from the middle one, both are.
func TestTimelineMatchesStep(t *testing.T) {
	srv := findServer(t)
	hasHref := func(body, hook string) bool {
		i := strings.Index(body, `data-hook="`+hook+`"`)
		if i < 0 {
			t.Fatalf("FAIL: no %s", hook)
		}
		j := strings.LastIndex(body[:i], "<a ")
		return strings.Contains(body[j:i], "href=")
	}
	_, body := getPage(t, srv, "/r/alpha/timeline?q=stylesheet")
	if !hasHref(body, "match-next") || hasHref(body, "match-prev") {
		t.Error("FAIL: the newest match should step older only")
	}
	if !strings.Contains(body, `data-hook="match-count" data-at="1" data-of="3"`) {
		t.Error("FAIL: the bar does not count the matches")
	}
	_, body = getPage(t, srv, "/r/alpha/timeline?q=stylesheet&c=c3")
	if !hasHref(body, "match-next") || !hasHref(body, "match-prev") {
		t.Error("FAIL: the middle match should step both ways")
	}
}

// As the reader types, the box lists what Enter would find: links that
// open the timeline centred there with the search kept, and the way out
// to the whole repository's search. An empty box lists nothing.
func TestTimelineBoxSuggests(t *testing.T) {
	srv := findServer(t)
	code, body := getPage(t, srv, "/r/alpha/timeline.suggest?q=stylesheet")
	if code != 200 || !strings.Contains(body, `data-hook="timeline-suggest"`) {
		t.Fatalf("FAIL: %d, no list", code)
	}
	if n := strings.Count(body, "/r/alpha/timeline?c="); n != 3 {
		t.Errorf("FAIL: %d suggestions, want the 3 matches", n)
	}
	if !strings.Contains(body, "/r/alpha/search?q=stylesheet") {
		t.Error("FAIL: no way to the whole repository's search")
	}
	_, empty := getPage(t, srv, "/r/alpha/timeline.suggest?q=")
	if strings.Contains(empty, "timeline?c=") || strings.Contains(empty, "/search?q") {
		t.Error("FAIL: an empty box suggests things")
	}
}