// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package reader import ( "reflect" "testing" "time" "git.stormux.org/storm/fenrir/internal/applicationoverrides" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" ) func TestApplicationComposerCursorFeedback(t *testing.T) { for _, mode := range []string{"left", "right", "status redraw", "leave review", "passive", "cursor off", "automatic off", "outside input", "edited input", "other adapter"} { t.Run(mode, func(t *testing.T) { r := New() r.Configure_review(true, true, true, true) before := fixture("Working", "› hello") before.X, before.Y = 7, 1 frame := applicationoverrides.Frame{Recognized: true, OwnsPresentation: true, InputCursor: true, Input: &screen.Region{Left: 2, Top: 1, Right: 19, Bottom: 1}} r.Update_application(before, frame) next := fixture("Working", "› hello") next.X, next.Y = 6, 1 want := []presentation.Message{presentation.Character_message('o')} if mode == "right" { before.X = 5 r.Update_application(before, frame) } if mode == "status redraw" { copy(next.Line(0), []rune("Reading file")) } if mode == "leave review" { r.Command(command.CurrentLine) } if mode != "passive" { r.Note_input(true, false, false) } switch mode { case "passive": want = nil case "cursor off": r.Configure(true, false) want = nil case "automatic off": r.Configure(false, true) want = nil case "outside input": next.Y = 0 want = nil case "edited input": next.Line(1)[2] = 'X' want = nil case "other adapter": frame.InputCursor = false want = nil } if got := r.Update_application(next, frame); !reflect.DeepEqual(got, want) { t.Fatal(mode, got, want) } if mode == "leave review" && r.review.Active() { t.Fatal("direct navigation remained in detached review") } }) } } func TestApplicationActivityUsesSoundTimerWithoutScreenRedraws(t *testing.T) { r := New() r.Configure_progress(true) r.Configure(false, false) // Working cues are independent of speech/automatic reading. s := fixture("Working", "› ") frame := applicationoverrides.Frame{Recognized: true, OwnsPresentation: true, Activity: true} r.Update_application(s, frame) now := time.Now() for _, elapsed := range []time.Duration{0, 4 * time.Second, 8 * time.Second} { if value, ok := r.Next_progress(now.Add(elapsed)); !ok || value != 50 { t.Fatal(elapsed, value, ok) } if _, ok := r.Next_progress(now.Add(elapsed + 3999*time.Millisecond)); ok { t.Fatal("activity flooded") } } frame.Activity = false r.Update_application(s, frame) if r.Progress_active() { t.Fatal("idle retained activity") } if _, ok := r.Next_progress(now.Add(12 * time.Second)); ok { t.Fatal("idle beeped") } frame.Activity = true r.Configure_progress(false) r.Update_application(s, frame) if _, ok := r.Next_progress(now.Add(16 * time.Second)); ok { t.Fatal("progress toggle ignored") } r.Configure_progress(true) r.Update_application(s, frame) s.Active = false r.Update_application(s, frame) if _, ok := r.Next_progress(now.Add(20 * time.Second)); ok { t.Fatal("inactive terminal beeped") } }