324 lines
11 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|