org/internal/ui/datepicker_test.go

183 lines
5.9 KiB
Go

package ui
import (
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
datepicker "github.com/ethanefung/bubble-datepicker"
"github.com/rwejlgaard/org/internal/config"
"github.com/rwejlgaard/org/internal/model"
)
func sameDay(a, b time.Time) bool {
return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day()
}
func TestNewDatePickerDefaultsToToday(t *testing.T) {
dp := newDatePicker(nil)
if !sameDay(dp.Time, time.Now()) {
t.Fatalf("expected today, got %v", dp.Time)
}
if !dp.Selected {
t.Fatal("expected date to be selected")
}
if dp.Focused != datepicker.FocusCalendar {
t.Fatalf("expected FocusCalendar, got %v", dp.Focused)
}
// Quit binding must be disabled so 'q' does not quit the app from the calendar.
if dp.KeyMap.Quit.Enabled() && len(dp.KeyMap.Quit.Keys()) > 0 {
t.Fatalf("expected Quit binding disabled, got keys %v", dp.KeyMap.Quit.Keys())
}
// Forwarding a 'q' key must not produce tea.Quit.
_, cmd := dp.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}})
if cmd != nil {
t.Fatal("expected no command from 'q' after disabling Quit")
}
}
func TestNewDatePickerUsesExisting(t *testing.T) {
existing := time.Date(2027, time.March, 9, 0, 0, 0, 0, time.Local)
dp := newDatePicker(&existing)
if !sameDay(dp.Time, existing) {
t.Fatalf("expected %v, got %v", existing, dp.Time)
}
}
func newDateModeModel(dateType string, existing *time.Time) (uiModel, *model.Item) {
cfg := config.DefaultConfig()
item := &model.Item{Title: "Task", Level: 1, Deadline: existing, Scheduled: existing}
of := &model.OrgFile{Items: []*model.Item{item}}
m := InitialModel(of, cfg, false, "")
m.editingItem = item
m.dateTextFocused = false
m.datepicker = newDatePicker(existing)
if dateType == "DEADLINE" {
m.mode = modeSetDeadline
} else {
m.mode = modeSetScheduled
}
return m, item
}
func TestCalendarEnterAppliesDate(t *testing.T) {
m, item := newDateModeModel("DEADLINE", nil)
m.datepicker.SetTime(time.Date(2027, time.March, 9, 0, 0, 0, 0, time.Local))
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyEnter}, "DEADLINE")
rm := res.(uiModel)
if item.Deadline == nil || !sameDay(*item.Deadline, time.Date(2027, time.March, 9, 0, 0, 0, 0, time.Local)) {
t.Fatalf("expected deadline 2027-03-09, got %v", item.Deadline)
}
if rm.mode != modeList {
t.Fatalf("expected mode to return to list, got %v", rm.mode)
}
}
func TestCalendarXClearsDate(t *testing.T) {
existing := time.Date(2027, time.March, 9, 0, 0, 0, 0, time.Local)
m, item := newDateModeModel("SCHEDULED", &existing)
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}, "SCHEDULED")
rm := res.(uiModel)
if item.Scheduled != nil {
t.Fatalf("expected scheduled cleared, got %v", item.Scheduled)
}
if rm.mode != modeList {
t.Fatalf("expected mode to return to list, got %v", rm.mode)
}
}
func TestDigitSwitchesToTextFocus(t *testing.T) {
m, _ := newDateModeModel("DEADLINE", nil)
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'2'}}, "DEADLINE")
rm := res.(uiModel)
if !rm.dateTextFocused {
t.Fatal("expected text focus after typing a digit")
}
if rm.textinput.Value() != "2" {
t.Fatalf("expected text seeded with '2', got %q", rm.textinput.Value())
}
if rm.mode != modeSetDeadline {
t.Fatalf("expected to stay in deadline mode, got %v", rm.mode)
}
}
func TestTextEnterParsesRelativeDate(t *testing.T) {
m, item := newDateModeModel("DEADLINE", nil)
m.dateTextFocused = true
m.textinput.SetValue("+7")
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyEnter}, "DEADLINE")
rm := res.(uiModel)
want := time.Now().AddDate(0, 0, 7)
if item.Deadline == nil || !sameDay(*item.Deadline, want) {
t.Fatalf("expected deadline %v, got %v", want, item.Deadline)
}
if rm.mode != modeList {
t.Fatalf("expected mode to return to list, got %v", rm.mode)
}
}
func TestTextEnterInvalidStaysInTextFocus(t *testing.T) {
m, item := newDateModeModel("DEADLINE", nil)
m.dateTextFocused = true
m.textinput.SetValue("not-a-date")
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyEnter}, "DEADLINE")
rm := res.(uiModel)
if item.Deadline != nil {
t.Fatalf("expected no deadline set, got %v", item.Deadline)
}
if rm.mode != modeSetDeadline || !rm.dateTextFocused {
t.Fatalf("expected to stay in text focus, got mode=%v textFocused=%v", rm.mode, rm.dateTextFocused)
}
}
func TestTextEscReturnsToCalendar(t *testing.T) {
m, _ := newDateModeModel("DEADLINE", nil)
m.dateTextFocused = true
m.textinput.SetValue("+3")
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyEsc}, "DEADLINE")
rm := res.(uiModel)
if rm.dateTextFocused {
t.Fatal("expected to return to calendar focus")
}
if rm.mode != modeSetDeadline {
t.Fatalf("expected to stay in deadline mode, got %v", rm.mode)
}
if rm.textinput.Value() != "" {
t.Fatalf("expected text cleared, got %q", rm.textinput.Value())
}
}
func TestCalendarEscCancels(t *testing.T) {
existing := time.Date(2027, time.March, 9, 0, 0, 0, 0, time.Local)
m, item := newDateModeModel("DEADLINE", &existing)
res, _ := m.updateSetDate(tea.KeyMsg{Type: tea.KeyEsc}, "DEADLINE")
rm := res.(uiModel)
if item.Deadline == nil {
t.Fatal("expected existing deadline untouched on cancel")
}
if rm.mode != modeList {
t.Fatalf("expected mode to return to list, got %v", rm.mode)
}
}
func TestOpeningDeadlineModeInitializesCalendar(t *testing.T) {
cfg := config.DefaultConfig()
item := &model.Item{Title: "Task", Level: 1}
of := &model.OrgFile{Items: []*model.Item{item}}
m := InitialModel(of, cfg, false, "")
// default deadline key is "d"
res, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}})
rm := res.(uiModel)
if rm.mode != modeSetDeadline {
t.Fatalf("expected modeSetDeadline, got %v", rm.mode)
}
if !rm.datepicker.Selected {
t.Fatal("expected datepicker to have a selected date")
}
if rm.dateTextFocused {
t.Fatal("expected calendar focus (dateTextFocused false)")
}
if rm.editingItem != item {
t.Fatal("expected editingItem to be the cursor item")
}
}