// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "strings" "testing" "time" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) func editable_caret_cursor(t *testing.T) (*DocumentCursor, caretTextReader) { t.Helper() reader := caretTextReader{ webID("/before"): "Suggested wording", webID("/editor"): "\ufffc\ufffc", webID("/first"): "Review the code.\nReport actionable issues.", webID("/second"): "Explain the evidence.", webID("/secret"): "Private fixture text", webID("/after"): "Following paragraph", } node := func(path, role string, editable bool, children ...string) atspi.TreeNode { snapshot := atspi.NewSnapshot(path, "", role, "", "") snapshot.BusName = webID(path).BusName snapshot.Editable = editable 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 } tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{ node("/doc", "document web", false, "/before", "/editor", "/password", "/after"), node("/before", "static", false), node("/editor", "entry", true, "/paragraph", "/second"), node("/paragraph", "paragraph", true, "/first"), node("/first", "static", true), node("/second", "static", true), node("/password", "password text", true, "/secret"), // Even a provider which omits editable state on a password child // must not expose that child's text through browse navigation. node("/secret", "static", false), node("/after", "static", false), }) 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 TestDocumentCaretReadsEditableDescendants(t *testing.T) { ctx := context.Background() cursor, reader := editable_caret_cursor(t) for _, step := range []struct { command DocumentCaretCommand want string }{ {CaretNextLine, "Review the code."}, {CaretNextLine, "Report actionable issues."}, {CaretNextLine, "Explain the evidence."}, {CaretNextLine, "Following paragraph"}, {CaretPreviousLine, "Explain the evidence."}, {CaretPreviousLine, "Report actionable issues."}, {CaretLineStart, "R"}, {CaretNextCharacter, "e"}, {CaretNextWord, "Report"}, {CaretPreviousLine, "Review the code."}, {CaretPreviousLine, "Suggested wording"}, } { move, err := cursor.MoveCaret(ctx, reader, step.command) if err != nil || !move.Moved || strings.TrimSpace(move.Text) != step.want { t.Fatalf("%s: %q at %s, moved %t, %v; want %q", step.command, move.Text, move.Position.Snapshot.Path, move.Moved, err, step.want) } } } func TestReadAllIncludesEditableDescendants(t *testing.T) { cursor, reader := editable_caret_cursor(t) capturer := &readAllCapturer{tree: cursor.tree, caretTextReader: reader} worker, err := NewWorker(capturer, time.Second) if err != nil { t.Fatal(err) } worker.SyncFocus(webID("/before")) result := worker.readAll(context.Background(), cursor) want := []string{"Suggested wording", "Review the code.", "Report actionable issues.", "Explain the evidence.", "Following paragraph"} if result.Err != nil || len(result.ReadAllLines) != len(want) { t.Fatalf("Read All: %d lines, %v; want %d", len(result.ReadAllLines), result.Err, len(want)) } for index, line := range result.ReadAllLines { if got := strings.TrimSpace(line.Contents[0].Text); got != want[index] { t.Fatalf("Read All line %d: %q; want %q", index, got, want[index]) } } }