Files
cthulhu/internal/review/document_visual_line_native_test.go

182 lines
6.2 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"os"
"os/exec"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
func TestNativeBrowserFragmentLines(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()
adapter := atspi.New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
// Reach the textarea without enabling the browser's native caret mode.
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab", "Tab").CombinedOutput(); err != nil {
t.Fatalf("focus: %v %s", err, output)
}
var focus atspi.ObjectID
for !focus.Valid() {
select {
case event := <-adapter.Events():
if (event.Type == atspi.FocusEvent || event.Type == atspi.FocusedStateEvent && event.Detail1 == 1) && event.Source.Name == "Fragment probe focus" {
focus = event.Source.ID()
}
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
tree, err := adapter.CaptureDocument(ctx, focus)
if err != nil {
t.Fatal(err)
}
cursor, err := NewDocumentCursor(tree)
if err != nil {
t.Fatal(err)
}
if _, err := cursor.SyncFocus(ctx, focus); err != nil {
t.Fatal(err)
}
reference, err := tree.Reference(focus)
if err != nil {
t.Fatal(err)
}
document, err := tree.Document(ctx, reference)
if err != nil {
t.Fatal(err)
}
glyphCount := 0
for _, node := range document.Nodes {
if node.Snapshot.Role == "static" && node.Snapshot.Name == "\uf005" && node.Snapshot.CharacterCount == 1 {
glyphCount++
}
}
if glyphCount < 5 {
t.Fatalf("fixture did not expose its star text objects: %d", glyphCount)
}
for _, step := range []struct {
command DocumentCaretCommand
want string
}{
{CaretDocumentStart, "Fragmented response"},
{CaretNextLine, "One two three."},
{CaretNextLine, "Four five six."},
{CaretPreviousLine, "One two three."},
{CaretLineEnd, "."},
{CaretNextLine, "Four five six."},
{CaretNextLine, "Next paragraph."},
{CaretNextLine, "Before"},
{CaretNextLine, "ordinary link"},
{CaretNextLine, "after."},
{CaretNextLine, "Split heading"},
{CaretNextLine, "Code first"},
{CaretNextLine, ""}, // Preserve the provider's separate preformatted newline.
{CaretNextLine, "Code second"},
{CaretNextLine, "Member rank"},
{CaretNextLine, "Join Date"}, // Skip the five separate decorative stars.
{CaretPreviousLine, "Member rank"},
{CaretNextLine, "Join Date"},
} {
move, err := cursor.MoveCaret(ctx, adapter, step.command)
if err != nil || !move.Moved || strings.TrimSpace(move.Text) != step.want {
t.Fatalf("%s: %+v, %v; want %q", step.command, move, err, step.want)
}
}
control, err := cursor.MoveCaret(ctx, adapter, CaretNextLine)
if err != nil || !control.Moved || control.Position.Snapshot.Role != "push button" && control.Position.Snapshot.Role != "button" || control.Position.Snapshot.Name != "Rating details" {
t.Fatalf("labeled icon control: %+v, %v", control, err)
}
for _, want := range []string{"Mixed \uf005 text.", "★"} {
move, err := cursor.MoveCaret(ctx, adapter, CaretNextLine)
if err != nil || !move.Moved || strings.TrimSpace(move.Text) != want {
t.Fatalf("meaningful symbol: %+v, %v; want %q", move, err, want)
}
}
if _, err := cursor.MoveCaret(ctx, adapter, CaretDocumentStart); err != nil {
t.Fatal(err)
}
inline, err := cursor.MoveCaret(ctx, adapter, CaretNextLine)
if err != nil {
t.Fatal(err)
}
worker, err := NewWorker(adapter, 3*time.Second)
if err != nil {
t.Fatal(err)
}
worker.SyncFocus(focus)
worker.SetDocumentBrailleEnabled(true)
if inline.Line == nil || len(inline.Line.Contents) < 2 {
t.Fatalf("missing inline fragments: %+v", inline)
}
content := inline.Line.Contents[1]
displayRoute := &documentBrailleRequest{ctx: ctx, focus: focus, document: inline.Position.Document.ID(), target: content.Snapshot.ID(),
line: content.RawText, start: content.Start, offset: content.Start + 1, done: make(chan error, 1)}
routed := worker.route_document_braille(ctx, cursor, request{brailleRoute: displayRoute})
if routed.Err != nil || !routed.Moved || routed.DocumentPosition.Offset != content.Start+1 || routed.DocumentCaretLine == nil || len(routed.DocumentCaretLine.Contents) < 2 {
t.Fatalf("native inline braille route = %+v", routed)
}
if err := <-displayRoute.done; err != nil {
t.Fatal(err)
}
worker.readDocumentBraille(ctx, routed)
// A mismatched displayed source must be rejected by the native owner.
ref, _ := tree.Reference(content.Snapshot.ID())
if err := adapter.RouteBraille(ctx, atspi.BrailleRoute{Position: ref, Focus: focus, Line: "changed", Start: content.Start, Offset: content.Start}); err == nil {
t.Fatal("native routing accepted changed display text")
}
if _, err := cursor.MoveCaret(ctx, adapter, CaretDocumentStart); err != nil {
t.Fatal(err)
}
if _, err := cursor.MoveCaret(ctx, adapter, CaretNextLine); err != nil {
t.Fatal(err)
}
result := worker.readAll(ctx, cursor)
if result.Err != nil || len(result.ReadAllLines) < 3 {
t.Fatalf("Read All: %+v", result)
}
for index, want := range []string{"One two three.", "Four five six.", "Next paragraph."} {
if got := result.ReadAllLines[index].Contents[0].Text; got != want {
t.Fatalf("Read All line %d: %q, want %q", index, got, want)
}
}
rankFound := false
for index, line := range result.ReadAllLines {
if line.Contents[0].Text == "Member rank" {
rankFound = true
if index+1 >= len(result.ReadAllLines) || result.ReadAllLines[index+1].Contents[0].Text != "Join Date" {
t.Fatal("Read All stopped on decorative rank icons")
}
}
}
if !rankFound {
t.Fatal("Read All did not reach member rank")
}
// Fragmented headings must also hand Down to following content as a unit.
if _, err := cursor.MoveCaret(ctx, adapter, CaretDocumentStart); err != nil {
t.Fatal(err)
}
move, err := cursor.Move(ctx, DocumentHeading, 2, false)
if err != nil || !move.Moved {
t.Fatalf("heading: %+v, %v", move, err)
}
next, err := cursor.MoveCaret(ctx, adapter, CaretNextLine)
if err != nil || next.Text != "Code first" {
t.Fatalf("Down after split heading: %+v, %v", next, err)
}
}