More LibreOffice work.
This commit is contained in:
1 parent
4ac59d4ced
commit
b5a5b86dda
15 files changed
+768
-28
No files matched your search
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
)
|
||||
|
||||
func TestLibreOfficeSelectionChangesKeepActiveCellAndBraille(t *testing.T) {
|
||||
current := calcTestEvent(atspi.FocusEvent, "/cell", "43", "C1")
|
||||
current.Source.TableColumnIndex = 2
|
||||
current.Source.LibreOffice.Formula = "B1+1"
|
||||
state := eventLoopState{}
|
||||
state.current, state.focusedID = current.Source, current.Source.ID()
|
||||
state.owner = windowOwner{window: current.Window, applicationBus: current.Source.BusName}
|
||||
state.presentationOptions = presentation.DefaultOptions()
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
||||
display := &flashRecorder{}
|
||||
live := &liveSettings{}
|
||||
live.Store(config.DefaultSettings())
|
||||
handler := accessibilityEventHandler{state: &state, logger: slog.New(slog.DiscardHandler),
|
||||
adapter: &fakeAdapter{}, speech: outputQueue{speech: speech, braille: display, live: live}, syncForeground: func() {}, webScripts: &webScriptController{}}
|
||||
event := current
|
||||
event.Type = atspi.SelectionChangedEvent
|
||||
event.Source.Path, event.Source.Role = "/sheet", "table"
|
||||
selection := func(cells ...atspi.LibreOfficeCell) {
|
||||
event.Source.LibreOffice = &atspi.LibreOfficeContext{InSpreadsheet: true,
|
||||
Selection: &atspi.LibreOfficeSelection{Kind: "cells", Count: len(cells), Complete: true, Cells: cells}}
|
||||
handler.handle(event)
|
||||
}
|
||||
expect := func(want string) {
|
||||
t.Helper()
|
||||
select {
|
||||
case got := <-speech.spoken:
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("missing %q", want)
|
||||
}
|
||||
if state.current.ID() != current.Source.ID() || state.focusedID != current.Source.ID() || presentation.LibreOfficeCellFormula(state.current) != "B1+1" {
|
||||
t.Fatal("selection displaced active cell/formula")
|
||||
}
|
||||
if display.messages[len(display.messages)-1] != want {
|
||||
t.Fatal("selection did not use system Braille flash")
|
||||
}
|
||||
}
|
||||
selection(atspi.LibreOfficeCell{Row: 0, Column: 0}, atspi.LibreOfficeCell{Row: 0, Column: 2})
|
||||
expect("Cell A1 selected.")
|
||||
handler.handle(event)
|
||||
if len(speech.spoken) != 0 {
|
||||
t.Fatal("duplicate selection repeated")
|
||||
}
|
||||
selection(atspi.LibreOfficeCell{Row: 0, Column: 0}, atspi.LibreOfficeCell{Row: 0, Column: 2}, atspi.LibreOfficeCell{Row: 0, Column: 4})
|
||||
expect("Cell E1 selected.")
|
||||
selection(atspi.LibreOfficeCell{Row: 0, Column: 2}, atspi.LibreOfficeCell{Row: 0, Column: 4})
|
||||
expect("Cell A1 unselected.")
|
||||
selection()
|
||||
expect("Selection cleared.")
|
||||
// A count-only change reports the new total, never a fabricated
|
||||
// unselection of the whole old range when selections overlap.
|
||||
event.Source.LibreOffice.Selection = &atspi.LibreOfficeSelection{Kind: "cells", Count: 1000000}
|
||||
handler.handle(event)
|
||||
expect("1000000 cells selected.")
|
||||
event.Source.LibreOffice.Selection = &atspi.LibreOfficeSelection{Kind: "cells", Count: 1000001}
|
||||
handler.handle(event)
|
||||
expect("1000001 cells selected.")
|
||||
// The same total with different boundary samples is a changed selection.
|
||||
event.Source.LibreOffice.Selection = &atspi.LibreOfficeSelection{Kind: "cells", Count: 1000001, First: atspi.LibreOfficeCell{Row: 2, Column: 2}}
|
||||
handler.handle(event)
|
||||
expect("1000001 cells selected.")
|
||||
// Whole-row/column shrinkage has the same delta policy when the
|
||||
// provider supplies an event (the real Calc harness documents gaps).
|
||||
for _, kind := range []string{"rows", "columns"} {
|
||||
state.libreOfficeSelection = libreOfficeSelectionState{}
|
||||
positions := []atspi.LibreOfficeCell{{Row: 0, Column: 0}, {Row: 1, Column: 0}}
|
||||
label := "Row 2 unselected."
|
||||
selectedLabel := "Rows 1 to 2 selected."
|
||||
if kind == "columns" {
|
||||
positions[1] = atspi.LibreOfficeCell{Row: 0, Column: 1}
|
||||
label = "Column B unselected."
|
||||
selectedLabel = "Columns A to B selected."
|
||||
}
|
||||
event.Source.LibreOffice.Selection = &atspi.LibreOfficeSelection{Kind: kind, Count: 2, Complete: true, Cells: positions}
|
||||
handler.handle(event)
|
||||
expect(selectedLabel)
|
||||
event.Source.LibreOffice.Selection = &atspi.LibreOfficeSelection{Kind: kind, Count: 1, Complete: true, Cells: positions[:1]}
|
||||
handler.handle(event)
|
||||
expect(label)
|
||||
}
|
||||
state.clearObjectFocus(nil, nil, nil)
|
||||
if state.libreOfficeSelection.previous != nil {
|
||||
t.Fatal("selection survived owned-focus reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibreOfficeSelectionRejectsUnownedAndSuppressedOutput(t *testing.T) {
|
||||
for _, name := range []string{"other sheet", "other bus", "other window", "wrong process", "defunct", "locked", "suspended", "stale foreground", "displayed text"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
current := calcTestEvent(atspi.FocusEvent, "/cell", "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.presentationOptions = presentation.DefaultOptions()
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 4)}
|
||||
gate := newFocusGate(nil)
|
||||
handler := accessibilityEventHandler{state: &state, logger: slog.New(slog.DiscardHandler), adapter: &fakeAdapter{}, speech: speech, focusGate: gate,
|
||||
foreground: &ocrTestForeground{state: state.foregroundState}, syncForeground: func() {}, webScripts: &webScriptController{}}
|
||||
event := current
|
||||
event.Type = atspi.SelectionChangedEvent
|
||||
event.Foreground.Epoch = 2
|
||||
event.Source.Path, event.Source.Role = "/sheet", "table"
|
||||
event.Source.LibreOffice = &atspi.LibreOfficeContext{InSpreadsheet: true, Selection: &atspi.LibreOfficeSelection{Kind: "all", Complete: true}}
|
||||
switch name {
|
||||
case "other sheet":
|
||||
event.Source.Path = "/other_sheet"
|
||||
case "other bus":
|
||||
event.Source.BusName = ":1.99"
|
||||
case "other window":
|
||||
event.Window.Path = "/other_window"
|
||||
case "wrong process":
|
||||
event.Source.ApplicationProcess = "example"
|
||||
case "defunct":
|
||||
event.Source.Defunct = true
|
||||
case "locked":
|
||||
gate.secure.Store(true)
|
||||
case "suspended":
|
||||
gate.suspended.Store(true)
|
||||
case "stale foreground":
|
||||
event.Foreground.Epoch = 1
|
||||
case "displayed text":
|
||||
reading := config.DefaultSettings().Reading
|
||||
reading.OnlySpeakDisplayedText = true
|
||||
state.presentationOptions.Reading = &reading
|
||||
}
|
||||
// Test the application hook after the ordinary foreground/security
|
||||
// gates; wrong-process events belong to the generic application path.
|
||||
if name == "wrong process" {
|
||||
handler.handleLibreOfficeSelection(event)
|
||||
} else {
|
||||
handler.handle(event)
|
||||
}
|
||||
if len(speech.spoken) != 0 || state.current.ID() != current.Source.ID() {
|
||||
t.Fatal("unowned/suppressed selection changed output or focus")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user