Fix panning sounds after scrolling.
This commit is contained in:
1 parent
b2f558c26c
commit
8e76d2341c
14 files changed
+338
-17
No files matched your search
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package review
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
type soundGeometryNavigator struct {
|
||||
*fakeDocumentNavigator
|
||||
geometry [2]atspi.SoundGeometry
|
||||
err error
|
||||
reads int
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (navigator *soundGeometryNavigator) CaptureDocumentSoundGeometry(_ context.Context, object, link atspi.ObjectReference) ([2]atspi.SoundGeometry, error) {
|
||||
navigator.reads++
|
||||
select {
|
||||
case placed := <-navigator.calls:
|
||||
if placed.position != object || link.ID() != webID("/topic_link") {
|
||||
navigator.t.Fatal("geometry refresh did not follow placement of the selected heading and link")
|
||||
}
|
||||
default:
|
||||
navigator.t.Fatal("geometry read before scrolling the target")
|
||||
}
|
||||
return navigator.geometry, navigator.err
|
||||
}
|
||||
|
||||
func TestDocumentSoundGeometryRefreshPreservesSemanticsAndPlacement(t *testing.T) {
|
||||
oldGeometry := atspi.SoundGeometry{ObjectKnown: true, Object: atspi.Rectangle{X: 900, Y: 3000, Width: 50, Height: 15}}
|
||||
freshGeometry := atspi.SoundGeometry{
|
||||
ObjectKnown: true, ReferenceKnown: true,
|
||||
Object: atspi.Rectangle{X: 100, Y: 300, Width: 50, Height: 15},
|
||||
Reference: atspi.Rectangle{Width: 1000, Height: 600},
|
||||
}
|
||||
for _, name := range []string{"fresh", "unavailable", "refresh failed", "placement failed"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
node := func(path, role string, children ...atspi.ObjectID) atspi.TreeNode {
|
||||
snapshot := atspi.NewSnapshot(path, "Topic", role, "", "")
|
||||
snapshot.BusName = webID(path).BusName
|
||||
snapshot.SoundGeometry = oldGeometry
|
||||
return atspi.TreeNode{Snapshot: snapshot, Children: children}
|
||||
}
|
||||
tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{
|
||||
node("/doc", "document web", webID("/heading")),
|
||||
node("/heading", "heading", webID("/topic_link")),
|
||||
node("/topic_link", "link"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursor, _ := NewDocumentCursor(tree)
|
||||
document, _ := tree.Root()
|
||||
heading, _ := tree.Reference(webID("/heading"))
|
||||
linkRef, _ := tree.Reference(webID("/topic_link"))
|
||||
snapshot, _ := tree.Resolve(heading)
|
||||
link, _ := tree.Resolve(linkRef)
|
||||
navigator := &soundGeometryNavigator{fakeDocumentNavigator: newFakeDocumentNavigator(), t: t}
|
||||
want := atspi.SoundGeometry{}
|
||||
switch name {
|
||||
case "fresh":
|
||||
navigator.geometry = [2]atspi.SoundGeometry{freshGeometry, freshGeometry}
|
||||
want = freshGeometry
|
||||
case "refresh failed":
|
||||
navigator.err = context.DeadlineExceeded
|
||||
case "placement failed":
|
||||
navigator.placementErr <- atspi.ErrStaleObject
|
||||
want = oldGeometry
|
||||
}
|
||||
worker, _ := NewWorker(navigator, time.Second)
|
||||
worker.SyncFocus(webID("/doc"))
|
||||
result := worker.placeDocument(context.Background(), cursor, Result{
|
||||
Moved: true, DocumentEpoch: worker.DocumentEpoch(), DocumentFocus: webID("/doc"),
|
||||
DocumentPosition: DocumentPosition{Document: document, Object: heading, Snapshot: snapshot, LinkedHeading: &link},
|
||||
})
|
||||
if result.Err != nil || !errors.Is(result.DocumentSoundErr, navigator.err) {
|
||||
t.Fatalf("sound refresh affected navigation: %+v", result)
|
||||
}
|
||||
if result.DocumentPosition.Snapshot.SoundGeometry != want || result.DocumentPosition.LinkedHeading.SoundGeometry != want {
|
||||
t.Fatalf("geometry after %s = %+v", name, result.DocumentPosition)
|
||||
}
|
||||
if link.SoundGeometry != oldGeometry {
|
||||
t.Fatal("refresh mutated the previously captured link snapshot")
|
||||
}
|
||||
cached, _ := tree.Resolve(heading)
|
||||
if !reflect.DeepEqual(cached, snapshot) {
|
||||
t.Fatal("refresh changed cached document semantics or geometry")
|
||||
}
|
||||
if name == "placement failed" && navigator.reads != 0 {
|
||||
t.Fatal("failed caret placement still queried sound geometry")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -488,5 +488,45 @@ func (worker *Worker) placeDocument(ctx context.Context, cursor *DocumentCursor,
|
||||
deadline, cancel := context.WithTimeout(ctx, worker.captureTimeout)
|
||||
defer cancel()
|
||||
result.CaretErr = navigator.SetDocumentPosition(deadline, result.DocumentPosition.Object, focus)
|
||||
if result.CaretErr == nil && worker.CurrentDocumentResult(result) {
|
||||
result = worker.refreshDocumentSoundGeometry(deadline, cursor, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (worker *Worker) refreshDocumentSoundGeometry(ctx context.Context, cursor *DocumentCursor, result Result) Result {
|
||||
reader, ok := worker.nativeCapturerFor(result.DocumentFocus).(interface {
|
||||
CaptureDocumentSoundGeometry(context.Context, atspi.ObjectReference, atspi.ObjectReference) ([2]atspi.SoundGeometry, error)
|
||||
})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
// Geometry belongs to this presentation, not the document's initial viewport.
|
||||
// Failed refreshes must not reuse an old position after scrolling or resizing.
|
||||
result.DocumentPosition.Snapshot.SoundGeometry = atspi.SoundGeometry{}
|
||||
link := atspi.ObjectReference{}
|
||||
if previous := result.DocumentPosition.LinkedHeading; previous != nil {
|
||||
copy := *previous
|
||||
copy.SoundGeometry = atspi.SoundGeometry{}
|
||||
result.DocumentPosition.LinkedHeading = ©
|
||||
if cursor == nil {
|
||||
result.DocumentSoundErr = atspi.ErrStaleObject
|
||||
return result
|
||||
}
|
||||
var err error
|
||||
link, err = cursor.tree.Reference(copy.ID())
|
||||
if err != nil {
|
||||
result.DocumentSoundErr = err
|
||||
return result
|
||||
}
|
||||
}
|
||||
geometry, err := reader.CaptureDocumentSoundGeometry(ctx, result.DocumentPosition.Object, link)
|
||||
result.DocumentSoundErr = err
|
||||
if err == nil {
|
||||
result.DocumentPosition.Snapshot.SoundGeometry = geometry[0]
|
||||
if result.DocumentPosition.LinkedHeading != nil {
|
||||
result.DocumentPosition.LinkedHeading.SoundGeometry = geometry[1]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -77,6 +77,7 @@ type Result struct {
|
||||
DocumentCaretContext []atspi.Snapshot
|
||||
DocumentBrailleLine *atspi.DocumentCaretText
|
||||
DocumentBrailleErr error
|
||||
DocumentSoundErr error
|
||||
DocumentBackwards bool
|
||||
DocumentLevel int
|
||||
DocumentWrapped bool
|
||||
|
||||
Reference in new issue
Block a user