// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "errors" "testing" "time" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) func mirroredCaretCursor(t *testing.T, text caretTextReader, multiple bool) *DocumentCursor { t.Helper() var nodes []atspi.TreeNode for _, path := range []string{"/doc", "/paragraph", "/text", "/other"} { snapshot := atspi.Snapshot{BusName: webID(path).BusName, Path: path, Role: "static", CharacterCount: utf8.RuneCountInString(text[webID(path)])} node := atspi.TreeNode{Snapshot: snapshot} if path == "/doc" { node.Snapshot.Role = "document web" node.Children = []atspi.ObjectID{webID("/paragraph")} } if path == "/paragraph" { node.Snapshot.Role = "paragraph" node.Children = []atspi.ObjectID{webID("/text")} if multiple { node.Children = append(node.Children, webID("/other")) } } if path == "/other" && !multiple { continue } nodes = append(nodes, node) } tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes) if err != nil { t.Fatal(err) } cursor, err := NewDocumentCursor(tree) if err != nil { t.Fatal(err) } return cursor } func TestNativeCaretSelectionEndpointTransfersToTextChild(t *testing.T) { text := caretTextReader{webID("/paragraph"): "When autocomplete", webID("/text"): "When autocomplete"} cursor := mirroredCaretCursor(t, text, false) for _, offset := range []int{6, 11, 17, 9, 0} { if err := cursor.syncNativeCaret(context.Background(), text, webID("/paragraph"), offset); err != nil { t.Fatal(err) } move, err := cursor.MoveCaret(context.Background(), text, CaretPreviousCharacter) if err != nil { t.Fatal(err) } if offset > 0 && (move.Position.Object.ID() != webID("/text") || move.Position.Offset != offset-1) { t.Fatalf("offset %d: move = %+v", offset, move) } } } func TestNativeCaretRejectsAmbiguousOffsetDomains(t *testing.T) { for _, test := range []struct { name, child string multiple bool offset int }{ {"different text same count", "other", false, 2}, {"different count", "shorter", false, 2}, {"embedded text", "hello", false, 2}, {"multiple children", "hello", true, 2}, {"invalid offset", "hello", false, 6}, } { t.Run(test.name, func(t *testing.T) { parent := "hello" if test.name == "embedded text" { parent = "\ufffc" } text := caretTextReader{webID("/paragraph"): parent, webID("/text"): test.child} cursor := mirroredCaretCursor(t, text, test.multiple) if err := cursor.syncNativeCaret(context.Background(), text, webID("/paragraph"), test.offset); !errors.Is(err, atspi.ErrNoDocumentCaret) { t.Fatalf("sync = %v", err) } }) } } func TestWorkerNativeCaretHandoffPrecedesNextArrow(t *testing.T) { text := caretTextReader{webID("/text"): "When autocomplete"} navigator := &boundaryNavigator{fakeCapturer: &fakeCapturer{}, caretTextReader: text} worker, err := NewWorker(navigator, time.Second) if err != nil { t.Fatal(err) } worker.SyncFocus(webID("/text")) cursor := newCaretTextCursor(t, text) syncResult, cursor := worker.syncDocumentCaret(context.Background(), cursor, request{nativeCaretFocus: webID("/text"), nativeCaretOffset: 11}) if syncResult.Err != nil || syncResult.DocumentPosition.Offset != 11 { t.Fatalf("handoff = %+v", syncResult) } result, _ := worker.moveDocumentCaret(context.Background(), cursor, CaretNextCharacter, 0) if result.Err != nil || result.DocumentPosition.Offset != 12 || result.DocumentCaretText != "p" || navigator.offset != 12 { t.Fatalf("move after selection = %+v; native offset = %d", result, navigator.offset) } worker.SyncFocus(webID("/next")) stale, _ := worker.syncDocumentCaret(context.Background(), cursor, request{nativeCaretFocus: webID("/text"), nativeCaretOffset: 2}) if !errors.Is(stale.Err, atspi.ErrStaleObject) { t.Fatalf("stale sync = %+v", stale) } }