121 lines
4.5 KiB
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)
|
|
}
|
|
}
|
|
}
|