// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package presentation import ( "bufio" "context" "fmt" "os" "os/exec" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) func TestNativeTextLabelRelations(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_TEXT_LABELS") != "1" { t.Skip("requires private GTK/AT-SPI session") } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() adapter := atspi.New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) fixture := exec.CommandContext(ctx, "/usr/bin/python3", "-u", "../../tests/text_label_fixture.py") fixture.Env = append(os.Environ(), "NO_AT_BRIDGE=0", "GTK_MODULES=gail:atk-bridge") 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() }() if scanner := bufio.NewScanner(stdout); !scanner.Scan() || scanner.Text() != "ready" { t.Fatal("fixture did not start") } var previous atspi.ObjectID for index, probe := range []struct{ name, value string }{ {"Destination:", ""}, {"Display name:", "Example document"}, {"Preferred name", "Example value"}, {"Secret:", ""}, {"", "Unlabeled value"}, {"Account identifier", ""}, {"Read-only summary:", "Example summary"}, } { if _, err := fmt.Fprintln(stdin, index); err != nil { t.Fatal(err) } waitFocus: for { select { case <-ctx.Done(): t.Fatal("missing entry focus", index) case event := <-adapter.Events(): if (event.Type != atspi.FocusEvent && event.Type != atspi.FocusedStateEvent) || !event.Source.Focused { continue } snapshot := event.Source if snapshot.Role != "text" && snapshot.Role != "password text" { continue } if snapshot.ID() == previous { continue } previous = snapshot.ID() if snapshot.Name != probe.name { t.Errorf("entry %d name = %q, want %q", index, snapshot.Name, probe.name) } if probe.name == "Read-only summary:" && snapshot.Editable { t.Error("read-only fixture unexpectedly exposes editable state") } speech := FocusText(snapshot) for _, part := range []string{probe.name, probe.value} { if part != "" && !strings.Contains(speech, part) { t.Errorf("entry %d speech %q omits %q", index, speech, part) } } if strings.Contains(speech, "private fixture value") { t.Error("password content leaked into speech") } break waitFocus } } } }