// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package reader import ( "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" "strings" "testing" "time" ) func attribute_fixture() screen.Snapshot { s := fixture("alpha", "bravo", "third") s.Attributes = make([]screen.Attribute, len(s.Cells)) for index := range s.Attributes { s.Attributes[index] = screen.Attribute{Foreground: screen.ColorDefault, Background: screen.ColorDefault} } s.Attributes[s.Cols] = screen.Attribute{Foreground: screen.ColorIndexed | 1, Background: screen.ColorIndexed | 4, Style: screen.Bold | screen.Blink} return s } func TestAttributeManualAndReviewOwnership(t *testing.T) { r := New() r.Configure_attributes("both", "fenrirFGColor fenrirBold fenrirBlink") s := attribute_fixture() r.Update(s) if got := r.Command(command.AttributeCursor).Text; got != "default " || r.review.Active() { t.Fatal(got, r.review.Active()) } result := r.Command(command.NextLine) if got := result.Attributes; got == nil || got.Cue != presentation.HasAttributes || got.Text != "red bold blink" { t.Fatal(got) } for range 2 { result := r.Command(command.AttributeCursor) if got := result.Text; got != "red bold blink" || result.Attributes != nil { t.Fatal(got, result.Attributes) } } result = r.Command(command.CurrentLine) if result.Attributes != nil { t.Fatal("same position repeated") } s.Y = 2 r.Update(s) // Detached review must still describe row 1. if got := r.Command(command.AttributeCursor).Text; got != "red bold blink" { t.Fatal(got) } r.Command(command.FollowCursor) if got := r.Command(command.AttributeCursor).Text; !strings.HasPrefix(got, "default") { t.Fatal(got) } s.Attributes = nil r.Update(s) if got := r.Command(command.AttributeCursor).Text; got != "" { t.Fatal("missing attributes invented", got) } } func TestAutomaticAttributeNavigationStyleOnlyDefaultsAndModes(t *testing.T) { for _, mode := range []string{"none", "speech", "beeps", "both"} { t.Run(mode, func(t *testing.T) { r := New() r.Configure_attributes(mode, "fenrirFGColor fenrirBold fenrirBlink") s := attribute_fixture() r.Update(s) r.Note_input(true, false, false) s.Y = 1 output := r.Update(s) feedback := attribute_messages(output) if (len(feedback) == 1) != (mode != "none") { t.Fatal(output) } if len(feedback) > 0 && ((feedback[0].Cue == presentation.HasAttributes) != (mode == "beeps" || mode == "both")) { t.Fatal(feedback) } if got := attribute_messages(r.Update(s)); len(got) != 0 { t.Fatal("repeat", got) } // Same cell/text/cursor, only its style changes, even after input expires. r.applicationInputAt = time.Time{} s.Attributes = append([]screen.Attribute(nil), s.Attributes...) s.Attributes[s.Cols].Style = screen.Underline if got := attribute_messages(r.Update(s)); (len(got) == 1) != (mode != "none") { t.Fatal("style-only", got) } r.Note_input(true, false, false) s.Y = 2 got := attribute_messages(r.Update(s)) if (len(got) == 1) != (mode == "speech" || mode == "both") { t.Fatal("default", got) } if len(got) > 0 && got[0].Cue != "" { t.Fatal("default presence cue", got) } }) } } func attribute_messages(messages []presentation.Message) []presentation.Message { var result []presentation.Message for _, message := range messages { if message.Attributes != nil { result = append(result, message) } } return result } func TestAttributeSessionMissingDataAndSuppression(t *testing.T) { for _, trial := range []string{"session", "inactive", "automatic", "cursor echo", "table", "highlight", "detached", "passive cursor", "missing"} { t.Run(trial, func(t *testing.T) { r := New() r.Configure_attributes("both", "fenrirBold") s := attribute_fixture() r.Update(s) r.Note_input(true, false, false) s.Y = 1 switch trial { case "session": s.Generation++ case "inactive": s.Active = false case "automatic": r.Configure(false, true) case "cursor echo": r.Configure(true, false) case "table", "highlight": r.Configure_tracking(trial) case "detached": r.Command(command.CurrentLine) case "passive cursor": r.applicationInputAt = time.Time{} case "missing": s.Attributes = nil } if got := attribute_messages(r.Update(s)); len(got) > 0 { t.Fatal(got) } }) } } func TestAttributeEmptyFormatDoesNotClaimDefaultHasAttributes(t *testing.T) { r := New() r.Configure_attributes("both", "") s := attribute_fixture() r.Update(s) r.Note_input(true, false, false) s.Y = 1 if got := attribute_messages(r.Update(s)); len(got) != 1 || got[0].Text != "has attribute" { t.Fatal(got) } r.Note_input(true, false, false) s.Y = 2 if got := attribute_messages(r.Update(s)); len(got) != 0 { t.Fatal(got) } } func TestAttributeSessionToggleResumesModeAndKeepsConfiguredDefault(t *testing.T) { for _, mode := range []string{"none", "speech", "beeps", "both"} { t.Run(mode, func(t *testing.T) { r := New() r.Configure_attributes(mode, "fenrirBold") s := attribute_fixture() r.Update(s) expected := "announcement of attributes disabled" if mode == "none" { expected = "announcement of attributes enabled" } if got := r.Command(command.ToggleAttributes).Text; got != expected { t.Fatal(got) } r.Configure_attributes(mode, "fenrirBold") // Unrelated quick/menu activity. result := r.Command(command.NextLine) message := result.Attributes if (message != nil) != (mode == "none") { t.Fatal(mode, message) } if message != nil && message.Cue != presentation.HasAttributes { t.Fatal(message) } if got := r.Command(command.AttributeCursor).Text; got != "bold" { t.Fatal("manual report", got) } r.Command(command.ToggleAttributes) r.Command(command.PreviousLine) result = r.Command(command.NextLine) message = result.Attributes if (message != nil) != (mode != "none") { t.Fatal("restored", mode, message) } if message != nil && message.Attributes.Speech != (mode != "beeps") { t.Fatal("lost mode", mode, message) } // Changing the actual preference explicitly applies its new mode. nextMode := "speech" if mode == nextMode { nextMode = "both" } r.Configure_attributes(nextMode, "fenrirBold") r.Command(command.PreviousLine) result = r.Command(command.NextLine) if got := result.Attributes; got == nil || got.Text != "bold" || !got.Attributes.Speech { t.Fatal(got) } }) } }