Progressbar fix.
This commit is contained in:
1 parent
6754c8ef6c
commit
9205e7280a
10 files changed
+266
-3
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# Progress reporting
|
||||
|
||||
Progress bars use the configured Off, Beep, Speak, or Both output mode and
|
||||
percentage thresholds. Background reporting is optional. Updates do not take
|
||||
focus or replace Braille for an unrelated object.
|
||||
|
||||
Native HTML `<progress>` and ARIA `progressbar` controls use their AT-SPI
|
||||
progress role and value events. Steam's shader dialog instead exposes changing
|
||||
static text such as `Processing Vulkan shaders (37%)`. The Steam webscript
|
||||
recognizes that English status under the verified `steamwebhelper` process and
|
||||
passes its percentage to the shared progress handler. Other text, containing
|
||||
sections, and ordinary browser processes do not use this exception. Translated
|
||||
Steam shader messages and other custom text-only progress widgets are not
|
||||
covered by this recognition.
|
||||
|
||||
## Automated checks
|
||||
|
||||
```sh
|
||||
GOCACHE=/tmp/cthulhu-go-cache go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1
|
||||
python3 tests/native_progress.py
|
||||
CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py
|
||||
```
|
||||
|
||||
The isolated GTK test checks foreground, background, and hidden progress. The
|
||||
isolated browser test drives real HTML and ARIA controls in a Chromium-family
|
||||
browser and verifies the resulting tone requests. Unit tests cover Steam text
|
||||
recognition, output modes, duplicate suppression, percentage thresholds,
|
||||
security, stale events, background settings, and unrelated Braille ownership.
|
||||
These checks do not establish audible output from the running reader.
|
||||
|
||||
## Live review before submission
|
||||
|
||||
Rebuild and restart the reader, then launch a Steam game that needs shader
|
||||
processing. With Beep selected and Steam in the foreground, confirm that pitch
|
||||
rises as the percentage increases without navigating onto the status text.
|
||||
With Speak or Both selected, verify the configured speech threshold. With Off
|
||||
selected, verify that automatic progress output stops.
|
||||
|
||||
Switch to another application during processing: reporting should follow the
|
||||
background-progress setting without moving focus or replacing that application's
|
||||
Braille. Check ordinary Steam navigation and notifications afterward. Open
|
||||
`tests/browser-progress.html` in a browser and activate Update progress to check
|
||||
native and ARIA bars. Physical keyboard, listening, and Braille checks remain
|
||||
separate acceptance gates.
|
||||
@@ -5,6 +5,7 @@ Cthulhu is a Linux screen reader written in Go.
|
||||
- [Repository Guidelines](AGENTS.md): layout, commands, and working safeguards.
|
||||
- [Contributing](CONTRIBUTING.md): development, accessibility verification, licensing, and the AI-assisted contribution policy.
|
||||
- [LibreOffice support](LIBREOFFICE_SUPPORT.md): integration boundaries, initial Calc behavior, remaining work, and verification.
|
||||
- [Progress reporting](PROGRESS_BARS.md): web progress bars, Steam shader processing, and verification.
|
||||
|
||||
AI-assisted contributions are welcome. Contributors remain responsible for human review and testing before submission; see the [full policy](CONTRIBUTING.md#ai-assisted-contributions).
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
"git.stormux.org/storm/cthulhu/internal/sound"
|
||||
"git.stormux.org/storm/cthulhu/internal/webscripts"
|
||||
)
|
||||
|
||||
type ProgressBarEntry struct {
|
||||
@@ -64,7 +65,8 @@ func (history *ProgressBarHistory) report(id atspi.ObjectID, percentage float64,
|
||||
// Handle progress before ordinary focused-object filtering. This path never
|
||||
// changes window ownership, native focus, review or browser mode.
|
||||
func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event) bool {
|
||||
if !isValueChangeEvent(event.Type) || event.Source.Role != "progress bar" {
|
||||
percentage, shaderProgress := webscripts.ShaderProgress(event.Source.ApplicationProcess, event.Type, event.Source.Role, event.Source.Name)
|
||||
if !shaderProgress && (!isValueChangeEvent(event.Type) || event.Source.Role != "progress bar") {
|
||||
return false
|
||||
}
|
||||
settings := config.DefaultSettings()
|
||||
@@ -75,10 +77,16 @@ func (handler *accessibilityEventHandler) handle_progress_bar(event atspi.Event)
|
||||
// NVDA falls back to ordinary focused-control value reporting here.
|
||||
return false
|
||||
}
|
||||
percentage, valid := presentation.ProgressPercentage(event.Source)
|
||||
if !valid {
|
||||
if shaderProgress && (event.Source.Defunct || event.Source.Indeterminate) {
|
||||
return false
|
||||
}
|
||||
if !shaderProgress {
|
||||
var valid bool
|
||||
percentage, valid = presentation.ProgressPercentage(event.Source)
|
||||
if !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
state := handler.state
|
||||
if handler.foreground != nil && event.Foreground.Epoch != state.foregroundState.Epoch {
|
||||
return true
|
||||
|
||||
@@ -6,6 +6,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
@@ -15,6 +16,73 @@ import (
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
)
|
||||
|
||||
func TestSteamShaderProgressUsesSharedOutputAndGates(t *testing.T) {
|
||||
for _, step := range []struct {
|
||||
name, mode string
|
||||
background, reportBackground bool
|
||||
hidden, defunct, locked, stale bool
|
||||
wantHandled, wantBeep, wantSpeech bool
|
||||
}{
|
||||
{name: "beep", mode: "beep", wantHandled: true, wantBeep: true},
|
||||
{name: "speak", mode: "speak", wantHandled: true, wantSpeech: true},
|
||||
{name: "both", mode: "both", wantHandled: true, wantBeep: true, wantSpeech: true},
|
||||
{name: "off", mode: "off"},
|
||||
{name: "background suppressed", mode: "both", background: true, wantHandled: true},
|
||||
{name: "background enabled", mode: "both", background: true, reportBackground: true, wantHandled: true, wantBeep: true, wantSpeech: true},
|
||||
{name: "hidden", mode: "both", hidden: true},
|
||||
{name: "defunct", mode: "both", defunct: true},
|
||||
{name: "secure", mode: "both", locked: true, wantHandled: true},
|
||||
{name: "stale", mode: "both", stale: true, wantHandled: true},
|
||||
} {
|
||||
t.Run(step.name, func(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.ProgressBar.OutputMode = step.mode
|
||||
settings.ProgressBar.ReportBackground = step.reportBackground
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)}
|
||||
frames := frameRecorder{frames: make(chan braille.Frame, 8)}
|
||||
state := &eventLoopState{}
|
||||
state.current = atspi.Snapshot{BusName: ":1.1", Path: "/button", Role: "button"}
|
||||
before := state.current
|
||||
gate := &focusGate{}
|
||||
gate.secure.Store(step.locked)
|
||||
handler := &accessibilityEventHandler{state: state, live: live, speech: speech, focusGate: gate, brailleOutput: frames}
|
||||
event := atspi.NewEvent(atspi.AccessibleNameEvent, atspi.Snapshot{BusName: ":1.1", Path: "/shader-status", Role: "static",
|
||||
ApplicationProcess: "steamwebhelper", StatesKnown: true, Visible: true, Showing: !step.hidden, Defunct: step.defunct})
|
||||
event.ForegroundMismatch = step.background
|
||||
if step.stale {
|
||||
handler.foreground = &titleProbeSource{}
|
||||
state.foregroundState.Epoch = 1
|
||||
}
|
||||
for _, percentage := range []int{18, 18, 19, 100} {
|
||||
event.Source.Name = fmt.Sprintf("Processing Vulkan shaders (%d%%)", percentage)
|
||||
if handled := handler.handle_progress_bar(event); handled != step.wantHandled {
|
||||
t.Fatalf("handled=%t, want %t", handled, step.wantHandled)
|
||||
}
|
||||
}
|
||||
wantCount := 0
|
||||
if step.wantBeep {
|
||||
wantCount = 3
|
||||
} else if step.wantSpeech {
|
||||
wantCount = 2
|
||||
}
|
||||
if len(speech.requests) != wantCount {
|
||||
t.Fatalf("got %d requests, want %d", len(speech.requests), wantCount)
|
||||
}
|
||||
if wantCount > 0 {
|
||||
request := <-speech.requests
|
||||
if !request.Append || (len(request.Sound) == 1) != step.wantBeep || (request.Text == "18 percent") != step.wantSpeech {
|
||||
t.Fatalf("shader output: %+v", request)
|
||||
}
|
||||
}
|
||||
if state.current.ID() != before.ID() || state.focusedID.Valid() || len(frames.frames) != 0 {
|
||||
t.Fatal("shader progress changed focus or unrelated braille")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressSecureEpochAndBrailleGates(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.ProgressBar.ReportBackground = true
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
// Standard web progress must reach the shared handler without Steam policy.
|
||||
func TestNativeBrowserProgress(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
|
||||
t.Skip("requires private browser session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
||||
defer cancel()
|
||||
adapter := atspi.New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab").CombinedOutput(); err != nil {
|
||||
t.Fatalf("fixture focus: %s, %v", output, err)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if event.Type == atspi.FocusedStateEvent && event.Detail1 == 1 && event.Source.Name == "Update progress" {
|
||||
goto focused
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("update button focus missing")
|
||||
}
|
||||
}
|
||||
focused:
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8)}
|
||||
handler := &accessibilityEventHandler{state: &eventLoopState{}, speech: speech}
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "space").CombinedOutput(); err != nil {
|
||||
t.Fatalf("update progress: %s, %v", output, err)
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for len(seen) < 2 {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if !isValueChangeEvent(event.Type) || event.Source.Role != "progress bar" {
|
||||
continue
|
||||
}
|
||||
wantTone := ""
|
||||
switch event.Source.Name {
|
||||
case "Native progress":
|
||||
wantTone = "generated-progress-tone:20"
|
||||
case "ARIA progress":
|
||||
wantTone = "generated-progress-tone:30"
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if seen[event.Source.Name] {
|
||||
continue
|
||||
}
|
||||
if !handler.handle_progress_bar(event) || len(speech.requests) != 1 {
|
||||
t.Fatalf("web progress was lost: %+v", event)
|
||||
}
|
||||
request := <-speech.requests
|
||||
if len(request.Sound) != 1 || request.Sound[0].Name != wantTone || !request.Append || request.Text != "" {
|
||||
t.Fatalf("web progress output: %+v", request)
|
||||
}
|
||||
seen[event.Source.Name] = true
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("web progress events missing; received %v", seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package webscripts
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -15,6 +16,23 @@ const steamNotificationDelay = 500 * time.Millisecond
|
||||
|
||||
var steamRelativeTime = regexp.MustCompile(`(?i)^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$`)
|
||||
|
||||
var steamShaderProgress = regexp.MustCompile(`^Processing Vulkan shaders \((\d{1,3})%\)$`)
|
||||
|
||||
// ShaderProgress recognizes Steam's English shader status, which Chromium
|
||||
// exposes as static text rather than an AT-SPI progress bar. Match the leaf
|
||||
// only: containing sections repeat this text alongside dialog controls.
|
||||
func ShaderProgress(processName, eventType, role, name string) (float64, bool) {
|
||||
if MatchProcess(processName) != Steam || eventType != "object:property-change:accessible-name" || role != "static" {
|
||||
return 0, false
|
||||
}
|
||||
match := steamShaderProgress.FindStringSubmatch(strings.TrimSpace(name))
|
||||
if match == nil {
|
||||
return 0, false
|
||||
}
|
||||
percentage, err := strconv.Atoi(match[1])
|
||||
return float64(percentage), err == nil && percentage <= 100
|
||||
}
|
||||
|
||||
// NotificationRoot mirrors Python's Steam root selection: notification/alert
|
||||
// ancestors take precedence over inherited live-region attributes on a leaf.
|
||||
// Candidates are ordered from the event source towards its application root.
|
||||
|
||||
@@ -10,6 +10,30 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSteamShaderProgress(t *testing.T) {
|
||||
for _, step := range []struct {
|
||||
process, eventType, role, name string
|
||||
percentage float64
|
||||
valid bool
|
||||
}{
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (0%)", 0, true},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 37, true},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (100%)", 100, true},
|
||||
{"brave", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 0, false},
|
||||
{"Chromium", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (37%)", 0, false},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "section", "Processing Vulkan shaders (37%) SKIP", 0, false},
|
||||
{"steamwebhelper", "object:state-changed:focused", "static", "Processing Vulkan shaders (37%)", 0, false},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Download (37%)", 0, false},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (101%)", 0, false},
|
||||
{"steamwebhelper", "object:property-change:accessible-name", "static", "Processing Vulkan shaders (-1%)", 0, false},
|
||||
} {
|
||||
percentage, valid := ShaderProgress(step.process, step.eventType, step.role, step.name)
|
||||
if valid != step.valid || valid && percentage != step.percentage {
|
||||
t.Fatalf("%+v: percentage=%g valid=%t", step, percentage, valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSteamNotificationMatchingAndRoot(t *testing.T) {
|
||||
message := &Notification{BusName: ":1.42", RootPath: "/toast", Text: "Username is now online"}
|
||||
for _, process := range []string{"Chromium", "chromium", "brave", "", "steamwebhelper.exe"} {
|
||||
|
||||
Vendored
+1
@@ -55,3 +55,4 @@ web-browse-braille-line,src/cthulhu/caret_navigation.py;src/cthulhu/scripts/web/
|
||||
web-explicit-link-names,src/cthulhu/scripts/web/script_utilities.py:_treatObjectAsWhole,web,Read explicitly named links as whole objects without losing descendant badges or ordinary link navigation,Storm Dragon,internal/atspi internal/review internal/app,in-progress,"go test -race ./...; CTHULHU_NATIVE_BROWSER_TESTS=namedlinks,browser,live,keys python3 tests/native_browser.py",Private Brave/Xorg named-link and key checks passed,,Fixture speech verified; real Steam listening pending,Named-link browse braille uses the full accessible label; physical check pending,docs/testing/steam-browser.md; actual Steam explicit-name exposure and game-card acceptance remain live gates
|
||||
steam-notifications,src/cthulhu/scripts/apps/steamwebhelper/script.py;tests/test_steam_notification_regressions.py,web,Coalesce verified Steam background toasts and present once through speech braille and notification history,Storm Dragon,internal/webscripts internal/atspi internal/app,in-progress,go test -race ./...; python3 tests/native_webscripts.py,Private GTK/AT-SPI process matching and alert-root capture passed,,Event-loop background delivery and Chromium isolation passed; real Steam toast pending,Event-loop flash verified; physical Steam toast display pending,docs/testing/steam-browser.md; independent root and bus queues deliberately prevent unrelated toast timestamps from merging
|
||||
libreoffice-calc-basic,Orca src/orca/scripts/apps/soffice/script.py; Orca src/orca/ax_utilities_table.py; Orca src/orca/speech_generator.py,libreoffice,Present Calc cells and recover logical focus around editing and sheet changes,Storm Dragon,internal/app internal/atspi internal/presentation,in-progress,GOCACHE=/tmp/cthulhu-go-cache go test -race ./internal/atspi ./internal/app ./internal/presentation -count=1; /usr/bin/python3 tests/native_libreoffice.py,Private real LibreOffice Calc 26.2.6.3 on Xvfb and a private AT-SPI bus validates cell movement blanks F2 typing Escape Enter and sheet changes.,,Recorded speech assertions pass; audible and physical-input acceptance pending.,Recorded value and coordinate frames pass; physical Braille and editor routing acceptance pending.,Current Orca is the primary Office reference. Dedicated application policy follows verified process identity after foreground/security gates; native capture remains bounded. Formulas range announcements dynamic headers full-row reading and Writer workflows remain open. See LIBREOFFICE_SUPPORT.md.
|
||||
steam-shader-progress,Steam shader dialog AT-SPI events,web,Report Steam Vulkan shader percentages through shared progress output,,internal/webscripts internal/app,in-progress,go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1; python3 tests/native_progress.py; CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py,Private GTK and Brave HTML/ARIA progress checks passed.,,Tone requests verified; live Steam listening pending.,Unrelated Braille ownership covered by unit tests; physical check pending.,English static shader-status name changes only; verified steamwebhelper identity; see PROGRESS_BARS.md.
|
||||
|
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<title>Cthulhu Browser Probe</title>
|
||||
<label>Probe start <input autofocus></label>
|
||||
<button id="update">Update progress</button>
|
||||
<label for="native">Native progress</label>
|
||||
<progress id="native" max="100" value="0"></progress>
|
||||
<div id="aria" role="progressbar" aria-label="ARIA progress"
|
||||
aria-valuemin="0" aria-valuemax="100" aria-valuenow="10"></div>
|
||||
<span id="shader">Processing Vulkan shaders (0%)</span>
|
||||
<script>
|
||||
document.getElementById('update').addEventListener('click', () => {
|
||||
document.getElementById('native').value = 20;
|
||||
document.getElementById('aria').setAttribute('aria-valuenow', '30');
|
||||
document.getElementById('shader').textContent = 'Processing Vulkan shaders (37%)';
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
@@ -10,6 +10,7 @@ import time
|
||||
|
||||
|
||||
BROWSER_PROOFS = {
|
||||
"progress": ("./internal/app", "^TestNativeBrowserProgress$", 30, 35, "browser-progress.html"),
|
||||
"fragments": ("./internal/review", "^TestNativeBrowserFragmentLines$", 30, 35, "browser-fragments.html"),
|
||||
"positions": ("./internal/atspi", "^TestNativeBrowserSetPositions$", 25, 30, "browser-positions.html"),
|
||||
"tabs": ("./internal/app", "^TestNativeBrowserTabs$", 30, 35),
|
||||
|
||||
Reference in new issue
Block a user