160 lines
5.7 KiB
Go
160 lines
5.7 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 TestNativeEmbeddedPanelApplet(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_PANEL_APPLET") != "1" {
|
|
t.Skip("requires private Xorg/AT-SPI session")
|
|
}
|
|
// Embedding belongs to the window ancestry contract, not a process-name
|
|
// exception. Both MATE and an ordinary GTK host must retain ownership.
|
|
for _, hostName := range []string{"panel", "other-host"} {
|
|
t.Run(hostName, func(t *testing.T) { check_native_embedded_applet(t, hostName) })
|
|
}
|
|
}
|
|
|
|
func check_native_embedded_applet(t *testing.T, hostName string) {
|
|
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(args ...string) (io.WriteCloser, string, string) {
|
|
t.Helper()
|
|
command := exec.CommandContext(ctx, "/usr/bin/python3", append([]string{"../../tests/gtk_panel_applet.py"}, args...)...)
|
|
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) != 3 || fields[0] != "ready" {
|
|
t.Fatalf("fixture readiness: %q, %v", line, err)
|
|
}
|
|
return input, fields[1], fields[2]
|
|
}
|
|
host, xid, socketID := start(hostName)
|
|
applet, _, _ := start("applet", socketID)
|
|
editor, editorXID, _ := start("editor")
|
|
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; filtered=%d last=%s %q window=%+v mismatch=%v", adapter.backgroundEventsFiltered.Load(), last.Type, last.Source.Name, last.Window, last.ForegroundMismatch)
|
|
}
|
|
}
|
|
}
|
|
key := func(args ...string) {
|
|
t.Helper()
|
|
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
|
|
t.Fatalf("xdotool %v: %s, %v", args, output, err)
|
|
}
|
|
}
|
|
key("windowfocus", "--sync", xid)
|
|
fmt.Fprintln(host, "first")
|
|
owned := wait(func(event Event) bool {
|
|
return event.Source.Name == "Before applet" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
key("key", "Tab")
|
|
icon := wait(func(event Event) bool { return event.Source.Role == "icon" && event.Source.Focused })
|
|
if icon.ForegroundMismatch || icon.Window != owned.Window || icon.Source.BusName == owned.Source.BusName || icon.Foreground != owned.Foreground {
|
|
t.Fatalf("embedded icon lost host ownership: host=%+v icon=%+v source=%+v mismatch=%v foreground=%+v want=%+v", owned.Window, icon.Window, icon.Source.ID(), icon.ForegroundMismatch, icon.Foreground, owned.Foreground)
|
|
}
|
|
if icon.Source.Name != "Network applet" || icon.Source.ApplicationProcess != "applet" {
|
|
t.Fatalf("embedded icon lost its label or provider identity: %+v", icon)
|
|
}
|
|
key("key", "Tab")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Name == "Applet settings" && event.Source.Focused && event.Window == owned.Window && !event.ForegroundMismatch
|
|
})
|
|
key("key", "shift+Tab")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Role == "icon" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
key("key", "Tab")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Name == "Applet settings" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
key("key", "Tab")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Name == "After applet" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
for _, name := range []string{"Applet settings", "Network applet", "Before applet"} {
|
|
key("key", "shift+Tab")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Name == name && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
}
|
|
key("windowfocus", "--sync", editorXID)
|
|
fmt.Fprintln(editor, "edit Foreground restored")
|
|
wait(func(event Event) bool { return event.Payload == "Foreground restored" && !event.ForegroundMismatch })
|
|
fmt.Fprintln(applet, "stale")
|
|
wait(func(event Event) bool {
|
|
if event.Source.Role == "icon" && !event.ForegroundMismatch {
|
|
t.Fatalf("background applet stole foreground: %+v", event)
|
|
}
|
|
return event.Payload == "Applet barrier"
|
|
})
|
|
// A separately published applet window cannot borrow the host's identity.
|
|
standalone, standaloneXID, _ := start("standalone")
|
|
key("windowfocus", "--sync", standaloneXID)
|
|
fmt.Fprintln(standalone, "first")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Role == "icon" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
key("windowfocus", "--sync", xid)
|
|
fmt.Fprintln(host, "first")
|
|
wait(func(event Event) bool {
|
|
return event.Source.Name == "Before applet" && event.Source.Focused && !event.ForegroundMismatch
|
|
})
|
|
fmt.Fprintln(standalone, "stale")
|
|
wait(func(event Event) bool {
|
|
if event.Source.Role == "icon" && !event.ForegroundMismatch {
|
|
t.Fatalf("unembedded applet borrowed host ownership: %+v", event)
|
|
}
|
|
return event.Payload == "Applet barrier"
|
|
})
|
|
fmt.Fprintln(host, "detach")
|
|
wait(func(event Event) bool { return event.Payload == "Detached host barrier" })
|
|
fmt.Fprintln(applet, "stale")
|
|
wait(func(event Event) bool {
|
|
if event.Source.Role == "icon" && !event.ForegroundMismatch {
|
|
t.Fatalf("detached applet retained host ownership: %+v", event)
|
|
}
|
|
return event.Payload == "Applet barrier"
|
|
})
|
|
}
|