153 lines
4.8 KiB
Go
153 lines
4.8 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 TestNativeBrowserDynamicNavigation(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
|
|
t.Skip("requires private browser session")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
page := `<!doctype html><title>Dynamic navigation regression</title>
|
|
<a href="#end">Start of page</a>
|
|
<button aria-label="Counter" onfocus="this.setAttribute('aria-label', 'Counter updated')">Counter</button>
|
|
<p>After control</p>
|
|
<svg role="img" aria-label="Avatar" width="16" height="16"><circle cx="8" cy="8" r="8"/></svg>
|
|
<button aria-label="Add content" onfocus="const p = document.createElement('p'); p.textContent = 'New content'; document.body.appendChild(p); this.setAttribute('aria-label', 'Add content done')">Add content</button>`
|
|
// A large document must remain navigable without exhausting the live
|
|
// two-second first-capture budget before any browse position exists.
|
|
for i := 0; i < 1000; i++ {
|
|
page += fmt.Sprintf("<p>Unrelated paragraph %d</p>", i)
|
|
}
|
|
page += `<p id="end">End of page</p>`
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprint(w, page)
|
|
}))
|
|
defer server.Close()
|
|
adapter := atspi.New()
|
|
adapter.SetCaptureSoundGeometry(true)
|
|
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
|
|
var applicationName string
|
|
for !document.Valid() {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Type == atspi.DocumentLoadCompleteEvent && event.Source.Name == "Dynamic navigation regression" {
|
|
document = event.Source.ID()
|
|
applicationName = event.Source.ApplicationName
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
worker, err := review.NewWorker(adapter, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := worker.SetFlatCaptureTimeout(10 * time.Second); 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():
|
|
got := result.DocumentCaretText
|
|
if got == "" {
|
|
got = result.DocumentPosition.Snapshot.Name
|
|
}
|
|
if result.Err != nil || result.CaretErr != nil || !result.Moved || !strings.Contains(got, want) {
|
|
t.Fatalf("next line = %q, want %q: %+v", got, want, result)
|
|
}
|
|
if result.DocumentPosition.Snapshot.ApplicationName != applicationName {
|
|
t.Fatalf("document target application = %q, want %q", result.DocumentPosition.Snapshot.ApplicationName, applicationName)
|
|
}
|
|
t.Logf("line=%q elapsed=%s capture=%s reason=%v", got, result.Elapsed, result.CaptureDuration, result.CaptureReason)
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
next := func(want string) {
|
|
t.Helper()
|
|
move(review.CaretNextLine, want)
|
|
}
|
|
next("Start of page")
|
|
next("Counter")
|
|
waitName := func(name string) {
|
|
t.Helper()
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Type == atspi.AccessibleNameEvent && event.Source.Name == name {
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("name change to %q was not observed", name)
|
|
}
|
|
}
|
|
}
|
|
waitName("Counter updated")
|
|
next("After control")
|
|
next("Avatar")
|
|
next("Add content")
|
|
waitName("Add content done")
|
|
next("Unrelated paragraph 0")
|
|
next("Unrelated paragraph 1")
|
|
move(review.CaretDocumentEnd, "New content")
|
|
for _, unit := range []review.FlatUnit{review.WindowEnd, review.Line} {
|
|
started := time.Now()
|
|
step := 0
|
|
if unit == review.Line {
|
|
step = -1
|
|
}
|
|
if err := worker.ReadFlat(unit, step); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case result := <-worker.Results():
|
|
t.Logf("flat review unit %d elapsed=%s capture=%s reason=%v", unit, time.Since(started), result.CaptureDuration, result.CaptureReason)
|
|
if result.Err != nil || strings.TrimSpace(result.FlatPosition.Text) == "" || unit == review.Line && !result.Moved {
|
|
t.Fatalf("large-page flat review unit %d = %+v", unit, result)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|