Files
cthulhu/internal/app/browser_tooltip_native_test.go

118 lines
3.5 KiB
Go

// 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, `<!doctype html><title>Tooltip navigation regression</title><style>li {list-style: none}</style>
<ul><li><a id="project" href="#after" aria-describedby="tip"
onfocus="document.getElementById('tip').hidden=false"
onblur="document.getElementById('tip').hidden=true">Example project</a>
<span id="tip" role="tooltip" hidden><span>Example project</span></span>
<p id="after">After project</p></li></ul>`)
}))
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")
}