225 lines
7.7 KiB
Go
225 lines
7.7 KiB
Go
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)
|
||
}
|
||
}
|