gomodoro/main_test.go
Ian Fijolek 631dc539f2
All checks were successful
continuous-integration/drone/push Build is passing
Move some of the test functions around for readability
2024-10-24 12:24:37 -07:00

310 lines
7.8 KiB
Go

package main
import (
"fmt"
"strings"
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
)
func notThis[T comparable](f compareFunc[T]) compareFunc[T] {
return func(a, b T) bool {
return !f(a, b)
}
}
type compareFunc[T comparable] func(T, T) bool
func assertFunc[T comparable](t *testing.T, compare compareFunc[T], actual, expected T, msg string) {
t.Helper()
if !compare(actual, expected) {
t.Errorf("%s: expected %v, got %v", msg, expected, actual)
}
}
// assertEqual checks if two values are equal and reports an error if they are not.
func assertEqual(t *testing.T, actual, expected interface{}, msg string) {
t.Helper()
if actual != expected {
t.Errorf("%s: expected %v, got %v", msg, expected, actual)
}
}
// assertNotEqual checks if two values are not equal and reports an error if they are.
func assertNotEqual(t *testing.T, actual, expected interface{}, msg string) {
t.Helper()
if actual == expected {
t.Errorf("%s: expected %v to be different from %v", msg, actual, expected)
}
}
// keyMsgs converts a series of strings or KeyTypes into a slice of KeyMsgs
func keyMsgs(keys ...interface{}) []tea.KeyMsg {
keyMessages := []tea.KeyMsg{}
for _, key := range keys {
switch keyType := key.(type) {
case tea.KeyType:
keyMessages = append(keyMessages, tea.KeyMsg{Type: keyType})
case string:
for _, r := range key.(string) {
keyMessages = append(keyMessages, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
}
default:
panic(fmt.Sprintf("Unknown key type: %v", key))
}
}
return keyMessages
}
func sendKeys[T tea.Model](m T, keys ...interface{}) (T, []tea.Cmd) {
var updatedModel tea.Model
var cmd tea.Cmd
var cmds []tea.Cmd
for _, key := range keyMsgs(keys...) {
updatedModel, cmd = m.Update(key)
m = updatedModel.(T)
if cmd != nil {
m, cmds, _ = batchCmdUpdate(m, cmd)
}
}
return m, cmds
}
func batchCmdUpdate[T tea.Model](m T, cmd tea.Cmd) (T, []tea.Cmd, error) {
var updatedModel tea.Model
var resultCmd tea.Cmd
var cmds []tea.Cmd
resultMsg := cmd()
batchMsg, ok := resultMsg.(tea.BatchMsg)
if !ok {
updatedModel, resultCmd = m.Update(resultMsg)
m = updatedModel.(T)
cmds = append(cmds, resultCmd)
return m, cmds, fmt.Errorf("Expected a batch message, got %v", resultMsg)
}
// Apply batch messages
for _, cmd := range batchMsg {
updatedModel, resultCmd = m.Update(cmd())
m = updatedModel.(T)
cmds = append(cmds, resultCmd)
}
return m, cmds, nil
}
type mockModel struct {
messages []tea.Msg
}
func (m mockModel) Init() tea.Cmd {
return nil
}
func (m mockModel) View() string {
s := ""
for _, msg := range m.messages {
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.Type == tea.KeyRunes {
s += string(keyMsg.Runes)
}
}
}
return s
}
func (m mockModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
m.messages = append(m.messages, msg)
if msg, ok := msg.(tea.KeyMsg); ok {
if msg.Type == tea.KeyEnter {
cmds = append(cmds, func() tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'\n'}} })
}
}
return m, tea.Batch(cmds...)
}
func TestBatchCmdUpdate(t *testing.T) {
m := mockModel{}
cmds := []tea.Cmd{
func() tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}} },
func() tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'b'}} },
}
m, _, err := batchCmdUpdate(m, tea.Batch(cmds...))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(m.messages) != 2 {
t.Fatalf("Expected 2 messages, got %v", len(m.messages))
}
if m.View() != "ab" {
t.Fatalf("Expected 'ab', got %s, %+v", m.View(), m)
}
m, _, err = batchCmdUpdate(m, func() tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}} })
if err == nil {
t.Fatalf("Expected error, got nil")
}
if len(m.messages) != 3 {
t.Fatalf("Expected 3 messages, got %v", len(m.messages))
}
if m.View() != "abc" {
t.Fatalf("Expected 'abc', got %v", m.View())
}
}
func TestSendKeys(t *testing.T) {
m := mockModel{}
m, _ = sendKeys(m, "ab")
if len(m.messages) != 2 {
t.Fatalf("Expected 2 messages, got %v", len(m.messages))
}
if m.View() != "ab" {
t.Fatalf("Expected 'ab', got %s", m.View())
}
m, _ = sendKeys(m, "c", tea.KeyEnter)
// We expect 5 because enter key should create a new message for the newline
if len(m.messages) != 5 {
t.Fatalf("Expected 5 messages, got %v", len(m.messages))
}
if m.View() != "abc\n" {
t.Fatalf("Expected 'abc\n', got %s", m.View())
}
}
// TestParseDuration tests the parseDuration function
func TestParseDuration(t *testing.T) {
tests := []struct {
input string
expected time.Duration
hasError bool
}{
{"10", 10 * time.Minute, false},
{"1h", 1 * time.Hour, false},
{"invalid", 0, true},
}
for _, test := range tests {
result, err := parseDuration(test.input)
if test.hasError {
assertNotEqual(t, err, nil, "Expected an error for input "+test.input)
} else {
assertEqual(t, err, nil, "Did not expect an error for input "+test.input)
assertEqual(t, result, test.expected, "Expected duration for input "+test.input)
}
}
}
// TestRunCommands tests the runCommands function
func TestRunCommands(t *testing.T) {
m := newModelBasic(false, "#ffdd57", "#57ddff")
m.onFocusStart = []string{"echo Focus Start"}
m.Init()
m.startCommands(m.onFocusStart)
}
// TestInputView tests the Update method of the model for the input view
func TestInputView(t *testing.T) {
m := newModel(false, "#ffdd57", "#57ddff", 0, 0, 0, []string{}, []string{}, []string{})
var err error
m, _, err = batchCmdUpdate(m, m.Init())
if err != nil {
t.Fatalf("Expected batch command after init: %v", err)
}
// Verify we're on the input screen
assertEqual(t, m.currentScreen, INPUT_SCREEN, "Expected currentScreen to be INPUT_SCREEN")
assertFunc(t, strings.Contains, m.View(), "Break time", "Expected view to contain 'Break time'")
// Fill the form
m, _ = sendKeys(m, "10m", tea.KeyEnter, "10m", tea.KeyEnter, "2", tea.KeyEnter)
// Pass on submit command, doesn't happen because we have multiple layers of batching at the end
m, _, _ = batchCmdUpdate(m, formSubmit)
assertEqual(t, m.focusTime, 10*time.Minute, "Expected focus time to be 10 minutes")
assertEqual(t, m.breakTime, 10*time.Minute, "Expected break time to be 10 minutes")
assertEqual(t, m.intervals, 2, "Expected rounds to be 2")
// Pass on start command, doesn't happen because we have multiple layers of batching at the end
m, _, _ = batchCmdUpdate(m, startTimer)
assertFunc(t, notThis(strings.Contains), m.View(), "Break time", "Expected view to NOT contain 'Break time'")
assertEqual(t, m.currentScreen, TIMER_SCREEN, "Expected currentScreen to be TIMER_SCREEN")
}
func TestTimerView(t *testing.T) {
m := newModel(false, "#ffdd57", "#57ddff", 10*time.Minute, 10*time.Minute, 2, []string{}, []string{}, []string{})
// Init model with timer values
m, _, err := batchCmdUpdate(m, m.Init())
if err != nil {
t.Fatalf("Expected batch command after init: %v", err)
}
// Start timer (batch result from above doesn't apply here)
m, _, _ = batchCmdUpdate(m, startTimer)
// Test timer view
assertEqual(t, m.currentScreen, TIMER_SCREEN, "Expected currentScreen to be timerScreen")
assertFunc(t, strings.Contains, m.View(), "Focus", "Expected view to contain 'Focus'")
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
oneSecCmd := func() tea.Msg { return timeMsg(time.Now().Add(1 * time.Second)) }
m, _, _ = batchCmdUpdate(m, oneSecCmd)
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
// Test switch to break time
m.startTime = m.startTime.Add(-10 * time.Minute)
m, _, _ = batchCmdUpdate(m, oneSecCmd)
assertEqual(t, m.state, "Break", "Expected state to be 'Break'")
// Switch back to focus time
m.startTime = m.startTime.Add(-10 * time.Minute)
m, _, _ = batchCmdUpdate(m, oneSecCmd)
assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'")
t.Logf("Incorrect state %+v", m)
}