1717 lines
No EOL
58 KiB
Go
1717 lines
No EOL
58 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
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, ColorChoiceA) }},
|
||
{"chess", func() tea.Model { return NewChessModel(CheckersDifficultyEasy, ColorChoiceA) }},
|
||
{"corpsestarch", func() tea.Model { return NewCorpseStarchModel() }},
|
||
{"go", func() tea.Model { return NewGoModel(9, GoDifficultyEasy, ColorChoiceA) }},
|
||
{"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, ColorChoiceA) }},
|
||
{"senet", func() tea.Model { return NewSenetModel(SenetDifficultyEasy, ColorChoiceA) }},
|
||
{"ur", func() tea.Model { return NewUrModel(UrDifficultyEasy, ColorChoiceA) }},
|
||
{"poker", func() tea.Model { return NewPokerModel(3) }},
|
||
}
|
||
|
||
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")) // подтверждаем язык -> экран выбора режима
|
||
m = next.(AppModel)
|
||
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 != stateModeSelect {
|
||
t.Fatalf("ожидался переход в stateModeSelect после выбора языка")
|
||
}
|
||
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 != stateModeSelect {
|
||
t.Fatalf("ожидался переход в stateModeSelect")
|
||
}
|
||
if CurrentLang() != LangRU {
|
||
t.Fatalf("ожидался язык по умолчанию LangRU")
|
||
}
|
||
}
|
||
|
||
func TestModeSelectGamesLeadsToMenu(t *testing.T) {
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("enter")) // язык -> выбор режима
|
||
m = next.(AppModel)
|
||
if m.state != stateModeSelect {
|
||
t.Fatalf("ожидался stateModeSelect, получено %v", m.state)
|
||
}
|
||
if m.modeCursor != 0 {
|
||
t.Fatalf("ожидался курсор по умолчанию на 'Просто игры' (0), получено %d", m.modeCursor)
|
||
}
|
||
|
||
next, _ = m.Update(key("enter")) // подтверждаем "Просто игры"
|
||
m = next.(AppModel)
|
||
if m.state != stateMenu {
|
||
t.Fatalf("выбор 'Просто игры' должен был вести в stateMenu, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestModeSelectStoryLeadsToDifficultyWhenNoSave(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("enter")) // язык -> выбор режима
|
||
m = next.(AppModel)
|
||
|
||
next, _ = m.Update(key("down")) // переключаемся на "Режим истории"
|
||
m = next.(AppModel)
|
||
if m.modeCursor != 1 {
|
||
t.Fatalf("ожидался курсор на 'Режим истории' (1), получено %d", m.modeCursor)
|
||
}
|
||
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(AppModel)
|
||
if m.state != stateStoryDifficulty {
|
||
t.Fatalf("выбор 'Режим истории' без сохранения должен был вести в stateStoryDifficulty, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestStoryDifficultyEscReturnsToModeSelect(t *testing.T) {
|
||
m := NewAppModel()
|
||
m.state = stateStoryDifficulty
|
||
|
||
next, _ := m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.state != stateModeSelect {
|
||
t.Errorf("esc из выбора сложности сторимода должен был вернуть на выбор режима, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestModeSelectEscReturnsToLanguage(t *testing.T) {
|
||
m := NewAppModel()
|
||
m.state = stateModeSelect
|
||
|
||
next, _ := m.Update(key("esc"))
|
||
m = next.(AppModel)
|
||
if m.state != stateLanguage {
|
||
t.Errorf("esc из выбора режима должен был вернуть на выбор языка, получено %v", m.state)
|
||
}
|
||
}
|
||
|
||
func TestModeSelectQuitViaCtrlC(t *testing.T) {
|
||
m := NewAppModel()
|
||
m.state = stateModeSelect
|
||
|
||
next, _ := m.Update(key("ctrl+c"))
|
||
m = next.(AppModel)
|
||
if !m.quitting {
|
||
t.Errorf("ctrl+c на экране выбора режима должен был завершить приложение")
|
||
}
|
||
}
|
||
|
||
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("right")) // переключаемся на "Сильный"
|
||
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("right")) // переключаемся на "Сильный"
|
||
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{}
|
||
}
|
||
|
||
func TestCapitalQQuitsFromLanguageScreen(t *testing.T) {
|
||
m := NewAppModel()
|
||
next, _ := m.Update(key("Q"))
|
||
m = next.(AppModel)
|
||
if !m.quitting {
|
||
t.Errorf("заглавная Q должна была завершить приложение с экрана выбора языка")
|
||
}
|
||
}
|
||
|
||
func TestCapitalQQuitsFromModeSelect(t *testing.T) {
|
||
m := AppModel{state: stateModeSelect}
|
||
next, _ := m.Update(key("Q"))
|
||
m = next.(AppModel)
|
||
if !m.quitting {
|
||
t.Errorf("заглавная Q должна была завершить приложение с экрана выбора режима")
|
||
}
|
||
}
|
||
|
||
func TestCapitalQQuitsFromDeepInsideStoryMode(t *testing.T) {
|
||
withTempConfigDir(t)
|
||
m := NewAppModel()
|
||
m.story = NewStoryModeState(StoryDifficultyEasy, time.Now())
|
||
m.story.Onboarding = StoryOnboardingDone
|
||
m.state = stateStoryUseItem // произвольный глубоко вложенный экран сторимода
|
||
|
||
next, _ := m.Update(key("Q"))
|
||
m = next.(AppModel)
|
||
if !m.quitting {
|
||
t.Errorf("заглавная Q должна была завершить приложение даже из глубоко вложенного экрана сторимода")
|
||
}
|
||
}
|
||
|
||
func TestCapitalQQuitsWhileGameIsActive(t *testing.T) {
|
||
m := AppModel{state: statePlaying, activeGame: NewTicTacToeModel(TicTacToeDifficultyEasy)}
|
||
|
||
next, _ := m.Update(key("Q"))
|
||
m = next.(AppModel)
|
||
if !m.quitting {
|
||
t.Errorf("заглавная Q должна была завершить приложение даже во время активной партии")
|
||
}
|
||
}
|
||
|
||
func TestLowercaseQDoesNotQuitAppFromModeSelect(t *testing.T) {
|
||
m := AppModel{state: stateModeSelect}
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(AppModel)
|
||
if m.quitting {
|
||
t.Errorf("строчная q не должна завершать приложение целиком — только возвращать на уровень назад")
|
||
}
|
||
if m.state != stateLanguage {
|
||
t.Errorf("строчная q должна была вернуть на экран выбора языка, получено %v", m.state)
|
||
}
|
||
} |