// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "strings" "testing" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" ) func TestAdditionalReportsUseActiveCursorWithoutMovingIt(t *testing.T) { for _, detached := range []bool{false, true} { var r Review s := fixture("top", " café Ω! ", "bottom") s.X, s.Y = 7, 1 r.Update(s) kind := " text cursor " if detached { r.Execute(command.CurrentChar) r.Set_mark() s.X, s.Y = 0, 2 r.Update(s) kind = " review cursor " } for _, trial := range []struct { id command.ID text, after string }{ {command.CursorLine, "2", kind + " line number"}, {command.CursorColumn, "8", kind + " column number"}, {command.LineToCursor, " café", kind}, {command.LineFromCursor, "Ω!", kind}, } { result := r.Execute(trial.id) if got := result.Text; got != trial.text || result.After.Text != trial.after || result.Cue != "" { t.Fatalf("%s detached=%v: %q after=%q cue=%q", trial.id, detached, got, result.After.Text, result.Cue) } if r.x != 7 || r.y != 1 || r.Active() != detached || r.screen.X != s.X || r.screen.Y != s.Y { t.Fatal("report moved or detached a cursor") } } if detached && r.markCount != 1 { t.Fatal("reports cleared marks") } // Attached reports must continue following subsequent application motion. s.X, s.Y = 1, 0 r.Update(s) want := "2" if detached { want = "8" } if got := r.Execute(command.CursorColumn).Text; got != want { t.Fatal("wrong cursor after passive update", got) } r.Execute(command.FollowCursor) if got := r.Execute(command.CursorLine).Text; got != "1" || r.Active() { t.Fatal("follow cursor failed", got) } result := r.Execute(command.CurrentChar) if result.After.Text != "" { t.Fatal("stale cursor announcement") } } } func TestAdditionalLineRangesEdgesAndBlankCues(t *testing.T) { for _, trial := range []struct { line string x int before, after string blankBefore, blankAfter bool }{ {"ab界cd", 2, "ab", "界cd", false, false}, {"alpha", 0, "blank", "alpha", true, false}, {"alpha", 19, "alpha", "blank", false, true}, {"", 4, "blank", "blank", true, true}, {"blank", 0, "blank", "blank", true, false}, {strings.Repeat("z", 20), 19, strings.Repeat("z", 19), "z", false, false}, } { var r Review s := fixture(trial.line) s.X = trial.x r.Update(s) for _, action := range []struct { id command.ID text string blank bool }{ {command.LineToCursor, trial.before, trial.blankBefore}, {command.LineFromCursor, trial.after, trial.blankAfter}, } { result := r.Execute(action.id) if got := result.Text; got != action.text || (result.Cue == presentation.Blank) != action.blank || result.After.Text != " text cursor " { t.Fatalf("%s %q x=%d: %q cue=%q after=%q", action.id, trial.line, trial.x, got, result.Cue, result.After.Text) } } } } func TestAdditionalScreenCharactersUsePhysicalCorners(t *testing.T) { for _, lines := range [][]string{{"First row ends Z", " last"}, {"", ""}, {"Ω"}, {strings.Repeat("x", 19) + "É"}} { var r Review s := fixture(lines...) r.Update(s) r.Set_table_mode(true) for _, trial := range []struct { id command.ID x, y int label string }{ {command.ScreenLastChar, 19, len(lines) - 1, "last character in screen"}, {command.ScreenFirstChar, 0, 0, "first character in screen"}, } { char := s.Line(trial.y)[trial.x] result := r.Execute(trial.id) if got := result.Text; got != presentation.Character(char)+" "+trial.label || r.x != trial.x || r.y != trial.y { t.Fatalf("%s: %q at %d,%d", trial.id, got, r.x, r.y) } if got := result.Character; got == nil || *got != char || !r.Active() || r.screen.X != 0 || r.screen.Y != 0 { t.Fatal("wrong character metadata or cursor ownership") } } } } func TestAdditionalReportsReadWholeTableRowAndResetWithSession(t *testing.T) { var r Review s := fixture("Name|Subject", "é|long word", "only") s.X = 7 r.Update(s) r.Execute(command.CurrentChar) r.Set_mark() r.Set_table_mode(true) r.Execute(command.CurrentWord) r.Set_mark() if got := r.Execute(command.LineToCursor).Text; got != "Name|Su" { t.Fatal("line report lost row scope", got) } if got := r.Execute(command.LineFromCursor).Text; got != "bject" || r.tableColumn != 1 || r.markCount != 1 { t.Fatal("line report disturbed table state", got) } r.Execute(command.ReviewDown) r.Execute(command.ReviewDown) if got := r.Execute(command.LineToCursor).Text; got != "only" || r.tableColumn != 1 { t.Fatal("missing cell changed whole-row report", got) } s.Generation++ s.X = 0 r.Update(s) result := r.Execute(command.CursorColumn) if got := result.Text; got != "1" || result.After.Text != " text cursor column number" { t.Fatal("session kept detached cursor", got, result.After.Text) } r.Update(screen.Snapshot{}) for _, id := range []command.ID{command.CursorLine, command.CursorColumn, command.LineToCursor, command.LineFromCursor, command.ScreenFirstChar, command.ScreenLastChar} { result := r.Execute(id) if got := result.Text; got != "no text console" || result.Cue != presentation.Error || result.After.Text != "" { t.Fatal("inactive screen", id, got) } } }