package main import ( "fmt" "reflect" "runtime" "strings" "testing" "time" tea "github.com/charmbracelet/bubbletea" ) 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) } } // 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) } func keyMsgs(keys ...interface{}) []tea.Msg { keyMessages := []tea.Msg{} 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(m model, keys ...interface{}) (model, tea.Cmd) { var updatedModel tea.Model var cmd tea.Cmd for _, key := range keyMsgs(keys...) { updatedModel, cmd = m.Update(key) m = updatedModel.(model) if cmd != nil { updatedModel, cmd = m.Update(cmd()) m = updatedModel.(model) } } return m, cmd } // 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 updatedModel tea.Model var resultCmd tea.Cmd updatedModel, _ = m.Update(m.Init()) m = updatedModel.(model) // Verify we're on the input screen assertEqual(t, m.currentScreen, INPUT_SCREEN, "Expected currentScreen to be inputScreen") assertFunc(t, strings.Contains, m.View(), "Break time", "Expected view to contain 'enter next'") // Fill the form m, resultCmd = sendKeys(m, "10m", tea.KeyEnter, "10m", tea.KeyEnter, "2", tea.KeyEnter) assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil") // Apply final next result updatedModel, resultCmd = m.Update(resultCmd()) m = updatedModel.(model) // Make sure the next command is to submit the form assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil") assertEqual( t, reflect.ValueOf(resultCmd).Pointer(), reflect.ValueOf(formSubmit).Pointer(), fmt.Sprintf("Expected resultCmd to be formSubmit, found %v", runtime.FuncForPC(reflect.ValueOf(resultCmd).Pointer()).Name()), ) // Apply submit form command updatedModel, resultCmd = m.Update(resultCmd()) m = updatedModel.(model) assertEqual(t, m.err, nil, "Expected no error") assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil") assertEqual( t, reflect.ValueOf(resultCmd).Pointer(), reflect.ValueOf(startTimer).Pointer(), fmt.Sprintf("Expected resultCmd to be startTimer, found %v", runtime.FuncForPC(reflect.ValueOf(resultCmd).Pointer()).Name()), ) } func TestTimerView(t *testing.T) { m := newModel(false, "#ffdd57", "#57ddff", 10*time.Minute, 10*time.Minute, 2, []string{}, []string{}, []string{}) var updatedModel tea.Model // Init model with timer values updatedModel, _ = m.Update(m.Init()) m = updatedModel.(model) // Start timer (batch result from above doesn't apply here) updatedModel, _ = m.Update(startTimer()) m = updatedModel.(model) // 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'") oneSec := timeMsg(time.Now().Add(1 * time.Second)) updatedModel, _ = m.Update(oneSec) m = updatedModel.(model) assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'") // Test switch to break time m.startTime = m.startTime.Add(-10 * time.Minute) updatedModel, _ = m.Update(oneSec) m = updatedModel.(model) assertEqual(t, m.state, "Break", "Expected state to be 'Break'") // Switch back to focus time m.startTime = m.startTime.Add(-10 * time.Minute) updatedModel, _ = m.Update(oneSec) m = updatedModel.(model) assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'") t.Logf("Incorrect state %+v", m) }