// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) func tooltip_cursor(t *testing.T, focusable, editable, hypertext bool) (*DocumentCursor, caretTextReader) { t.Helper() reader := caretTextReader{webID("/before"): "Project", webID("/tiptext"): "Project", webID("/after"): "After"} node := func(path, role string, children ...atspi.ObjectID) atspi.TreeNode { return atspi.TreeNode{Snapshot: atspi.Snapshot{BusName: webID(path).BusName, Path: path, Role: role, StatesKnown: true, CharacterCount: len(reader[webID(path)])}, Children: children} } tip := node("/tip", "tool tip", webID("/tiptext")) tip.Snapshot.Focusable, tip.Snapshot.Editable = focusable, editable if hypertext { // Text around an embedded child must not bypass subtree exclusion. tip.Snapshot.CharacterCount = 3 tip.Snapshot.TextChildren = []atspi.TextChild{{Offset: 1, Child: webID("/tiptext")}} reader[webID("/tip")] = "x\ufffcy" } tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{ node("/doc", "document web", webID("/before"), webID("/tip"), webID("/after")), node("/before", "static"), tip, node("/tiptext", "static"), node("/after", "static"), }) if err != nil { t.Fatal(err) } cursor, err := NewDocumentCursor(tree) if err != nil { t.Fatal(err) } if _, err := cursor.SyncFocus(context.Background(), webID("/before")); err != nil { t.Fatal(err) } return cursor, reader } func TestDocumentCaretSkipsPassiveTooltip(t *testing.T) { for _, hypertext := range []bool{false, true} { for _, command := range []DocumentCaretCommand{CaretNextLine, CaretNextCharacter, CaretNextWord} { t.Run(command.String(), func(t *testing.T) { cursor, reader := tooltip_cursor(t, false, false, hypertext) if command != CaretNextLine { cursor.places[0].offset = len("Project") - 1 } move, err := cursor.MoveCaret(context.Background(), reader, command) if err != nil || !move.Moved || move.Position.Object.ID() != webID("/after") { t.Fatalf("forward: %+v, %v", move, err) } reverse := CaretPreviousLine if command == CaretNextCharacter { reverse = CaretPreviousCharacter } else if command == CaretNextWord { reverse = CaretPreviousWord } cursor.places[0].offset = 0 move, err = cursor.MoveCaret(context.Background(), reader, reverse) if err != nil || !move.Moved || move.Position.Object.ID() != webID("/before") { t.Fatalf("backward: %+v, %v", move, err) } }) } } } func TestDocumentCaretPreservesInteractiveTooltip(t *testing.T) { for _, focusable := range []bool{false, true} { cursor, reader := tooltip_cursor(t, focusable, !focusable, false) move, err := cursor.MoveCaret(context.Background(), reader, CaretNextLine) if err != nil || !move.Moved || move.Position.Object.ID() != webID("/tiptext") { t.Fatalf("interactive tooltip: %+v, %v", move, err) } } } func TestReadAllSkipsPassiveTooltip(t *testing.T) { cursor, reader := tooltip_cursor(t, false, false, true) worker, err := NewWorker(&readAllCapturer{tree: cursor.tree, caretTextReader: reader}, time.Second) if err != nil { t.Fatal(err) } worker.SyncFocus(webID("/before")) result := worker.readAll(context.Background(), cursor) if result.Err != nil || len(result.ReadAllLines) != 2 { t.Fatalf("Read All: %+v", result) } for index, want := range []string{"Project", "After"} { if got := result.ReadAllLines[index].Contents[0].Text; got != want { t.Fatalf("line %d = %q, want %q", index, got, want) } } }