// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "bufio" "context" "fmt" "io" "log/slog" "os" "os/exec" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/braille" ) func TestNativeMatePanelMenuPresentation(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_PANEL_MENU") != "1" { t.Skip("requires private Xorg/AT-SPI session") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() adapter := atspi.New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) start := func(name string) (io.WriteCloser, string) { t.Helper() command := exec.CommandContext(ctx, "/usr/bin/python3", "../../tests/gtk_panel_menu.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] } panel, _ := start("mate-panel") editor, xid := start("editor") key := func(args ...string) { t.Helper() if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil { t.Fatalf("xdotool: %s, %v", output, err) } } key("windowfocus", "--sync", xid) speech := &fakeSpeechQueue{spoken: make(chan string, 128)} frames := make(chan braille.Frame, 128) done := make(chan struct{}) go func() { defer close(done) processEventsWithInput(ctx, slog.New(slog.DiscardHandler), outputQueue{speech: speech, braille: browserBrailleProbe{frames: frames}}, adapter, nil, nil, 0, nil, nil, nil, nil) }() defer func() { cancel(); <-done }() wait := func(name string) { t.Helper() spoken, displayed := false, false var messages []string for !spoken || !displayed { select { case text := <-speech.spoken: messages = append(messages, text) spoken = spoken || strings.Contains(text, name) case frame := <-frames: displayed = displayed || strings.Contains(frame.Text, name) case <-ctx.Done(): t.Fatalf("missing presentation %q: speech=%v braille=%v messages=%q", name, spoken, displayed, messages) } } } fmt.Fprintln(editor, "edit Foreground ready") wait("Foreground ready") for iteration := 0; iteration < 2; iteration++ { fmt.Fprintln(panel, "popup") wait("menu") // The initial bare menu may have no label. Navigation must acquire // its first item, including after dismissal and reopening. for _, step := range []struct{ key, name string }{ {"Down", "First application"}, {"Down", "Utilities"}, {"Right", "Example utility"}, {"Left", "Utilities"}, {"Down", "Last application"}, } { key("key", step.key) wait(step.name) } key("key", "Escape") text := fmt.Sprintf("Returned to editor %d", iteration) fmt.Fprintln(editor, "edit "+text) wait(text) } }