// 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") } } }