Files
homarr/tools/homarr-dev/internal/tui/dev_test.go
T

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)
}
}