go_games_collection/chess_tui_test.go

225 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
func TestChessModelSelectAndMoveViaKeys(t *testing.T) {
m := NewChessModel(0)
m.cursor = ChessPos{Row: 6, Col: 4}
next, _ := m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка выбора: %s", m.message)
}
if m.selected == nil {
t.Fatalf("ожидался выбор фигуры")
}
m.cursor = ChessPos{Row: 4, Col: 4}
next, _ = m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка хода: %s", m.message)
}
if m.game.Board[4][4] == nil {
t.Fatalf("пешка должна была переместиться на e4")
}
if m.selected != nil {
t.Errorf("выбор должен был сброситься после хода")
}
}
func TestChessModelCastlingViaKeys(t *testing.T) {
m := NewChessModel(0)
m.game = &ChessGameState{Turn: ChessWhite}
m.game.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing}
m.game.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook}
m.game.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing}
m.cursor = ChessPos{Row: 7, Col: 4}
next, _ := m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка выбора короля: %s", m.message)
}
m.cursor = ChessPos{Row: 7, Col: 6}
next, _ = m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка рокировки: %s", m.message)
}
if m.game.Board[7][6] == nil || m.game.Board[7][6].Type != ChessKing {
t.Errorf("король должен был оказаться на g1")
}
if m.game.Board[7][5] == nil || m.game.Board[7][5].Type != ChessRook {
t.Errorf("ладья должна была оказаться на f1")
}
}
// TestChessModelCursorVisibleOnLegalDestination проверяет исправление
// бага: раньше подсветка "легальный ход" рендерилась поверх курсора,
// делая его невидимым, стоило навести курсор на клетку, куда можно
// сходить (именно эта клетка обычно и нужна пользователю).
// TestChessCursorOnPieceUsesOneCombinedStyle — тот же баг и та же
// проверка, что и в Шашках: цвет фигуры и фон клетки должны
// составлять один комбинированный стиль, а не вложенные Render().
func TestChessCursorOnPieceUsesOneCombinedStyle(t *testing.T) {
oldProfile := lipgloss.ColorProfile()
lipgloss.SetColorProfile(termenv.ANSI256)
defer lipgloss.SetColorProfile(oldProfile)
piece := &ChessPiece{Color: ChessBlack, Type: ChessKnight}
text := chessGlyphChar(piece)
fg, ok := chessPieceForegroundColor(piece)
if !ok {
t.Fatalf("ожидался цвет для непустой клетки")
}
combined := checkersCursorStyle.Foreground(fg).Bold(true)
rendered := combined.Render(" " + text + " ")
resets := strings.Count(rendered, "\x1b[0m")
if resets != 1 {
t.Errorf("ожидался ровно один сброс ANSI на всю клетку (единый стиль), получено %d: %q", resets, rendered)
}
opens := strings.Count(rendered, "\x1b[")
if opens != 2 {
t.Errorf("ожидалось ровно 2 вхождения ESC (открытие+закрытие, без вложенности), получено %d: %q", opens, rendered)
}
}
func TestChessModelCursorVisibleOnLegalDestination(t *testing.T) {
oldProfile := lipgloss.ColorProfile()
lipgloss.SetColorProfile(termenv.ANSI256)
defer lipgloss.SetColorProfile(oldProfile)
m := NewChessModel(0)
m.cursor = ChessPos{Row: 6, Col: 4}
next, _ := m.Update(key("enter")) // выбираем пешку e2
m = next.(ChessModel)
m.cursor = ChessPos{Row: 4, Col: 4} // e4 — легальный ход этой пешки
dests := m.legalDestinationSet()
if !dests[m.cursor] {
t.Fatalf("тест предполагает, что курсор наведён именно на легальный ход")
}
board := m.renderBoard()
cursorTag := checkersCursorStyle.Render("X")
// вытаскиваем именно код фона курсора из отрендеренной "X"
start := strings.Index(cursorTag, "\x1b[")
end := strings.Index(cursorTag, "m")
cursorColorCode := cursorTag[start : end+1]
if !strings.Contains(board, cursorColorCode) {
t.Errorf("цвет курсора должен присутствовать в отрисовке доски даже на клетке легального хода")
}
}
func TestChessModelCursorMovement(t *testing.T) {
m := NewChessModel(0)
m.cursor = ChessPos{Row: 4, Col: 4}
next, _ := m.Update(key("up"))
m = next.(ChessModel)
if m.cursor.Row != 3 {
t.Errorf("курсор должен был сдвинуться вверх")
}
next, _ = m.Update(key("right"))
m = next.(ChessModel)
if m.cursor.Col != 5 {
t.Errorf("курсор должен был сдвинуться вправо")
}
}
func TestChessModelEscClearsSelection(t *testing.T) {
m := NewChessModel(0)
m.cursor = ChessPos{Row: 6, Col: 4}
next, _ := m.Update(key("enter"))
m = next.(ChessModel)
if m.selected == nil {
t.Fatalf("ожидался выбор")
}
next, _ = m.Update(key("esc"))
m = next.(ChessModel)
if m.selected != nil {
t.Errorf("esc должен был сбросить выбор")
}
}
func TestChessModelCannotSelectEmptySquare(t *testing.T) {
m := NewChessModel(0)
m.cursor = ChessPos{Row: 4, Col: 4}
next, _ := m.Update(key("enter"))
m = next.(ChessModel)
if !m.isError {
t.Fatalf("ожидалась ошибка выбора пустой клетки")
}
}
func TestChessModelQReturnsToMenu(t *testing.T) {
m := NewChessModel(0)
next, _ := m.Update(key("q"))
m = next.(ChessModel)
if !m.backToMenu {
t.Fatalf("ожидался флаг backToMenu после 'q'")
}
if m.quitting {
t.Fatalf("'q' не должен закрывать всё приложение")
}
}
func TestChessModelNewGameAfterResult(t *testing.T) {
m := NewChessModel(0)
m.game.Result = &ChessResult{Winner: ChessWhite, Checkmate: true}
next, _ := m.Update(key("n"))
m = next.(ChessModel)
if m.game.Result != nil {
t.Errorf("новая партия не должна была начаться с результатом")
}
}
// TestChessModelFullAutoPlaythrough прогоняет TUI-модель от начала
// до конца, где "человек" отыгрывает свои ходы через бота
// (небольшая глубина — ради скорости), проверяя, что весь путь
// через Update не паникует и партия завершается.
func TestChessModelFullAutoPlaythrough(t *testing.T) {
m := NewChessModel(1)
helperBot := ChessBot{Depth: 1}
const maxSteps = 600
for i := 0; i < maxSteps && m.game.Result == nil; i++ {
if m.game.Turn != m.human {
next, _ := m.Update(botMoveMsg{})
m = next.(ChessModel)
continue
}
mv, ok := helperBot.DecideMove(m.game)
if !ok {
break
}
m.cursor = mv.From
next, _ := m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка выбора хода человеком: %s", m.message)
}
m.cursor = mv.To
next, _ = m.Update(key("enter"))
m = next.(ChessModel)
if m.isError {
t.Fatalf("неожиданная ошибка хода человеком: %s", m.message)
}
}
if m.game.Result == nil {
t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps)
}
}