go_games_collection/menu_test.go

1589 lines
53 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"
tea "github.com/charmbracelet/bubbletea"
)
// newTestAppModel создаёт AppModel и сразу подтверждает язык по
// умолчанию (первый пункт — русский), чтобы остальные тесты меню
// могли считать, что стартуют сразу в stateMenu, как и раньше.
// TestQReturnsToMenuForAllGames проверяет, что после запуска КАЖДОЙ
// игры коллекции нажатие 'q' возвращает в главное меню. Существует
// специально из-за реальной регрессии: переключатель типов,
// проверяющий backToMenu после m.activeGame.Update(...), несколько
// раз "забывал" недавно добавленные игры (крестики-нолики, нарды,
// Сенет, Ур) — сама модель корректно выставляла backToMenu=true, но
// AppModel никогда этого не замечал, так что 'q' в этих играх молча
// ничего не делал.
func TestQReturnsToMenuForAllGames(t *testing.T) {
games := []struct {
name string
build func() tea.Model
}{
{"tonk", func() tea.Model { return NewModel() }},
{"durak", func() tea.Model { return NewDurakModel(2) }},
{"blackjack", func() tea.Model { return NewBlackjackModel(1, 500, 10) }},
{"101", func() tea.Model { return NewOneOhOneModel(2) }},
{"1000", func() tea.Model { return NewThousandModel() }},
{"klondike", func() tea.Model { return NewKlondikeModel() }},
{"checkers", func() tea.Model { return NewCheckersModel(CheckersDifficultyEasy) }},
{"chess", func() tea.Model { return NewChessModel(CheckersDifficultyEasy) }},
{"corpsestarch", func() tea.Model { return NewCorpseStarchModel() }},
{"go", func() tea.Model { return NewGoModel(9, GoDifficultyEasy) }},
{"minesweeper", func() tea.Model { return NewMinesweeperModel(9, 9, 10) }},
{"wayfarer", func() tea.Model { return NewWayfarerModel() }},
{"tictactoe", func() tea.Model { return NewTicTacToeModel(TicTacToeDifficultyEasy) }},
{"nardy", func() tea.Model { return NewNardyModel(NardyShort, NardyDifficultyEasy) }},
{"senet", func() tea.Model { return NewSenetModel(SenetDifficultyEasy) }},
{"ur", func() tea.Model { return NewUrModel(UrDifficultyEasy) }},
}
for _, g := range games {
t.Run(g.name, func(t *testing.T) {
m := newTestAppModel()
m.state = statePlaying
m.activeGame = g.build()
next, _ := m.Update(key("q"))
m2 := next.(AppModel)
if m2.state != stateMenu {
t.Errorf("%s: 'q' должен был вернуть в главное меню, получено state=%v", g.name, m2.state)
}
})
}
}
func newTestAppModel() AppModel {
m := NewAppModel()
next, _ := m.Update(key("enter"))
return next.(AppModel)
}
func TestLanguageScreenIsInitialState(t *testing.T) {
SetLanguage(LangRU)
m := NewAppModel()
if m.state != stateLanguage {
t.Fatalf("ожидалось начальное состояние stateLanguage, получено %v", m.state)
}
}
func TestLanguageScreenSelectEnglish(t *testing.T) {
defer SetLanguage(LangRU) // не влияем на остальные тесты пакета
m := NewAppModel()
if m.langCursor != 0 {
t.Fatalf("ожидался курсор на первом пункте (русский), получено %d", m.langCursor)
}
next, _ := m.Update(key("down"))
m = next.(AppModel)
if m.langCursor != 1 {
t.Fatalf("ожидался курсор на английском после down, получено %d", m.langCursor)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("ожидался переход в stateMenu после выбора языка")
}
if CurrentLang() != LangEN {
t.Fatalf("ожидался установленный язык LangEN")
}
if T("menu.quit") != "Quit" {
t.Errorf("после выбора английского T(\"menu.quit\") должен вернуть 'Quit', получено %q", T("menu.quit"))
}
}
func TestLanguageScreenDefaultsToRussian(t *testing.T) {
defer SetLanguage(LangRU)
m := NewAppModel()
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("ожидался переход в stateMenu")
}
if CurrentLang() != LangRU {
t.Fatalf("ожидался язык по умолчанию LangRU")
}
}
func TestMenuHasLanguageOption(t *testing.T) {
m := newTestAppModel()
found := false
for _, opt := range m.options {
if opt.kind == "language" {
found = true
}
}
if !found {
t.Fatalf("ожидался пункт меню для смены языка")
}
}
func TestMenuScrollFollowsCursorDown(t *testing.T) {
m := newTestAppModel()
m.termHeight = 20 // тесный терминал — гарантированно меньше пунктов, чем видно целиком
visible := m.menuVisibleItems()
if visible >= len(m.options) {
t.Fatalf("тест предполагает, что не все пункты помещаются сразу")
}
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
t.Fatalf("курсор %d выпал из видимого окна [%d,%d) после шага %d",
m.cursor, m.menuScroll, m.menuScroll+visible, i)
}
}
}
func TestMenuScrollFollowsCursorUp(t *testing.T) {
m := newTestAppModel()
m.termHeight = 20
visible := m.menuVisibleItems()
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("up"))
m = next.(AppModel)
if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
t.Fatalf("курсор %d выпал из видимого окна [%d,%d) при прокрутке вверх",
m.cursor, m.menuScroll, m.menuScroll+visible)
}
}
if m.menuScroll != 0 {
t.Errorf("после возврата к самому первому пункту прокрутка должна была вернуться к 0, получено %d", m.menuScroll)
}
}
func TestMenuScrollIndicatorsAppearWhenScrolled(t *testing.T) {
m := newTestAppModel()
m.termHeight = 20
view := m.viewMenu()
if strings.Contains(view, "▲") {
t.Errorf("в самом начале списка индикатор 'выше' не должен показываться")
}
if !strings.Contains(view, "▼") {
t.Errorf("при неполном списке индикатор 'ниже' должен показываться")
}
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
view = m.viewMenu()
if !strings.Contains(view, "▲") {
t.Errorf("в конце списка индикатор 'выше' должен показываться")
}
if strings.Contains(view, "▼") {
t.Errorf("в конце списка индикатор 'ниже' не должен показываться")
}
}
func TestMenuCompactBannerOnSmallTerminal(t *testing.T) {
m := newTestAppModel()
m.termHeight = 20
if !m.menuUsesCompactBanner() {
t.Errorf("на тесном терминале должен использоваться компактный баннер")
}
view := m.viewMenu()
if !strings.Contains(view, "GO GAMES COLLECTION") {
t.Errorf("компактный заголовок должен присутствовать в рендере")
}
}
func TestMenuFullBannerOnLargeTerminal(t *testing.T) {
m := newTestAppModel()
m.termHeight = 60
if m.menuUsesCompactBanner() {
t.Errorf("на просторном терминале должен использоваться полный баннер")
}
if m.menuVisibleItems() < len(m.options) {
t.Errorf("на просторном терминале весь список должен помещаться целиком")
}
}
func TestRulesMenuScrollFollowsCursor(t *testing.T) {
m := newTestAppModel()
m.termHeight = 12
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
if m.state != stateRulesMenu {
t.Fatalf("ожидалось состояние stateRulesMenu")
}
visible := m.rulesMenuVisibleItems()
if visible >= len(gameRegistry) {
t.Fatalf("тест предполагает, что не все игры помещаются сразу при высоте 12")
}
for i := 0; i < len(gameRegistry)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
if m.rulesCursor < m.rulesMenuScroll || m.rulesCursor >= m.rulesMenuScroll+visible {
t.Fatalf("курсор правил %d выпал из видимого окна [%d,%d)",
m.rulesCursor, m.rulesMenuScroll, m.rulesMenuScroll+visible)
}
}
}
func TestMenuScrollRecalculatedOnResize(t *testing.T) {
m := newTestAppModel()
m.termHeight = 50 // просторный терминал — весь список виден, курсор можно увести глубоко
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
if m.cursor != len(m.options)-1 {
t.Fatalf("ожидался курсор на последнем пункте")
}
// пользователь на лету уменьшает терминал — курсор должен остаться видимым
next, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 15})
m = next.(AppModel)
visible := m.menuVisibleItems()
if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
t.Errorf("после уменьшения терминала курсор %d должен был остаться в видимом окне [%d,%d)",
m.cursor, m.menuScroll, m.menuScroll+visible)
}
}
func TestMenuNavigation(t *testing.T) {
m := newTestAppModel()
if m.state != stateMenu {
t.Fatalf("ожидалось начальное состояние stateMenu")
}
if len(m.options) != len(gameRegistry)+3 {
t.Fatalf("ожидалось %d пунктов меню, получено %d", len(gameRegistry)+3, len(m.options))
}
next, _ := m.Update(key("down"))
nm := next.(AppModel)
if nm.cursor != 1 {
t.Fatalf("ожидался курсор на позиции 1, получено %d", nm.cursor)
}
next, _ = nm.Update(key("up"))
nm = next.(AppModel)
if nm.cursor != 0 {
t.Fatalf("ожидался курсор на позиции 0 после возврата, получено %d", nm.cursor)
}
}
func TestMenuLeadsToStakeSettings(t *testing.T) {
m := newTestAppModel()
// первый пункт меню — первая (и пока единственная) игра, которая
// использует ставки -> должен открыться экран настроек, а не игра
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateSettings {
t.Fatalf("ожидалось состояние stateSettings после выбора игры со ставками")
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения настроек")
}
}
func TestStakeSettingsAdjustAndStart(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("enter")) // входим в настройки
m = next.(AppModel)
initialCapital := m.settings.capital()
next, _ = m.Update(key("right")) // увеличиваем капитал на шаг пресета
m = next.(AppModel)
if m.settings.capital() == initialCapital {
t.Fatalf("значение капитала должно было измениться после [right]")
}
next, _ = m.Update(key("down")) // переходим к полю анте
m = next.(AppModel)
if m.settings.fieldCursor != 1 {
t.Fatalf("ожидался курсор на поле анте (1), получено %d", m.settings.fieldCursor)
}
chosenCapital := m.settings.capital()
chosenAnte := m.settings.ante()
chosenMult := m.settings.multiplier()
next, _ = m.Update(key("enter")) // подтверждаем и запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось состояние statePlaying после подтверждения настроек")
}
tm, ok := m.activeGame.(Model)
if !ok {
t.Fatalf("активная игра должна быть моделью Тонка")
}
if tm.game.StartingCapital != chosenCapital {
t.Errorf("стартовый капитал в игре (%d) не совпадает с выбранным (%d)",
tm.game.StartingCapital, chosenCapital)
}
if tm.game.AnteAmount != chosenAnte {
t.Errorf("анте в игре (%d) не совпадает с выбранным (%d)", tm.game.AnteAmount, chosenAnte)
}
if tm.game.TonkMultiplier != chosenMult {
t.Errorf("множитель в игре (%d) не совпадает с выбранным (%d)", tm.game.TonkMultiplier, chosenMult)
}
}
func TestStakeSettingsEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateSettings {
t.Fatalf("ожидался экран настроек")
}
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране настроек должен вернуть в меню")
}
}
func TestStakeSettingsCycleClampsAtBounds(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("enter"))
m = next.(AppModel)
// уводим значение влево много раз — не должно уйти за границу
// списка пресетов (индекс не может стать отрицательным)
for i := 0; i < 10; i++ {
next, _ = m.Update(key("left"))
m = next.(AppModel)
}
if m.settings.capitalIdx != 0 {
t.Errorf("индекс капитала должен был остановиться на 0, получено %d", m.settings.capitalIdx)
}
}
func TestMenuShowsRules(t *testing.T) {
m := newTestAppModel()
// переходим к пункту "Правила" (сразу после списка игр)
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRulesMenu {
t.Fatalf("ожидалось состояние stateRulesMenu (подменю выбора игры)")
}
// в подменю правил курсор по умолчанию на первой игре (Тонк)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ТОНК") {
t.Fatalf("текст правил должен содержать описание Тонка")
}
// esc из текста правил возвращает в подменю выбора игры, а не сразу в меню
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateRulesMenu {
t.Fatalf("ожидалось возвращение в stateRulesMenu после esc из текста правил")
}
// esc из подменю возвращает в главное меню
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("ожидалось возвращение в stateMenu после esc из подменю правил")
}
}
func TestMenuShowsDurakRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
next, _ = m.Update(key("down")) // выбираем вторую игру (Дурак)
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ДУРАК") {
t.Fatalf("текст правил должен содержать описание Дурака, получено начало: %.50s", m.rulesText)
}
}
func TestMenuLeadsToPlayerCountForDurak(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down")) // второй пункт меню — Дурак
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlayerCount {
t.Fatalf("ожидалось состояние statePlayerCount после выбора Дурака")
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения числа игроков")
}
}
func TestPlayerCountAdjustAndStart(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter")) // экран выбора числа игроков
m = next.(AppModel)
if m.playerCount.count != 4 {
t.Fatalf("ожидалось значение по умолчанию 4, получено %d", m.playerCount.count)
}
next, _ = m.Update(key("right"))
m = next.(AppModel)
if m.playerCount.count != 5 {
t.Fatalf("ожидалось 5 после [right], получено %d", m.playerCount.count)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения числа игроков")
}
dm, ok := m.activeGame.(DurakModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Дурака")
}
if len(dm.game.Players) != 5 {
t.Errorf("ожидалось 5 игроков в запущенной партии, получено %d", len(dm.game.Players))
}
}
func TestPlayerCountClampsAtBounds(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
for i := 0; i < 10; i++ {
next, _ = m.Update(key("left"))
m = next.(AppModel)
}
if m.playerCount.count != 2 {
t.Errorf("число игроков должно было остановиться на 2, получено %d", m.playerCount.count)
}
for i := 0; i < 10; i++ {
next, _ = m.Update(key("right"))
m = next.(AppModel)
}
if m.playerCount.count != 6 {
t.Errorf("число игроков должно было остановиться на 6, получено %d", m.playerCount.count)
}
}
func TestPlayerCountEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране выбора числа игроков должен вернуть в меню")
}
}
func TestDurakQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter")) // экран числа игроков
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем Дурака
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsBlackjackRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Блэкджек — третья запись в реестре (индекс 2)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "БЛЭКДЖЕК") {
t.Fatalf("текст правил должен содержать описание Блэкджека, получено начало: %.50s", m.rulesText)
}
}
func TestMenuLeadsToBlackjackSettings(t *testing.T) {
m := newTestAppModel()
// Блэкджек — третий пункт меню (после Тонка и Дурака)
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateBlackjackSettings {
t.Fatalf("ожидалось состояние stateBlackjackSettings после выбора Блэкджека")
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения настроек")
}
}
func TestBlackjackSettingsAdjustAndStart(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter")) // экран настроек
m = next.(AppModel)
if m.blackjack.playerCount != 1 {
t.Fatalf("ожидалось значение по умолчанию 1 игрок, получено %d", m.blackjack.playerCount)
}
next, _ = m.Update(key("right"))
m = next.(AppModel)
if m.blackjack.playerCount != 2 {
t.Fatalf("ожидалось 2 после [right], получено %d", m.blackjack.playerCount)
}
next, _ = m.Update(key("down")) // поле капитала
m = next.(AppModel)
if m.blackjack.fieldCursor != 1 {
t.Fatalf("ожидался курсор на поле капитала (1), получено %d", m.blackjack.fieldCursor)
}
chosenPlayers := m.blackjack.playerCount
chosenCapital := m.blackjack.capital()
chosenBet := m.blackjack.bet()
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения настроек")
}
bm, ok := m.activeGame.(BlackjackModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Блэкджека")
}
if len(bm.game.Players) != chosenPlayers {
t.Errorf("число игроков в игре (%d) не совпадает с выбранным (%d)", len(bm.game.Players), chosenPlayers)
}
if bm.game.StartingCapital != chosenCapital {
t.Errorf("капитал в игре (%d) не совпадает с выбранным (%d)", bm.game.StartingCapital, chosenCapital)
}
if bm.game.Bet != chosenBet {
t.Errorf("ставка в игре (%d) не совпадает с выбранной (%d)", bm.game.Bet, chosenBet)
}
}
func TestBlackjackSettingsEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране настроек Блэкджека должен вернуть в меню")
}
}
func TestBlackjackQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("down"))
m = next.(AppModel)
next, _ = m.Update(key("enter")) // настройки
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsOneOhOneRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// 101 — четвёртая запись в реестре (индекс 3)
for i := 0; i < 3; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "101") {
t.Fatalf("текст правил должен содержать описание 101, получено начало: %.50s", m.rulesText)
}
}
func TestMenuLeadsToPlayerCountForOneOhOne(t *testing.T) {
m := newTestAppModel()
// 101 — четвёртый пункт меню
for i := 0; i < 3; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlayerCount {
t.Fatalf("ожидалось состояние statePlayerCount после выбора 101")
}
}
func TestOneOhOneQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 3; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран числа игроков
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsThousandRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// 1000 — пятая запись в реестре (индекс 4)
for i := 0; i < 4; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ТЫСЯЧА") {
t.Fatalf("текст правил должен содержать описание Тысячи, получено начало: %.50s", m.rulesText)
}
}
func TestThousandStartsDirectlyWithoutSettingsScreen(t *testing.T) {
m := newTestAppModel()
// 1000 — пятый пункт меню, запускается сразу (без экрана настроек)
for i := 0; i < 4; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось немедленное statePlaying для 1000, получено %v", m.state)
}
if _, ok := m.activeGame.(ThousandModel); !ok {
t.Fatalf("активная игра должна быть моделью 1000")
}
}
func TestThousandQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 4; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsKlondikeRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Косынка — шестая запись в реестре (индекс 5)
for i := 0; i < 5; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "КОСЫНКА") {
t.Fatalf("текст правил должен содержать описание Косынки, получено начало: %.50s", m.rulesText)
}
}
func TestKlondikeStartsDirectlyWithoutSettingsScreen(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 5; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось немедленное statePlaying для Косынки, получено %v", m.state)
}
if _, ok := m.activeGame.(KlondikeModel); !ok {
t.Fatalf("активная игра должна быть моделью Косынки")
}
}
func TestKlondikeQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 5; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsCheckersRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Шашки — седьмая запись в реестре (индекс 6)
for i := 0; i < 6; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ШАШКИ") {
t.Fatalf("текст правил должен содержать описание Шашек, получено начало: %.50s", m.rulesText)
}
}
func TestCheckersLeadsToDifficultySettings(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 6; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateCheckersSettings {
t.Fatalf("ожидалось состояние stateCheckersSettings для Шашек, получено %v", m.state)
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения уровня сложности")
}
}
func TestCheckersDifficultySelectionAndStart(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 6; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран выбора сложности
m = next.(AppModel)
if m.checkers.levelIdx != 0 {
t.Fatalf("ожидался уровень по умолчанию 0 (Простой), получено %d", m.checkers.levelIdx)
}
next, _ = m.Update(key("down")) // переключаемся на "Сильный"
m = next.(AppModel)
if m.checkers.levelIdx != 1 {
t.Fatalf("ожидался уровень 1 (Сильный) после переключения, получено %d", m.checkers.levelIdx)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения уровня")
}
cm, ok := m.activeGame.(CheckersModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Шашек")
}
if cm.bot.Depth != CheckersDifficultyHard {
t.Errorf("ожидалась глубина бота %d (Сильный), получено %d", CheckersDifficultyHard, cm.bot.Depth)
}
}
func TestCheckersDifficultyEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 6; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране сложности должен вернуть в меню")
}
}
func TestCheckersQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 6; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран сложности
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsCorpseStarchRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Трупные батончики — девятая запись в реестре (индекс 8)
for i := 0; i < 8; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ТРУПНЫЕ БАТОНЧИКИ") {
t.Fatalf("текст правил должен содержать описание игры, получено начало: %.50s", m.rulesText)
}
}
func TestCorpseStarchStartsDirectlyWithoutSettingsScreen(t *testing.T) {
withTempConfigDir(t)
m := newTestAppModel()
for i := 0; i < 8; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось немедленное statePlaying, получено %v", m.state)
}
if _, ok := m.activeGame.(CorpseStarchModel); !ok {
t.Fatalf("активная игра должна быть моделью Трупных батончиков")
}
}
func TestCorpseStarchQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
withTempConfigDir(t)
m := newTestAppModel()
for i := 0; i < 8; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsWayfarerRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Странник — двенадцатая запись в реестре (индекс 11)
for i := 0; i < 11; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "СТРАННИК") {
t.Fatalf("текст правил должен содержать описание Странника, получено начало: %.50s", m.rulesText)
}
}
func TestWayfarerStartsDirectlyWithoutSettingsScreen(t *testing.T) {
withTempConfigDir(t)
m := newTestAppModel()
for i := 0; i < 11; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось немедленное statePlaying, получено %v", m.state)
}
if _, ok := m.activeGame.(WayfarerModel); !ok {
t.Fatalf("активная игра должна быть моделью Странника")
}
}
func TestWayfarerQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
withTempConfigDir(t)
m := newTestAppModel()
for i := 0; i < 11; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsMinesweeperRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Сапёр — одиннадцатая запись в реестре (индекс 10)
for i := 0; i < 10; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "САПЁР") {
t.Fatalf("текст правил должен содержать описание Сапёра, получено начало: %.50s", m.rulesText)
}
}
func TestMinesweeperLeadsToSettings(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 10; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateMinesweeperSettings {
t.Fatalf("ожидалось состояние stateMinesweeperSettings, получено %v", m.state)
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения пресета")
}
}
func TestMinesweeperSettingsSelectionAndStart(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 10; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран настроек
m = next.(AppModel)
if m.minesweeper.presetIdx != 0 {
t.Fatalf("ожидался пресет по умолчанию 0 (Новичок), получено %d", m.minesweeper.presetIdx)
}
next, _ = m.Update(key("down")) // Новичок -> Любитель
m = next.(AppModel)
if m.minesweeper.presetIdx != 1 {
t.Fatalf("ожидался пресет 1 (Любитель) после переключения, получено %d", m.minesweeper.presetIdx)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения пресета")
}
mm, ok := m.activeGame.(MinesweeperModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Сапёра")
}
if mm.game.Width != 16 || mm.game.Height != 16 || mm.game.MineCount != 40 {
t.Errorf("ожидался пресет 'Любитель' (16x16, 40 мин) в запущенной игре, получено %dx%d/%d",
mm.game.Width, mm.game.Height, mm.game.MineCount)
}
}
func TestMinesweeperSettingsEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 10; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране настроек должен вернуть в меню")
}
}
func TestMinesweeperQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 10; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран настроек
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsGoRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Го — десятая запись в реестре (индекс 9)
for i := 0; i < 9; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ГО (GO)") {
t.Fatalf("текст правил должен содержать описание Го, получено начало: %.50s", m.rulesText)
}
}
func TestGoLeadsToSettings(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 9; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateGoSettings {
t.Fatalf("ожидалось состояние stateGoSettings для Го, получено %v", m.state)
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения настроек")
}
}
func TestGoSettingsSelectionAndStart(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 9; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран настроек
m = next.(AppModel)
if m.goSetup.size() != 9 {
t.Fatalf("ожидался размер доски по умолчанию 9, получено %d", m.goSetup.size())
}
next, _ = m.Update(key("right")) // 9 -> 13
m = next.(AppModel)
if m.goSetup.size() != 13 {
t.Fatalf("ожидался размер доски 13 после переключения, получено %d", m.goSetup.size())
}
next, _ = m.Update(key("down")) // переходим к полю сложности
m = next.(AppModel)
if m.goSetup.fieldCursor != 1 {
t.Fatalf("ожидался курсор на поле сложности (1), получено %d", m.goSetup.fieldCursor)
}
next, _ = m.Update(key("right")) // Простой -> Средний
m = next.(AppModel)
if m.goSetup.difficulty() != GoDifficultyMedium {
t.Fatalf("ожидалась сложность Средний после переключения")
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения настроек")
}
gm, ok := m.activeGame.(GoModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Го")
}
if gm.game.Size != 13 {
t.Errorf("ожидался размер доски 13 в запущенной игре, получено %d", gm.game.Size)
}
if gm.bot.Difficulty != GoDifficultyMedium {
t.Errorf("ожидалась сложность Средний в запущенной игре")
}
}
func TestGoSettingsEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 9; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране настроек должен вернуть в меню")
}
}
func TestGoQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 9; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран настроек
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestMenuShowsChessRules(t *testing.T) {
m := newTestAppModel()
for i := 0; i < len(gameRegistry); i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // подменю правил
m = next.(AppModel)
// Шахматы — восьмая запись в реестре (индекс 7)
for i := 0; i < 7; i++ {
next, _ = m.Update(key("down"))
m = next.(AppModel)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
if !strings.Contains(m.rulesText, "ШАХМАТЫ") {
t.Fatalf("текст правил должен содержать описание Шахмат, получено начало: %.50s", m.rulesText)
}
}
func TestChessLeadsToDifficultySettings(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 7; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
if m.state != stateChessSettings {
t.Fatalf("ожидалось состояние stateChessSettings для Шахмат, получено %v", m.state)
}
if m.activeGame != nil {
t.Fatalf("игра не должна была запуститься до подтверждения уровня сложности")
}
}
func TestChessDifficultySelectionAndStart(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 7; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран выбора сложности
m = next.(AppModel)
if m.chess.levelIdx != 0 {
t.Fatalf("ожидался уровень по умолчанию 0 (Простой), получено %d", m.chess.levelIdx)
}
next, _ = m.Update(key("down")) // переключаемся на "Сильный"
m = next.(AppModel)
if m.chess.levelIdx != 1 {
t.Fatalf("ожидался уровень 1 (Сильный) после переключения, получено %d", m.chess.levelIdx)
}
next, _ = m.Update(key("enter"))
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying после подтверждения уровня")
}
cm, ok := m.activeGame.(ChessModel)
if !ok {
t.Fatalf("активная игра должна быть моделью Шахмат")
}
if cm.bot.Depth != ChessDifficultyHard {
t.Errorf("ожидалась глубина бота %d (Сильный), получено %d", ChessDifficultyHard, cm.bot.Depth)
}
}
func TestChessDifficultyEscReturnsToMenu(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 7; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter"))
m = next.(AppModel)
next, _ = m.Update(key("esc"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("esc на экране сложности должен вернуть в меню")
}
}
func TestChessQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
for i := 0; i < 7; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, _ := m.Update(key("enter")) // экран сложности
m = next.(AppModel)
next, _ = m.Update(key("enter")) // запускаем игру
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, _ = m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться")
}
}
func TestRulesScrollDownAndUp(t *testing.T) {
m := newTestAppModel()
m.termHeight = 10 // маленький экран, чтобы правила точно не поместились
m.rulesText = strings.Repeat("строка\n", 50)
m.state = stateRules
m.rulesScroll = 0
next, _ := m.Update(key("down"))
m = next.(AppModel)
if m.rulesScroll != 1 {
t.Fatalf("ожидалась прокрутка на 1 строку вниз, получено %d", m.rulesScroll)
}
next, _ = m.Update(key("up"))
m = next.(AppModel)
if m.rulesScroll != 0 {
t.Fatalf("ожидался возврат к строке 0, получено %d", m.rulesScroll)
}
next, _ = m.Update(key("up"))
m = next.(AppModel)
if m.rulesScroll != 0 {
t.Errorf("прокрутка не должна была уйти в отрицательные значения, получено %d", m.rulesScroll)
}
}
func TestRulesScrollClampsAtBottom(t *testing.T) {
m := newTestAppModel()
m.termHeight = 10
m.rulesText = strings.Repeat("строка\n", 20)
m.state = stateRules
for i := 0; i < 100; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
maxScroll := m.rulesMaxScroll()
if m.rulesScroll != maxScroll {
t.Errorf("прокрутка должна была остановиться на максимуме %d, получено %d", maxScroll, m.rulesScroll)
}
}
func TestRulesPageDownMovesByVisibleLines(t *testing.T) {
m := newTestAppModel()
m.termHeight = 14
m.rulesText = strings.Repeat("строка\n", 100)
m.state = stateRules
next, _ := m.Update(key("pgdown"))
m = next.(AppModel)
if m.rulesScroll != m.rulesVisibleLines() {
t.Errorf("ожидалась прокрутка на %d строк за страницу, получено %d", m.rulesVisibleLines(), m.rulesScroll)
}
}
func TestRulesHomeAndEndKeys(t *testing.T) {
m := newTestAppModel()
m.termHeight = 10
m.rulesText = strings.Repeat("строка\n", 50)
m.state = stateRules
next, _ := m.Update(key("G"))
m = next.(AppModel)
if m.rulesScroll != m.rulesMaxScroll() {
t.Fatalf("'G' должна была прокрутить в самый конец")
}
next, _ = m.Update(key("g"))
m = next.(AppModel)
if m.rulesScroll != 0 {
t.Errorf("'g' должна была прокрутить в самое начало")
}
}
func TestRulesScrollResetsWhenReopened(t *testing.T) {
m := newTestAppModel()
m.termHeight = 10
for i := 0; i < len(gameRegistry); i++ {
n, _ := m.Update(key("down"))
m = n.(AppModel)
}
n, _ := m.Update(key("enter")) // подменю правил
m = n.(AppModel)
n, _ = m.Update(key("enter")) // открываем правила первой игры
m = n.(AppModel)
if m.state != stateRules {
t.Fatalf("ожидалось состояние stateRules")
}
n, _ = m.Update(key("down"))
m = n.(AppModel)
if m.rulesScroll == 0 {
t.Fatalf("ожидалась прокрутка вниз перед проверкой сброса")
}
n, _ = m.Update(key("esc")) // назад в подменю правил
m = n.(AppModel)
n, _ = m.Update(key("enter")) // снова открываем те же правила
m = n.(AppModel)
if m.rulesScroll != 0 {
t.Errorf("прокрутка должна была сброситься при повторном открытии правил, получено %d", m.rulesScroll)
}
}
func TestWindowSizeMsgUpdatesDimensions(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
m = next.(AppModel)
if m.termWidth != 100 || m.termHeight != 40 {
t.Errorf("ожидались обновлённые размеры терминала 100x40, получено %dx%d", m.termWidth, m.termHeight)
}
}
func TestMenuQuit(t *testing.T) {
m := newTestAppModel()
// последний пункт меню — "Выход"
for i := 0; i < len(m.options)-1; i++ {
next, _ := m.Update(key("down"))
m = next.(AppModel)
}
next, cmd := m.Update(key("enter"))
m = next.(AppModel)
if !m.quitting {
t.Fatalf("ожидался флаг quitting после выбора пункта 'Выход'")
}
if cmd == nil {
t.Fatalf("ожидалась команда tea.Quit")
}
}
func TestGameQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
m := newTestAppModel()
next, _ := m.Update(key("enter")) // открываем настройки ставки
m = next.(AppModel)
next, _ = m.Update(key("enter")) // подтверждаем настройки и запускаем Тонк
m = next.(AppModel)
if m.state != statePlaying {
t.Fatalf("ожидалось statePlaying")
}
next, cmd := m.Update(key("q"))
m = next.(AppModel)
if m.state != stateMenu {
t.Fatalf("нажатие 'q' в игре должно вернуть в меню, а не закрыть приложение")
}
if m.quitting {
t.Fatalf("приложение не должно закрываться при возврате в меню")
}
if cmd != nil {
// допустима nil-команда — важно лишь, что это не tea.Quit
_ = cmd
}
}
func TestAppModelImplementsTeaModel(t *testing.T) {
var _ tea.Model = AppModel{}
}