189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNativeMatePanelMenuOutsideForeground(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_PANEL_MENU") != "1" {
|
|
t.Skip("requires private Xorg/AT-SPI session")
|
|
}
|
|
for _, test := range []struct {
|
|
name, processName string
|
|
empty bool
|
|
}{
|
|
{"over editor", "mate-panel", false},
|
|
{"empty desktop", "mate-panel", true},
|
|
{"impostor", "impostor", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) { check_native_panel_menu(t, test.processName, test.empty) })
|
|
}
|
|
}
|
|
|
|
func check_native_panel_menu(t *testing.T, processName string, empty bool) {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
adapter := New()
|
|
if err := adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
start := func(name string) (io.WriteCloser, string) {
|
|
t.Helper()
|
|
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_panel_menu.py", name)
|
|
input, err := command.StdinPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
output, err := command.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
command.Stderr = os.Stderr
|
|
if err := command.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { input.Close(); command.Process.Kill(); command.Wait() })
|
|
line, err := bufio.NewReader(output).ReadString('\n')
|
|
fields := strings.Fields(line)
|
|
if err != nil || len(fields) != 2 || fields[0] != "ready" {
|
|
t.Fatalf("fixture readiness: %q, %v", line, err)
|
|
}
|
|
return input, fields[1]
|
|
}
|
|
panel, _ := start(processName)
|
|
editor, xid := start("editor")
|
|
tree, err := adapter.CaptureDesktop(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check_native_panel_icons(t, tree)
|
|
key := func(args ...string) {
|
|
t.Helper()
|
|
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
|
|
t.Fatalf("xdotool: %s, %v", output, err)
|
|
}
|
|
}
|
|
key("windowfocus", "--sync", xid)
|
|
wait := func(predicate func(Event) bool) Event {
|
|
t.Helper()
|
|
var last Event
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
last = event
|
|
if predicate(event) {
|
|
return event
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("native event missing: %v; filtered=%d last=%+v", ctx.Err(), adapter.backgroundEventsFiltered.Load(), last)
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintln(editor, "edit Foreground ready")
|
|
owned := wait(func(event Event) bool { return event.Payload == "Foreground ready" && !event.ForegroundMismatch })
|
|
if empty {
|
|
key("windowfocus", "0")
|
|
for adapter.Foreground().Client.Window != 0 {
|
|
select {
|
|
case <-adapter.ForegroundChanges():
|
|
case <-ctx.Done():
|
|
t.Fatal("empty desktop foreground missing")
|
|
}
|
|
}
|
|
owned.Foreground = adapter.Foreground()
|
|
}
|
|
fmt.Fprintln(panel, "popup")
|
|
activation := wait(func(event Event) bool {
|
|
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel"
|
|
})
|
|
if processName != "mate-panel" {
|
|
if !activation.ForegroundMismatch {
|
|
t.Fatalf("accessible name enabled panel exception for another process: %+v", activation)
|
|
}
|
|
key("key", "Escape")
|
|
return
|
|
}
|
|
if activation.ForegroundMismatch || activation.Foreground != owned.Foreground {
|
|
t.Fatalf("panel popup rejected with unchanged X11 foreground: %+v", activation)
|
|
}
|
|
for _, step := range []struct{ key, name string }{
|
|
{"Down", "First application"}, {"Down", "Utilities"}, {"Right", "Example utility"},
|
|
{"Left", "Utilities"}, {"Down", "Last application"}, {"Up", "Utilities"},
|
|
} {
|
|
key("key", step.key)
|
|
event := wait(func(event Event) bool {
|
|
return event.Source.Name == step.name && (event.Type == FocusEvent || event.Type == FocusedStateEvent || event.Type == SelectionChangedEvent)
|
|
})
|
|
if event.ForegroundMismatch || event.Foreground != owned.Foreground || event.Source.ApplicationProcess != "mate-panel" {
|
|
t.Fatalf("panel navigation lost ownership: %+v", event)
|
|
}
|
|
}
|
|
key("key", "Escape")
|
|
fmt.Fprintln(panel, "ordinary")
|
|
ordinary := wait(func(event Event) bool {
|
|
return event.Type == WindowActivatedEvent && event.Source.ApplicationName == "mate-panel"
|
|
})
|
|
if !ordinary.ForegroundMismatch {
|
|
t.Fatalf("ordinary panel window borrowed menu exception: %+v", ordinary)
|
|
}
|
|
fmt.Fprintln(panel, "stale")
|
|
wait(func(event Event) bool {
|
|
if event.Source.Name == "First application" && (event.Type == FocusEvent || event.Type == FocusedStateEvent) {
|
|
t.Fatalf("hidden menu item escaped filtering: %+v", event)
|
|
}
|
|
return event.Payload == "Stale barrier"
|
|
})
|
|
if empty {
|
|
key("windowfocus", "--sync", xid)
|
|
}
|
|
fmt.Fprintln(editor, "edit Foreground restored")
|
|
restored := wait(func(event Event) bool { return event.Payload == "Foreground restored" && !event.ForegroundMismatch })
|
|
if restored.Window != owned.Window || !empty && restored.Foreground != owned.Foreground {
|
|
t.Fatalf("panel dismissal changed editor ownership: %+v", restored)
|
|
}
|
|
}
|
|
|
|
func check_native_panel_icons(t *testing.T, tree *AccessibleTree) {
|
|
t.Helper()
|
|
tree.mu.RLock()
|
|
defer tree.mu.RUnlock()
|
|
icons := 0
|
|
for _, node := range tree.nodes {
|
|
icon := node.snapshot
|
|
if icon.Role != "icon" {
|
|
continue
|
|
}
|
|
icons++
|
|
parent := tree.nodes[node.parent].snapshot
|
|
want := ""
|
|
switch parent.Name {
|
|
case "Network applet":
|
|
want = parent.Name
|
|
case "Unused applet name":
|
|
if icon.Description == "" {
|
|
want = "Named icon"
|
|
}
|
|
}
|
|
if icon.Name != want {
|
|
t.Fatalf("panel icon name=%q description=%q parent=%q; want %q", icon.Name, icon.Description, parent.Name, want)
|
|
}
|
|
}
|
|
if icons != 8 {
|
|
t.Fatalf("captured %d panel icons; want four cases from each provider", icons)
|
|
}
|
|
}
|