// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package presentation import ( "testing" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/sound" ) func TestPositionSoundPolicy(t *testing.T) { base := Options{EnableSound: true, PlaySoundForPositionInSet: true, RoleSoundPresentation: "sound_and_speech"} for _, test := range []struct { name string position, size int want string }{ {"first", 1, 3, sound.PositionCueName(0)}, {"middle", 2, 3, sound.PositionCueName(0.5)}, {"last", 3, 3, sound.PositionCueName(1)}, {"virtual set", 501, 1001, sound.PositionCueName(0.5)}, {"unknown", 0, 0, ""}, {"negative", -1, 3, ""}, {"outside set", 4, 3, ""}, {"unknown size", 1, -1, ""}, {"single item", 1, 1, ""}, } { t.Run(test.name, func(t *testing.T) { snapshot := atspi.Snapshot{Name: "Example", Role: "list item", PositionInSet: test.position, SetSize: test.size} cues := SoundCues(snapshot, base) if test.want == "" { if len(cues) != 0 { t.Fatalf("invalid set produced cues: %v", cues) } } else if len(cues) != 1 || cues[0].Name != test.want { t.Fatalf("position cues = %v, want %s", cues, test.want) } if got := FocusTextWithOptions(snapshot, base); got != "Example, list item" { t.Fatalf("position cue changed speech: %q", got) } }) } snapshot := atspi.Snapshot{Role: "radio button", Checked: true, PositionInSet: 2, SetSize: 3} for _, gate := range []string{"disabled sounds", "disabled position", "speech only", "unavailable", "defunct"} { options, source := base, snapshot switch gate { case "disabled sounds": options.EnableSound = false case "disabled position": options.PlaySoundForPositionInSet = false case "speech only": options.RoleSoundPresentation = "speech_only" case "unavailable": options.SoundCueAvailable = func(string) bool { return false } case "defunct": source.Defunct = true } if got := SoundCues(source, options); len(got) != 0 { t.Errorf("%s cues: %v", gate, got) } } base.PlaySoundForState = true base.RoleSoundPresentation = "sound_only" if got := SoundCues(snapshot, base); len(got) != 2 || got[0].Name != "radiobutton_checked" || got[1].Name != sound.PositionCueName(0.5) { t.Fatalf("state/position order = %v", got) } if got := EventSoundCues(atspi.NewEvent("object:state-changed:checked", snapshot), base); len(got) != 1 || got[0].Name != "radiobutton_checked" { t.Fatalf("state change replayed position: %v", got) } for _, eventType := range []string{atspi.FocusEvent, atspi.FocusedStateEvent, atspi.ActiveDescendantEvent, atspi.SelectionChangedEvent} { if got := EventSoundCues(atspi.NewEvent(eventType, snapshot), base); len(got) != 2 { t.Errorf("%s cues: %v", eventType, got) } } line := atspi.DocumentLine{Contents: []atspi.DocumentContent{{Snapshot: snapshot}, {Snapshot: snapshot}}} if got := DocumentLineSoundCues(line, base); len(got) != 2 { t.Fatalf("line replayed position: %v", got) } }