Files
cthulhu/internal/app/browser_ticking_native_test.go
hjozwiak 6c10ed0517 Add native coverage for ticking browser pages
Incremental-game style fixture whose AT-SPI leaves recreate every 250 ms, native navigation test, private-session AT-SPI daemon launch in the harness (no systemd, no host session required), and testing notes.
2026-11-03 10:35:00 -05:00

146 lines
4.1 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"
)
// Incremental games rebuild their text leaves on a fast timer. Arrow-key
// reading must keep progressing through the rebuilt lines instead of
// restarting at the first line after every tick, which is how the reported
// Exotic Matter Dimensions and Ordinal Pringles failures presented.
func TestNativeBrowserTickingNavigation(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
t.Skip("requires private browser session")
}
ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second)
defer cancel()
page := `<!doctype html><title>Ticking navigation regression</title>
<h2>Rebuilt region</h2>
<p id="rebuilt"></p>
<p>After rebuilt region</p>
<button>Final action</button>
<p>Final paragraph</p>
<script>
"use strict";
let tick = 0;
const rebuilt = document.getElementById("rebuilt");
function render() {
tick++;
const lines = [];
for (let line = 1; line <= 3; line++) {
const span = document.createElement("span");
span.textContent = "Rebuilt line " + line + " value " + (tick * 10 + line);
lines.push(span, document.createElement("br"));
}
rebuilt.replaceChildren(...lines);
}
render();
setInterval(render, 300);
</script>`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, page)
}))
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 == "Ticking 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)
next := func() (string, bool) {
t.Helper()
if err := worker.MoveDocumentCaret(review.CaretNextLine); err != nil {
t.Fatal(err)
}
select {
case result := <-worker.Results():
got := result.DocumentCaretText
if got == "" {
got = result.DocumentPosition.Snapshot.Name
}
if result.Err != nil {
t.Fatalf("next line on ticking page: %+v", result)
}
t.Logf("line=%q moved=%v elapsed=%s capture=%s reason=%v", got, result.Moved, result.Elapsed, result.CaptureDuration, result.CaptureReason)
return got, result.Moved
case <-ctx.Done():
t.Fatal(ctx.Err())
return "", false
}
}
seen := []int{}
previous := ""
for range 14 {
got, moved := next()
if !moved {
break
}
if got == previous && strings.HasPrefix(got, "Rebuilt line") {
t.Fatalf("ticking line repeated instead of progressing: %q", got)
}
previous = got
if line, ok := strings.CutPrefix(got, "Rebuilt line "); ok {
var number int
if _, err := fmt.Sscanf(line, "%d", &number); err == nil {
seen = append(seen, number)
// Let several ticks recreate every leaf before moving on.
time.Sleep(700 * time.Millisecond)
}
continue
}
if strings.Contains(got, "After rebuilt region") {
if fmt.Sprint(seen) != "[1 2 3]" {
t.Fatalf("rebuilt lines visited = %v, want [1 2 3] in order", seen)
}
if final, _ := next(); !strings.Contains(final, "Final action") {
t.Fatalf("line after rebuilt region = %q, want Final action", final)
}
return
}
}
t.Fatalf("never reached text after the rebuilt region; rebuilt lines visited = %v", seen)
}