package app import ( "strings" "testing" "glint/internal/editor" tea "github.com/charmbracelet/bubbletea" ) // altSemicolon is the Alt+; key event that opens the spell popup. var altSemicolon = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(";"), Alt: true} func editorPos(row, col int) editor.Position { return editor.Position{Row: row, Col: col} } func TestAltSemicolonOpensPopupOnMisspelling(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("a recieve here")) a.editor.SetCursor(editorPos(0, 4)) // inside "recieve" a.handleKey(altSemicolon) if a.mode != ModeSpell { t.Fatalf("mode = %d, want ModeSpell", a.mode) } if a.spell.word != "recieve" { t.Errorf("popup word = %q, want recieve", a.spell.word) } if len(a.spell.options) < 3 { t.Errorf("want suggestions + add + ignore, got %d options", len(a.spell.options)) } } func TestAltSemicolonNoMisspellingOpensToggleOnly(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("all correct words")) a.editor.SetCursor(editorPos(0, 1)) a.handleKey(altSemicolon) // With no flagged word, Alt+; opens a minimal toggle-only popup (no word, a // single Toggle option) so spellcheck can always be turned off/on. if a.mode != ModeSpell { t.Fatal("Alt+; did not open the toggle-only popup") } if a.spell.word != "" || len(a.spell.options) != 1 || a.spell.options[0].kind != spellToggle { t.Errorf("want toggle-only popup, got word=%q opts=%+v", a.spell.word, a.spell.options) } } func TestSpellPopupNumberKeyReplaces(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("a recieve here")) a.editor.SetCursor(editorPos(0, 4)) a.handleKey(altSemicolon) // The first suggestion for "recieve" is "receive"; press "1" to apply it. if a.spell.options[0].value != "receive" { t.Fatalf("first suggestion = %q, want receive", a.spell.options[0].value) } a.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("1")}) if a.mode != ModeEditor { t.Errorf("popup did not close after applying; mode = %d", a.mode) } if got := a.editor.Lines[0]; got != "a receive here" { t.Errorf("line after replace = %q, want \"a receive here\"", got) } } func TestSpellPopupAddKey(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("my zzplonk word")) a.editor.SetCursor(editorPos(0, 4)) a.handleKey(altSemicolon) if a.mode != ModeSpell { t.Fatalf("popup did not open on zzplonk") } a.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("i")}) // ignore for session if a.mode != ModeEditor { t.Errorf("popup did not close after ignore") } if strings.Contains(a.status, "failed") { t.Errorf("ignore reported failure: %q", a.status) } } func TestSpellPopupToggleOff(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("a recieve here")) a.editor.SetCursor(editorPos(0, 4)) a.handleKey(altSemicolon) a.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("t")}) // toggle off if a.editor.SpellEnabled() { t.Error("toggle did not disable spellcheck") } // With spellcheck off there are no underlines, but Alt+; still opens a // toggle-only popup so it can be turned back on. a.handleKey(altSemicolon) if a.mode != ModeSpell { t.Fatal("Alt+; with spellcheck off did not open the toggle popup") } a.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("t")}) // toggle on if !a.editor.SpellEnabled() { t.Error("toggle did not re-enable spellcheck") } } // A plain click (press then release) on a misspelled word opens its popup // (TASK-027: the popup must wait for release so a drag can win instead). func TestMouseClickOnMisspellingOpensPopup(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("a recieve here")) lm := a.leftMargin() y := a.topPad() - 1 x := lm + 4 // inside "recieve" a.Update(tea.MouseMsg{Button: tea.MouseButtonLeft, Action: tea.MouseActionPress, X: x, Y: y}) a.Update(tea.MouseMsg{Button: tea.MouseButtonLeft, Action: tea.MouseActionRelease, X: x, Y: y}) if a.mode != ModeSpell { t.Fatalf("plain click on a misspelling: mode = %d, want ModeSpell", a.mode) } } // Dragging from a misspelled word selects text and must NOT pop the spell popup. func TestMouseDragFromMisspellingSelectsNotPopup(t *testing.T) { a := newApp() a.setSize(100, 24) a.editor.SetContent([]byte("a recieve here")) lm := a.leftMargin() y := a.topPad() - 1 a.Update(tea.MouseMsg{Button: tea.MouseButtonLeft, Action: tea.MouseActionPress, X: lm + 4, Y: y}) a.Update(tea.MouseMsg{Button: tea.MouseButtonLeft, Action: tea.MouseActionMotion, X: lm + 9, Y: y}) if a.mode == ModeSpell { t.Fatal("drag from a misspelling opened the spell popup") } if !a.editor.HasSelection() { t.Fatal("drag from a misspelling did not select") } }