314 lines
9.8 KiB
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
|
|
}
|
|
}
|
|
}
|
|
}
|