Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package review
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
func hypertextCursor(t *testing.T) (*DocumentCursor, caretTextReader) {
|
||||
t.Helper()
|
||||
return hypertextCursorWithLabel(t, "")
|
||||
}
|
||||
|
||||
func hypertextCursorWithLabel(t *testing.T, label string) (*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")}}
|
||||
link := node("/link", "link")
|
||||
if label != "" {
|
||||
link.Snapshot.Name, link.Snapshot.ExplicitName, link.Snapshot.CharacterCount = label, true, 0
|
||||
}
|
||||
tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{
|
||||
node("/doc", "document web", "/body", "/tail"), body, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentCaretEmbeddedNamedLinkKeepsLabelOffsets(t *testing.T) {
|
||||
cursor, reader := hypertextCursorWithLabel(t, "É猫 Info")
|
||||
for _, step := range []struct {
|
||||
command DocumentCaretCommand
|
||||
text, path string
|
||||
offset int
|
||||
}{
|
||||
{CaretDocumentStart, "Before ", "/body", 0},
|
||||
{CaretNextLine, "", "/link", 0},
|
||||
{CaretNextCharacter, "猫", "/link", 1},
|
||||
{CaretNextWord, "Info", "/link", 7},
|
||||
{CaretNextCharacter, " ", "/body", 8},
|
||||
{CaretPreviousCharacter, "o", "/link", 6},
|
||||
{CaretLineStart, "É", "/link", 0},
|
||||
{CaretPreviousCharacter, " ", "/body", 6},
|
||||
} {
|
||||
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: text=%q at %s:%d err=%v; want %q at %s:%d", step.command, move.Text, move.Position.Object.ID().Path, move.Position.Offset, err, step.text, step.path, step.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentCaretHypertextCannotEscapePasswordSubtree(t *testing.T) {
|
||||
cursor, _ := hypertextCursor(t)
|
||||
doc, err := cursor.tree.Document(context.Background(), cursor.places[0].object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Even if editable state is missing, no nested hypertext may reintroduce
|
||||
// the text suppressed by the password ancestor.
|
||||
doc.Nodes[0].Snapshot.Role = "password text"
|
||||
if ranges := caretNodeRanges(doc.Nodes); len(ranges) != 0 {
|
||||
t.Fatalf("password subtree exposed caret ranges: %+v", ranges)
|
||||
}
|
||||
}
|
||||
|
||||
// Plain-text changes need not change accessible children or their identities.
|
||||
func TestDocumentCaretRefreshesChangedHypertextAncestor(t *testing.T) {
|
||||
for _, command := range []DocumentCaretCommand{CaretNextCharacter, CaretPreviousCharacter, CaretNextWord, CaretPreviousWord, CaretNextLine, CaretPreviousLine, CaretReadLine} {
|
||||
t.Run(command.String(), func(t *testing.T) {
|
||||
reader := caretTextReader{webID("/body"): "\ufffc\ufffc", webID("/one"): "A", webID("/two"): "B"}
|
||||
makeNode := 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 := makeNode("/body", "paragraph", "/one", "/two")
|
||||
body.Snapshot.TextChildren = []atspi.TextChild{{Offset: 0, Child: webID("/one")}, {Offset: 1, Child: webID("/two")}}
|
||||
nodes := []atspi.TreeNode{makeNode("/doc", "document web", "/body"), body, makeNode("/one", "link"), makeNode("/two", "link")}
|
||||
tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursor, _ := NewDocumentCursor(tree)
|
||||
path := "/one"
|
||||
if command == CaretPreviousCharacter || command == CaretPreviousWord || command == CaretPreviousLine {
|
||||
path = "/two"
|
||||
}
|
||||
if _, err = cursor.SyncFocus(context.Background(), webID(path)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader[webID("/body")] = "\ufffc inserted \ufffc"
|
||||
tree.InvalidateText(webID("/body"))
|
||||
if command == CaretReadLine {
|
||||
cursor.places[0].offset = 1
|
||||
}
|
||||
before := cursor.places[0]
|
||||
move, err := cursor.MoveCaret(context.Background(), reader, command)
|
||||
if !errors.Is(err, atspi.ErrStaleText) {
|
||||
t.Fatalf("skipped parent insertion: move=%+v error=%v", move, err)
|
||||
}
|
||||
if after := cursor.places[0]; after.object != before.object || after.offset != before.offset {
|
||||
t.Fatal("failed move changed position")
|
||||
}
|
||||
// A successful refresh must expose the insertion on the retry,
|
||||
// retaining both embedded link identities and the original position.
|
||||
nodes[1].Snapshot.CharacterCount = utf8.RuneCountInString(reader[webID("/body")])
|
||||
nodes[1].Snapshot.TextChildren = []atspi.TextChild{{Offset: 0, Child: webID("/one")}, {Offset: nodes[1].Snapshot.CharacterCount - 1, Child: webID("/two")}}
|
||||
if err := tree.Replace(webID("/doc"), nodes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
move, err = cursor.MoveCaret(context.Background(), reader, command)
|
||||
if err != nil || !move.Moved || move.Position.Object.ID() != webID("/body") {
|
||||
t.Fatalf("refreshed move skipped insertion: %+v error=%v", move, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentCaretLocalMoveSurvivesHypertextInsertion(t *testing.T) {
|
||||
cursor, reader := hypertextCursor(t)
|
||||
if _, err := cursor.SyncFocus(context.Background(), webID("/link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursor.tree.InvalidateText(webID("/body"))
|
||||
move, err := cursor.MoveCaret(context.Background(), reader, CaretNextCharacter)
|
||||
if err != nil || move.Text != "e" {
|
||||
t.Fatalf("local character: %+v %v", move, err)
|
||||
}
|
||||
if _, err = cursor.MoveCaret(context.Background(), reader, CaretNextLine); !errors.Is(err, atspi.ErrStaleText) {
|
||||
t.Fatalf("crossing changed hypertext: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user