gralculator/internal/ui/ui_test.go
Grok 17303d1446
Some checks failed
CI / Test (push) Successful in 16s
Release / Create Release (push) Failing after 2m1s
CI / Build (push) Has been skipped
CI / Lint (push) Failing after 26s
feat(calc,ui): complete tight-mode HEX entry with cross-base chaining and result formatting
- Add formatResultEntryForBase + update Equals/Mod/ChangeSign so committed results
  format in the active display base (e.g. HEX shows "C8", "1B0" not decimal).
- Robust per-line centering for dynamic A-F hexRow and keypad rows under display
  (prevents split key borders/decorations on bottom row).
- Updated test expectation for HEX result digits.
- Enables validated cross-base flow: DEC entry, Tab to HEX, continue op, see
  result in active base, Tab back converts displayed value correctly.
- style: gofmt -s all touched sources (ui_test.go, version_test.go etc.).
- ci: add lint and cross targets to Makefile.
  - cross produces the 6 platform binaries expected by release.yml on v* tags.
  - lint target for local parity (CI continues to use golangci-lint-action).

This is the v0.3.0 release-ready state (user-validated: 100 DEC -> Tab HEX +64 = C8 -> Tab DEC 200).
2026-06-06 17:04:11 +01:00

169 lines
4.2 KiB
Go

package ui
import (
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/gmgauthier/gralculator/internal/calc"
)
func TestNewApp(t *testing.T) {
a := NewApp()
if a == nil {
t.Fatal("NewApp returned nil")
}
if a.engine == nil {
t.Error("engine not initialized")
}
if a.engine.CurrentBase() != calc.BaseDEC {
t.Error("default base should be DEC")
}
}
func TestApp_Update_Quit(t *testing.T) {
a := NewApp()
_, cmd := a.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
if cmd == nil {
t.Error("expected quit cmd for ctrl+c")
}
// Simulate the quit msg? But for now, check it returns quit-like
}
func TestApp_Update_DigitsAndDisplay(t *testing.T) {
a := NewApp()
a.width = 80
a.height = 24
// Type 1 2 3
for _, d := range "123" {
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{d}})
}
view := a.View()
if !contains(view, "123") {
t.Errorf("expected 123 in view, got: %s", view)
}
}
func TestApp_Update_Operators(t *testing.T) {
a := NewApp()
a.width = 80
// 2 + 3 =
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'2'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'+'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'3'}})
a.Update(tea.KeyMsg{Type: tea.KeyEnter})
view := a.View()
if !contains(view, "5") {
t.Errorf("expected result 5, got: %s", view)
}
}
func TestApp_Update_Mod(t *testing.T) {
a := NewApp()
a.width = 80
// 23 mod 6
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'2'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'3'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'m'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'6'}})
a.Update(tea.KeyMsg{Type: tea.KeyEnter})
view := a.View()
if !contains(view, "5") {
t.Errorf("expected 23 mod 6 =5, got: %s", view)
}
}
func TestApp_Update_BaseCycleAndCERR(t *testing.T) {
a := NewApp()
a.width = 80
// 1 / 3 = (fraction)
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'1'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'3'}})
a.Update(tea.KeyMsg{Type: tea.KeyEnter})
// Tab should CERR
a.Update(tea.KeyMsg{Type: tea.KeyTab})
view := a.View()
if !contains(view, "CERR") {
t.Errorf("expected CERR flash on fractional base switch, got: %s", view)
}
}
func TestApp_Update_HEXEntry(t *testing.T) {
a := NewApp()
a.width = 80
// Tab to HEX
a.Update(tea.KeyMsg{Type: tea.KeyTab})
// Type 1 A F (hex entry)
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'1'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'f'}})
view := a.View()
if !contains(view, "1AF") {
t.Errorf("expected raw HEX entry 1AF in display, got: %s", view)
}
if !contains(view, "[HEX]") {
t.Error("expected HEX base indicator")
}
}
func TestApp_Update_PressedFlash(t *testing.T) {
a := NewApp()
a.width = 80
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'5'}})
// After key, flash should be set (we can check internal or view for style, but simple state check)
if a.flash == "" && a.pressedKey == "" {
t.Error("expected flash or pressedKey after digit")
}
// Simulate time tick clear
a.Update(clearFlashMsg{which: "key"})
if a.flash != "" || a.pressedKey != "" {
t.Error("flash should clear after msg")
}
}
func TestApp_Update_ClearKeys(t *testing.T) {
a := NewApp()
a.width = 80
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'1'}})
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'2'}})
// Backspace as C (clear entry)
a.Update(tea.KeyMsg{Type: tea.KeyBackspace})
view := a.View()
if !contains(view, "0") || contains(view, "12") {
t.Errorf("backspace/C should clear entry to 0, got: %s", view)
}
a.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'9'}})
// Del as AC
a.Update(tea.KeyMsg{Type: tea.KeyDelete})
if a.engine.CurrentBase() != calc.BaseDEC {
t.Error("Del/AC should reset base")
}
}
func contains(s, substr string) bool {
return strings.Contains(s, substr)
}
// Note: clearFlashMsg is defined in ui.go (same package), so we can send it directly in tests.
// The tick func is also package-private but we test via direct messages where possible.