// 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 TestNativeEmbeddedPanelAppletPresentation(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_PANEL_APPLET") != "1" { t.Skip("requires private Xorg/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()) start := func(args ...string) (io.WriteCloser, string, string) { t.Helper() command := exec.CommandContext(ctx, "/usr/bin/python3", append([]string{"../../tests/gtk_panel_applet.py"}, args...)...) 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) != 3 || fields[0] != "ready" { t.Fatalf("fixture readiness: %q, %v", line, err) } return input, fields[1], fields[2] } host, xid, socketID := start("panel") start("applet", socketID) editor, editorXID, _ := 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) } } 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) } } } for iteration := 0; iteration < 2; iteration++ { key("windowfocus", "--sync", xid) fmt.Fprintln(host, "first") wait("Before applet") key("key", "Tab") wait("Network applet") key("key", "Tab") wait("Applet settings") key("key", "shift+Tab") wait("Network applet") key("key", "Tab") wait("Applet settings") key("key", "Tab") wait("After applet") for _, name := range []string{"Applet settings", "Network applet", "Before applet"} { key("key", "shift+Tab") wait(name) } key("windowfocus", "--sync", editorXID) text := fmt.Sprintf("Returned to editor %d", iteration) fmt.Fprintln(editor, "edit "+text) wait(text) } }