156 lines
4.7 KiB
Go
156 lines
4.7 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 TestNativeBrowserRevealedHeadings(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>Revealed headings regression</title>
|
|
<style>.hidden { display: none }</style>
|
|
<section><h2>Across</h2><div data-clues>`)
|
|
for i := 1; i <= 12; i++ {
|
|
class := ""
|
|
if i > 5 {
|
|
class = "hidden"
|
|
}
|
|
fmt.Fprintf(w, `<div class="%s"><div><span>%d</span><h3><a href="/clue/%d">Clue %d</a></h3><div><button>A</button><button>B</button><button>C</button></div></div><div><button>Answer</button><button>Hint</button></div></div>`, class, i, i, i)
|
|
}
|
|
fmt.Fprint(w, `</div><div><button onclick="document.querySelectorAll('[data-clues] > div').forEach((e,i) => { if (i > 4) e.classList.toggle('hidden') }); this.firstChild.firstChild.data = this.textContent === 'More clues' ? 'Fewer clues' : 'More clues' "><span>More clues</span></button></div></section>
|
|
<h2>Following section</h2>`)
|
|
}))
|
|
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", strings.Fields(string(window))[0])
|
|
command("key", "--clearmodifiers", "ctrl+l")
|
|
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 == "Revealed headings 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)
|
|
result := func(want string) {
|
|
t.Helper()
|
|
select {
|
|
case result := <-worker.Results():
|
|
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)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
if err := worker.MoveDocument(review.DocumentHeading, 0, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result("Across")
|
|
if err := worker.MoveDocument(review.DocumentHeading, 0, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result("Following section")
|
|
if err := worker.MoveDocumentCaret(review.CaretPreviousLine); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result("More clues")
|
|
activate := func(want string) {
|
|
t.Helper()
|
|
if err := worker.ActivateDocumentCaret(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case result := <-worker.Results():
|
|
if result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Type == atspi.AccessibleNameEvent && event.Source.Name == want && event.Source.Role == "button" {
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("clue toggle was not observed: ", ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
activate("Fewer clues")
|
|
// The added headings precede the button. Browse in both directions
|
|
// without ever tabbing into the new content.
|
|
for _, step := range []struct {
|
|
backwards bool
|
|
want string
|
|
}{
|
|
{true, "Clue 12"},
|
|
{true, "Clue 11"},
|
|
{false, "Clue 12"},
|
|
{false, "Following section"},
|
|
} {
|
|
if err := worker.MoveDocument(review.DocumentHeading, 0, step.backwards); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result(step.want)
|
|
}
|
|
if err := worker.MoveDocumentCaret(review.CaretPreviousLine); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result("Fewer clues")
|
|
activate("More clues")
|
|
if err := worker.MoveDocument(review.DocumentHeading, 0, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result("Clue 5")
|
|
}
|