Files

324 lines
11 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package reader
import (
"reflect"
"strconv"
"strings"
"testing"
"time"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/screen"
)
func region_mark(t *testing.T, r *Reader, x, y int) {
t.Helper()
snapshot := r.screen
snapshot.X, snapshot.Y = x, y
r.Update(snapshot)
r.Command(command.FollowCursor)
r.Command(command.CurrentChar)
if result := r.Set_mark(); result.Text != "set mark" {
t.Fatal(result)
}
}
func region_command(t *testing.T, r *Reader, id command.ID, want string) {
t.Helper()
if result := r.Command(id); result.Text != want {
t.Fatalf("%s: %+v; want %q", id, result, want)
}
}
func TestLiveRegionVimRulerTypingLatestReadAndClear(t *testing.T) {
for _, echo := range []bool{false, true} {
t.Run(map[bool]string{false: "echo disabled", true: "echo enabled"}[echo], func(t *testing.T) {
r := New()
r.Configure_echo(echo, false, false)
before := fixture("compose", "1,8 All")
before.X = 7
r.Update(before)
region_mark(t, r, 0, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
if _, ok := r.Marked_text(true); ok {
t.Fatal("successful creation retained marks")
}
r.Command(command.FollowCursor)
r.Update(before)
after := fixture("composeX", "1,9 All")
after.X = 8
r.Note_input(true, true, false)
got := r.Update(after)
var want []presentation.Message
if echo {
want = []presentation.Message{presentation.Character_message('X')}
}
if !reflect.DeepEqual(got, want) {
t.Fatal("ruler broke typing echo", got, want)
}
region_command(t, r, command.ReadLiveRegion1, "1,9 All")
if !strings.Contains(r.Command(command.ReadScreen).Text, "1,9 All") {
t.Fatal("manual read lost excluded text")
}
after = fixture("composeX", "2,9 All")
after.X = 8
if got := r.Update(after); len(got) != 0 {
t.Fatal("passive ruler spoke", got)
}
region_command(t, r, command.ClearLiveRegion1, "Live region 1 cleared")
if got := r.Update(after); len(got) != 0 {
t.Fatal("clear replayed hidden history", got)
}
after = fixture("composeX", "3,9 All")
after.X = 8
if got := r.Update(after); len(got) == 0 {
t.Fatal("clearing did not restore automatic reading")
}
})
}
}
func TestLiveRegionRectangleUnicodeEdgesAndReversedMarks(t *testing.T) {
r := New()
s := fixture("LEFTabcRIGHT", "LEFT界éxRIGHT", "untouched")
r.Update(s)
region_mark(t, r, 6, 1)
region_mark(t, r, 4, 0)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
region_command(t, r, command.ReadLiveRegion1, "abc\n界éx")
r.Command(command.FollowCursor)
next := fixture("LEFTxyzRIGHT", "LEFTé界qRIGHT", "untouched")
next.X, next.Y = r.screen.X, r.screen.Y
if got := r.Update(next); len(got) != 0 {
t.Fatal("rectangle boundary leaked", got)
}
next = fixture("LEFTxyzRIGH!", "LEFTé界qRIGHT", "untouched")
next.X, next.Y = r.screen.X, r.screen.Y
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('!')}) {
t.Fatal("adjacent character lost", got)
}
if s.Text(0) != "LEFTabcRIGHT" {
t.Fatal("filter mutated the original snapshot")
}
}
func TestLiveRegionSlotsReuseLimitListingAndIndependentTerminals(t *testing.T) {
r := New()
r.Update(fixture("first", "second"))
region_command(t, r, command.ReadLiveRegion2, "Live region 2 not set")
region_command(t, r, command.SetLiveRegion, "One or two marks are needed")
for slot := range 10 {
region_mark(t, r, 0, slot%2)
region_command(t, r, command.SetLiveRegion, "Live region "+strconv.Itoa(slot+1)+" set")
}
region_mark(t, r, 0, 0)
region_command(t, r, command.SetLiveRegion, "All 10 live regions are set")
if _, ok := r.Marked_text(true); !ok {
t.Fatal("failed creation discarded marks")
}
listing := r.Command(command.ListReadingRegions).Text
if !strings.Contains(listing, "Live region 10: row 2, column 1 to end of line") {
t.Fatal(listing)
}
region_command(t, r, command.ClearLiveRegion4, "Live region 4 cleared")
region_command(t, r, command.SetLiveRegion, "Live region 4 set")
region_command(t, r, command.ReadLiveRegion4, "first")
other := fixture("another terminal", "status")
other.Terminal = 3
r.Update(other)
region_command(t, r, command.ReadLiveRegion1, "Live region 1 not set")
region_command(t, r, command.ListReadingRegions, "No reading regions set")
region_mark(t, r, 0, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
region_command(t, r, command.ClearReadingRegions, "Reading regions cleared")
r.Update(fixture("new first", "new second"))
region_command(t, r, command.ReadLiveRegion1, "new first")
region_command(t, r, command.ClearLiveRegions, "Live regions cleared")
region_command(t, r, command.ReadLiveRegion10, "Live region 10 not set")
fresh := New()
fresh.Update(r.screen)
region_command(t, fresh, command.ReadLiveRegion1, "Live region 1 not set")
}
func TestRegionsSurviveFocusBufferAndResizeWithClippedReads(t *testing.T) {
r := New()
r.Update(fixture("editor", "status at bottom"))
region_mark(t, r, 7, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
s := r.screen
s.Active = false
r.Update(s)
s.Active = true
r.Update(s)
region_command(t, r, command.ReadLiveRegion1, "at bottom")
s.Generation++
r.Update(s)
region_command(t, r, command.ReadLiveRegion1, "at bottom")
s = screen.Snapshot{Terminal: s.Terminal, Active: true, Rows: 1, Cols: 5, Cells: []rune("small")}
r.Update(s)
region_command(t, r, command.ReadLiveRegion1, "Live region 1 outside screen")
r.Update(fixture("editor", "status new contents"))
region_command(t, r, command.ReadLiveRegion1, "new contents")
}
func TestReadingWindowAndLiveRegionComposeWithoutRestrictingReview(t *testing.T) {
r := New()
r.Update(fixture("outer text", "IN abc OUT", "IN def OUT"))
region_command(t, r, command.SetWindow, "Set window begin and end marks")
region_mark(t, r, 5, 2)
region_mark(t, r, 3, 1)
region_command(t, r, command.SetWindow, "Window Mode on for terminal 2")
if got := r.Automatic_snapshot().Text(1); got != " abc" {
t.Fatal(got)
}
region_mark(t, r, 3, 2)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
next := fixture("changed outer", "IN abc NEW", "IN XYZ NEW")
next.X, next.Y = r.screen.X, r.screen.Y
if got := r.Update(next); len(got) != 0 {
t.Fatal("outside window/live region spoke", got)
}
region_command(t, r, command.ReadLiveRegion1, "XYZ NEW")
if got := r.Command(command.ReadScreen).Text; !strings.Contains(got, "changed outer") || !strings.Contains(got, "XYZ NEW") {
t.Fatal("window clipped manual review", got)
}
next = fixture("changed outer", "IN qbc NEW", "IN XYZ NEW")
next.X, next.Y = r.screen.X, r.screen.Y
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('q')}) {
t.Fatal("inside window did not speak", got)
}
region_command(t, r, command.ClearWindow, "Window Mode off for terminal 2")
region_command(t, r, command.ReadLiveRegion1, "XYZ NEW")
region_command(t, r, command.ClearReadingRegions, "Reading regions cleared")
region_command(t, r, command.ListReadingRegions, "No reading regions set")
}
func TestLiveRegionProgressAloneAndConcurrentTyping(t *testing.T) {
for _, typing := range []bool{false, true} {
r := New()
r.Configure_progress(true)
r.Configure_echo(true, false, false)
before := fixture("compose", "Download 20%")
before.X = 7
r.Update(before)
region_mark(t, r, 0, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
r.Command(command.FollowCursor)
r.Update(before)
after := fixture("compose", "Download 30%")
after.X = 7
if typing {
after = fixture("composeX", "Download 30%")
after.X = 8
r.Note_input(true, true, false)
}
output := r.Update(after)
if len(output) > 0 && (!typing || len(output) != 1 || output[0].Character == nil || *output[0].Character != 'X') {
t.Fatal("progress region spoke", typing, output)
}
if value, ok := r.Next_progress(time.Now().Add(time.Second)); !ok || value != 30 {
t.Fatal("lost region progress", typing, value, ok)
}
r.Configure_progress(false)
after = fixture("composeX", "Download 40%")
after.X = 8
r.Update(after)
if _, ok := r.Next_progress(time.Now().Add(2 * time.Second)); ok {
t.Fatal("disabled monitor beeped")
}
}
}
func TestLiveRegionKeepsProgressOutsideWindowAndSuppressesSameRowTypedPercentages(t *testing.T) {
r := New()
r.Configure_progress(true)
r.Update(fixture("editor", "Download 20%"))
if err := r.Set_window(0, 0, 19, 0); err != nil {
t.Fatal(err)
}
region_mark(t, r, 0, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
next := fixture("editor", "Download 30%")
next.Y = 1
if got := r.Update(next); len(got) != 0 {
t.Fatal(got)
}
if value, ok := r.Next_progress(time.Now()); !ok || value != 30 {
t.Fatal("window hid an explicit live progress region", value, ok)
}
// A progress field and typing can also share one physical row.
r = New()
r.Configure_progress(true)
s := fixture("20 Download 20%")
s.X = 2
r.Update(s)
region_mark(t, r, 4, 0)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
r.Command(command.FollowCursor)
r.Update(s)
next = fixture("20% Download 30%")
next.X = 3
r.Note_input(true, true, false)
r.Update(next)
if value, ok := r.Next_progress(time.Now()); !ok || value != 30 {
t.Fatal("typed percentage won over live progress on the same row", value, ok)
}
}
func TestLiveRegionFilteringPreservesScrollAndDeletion(t *testing.T) {
r := New()
s := fixture("old first", "old second", "old third", "status 1")
r.Update(s)
region_mark(t, r, 0, 3)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
r.Command(command.FollowCursor)
next := fixture("old second", "old third", "new fourth", "status 2")
next.Y = 3
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{{Text: "new fourth"}}) {
t.Fatal("retained scroll history repeated", got)
}
s = fixture("composeX", "old third", "new fourth", "status 2")
s.X = 8
r.Update(s)
r.Note_input(true, false, false)
r.Note_deletion()
next = fixture("compose", "old third", "new fourth", "status 3")
next.X = 7
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('X')}) {
t.Fatal("ruler hid deletion feedback", got)
}
}
func TestLiveRegionSuppressesCursorHighlightAttributesIndentationAndWordFeedback(t *testing.T) {
for _, mode := range []string{"cursor", "table", "highlight"} {
r := New()
s := fixture("outside", " helo")
s.Attributes = make([]screen.Attribute, len(s.Cells))
r.Update(s)
region_mark(t, r, 0, 1)
region_command(t, r, command.SetLiveRegion, "Live region 1 set")
r.Command(command.FollowCursor)
r.Configure_tracking(mode)
r.Configure_indentation("both")
r.Configure_attributes("both", "fenrirBold")
r.Configure_echo(true, true, true)
next := fixture("outside", " helo ")
next.Y, next.X = 1, 9
next.Attributes = make([]screen.Attribute, len(next.Cells))
for col := range 9 {
next.Attributes[next.Cols+col].Style = screen.Bold
}
r.Note_input(true, true, true)
if got := r.Update(next); len(got) != 0 || len(r.Completed_words()) != 0 {
t.Fatal("excluded automatic feedback", mode, got, r.Completed_words())
}
if got := r.Command(command.CurrentLine).Text; !strings.Contains(got, "helo") {
t.Fatal("manual review lost live content", mode, got)
}
}
}