Skip to content
quarry Artifact [3ccb22d387]
Login

Artifact [3ccb22d387]

Artifact 3ccb22d387141219ed289b9517c65d67677aaff976de37553d67a8f4c414537c:


package web

import (
	"bytes"
	"html/template"
	"net/url"
	"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)
	}
}

// Rewriting the body or the title is an event too — GitHub's "edited"
// — and a retitle names the new title, not an arrow between two
// paragraphs.
func TestTicketTimelineNamesEdits(t *testing.T) {
	tk := tktFixture()
	tk.Changes = append(tk.Changes, struct {
		Date   string        `json:"date"`
		User   string        `json:"user"`
		Fields []ticketField `json:"fields"`
	}{Date: "2026-08-04T10:00:00", User: "dee", Fields: []ticketField{{Name: "comment", Value: "new body"}, {Name: "cmimetype", Value: "text/x-markdown"}, {Name: "title", Value: "Short"}}})
	html := renderTkt(t, "tkt-timeline", pageData{Timeline: ticketTimeline(tk, nil)})
	if !strings.Contains(html, "dee edited the description and retitled <b>Short</b>") {
		t.Errorf("edit event missing:\n%s", html)
	}
}

// A field with known values that must still accept a new one. The
// no-script form shows BOTH controls, so what a reader sees is what
// happens: naming a new subsystem wins over the select, and the
// "Other" sentinel never reaches the tracker.
func TestNewSubsystemBeatsTheSelect(t *testing.T) {
	for _, tc := range []struct {
		name            string
		selected, typed string
		want            string
	}{
		{"picks an existing one", "web", "", "web"},
		{"names a new one", newFieldSentinel, "telemetry", "telemetry"},
		{"a new name wins over a stale selection", "web", "telemetry", "telemetry"},
		{"Other chosen but nothing typed stores nothing", newFieldSentinel, "", ""},
		{"nothing chosen at all", "", "", ""},
	} {
		t.Run(tc.name, func(t *testing.T) {
			form := url.Values{}
			if tc.selected != "" {
				form.Set("subsystem", tc.selected)
			}
			if tc.typed != "" {
				form.Set("subsystem_new", tc.typed)
			}
			vals := url.Values{}
			if v := strings.TrimSpace(form.Get("subsystem")); v != "" {
				vals.Set("subsystem", v)
			}
			if v := strings.TrimSpace(form.Get("subsystem_new")); v != "" {
				vals.Set("subsystem", v)
			} else if vals.Get("subsystem") == newFieldSentinel {
				vals.Del("subsystem")
			}
			if got := vals.Get("subsystem"); got != tc.want {
				t.Errorf("subsystem = %q, want %q", got, tc.want)
			}
			if got := vals.Get("subsystem"); got == newFieldSentinel {
				t.Error("the Other sentinel reached the tracker")
			}
		})
	}
}