// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package progress import ( "fmt" "strings" "testing" "time" "git.stormux.org/storm/fenrir/internal/screen" ) func snapshot(lines ...string) screen.Snapshot { s := screen.Snapshot{Terminal: 1, Active: true, Rows: len(lines), Cols: 300} for _, line := range lines { s.Cells = append(s.Cells, []rune(line+strings.Repeat(" ", s.Cols-len([]rune(line))))...) } return s } func TestWholeRowThrottlingAndFinalValue(t *testing.T) { var m Monitor now := time.Unix(100, 0) before := snapshot("Download 49%", "") after := snapshot("Download 50%", "") m.Update(before, after, -1) if value, ok := m.Next(now); !ok || value != 50 { t.Fatal(value, ok) } for percentage := 51; percentage <= 100; percentage++ { next := snapshot("Download "+format_percent(percentage), "") m.Update(after, next, -1) after = next } if _, ok := m.Next(now.Add(50 * time.Millisecond)); ok { t.Fatal("flood not throttled") } if value, ok := m.Next(now.Add(100 * time.Millisecond)); !ok || value != 100 { t.Fatal("lost final pending value", value, ok) } if _, ok := m.Next(now.Add(time.Second)); ok { t.Fatal("repeated without output") } next := snapshot("Download 100% complete", "") m.Update(after, next, -1) if _, ok := m.Next(now.Add(time.Second)); ok { t.Fatal("duplicate percentage") } // A new operation may decrease; completion never disables monitoring. m.Update(next, snapshot("Download 1%", ""), -1) if value, ok := m.Next(now.Add(2 * time.Second)); !ok || value != 1 { t.Fatal(value, ok) } } func TestMonitorResetFilters(t *testing.T) { for _, mode := range []string{"initial", "inactive", "terminal", "generation", "resize", "typing", "prompt", "password", "navigation", "large cursor", "ordinary output"} { t.Run(mode, func(t *testing.T) { var m Monitor now := time.Unix(100, 0) before, after := snapshot("Download 20%", "", "", ""), snapshot("Download 30%", "", "", "") m.Update(before, after, -1) m.Next(now) next := snapshot("Download 40%", "", "", "") editingRow := -1 switch mode { case "initial": after = screen.Snapshot{} case "inactive": next.Active = false case "terminal": next.Terminal++ case "generation": next.Generation++ case "resize": next = snapshot("Download 40%", "") case "typing": editingRow = next.Y case "prompt": next = snapshot("Download 40%", "Username@host:~$", "", "") next.Y = 1 case "password": next = snapshot("Download 40%", "Password:", "", "") next.Y = 1 case "navigation": next = after next.X = 1 case "large cursor": next.Y = 3 case "ordinary output": next = snapshot("done", "", "", "") } m.Update(after, next, editingRow) if _, ok := m.Next(now.Add(time.Second)); ok || m.Active() { t.Fatal("retained progress") } }) } } func TestMultilineLongRowsAndActivity(t *testing.T) { now := time.Unix(100, 0) for _, testCase := range []struct { lines []string want float64 ok bool }{ {[]string{"package 20% [## ]", "Total 80% [######## ]", ""}, 80, true}, {[]string{strings.Repeat("package", 35) + " 60% |###### |", "", ""}, 60, true}, {[]string{"humidity 50%", "some other output", ""}, 0, false}, {[]string{strings.Repeat("unrelated", 30) + " 50%", "", ""}, 0, false}, {[]string{"50%", "some other output", ""}, 0, false}, } { var m Monitor m.Update(snapshot("", "", ""), snapshot(testCase.lines...), -1) value, ok := m.Next(now) if ok != testCase.ok || ok && value != testCase.want { t.Fatal(testCase, value, ok) } } var m Monitor before, after := snapshot("", ""), snapshot("⠋ Working", "") m.Update(before, after, -1) if value, ok := m.Next(now); !ok || value != 50 { t.Fatal(value, ok) } m.Update(after, snapshot("⠙ Working", ""), -1) if _, ok := m.Next(now.Add(500 * time.Millisecond)); ok { t.Fatal("activity flood") } if _, ok := m.Next(now.Add(time.Second)); !ok { t.Fatal("missing activity") } if _, ok := m.Next(now.Add(2 * time.Second)); ok { t.Fatal("beeping stopped spinner") } } func format_percent(value int) string { return fmt.Sprint(value) + "%" }