214 lines
7.3 KiB
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")
|
|
}
|
|
}
|
|
}
|