// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "bufio" "context" "encoding/json" "os" "os/exec" "testing" "time" ) func TestNativeForegroundTitlePublication(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_WINDOW_TITLES") != "1" { t.Skip("private Xvfb and D-Bus fixture only") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() fixture := exec.CommandContext(ctx, "python3", "-u", "../../tests/window_title_fixture.py") stdin, err := fixture.StdinPipe() if err != nil { t.Fatal(err) } stdout, err := fixture.StdoutPipe() if err != nil { t.Fatal(err) } fixture.Stderr = os.Stderr if err = fixture.Start(); err != nil { t.Fatal(err) } defer func() { stdin.Close(); fixture.Wait() }() scanner := bufio.NewScanner(stdout) if !scanner.Scan() { t.Fatal("fixture did not start") } var windows map[string]uint64 if err = json.Unmarshal(scanner.Bytes(), &windows); err != nil { t.Fatal(err) } adapter := New() if err = adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) waitTitle := func(window, child uint64, text string) ForegroundTitle { t.Helper() for { value := adapter.ForegroundTitle() if value.Title.Window == window && value.Title.TitleWindow == child && value.Title.Title == text { return value } select { case <-adapter.TitleChanges(): case <-ctx.Done(): t.Fatalf("missing title %q: %+v", text, value) } } } command := func(action, window, title string) { t.Helper() data, _ := json.Marshal(map[string]string{"action": action, "window": window, "title": title}) if _, err = stdin.Write(append(data, '\n')); err != nil { t.Fatal(err) } if !scanner.Scan() || scanner.Text() != "ready" { t.Fatal("fixture command failed") } } initial := waitTitle(windows["normal"], windows["normal"], "Example title") command("title", "normal", "Changed without AT-SPI") changed := waitTitle(windows["normal"], windows["normal"], "Changed without AT-SPI") if changed.Epoch != initial.Epoch { t.Fatal("title change reset foreground ownership") } command("activate", "desktop", "") command("focus", "control", "") wine := waitTitle(windows["desktop"], windows["game"], "Wine game") if wine.Epoch == initial.Epoch || wine.Title.Application != "game.exe" { t.Fatalf("Wine ownership = %+v", wine) } if !wine.Title.Wine || !wine.Title.Focused || !wine.Title.Client.Known || wine.Title.Client.Window != windows["game"] || wine.Title.Client.Width != 250 || wine.Title.Client.Height != 150 { t.Fatalf("Wine child geometry/focus missing: %+v", wine) } command("focus", "second", "") dialog := waitTitle(windows["desktop"], windows["second"], "Wine dialog") if dialog.Epoch != wine.Epoch { t.Fatal("Wine child change reset desktop ownership") } command("destroy", "desktop", "") empty := waitTitle(0, 0, "") if empty.Epoch == wine.Epoch { t.Fatal("lost foreground kept its epoch") } }