// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "context" "os" "os/exec" "strings" "testing" "time" ) func TestNativeBrowserLogicalLines(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" { t.Skip("requires private browser session") } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() adapter := New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab").CombinedOutput(); err != nil { t.Fatalf("focus editable lines: %v %s", err, out) } var focus ObjectID for !focus.Valid() { select { case event := <-adapter.Events(): if (event.Type == FocusEvent || event.Type == FocusedStateEvent && event.Detail1 == 1) && event.Source.Name == "Editable lines" { focus = event.Source.ID() } case <-ctx.Done(): t.Fatal(ctx.Err()) } } tree, err := adapter.CaptureDocument(ctx, focus) if err != nil { t.Fatal(err) } text := findNamedReference(t, tree, "What's on your mind?") for _, offset := range []int{0, 7, 10, 15, 20} { line, err := adapter.ReadDocumentCaret(ctx, text, offset, DocumentCaretBrailleLine) if err != nil || line.Start != 0 || line.End != 20 || line.Text != "What's on your mind?" { t.Fatalf("line at %d = %+v, %v", offset, line, err) } } remainder, err := adapter.ReadDocumentCaret(ctx, text, 7, DocumentCaretLineRemainder) if err != nil || remainder.Start != 7 || remainder.Text != "on your mind?" { t.Fatalf("remainder = %+v, %v", remainder, err) } for _, test := range []struct{ name, first string }{ {"First Second", "First"}, {"Code text", "Code"}, {"Editable lines", "First"}, } { ref := findNamedReference(t, tree, test.name) line, err := adapter.ReadDocumentCaret(ctx, ref, 0, DocumentCaretBrailleLine) if err != nil || strings.TrimSpace(line.Text) != test.first { t.Fatalf("preserved %s line = %+v, %v", test.name, line, err) } } for _, name := range []string{"Truncated preview…", "猫🐈…", "…"} { ref := findNamedReference(t, tree, name) if !ref.Valid() { t.Fatalf("ellipsis object %q missing", name) } offset := len([]rune(name)) - 1 for _, unit := range []DocumentCaretUnit{DocumentCaretLine, DocumentCaretBrailleLine, DocumentCaretLineRemainder} { line, err := adapter.ReadDocumentCaret(ctx, ref, offset, unit) if err != nil || line.End != offset+1 || !strings.HasSuffix(line.Text, "…") { t.Fatalf("ellipsis %q unit %d = %+v, %v", name, unit, line, err) } } if err := adapter.SetDocumentCaretPosition(ctx, ref, focus, offset); err != nil { t.Fatalf("place caret on ellipsis: %v", err) } // Drain the actual native caret event, exercising snapshot text, // character/word capture and logical-line repair before proceeding. for { select { case event := <-adapter.Events(): if event.Type == CaretEvent && strings.HasSuffix(event.Source.BrailleLine, "…") { goto caretCaptured } case <-ctx.Done(): t.Fatal("ellipsis caret event missing: ", ctx.Err()) } } caretCaptured: } }