package main import ( "regexp" "strings" "testing" ) var ansiEscapeRe = regexp.MustCompile("\x1b\\[[0-9;]*m") func stripANSI(s string) string { return ansiEscapeRe.ReplaceAllString(s, "") } // TestKlondikeTableauAlignmentWithMixedCardWidths проверяет // исправление бага: карта "10" (единственный ранг длиной в 2 // символа) без паддинга оказывалась короче остальных карт и // закрытой клетки "[?]", из-за чего колонки правее в той же строке // сдвигались и вертикальное выравнивание таблицы ломалось. func TestKlondikeTableauAlignmentWithMixedCardWidths(t *testing.T) { m := NewKlondikeModel() m.game.Tableau[0] = []KlondikeCard{{Card: Card{Rank: Ace, Suit: Spades}, FaceUp: true}} m.game.Tableau[1] = []KlondikeCard{{Card: Card{Rank: Ten, Suit: Hearts}, FaceUp: true}} m.game.Tableau[2] = []KlondikeCard{{Card: Card{Rank: King, Suit: Clubs}, FaceUp: false}} m.game.Tableau[3] = []KlondikeCard{{Card: Card{Rank: Queen, Suit: Diamonds}, FaceUp: true}} m.game.Tableau[4] = []KlondikeCard{} m.game.Tableau[5] = []KlondikeCard{} m.game.Tableau[6] = []KlondikeCard{} rendered := stripANSI(m.renderTableau()) lines := strings.Split(rendered, "\n") if len(lines) < 2 { t.Fatalf("ожидалось минимум 2 строки (заголовок + ряд карт), получено %d", len(lines)) } headerWidth := len([]rune(lines[0])) for i, line := range lines[1:] { if w := len([]rune(line)); w != headerWidth { t.Errorf("строка %d имеет ширину %d, отличную от заголовка (%d): %q", i+1, w, headerWidth, line) } } } func TestKlondikeModelDrawViaKeys(t *testing.T) { m := NewKlondikeModel() stockBefore := len(m.game.Stock) next, _ := m.Update(key("d")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка: %s", m.message) } if len(m.game.Stock) != stockBefore-1 { t.Fatalf("колода должна была уменьшиться на 1") } } func TestKlondikeModelPartialRunTransferViaKeys(t *testing.T) { m := NewKlondikeModel() m.game.Tableau[0] = []KlondikeCard{ {Card: c(King, Spades), FaceUp: true}, {Card: c(Queen, Diamonds), FaceUp: true}, {Card: c(Jack, Spades), FaceUp: true}, } m.game.Tableau[1] = []KlondikeCard{ {Card: c(King, Clubs), FaceUp: true}, } next, _ := m.Update(key("1")) m = next.(KlondikeModel) if m.runLen != 3 { t.Fatalf("сразу после выбора ожидалась вся последовательность (3), получено %d", m.runLen) } next, _ = m.Update(key("down")) m = next.(KlondikeModel) if m.runLen != 2 { t.Fatalf("после уменьшения ожидалось 2, получено %d", m.runLen) } next, _ = m.Update(key("2")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка: %s", m.message) } if len(m.game.Tableau[0]) != 1 { t.Errorf("в колонне-источнике должна была остаться только одна карта (K♠), получено %d", len(m.game.Tableau[0])) } if len(m.game.Tableau[1]) != 3 { t.Errorf("в колонне назначения ожидалось 3 карты, получено %d", len(m.game.Tableau[1])) } } func TestKlondikeModelRunLenClampedBetweenOneAndMax(t *testing.T) { m := NewKlondikeModel() m.game.Tableau[0] = []KlondikeCard{ {Card: c(King, Spades), FaceUp: true}, {Card: c(Queen, Diamonds), FaceUp: true}, } next, _ := m.Update(key("1")) m = next.(KlondikeModel) for i := 0; i < 5; i++ { next, _ = m.Update(key("up")) m = next.(KlondikeModel) } if m.runLen != 2 { t.Errorf("runLen не должен был превысить полную последовательность (2), получено %d", m.runLen) } for i := 0; i < 5; i++ { next, _ = m.Update(key("down")) m = next.(KlondikeModel) } if m.runLen != 1 { t.Errorf("runLen не должен был опуститься ниже 1, получено %d", m.runLen) } } func TestKlondikeModelSelectAndMoveTableauToTableau(t *testing.T) { m := NewKlondikeModel() m.game.Tableau[0] = []KlondikeCard{{Card: c(Seven, Hearts), FaceUp: true}} m.game.Tableau[1] = []KlondikeCard{{Card: c(Six, Spades), FaceUp: true}} next, _ := m.Update(key("2")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка выбора: %s", m.message) } if m.selected != klondikePileRef(1) { t.Fatalf("ожидался выбор колонны 1 (индекс), получено %v", m.selected) } next, _ = m.Update(key("1")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка хода: %s", m.message) } if len(m.game.Tableau[0]) != 2 { t.Fatalf("ожидалось 2 карты в целевой колонне, получено %d", len(m.game.Tableau[0])) } if m.selected != klondikeNoSelection { t.Errorf("выбор должен был сброситься после хода") } } func TestKlondikeModelSelectWasteAndSendToFoundation(t *testing.T) { m := NewKlondikeModel() m.game.Waste = []Card{c(Ace, Hearts)} next, _ := m.Update(key("0")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка: %s", m.message) } if m.selected != klondikeWasteRef { t.Fatalf("ожидался выбор отбоя") } next, _ = m.Update(key("f")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка отправки на фундамент: %s", m.message) } if m.game.Foundations[Hearts] != int(Ace) { t.Errorf("туз червей должен был оказаться на фундаменте") } } func TestKlondikeModelEscClearsSelection(t *testing.T) { m := NewKlondikeModel() m.game.Tableau[0] = []KlondikeCard{{Card: c(Seven, Hearts), FaceUp: true}} next, _ := m.Update(key("1")) m = next.(KlondikeModel) if m.selected == klondikeNoSelection { t.Fatalf("ожидался выбор колонны 1") } next, _ = m.Update(key("esc")) m = next.(KlondikeModel) if m.selected != klondikeNoSelection { t.Errorf("esc должен был сбросить выбор") } } func TestKlondikeModelUndoViaKeys(t *testing.T) { m := NewKlondikeModel() m.game.Waste = []Card{c(Ace, Hearts)} next, _ := m.Update(key("0")) m = next.(KlondikeModel) next, _ = m.Update(key("f")) m = next.(KlondikeModel) if m.game.Foundations[Hearts] != int(Ace) { t.Fatalf("ход должен был примениться") } next, _ = m.Update(key("u")) m = next.(KlondikeModel) if m.isError { t.Fatalf("неожиданная ошибка отмены: %s", m.message) } if m.game.Foundations[Hearts] != 0 { t.Errorf("после отмены фундамент должен был вернуться в исходное состояние") } } func TestKlondikeModelNewGameViaKeys(t *testing.T) { m := NewKlondikeModel() oldStock := m.game.Stock next, _ := m.Update(key("n")) m = next.(KlondikeModel) if len(m.game.Stock)+28 != 52 { t.Fatalf("новая партия должна быть корректно раздана") } same := true for i := range oldStock { if i >= len(m.game.Stock) || oldStock[i] != m.game.Stock[i] { same = false break } } if same { t.Errorf("новая партия должна была перетасовать колоду заново") } } func TestKlondikeModelQReturnsToMenu(t *testing.T) { m := NewKlondikeModel() next, _ := m.Update(key("q")) m = next.(KlondikeModel) if !m.backToMenu { t.Fatalf("ожидался флаг backToMenu после 'q'") } if m.quitting { t.Fatalf("'q' не должен закрывать всё приложение") } } func TestKlondikeModelWinShowsResultAndAllowsNewGame(t *testing.T) { m := NewKlondikeModel() for s := Clubs; s <= Spades; s++ { m.game.Foundations[s] = int(King) } m.game.Foundations[Hearts] = int(Queen) m.game.Waste = []Card{c(King, Hearts)} next, _ := m.Update(key("0")) m = next.(KlondikeModel) next, _ = m.Update(key("f")) m = next.(KlondikeModel) if !m.game.Won { t.Fatalf("партия должна была завершиться победой") } next, _ = m.Update(key("n")) m = next.(KlondikeModel) if m.game.Won { t.Errorf("новая партия не должна была начаться выигранной") } }