Files
cthulhu/internal/app/browser_nested_document_native_test.go

131 lines
4.2 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"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/review"
)
func TestNativeBrowserNestedDocumentHeadings(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>Nested document headings</title><main><h1>First heading</h1>`)
for i := 0; i < 200; i++ {
fmt.Fprint(w, `<article>`)
for depth := 0; depth < 15; depth++ {
fmt.Fprint(w, `<section>`)
}
fmt.Fprintf(w, `<h2>Post %d</h2><p>Post body %d</p><button>Reply</button>`, i, i)
for depth := 0; depth < 15; depth++ {
fmt.Fprint(w, `</section>`)
}
fmt.Fprint(w, `</article>`)
}
fmt.Fprint(w, `<h1>Last heading</h1></main>`)
}))
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)
}
}
window, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "Cthulhu Browser Probe").Output()
if err != nil || len(strings.Fields(string(window))) == 0 {
t.Fatalf("browser window: %s: %v", window, err)
}
command("windowfocus", "--sync", strings.Fields(string(window))[0])
command("key", "--clearmodifiers", "ctrl+l")
// The X window can appear before browser startup finishes. Wait for
// native address-bar focus so startup cannot swallow the URL keystrokes.
addressFocused := false
retryFocus := time.NewTicker(time.Second)
defer retryFocus.Stop()
for !addressFocused {
select {
case event := <-adapter.Events():
addressFocused = event.Type == atspi.FocusedStateEvent && event.Detail1 == 1 && event.Source.Editable
case <-retryFocus.C:
command("key", "--clearmodifiers", "ctrl+l")
case <-ctx.Done():
t.Fatal("browser address-bar focus: ", ctx.Err())
}
}
command("key", "--clearmodifiers", "ctrl+a")
command("type", "--clearmodifiers", "--delay", "1", 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 == "Nested document headings" {
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.SetDocumentCaptureTimeout(documentCaptureTimeout); err != nil {
t.Fatal(err)
}
if err := worker.Start(ctx); err != nil {
t.Fatal(err)
}
defer worker.Stop(context.Background())
worker.SyncFocus(document)
result := func(want string) {
t.Helper()
select {
case result := <-worker.Results():
if result.CaptureDuration > 0 {
t.Logf("whole-document capture: %v", result.CaptureDuration)
}
if result.Err != nil || result.DocumentPosition.Snapshot.Name != want {
t.Fatalf("navigation = %q, want %q: err=%v capture=%v diagnostics=%+v", result.DocumentPosition.Snapshot.Name, want, result.Err, result.CaptureReason, result.DocumentDiagnostics)
}
if result.DocumentSoundErr != nil || !result.DocumentPosition.Snapshot.SoundGeometry.ObjectKnown || !result.DocumentPosition.Snapshot.SoundGeometry.ReferenceKnown {
t.Fatalf("heading sound geometry unavailable: %+v, %v", result.DocumentPosition.Snapshot.SoundGeometry, result.DocumentSoundErr)
}
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
for _, step := range []struct {
backwards bool
want string
}{
{false, "First heading"}, {true, "Last heading"}, {true, "Post 199"}, {false, "Last heading"}, {false, "First heading"},
} {
if err := worker.MoveDocument(review.DocumentHeading, 0, step.backwards); err != nil {
t.Fatal(err)
}
result(step.want)
}
}