Files
fenrir/internal/review/screen_range_test.go

121 lines
4.5 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
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)
}
}
}