Files
cthulhu/internal/presentation/control_values_test.go

206 lines
9.1 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// 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 presentation
import (
"testing"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
func TestFocusTextPresentsControlStates(t *testing.T) {
tests := []struct {
name string
snapshot atspi.Snapshot
want string
}{
{name: "unchecked checkbox", snapshot: atspi.Snapshot{Name: "Updates", Role: "check box"}, want: "Updates, check box, not checked"},
{name: "checked checkbox", snapshot: atspi.Snapshot{Name: "Updates", Role: "check box", Checked: true}, want: "Updates, check box, checked"},
{name: "mixed checkbox", snapshot: atspi.Snapshot{Name: "Updates", Role: "check box", Indeterminate: true}, want: "Updates, check box, partially checked"},
{name: "selected radio", snapshot: atspi.Snapshot{Name: "Automatic", Role: "radio button", Checked: true}, want: "Automatic, radio button, selected"},
{name: "radio group", snapshot: atspi.Snapshot{Name: "Some", Role: "radio button", RadioGroupName: "Punctuation Level", Checked: true}, want: "Punctuation Level, Some, radio button, selected"},
{name: "switch off", snapshot: atspi.Snapshot{Name: "Network", Role: "switch"}, want: "Network, switch, off"},
{name: "toggle pressed", snapshot: atspi.Snapshot{Name: "Bold", Role: "toggle button", Checked: true}, want: "Bold, toggle button, pressed"},
{name: "collapsed item", snapshot: atspi.Snapshot{Name: "Documents", Role: "tree item", Expandable: true}, want: "Documents, tree item, collapsed"},
{name: "expanded selected item", snapshot: atspi.Snapshot{Name: "Documents", Role: "tree item", Expandable: true, Expanded: true, Selected: true}, want: "Documents, tree item, expanded, selected"},
{name: "selected menu item", snapshot: atspi.Snapshot{Name: "Open", Role: "menu item", Selected: true}, want: "Open, menu item"},
{name: "selected table cell", snapshot: atspi.Snapshot{Name: "First post", Role: "table cell", Selected: true}, want: "First post"},
{name: "field requirements", snapshot: atspi.Snapshot{Name: "Account", Role: "text", ReadOnly: true, Required: true, Invalid: true}, want: "Account, text, read only, required, invalid entry"},
{name: "disabled control", snapshot: atspi.Snapshot{Name: "Apply", Role: "push button", SensitiveKnown: true}, want: "Apply, push button, grayed"},
{name: "unknown sensitivity", snapshot: atspi.Snapshot{Name: "Apply", Role: "push button"}, want: "Apply, push button"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := FocusText(test.snapshot); got != test.want {
t.Fatalf("FocusText() = %q, want %q", got, test.want)
}
})
}
}
func TestEventTextPresentsOnlyChangedState(t *testing.T) {
tests := []struct {
name string
event atspi.Event
want string
}{
{name: "checked", event: atspi.NewEvent("object:state-changed:checked", atspi.Snapshot{Name: "Updates", Role: "check box", Checked: true}), want: "checked"},
{name: "expanded", event: atspi.NewEvent("object:state-changed:expanded", atspi.Snapshot{Name: "Files", Role: "tree item", Expanded: true}), want: "expanded"},
{name: "menu selection omitted", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "Open", Role: "menu item", Selected: true}), want: ""},
{name: "tree selection", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "Files", Role: "tree item", Selected: true}), want: "selected"},
{name: "table cell selected", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "First post", Role: "table cell", Selected: true}), want: "selected"},
{name: "table cell unselected", event: atspi.NewEvent("object:state-changed:selected", atspi.Snapshot{Name: "First post", Role: "table cell"}), want: ""},
{name: "available omitted", event: atspi.NewEvent("object:state-changed:sensitive", atspi.Snapshot{Name: "Save", Role: "push button", SensitiveKnown: true, Sensitive: true}), want: ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := EventText(test.event); got != test.want {
t.Fatalf("EventText() = %q, want %q", got, test.want)
}
})
}
}
func TestEventTextPresentsValueWithoutRepeatingControl(t *testing.T) {
snapshot := atspi.Snapshot{Name: "Volume", Role: "slider", ValueKnown: true,
CurrentValue: 42, MinimumValue: 0, MaximumValue: 100}
event := atspi.NewEvent(atspi.ValueChangedEvent, snapshot)
if got := EventText(event); got != "42" {
t.Fatalf("EventText() = %q, want %q", got, "42")
}
}
func TestFocusTextPresentsControlValues(t *testing.T) {
tests := []struct {
name string
snapshot atspi.Snapshot
want string
}{
{
name: "application value text",
snapshot: atspi.Snapshot{Name: "Volume", Role: "slider", ValueKnown: true,
CurrentValue: 50, MinimumValue: 0, MaximumValue: 100, ValueText: "medium"},
want: "Volume, slider, medium",
},
{
name: "combo selected item",
snapshot: atspi.Snapshot{Name: "Time format", Role: "combo box", ValueText: "24-hour"},
want: "Time format, combo box, 24-hour",
},
{
name: "spin button actual value from nonzero range",
snapshot: atspi.Snapshot{Name: "Zoom", Role: "spin button", ValueKnown: true,
CurrentValue: 150, MinimumValue: 100, MaximumValue: 200},
want: "Zoom, spin button, 150",
},
{
name: "rounded percentage",
snapshot: atspi.Snapshot{Name: "Download", Role: "progress bar", ValueKnown: true,
CurrentValue: 1, MinimumValue: 0, MaximumValue: 3},
want: "Download, progress bar, 33 percent",
},
{
name: "unknown value",
snapshot: atspi.Snapshot{Name: "Volume", Role: "slider",
CurrentValue: 50, MinimumValue: 0, MaximumValue: 100},
want: "Volume, slider",
},
{
name: "actual value remains available for a fixed range",
snapshot: atspi.Snapshot{Name: "Volume", Role: "slider", ValueKnown: true,
CurrentValue: 50, MinimumValue: 100, MaximumValue: 100},
want: "Volume, slider, 50",
},
{
name: "value excluded from unrelated role",
snapshot: atspi.Snapshot{Name: "Save", Role: "push button", ValueKnown: true,
CurrentValue: 50, MinimumValue: 0, MaximumValue: 100, ValueText: "medium"},
want: "Save, push button",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := FocusText(test.snapshot); got != test.want {
t.Fatalf("FocusText() = %q, want %q", got, test.want)
}
})
}
}
func TestNumericValueUsesProviderPrecision(t *testing.T) {
for _, test := range []struct {
name string
value, increment float64
want string
}{
{"integer", 150, 1, "150"},
{"hundredths", 1.25, 0.01, "1.25"},
{"quarter step", 1.25, 0.25, "1.2"},
{"zero increment fractional", 0.5, 0, "0.5"},
{"negative fractional", -0.5, 0, "-0.5"},
{"invalid increment", 1, -1, ""},
} {
t.Run(test.name, func(t *testing.T) {
for _, role := range []string{"slider", "spin button", "dial", "scroll bar", "split pane"} {
snapshot := atspi.Snapshot{Role: role, ValueKnown: true, CurrentValue: test.value,
MinimumIncrement: test.increment, MinimumValue: -100, MaximumValue: 200}
if got := EventText(atspi.NewEvent(atspi.ValueChangedEvent, snapshot)); got != test.want {
t.Fatalf("%s: value = %q, want %q", role, got, test.want)
}
}
})
}
}
func TestSliderFocusUsesCurrentValueInsteadOfStaleText(t *testing.T) {
for _, tc := range []struct {
name, text, valueText, description, want string
valueKnown bool
}{
{"previous value", "4.0", "", "", "4.5", true},
{"previous tenth", "4.4", "", "", "4.5", true},
{"same value different formatting", "4.50", "", "", "4.5", true},
{"provider value text", "4.0", "medium", "", "medium", true},
{"description", "4.0", "", "Voice pitch", "4.5, Voice pitch", true},
{"text without value", "4.0", "", "", "4.0", false},
} {
t.Run(tc.name, func(t *testing.T) {
snapshot := atspi.Snapshot{Name: "Pitch:", Role: "slider", Text: tc.text,
ValueKnown: tc.valueKnown, CurrentValue: 4.5, MinimumIncrement: 0.1,
ValueText: tc.valueText, Description: tc.description}
if got := FocusText(snapshot); got != "Pitch:, slider, "+tc.want {
t.Fatalf("focus speech = %q", got)
}
options := DefaultOptions()
reading := reading_options(options)
reading.OnlySpeakDisplayedText = true
options.Reading = &reading
want := tc.want
if tc.description != "" {
want = "4.5"
}
if got := FocusTextWithOptions(snapshot, options); got != "Pitch: "+want {
t.Fatalf("displayed-only speech = %q", got)
}
})
}
}