clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/elo-difficulty
puzzletea / app / screen_create_test.go
screen_create_test.go 11.2K
1package app23import (4 "strings"5 "testing"67 "github.com/FelineStateMachine/puzzletea/config"89 tea "charm.land/bubbletea/v2"10 "github.com/charmbracelet/x/ansi"11)1213func TestCreateScreenSpaceTogglesParentDescendantsAndLeaves(t *testing.T) {14 state := newCreateState(config.CreateConfig{}, 80)15 parentIndex := visibleNodeIndexByTitle(t, state, "Sudoku")16 state.cursor = parentIndex17 screen := createScreen{create: state}18 parent := state.visibleNodes()[parentIndex].node19 _, parentTotal := state.descendantLeafCounts(parent)2021 next, _, _ := screen.Update(tea.KeyPressMsg{Code: tea.KeySpace})22 got := next.(createScreen)23 if got.create.selectedCount() != parentTotal {24 t.Fatalf("selected count after parent space = %d, want %d", got.create.selectedCount(), parentTotal)25 }2627 got.create.expanded[parent.id] = true28 got.create.cursor = visibleLeafIndexByID(t, got.create, firstDescendantLeafID(t, parent.children[0]))29 next, _, _ = got.Update(tea.KeyPressMsg{Code: tea.KeySpace})30 got = next.(createScreen)31 if got.create.selectedCount() != parentTotal-1 {32 t.Fatalf("selected count after leaf space = %d, want %d", got.create.selectedCount(), parentTotal-1)33 }34}3536func TestCreateScreenEnterExpandsAndCollapsesParent(t *testing.T) {37 state := newCreateState(config.CreateConfig{}, 80)38 state.cursor = visibleNodeIndexByTitle(t, state, "Sudoku")39 screen := createScreen{create: state}40 parentID := screen.create.visibleNodes()[state.cursor].node.id4142 next, _, _ := screen.Update(tea.KeyPressMsg{Code: tea.KeyEnter})43 expanded := next.(createScreen)44 if !expanded.create.expanded[parentID] {45 t.Fatal("expected parent to expand")46 }47 if expanded.create.selectedCount() != 0 {48 t.Fatalf("selected count after parent enter = %d, want 0", expanded.create.selectedCount())49 }5051 next, _, _ = expanded.Update(tea.KeyPressMsg{Code: tea.KeyEnter})52 collapsed := next.(createScreen)53 if collapsed.create.expanded[parentID] {54 t.Fatal("expected parent to collapse")55 }56}5758func TestCreateScreenGenerateButtonProducesAction(t *testing.T) {59 state := newCreateState(config.CreateConfig{SelectedLeafIDs: []string{"nonogram/nonogram"}}, 80)60 state.focus = createFocusGenerate61 screen := createScreen{create: state}6263 _, _, action := screen.Update(tea.KeyPressMsg{Code: tea.KeyEnter})64 if _, ok := action.(createGenerateAction); !ok {65 t.Fatalf("action = %T, want createGenerateAction", action)66 }67}6869func TestCreateScreenGenerateButtonDisabledWithoutSelection(t *testing.T) {70 state := newCreateState(config.CreateConfig{Elo: 1200}, 80)71 state.focus = createFocusGenerate72 screen := createScreen{create: state}7374 _, _, action := screen.Update(tea.KeyPressMsg{Code: tea.KeyEnter})75 if action != nil {76 t.Fatalf("action = %T, want nil", action)77 }78}7980func TestCreateScreenMouseClickFocusesFields(t *testing.T) {81 state := newCreateState(config.CreateConfig{SelectedLeafIDs: []string{"nonogram/nonogram"}}, 120)82 screen := createScreen{create: state}.Resize(120, 36).(createScreen)83 contentWidth, contentHeight := createContentBounds(screen.width, screen.height)84 metrics := createMetrics(screen.create, contentWidth, contentHeight)85 _, _, contentX, contentY := createPanelContentOrigin(screen.width, screen.height, createBody(screen.create, screen.width, screen.height))8687 next, _, _, handled := screen.handleMouseClick(tea.MouseClickMsg{88 X: contentX + metrics.Settings.Seed.X + 8,89 Y: contentY + metrics.Settings.Seed.Y + 1,90 }, contentWidth, contentHeight)91 if !handled {92 t.Fatal("expected seed field click to be handled")93 }94 screen = next.(createScreen)95 if screen.create.focus != createFocusSeed {96 t.Fatalf("focus = %v, want createFocusSeed", screen.create.focus)97 }9899 metrics = createMetrics(screen.create, contentWidth, contentHeight)100 next, _, _, handled = screen.handleMouseClick(tea.MouseClickMsg{101 X: contentX + metrics.Settings.Elo.X + 7,102 Y: contentY + metrics.Settings.Elo.Y + 1,103 }, contentWidth, contentHeight)104 if !handled {105 t.Fatal("expected elo field click to be handled")106 }107 screen = next.(createScreen)108 if screen.create.focus != createFocusElo {109 t.Fatalf("focus = %v, want createFocusElo", screen.create.focus)110 }111}112113func TestCreateScreenMouseClickDisabledSeedDoesNotFocusSeed(t *testing.T) {114 state := newCreateState(config.CreateConfig{115 SelectedLeafIDs: []string{"nonogram/nonogram", "netwalk/netwalk"},116 }, 120)117 screen := createScreen{create: state}.Resize(120, 36).(createScreen)118 contentWidth, contentHeight := createContentBounds(screen.width, screen.height)119 metrics := createMetrics(screen.create, contentWidth, contentHeight)120 _, _, contentX, contentY := createPanelContentOrigin(screen.width, screen.height, createBody(screen.create, screen.width, screen.height))121122 next, _, _, handled := screen.handleMouseClick(tea.MouseClickMsg{123 X: contentX + metrics.Settings.Seed.X + 8,124 Y: contentY + metrics.Settings.Seed.Y + 1,125 }, contentWidth, contentHeight)126 if !handled {127 t.Fatal("expected disabled seed field click to be handled")128 }129 got := next.(createScreen)130 if got.create.focus == createFocusSeed {131 t.Fatal("disabled seed click should not focus the seed input")132 }133}134135func TestCreateScreenMouseClickGenerateButtonProducesAction(t *testing.T) {136 state := newCreateState(config.CreateConfig{SelectedLeafIDs: []string{"nonogram/nonogram"}}, 120)137 screen := createScreen{create: state}.Resize(120, 36).(createScreen)138 contentWidth, contentHeight := createContentBounds(screen.width, screen.height)139 metrics := createMetrics(screen.create, contentWidth, contentHeight)140 _, _, contentX, contentY := createPanelContentOrigin(screen.width, screen.height, createBody(screen.create, screen.width, screen.height))141142 _, _, action, handled := screen.handleMouseClick(tea.MouseClickMsg{143 X: contentX + metrics.SummaryButton.X + 1,144 Y: contentY + metrics.SummaryButton.Y,145 }, contentWidth, contentHeight)146 if !handled {147 t.Fatal("expected generate button click to be handled")148 }149 if _, ok := action.(createGenerateAction); !ok {150 t.Fatalf("action = %T, want createGenerateAction", action)151 }152}153154func TestCreateScreenMouseClickTreeRowsSelectsAndToggles(t *testing.T) {155 state := newCreateState(config.CreateConfig{}, 120)156 parentID := state.visibleNodes()[0].node.id157 state.expanded[parentID] = true158 screen := createScreen{create: state}.Resize(120, 36).(createScreen)159 contentWidth, contentHeight := createContentBounds(screen.width, screen.height)160 metrics := createMetrics(screen.create, contentWidth, contentHeight)161 _, _, contentX, contentY := createPanelContentOrigin(screen.width, screen.height, createBody(screen.create, screen.width, screen.height))162163 leafIndex := -1164 for i, item := range screen.create.visibleNodes() {165 if item.node.leaf != nil {166 leafIndex = i167 break168 }169 }170 if leafIndex < 0 {171 t.Fatal("test setup expected a visible leaf")172 }173 row := leafIndex - metrics.TreeStart174 next, _, _, handled := screen.handleMouseClick(tea.MouseClickMsg{175 X: contentX + metrics.Tree.X + 4,176 Y: contentY + metrics.Tree.Y + 2 + row,177 }, contentWidth, contentHeight)178 if !handled {179 t.Fatal("expected tree leaf click to be handled")180 }181 got := next.(createScreen)182 if got.create.focus != createFocusTree {183 t.Fatalf("focus = %v, want createFocusTree", got.create.focus)184 }185 if got.create.cursor != leafIndex {186 t.Fatalf("cursor = %d, want %d", got.create.cursor, leafIndex)187 }188 if got.create.selectedCount() != 1 {189 t.Fatalf("selected count = %d, want 1", got.create.selectedCount())190 }191}192193func TestCreateScreenMouseClickTreeParentExpands(t *testing.T) {194 state := newCreateState(config.CreateConfig{}, 120)195 parentIndex := visibleNodeIndexByTitle(t, state, "Sudoku")196 parentID := state.visibleNodes()[parentIndex].node.id197 screen := createScreen{create: state}.Resize(120, 36).(createScreen)198 contentWidth, contentHeight := createContentBounds(screen.width, screen.height)199 metrics := createMetrics(screen.create, contentWidth, contentHeight)200 _, _, contentX, contentY := createPanelContentOrigin(screen.width, screen.height, createBody(screen.create, screen.width, screen.height))201202 next, _, _, handled := screen.handleMouseClick(tea.MouseClickMsg{203 X: contentX + metrics.Tree.X + 4,204 Y: contentY + metrics.Tree.Y + 2 + parentIndex,205 }, contentWidth, contentHeight)206 if !handled {207 t.Fatal("expected tree parent click to be handled")208 }209 got := next.(createScreen)210 if !got.create.expanded[parentID] {211 t.Fatal("expected parent click to expand the tree row")212 }213}214215func TestCreateScreenRenderedParentRowsIncludeRollupCounts(t *testing.T) {216 state := newCreateState(config.CreateConfig{}, 80)217 sudoku := requireCreateNode(t, state.tree, "Sudoku")218 state.checked[firstDescendantLeafID(t, sudoku.children[0])] = true219220 line := renderCreateTreeLine(state, createVisibleNode{node: sudoku}, false)221 if !strings.Contains(line, "Sudoku") || !strings.Contains(line, "1/2") {222 t.Fatalf("rendered line = %q, want Sudoku with 1/2 rollup", line)223 }224}225226func TestCreateScreenRenderedTopLevelLeafOmitsRollupCounts(t *testing.T) {227 state := newCreateState(config.CreateConfig{}, 80)228 nonogram := requireCreateNode(t, state.tree, "Nonogram")229 state.checked[firstDescendantLeafID(t, nonogram)] = true230231 line := renderCreateTreeLine(state, createVisibleNode{node: nonogram}, false)232 if strings.Contains(line, "1/1") || strings.Contains(line, "[-]") {233 t.Fatalf("rendered line = %q, want selected leaf without rollup count", line)234 }235 if !strings.Contains(line, "[x] Nonogram") {236 t.Fatalf("rendered line = %q, want checked top-level leaf", line)237 }238}239240func TestCreateScreenRenderedParentRowsSignalSelectionState(t *testing.T) {241 state := newCreateState(config.CreateConfig{}, 80)242 sudoku := requireCreateNode(t, state.tree, "Sudoku")243244 line := renderCreateTreeLine(state, createVisibleNode{node: sudoku}, false)245 if !strings.Contains(line, "[ ] Sudoku") {246 t.Fatalf("unchecked parent line = %q, want unchecked marker", line)247 }248249 state.checked[firstDescendantLeafID(t, sudoku.children[0])] = true250 line = renderCreateTreeLine(state, createVisibleNode{node: sudoku}, false)251 if !strings.Contains(line, "[-] Sudoku") {252 t.Fatalf("partial parent line = %q, want partial marker", line)253 }254255 state.toggleDescendantLeaves(sudoku)256 line = renderCreateTreeLine(state, createVisibleNode{node: sudoku}, false)257 if !strings.Contains(line, "[x] Sudoku") {258 t.Fatalf("selected parent line = %q, want checked marker", line)259 }260}261262func TestCreateScreenEloFieldDoesNotRenderPromptMarker(t *testing.T) {263 state := newCreateState(config.CreateConfig{Elo: 1200}, 80)264 state.focus = createFocusElo265 screen, _, _ := (createScreen{create: state}).syncFocus()266 state = screen.(createScreen).create267268 settings := ansi.Strip(createSettingsPanel(state, 80))269 if strings.Contains(settings, "> 1200") || strings.Contains(settings, ">1200") {270 t.Fatalf("settings rendered Elo prompt marker: %q", settings)271 }272 if got := state.eloInput.Value(); got != "1200" {273 t.Fatalf("elo input value = %q, want 1200", got)274 }275}276277func visibleNodeIndexByTitle(t *testing.T, state createState, title string) int {278 t.Helper()279 for i, item := range state.visibleNodes() {280 if item.node.title == title {281 return i282 }283 }284 t.Fatalf("missing visible node %q", title)285 return 0286}287288func visibleLeafIndexByID(t *testing.T, state createState, id string) int {289 t.Helper()290 for i, item := range state.visibleNodes() {291 if item.node.leaf != nil && item.node.leaf.id == id {292 return i293 }294 }295 t.Fatalf("missing visible leaf %q", id)296 return 0297}