libreoffice support started, still broken. Some updates to how wine-access gets called.
This commit is contained in:
1 parent
8e76d2341c
commit
7f78177786
26 files changed
+1173
-21
No files matched your search
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import "git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
|
||||
type applicationEventResult struct {
|
||||
event atspi.Event
|
||||
adoptFocus bool
|
||||
suppress bool
|
||||
resetPresentation bool
|
||||
}
|
||||
|
||||
// prepareApplicationEvent runs after security/foreground checks and before
|
||||
// generic focus and text filters. Application policy receives copied semantics;
|
||||
// the reader retains ownership, review, cancellation, and output services.
|
||||
// The first handler is stateless: document-specific state can be owned here
|
||||
// when a concrete feature needs it, rather than hidden in native callbacks.
|
||||
func (handler *accessibilityEventHandler) prepareApplicationEvent(event atspi.Event) (atspi.Event, bool) {
|
||||
result := applicationEventResult{event: event}
|
||||
if atspi.IsLibreOfficeProcess(event.Source.ApplicationProcess) {
|
||||
result = prepareLibreOfficeEvent(event, handler.state.current, handler.state.owner)
|
||||
}
|
||||
if result.adoptFocus {
|
||||
if handler.focusGate.Suspended() {
|
||||
return event, false
|
||||
}
|
||||
state := handler.state
|
||||
if !state.owner.follow(result.event) {
|
||||
return event, true
|
||||
}
|
||||
state.pending.caret.clear()
|
||||
state.focusedID = result.event.Source.ID()
|
||||
state.current = result.event.Source
|
||||
state.managedContainerID, state.managedDescendantID = atspi.ObjectID{}, atspi.ObjectID{}
|
||||
if result.event.Source.Role != "table" && result.event.Source.Role != "table cell" {
|
||||
// A late-created editor has no ordinary focus event to reset
|
||||
// row history. Returning to its old cell is a fresh focus.
|
||||
state.lastTable, state.lastTableCell = atspi.ObjectID{}, atspi.ObjectID{}
|
||||
}
|
||||
state.selection.reset(result.event.Source)
|
||||
if handler.objectReview != nil {
|
||||
handler.objectReview.SyncFocus(result.event.Source.ID())
|
||||
}
|
||||
}
|
||||
if result.resetPresentation {
|
||||
handler.state.pending.caret.clear()
|
||||
handler.state.lastSpokenPath, handler.state.lastSpokenText, handler.state.lastSound = "", "", ""
|
||||
}
|
||||
if result.suppress {
|
||||
handler.logger.Debug("skipped application-specific accessibility event", "type", event.Type,
|
||||
"application_process", event.Source.ApplicationProcess, "role", event.Source.Role)
|
||||
}
|
||||
return result.event, result.suppress
|
||||
}
|
||||
@@ -160,6 +160,9 @@ func brailleFocusFrameWithOptions(s atspi.Snapshot, options config.BrailleOption
|
||||
}
|
||||
|
||||
func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, configured ...config.TextAttributeSettings) braille.Frame {
|
||||
if frame, handled := libreOfficeBrailleFrame(s, options); handled {
|
||||
return frame
|
||||
}
|
||||
name, role := strings.TrimSpace(s.Name), s.Role
|
||||
if role == "terminal" || brailleTextRole(role) && s.TextCaretKnown {
|
||||
label := name
|
||||
|
||||
@@ -135,6 +135,11 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
|
||||
}
|
||||
return
|
||||
}
|
||||
var applicationSuppressed bool
|
||||
event, applicationSuppressed = handler.prepareApplicationEvent(event)
|
||||
if applicationSuppressed {
|
||||
return
|
||||
}
|
||||
if handler.handleLifecycle(event) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
)
|
||||
|
||||
// prepareLibreOfficeEvent follows current Orca's soffice focus/caret policy.
|
||||
// A Calc active cell is the logical focus even when editing destroyed the old
|
||||
// focus or a sheet switch changed its managed container (LO bug 161444).
|
||||
func prepareLibreOfficeEvent(event atspi.Event, current atspi.Snapshot, owner windowOwner) applicationEventResult {
|
||||
result := applicationEventResult{event: event}
|
||||
context := event.Source.LibreOffice
|
||||
if !atspi.IsLibreOfficeProcess(event.Source.ApplicationProcess) ||
|
||||
event.Source.Defunct || !event.Source.ID().Valid() ||
|
||||
owner.window.Valid() && (eventWindow(event) != owner.window || event.Source.BusName != owner.applicationBus) {
|
||||
return result
|
||||
}
|
||||
if isFocusGainEvent(event) && event.Type != atspi.WindowActivatedEvent &&
|
||||
current.LibreOffice != nil && current.LibreOffice.InSpreadsheet {
|
||||
// LibreOffice also claims focus for ancestors of the real focused
|
||||
// object. Orca ignores these; copied ancestry is enough to identify
|
||||
// them without another native tree query.
|
||||
for _, ancestor := range current.BrailleContext {
|
||||
if ancestor.ID().Valid() && ancestor.ID() == event.Source.ID() {
|
||||
result.suppress = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
if context != nil && context.InSpreadsheet && context.Cell && event.Source.Role == "table cell" &&
|
||||
event.Type == atspi.ActiveDescendantEvent &&
|
||||
(event.Source.Focused || context.ContainerFocused) {
|
||||
// Preserve NativeType/OriginPath for diagnostics. Ordinary focus
|
||||
// handling now owns review, duplicate suppression, and Braille.
|
||||
if result.event.NativeType == "" {
|
||||
result.event.NativeType = event.Type
|
||||
}
|
||||
result.event.Type = atspi.FocusEvent
|
||||
}
|
||||
if context != nil && context.InSpreadsheet && event.Type == atspi.SelectionChangedEvent {
|
||||
// Selection can name the first cell of a range rather than the
|
||||
// active cell. Movement follows active-descendant events; range
|
||||
// presentation belongs to the later selection-support slice.
|
||||
result.suppress = true
|
||||
}
|
||||
if context != nil && context.InSpreadsheet && event.Source.Role == "table" &&
|
||||
isFocusGainEvent(event) && event.Type != atspi.WindowActivatedEvent &&
|
||||
current.LibreOffice != nil &&
|
||||
(current.LibreOffice.CellEditor || current.LibreOffice.Cell && current.TablePath == event.Source.Path) {
|
||||
// Orca treats this as the sheet reclaiming focus after editing.
|
||||
// Own it silently; the following active cell supplies presentation.
|
||||
result.adoptFocus, result.suppress = true, true
|
||||
}
|
||||
if context != nil && context.Cell && isFocusGainEvent(result.event) && current.ID() != event.Source.ID() {
|
||||
// Returning to the cell after typing must speak it even if its
|
||||
// value is unchanged; typing uses a separate presentation history.
|
||||
result.resetPresentation = true
|
||||
}
|
||||
if context != nil && context.CellEditor && event.Source.Role == "paragraph" &&
|
||||
(isTextChangeEvent(event.Type) || event.Type == atspi.CaretEvent) &&
|
||||
owner.window.Valid() && event.Source.ID() != current.ID() &&
|
||||
(event.Source.Focused || context.EditorParent == current.ID() && current.LibreOffice != nil && current.LibreOffice.CellEditor) {
|
||||
// Empty-cell F2 can focus a panel before its paragraph exists.
|
||||
// Admit that editor's first mutation before generic text filtering.
|
||||
result.adoptFocus = true
|
||||
}
|
||||
if event.Type == atspi.CaretEvent && current.LibreOffice != nil && current.LibreOffice.Cell &&
|
||||
(context == nil || !context.CellEditor) {
|
||||
// Formula-bar/background caret events must not displace the cell.
|
||||
result.suppress = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func libreOfficeBrailleFrame(snapshot atspi.Snapshot, options config.BrailleOptions) (braille.Frame, bool) {
|
||||
context := snapshot.LibreOffice
|
||||
if !atspi.IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Role != "table cell" ||
|
||||
context == nil || !context.InSpreadsheet || !context.Cell {
|
||||
return braille.Frame{}, false
|
||||
}
|
||||
// Orca presents the value followed by its cell address. This is a
|
||||
// component, not an editable-text route; cell focus has no text caret.
|
||||
return braille.Frame{Text: brailleJoin(context.CellText, context.Coordinates), Cursor: 0,
|
||||
WordWrap: options.WordWrap}, true
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
)
|
||||
|
||||
type calcFrameQueue struct{ frames chan braille.Frame }
|
||||
|
||||
func (queue calcFrameQueue) EnqueueFrame(frame braille.Frame) { queue.frames <- frame }
|
||||
|
||||
// This runs the production native adapter and event loop against real Calc.
|
||||
// Speech/Braille are recorded queues, not audible or physical display proof.
|
||||
func TestNativeLibreOfficeCalc(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_LIBREOFFICE") != "1" {
|
||||
t.Skip("requires tests/native_libreoffice.py")
|
||||
}
|
||||
window := os.Getenv("CTHULHU_CALC_WINDOW")
|
||||
if window == "" {
|
||||
t.Fatal("Calc window missing")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second)
|
||||
defer cancel()
|
||||
adapter := atspi.New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer stopCancel()
|
||||
if err := adapter.Stop(stopCtx); err != nil {
|
||||
t.Errorf("stop adapter: %v", err)
|
||||
}
|
||||
}()
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 256)}
|
||||
frames := make(chan braille.Frame, 256)
|
||||
done := make(chan error, 1)
|
||||
var logBuffer bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
options := presentation.DefaultOptions()
|
||||
options.EnableEchoByCharacter = true
|
||||
go func() {
|
||||
done <- processEvents(ctx, logger, outputQueue{speech: speech, braille: calcFrameQueue{frames}}, adapter, 0, options)
|
||||
}()
|
||||
defer func() {
|
||||
cancel()
|
||||
<-done
|
||||
if t.Failed() {
|
||||
t.Log(logBuffer.String())
|
||||
}
|
||||
}()
|
||||
press := func(keys ...string) {
|
||||
t.Helper()
|
||||
args := append([]string{"key", "--clearmodifiers"}, keys...)
|
||||
if err := exec.CommandContext(ctx, "xdotool", args...).Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
waitSpeech := func(want string) {
|
||||
t.Helper()
|
||||
timeout := time.NewTimer(5 * time.Second)
|
||||
defer timeout.Stop()
|
||||
for {
|
||||
select {
|
||||
case got := <-speech.spoken:
|
||||
t.Logf("Calc speech: %q", got)
|
||||
if strings.Contains(got, "1048576") || strings.Contains(got, "grayed") {
|
||||
t.Fatalf("Calc capacity/state chatter: %q", got)
|
||||
}
|
||||
if got == want {
|
||||
return
|
||||
}
|
||||
case <-timeout.C:
|
||||
t.Fatalf("missing Calc speech %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
waitFrame := func(suffix string) {
|
||||
t.Helper()
|
||||
timeout := time.NewTimer(5 * time.Second)
|
||||
defer timeout.Stop()
|
||||
for {
|
||||
select {
|
||||
case frame := <-frames:
|
||||
if strings.HasSuffix(frame.Text, suffix) {
|
||||
return
|
||||
}
|
||||
case <-timeout.C:
|
||||
t.Fatalf("missing Calc Braille ending %q", suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
enterEditor := func(coordinate string) {
|
||||
t.Helper()
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case <-frames:
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
press("F2")
|
||||
timeout := time.NewTimer(5 * time.Second)
|
||||
defer timeout.Stop()
|
||||
for {
|
||||
select {
|
||||
case frame := <-frames:
|
||||
if strings.Contains(frame.Text, "Cell "+coordinate) {
|
||||
return
|
||||
}
|
||||
case <-timeout.C:
|
||||
t.Fatal("Calc did not expose its temporary cell editor")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Focus once. Refocusing before every key sequence can hide failures
|
||||
// to retain or recover Calc's document focus during normal keyboard use.
|
||||
if err := exec.CommandContext(ctx, "xdotool", "windowfocus", window).Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
press("Escape", "ctrl+Home", "Right")
|
||||
waitSpeech("42, B1")
|
||||
waitFrame("42 B1")
|
||||
press("Down")
|
||||
waitSpeech("Beta, B2")
|
||||
press("Left")
|
||||
waitSpeech("blank, A2")
|
||||
waitFrame("A2")
|
||||
enterEditor("A2")
|
||||
press("x")
|
||||
waitSpeech("x")
|
||||
press("Escape")
|
||||
waitSpeech("blank, A2")
|
||||
enterEditor("A2")
|
||||
press("y")
|
||||
waitSpeech("y")
|
||||
press("Return")
|
||||
waitSpeech("blank, A3")
|
||||
press("Up")
|
||||
waitSpeech("y, A2")
|
||||
press("ctrl+Page_Down", "ctrl+Home")
|
||||
waitSpeech("Gamma, A1")
|
||||
press("ctrl+Page_Up", "ctrl+Home")
|
||||
waitSpeech("Alpha, A1")
|
||||
press("Right")
|
||||
waitSpeech("42, B1")
|
||||
enterEditor("B1")
|
||||
press("Left")
|
||||
waitSpeech("2")
|
||||
press("Escape")
|
||||
waitSpeech("42, B1")
|
||||
press("F6")
|
||||
press("ctrl+F6", "Down")
|
||||
waitSpeech("Beta, B2")
|
||||
waitFrame("Beta B2")
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
)
|
||||
|
||||
func calcTestEvent(eventType, path, value, coordinate string) atspi.Event {
|
||||
source := atspi.Snapshot{BusName: ":1.42", Path: path, Role: "table cell", Name: coordinate,
|
||||
Text: value, ApplicationProcess: "soffice.bin", Focused: true,
|
||||
TablePath: "/sheet", TablePositionKnown: true,
|
||||
LibreOffice: &atspi.LibreOfficeContext{InSpreadsheet: true, Cell: true, CellText: value, Coordinates: coordinate}}
|
||||
event := atspi.NewEvent(eventType, source)
|
||||
event.NativeType = eventType
|
||||
event.Window = atspi.NewObjectID(source.BusName, "/window")
|
||||
event.OriginPath = "/sheet"
|
||||
return event
|
||||
}
|
||||
|
||||
func TestLibreOfficeMovementEditingAndFocusReturn(t *testing.T) {
|
||||
state := eventLoopState{}
|
||||
state.presentationOptions = presentation.DefaultOptions()
|
||||
state.presentationOptions.EnableEchoByCharacter = true
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 32)}
|
||||
handler := accessibilityEventHandler{state: &state, logger: slog.New(slog.DiscardHandler),
|
||||
speech: speech, adapter: &fakeAdapter{}, clearOwnedFocus: func() {}, syncForeground: func() {}}
|
||||
presenter := eventPresenter{state: &state, logger: handler.logger, speech: speech}
|
||||
handler.speak = presenter.speak
|
||||
handler.titles = &windowTitleController{state: &state, speech: speech, logger: handler.logger}
|
||||
handler.webScripts = &webScriptController{speech: speech}
|
||||
expectSpeech := func(want string) {
|
||||
t.Helper()
|
||||
select {
|
||||
case got := <-speech.spoken:
|
||||
if got != want {
|
||||
t.Fatalf("Calc speech = %q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("missing Calc speech %q", want)
|
||||
}
|
||||
}
|
||||
first := calcTestEvent(atspi.ActiveDescendantEvent, "/cell1", "42", "B1")
|
||||
first.Source.TableColumnIndex = 1
|
||||
handler.handle(first)
|
||||
expectSpeech("42, B1")
|
||||
ancestor := first
|
||||
ancestor.Type = atspi.FocusEvent
|
||||
ancestor.Source.Path, ancestor.Source.Role, ancestor.Source.Name = "/root_pane", "root pane", "Example"
|
||||
ancestor.Source.LibreOffice = nil
|
||||
state.current.BrailleContext = []atspi.Snapshot{ancestor.Source}
|
||||
handler.handle(ancestor)
|
||||
if state.current.ID() != first.Source.ID() || len(speech.spoken) != 0 {
|
||||
t.Fatal("ancestor focus displaced the active cell")
|
||||
}
|
||||
handler.handle(first)
|
||||
if len(speech.spoken) != 0 {
|
||||
t.Fatal("repeated cell descendant was spoken")
|
||||
}
|
||||
blank := calcTestEvent(atspi.ActiveDescendantEvent, "/cell2", "", "A2")
|
||||
blank.Source.TableRowIndex = 1
|
||||
handler.handle(blank)
|
||||
expectSpeech("blank, A2")
|
||||
backgroundCaret := blank
|
||||
backgroundCaret.Type = atspi.CaretEvent
|
||||
backgroundCaret.Source.Path = "/input_line"
|
||||
backgroundCaret.Source.Role = "paragraph"
|
||||
backgroundCaret.Source.Text = "unrelated"
|
||||
backgroundCaret.Source.LibreOffice = nil
|
||||
handler.handle(backgroundCaret)
|
||||
if state.current.ID() != blank.Source.ID() || len(speech.spoken) != 0 {
|
||||
t.Fatal("formula-bar caret displaced cell focus")
|
||||
}
|
||||
editor := blank
|
||||
editor.Type = atspi.TextInsertedEvent
|
||||
editor.Source.Path, editor.Source.Role, editor.Source.Name = "/editor", "paragraph", ""
|
||||
editor.Source.Editable = true
|
||||
editor.Source.Text, editor.Payload = "x", "x"
|
||||
editor.Source.LibreOffice = &atspi.LibreOfficeContext{InSpreadsheet: true, CellEditor: true}
|
||||
handler.handle(editor)
|
||||
if state.current.ID() != editor.Source.ID() || state.focusedID != editor.Source.ID() {
|
||||
t.Fatal("first editor mutation was filtered before focus adoption")
|
||||
}
|
||||
expectSpeech("x")
|
||||
table := editor
|
||||
table.Type = atspi.FocusEvent
|
||||
table.Source.Path, table.Source.Role, table.Source.Name = "/sheet", "table", "Sheet Example"
|
||||
table.Source.LibreOffice = &atspi.LibreOfficeContext{InSpreadsheet: true}
|
||||
handler.handle(table)
|
||||
if state.current.ID() != table.Source.ID() || len(speech.spoken) != 0 {
|
||||
t.Fatal("post-edit table focus was not owned silently")
|
||||
}
|
||||
handler.handle(blank)
|
||||
expectSpeech("blank, A2")
|
||||
otherSheet := calcTestEvent(atspi.ActiveDescendantEvent, "/other_cell", "Gamma", "A1")
|
||||
otherSheet.OriginPath = "/other_sheet"
|
||||
otherSheet.Source.TablePath = "/other_sheet"
|
||||
handler.handle(otherSheet)
|
||||
expectSpeech("Gamma, A1")
|
||||
selection := blank
|
||||
selection.Type = atspi.SelectionChangedEvent
|
||||
handler.handle(selection)
|
||||
if state.current.ID() != otherSheet.Source.ID() || len(speech.spoken) != 0 {
|
||||
t.Fatal("range selection displaced the active cell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficePolicyCannotAdoptAnotherWindowOrProcess(t *testing.T) {
|
||||
current := calcTestEvent(atspi.FocusEvent, "/cell", "42", "B1")
|
||||
owner := windowOwner{window: current.Window, applicationBus: current.Source.BusName}
|
||||
for _, change := range []func(*atspi.Event){
|
||||
func(event *atspi.Event) { event.Window.Path = "/other_window" },
|
||||
func(event *atspi.Event) { event.Source.ApplicationProcess = "chromium" },
|
||||
func(event *atspi.Event) { event.Source.Focused = false },
|
||||
func(event *atspi.Event) { event.Source.Defunct = true },
|
||||
} {
|
||||
event := calcTestEvent(atspi.ActiveDescendantEvent, "/other_cell", "wrong", "C1")
|
||||
change(&event)
|
||||
result := prepareLibreOfficeEvent(event, current.Source, owner)
|
||||
if result.event.Type != atspi.ActiveDescendantEvent || result.adoptFocus {
|
||||
t.Fatalf("unowned cell became focus: %+v", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeBraillePreservesSpacingAndHasNoEditorRoute(t *testing.T) {
|
||||
event := calcTestEvent(atspi.FocusEvent, "/cell", "é 猫", "B3")
|
||||
frame := brailleFocusFrameWithOptions(event.Source, config.DefaultSettings().Braille)
|
||||
if frame.Text != "é 猫 B3" || frame.Cursor != 0 || len(frame.Routes) != 0 {
|
||||
t.Fatalf("Calc Braille = %+v", frame)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeRoutingHonorsLockAndForegroundBeforePolicy(t *testing.T) {
|
||||
for _, locked := range []bool{true, false} {
|
||||
t.Run(map[bool]string{true: "locked", false: "stale foreground"}[locked], func(t *testing.T) {
|
||||
current := calcTestEvent(atspi.FocusEvent, "/current", "42", "B1")
|
||||
state := eventLoopState{}
|
||||
state.current, state.focusedID = current.Source, current.Source.ID()
|
||||
state.owner = windowOwner{window: current.Window, applicationBus: current.Source.BusName}
|
||||
state.foregroundState = atspi.ForegroundState{Epoch: 2}
|
||||
state.lastSpokenText = "unchanged"
|
||||
gate := newFocusGate(nil)
|
||||
gate.secure.Store(locked)
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 4)}
|
||||
handler := accessibilityEventHandler{state: &state, focusGate: gate, speech: speech,
|
||||
logger: slog.New(slog.DiscardHandler), adapter: &fakeAdapter{},
|
||||
foreground: &ocrTestForeground{state: state.foregroundState},
|
||||
syncForeground: func() {}, webScripts: &webScriptController{}}
|
||||
event := calcTestEvent(atspi.ActiveDescendantEvent, "/new_cell", "wrong", "C1")
|
||||
event.Foreground.Epoch = 1
|
||||
handler.handle(event)
|
||||
if state.current.ID() != current.Source.ID() || state.lastSpokenText != "unchanged" || len(speech.spoken) != 0 {
|
||||
t.Fatal("LibreOffice policy ran before security/foreground rejection")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user