// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "testing" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) func hypertextCursor(t *testing.T) (*DocumentCursor, caretTextReader) { t.Helper() reader := caretTextReader{webID("/body"): "Before \ufffc after.", webID("/link"): "Read more", webID("/tail"): "Next paragraph"} node := func(path, role string, children ...string) atspi.TreeNode { snapshot := atspi.NewSnapshot(path, "", role, "", "") snapshot.BusName = webID(path).BusName snapshot.CharacterCount = utf8.RuneCountInString(reader[webID(path)]) result := atspi.TreeNode{Snapshot: snapshot} for _, child := range children { result.Children = append(result.Children, webID(child)) } return result } body := node("/body", "section", "/link") body.Snapshot.TextChildren = []atspi.TextChild{{Offset: 7, Child: webID("/link")}} tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{ node("/doc", "document web", "/body", "/tail"), body, node("/link", "link"), node("/tail", "paragraph"), }) if err != nil { t.Fatal(err) } cursor, err := NewDocumentCursor(tree) if err != nil { t.Fatal(err) } if _, err := cursor.SyncFocus(context.Background(), webID("/doc")); err != nil { t.Fatal(err) } return cursor, reader } func TestDocumentCaretReadsTextAroundEmbeddedLink(t *testing.T) { cursor, reader := hypertextCursor(t) for _, step := range []struct { command DocumentCaretCommand text, path string offset int }{ {CaretDocumentStart, "Before ", "/body", 0}, {CaretNextLine, "Read more", "/link", 0}, {CaretNextLine, " after.", "/body", 8}, {CaretNextLine, "Next paragraph", "/tail", 0}, {CaretPreviousLine, " after.", "/body", 8}, {CaretPreviousLine, "Read more", "/link", 0}, {CaretPreviousLine, "Before ", "/body", 0}, } { move, err := cursor.MoveCaret(context.Background(), reader, step.command) if err != nil || !move.Moved || move.Text != step.text || move.Position.Object.ID() != webID(step.path) || move.Position.Offset != step.offset { t.Fatalf("%s: move=%+v err=%v; want %q at %s:%d", step.command, move, err, step.text, step.path, step.offset) } } } func TestDocumentCaretCharactersCrossEmbeddedLink(t *testing.T) { cursor, reader := hypertextCursor(t) if _, err := cursor.MoveCaret(context.Background(), reader, CaretDocumentStart); err != nil { t.Fatal(err) } var text string for i := 0; i < 64; i++ { move, err := cursor.MoveCaret(context.Background(), reader, CaretNextCharacter) if err != nil { t.Fatal(err) } if move.Position.Object.ID() == webID("/tail") { break } text += move.Text } if text != "efore Read more after." { t.Fatalf("characters=%q", text) } } func TestNativeCaretKeepsHypertextParentOffsets(t *testing.T) { cursor, reader := hypertextCursor(t) if err := cursor.syncNativeCaret(context.Background(), reader, webID("/body"), 9); err != nil { t.Fatal(err) } move, err := cursor.MoveCaret(context.Background(), reader, CaretPreviousCharacter) if err != nil || move.Text != " " || move.Position.Offset != 8 || move.Position.Object.ID() != webID("/body") { t.Fatalf("native hypertext caret: text=%q offset=%d err=%v", move.Text, move.Position.Offset, err) } } func TestDocumentCaretWordsCrossEmbeddedLink(t *testing.T) { cursor, reader := hypertextCursor(t) if _, err := cursor.MoveCaret(context.Background(), reader, CaretDocumentStart); err != nil { t.Fatal(err) } for _, step := range []struct { command DocumentCaretCommand text, path string offset int }{ {CaretNextWord, "Before ", "/body", 6}, {CaretNextWord, "Read ", "/link", 4}, {CaretNextWord, "more", "/link", 9}, {CaretNextWord, "after.", "/body", 15}, {CaretPreviousWord, "after.", "/body", 9}, {CaretPreviousWord, "more", "/link", 5}, } { move, err := cursor.MoveCaret(context.Background(), reader, step.command) if err != nil || move.Text != step.text || move.Position.Object.ID() != webID(step.path) || move.Position.Offset != step.offset { t.Fatalf("%s: text=%q offset=%d err=%v; want %q at %s:%d", step.command, move.Text, move.Position.Offset, err, step.text, step.path, step.offset) } } } func TestNativeCaretOnAdjacentEmbeddedLinks(t *testing.T) { reader := caretTextReader{webID("/body"): "A\ufffc\ufffcZ", webID("/link1"): "One", webID("/link2"): "Two"} node := func(path, role string, children ...string) atspi.TreeNode { s := atspi.NewSnapshot(path, "", role, "", "") s.BusName = webID(path).BusName s.CharacterCount = utf8.RuneCountInString(reader[webID(path)]) n := atspi.TreeNode{Snapshot: s} for _, child := range children { n.Children = append(n.Children, webID(child)) } return n } body := node("/body", "paragraph", "/link1", "/link2") body.Snapshot.TextChildren = []atspi.TextChild{{Offset: 1, Child: webID("/link1")}, {Offset: 2, Child: webID("/link2")}} tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{node("/doc", "document web", "/body"), body, node("/link1", "link"), node("/link2", "link")}) if err != nil { t.Fatal(err) } for _, cmd := range []DocumentCaretCommand{CaretNextCharacter, CaretPreviousCharacter} { cursor, err := NewDocumentCursor(tree) if err != nil { t.Fatal(err) } if err = cursor.syncNativeCaret(context.Background(), reader, webID("/body"), 2); err != nil { t.Fatal(err) } move, err := cursor.MoveCaret(context.Background(), reader, cmd) t.Logf("command=%s text=%q object=%s offset=%d err=%v", cmd, move.Text, move.Position.Object.ID().Path, move.Position.Offset, err) want := "w" if cmd == CaretPreviousCharacter { want = "e" } if move.Text != want { t.Errorf("want %q", want) } } }