157 lines
5.6 KiB
Go
157 lines
5.6 KiB
Go
// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package reader
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/command"
|
|
"git.stormux.org/storm/fenrir/internal/presentation"
|
|
"git.stormux.org/storm/fenrir/internal/screen"
|
|
)
|
|
|
|
func capture_diagnostics(t *testing.T, level slog.Leveler) *bytes.Buffer {
|
|
t.Helper()
|
|
var log bytes.Buffer
|
|
previousLogger := slog.Default()
|
|
slog.SetDefault(slog.New(slog.NewJSONHandler(&log, &slog.HandlerOptions{Level: level})))
|
|
t.Cleanup(func() { slog.SetDefault(previousLogger) })
|
|
return &log
|
|
}
|
|
|
|
func check_update_record(t *testing.T, log *bytes.Buffer, fields map[string]string) map[string]json.RawMessage {
|
|
t.Helper()
|
|
lines := bytes.Split(bytes.TrimSpace(log.Bytes()), []byte("\n"))
|
|
var record map[string]json.RawMessage
|
|
if err := json.Unmarshal(lines[len(lines)-1], &record); err != nil {
|
|
t.Fatal(err, log.String())
|
|
}
|
|
fields["msg"] = `"reader update"`
|
|
for field, want := range fields {
|
|
if got := string(record[field]); got != want {
|
|
t.Errorf("%s=%s, want %s", field, got, want)
|
|
}
|
|
}
|
|
if strings.Contains(log.String(), "PRIVATE") {
|
|
t.Fatal("diagnostics exposed screen or output text")
|
|
}
|
|
return record
|
|
}
|
|
|
|
func TestReaderDiagnosticsCursorAndStatusWithoutText(t *testing.T) {
|
|
var level slog.LevelVar
|
|
level.Set(slog.LevelDebug)
|
|
log := capture_diagnostics(t, &level)
|
|
r := New()
|
|
r.Update(fixture("PRIVATE first", "PRIVATE second", "1/2 PRIVATE"))
|
|
r.Command(command.CurrentLine)
|
|
r.Command(command.NextLine)
|
|
log.Reset()
|
|
next := fixture("PRIVATE first", "PRIVATE second", "2/2 PRIVATE")
|
|
next.Y = 1
|
|
r.Note_input(true, false, false)
|
|
if got := r.Update(next); !reflect.DeepEqual(got, []presentation.Message{{Text: "PRIVATE second"}}) {
|
|
t.Fatal("diagnostics changed cursor feedback", got)
|
|
}
|
|
check_update_record(t, log, map[string]string{
|
|
"cursor": "[0,1]", "previous_cursor": "[0,0]", "mode": `"cursor"`,
|
|
"text_rows": "[2]", "text_cells": "1", "messages": "1", "message_lengths": "[14]",
|
|
"path": `"screen-changes"`, "review": "false", "cursor_echo": "true",
|
|
})
|
|
log.Reset()
|
|
level.Set(slog.LevelInfo)
|
|
r.Update(next)
|
|
if log.Len() != 0 {
|
|
t.Fatal("reader diagnostics were not opt-in", log.String())
|
|
}
|
|
}
|
|
|
|
func TestReaderDiagnosticsStationaryCursorAndSuppression(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
name, mode, path string
|
|
review, automatic bool
|
|
messages string
|
|
}{
|
|
{"cursor", "cursor", "screen-changes", false, true, "0"},
|
|
{"highlight", "highlight", "highlight", false, true, "1"},
|
|
{"review", "cursor", "screen-changes", true, true, "0"},
|
|
{"automatic off", "cursor", "automatic-disabled", false, false, "0"},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
log := capture_diagnostics(t, slog.LevelDebug)
|
|
before := fixture("PRIVATE first", "PRIVATE second", "third", "status")
|
|
before.Attributes = make([]screen.Attribute, len(before.Cells))
|
|
next := before
|
|
next.Attributes = make([]screen.Attribute, len(before.Cells))
|
|
selected := screen.Attribute{Foreground: 7, Background: 4}
|
|
for col := range before.Cols {
|
|
before.Attributes[col] = selected
|
|
next.Attributes[next.Cols+col] = selected
|
|
}
|
|
r := New()
|
|
r.Configure(testCase.automatic, true)
|
|
r.Configure_tracking(testCase.mode)
|
|
r.Update(before)
|
|
if testCase.review {
|
|
r.Command(command.CurrentLine)
|
|
}
|
|
log.Reset()
|
|
r.Update(next)
|
|
check_update_record(t, log, map[string]string{
|
|
"cursor": "[0,0]", "previous_cursor": "[0,0]", "text_rows": "[]",
|
|
"attribute_rows": "[0,1]", "attribute_cells": "40", "attributes": "true",
|
|
"path": `"` + testCase.path + `"`, "messages": testCase.messages,
|
|
"review": map[bool]string{true: "true", false: "false"}[testCase.review],
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReaderDiagnosticsBoundAttributeDetailsAndHandleSessionChanges(t *testing.T) {
|
|
log := capture_diagnostics(t, slog.LevelDebug)
|
|
r := New()
|
|
r.Update(screen.Snapshot{}) // Inactive captures may have no geometry or cells.
|
|
check_update_record(t, log, map[string]string{"path": `"inactive"`, "comparable": "false"})
|
|
before := fixture("PRIVATE", "PRIVATE", "PRIVATE", "PRIVATE")
|
|
before.Attributes = make([]screen.Attribute, len(before.Cells))
|
|
r.Update(before)
|
|
check_update_record(t, log, map[string]string{"path": `"session-change"`, "comparable": "false"})
|
|
next := before
|
|
next.Attributes = make([]screen.Attribute, len(before.Cells))
|
|
for index := range next.Attributes {
|
|
next.Attributes[index].Background = uint32(index%2 + 1)
|
|
}
|
|
log.Reset()
|
|
r.Update(next)
|
|
record := check_update_record(t, log, map[string]string{
|
|
"attribute_changes_truncated": "true", "attribute_cells": "80", "attribute_rows": "[0,1,2,3]",
|
|
})
|
|
var changes []AttributeChange
|
|
if err := json.Unmarshal(record["attribute_changes"], &changes); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(changes) != 32 || changes[0].Row != 0 || changes[0].Start != 0 || changes[0].End != 1 || changes[0].After.Background != 1 {
|
|
t.Fatal("attribute details lost geometry/appearance or exceeded their bound", changes)
|
|
}
|
|
}
|