Fixed a regression in wine title reading.
This commit is contained in:
1 parent
a121c26d63
commit
e0c8c6198c
3 files changed
+141
-2
No files matched your search
@@ -194,7 +194,13 @@ func processEventsWithAttributes(
|
||||
if live != nil && live.wineReview != nil {
|
||||
// Wine control revisions reset object review, but a pending title and
|
||||
// its duplicate history belong to the foreground window's lifetime.
|
||||
wineReview = &wineReviewFocusController{registry: live.wineReview, logger: logger, state: &state, foreground: foreground, titles: titles.source, gate: focusGate, worker: objectReview, clear: clearObjectFocus}
|
||||
// These notices carry metadata, not an instruction to stop speech.
|
||||
// Foreground switches and user input already own speech cancellation.
|
||||
clearWineReviewFocus := func() {
|
||||
ocrSession.Retire()
|
||||
state.clearObjectFocus(brailleOutput, browser, objectReview)
|
||||
}
|
||||
wineReview = &wineReviewFocusController{registry: live.wineReview, logger: logger, state: &state, foreground: foreground, titles: titles.source, gate: focusGate, worker: objectReview, clear: clearWineReviewFocus}
|
||||
wineReviewChanges = live.wineReview.Changes()
|
||||
wineReview.changed()
|
||||
defer wineReview.retire()
|
||||
|
||||
@@ -130,6 +130,13 @@ func (state *eventLoopState) ownedAutocompletePopup(event atspi.Event) bool {
|
||||
// clearOwnedFocus retires only the state cleared by the former event-loop
|
||||
// closure. Session presentation history and chrome load verification survive.
|
||||
func (state *eventLoopState) clearOwnedFocus(brailleOutput brailleFrameEnqueuer, browser *browserModeController, objectReview *review.Worker, speech speechEnqueuer) {
|
||||
state.clearObjectFocus(brailleOutput, browser, objectReview)
|
||||
speech.Interrupt()
|
||||
}
|
||||
|
||||
// Review metadata can change without new input or a foreground switch. Reset
|
||||
// its navigation state independently so title announcements keep playing.
|
||||
func (state *eventLoopState) clearObjectFocus(brailleOutput brailleFrameEnqueuer, browser *browserModeController, objectReview *review.Worker) {
|
||||
state.selection = textSelection{}
|
||||
state.documentSelection = textSelection{}
|
||||
state.lastClipboardSequence = 0
|
||||
@@ -169,7 +176,6 @@ func (state *eventLoopState) clearOwnedFocus(brailleOutput brailleFrameEnqueuer,
|
||||
if objectReview != nil {
|
||||
objectReview.ClearFocus()
|
||||
}
|
||||
speech.Interrupt()
|
||||
}
|
||||
|
||||
func (state *eventLoopState) syncForeground(foreground foregroundMonitor, logger *slog.Logger, clearOwnedFocus func()) {
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
"git.stormux.org/storm/cthulhu/internal/wine"
|
||||
)
|
||||
|
||||
@@ -20,6 +22,131 @@ type wineTitleLoopAdapter struct {
|
||||
*reviewFocusMonitor
|
||||
}
|
||||
|
||||
type changingWineTitleAdapter struct {
|
||||
wineTitleLoopAdapter
|
||||
mu sync.Mutex
|
||||
changes chan struct{}
|
||||
}
|
||||
|
||||
func (adapter *changingWineTitleAdapter) Foreground() atspi.ForegroundState {
|
||||
adapter.mu.Lock()
|
||||
defer adapter.mu.Unlock()
|
||||
return adapter.state
|
||||
}
|
||||
|
||||
func (adapter *changingWineTitleAdapter) ForegroundTitle() atspi.ForegroundTitle {
|
||||
adapter.mu.Lock()
|
||||
defer adapter.mu.Unlock()
|
||||
return adapter.title
|
||||
}
|
||||
|
||||
func (adapter *changingWineTitleAdapter) TitleChanges() <-chan struct{} { return adapter.changes }
|
||||
|
||||
func TestPrivateWineReviewUpdatesPreserveQueuedTitleSpeech(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_WINE_TITLE_DBUS") != "1" {
|
||||
t.Skip("requires a private D-Bus session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
registry := wine.NewReviewRegistry(logger)
|
||||
if err := registry.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registry.Stop(context.Background())
|
||||
_, fixture, monitor, reviewWorker := reviewFocusSetup(t)
|
||||
notice := fixture.notices[0]
|
||||
if !registry.RegisterProvider(notice.Sender, wine.ReviewObjectPath, wine.ReviewProtocolVersion) || !registry.PresentFocus(notice.Sender, notice) {
|
||||
t.Fatal("could not register private Wine focus")
|
||||
}
|
||||
adapter := &changingWineTitleAdapter{wineTitleLoopAdapter: wineTitleLoopAdapter{fakeAdapter: fakeAdapter{events: make(chan atspi.Event)}, reviewFocusMonitor: monitor}, changes: make(chan struct{}, 1)}
|
||||
live := &liveSettings{wineReview: registry}
|
||||
live.Store(config.DefaultSettings())
|
||||
speech := newSpeechWorker(logger, &fakeSpeaker{spoken: make(chan string, 4)})
|
||||
speech.running = true
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- processEventsWithInput(ctx, logger, speech, adapter, nil, reviewWorker, 0, live, nil, nil, nil)
|
||||
}()
|
||||
defer func() {
|
||||
cancel()
|
||||
if err := <-done; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
wait := func(check func() bool) {
|
||||
t.Helper()
|
||||
for !check() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Wine title replay timed out")
|
||||
case <-time.After(time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
wait(func() bool { return reviewWorker.Focus() == notice.ID() })
|
||||
speech.mutex.Lock()
|
||||
initiallyInterrupted := speech.generation != 0
|
||||
speech.mutex.Unlock()
|
||||
if initiallyInterrupted {
|
||||
t.Fatal("Wine review focus reset requested speech cancellation")
|
||||
}
|
||||
var initial speechRequest
|
||||
wait(func() bool { var ok bool; initial, ok = speech.next(); return ok })
|
||||
if err := speech.deliver(ctx, initial); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, title := range []string{"East", "In front of the safe zone", "Leaving in front of the safe zone"} {
|
||||
// The native caption changes before the helper's next metadata notice.
|
||||
// The old notice loses eligibility, then a new revision reclaims focus.
|
||||
adapter.mu.Lock()
|
||||
adapter.title.Title.Title = title
|
||||
adapter.mu.Unlock()
|
||||
adapter.changes <- struct{}{}
|
||||
wait(func() bool { return !reviewWorker.Focus().Valid() })
|
||||
speech.mutex.Lock()
|
||||
interrupted := speech.generation != 0
|
||||
speech.mutex.Unlock()
|
||||
if interrupted {
|
||||
t.Fatal("Wine metadata retirement canceled title speech")
|
||||
}
|
||||
notice.Title = title
|
||||
notice.Revision++
|
||||
if !registry.PresentFocus(notice.Sender, notice) {
|
||||
t.Fatal("could not update Wine metadata")
|
||||
}
|
||||
wait(func() bool { return reviewWorker.Focus() == notice.ID() })
|
||||
wait(func() bool {
|
||||
speech.mutex.Lock()
|
||||
defer speech.mutex.Unlock()
|
||||
return len(speech.pending) > 0 && speech.pending[len(speech.pending)-1].Text == title
|
||||
})
|
||||
if speech.InterruptForInput(input.Event{Name: "Up", Pressed: true, Repeat: true}) {
|
||||
t.Fatal("metadata update removed held-key protection")
|
||||
}
|
||||
}
|
||||
speech.mutex.Lock()
|
||||
generation := speech.generation
|
||||
count := len(speech.pending)
|
||||
speech.mutex.Unlock()
|
||||
if generation != 0 || count != 3 {
|
||||
t.Fatalf("metadata interrupted titles: generation=%d pending=%d", generation, count)
|
||||
}
|
||||
for _, want := range []string{"East", "In front of the safe zone", "Leaving in front of the safe zone"} {
|
||||
request, ok := speech.next()
|
||||
if !ok || request.Text != want || !request.Append {
|
||||
t.Fatalf("queued title = %+v, want %q", request, want)
|
||||
}
|
||||
if err := speech.deliver(ctx, request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !speech.InterruptForInput(input.Event{Name: "Down", Pressed: true}) {
|
||||
t.Fatal("fresh press failed to stop speech")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateWineTitleSurvivesReviewFocusBurst(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_WINE_TITLE_DBUS") != "1" {
|
||||
t.Skip("requires a private D-Bus session")
|
||||
|
||||
Reference in new issue
Block a user