// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package review import ( "testing" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" ) func TestScreenRangesIncludeCursorAndPreserveRows(t *testing.T) { for _, testCase := range []struct { name string lines []string x, y int before, after string }{ {"unicode middle", []string{" first", "", "café 界!", " last"}, 5, 2, " first\n\ncafé 界", "界!\n last"}, {"top left", []string{"alpha", "bravo"}, 0, 0, "a", "alpha\nbravo"}, {"bottom right", []string{"alpha", "bravo"}, 19, 1, "alpha\nbravo", "blank"}, {"one row", []string{"ab界cd"}, 2, 0, "ab界", "界cd"}, {"blank row", []string{"alpha", "", "last"}, 7, 1, "alpha\n", "\nlast"}, {"trailing padding", []string{"alpha", " last"}, 14, 0, "alpha", "\n last"}, {"blank screen", []string{"", ""}, 4, 1, "blank", "blank"}, {"literal blank", []string{"blank"}, 0, 0, "b", "blank"}, {"last visible character", []string{"界"}, 0, 0, "界", "界"}, } { t.Run(testCase.name, func(t *testing.T) { var review Review snapshot := fixture(testCase.lines...) snapshot.X, snapshot.Y = testCase.x, testCase.y review.Update(snapshot) for _, action := range []struct { id command.ID want string }{ {command.ScreenBeforeCursor, testCase.before}, {command.ScreenAfterCursor, testCase.after}, } { result := review.Execute(action.id) if got := result.Text; got != action.want { t.Fatalf("%s: got %q, want %q", action.id, got, action.want) } wantCue := presentation.Cue("") if testCase.name == "blank screen" || testCase.name == "bottom right" && action.id == command.ScreenAfterCursor { wantCue = presentation.Blank } if result.Cue != wantCue { t.Fatalf("%s: cue %q, want %q", action.id, result.Cue, wantCue) } if review.x != testCase.x || review.y != testCase.y || review.screen.X != testCase.x || review.screen.Y != testCase.y { t.Fatal("range action moved a cursor") } } }) } } func TestScreenRangesPreferDetachedReviewAndKeepTableScope(t *testing.T) { var review Review snapshot := fixture("Name | City", "Ana | Paris", "Zoë | Rome") snapshot.X, snapshot.Y = 7, 0 review.Update(snapshot) review.Execute(command.CurrentChar) review.Set_mark() review.Set_table_mode(true) review.Execute(command.NextLine) // Application cursor moves while detached review stays on the second row. snapshot.X, snapshot.Y = 0, 2 review.Update(snapshot) for _, action := range []struct { id command.ID want string }{ {command.ScreenBeforeCursor, "Name | City\nAna | Pa"}, {command.ScreenAfterCursor, "aris\nZoë | Rome"}, } { if got := review.Execute(action.id).Text; got != action.want { t.Fatalf("%s: %q, want %q", action.id, got, action.want) } if review.x != 7 || review.y != 1 || review.tableColumn != 1 || review.markCount != 1 || !review.Active() { t.Fatal("range action disturbed review, column or mark state") } } review.Execute(command.FollowCursor) if got := review.Execute(command.ScreenAfterCursor).Text; got != "Zoë | Rome" { t.Fatal("follow did not restore application cursor scope", got) } // Screen generation and geometry changes must discard the old anchor. snapshot = fixture("new") snapshot.Generation = 1 review.Update(snapshot) if got := review.Execute(command.ScreenBeforeCursor).Text; got != "n" { t.Fatal("range kept an anchor from another screen", got) } review.Update(screen.Snapshot{}) for _, id := range []command.ID{command.ScreenBeforeCursor, command.ScreenAfterCursor} { result := review.Execute(id) if got := result.Text; got != "no text console" || result.Cue != presentation.Error { t.Fatal("inactive screen", got, result.Cue) } } }