mirror of
https://github.com/RWejlgaard/org.git
synced 2026-09-01 17:05:34 +00:00
* feat: add datepicker dependency and init helper * feat: calendar-first set-date modes with text fallback * feat: initialize datepicker when opening set-date modes * feat: render calendar and hints in set-date view * chore: go mod tidy for bubble-datepicker direct dependency Promotes github.com/ethanefung/bubble-datepicker from indirect to a direct require (it is imported directly by internal/ui) and drops the now-unused golang.org/x/text v0.3.8 checksums.
183 lines
5.9 KiB
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")
|
|
}
|
|
}
|