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.
146 lines
4.1 KiB
Go
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)
|
|
}
|