142 lines
4.6 KiB
Go
142 lines
4.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"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNativeWebScriptNotificationCapture(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_WEBSCRIPTS") != "1" {
|
|
t.Skip("requires private Xorg/AT-SPI session")
|
|
}
|
|
adapterCtx, adapterCancel := context.WithTimeout(context.Background(), 25*time.Second)
|
|
defer adapterCancel()
|
|
adapter := New()
|
|
if err := adapter.Start(adapterCtx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
for _, processName := range []string{"steamwebhelper", "chromium"} {
|
|
t.Run(processName, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, "python3", "../../tests/gtk_web_notification.py", processName)
|
|
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)
|
|
}
|
|
defer func() { input.Close(); command.Process.Kill(); command.Wait() }()
|
|
ready, err := bufio.NewReader(output).ReadString('\n')
|
|
fields := strings.Fields(ready)
|
|
if err != nil || len(fields) != 2 || fields[0] != "ready" {
|
|
t.Fatalf("fixture readiness: %q, %v", ready, err)
|
|
}
|
|
// Enumerate the fixture once so GTK publishes its initially hidden
|
|
// alert accessibles before we request showing/text events.
|
|
if _, err := adapter.CaptureDesktop(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Keep the toast window outside foreground ownership. Events must
|
|
// carry copied notification data without acquiring native focus.
|
|
if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", fields[1]).CombinedOutput(); err != nil {
|
|
t.Fatalf("foreground focus unavailable: %s, %v", output, err)
|
|
}
|
|
fmt.Fprintln(input, "show")
|
|
if processName == "chromium" {
|
|
fmt.Fprintln(input, "update")
|
|
}
|
|
var root ObjectID
|
|
for !root.Valid() {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Source.ApplicationProcess != processName {
|
|
continue
|
|
}
|
|
if processName == "chromium" {
|
|
if event.Type != TextInsertedEvent || event.Payload != "Username is now online" {
|
|
continue
|
|
}
|
|
if event.WebNotification != nil {
|
|
t.Fatal("generic Chromium received Steam metadata")
|
|
}
|
|
return
|
|
}
|
|
if event.Type != ShowingStateEvent || event.Detail1 == 0 || event.Source.Role != "alert" {
|
|
continue
|
|
}
|
|
message := event.WebNotification
|
|
if !event.ForegroundMismatch {
|
|
t.Fatal("toast fixture was not outside foreground ownership")
|
|
}
|
|
if message == nil || message.Text != "Username Playing: Example Game" {
|
|
t.Fatalf("toast text: %+v", message)
|
|
}
|
|
root = NewObjectID(message.BusName, message.RootPath)
|
|
if root != event.Source.ID() {
|
|
t.Fatal("showing event did not use the alert root")
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("toast showing event missing")
|
|
}
|
|
}
|
|
fmt.Fprintln(input, "update")
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Source.ApplicationProcess != processName || event.Type != TextInsertedEvent || event.Payload != "Username is now online" {
|
|
continue
|
|
}
|
|
message := event.WebNotification
|
|
if message == nil || NewObjectID(message.BusName, message.RootPath) != root || !strings.Contains(message.Text, "Username is now online") {
|
|
t.Fatalf("fragment did not recover complete alert ancestor: %+v", message)
|
|
}
|
|
fmt.Fprintln(input, "destroy")
|
|
for {
|
|
select {
|
|
case ending := <-adapter.Events():
|
|
if ending.Type == DefunctStateEvent && ending.Source.ID() == root && ending.Detail1 != 0 {
|
|
fmt.Fprintln(input, "add")
|
|
for {
|
|
select {
|
|
case added := <-adapter.Events():
|
|
if added.Type == ChildrenAddedEvent && added.Source.ApplicationProcess == processName && added.WebNotification != nil && added.WebNotification.RootPath != added.Source.Path {
|
|
if added.WebNotification.Text != "A new toast" {
|
|
t.Fatalf("child-added did not use the added toast root: %+v", added.WebNotification)
|
|
}
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("added toast did not reach notification handling")
|
|
}
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("destroyed toast root did not reach lifecycle handling")
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("toast text insertion missing")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|