Review and strengthen LibreOffice code so far.
This commit is contained in:
1 parent
f885333520
commit
04e6c08fff
8 files changed
+186
-78
No files matched your search
@@ -130,12 +130,16 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
activate := func(window string) {
|
||||
t.Helper()
|
||||
if err := exec.CommandContext(ctx, "xdotool", "windowactivate", "--sync", window).Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// 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")
|
||||
activate(window)
|
||||
press("Escape", "F6", "ctrl+F6", "ctrl+Home", "Right")
|
||||
waitSpeech("42, B1")
|
||||
waitFrame("42 B1")
|
||||
press("Down")
|
||||
@@ -277,7 +281,9 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
|
||||
press("ctrl+Page_Up", "ctrl+Home", "Right", "Down")
|
||||
waitSpeech("y row header, Beta, B2")
|
||||
// A heading edit is visible on the next cell snapshot, without a cache.
|
||||
press("Up", "8", "Return")
|
||||
press("Up")
|
||||
waitSpeech("Alpha row header, 42, B1")
|
||||
press("8", "Return")
|
||||
waitSpeech("y row header, Beta, B2")
|
||||
waitFrame("y 8 Beta B2")
|
||||
commands <- input.WhereAmI
|
||||
@@ -320,10 +326,7 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
|
||||
if secondWindow == "" {
|
||||
t.Fatal("second private workbook window missing")
|
||||
}
|
||||
// A virtual display without a window manager does not focus new windows.
|
||||
if err := exec.CommandContext(ctx, "xdotool", "windowfocus", secondWindow).Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
activate(secondWindow)
|
||||
press("ctrl+F6", "ctrl+Page_Down", "ctrl+Home", "Down")
|
||||
waitSpeech("blank, A2")
|
||||
commands <- input.WhereAmI
|
||||
@@ -338,4 +341,55 @@ func TestNativeLibreOfficeCalc(t *testing.T) {
|
||||
waitSpeech("blank, A2")
|
||||
commands <- input.WhereAmI
|
||||
waitSpeech("d column header, blank, A2")
|
||||
// Return to the first workbook, then back again: identical sheet names
|
||||
// must retain independent choices and fresh cells in both directions.
|
||||
// Use normal navigation here, without the Ctrl+F6 recovery shortcut.
|
||||
activate(window)
|
||||
press("ctrl+Home", "Down")
|
||||
waitSpeech("blank, A2")
|
||||
commands <- input.WhereAmI
|
||||
waitSpeech("Gamma column header, blank, A2")
|
||||
waitFrame("Gamma A2")
|
||||
activate(secondWindow)
|
||||
press("ctrl+Home", "Down")
|
||||
waitSpeech("blank, A2")
|
||||
commands <- input.WhereAmI
|
||||
waitSpeech("d column header, blank, A2")
|
||||
waitFrame("d A2")
|
||||
// Dialog activation still resolves its focused control. Cancelling it
|
||||
// must leave the document navigable with the same header choices.
|
||||
press("ctrl+h")
|
||||
dialogWindow := ""
|
||||
deadline = time.Now().Add(5 * time.Second)
|
||||
for dialogWindow == "" && time.Now().Before(deadline) {
|
||||
output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output()
|
||||
if candidates := strings.Fields(string(output)); len(candidates) == 1 {
|
||||
dialogWindow = candidates[0]
|
||||
} else {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if dialogWindow == "" {
|
||||
t.Fatal("private Find and Replace dialog window missing")
|
||||
}
|
||||
activate(dialogWindow)
|
||||
press("x")
|
||||
waitSpeech("x")
|
||||
press("Escape")
|
||||
deadline = time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output()
|
||||
if len(strings.Fields(string(output))) == 0 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("private Find and Replace dialog did not close")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
activate(secondWindow)
|
||||
press("ctrl+F6", "ctrl+Home", "Down")
|
||||
waitSpeech("blank, A2")
|
||||
commands <- input.WhereAmI
|
||||
waitSpeech("d column header, blank, A2")
|
||||
}
|
||||
@@ -174,13 +174,19 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) {
|
||||
}
|
||||
sounds := &recordingSound{started: make(chan struct{})}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
completed := make(chan error, 1)
|
||||
// Sound starts first; cancellation there races later service startup.
|
||||
// The startup callback runs only once the complete runtime is ready.
|
||||
runtimeStarted := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
completed <- Run(ctx, Options{
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Settings: &settings,
|
||||
newSound: func() soundService { return sounds },
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Settings: &settings,
|
||||
OpenPreferences: true,
|
||||
onPreferences: func() { close(runtimeStarted) },
|
||||
newSound: func() soundService { return sounds },
|
||||
newCommands: func() commandListener {
|
||||
return silentCommands{}
|
||||
},
|
||||
@@ -190,9 +196,9 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) {
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-sounds.started:
|
||||
case <-runtimeStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("sound service did not start")
|
||||
t.Fatal("runtime did not finish startup")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
@@ -217,13 +223,17 @@ func TestRunReportsSoundStopError(t *testing.T) {
|
||||
soundError := errors.New("stop cue failed")
|
||||
sounds := &recordingSound{started: make(chan struct{}), stopError: soundError}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
completed := make(chan error, 1)
|
||||
runtimeStarted := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
completed <- Run(ctx, Options{
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Settings: &settings,
|
||||
newSound: func() soundService { return sounds },
|
||||
Logger: slog.New(slog.DiscardHandler),
|
||||
Settings: &settings,
|
||||
OpenPreferences: true,
|
||||
onPreferences: func() { close(runtimeStarted) },
|
||||
newSound: func() soundService { return sounds },
|
||||
newCommands: func() commandListener {
|
||||
return silentCommands{}
|
||||
},
|
||||
@@ -235,9 +245,9 @@ func TestRunReportsSoundStopError(t *testing.T) {
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-sounds.started:
|
||||
case <-runtimeStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("sound service did not start")
|
||||
t.Fatal("runtime did not finish startup")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
|
||||
@@ -184,7 +184,11 @@ func (adapter *Adapter) recoverForeground() {
|
||||
continue
|
||||
}
|
||||
if state.Epoch == adapter.Foreground().Epoch && adapter.matchesForeground(window, nativeObjectID(window)) {
|
||||
target := C.cthulhu_focused_descendant(window)
|
||||
target := adapter.libreOfficeFrameTarget(window)
|
||||
officeFrame := target != nil
|
||||
if target == nil {
|
||||
target = C.cthulhu_focused_descendant(window)
|
||||
}
|
||||
if target == nil {
|
||||
target = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(window)))
|
||||
}
|
||||
@@ -202,6 +206,9 @@ func (adapter *Adapter) recoverForeground() {
|
||||
target = child
|
||||
}
|
||||
snapshot := snapshotFromNativeWithCaret(target, true, false, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load())
|
||||
if officeFrame {
|
||||
snapshot.ApplicationProcess = adapter.applicationProcess(target)
|
||||
}
|
||||
event := NewEvent(WindowActivatedEvent, snapshot)
|
||||
event.Window = nativeObjectID(window)
|
||||
name := C.atspi_accessible_get_name(window, nil)
|
||||
|
||||
@@ -11,15 +11,19 @@ import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// libreOfficeFocusTarget resolves a Calc editor's direct text children and
|
||||
// preserves the sheet source for selection events. F2 focuses its temporary
|
||||
// panel; the paragraph can appear later, on typing.
|
||||
// libreOfficeFocusTarget keeps Office frame activation separate from document
|
||||
// focus, resolves a Calc editor's direct text children, and preserves the sheet
|
||||
// source for selection events. F2 focuses its temporary panel; the paragraph
|
||||
// can appear later, on typing.
|
||||
// The callback owns and releases the returned reference. No sheet is walked.
|
||||
func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType string) *C.AtspiAccessible {
|
||||
if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent ||
|
||||
if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent && eventType != WindowActivatedEvent ||
|
||||
!IsLibreOfficeProcess(adapter.applicationProcess(event.source)) {
|
||||
return nil
|
||||
}
|
||||
if eventType == WindowActivatedEvent {
|
||||
return adapter.libreOfficeFrameTarget(event.source)
|
||||
}
|
||||
role := C.atspi_accessible_get_role(event.source, nil)
|
||||
if eventType == SelectionChangedEvent {
|
||||
// Keep the sheet as the selection source. Its first selected cell
|
||||
@@ -79,6 +83,18 @@ func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType st
|
||||
return nil
|
||||
}
|
||||
|
||||
// Both real activation events and X11 foreground recovery must avoid searching
|
||||
// Calc's previous view while it rebuilds controls on workbook return. Own the
|
||||
// frame and let the following cell/editor events establish document focus.
|
||||
// Dialogs retain ordinary focused-control resolution. The caller owns the ref.
|
||||
func (adapter *Adapter) libreOfficeFrameTarget(source *C.AtspiAccessible) *C.AtspiAccessible {
|
||||
if !IsLibreOfficeProcess(adapter.applicationProcess(source)) ||
|
||||
C.atspi_accessible_get_role(source, nil) != C.ATSPI_ROLE_FRAME {
|
||||
return nil
|
||||
}
|
||||
return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source)))
|
||||
}
|
||||
|
||||
func captureLibreOfficeContext(source *C.AtspiAccessible, snapshot *Snapshot) {
|
||||
if !IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Defunct {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -540,13 +541,14 @@ func TestDisableKillsGroupDescendantsAndJoinsPipes(t *testing.T) {
|
||||
wait_signal(t, session.Done())
|
||||
// SIGKILL delivery/reaping is scheduled by the kernel; wait for /proc to
|
||||
// report disappearance or zombie state, not an arbitrary sleep duration.
|
||||
// A task that exits during ReadFile can return ESRCH rather than ENOENT.
|
||||
deadline := time.NewTimer(2 * time.Second)
|
||||
defer deadline.Stop()
|
||||
ticker := time.NewTicker(time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
data, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
|
||||
if os.IsNotExist(err) {
|
||||
if os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user