Files
fenrir/internal/review/navigation_test.go

214 lines
7.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/screen"
"testing"
)
func TestCharacterCrossesPhysicalRowsAndKeepsCharacterMetadata(t *testing.T) {
for _, feedback := range []bool{false, true} {
var r Review
r.Configure(feedback, feedback, true)
s := screen.Snapshot{Active: true, Rows: 3, Cols: 3, Cells: []rune("abÉ 界cd"), X: 2}
r.Update(s)
for _, trial := range []struct {
id command.ID
text string
x, y int
crossing bool
}{
{command.NextChar, "space", 0, 1, true},
{command.PreviousChar, "É", 2, 0, true},
{command.NextChar, "space", 0, 1, true},
{command.NextChar, "space", 1, 1, false},
{command.NextChar, "space", 2, 1, false},
{command.NextChar, "界", 0, 2, true},
} {
result := r.Execute(trial.id)
if got := result.Text; got != trial.text || r.x != trial.x || r.y != trial.y || result.Cue != "" {
t.Fatalf("%s: %q at %d,%d", trial.id, got, r.x, r.y)
}
if result.Character == nil {
t.Fatal("lost character metadata")
}
if (result.After.Text == "line break") != (feedback && trial.crossing) || (result.After.Cue == presentation.LineEnd) != (feedback && trial.crossing) {
t.Fatal(result.After.Text, result.After.Cue)
}
}
r.Execute(command.ScreenLastChar)
result := r.Execute(command.NextChar)
text := result.Text
if feedback && (text != "end of screen" || result.Cue != presentation.Bottom) || !feedback && (text != "" || result.Cue != "") {
t.Fatal(text, result.Cue)
}
if r.x != 2 || r.y != 2 || result.After.Text != "" {
t.Fatal("screen edge moved cursor or retained line break")
}
r.Execute(command.Top)
text = r.Execute(command.PreviousChar).Text
if feedback && text != "start of screen" || !feedback && text != "" {
t.Fatal(text)
}
}
}
func TestWordSelectionFromInsideAndBetweenWords(t *testing.T) {
for _, trial := range []struct {
x, y int
id command.ID
text string
targetX, targetY int
crossing bool
}{
{4, 0, command.CurrentWord, "alpha", 2, 0, false},
{4, 0, command.PreviousWord, "start of screen", 4, 0, false},
{9, 0, command.PreviousWord, "alpha", 2, 0, false},
{8, 0, command.CurrentWord, "alpha", 2, 0, false},
{8, 0, command.NextWord, "beta!", 9, 0, false},
{0, 1, command.CurrentWord, "beta!", 9, 0, true},
{0, 1, command.PreviousWord, "alpha", 2, 0, true},
{9, 0, command.NextWord, "界é", 1, 2, true},
{1, 2, command.PreviousWord, "beta!", 9, 0, true},
{0, 0, command.CurrentWord, "start of screen", 0, 0, false},
{8, 2, command.NextWord, "end of screen", 8, 2, false},
} {
var r Review
s := fixture(" alpha beta!", "", " 界é last")
s.X, s.Y = trial.x, trial.y
r.Update(s)
result := r.Execute(trial.id)
got := result.Text
if got != trial.text || r.x != trial.targetX || r.y != trial.targetY || (result.After.Text == "line break") != trial.crossing {
t.Fatalf("%+v: %q at %d,%d after=%q", trial, got, r.x, r.y, result.After.Text)
}
if s.X != r.screen.X || s.Y != r.screen.Y {
t.Fatal("moved application cursor")
}
}
var r Review
r.Update(fixture("", ""))
for _, id := range []command.ID{command.CurrentWord, command.PreviousWord, command.NextWord} {
result := r.Execute(id)
if got := result.Text; got != "blank" || result.Cue != presentation.Blank {
t.Fatal(id, got, result.Cue)
}
}
}
func TestReviewLineEndpointsAndTopBottom(t *testing.T) {
var r Review
s := fixture(" Éx!", "\t\u2003", "last")
s.X = 3
r.Update(s)
for _, trial := range []struct {
id command.ID
text, after string
x, y int
}{
{command.FirstText, "É", "first character in line indent 2", 2, 0},
{command.LastText, "bang", "last character in line", 4, 0},
{command.NextLine, "\t\u2003", "", 4, 1},
{command.FirstText, "line is empty", "", 4, 1},
{command.LastText, "line is empty", "", 4, 1},
{command.LineStart, "blank", "beginning of line", 0, 1},
{command.LineEnd, "space", "end of line", 19, 1},
{command.CurrentLine, "\t\u2003", "", 0, 1},
{command.Bottom, "Bottom", "", 0, 2},
{command.NextChar, "a", "", 1, 2},
{command.Top, "Top", "", 0, 0},
} {
// Whitespace-only row content is presented as blank, including Unicode.
want := trial.text
if trial.id == command.NextLine || trial.id == command.CurrentLine {
want = "blank"
}
result := r.Execute(trial.id)
if got := result.Text; got != want || result.After.Text != trial.after || r.x != trial.x || r.y != trial.y {
t.Fatalf("%s: %q after=%q at %d,%d", trial.id, got, result.After.Text, r.x, r.y)
}
}
}
func TestTableBoundaryContextAndModeSynchronization(t *testing.T) {
var r Review
r.Update(fixture("Name|Subject|Empty", "José|É!|"))
r.Set_table_mode(true)
r.Execute(command.CurrentWord)
r.Set_mark()
r.Execute(command.NextLine)
for _, trial := range []struct {
id command.ID
text, after string
cue presentation.Cue
}{
{command.NextWord, "É! Subject", "", ""},
{command.PreviousChar, "É", "start of cell", presentation.LineStart},
{command.NextChar, "bang", "", ""},
{command.NextChar, "bang", "end of cell", presentation.LineEnd},
{command.NextWord, "blank Empty", "", ""},
{command.NextChar, "blank", "end of cell", presentation.LineEnd},
{command.NextWord, "blank Empty", "end of line", presentation.LineEnd},
{command.LineStart, "José Name", "first cell", ""},
{command.PreviousWord, "José Name", "start of line", presentation.LineStart},
{command.LineEnd, "blank Empty", "last cell", ""},
} {
result := r.Execute(trial.id)
if got := result.Text; got != trial.text || result.After.Text != trial.after || result.After.Cue != trial.cue {
t.Fatalf("%s: %q after=%q cue=%q", trial.id, got, result.After.Text, result.After.Cue)
}
}
r.Execute(command.Top)
if got := r.Execute(command.CurrentWord).Text; got != "Name Name" {
t.Fatal("Top kept old table column", got)
}
r.Execute(command.LineEnd)
r.Execute(command.Bottom)
if got := r.Execute(command.CurrentWord).Text; got != "José Name" {
t.Fatal("Bottom kept old table column", got)
}
r.Execute(command.FollowCursor)
r.Set_table_mode(false)
r.Execute(command.LineEnd)
r.Set_table_mode(true)
if got := r.Execute(command.CurrentWord).Text; got != "Empty Column 3" {
t.Fatal("mode change did not resynchronize", got)
}
}
func TestReviewScreenChangePolicyAndClamping(t *testing.T) {
for _, leave := range []bool{false, true} {
var r Review
r.Configure(true, true, leave)
s := fixture("header|value", "data|cell")
r.Update(s)
r.Set_table_mode(true)
r.Execute(command.CurrentWord)
r.Set_mark()
r.Execute(command.Bottom)
r.Execute(command.LineEnd)
s.Terminal++
s.Generation++
r.Update(s)
if r.Active() == leave || len(r.tableHeaders) != 0 {
t.Fatal("wrong review/header ownership", r.Active())
}
if leave && (r.x != 0 || r.y != 0) || !leave && (r.x != 5 || r.y != 1) {
t.Fatal(r.x, r.y)
}
// A resized screen keeps the anchor only when explicitly requested,
// and bounds it before any action can index cells.
r.Execute(command.ScreenLastChar)
r.Update(fixture("x"))
r.Execute(command.CurrentChar)
r.Update(screen.Snapshot{})
if r.Active() {
t.Fatal("inactive session retained review")
}
}
}