Files
scrawl/game/model_test.go
Fredrik Johansson 039e131304
All checks were successful
Docker / build-and-push (push) Successful in 50s
Rebind char cycling to ,/. instead of [/]
[ and ] needed a reach/shift on most layouts and felt awkward next to
the otherwise all-unshifted control scheme (hjkl, space, digits). ,
and . are adjacent, unshifted, easy to hit without looking away from
the canvas -- a natural prev/next pair. Old bindings kept as a fallback
rather than removed.

Verified both in isolation (Update() with a KeyMsg) and live over a
real SSH session -- the status-line ink swatch visibly cycles from █
to ▓ on a fresh connection. An initial live test after the rebind
appeared to fail, but that traced to a stale session left over from
the server restart, not an actual bug -- confirmed working correctly
on a fresh connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:55:59 +02:00

145 lines
4.5 KiB
Go

package game
import (
"io"
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
func TestColorAndCharSwitching(t *testing.T) {
c := NewCanvas()
m := NewModel(c, "test-id", "tester", newTestRenderer())
c.Join("test-id", "tester", nil)
// First keypress just exits the intro screen, per Update()'s own logic.
mi, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("3")})
m = mi.(Model)
if m.state != stateCanvas {
t.Fatalf("expected first keypress to exit intro, state=%v", m.state)
}
if m.inkColorIdx != 0 {
t.Fatalf("expected inkColorIdx unchanged by the intro-exit keypress, got %d", m.inkColorIdx)
}
// Now actually press "3" for color selection.
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("3")})
m = mi.(Model)
if m.inkColorIdx != 2 {
t.Fatalf("expected inkColorIdx=2 after pressing '3', got %d", m.inkColorIdx)
}
// Paint with the selected color.
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace})
m = mi.(Model)
cells, cursors := c.Snapshot()
cur := cursors["test-id"]
painted := cells[cur.Y][cur.X]
if painted.Char != charPalette[0] {
t.Fatalf("expected painted char to be default charPalette[0]=%q, got %q", charPalette[0], painted.Char)
}
if painted.Color != inkPalette[2] {
t.Fatalf("expected painted color to be inkPalette[2]=%q (selected via '3'), got %q", inkPalette[2], painted.Color)
}
}
func TestCharCycling(t *testing.T) {
c := NewCanvas()
m := NewModel(c, "test-id", "tester", newTestRenderer())
c.Join("test-id", "tester", nil)
mi, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) // exit intro
m = mi.(Model)
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("]")})
m = mi.(Model)
if m.charIdx != 1 {
t.Fatalf("expected charIdx=1 after ']', got %d", m.charIdx)
}
}
func TestViewChangesAfterMoveAwayFromPaint(t *testing.T) {
c := NewCanvas()
m := NewModel(c, "test-id", "tester", newTestRenderer())
c.Join("test-id", "tester", nil)
mi, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) // exit intro
m = mi.(Model)
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace}) // paint
m = mi.(Model)
viewAtPaintPosition := m.View()
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")}) // move right
m = mi.(Model)
viewAfterMove := m.View()
if viewAtPaintPosition == viewAfterMove {
t.Fatal("expected View() output to differ after moving away from a painted cell, but it's identical")
}
t.Logf("view at paint position:\n%s", viewAtPaintPosition)
t.Logf("view after move:\n%s", viewAfterMove)
}
func TestMoveCursorActuallyMoves(t *testing.T) {
c := NewCanvas()
m := NewModel(c, "test-id", "tester", newTestRenderer())
c.Join("test-id", "tester", nil)
before := c.CursorsSnapshot()["test-id"]
t.Logf("before: x=%d y=%d", before.X, before.Y)
mi, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) // exit intro
m = mi.(Model)
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("l")}) // move right
m = mi.(Model)
after := c.CursorsSnapshot()["test-id"]
t.Logf("after: x=%d y=%d", after.X, after.Y)
if after.X != before.X+1 {
t.Fatalf("expected cursor X to move from %d to %d, got %d", before.X, before.X+1, after.X)
}
}
// newTestRenderer forces TrueColor rather than letting lipgloss
// auto-detect from the writer (io.Discard, like any non-*os.File writer,
// would otherwise be treated as non-color-capable) -- matches main.go's
// real per-session renderer setup, so these tests actually exercise the
// same styling path a real SSH session does instead of silently testing
// an unstyled/plain-text code path that would mask exactly this class
// of bug.
func newTestRenderer() *lipgloss.Renderer {
r := lipgloss.NewRenderer(io.Discard)
r.SetColorProfile(termenv.TrueColor)
return r
}
func TestCommaPeriodCharCycling(t *testing.T) {
c := NewCanvas()
m := NewModel(c, "test-id", "tester", newTestRenderer())
c.Join("test-id", "tester", nil)
mi, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("x")}) // exit intro
m = mi.(Model)
t.Logf("charIdx before: %d", m.charIdx)
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(".")})
m = mi.(Model)
t.Logf("charIdx after '.': %d", m.charIdx)
if m.charIdx != 1 {
t.Fatalf("expected charIdx=1 after '.', got %d", m.charIdx)
}
mi, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(",")})
m = mi.(Model)
t.Logf("charIdx after ',': %d", m.charIdx)
if m.charIdx != 0 {
t.Fatalf("expected charIdx=0 after ',', got %d", m.charIdx)
}
}