package grid import ( "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" "ticktock/internal/config" "ticktock/internal/store" "ticktock/internal/tui/form" ) type fakeStore struct { entries []store.Entry added []store.Entry removed []string repl []store.Entry } func (f *fakeStore) Day(string) ([]store.Entry, error) { return f.entries, nil } func (f *fakeStore) Add(e store.Entry) error { f.added = append(f.added, e); return nil } func (f *fakeStore) Remove(k string) error { f.removed = append(f.removed, k); return nil } func (f *fakeStore) SafeReplace(k string, u, o store.Entry) error { f.repl = append(f.repl, u) return nil } func cfg() config.Config { return config.Default() } func key(s string) tea.KeyMsg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} } func special(t tea.KeyType) tea.KeyMsg { return tea.KeyMsg{Type: t} } // loaded returns a model that has already ingested its entries. func loaded(f *fakeStore) Model { m := New(f, cfg(), "2026-07-07", form.Suggest{}) nm, _ := m.Update(entriesMsg{entries: f.entries}) return nm.(Model) } func TestColumnToggle(t *testing.T) { m := loaded(&fakeStore{}) if m.col != ColActivity { t.Fatalf("default column should be ACTIVITY") } nm, _ := m.Update(special(tea.KeyRight)) if nm.(Model).col != ColLogged { t.Errorf("right should switch to LOGGED") } nm, _ = nm.(Model).Update(special(tea.KeyLeft)) if nm.(Model).col != ColActivity { t.Errorf("left should switch back to ACTIVITY") } } func TestCursorMovesWithinBounds(t *testing.T) { m := loaded(&fakeStore{}) up, _ := m.Update(special(tea.KeyUp)) if up.(Model).cursor != 0 { t.Errorf("cursor should clamp at 0 going up") } dn, _ := m.Update(special(tea.KeyDown)) if dn.(Model).cursor != 1 { t.Errorf("down should move to row 1") } } func TestSpaceAnchorsThenExtends(t *testing.T) { m := loaded(&fakeStore{}) // space anchors at row 0 (ACTIVITY column) nm, _ := m.Update(key(" ")) mm := nm.(Model) if mm.anchor == nil || *mm.anchor != 0 { t.Fatalf("space should anchor at cursor 0") } nm, _ = mm.Update(special(tea.KeyDown)) mm = nm.(Model) lo, hi, active := mm.selectionRange() if !active || lo != 0 || hi != 1 { t.Errorf("range after down = %d..%d active=%v, want 0..1 true", lo, hi, active) } } func TestEscClearsSelection(t *testing.T) { m := loaded(&fakeStore{}) nm, _ := m.Update(key(" ")) nm, _ = nm.(Model).Update(special(tea.KeyEsc)) if _, _, active := nm.(Model).selectionRange(); active { t.Errorf("esc should clear selection") } } func TestDayNavReloads(t *testing.T) { f := &fakeStore{} m := loaded(f) nm, cmd := m.Update(key("p")) if nm.(Model).date != "2026-07-06" { t.Errorf("p should go to previous day, got %s", nm.(Model).date) } if cmd == nil { t.Errorf("day nav should issue a reload cmd") } // n goes forward: from 2026-07-06 back to 2026-07-07 nm, _ = nm.(Model).Update(key("n")) if nm.(Model).date != "2026-07-07" { t.Errorf("n should go to next day, got %s", nm.(Model).date) } nm, _ = nm.(Model).Update(key("t")) // t jumps to today; just assert it changed away from 2026-07-07 if nm.(Model).date == "2026-07-06" { t.Errorf("t should jump to today") } _ = time.Now } func TestViewRendersColumns(t *testing.T) { f := &fakeStore{entries: []store.Entry{{ Key: "2026-07-07-01", Start: at(8, 0), End: at(8, 30), Description: "standup", Duration: 30 * 60 * 1e9, }}} out := loaded(f).View() if !strings.Contains(out, "TIME") || !strings.Contains(out, "LOGGED") { t.Errorf("view missing column headers:\n%s", out) } if !strings.Contains(out, "standup") { t.Errorf("view missing logged label:\n%s", out) } }