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 }