229 lines
7.3 KiB
Go
229 lines
7.3 KiB
Go
package tui
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh"
|
|
)
|
|
|
|
func TestPRTableRendersRows(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rows = []prRow{{
|
|
kind: "remote",
|
|
pr: gh.PR{Number: 6441, Title: "Human PR", Author: "ajnart", CIState: "SUCCESS"},
|
|
imageState: "yes",
|
|
}}
|
|
m.table.SetRows(buildTableRows(m.rows))
|
|
|
|
if got := m.View().Content; !strings.Contains(got, "Human PR") {
|
|
t.Fatalf("table view omitted rows: %q", got)
|
|
}
|
|
if !m.View().AltScreen {
|
|
t.Fatal("development browser must use the alternate screen")
|
|
}
|
|
}
|
|
|
|
func TestPRTableArrowNavigation(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rows = []prRow{
|
|
{kind: "remote", pr: gh.PR{Number: 1, Title: "First"}},
|
|
{kind: "remote", pr: gh.PR{Number: 2, Title: "Second"}},
|
|
}
|
|
m.table.SetRows(buildTableRows(m.rows))
|
|
|
|
updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
|
|
got := updated.(prsModel).table.SelectedRow()
|
|
if len(got) < 3 || got[2] != "2" {
|
|
t.Fatalf("down arrow selected %v, want PR 2", got)
|
|
}
|
|
}
|
|
|
|
func TestPRImageChecksAreBounded(t *testing.T) {
|
|
m := newPRsModel()
|
|
prs := make([]gh.PR, 20)
|
|
for i := range prs {
|
|
prs[i] = gh.PR{Number: i + 1, Title: "PR"}
|
|
}
|
|
|
|
updated, cmd := m.Update(prsLoadedMsg{prs: prs})
|
|
got := updated.(prsModel)
|
|
if cmd == nil {
|
|
t.Fatal("expected image check commands")
|
|
}
|
|
if got.imageActive != 6 || len(got.imageQueue) != 14 {
|
|
t.Fatalf("active=%d queued=%d, want active=6 queued=14", got.imageActive, len(got.imageQueue))
|
|
}
|
|
}
|
|
|
|
func TestPRLoadedReusesImageChecksUnlessRefreshIsForced(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 1}, imageState: "yes"}}
|
|
|
|
updated, _ := m.Update(prsLoadedMsg{prs: []gh.PR{{Number: 1}, {Number: 2}}})
|
|
cached := updated.(prsModel)
|
|
if cached.rows[0].imageState != "yes" || len(cached.imageQueue) != 0 || cached.imageActive != 1 {
|
|
t.Fatalf("cached states were not reused: rows=%#v queue=%v active=%d", cached.rows, cached.imageQueue, cached.imageActive)
|
|
}
|
|
|
|
updated, _ = cached.Update(prsLoadedMsg{prs: []gh.PR{{Number: 1}, {Number: 2}}, refresh: true})
|
|
refreshed := updated.(prsModel)
|
|
if refreshed.rows[0].imageState != "checking" || refreshed.imageActive != 2 {
|
|
t.Fatalf("forced refresh reused image states: rows=%#v active=%d", refreshed.rows, refreshed.imageActive)
|
|
}
|
|
}
|
|
|
|
func TestDevelopmentRowsFoldCurrentLocalPRImage(t *testing.T) {
|
|
prs := []gh.PR{{Number: 6502, HeadSHA: "current"}}
|
|
images := []docker.Image{
|
|
{Tag: "pr-6502", PRNumber: 6502, Revision: "current"},
|
|
{Tag: "pr-6502-stale", PRNumber: 6502, Revision: "stale"},
|
|
}
|
|
|
|
rows, queue := developmentRows(prs, images, nil)
|
|
if len(rows) != 2 || !rows[0].hasCurrentLocalImage() || rows[0].local.Tag != "pr-6502" {
|
|
t.Fatalf("current image was not folded into PR row: %#v", rows)
|
|
}
|
|
if rows[1].kind != "local" || rows[1].local.Tag != "pr-6502-stale" {
|
|
t.Fatalf("stale image should remain separate: %#v", rows[1])
|
|
}
|
|
if got := buildTableRows(rows)[0][6]; got != imgLocal {
|
|
t.Fatalf("image marker = %q, want %q", got, imgLocal)
|
|
}
|
|
if len(queue) != 0 {
|
|
t.Fatalf("current local image queued unnecessary registry checks: %v", queue)
|
|
}
|
|
}
|
|
|
|
func TestLocalPRPlanUsesPRContainerIdentity(t *testing.T) {
|
|
row := prRow{
|
|
kind: "remote",
|
|
pr: gh.PR{Number: 6502, HeadSHA: "abcdef1234567890"},
|
|
local: docker.Image{Tag: "pr-6502", PRNumber: 6502, Revision: "abcdef1234567890"},
|
|
}
|
|
plan, err := localPRPlan(row, false, func(int) int { return 7575 })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if plan.Image != "homarr:pr-6502" || plan.Name != "homarr_pr_6502" || plan.Volume != "homarr_pr_6502_data" || plan.Pull || plan.PRNumber != 6502 {
|
|
t.Fatalf("unexpected local PR plan: %#v", plan)
|
|
}
|
|
}
|
|
|
|
func TestRemotePRBuildUsesTemporaryPRWorkflow(t *testing.T) {
|
|
image, action, status := localBuildForRow(prRow{kind: "remote", pr: gh.PR{Number: 6441}})
|
|
if image.Tag != "pr-6441" || image.PRNumber != 6441 || action != "built" || !strings.Contains(status, "building PR #6441 locally") {
|
|
t.Fatalf("image=%#v action=%q status=%q", image, action, status)
|
|
}
|
|
}
|
|
|
|
func TestPRImageCheckInProgressDoesNotLaunch(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 6441, Title: "Pending image"}, imageState: "checking"}}
|
|
m.table.SetRows(buildTableRows(m.rows))
|
|
|
|
updated, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter}))
|
|
got := updated.(prsModel)
|
|
if cmd != nil {
|
|
t.Fatal("unavailable image returned a launch command")
|
|
}
|
|
if !strings.Contains(got.status, "still in progress") {
|
|
t.Fatalf("status = %q, want launch rejection", got.status)
|
|
}
|
|
}
|
|
|
|
func TestPullErrorIncludesLastDockerMessage(t *testing.T) {
|
|
err := commandErrorWithDetail(errors.New("exit status 1"), "manifest unknown")
|
|
if !strings.Contains(err.Error(), "manifest unknown") {
|
|
t.Fatalf("pull error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOutputTailRetainsBuildFailure(t *testing.T) {
|
|
tail := &outputTail{}
|
|
_, _ = tail.Write([]byte("building\r\nERROR: Dockerfile not found\n"))
|
|
if got := tail.lastLine(); got != "ERROR: Dockerfile not found" {
|
|
t.Fatalf("last line = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDevTableRendersLocalImageSource(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rows = []prRow{{
|
|
kind: "local",
|
|
local: docker.Image{Tag: "feature", Source: "/tmp/homarr", Revision: "abcdef1234567890"},
|
|
imageState: "yes",
|
|
}}
|
|
m.table.SetRows(buildTableRows(m.rows))
|
|
|
|
content := m.View().Content
|
|
for _, want := range []string{"local", "homarr:feature", "/tmp/homarr", "abcdef123456"} {
|
|
if !strings.Contains(content, want) {
|
|
t.Fatalf("development view omitted %q: %q", want, content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRebuildCompletionRestoresLoadingState(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rebuilding = true
|
|
image := docker.Image{Tag: "feature"}
|
|
|
|
updated, cmd := m.Update(rebuildDoneMsg{image: image})
|
|
got := updated.(prsModel)
|
|
if got.rebuilding || !got.loading || got.status != "rebuilt homarr:feature" || cmd == nil {
|
|
t.Fatalf("unexpected rebuild state: rebuilding=%v loading=%v status=%q", got.rebuilding, got.loading, got.status)
|
|
}
|
|
}
|
|
|
|
func TestRebuildCanBeCanceled(t *testing.T) {
|
|
m := newPRsModel()
|
|
canceled := false
|
|
m.rebuilding = true
|
|
m.rebuildCancel = func() { canceled = true }
|
|
|
|
updated, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'q'}))
|
|
got := updated.(prsModel)
|
|
if !canceled || cmd != nil || got.status != "canceling rebuild…" {
|
|
t.Fatalf("cancel state: canceled=%v status=%q", canceled, got.status)
|
|
}
|
|
}
|
|
|
|
func TestNavigationRemainsAvailableDuringBuild(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.rebuilding = true
|
|
m.rows = []prRow{
|
|
{kind: "remote", pr: gh.PR{Number: 1}},
|
|
{kind: "remote", pr: gh.PR{Number: 2}},
|
|
}
|
|
m.table.SetRows(buildTableRows(m.rows))
|
|
|
|
updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown}))
|
|
selected := updated.(prsModel).table.SelectedRow()
|
|
if len(selected) < 3 || selected[2] != "2" {
|
|
t.Fatalf("build blocked navigation: %v", selected)
|
|
}
|
|
}
|
|
|
|
func TestCompletedPullWithoutPlanDoesNotPanic(t *testing.T) {
|
|
m := newPRsModel()
|
|
m.pulling = true
|
|
m.pullCancel = func() {}
|
|
|
|
updated, cmd := m.Update(pullEvent{done: true})
|
|
got := updated.(prsModel)
|
|
if got.pulling || cmd != nil {
|
|
t.Fatalf("pulling=%v cmd=%v", got.pulling, cmd)
|
|
}
|
|
}
|
|
|
|
func TestTruncateTextPreservesUTF8(t *testing.T) {
|
|
if got := truncateText("🦞homarr", 2); got != "🦞h" {
|
|
t.Fatalf("truncateText() = %q", got)
|
|
}
|
|
}
|