// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "fmt" "net/http" "net/http/httptest" "os" "os/exec" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/review" ) func TestNativeBrowserTooltipNavigation(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" { t.Skip("requires private browser session") } ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `Tooltip navigation regression `) })) defer server.Close() adapter := atspi.New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) command := func(args ...string) { t.Helper() if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil { t.Fatalf("xdotool: %v: %s", err, output) } } command("key", "--clearmodifiers", "ctrl+l") command("type", "--clearmodifiers", "--delay", "0", server.URL) command("key", "Return") var document atspi.ObjectID for !document.Valid() { select { case event := <-adapter.Events(): if event.Type == atspi.DocumentLoadCompleteEvent && event.Source.Name == "Tooltip navigation regression" { document = event.Source.ID() } case <-ctx.Done(): t.Fatal(ctx.Err()) } } worker, err := review.NewWorker(adapter, 2*time.Second) if err != nil { t.Fatal(err) } if err := worker.Start(ctx); err != nil { t.Fatal(err) } defer worker.Stop(context.Background()) worker.SyncFocus(document) move := func(command review.DocumentCaretCommand, want string) { t.Helper() if err := worker.MoveDocumentCaret(command); err != nil { t.Fatal(err) } select { case result := <-worker.Results(): if result.Err != nil || result.CaretErr != nil || !result.Moved || result.DocumentCaretText != want { t.Fatalf("%s: text=%q want=%q err=%v caret=%v diagnostics=%+v", command, result.DocumentCaretText, want, result.Err, result.CaretErr, result.DocumentDiagnostics) } case <-ctx.Done(): t.Fatal(ctx.Err()) } } move(review.CaretNextLine, "Example project") // Capture after focus has exposed the tooltip. This proves the fixture // supplies a real native tooltip subtree, rather than merely hidden text. for { tree, err := adapter.CaptureDocument(ctx, document) if err != nil { t.Fatal(err) } ref, _ := tree.Reference(document) doc, err := tree.Document(ctx, ref) found := false if err == nil { for _, node := range doc.Nodes { if node.Snapshot.Role == "tool tip" && !node.Hidden && len(node.Children) > 0 { found = true } } } if found { break } select { case <-adapter.Events(): case <-ctx.Done(): t.Fatal("visible native tooltip missing: ", ctx.Err()) } } move(review.CaretNextLine, "After project") move(review.CaretPreviousLine, "Example project") move(review.CaretLineEnd, "t") move(review.CaretNextCharacter, "A") move(review.CaretPreviousCharacter, "t") }