206 lines
9.1 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|