diff --git a/pkg/tui/payment_form.go b/pkg/tui/payment_form.go index 724876b..a237965 100644 --- a/pkg/tui/payment_form.go +++ b/pkg/tui/payment_form.go @@ -355,11 +355,16 @@ func (m Model) RenderCardList() string { boxes = append(boxes, box) } - sshFocused := m.payment.cardCursor == len(m.SavedCards) - sshBox := m.CreateBox(m.formatListItem("+ add card via ssh", sshFocused), sshFocused) + // SSH inline card-add is dormant until i have a norwegian org + // number wired to stripe, something i dont have since its $300+. Keep the row visible so the option is + // discoverable, but render it dimmed and exclude it from cursor space + // (handled in PaymentUpdate below) so it can not be picked + dim := m.theme.TextDim().Render + sshLabel := dim("+ add card via ssh") + " " + dim("(WIP)") + sshBox := m.CreateBox(m.formatListItem(sshLabel, false), false) boxes = append(boxes, sshBox) - httpsFocused := m.payment.cardCursor == len(m.SavedCards)+1 + httpsFocused := m.payment.cardCursor == len(m.SavedCards) httpsBox := m.CreateBox(m.formatListItem("+ add payment via browser", httpsFocused), httpsFocused) boxes = append(boxes, httpsBox) @@ -501,7 +506,7 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { m.payment.cardCursor-- } case "down", "j": - if m.payment.cardCursor < len(m.SavedCards)+1 { + if m.payment.cardCursor < len(m.SavedCards) { m.payment.cardCursor++ } @@ -511,17 +516,8 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { m.SelectedCard = &selected return m.ReviewSwitch() } - if m.payment.cardCursor == len(m.SavedCards) { - m.payment.view = 1 - m.payment.form = m.InitPaymentForm() - m.footer = []footerCommand{ - {key: "tab", value: "next"}, - {key: "enter", value: "submit"}, - {key: "esc", value: "back"}, - } - return m, m.payment.form.form.Init() - } - // Browser payment + // Browser flow, the only reachable add-card path while the SSH + // inline form is dormant (see RenderCardList) m.payment.view = 2 m.payment.collectURL = nil m.payment.collectTimeOut = false @@ -546,16 +542,6 @@ func (m Model) PaymentUpdate(msg tea.Msg) (Model, tea.Cmd) { if m.payment.cardCursor >= len(m.SavedCards) && m.payment.cardCursor > 0 { m.payment.cardCursor-- } - if len(m.SavedCards) == 0 { - m.payment.view = 1 - m.payment.form = m.InitPaymentForm() - m.footer = []footerCommand{ - {key: "tab", value: "next"}, - {key: "enter", value: "submit"}, - {key: "esc", value: "back"}, - } - return m, tea.Batch(m.payment.form.form.Init(), m.deleteCardCmd(card.ID)) - } return m, m.deleteCardCmd(card.ID) } } diff --git a/pkg/tui/payment_form_test.go b/pkg/tui/payment_form_test.go new file mode 100644 index 0000000..596c1dd --- /dev/null +++ b/pkg/tui/payment_form_test.go @@ -0,0 +1,91 @@ +package tui + +import ( + "strings" + "testing" + + "terminalShop/pkg/models" + + tea "charm.land/bubbletea/v2" +) + +// newPaymentCardListModel parks a Model on the payment page card-list view +// with n synthetic saved cards. Baseline for assertint that the dormant +// "+ add card via ssh" row is dimmed and unreachable by cursor navigation. +func newPaymentCardListModel(t *testing.T, n int) Model { + t.Helper() + m := NewModel("test") + m = m.updateLayout(120, 40) + m.currentPage = paymentPage + m.payment.view = 0 + m.payment.form = nil + m.payment.cardCursor = 0 + for i := range n { + m.SavedCards = append(m.SavedCards, models.Card{ + ID: uint(i + 1), + Brand: "visa", + Last4: "4242", + ExpMonth: 12, + ExpYear: 2030, + }) + } + return m +} + +func keyEnter() tea.KeyMsg { return tea.KeyPressMsg{Code: tea.KeyEnter} } + +func TestRenderCardList_SshOptionComingSoon(t *testing.T) { + m := newPaymentCardListModel(t, 1) + out := m.RenderCardList() + if !strings.Contains(out, "add card via ssh") { + t.Fatalf("ssh option missing from card list output:\n%s", out) + } + if !strings.Contains(out, "WIP") { + t.Fatalf("want 'WIP' marked on ssh option:\n%s", out) + } +} + +func TestPaymentUpdate_DownNavigationStopsAtBrowserSlot(t *testing.T) { + m := newPaymentCardListModel(t, 2) + for range 10 { + var cmd tea.Cmd + m, cmd = m.PaymentUpdate(keyJ()) + _ = cmd + } + want := len(m.SavedCards) // browser slot; ssh slot in unreachable + if m.payment.cardCursor != want { + t.Fatalf("cursor stopped at %d, want %d (browser slot)", m.payment.cardCursor, want) + } +} + +func TestPaymentUpdate_EnterOnBrowserSlotLaunchesBrowserFlow(t *testing.T) { + m := newPaymentCardListModel(t, 1) + m.payment.cardCursor = len(m.SavedCards) // browser slot + newM, cmd := m.PaymentUpdate(keyEnter()) + if newM.payment.view != 2 { + t.Fatalf("payment.view = %d, want 2 (browser flow)", newM.payment.view) + } + if newM.payment.form != nil { + t.Fatalf("payment.form must be nil after enter; ssh inline form is dormant") + } + if cmd == nil { + t.Fatalf("expected collectCardCmd, got nil") + } +} + +func TestPaymentUpdate_EnterWithZeroCardsLaunchesBrowserFlow(t *testing.T) { + m := newPaymentCardListModel(t, 0) + if m.payment.cardCursor != 0 { + t.Fatalf("default cursor = %d, want 0 ", m.payment.cardCursor) + } + newM, cmd := m.PaymentUpdate(keyEnter()) + if newM.payment.view != 2 { + t.Fatalf("payment.view = %d, want 2 (browser flow)", newM.payment.view) + } + if newM.payment.form != nil { + t.Fatalf("payment.form must be nil; ssh inline form is dormant") + } + if cmd == nil { + t.Fatalf("expected collectCardCmd, got nil") + } +}