package harness
import (
"io"
"net/http"
"net/url"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ops"
)
// The ticket board must work with no script at all. Columns are grouped
// on the server and a card is moved with a form that submits — the
// select is the MECHANISM, not a fallback to be hidden once JavaScript
// loads. Any drag layer added later sits on top of this; if hiding the
// select ever makes this test fail, the board has stopped being
// operable by touch and keyboard readers.
func TestBoardWorksWithoutScript(t *testing.T) {
stores := map[string]string{"alpha": buildStore(t, "boardseed")}
srv := ingestAndServeAuth(t, stores, t.TempDir(),
func(repo, user, pass string, _ ops.Auth) api.Decision {
if repo == "alpha" && user == "dan" && pass == "hunter2" {
return api.Allowed
}
return api.Denied
})
client := srv.Client()
client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
post := func(path string, form url.Values) *http.Response {
req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("dan", "hunter2")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp
}
get := func(path string) string {
req, _ := http.NewRequest("GET", srv.URL+path, nil)
req.SetBasicAuth("dan", "hunter2")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return string(b)
}
if resp := post("/r/alpha/ticket/new", url.Values{
"title": {"a card to move"}, "comment": {"body"},
}); resp.StatusCode != http.StatusSeeOther {
t.Fatalf("ticket new: %d", resp.StatusCode)
}
board := get("/r/alpha/ticket?view=board")
if !strings.Contains(board, `class="board"`) {
t.Fatalf("no board rendered:\n%s", board)
}
if !strings.Contains(board, "a card to move") {
t.Error("the new ticket is not on the board")
}
// The move control must be real, submitting markup — not something a
// script has to activate. The Move menu is a native popover holding
// one plain form per destination column (the drag layer is an
// enhancement over the same POST).
if !strings.Contains(board, `class="mv-btn"`) {
t.Error("no Move control on the card")
}
if !strings.Contains(board, `/move?to=`) {
t.Error("the move control is not a submitting form")
}
// Move it exactly as a reader with no JavaScript would: submit the form.
id := regexp.MustCompile(`/r/alpha/ticket/([0-9a-f]{10})/move\?to=`).FindStringSubmatch(board)
if id == nil {
t.Fatalf("no move form target in the board:\n%s", board)
}
if resp := post("/r/alpha/ticket/"+id[1]+"/move?to=Closed&field=status", url.Values{}); resp.StatusCode != http.StatusSeeOther {
t.Fatalf("move: %d", resp.StatusCode)
}
// It left the open board, and appears on the one that shows closed.
if b := get("/r/alpha/ticket?view=board"); strings.Contains(b, "a card to move") {
t.Error("a closed ticket is still on the open board")
}
all := get("/r/alpha/ticket?view=board&closed=1")
if !strings.Contains(all, "a card to move") {
t.Errorf("the moved ticket is on neither board:\n%s", all)
}
if !strings.Contains(all, "Closed") {
t.Error("no Closed column after moving a ticket into it")
}
}