// 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 TestNativeMateWindowSwitcherOutsideForeground(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_WINDOW_SWITCHER") != "1" { t.Skip("requires private Xorg/AT-SPI session") } 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) { command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_window_switcher.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] } switcher, _ := start("marco") foreground, xid := start("editor") if _, err := adapter.CaptureDesktop(ctx); err != nil { t.Fatal(err) } if output, err := exec.CommandContext(ctx, "xdotool", "windowfocus", "--sync", xid).CombinedOutput(); err != nil { t.Fatalf("focus: %s, %v", output, err) } wait := func(eventType, title string, switcher bool) Event { t.Helper() var last Event count := 0 for { select { case event := <-adapter.Events(): last = event count++ if event.Type == eventType && (event.Source.Name == title || event.Payload == title) { if IsWindowSwitcherSelection(event) != switcher { t.Fatalf("switcher classification: %+v", event) } return event } case <-ctx.Done(): t.Fatalf("missing %s %q; background-filtered=%d received=%d last=%+v", eventType, title, adapter.backgroundEventsFiltered.Load(), count, last) } } } fmt.Fprintln(foreground, "select Foreground ready") owned := wait(TextInsertedEvent, "Foreground ready", false) if owned.ForegroundMismatch || !owned.Foreground.Client.Known { t.Fatalf("fixture has no correlated foreground: %+v", owned) } for _, title := range []string{"Editor", "Terminal", "Editor"} { fmt.Fprintln(switcher, "select "+title) event := wait(TextInsertedEvent, title, true) if !event.ForegroundMismatch || event.Foreground != owned.Foreground || event.Source.Focused { t.Fatalf("switcher acquired foreground/native focus: %+v", event) } } fmt.Fprintln(switcher, "showing") wait(ShowingStateEvent, "Editor", true) // The provider stays separate from the old foreground after the popup // disappears. Ordinary application events still use native correlation. fmt.Fprintln(switcher, "hide") fmt.Fprintln(foreground, "select Foreground retained") retained := wait(TextInsertedEvent, "Foreground retained", false) if retained.ForegroundMismatch || retained.Window != owned.Window { t.Fatalf("switcher changed foreground ownership: %+v", retained) } }