▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
3.6 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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)
	}
}