291 lines
9.0 KiB
Go
291 lines
9.0 KiB
Go
package tui
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"superwork-tui/internal/issue"
|
|
)
|
|
|
|
// sendEnter sends the Enter key to the model.
|
|
func sendEnter(m Model) (Model, tea.Cmd) {
|
|
next, cmd := m.Update(keyMsg("enter"))
|
|
return next.(Model), cmd
|
|
}
|
|
|
|
// sendEsc sends the Esc key to the model.
|
|
func sendEsc(m Model) (Model, tea.Cmd) {
|
|
next, cmd := m.Update(keyMsg("esc"))
|
|
return next.(Model), cmd
|
|
}
|
|
|
|
// typeInto simulates typing each character into the model sequentially.
|
|
func typeInto(m Model, s string) Model {
|
|
for _, ch := range s {
|
|
next, _ := m.Update(keyMsg(string(ch)))
|
|
m = next.(Model)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// ── p key enters input state ─────────────────────────────────────────────────
|
|
|
|
func TestPKey_EntersInputState(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 7, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m2, _ := sendKey(m, "p")
|
|
|
|
if m2.state != stateInput {
|
|
t.Errorf("state = %v, want stateInput", m2.state)
|
|
}
|
|
if m2.inputPend != 7 {
|
|
t.Errorf("inputPend = %d, want 7", m2.inputPend)
|
|
}
|
|
}
|
|
|
|
func TestPKey_NoIssue_NoOp(t *testing.T) {
|
|
m := makeLoadedModel(nil, 0, 0)
|
|
m2, _ := sendKey(m, "p")
|
|
if m2.state != stateLoaded {
|
|
t.Errorf("state = %v, want stateLoaded (no-op when no issues)", m2.state)
|
|
}
|
|
}
|
|
|
|
// ── esc cancels input ────────────────────────────────────────────────────────
|
|
|
|
func TestInputEsc_CancelsWithoutDispatch(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 3, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
if m.state != stateInput {
|
|
t.Fatal("precondition: should be in input state")
|
|
}
|
|
m2, cmd := sendEsc(m)
|
|
if m2.state != stateLoaded {
|
|
t.Errorf("state = %v, want stateLoaded after esc", m2.state)
|
|
}
|
|
if m2.inputPend != 0 {
|
|
t.Errorf("inputPend = %d, want 0 after esc", m2.inputPend)
|
|
}
|
|
if cmd != nil {
|
|
t.Errorf("cmd should be nil after esc cancel")
|
|
}
|
|
}
|
|
|
|
// ── empty enter cancels ──────────────────────────────────────────────────────
|
|
|
|
func TestInputEnterEmpty_Cancels(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 3, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
m2, cmd := sendEnter(m) // empty value
|
|
if m2.state != stateLoaded {
|
|
t.Errorf("state = %v, want stateLoaded after empty enter", m2.state)
|
|
}
|
|
if cmd != nil {
|
|
t.Errorf("cmd should be nil after empty enter cancel")
|
|
}
|
|
}
|
|
|
|
// ── valid enter dispatches setDepCmd ─────────────────────────────────────────
|
|
|
|
func TestInputEnterValid_DispatchesCmd(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
m = typeInto(m, "2") // type "2" as prereq number
|
|
m2, cmd := sendEnter(m)
|
|
|
|
if m2.state != stateLoaded {
|
|
t.Errorf("state = %v, want stateLoaded after valid enter", m2.state)
|
|
}
|
|
if cmd == nil {
|
|
t.Error("cmd should be non-nil after valid prereq entered")
|
|
}
|
|
// Optimistic update: Prerequisite should be set on issue #5
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 5 {
|
|
if iss.Prerequisite != 2 {
|
|
t.Errorf("optimistic Prerequisite = %d, want 2", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #5 not found after optimistic update")
|
|
}
|
|
|
|
func TestInputEnterInvalid_SetsStatusNoCmd(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
m = typeInto(m, "abc") // invalid number
|
|
m2, cmd := sendEnter(m)
|
|
|
|
if cmd != nil {
|
|
t.Error("cmd should be nil for invalid prereq input")
|
|
}
|
|
if m2.statusMsg == "" {
|
|
t.Error("statusMsg should be set for invalid input")
|
|
}
|
|
}
|
|
|
|
// ── other keys blocked in input state ────────────────────────────────────────
|
|
|
|
func TestInputState_BlocksOtherKeys(t *testing.T) {
|
|
issues := []issue.Issue{
|
|
{Number: 1, Column: issue.ColumnTodo},
|
|
{Number: 2, Column: issue.ColumnTodo},
|
|
}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
// Try sending 'q' — should NOT quit (it's still in input state)
|
|
m2, cmd := sendKey(m, "q")
|
|
if m2.state == stateLoaded && cmd != nil {
|
|
// If cmd is tea.Quit the program would exit; we just verify state stays input-ish
|
|
// In practice tea.Quit is a Cmd, so if it was dispatched that's a bug.
|
|
// The simplest check: 'q' in input mode should forward to textinput, not quit.
|
|
}
|
|
// State should still be stateInput (q is just typed into the textinput)
|
|
if m2.state != stateInput {
|
|
t.Errorf("state = %v after 'q' in input mode, want stateInput (should not quit)", m2.state)
|
|
}
|
|
}
|
|
|
|
// ── P key clears prerequisite ────────────────────────────────────────────────
|
|
|
|
func TestPShift_ClearsPrerequisite(t *testing.T) {
|
|
issues := []issue.Issue{
|
|
{Number: 10, Column: issue.ColumnTodo, Prerequisite: 3},
|
|
}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m2, cmd := sendKey(m, "P")
|
|
|
|
if cmd == nil {
|
|
t.Error("cmd should be non-nil after P (clearDepCmd)")
|
|
}
|
|
// Optimistic clear
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 10 {
|
|
if iss.Prerequisite != 0 {
|
|
t.Errorf("optimistic Prerequisite = %d, want 0 after P", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #10 not found")
|
|
}
|
|
|
|
func TestPShift_NoPrereq_NoOp(t *testing.T) {
|
|
issues := []issue.Issue{
|
|
{Number: 10, Column: issue.ColumnTodo, Prerequisite: 0},
|
|
}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m2, cmd := sendKey(m, "P")
|
|
if cmd != nil {
|
|
t.Error("cmd should be nil when no prereq to clear")
|
|
}
|
|
_ = m2
|
|
}
|
|
|
|
// ── setDepResultMsg optimistic update ────────────────────────────────────────
|
|
|
|
func TestSetDepResult_Success_UpdatesPrerequisite(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo, Prerequisite: 0}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
|
|
msg := setDepResultMsg{issueNumber: 5, prerequisite: 2, err: nil}
|
|
next, _ := m.Update(msg)
|
|
m2 := next.(Model)
|
|
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 5 {
|
|
if iss.Prerequisite != 2 {
|
|
t.Errorf("Prerequisite = %d, want 2", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #5 not found")
|
|
}
|
|
|
|
func TestSetDepResult_Error_RollsBackToOldPrereq(t *testing.T) {
|
|
// Issue starts with prereq=5; user tries to set to 7 (optimistic write already done).
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo, Prerequisite: 7}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
|
|
msg := setDepResultMsg{issueNumber: 5, prerequisite: 7, oldPrereq: 5, err: errors.New("network error")}
|
|
next, _ := m.Update(msg)
|
|
m2 := next.(Model)
|
|
|
|
if m2.statusMsg == "" {
|
|
t.Error("statusMsg should be set on setDep error")
|
|
}
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 5 {
|
|
if iss.Prerequisite != 5 {
|
|
t.Errorf("Prerequisite = %d, want 5 (rollback to old value)", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #5 not found")
|
|
}
|
|
|
|
// ── clearDepResultMsg optimistic update ─────────────────────────────────────
|
|
|
|
func TestClearDepResult_Success_ClearsPrerequisite(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo, Prerequisite: 3}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
|
|
msg := clearDepResultMsg{issueNumber: 5, oldPrereq: 3, err: nil}
|
|
next, _ := m.Update(msg)
|
|
m2 := next.(Model)
|
|
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 5 {
|
|
if iss.Prerequisite != 0 {
|
|
t.Errorf("Prerequisite = %d, want 0 after clear", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #5 not found")
|
|
}
|
|
|
|
func TestClearDepResult_Error_RollsBackToOldPrereq(t *testing.T) {
|
|
// Issue starts with prereq=3 already optimistically cleared to 0.
|
|
issues := []issue.Issue{{Number: 5, Column: issue.ColumnTodo, Prerequisite: 0}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
|
|
msg := clearDepResultMsg{issueNumber: 5, oldPrereq: 3, err: errors.New("api error")}
|
|
next, _ := m.Update(msg)
|
|
m2 := next.(Model)
|
|
|
|
if m2.statusMsg == "" {
|
|
t.Error("statusMsg should be set on clearDep error")
|
|
}
|
|
for _, iss := range m2.issues {
|
|
if iss.Number == 5 {
|
|
if iss.Prerequisite != 3 {
|
|
t.Errorf("Prerequisite = %d, want 3 (rollback to old value)", iss.Prerequisite)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("issue #5 not found")
|
|
}
|
|
|
|
// ── statusBar shows input prompt ──────────────────────────────────────────────
|
|
|
|
func TestStatusBar_ShowsInputPrompt(t *testing.T) {
|
|
issues := []issue.Issue{{Number: 42, Column: issue.ColumnTodo}}
|
|
m := makeLoadedModel(issues, 0, 0)
|
|
m, _ = sendKey(m, "p")
|
|
|
|
bar := m.statusBar()
|
|
if bar == "" {
|
|
t.Error("statusBar should show input prompt in stateInput")
|
|
}
|
|
}
|