150 lines
6.2 KiB
Go
150 lines
6.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
"git.stormux.org/storm/cthulhu/internal/review"
|
|
)
|
|
|
|
func where_am_i_observation_handler(t *testing.T, privacy string) (*eventReviewHandler, *fakeSpeechQueue) {
|
|
t.Helper()
|
|
// Use owned-tree ancestry, as document movement does, rather than treating
|
|
// the presentation's filtered SpeechContext as a complete source capture.
|
|
position := review_observation_result(t, "object", privacy).Position
|
|
focus := atspi.NewObjectID(":1.42", "/document")
|
|
worker, err := review.NewWorker(&fakeAdapter{}, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
worker.SyncFocus(focus)
|
|
queue := &fakeSpeechQueue{requests: make(chan speechRequest, 4), spoken: make(chan string, 4)}
|
|
handler := &eventReviewHandler{state: &eventLoopState{}, objectReview: worker, speech: queue,
|
|
logger: slog.New(slog.DiscardHandler), focusGate: &focusGate{}, syncForeground: func() {}}
|
|
handler.state.current = atspi.Snapshot{BusName: focus.BusName, Path: focus.Path, Role: "document web"}
|
|
handler.state.presentationOptions = presentation.DefaultOptions()
|
|
handler.handle(review.Result{Moved: true, DocumentCaretCommand: review.CaretNextLine, DocumentFocus: focus,
|
|
DocumentPosition: review.DocumentPosition{Snapshot: position.Current}, DocumentCaretContext: position.SourceContext,
|
|
DocumentCaretText: position.Current.Name})
|
|
<-queue.requests
|
|
<-queue.spoken
|
|
return handler, queue
|
|
}
|
|
|
|
func TestRemoteWhereAmIInvalidatesSource(t *testing.T) {
|
|
for _, change := range []string{"same-object-update", "say-all-position", "focus", "epoch", "foreground", "missing-owner", "no-location"} {
|
|
t.Run(change, func(t *testing.T) {
|
|
handler, queue := where_am_i_observation_handler(t, "ordinary")
|
|
switch change {
|
|
case "same-object-update":
|
|
// Even identical native text cannot reuse older capture ancestry.
|
|
events := accessibilityEventHandler{state: handler.state, objectReview: handler.objectReview, logger: handler.logger}
|
|
events.acceptFocusedEvent(atspi.NewEvent(atspi.FocusEvent, handler.state.current), input.Activity{})
|
|
case "say-all-position":
|
|
handler.handle(review.Result{ReadAllPosition: true, DocumentFocus: handler.objectReview.Focus(),
|
|
DocumentPosition: review.DocumentPosition{Snapshot: handler.state.current}})
|
|
case "focus":
|
|
handler.objectReview.SyncFocus(atspi.NewObjectID(":1.42", "/other"))
|
|
case "epoch":
|
|
handler.objectReview.ResetDocument(handler.objectReview.Focus())
|
|
case "foreground":
|
|
handler.syncForeground = func() { handler.state.setCurrent(handler.state.current) }
|
|
case "missing-owner":
|
|
handler.objectReview = nil
|
|
case "no-location":
|
|
handler.state.current.Name, handler.state.current.Role = "", ""
|
|
}
|
|
handler.whereAmI()
|
|
request := <-queue.requests
|
|
if exportable_speech(request) {
|
|
t.Fatal("Where Am I reused retired source classification")
|
|
}
|
|
if change != "no-location" && !strings.Contains(request.Text, "Review colour.") {
|
|
t.Fatal("source invalidation changed local reading", request.Text)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoteWhereAmIRejectsMismatchedSource(t *testing.T) {
|
|
handler, queue := where_am_i_observation_handler(t, "ordinary")
|
|
current := handler.state.current
|
|
handler.state.setCurrentDocument(review.Result{DocumentFocus: handler.objectReview.Focus(),
|
|
DocumentPosition: review.DocumentPosition{Snapshot: current},
|
|
DocumentCaretContext: []atspi.Snapshot{{BusName: current.BusName, Path: "/other", Role: "static"}}})
|
|
handler.whereAmI()
|
|
if exportable_speech(<-queue.requests) {
|
|
t.Fatal("unrelated ancestry authorized Where Am I export")
|
|
}
|
|
}
|
|
|
|
func TestRemoteWhereAmISecureFocus(t *testing.T) {
|
|
for _, mode := range []string{"lock", "handoff", "foreground-handoff"} {
|
|
handler, queue := where_am_i_observation_handler(t, "ordinary")
|
|
if mode == "lock" {
|
|
handler.focusGate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{})
|
|
} else if mode == "foreground-handoff" {
|
|
handler.syncForeground = func() { handler.focusGate.SetSuspended(true) }
|
|
} else {
|
|
handler.focusGate.SetSuspended(true)
|
|
}
|
|
handler.whereAmI()
|
|
if len(queue.requests) != 0 {
|
|
t.Fatal("Where Am I spoke during lock or handoff")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRemoteWhereAmIResolvedOutput(t *testing.T) {
|
|
worker, session, _, _ := observation_fixture(t)
|
|
worker.textTransform = func(text string) string { return strings.ReplaceAll(text, "colour", "color") }
|
|
for _, privacy := range []string{"ordinary", "snapshot-ancestor", "missing", "stale"} {
|
|
request := where_am_i_observation_request(t, privacy)
|
|
if err := worker.EnqueueAndWait(t.Context(), request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for len(worker.speaker.(*observationSpeaker).spoken) > 0 {
|
|
want := <-worker.speaker.(*observationSpeaker).spoken
|
|
if privacy == "ordinary" {
|
|
value := observed_speech(t, session)
|
|
if value.Kind != "speak" || len(value.Parts) != 2 || value.Parts[0].Text != want || !value.Parts[1].End {
|
|
t.Fatal("Where Am I export differed from resolved local chunks", value, want)
|
|
}
|
|
}
|
|
}
|
|
worker.InterruptForUser()
|
|
if value := observed_speech(t, session); value.Kind != "cancel" {
|
|
t.Fatal("private Where Am I reached the child", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func where_am_i_observation_request(t *testing.T, privacy string) speechRequest {
|
|
t.Helper()
|
|
handler, queue := where_am_i_observation_handler(t, privacy)
|
|
handler.whereAmI()
|
|
return <-queue.requests
|
|
}
|
|
|
|
func TestRemoteWhereAmISourcePrivacy(t *testing.T) {
|
|
for _, privacy := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor", "unknown", "unknown-role", "invalid", "defunct", "unknown-ancestor", "unknown-snapshot-ancestor", "missing", "stale"} {
|
|
t.Run(privacy, func(t *testing.T) {
|
|
request := where_am_i_observation_request(t, privacy)
|
|
if exportable_speech(request) != (privacy == "ordinary") {
|
|
t.Fatalf("privacy %s produced exportable=%v", privacy, exportable_speech(request))
|
|
}
|
|
if privacy != "password" && !strings.Contains(request.Text, "Review colour.") {
|
|
t.Fatal("source privacy changed local Where Am I", request.Text)
|
|
}
|
|
})
|
|
}
|
|
}
|