Fixed some element types being spoken even though sound only is selected.

This commit is contained in:
Storm Dragon committed 2026-10-26 02:41:07 -04:00
1 parent 6ee0454fbe
commit 0d76e44f9a
5 files changed
+53 -6

No files matched your search

+2 -1
View File
@@ -22,6 +22,7 @@ func radio_sound_fixture(t *testing.T) (*eventPresenter, *fakeSpeechQueue, func(
state := &eventLoopState{} state := &eventLoopState{}
state.presentationOptions = presentation.DefaultOptions() state.presentationOptions = presentation.DefaultOptions()
state.presentationOptions.EnableSound = true state.presentationOptions.EnableSound = true
state.presentationOptions.PlaySoundForRole = true
state.presentationOptions.PlaySoundForState = true state.presentationOptions.PlaySoundForState = true
state.presentationOptions.RoleSoundPresentation = "sound_only" state.presentationOptions.RoleSoundPresentation = "sound_only"
queue := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)} queue := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)}
@@ -40,7 +41,7 @@ func TestRadioFocusSoundCoalescesSelectionWithoutDelayingLabel(t *testing.T) {
option := atspi.Snapshot{BusName: ":1.7", Path: "/radio", Role: "radio button", Name: "Laptop", Mnemonic: "<Alt>l"} option := atspi.Snapshot{BusName: ":1.7", Path: "/radio", Role: "radio button", Name: "Laptop", Mnemonic: "<Alt>l"}
present(atspi.NewEvent(atspi.FocusEvent, option)) present(atspi.NewEvent(atspi.FocusEvent, option))
request := <-queue.requests request := <-queue.requests
if !strings.Contains(request.Text, "Laptop") || len(request.Sound) != 0 || presenter.state.pending.radioSound.ready == nil { if !strings.Contains(request.Text, "Laptop") || strings.Contains(request.Text, "radio button") || len(request.Sound) != 0 || presenter.state.pending.radioSound.ready == nil {
t.Fatalf("focus label/sound = %+v", request) t.Fatalf("focus label/sound = %+v", request)
} }
// The old member loses selection before GTK checks the focused member. // The old member loses selection before GTK checks the focused member.
+4 -2
View File
@@ -127,7 +127,10 @@ func FocusSpeechWithOptions(snapshot atspi.Snapshot, options Options) Speech {
role = "" role = ""
} }
_, replaceRole := roleSoundCue(snapshot, options) _, replaceRole := roleSoundCue(snapshot, options)
if options.RoleSoundPresentation == "sound_only" && replaceRole { _, replaceState := stateSoundCue(snapshot, options)
// A control-specific state cue also identifies its role. Keep the role
// spoken when role sounds are disabled or no replacement cue is available.
if options.RoleSoundPresentation == "sound_only" && (replaceRole || options.PlaySoundForRole && replaceState) {
role = "" role = ""
} }
if options.SpeechVerbosityLevel != 0 && snapshot.ChildCountKnown && snapshot.ChildCount > 0 { if options.SpeechVerbosityLevel != 0 && snapshot.ChildCountKnown && snapshot.ChildCount > 0 {
@@ -147,7 +150,6 @@ func FocusSpeechWithOptions(snapshot atspi.Snapshot, options Options) Speech {
} }
fields = append(fields, snapshot.Name, role) fields = append(fields, snapshot.Name, role)
states := controlStates(snapshot) states := controlStates(snapshot)
_, replaceState := stateSoundCue(snapshot, options)
if options.RoleSoundPresentation == "sound_only" && replaceState { if options.RoleSoundPresentation == "sound_only" && replaceState {
states = removeReplacedState(states) states = removeReplacedState(states)
} }
+43 -1
View File
@@ -18,6 +18,7 @@
package presentation package presentation
import ( import (
"fmt"
"math" "math"
"reflect" "reflect"
"testing" "testing"
@@ -209,7 +210,7 @@ func TestElementPresentationStrategiesKeepRoleAndStateIndependent(t *testing.T)
checkbox := atspi.Snapshot{Name: "Accept", Role: "check box", Checked: true, Required: true, Invalid: true} checkbox := atspi.Snapshot{Name: "Accept", Role: "check box", Checked: true, Required: true, Invalid: true}
options := Options{EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, RoleSoundPresentation: "sound_only"} options := Options{EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, RoleSoundPresentation: "sound_only"}
if got := FocusTextWithOptions(checkbox, options); got != "Accept, check box, required, invalid entry" { if got := FocusTextWithOptions(checkbox, options); got != "Accept, required, invalid entry" {
t.Fatalf("sound-only checkbox text = %q", got) t.Fatalf("sound-only checkbox text = %q", got)
} }
unsupported := atspi.Snapshot{Name: "Document", Role: "text frame", Expandable: true, Expanded: true} unsupported := atspi.Snapshot{Name: "Document", Role: "text frame", Expandable: true, Expanded: true}
@@ -222,6 +223,47 @@ func TestElementPresentationStrategiesKeepRoleAndStateIndependent(t *testing.T)
} }
} }
func TestStateCueReplacesControlRoleInSoundOnlyFocus(t *testing.T) {
for _, role := range []string{"check box", "check menu item", "radio button", "radio menu item", "switch", "menu item"} {
for _, checked := range []bool{false, true} {
for _, strategy := range []string{"sound_only", "sound_and_speech", "speech_only"} {
t.Run(fmt.Sprintf("%s/%t/%s", role, checked, strategy), func(t *testing.T) {
snapshot := atspi.Snapshot{Name: "Option", Role: role, Checked: checked, Checkable: role == "menu item"}
options := Options{EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, RoleSoundPresentation: strategy}
want := FocusText(snapshot)
if strategy == "sound_only" {
want = "Option"
}
if got := FocusTextWithOptions(snapshot, options); got != want {
t.Fatalf("focus speech = %q, want %q", got, want)
}
if strategy != "sound_only" {
return
}
for _, disabled := range []string{"sound", "state", "unavailable"} {
fallback := options
switch disabled {
case "sound":
fallback.EnableSound = false
case "state":
fallback.PlaySoundForState = false
case "unavailable":
fallback.SoundCueAvailable = func(string) bool { return false }
}
if got := FocusTextWithOptions(snapshot, fallback); got != FocusText(snapshot) {
t.Errorf("%s fallback = %q, want %q", disabled, got, FocusText(snapshot))
}
}
options.PlaySoundForRole = false
if got, want := FocusTextWithOptions(snapshot, options), "Option, "+snapshot.ControlRole(); got != want {
t.Errorf("disabled role sound speech = %q, want %q", got, want)
}
})
}
}
}
}
func TestStateChangeSoundOnlySuppressesOnlyReplacedState(t *testing.T) { func TestStateChangeSoundOnlySuppressesOnlyReplacedState(t *testing.T) {
options := Options{EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, RoleSoundPresentation: "sound_only"} options := Options{EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, RoleSoundPresentation: "sound_only"}
event := atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Role: "check box", Checked: true}) event := atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Role: "check box", Checked: true})
+1 -1
View File
@@ -21,7 +21,7 @@ import "strings"
const Name = "Cthulhu" const Name = "Cthulhu"
const Version = "2026.10.25" const Version = "2026.10.26"
const Channel = "testing" const Channel = "testing"
var Build = Version var Build = Version
+3 -1
View File
@@ -143,7 +143,9 @@ def check_radio_buttons(controls, tabs, server, readerLog):
time.sleep(0.35) time.sleep(0.35)
cues = radio_cues()[cueOffset:] cues = radio_cues()[cueOffset:]
assert cues == ["radiobutton_checked"], (keyName, target, cues) assert cues == ["radiobutton_checked"], (keyName, target, cues)
assert any(target in record["text"] for record in server.snapshot()[speechOffset:]), target + " label was lost" spoken = [record["text"] for record in server.snapshot()[speechOffset:]]
assert any(target in text for text in spoken), target + " label was lost"
assert not any("radio button" in text for text in spoken), (target, spoken)
records = [json.loads(line) for line in readerLog.read_text().splitlines() if line.startswith("{")] records = [json.loads(line) for line in readerLog.read_text().splitlines() if line.startswith("{")]
errors = [record for record in records if record.get("message") == "play sound" errors = [record for record in records if record.get("message") == "play sound"
and record.get("cue", "").startswith("radiobutton_") and record.get("error") != "context canceled"] and record.get("cue", "").startswith("radiobutton_") and record.get("error") != "context canceled"]