Files
cthulhu/internal/atspi/alert_native_test.go

314 lines
9.8 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
import (
"context"
"os"
"os/exec"
"strings"
"testing"
"time"
)
func TestNativeGTKAlertMessage(t *testing.T) {
window := os.Getenv("CTHULHU_ALERT_WINDOW")
if window == "" {
t.Skip("requires the private GTK alert fixture")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
adapter := 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)
}
}()
for _, args := range [][]string{{"windowfocus", window}, {"key", "--clearmodifiers", "u"}} {
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
t.Fatalf("xdotool %s: %v: %s", args[0], err, output)
}
}
for {
select {
case <-ctx.Done():
t.Fatal("GTK alert window:create with its message was not captured")
case event := <-adapter.Events():
if event.Type != WindowCreateEvent || event.Source.Role != "alert" {
continue
}
if event.Source.Text != "Version 0.6.0 is available. Open the releases page in your browser?" {
t.Fatalf("alert text = %q", event.Source.Text)
}
return
}
}
}
func TestNativeYADQuestionMessage(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_YAD") != "1" {
t.Skip("requires private Xvfb/AT-SPI session")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
adapter := 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)
}
}()
command := exec.CommandContext(ctx, "yad", "--image", "dialog-question", "--title", "Alert",
"--button=gtk-yes:0", "--button=gtk-no:1",
"--text", "Microsoft Windows has been found! Would you like to remove it?")
if err := command.Start(); err != nil {
t.Fatal(err)
}
defer func() {
_ = command.Process.Kill()
_ = command.Wait()
}()
var yadWindow string
for yadWindow == "" && ctx.Err() == nil {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Alert$").Output()
if err == nil {
yadWindow = strings.TrimSpace(string(output))
}
if yadWindow == "" {
time.Sleep(20 * time.Millisecond)
}
}
if yadWindow == "" {
t.Fatal("YAD window did not appear")
}
if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", strings.Split(yadWindow, "\n")[0]).CombinedOutput(); err != nil {
t.Fatalf("focus YAD window: %v: %s", err, output)
}
for {
select {
case <-ctx.Done():
t.Fatal("YAD question was not captured on activation or button focus")
case event := <-adapter.Events():
question := "Microsoft Windows has been found! Would you like to remove it?"
if event.Type == WindowActivatedEvent && event.Source.Role == "frame" && event.Source.Name == "Alert" {
if !event.Window.Valid() || event.Window != event.Source.ID() {
t.Fatalf("YAD frame ownership = %#v, source = %#v", event.Window, event.Source.ID())
}
if event.Source.Text == question {
return
}
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab").CombinedOutput(); err != nil {
t.Fatalf("focus YAD button: %v: %s", err, output)
}
}
if (event.Type == FocusEvent || event.Type == FocusedStateEvent && event.Detail1 == 1) &&
event.Source.Role == "button" && event.WindowMessage == question {
if !event.Window.Valid() || event.Window == event.Source.ID() {
t.Fatalf("YAD button ownership = %#v, source = %#v", event.Window, event.Source.ID())
}
return
}
}
}
}
func TestNativeYADListMessage(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_YAD") != "1" {
t.Skip("requires private Xvfb/AT-SPI session")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
adapter := 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)
}
}()
command := exec.CommandContext(ctx, "yad", "--list", "--title=Select Sound Type",
"--text=Which sound type would you like to use?", "--column=Option", "--no-headers",
"--selectable-labels", "--search-column=1", "--height=400", "--width=600")
command.Stdin = strings.NewReader("stereo\nhrtf\n")
if err := command.Start(); err != nil {
t.Fatal(err)
}
defer func() {
_ = command.Process.Kill()
_ = command.Wait()
}()
var yadWindow string
for yadWindow == "" && ctx.Err() == nil {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Select Sound Type$").Output()
if err == nil {
yadWindow = strings.TrimSpace(string(output))
}
if yadWindow == "" {
time.Sleep(20 * time.Millisecond)
}
}
if yadWindow == "" {
t.Fatal("YAD list window did not appear")
}
if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", strings.Split(yadWindow, "\n")[0]).CombinedOutput(); err != nil {
t.Fatalf("focus YAD list window: %v: %s", err, output)
}
for {
select {
case <-ctx.Done():
t.Fatal("YAD list question was not captured on activation")
case event := <-adapter.Events():
if event.Source.ApplicationName != "yad" ||
(event.Type != WindowActivatedEvent && event.Type != FocusEvent &&
(event.Type != FocusedStateEvent || event.Detail1 != 1)) {
continue
}
if event.Source.Role != "tree table" && event.Source.Role != "frame" {
continue
}
question := event.WindowMessage
if event.Source.Role == "frame" && event.Type == WindowActivatedEvent {
question = event.Source.Text
}
if question == "" {
continue
}
if question != "Which sound type would you like to use?" {
t.Fatalf("YAD list question = %q on %q activation", question, event.Source.Role)
}
return
}
}
}
func TestNativeYADPowerListMessage(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_YAD") != "1" {
t.Skip("requires private Xvfb/AT-SPI session")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
adapter := 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)
}
}()
command := exec.CommandContext(ctx, "yad", "--title", "I38", "--text", "Power options", "--list",
"--column", "Action", "--column", "Description",
"Power off", "Shut down the system", "Reboot", "Restart the system",
"Log out", "Exit the current session", "--print-column=1", "--separator=",
"--button", "Select:0", "--button", "Cancel:1")
if err := command.Start(); err != nil {
t.Fatal(err)
}
defer func() {
_ = command.Process.Kill()
_ = command.Wait()
}()
var window string
for window == "" && ctx.Err() == nil {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^I38$").Output()
if err == nil {
window = strings.TrimSpace(string(output))
}
if window == "" {
time.Sleep(20 * time.Millisecond)
}
}
if window == "" {
t.Fatal("I38 list did not appear")
}
if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", strings.Split(window, "\n")[0]).CombinedOutput(); err != nil {
t.Fatalf("focus I38 list: %v: %s", err, output)
}
for {
select {
case <-ctx.Done():
t.Fatal("I38 power prompt was not captured")
case event := <-adapter.Events():
if event.Source.ApplicationName == "yad" && (event.Source.Role == "tree table" || event.Source.Role == "table cell") &&
(event.Type == WindowActivatedEvent || event.Type == FocusedStateEvent && event.Detail1 == 1) &&
event.WindowMessage == "Power options" {
return
}
}
}
}
func TestNativeGTKRadioGroupName(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_RADIO") != "1" {
t.Skip("requires private Xvfb/AT-SPI session")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
adapter := 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)
}
}()
command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_radio_group.py")
if err := command.Start(); err != nil {
t.Fatal(err)
}
defer func() {
_ = command.Process.Kill()
_ = command.Wait()
}()
var window string
for window == "" && ctx.Err() == nil {
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Radio group probe$").Output()
if err == nil {
window = strings.TrimSpace(string(output))
}
if window == "" {
time.Sleep(20 * time.Millisecond)
}
}
if window == "" {
t.Fatal("GTK radio window did not appear")
}
for _, args := range [][]string{{"windowfocus", strings.Split(window, "\n")[0]}, {"key", "Tab"}} {
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
t.Fatalf("xdotool %s: %v: %s", args[0], err, output)
}
}
for {
select {
case <-ctx.Done():
t.Fatal("GTK radio group name was not captured")
case event := <-adapter.Events():
if event.Source.Role == "radio button" && event.Source.Focused {
if event.Source.RadioGroupName != "Punctuation Level" {
t.Fatalf("radio group = %q", event.Source.RadioGroupName)
}
return
}
}
}
}