Skip to content
quarry Artifact [0b7cf8f591]
Login

Artifact [0b7cf8f591]

Artifact 0b7cf8f591a09ea365d480b4ce9305d4c7917e1b8689802553e7e16d1557f04b:


package web

import (
	"bytes"
	"html/template"
	"strings"
	"testing"
)

func tktFixture() ticketDetail {
	t := ticketDetail{ID: "abcdef0123456789abcdef0123456789abcdef01",
		Fields: []ticketField{{Name: "title", Value: "T"}, {Name: "status", Value: "Review"},
			{Name: "priority", Value: "Urgent"}}}
	t.Changes = append(t.Changes, struct {
		Date   string        `json:"date"`
		User   string        `json:"user"`
		Fields []ticketField `json:"fields"`
	}{Date: "2026-08-01T10:00:00", User: "ann", Fields: []ticketField{{Name: "status", Value: "Open"}, {Name: "comment", Value: "desc"}}})
	t.Changes = append(t.Changes, struct {
		Date   string        `json:"date"`
		User   string        `json:"user"`
		Fields []ticketField `json:"fields"`
	}{Date: "2026-08-02T10:00:00", User: "bob", Fields: []ticketField{{Name: "status", Value: "Review"}, {Name: "priority", Value: "Urgent"}, {Name: "icomment", Value: "x"}}})
	return t
}

func renderTkt(t *testing.T, name string, data pageData) string {
	t.Helper()
	var buf bytes.Buffer
	if err := ticketPage.ExecuteTemplate(&buf, name, data); err != nil {
		t.Fatal(err)
	}
	return buf.String()
}

// The sidebar is selects from the vocabulary, and a word the tracker
// already uses but the vocabulary lacks is still offered — selected.
func TestTicketSidebarSelectsKeepForeignWords(t *testing.T) {
	vocab := map[string][]string{"status": {"Open", "Review", "Closed"}, "priority": {"High", "Low"}}
	tk := tktFixture()
	html := renderTkt(t, "tkt-side", pageData{Repo: "q", Ticket: tk, CanWrite: true,
		TicketFields: ticketControls(tk, vocab), Vocab: vocab})
	if !strings.Contains(html, `<select name="status"`) || !strings.Contains(html, `<option selected>Review</option>`) {
		t.Errorf("status select with current value missing:\n%s", html)
	}
	if !strings.Contains(html, `<option selected>Urgent</option>`) {
		t.Errorf("out-of-vocabulary priority was refused:\n%s", html)
	}
	if strings.Contains(html, `<input name="subsystem"`) == false {
		t.Errorf("a field with no vocabulary should stay free text:\n%s", html)
	}
	if strings.Contains(html, `name="resolution"`) {
		t.Errorf("resolution is editable on an open ticket:\n%s", html)
	}
}

// Readers and read-only mirrors get facts, not forms.
func TestTicketSidebarReadOnlyHasNoForm(t *testing.T) {
	tk := tktFixture()
	for _, d := range []pageData{
		{Repo: "q", Ticket: tk, TicketFields: ticketControls(tk, nil)},
		func() pageData {
			d := pageData{Repo: "q", Ticket: tk, CanWrite: true, TicketFields: ticketControls(tk, nil)}
			d.Posture.Mirror.Tracking, d.Posture.Mirror.URL = true, "https://x/y"
			return d
		}(),
	} {
		if html := renderTkt(t, "tkt-side", d); strings.Contains(html, "<form") {
			t.Errorf("form rendered for a non-writer:\n%s", html)
		}
	}
}

// A field change lands between the remarks it was made between, and
// names where the field came from.
func TestTicketTimelineInterleavesChanges(t *testing.T) {
	tk := tktFixture()
	ev := ticketTimeline(tk, []ticketComment{
		{Date: "2026-08-01T12:00:00", Login: "ann", HTML: template.HTML("<p>first</p>")},
		{Date: "2026-08-03T12:00:00", Login: "cid", HTML: template.HTML("<p>later</p>")},
	})
	if len(ev) != 3 || ev[1].Login != "bob" || len(ev[1].Deltas) != 2 {
		t.Fatalf("want comment, change, comment; got %+v", ev)
	}
	html := renderTkt(t, "tkt-timeline", pageData{Timeline: ev})
	if !strings.Contains(html, "status <b>Open → Review</b>") || !strings.Contains(html, "priority <b>Urgent</b>") {
		t.Errorf("delta line missing:\n%s", html)
	}
}

// Open tickets offer Comment & close with a resolution; closed ones
// offer Reopen.
func TestTicketComposerCloseOrReopen(t *testing.T) {
	vocab := map[string][]string{"resolution": {"Fixed", "Rejected"}}
	tk := tktFixture()
	open := renderTkt(t, "content", pageData{Repo: "q", Ticket: tk, CanWrite: true, TicketFields: ticketControls(tk, vocab)})
	if !strings.Contains(open, `value="Closed">Comment &amp; close`) || !strings.Contains(open, `<select name="resolution"`) {
		t.Errorf("open ticket lacks close control:\n%s", open)
	}
	tk.Fields[1].Value = "Closed"
	closed := renderTkt(t, "content", pageData{Repo: "q", Ticket: tk, CanWrite: true, TicketFields: ticketControls(tk, vocab)})
	if !strings.Contains(closed, `value="Open">Reopen`) || strings.Contains(closed, "Comment &amp; close") {
		t.Errorf("closed ticket lacks reopen:\n%s", closed)
	}
}