Files

305 lines
11 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package ninjam_test
import (
"slices"
"strconv"
"strings"
"testing"
"git.stormux.org/storm/fenrir/internal/applicationoverrides/ninjam"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/presentation"
"git.stormux.org/storm/fenrir/internal/reader"
"git.stormux.org/storm/fenrir/internal/screen"
)
func snapshot(selected int, mute, volume string, messages ...string) screen.Snapshot {
s := screen.Snapshot{Terminal: 1, Active: true, Rows: 24, Cols: 100, Y: 23, Cells: make([]rune, 2400), Attributes: make([]screen.Attribute, 2400)}
for i := range s.Cells {
s.Cells[i] = ' '
s.Attributes[i] = screen.Attribute{Foreground: screen.ColorIndexed | 7, Background: screen.ColorIndexed}
}
paint := func(row int, text string) { copy(s.Line(row), []rune(text)) }
paint(0, "LOCAL")
paint(1, " master: ["+mute+"]mute ["+volume+"] | metronome: [ ]mute [0.0dB center]")
paint(2, " [Channel] [X]xmit [Input] [ ]mute [0.0dB center] [del] <-20.0dB>")
paint(3, "REMOTE")
paint(4, "Username")
paint(5, " \"Remote channel\" [X]recv [ ]mute [0.0dB center] <-20.0dB>")
paint(16, "CHAT")
for i, text := range messages {
if i < 4 {
paint(21-len(messages)+i, text)
}
}
paint(22, "####")
paint(23, "[QUIT NINJAM] : fixture.invalid : 120.0BPM 16BPI : 48000Hz")
field := 0
for row := range s.Rows {
inside := false
for col, char := range s.Line(row) {
if char == ']' {
inside = false
}
if inside && field-1 == selected {
s.Attributes[row*s.Cols+col] = screen.Attribute{Foreground: screen.ColorIndexed, Background: screen.ColorIndexed | 7}
}
if char == '[' {
inside = true
field++
}
}
}
return s
}
func texts(messages []presentation.Message) []string {
var result []string
for _, message := range messages {
if message.Text != "" {
result = append(result, message.Text)
}
}
return result
}
func TestControlsHaveContextAndState(t *testing.T) {
for _, trial := range []struct {
selected int
mute, want string
}{
{0, " ", "Master unmuted"}, {0, "X", "Master muted"},
{1, " ", "Master Volume and pan 0.0dB center"},
{2, " ", "Metronome unmuted"}, {5, " ", "Local channel Channel transmitting"},
{10, " ", "Remote Username, channel Remote channel receiving"},
} {
a := &ninjam.Adapter{}
frame := a.Observe(snapshot(trial.selected, trial.mute, "0.0dB center"))
if got := texts(frame.Focus); !frame.Recognized || !slices.Equal(got, []string{trial.want}) {
t.Fatalf("%+v: %v", trial, got)
}
}
}
func TestFocusChatMetersAndValueReflow(t *testing.T) {
a := &ninjam.Adapter{}
r := reader.New()
update := func(s screen.Snapshot) []string { return texts(r.Update_application(s, a.Observe(s))) }
update(snapshot(0, " ", "0.0dB center", "<Username> first"))
got := update(snapshot(1, " ", "0.0dB center", "<Username> first", "<Username> second"))
if !slices.Equal(got, []string{"Master Volume and pan 0.0dB center", "<Username> second"}) {
t.Fatal(got)
}
got = update(snapshot(1, " ", "-0.5dB center", "<Username> first", "<Username> second"))
if !slices.Equal(got, []string{"minus 0.5dB"}) {
t.Fatal("reflow", got)
}
s := snapshot(1, " ", "-0.5dB center", "<Username> first", "<Username> second")
copy(s.Line(22), []rune("##################"))
copy(s.Line(2)[65:], []rune("<-10.0dB>"))
if got := update(s); len(got) != 0 {
t.Fatal("meter chatter", got)
}
if result, _ := a.Command(command.ReadLiveRegion1); result.Text != "<Username> second" {
t.Fatal(result)
}
if result, _ := a.Command(command.ReadLiveRegion3); !strings.Contains(result.Text, "120.0BPM") || !strings.Contains(result.Text, "Interval position 18 of 100") {
t.Fatal(result)
}
}
func TestReviewResumesOnlyOnApplicationNavigation(t *testing.T) {
for _, direct := range []bool{false, true} {
a, r := &ninjam.Adapter{}, reader.New()
s := snapshot(0, " ", "0.0dB center", "<Username> old")
r.Update_application(s, a.Observe(s))
r.Command(command.CurrentLine)
if direct {
r.Note_input(true, false, false)
}
s = snapshot(1, " ", "0.0dB center", "<Username> old", "<Username> new")
got := texts(r.Update_application(s, a.Observe(s)))
want := []string{"<Username> new"}
if direct {
want = append([]string{"Master Volume and pan 0.0dB center"}, want...)
}
if !slices.Equal(got, want) {
t.Fatal(direct, got)
}
}
}
func TestChatScrollPagingRepetitionAndResize(t *testing.T) {
a := &ninjam.Adapter{}
a.Observe(snapshot(0, " ", "0.0dB center", "<Username> a", "<Username> b", "<Username> c", "<Username> d"))
frame := a.Observe(snapshot(0, " ", "0.0dB center", "<Username> b", "<Username> c", "<Username> d", "<Username> d"))
if got := texts(frame.Updates); !slices.Equal(got, []string{"<Username> d"}) {
t.Fatal("repeated message", got)
}
a.Note_key(104)
// An interval update can arrive before the application's history redraw.
a.Observe(snapshot(0, " ", "0.0dB center", "<Username> b", "<Username> c", "<Username> d", "<Username> d"))
frame = a.Observe(snapshot(0, " ", "0.0dB center", "<Username> old", "<Username> b", "<Username> c", "<Username> d"))
if len(frame.Updates) != 0 {
t.Fatal("history announced", frame.Updates)
}
s := snapshot(0, " ", "0.0dB center", "<Username> old history")
s.Generation++
if frame = a.Observe(s); len(frame.Updates) != 0 {
t.Fatal("buffer rebaseline", frame.Updates)
}
copy(s.Line(0), []rune("SHELL"))
if a.Observe(s).Recognized {
t.Fatal("unrelated screen recognized")
}
}
func TestChatAlongsideTypingAndMutedAutomaticReading(t *testing.T) {
a, r := &ninjam.Adapter{}, reader.New()
s := snapshot(-1, " ", "0.0dB center", "<Username> old")
s.Y = 21
r.Update_application(s, a.Observe(s))
r.Note_input(true, true, false)
s = snapshot(-1, " ", "0.0dB center", "<Username> old", "<Username> incoming")
s.Y, s.X = 21, 1
s.Line(21)[0] = 'a'
got := texts(r.Update_application(s, a.Observe(s)))
if !slices.Equal(got, []string{"a", "<Username> incoming"}) {
t.Fatal(got)
}
r.Configure(false, true)
s = snapshot(-1, " ", "0.0dB center", "<Username> incoming", "<Username> silent")
s.Y = 21
if got := texts(r.Update_application(s, a.Observe(s))); len(got) != 0 {
t.Fatal(got)
}
if result, _ := a.Command(command.ReadLiveRegion1); result.Text != "<Username> silent" {
t.Fatal("latest lost while autoread off", result)
}
}
func TestChatBurstAndTransientClearedArea(t *testing.T) {
a := &ninjam.Adapter{}
old := []string{"<Username> a", "<Username> b", "<Username> c", "<Username> d"}
a.Observe(snapshot(0, " ", "0.0dB center", old...))
if frame := a.Observe(snapshot(0, " ", "0.0dB center")); len(frame.Updates) != 0 {
t.Fatal("cleared area announced", frame.Updates)
}
if frame := a.Observe(snapshot(0, " ", "0.0dB center", old...)); len(frame.Updates) != 0 {
t.Fatal("redraw replayed history", frame.Updates)
}
burst := []string{"<Username> e", "<Username> f", "<Username> g", "<Username> h"}
if got := texts(a.Observe(snapshot(0, " ", "0.0dB center", burst...)).Updates); !slices.Equal(got, burst) {
t.Fatal("visible burst lost", got)
}
}
func TestWrappedChatRecallAndChangedGeometry(t *testing.T) {
a := &ninjam.Adapter{}
a.Observe(snapshot(0, " ", "0.0dB center", "<Username> old"))
first := "<Username> " + strings.Repeat("x", 99-len("<Username> "))
frame := a.Observe(snapshot(0, " ", "0.0dB center", "<Username> old", first, " continuation"))
want := first + " continuation"
if got := texts(frame.Updates); !slices.Equal(got, []string{want}) {
t.Fatal("wrapped message", got)
}
if result, _ := a.Command(command.ReadLiveRegion1); result.Text != want {
t.Fatal("recall lost continuation", result)
}
old := snapshot(0, " ", "0.0dB center")
resized := screen.Snapshot{Terminal: 1, Active: true, Rows: 20, Cols: 80, Y: 19, Cells: make([]rune, 1600), Attributes: make([]screen.Attribute, 1600)}
for i := range resized.Cells {
resized.Cells[i] = ' '
}
for row := 0; row < 3; row++ {
copy(resized.Line(row), old.Line(row)[:80])
copy(resized.Attributes[row*80:(row+1)*80], old.Attributes[row*100:row*100+80])
}
copy(resized.Line(13), []rune("CHAT"))
copy(resized.Line(16), []rune("<Username> old history"))
copy(resized.Line(18), []rune("#####"))
copy(resized.Line(19), old.Line(23)[:80])
if frame := a.Observe(resized); len(frame.Updates) != 0 {
t.Fatal("resize replayed history", frame.Updates)
}
copy(resized.Line(15), resized.Line(16))
clear(resized.Line(16))
copy(resized.Line(16), []rune("<Username> after resize"))
if got := texts(a.Observe(resized).Updates); !slices.Equal(got, []string{"<Username> after resize"}) {
t.Fatal("resized chat region", got)
}
if result, _ := a.Command(command.ReadLiveRegion3); !strings.Contains(result.Text, "of 80") {
t.Fatal("resized interval region", result)
}
}
func TestVolumeAndPanAnnounceOnlyChangedComponent(t *testing.T) {
for _, selected := range []int{1, 3, 8, 12} {
t.Run(strconv.Itoa(selected), func(t *testing.T) {
a, r := &ninjam.Adapter{}, reader.New()
makeSnapshot := func(value string, chat ...string) screen.Snapshot {
s := snapshot(selected, " ", "0.0dB center", chat...)
// Repaint the selected bracket with the same style, including
// changes in width at center and across signed dB values.
for row := range s.Rows {
for col, attribute := range s.Attributes[row*s.Cols : (row+1)*s.Cols] {
if attribute.Background != screen.ColorIndexed|7 {
continue
}
line := string(s.Line(row))
start := col
end := start + strings.Index(line[start:], "]")
line = line[:start] + value + line[end:]
for i := range s.Line(row) {
s.Line(row)[i] = ' '
s.Attributes[row*s.Cols+i] = screen.Attribute{Foreground: screen.ColorIndexed | 7, Background: screen.ColorIndexed}
}
copy(s.Line(row), []rune(line))
for i := start; i < start+len(value); i++ {
s.Attributes[row*s.Cols+i] = attribute
}
return s
}
}
t.Fatal("missing selected field")
return s
}
update := func(value string, chat ...string) []string {
s := makeSnapshot(value, chat...)
return texts(r.Update_application(s, a.Observe(s)))
}
update("0.0dB center", "<Username> old")
for _, trial := range []struct{ value, want string }{
{"0.0dB 1%L", "1 percent left"},
{"0.0dB 2%L", "2 percent left"},
{"0.0dB center", "center"},
{"0.0dB 1%R", "1 percent right"},
{"-0.5dB 1%R", "minus 0.5dB"},
{"0.0dB 1%R", "0.0dB"},
{"+0.5dB 1%R", "plus 0.5dB"},
{"+1.0dB 2%R", "plus 1.0dB 2 percent right"},
} {
if got := update(trial.value, "<Username> old"); !slices.Equal(got, []string{trial.want}) {
t.Fatalf("%s: %v", trial.value, got)
}
}
if got := update("+1.0dB 2%R", "<Username> old"); len(got) != 0 {
t.Fatal("unchanged value repeated", got)
}
if got := update("+1.0dB 3%R", "<Username> old", "<Username> new"); !slices.Equal(got, []string{"3 percent right", "<Username> new"}) {
t.Fatal("chat alongside pan", got)
}
if result, _ := a.Command(command.ReadLiveRegion2); !strings.Contains(result.Text, "Volume and pan plus 1.0dB 3 percent right") {
t.Fatal("full control recall lost", result)
}
r.Command(command.CurrentLine)
if got := update("+1.0dB 4%R", "<Username> new", "<Username> review"); !slices.Equal(got, []string{"<Username> review"}) {
t.Fatal("pan disturbed detached review", got)
}
})
}
}