diff --git a/internal/app/document_review_test.go b/internal/app/document_review_test.go index 0a35fb6..0f6c4d4 100644 --- a/internal/app/document_review_test.go +++ b/internal/app/document_review_test.go @@ -127,11 +127,11 @@ func TestDocumentPresentationUsesObjectSoundPolicy(t *testing.T) { if got := <-speech.spoken; got != "Power" { t.Fatalf("sound-only structural speech = %q", got) } - for _, name := range []string{"button", "togglebutton_checked"} { - cue := <-queue.sounds - if cue.Name != name || cue.Pan != -1 { - t.Fatalf("structural cue = %#v, want %s panned left", cue, name) - } + if len(queue.sounds) != 1 { + t.Fatalf("structural cues = %d, want one toggle state cue", len(queue.sounds)) + } + if cue := <-queue.sounds; cue.Name != "togglebutton_checked" || cue.Pan != -1 { + t.Fatalf("structural cue = %#v, want togglebutton_checked panned left", cue) } } diff --git a/internal/presentation/sound_cues.go b/internal/presentation/sound_cues.go index 3411ef7..6e09762 100644 --- a/internal/presentation/sound_cues.go +++ b/internal/presentation/sound_cues.go @@ -73,12 +73,15 @@ func SoundCues(snapshot atspi.Snapshot, options Options) []sound.Cue { return nil } + stateCue, hasStateCue := stateSoundCue(snapshot, options) var cues []sound.Cue - if cue, ok := roleSoundCue(snapshot, options); ok { + // A toggle's state cue already identifies the button. Keep role and state + // replacement decisions independent for speech and disabled/unavailable cues. + if cue, ok := roleSoundCue(snapshot, options); ok && !(snapshot.Role == "toggle button" && hasStateCue) { cues = append(cues, cue) } - if cue, ok := stateSoundCue(snapshot, options); ok { - cues = append(cues, cue) + if hasStateCue { + cues = append(cues, stateCue) } if cue, ok := position_sound_cue(snapshot, options); ok { cues = append(cues, cue) diff --git a/internal/presentation/sound_cues_test.go b/internal/presentation/sound_cues_test.go index 220c845..6a593db 100644 --- a/internal/presentation/sound_cues_test.go +++ b/internal/presentation/sound_cues_test.go @@ -40,7 +40,10 @@ func TestRoleAndStateSoundPolicy(t *testing.T) { {atspi.Snapshot{Role: "menu item", Checked: true}, []string{"checkbox_checked"}}, {atspi.Snapshot{Role: "menu item", Indeterminate: true}, []string{"checkbox_mixed"}}, {atspi.Snapshot{Role: "menu item"}, nil}, - {atspi.Snapshot{Role: "toggle button", Checked: true}, []string{"button", "togglebutton_checked"}}, + {atspi.Snapshot{Role: "toggle button", Checked: true}, []string{"togglebutton_checked"}}, + {atspi.Snapshot{Role: "toggle button"}, []string{"togglebutton_unchecked"}}, + {atspi.Snapshot{Role: "switch", Checked: true}, []string{"switch_on"}}, + {atspi.Snapshot{Role: "switch"}, []string{"switch_off"}}, {atspi.Snapshot{Role: "link", Visited: true}, []string{"link_visited"}}, {atspi.Snapshot{Role: "entry"}, nil}, {atspi.Snapshot{Role: "terminal"}, nil}, @@ -88,7 +91,8 @@ func TestFocusedEventSoundCues(t *testing.T) { {"button", atspi.Snapshot{Role: "button"}, []string{"button"}}, {"link", atspi.Snapshot{Role: "link"}, []string{"link"}}, {"visited link", atspi.Snapshot{Role: "link", Visited: true}, []string{"link_visited"}}, - {"checked toggle", atspi.Snapshot{Role: "toggle button", Checked: true}, []string{"button", "togglebutton_checked"}}, + {"checked toggle", atspi.Snapshot{Role: "toggle button", Checked: true}, []string{"togglebutton_checked"}}, + {"unchecked toggle", atspi.Snapshot{Role: "toggle button"}, []string{"togglebutton_unchecked"}}, } { t.Run(test.name, func(t *testing.T) { for _, eventType := range []string{"focus:", "object:state-changed:focused"} { @@ -109,6 +113,27 @@ func TestFocusedEventSoundCues(t *testing.T) { } } +func TestToggleSoundPreservesUnavailableStateFeedback(t *testing.T) { + options := Options{ + EnableSound: true, PlaySoundForRole: true, PlaySoundForState: true, + RoleSoundPresentation: "sound_only", + SoundCueAvailable: func(name string) bool { return name == "button" }, + } + for _, checked := range []bool{false, true} { + snapshot := atspi.Snapshot{Name: "Power", Role: "toggle button", Checked: checked} + if got := cueNames(SoundCues(snapshot, options)); !reflect.DeepEqual(got, []string{"button"}) { + t.Fatalf("unavailable state cue, checked=%v: %v", checked, got) + } + wantText := "Power, not pressed" + if checked { + wantText = "Power, pressed" + } + if got := FocusTextWithOptions(snapshot, options); got != wantText { + t.Fatalf("unavailable state speech = %q, want %q", got, wantText) + } + } +} + func cueNames(cues []sound.Cue) []string { if cues == nil { return nil @@ -167,8 +192,8 @@ func TestElementPresentationStrategiesKeepRoleAndStateIndependent(t *testing.T) cues []string }{ {"speech_only", true, true, "Power, toggle button, pressed", nil}, - {"sound_and_speech", true, true, "Power, toggle button, pressed", []string{"button", "togglebutton_checked"}}, - {"sound_only", true, true, "Power", []string{"button", "togglebutton_checked"}}, + {"sound_and_speech", true, true, "Power, toggle button, pressed", []string{"togglebutton_checked"}}, + {"sound_only", true, true, "Power", []string{"togglebutton_checked"}}, {"sound_only", false, true, "Power, toggle button", []string{"togglebutton_checked"}}, {"sound_only", true, false, "Power, pressed", []string{"button"}}, {"sound_only", false, false, "Power, toggle button, pressed", nil},