// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "errors" "reflect" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) type documentBrailleReader struct { *fakeCapturer line atspi.DocumentCaretText err error calls int } func (reader *documentBrailleReader) ReadDocumentCaret(_ context.Context, _ atspi.ObjectReference, _ int, unit atspi.DocumentCaretUnit) (atspi.DocumentCaretText, error) { if unit != atspi.DocumentCaretBrailleLine { return atspi.DocumentCaretText{}, errors.New("expected raw braille line") } reader.calls++ return reader.line, reader.err } func TestDocumentBrailleReadsOnlyEnabledAcceptedNavigation(t *testing.T) { reader := &documentBrailleReader{fakeCapturer: &fakeCapturer{}, line: atspi.DocumentCaretText{Start: 10, End: 17, Text: "é猫 line"}} worker, err := NewWorker(reader, time.Second) if err != nil { t.Fatal(err) } cursor := newCaretTextCursor(t, caretTextReader{webID("/text"): "Text"}) position, err := cursor.tree.Reference(webID("/text")) if err != nil { t.Fatal(err) } worker.SyncFocus(webID("/text")) result := Result{Moved: true, DocumentCaretCommand: CaretNextCharacter, DocumentFocus: webID("/text"), DocumentPosition: DocumentPosition{Object: position, Offset: 12}} for _, test := range []struct { name string modify func(*Result) }{ {"disabled", func(*Result) { worker.SetDocumentBrailleEnabled(false) }}, {"boundary", func(r *Result) { r.Moved = false }}, {"navigation failure", func(r *Result) { r.Err = atspi.ErrStaleObject }}, {"placement failure", func(r *Result) { r.CaretErr = atspi.ErrStaleObject }}, {"retired page", func(r *Result) { r.DocumentEpoch++ }}, {"other focus", func(r *Result) { r.DocumentFocus = webID("/other") }}, {"table", func(r *Result) { r.DocumentCaretCommand = TableRight }}, {"password", func(r *Result) { r.DocumentPosition.Snapshot.Role = "password text" }}, {"explicit link", func(r *Result) { r.DocumentPosition.Snapshot = atspi.Snapshot{Role: "link", Name: "Example Game 2 copies", ExplicitName: true} }}, } { t.Run(test.name, func(t *testing.T) { worker.SetDocumentBrailleEnabled(true) r := result test.modify(&r) got := worker.readDocumentBraille(context.Background(), r) if got.DocumentBrailleLine != nil || reader.calls != 0 { t.Fatalf("unaccepted navigation read display text: %+v; calls=%d", got, reader.calls) } }) } worker.SetDocumentBrailleEnabled(true) got := worker.readDocumentBraille(context.Background(), result) if got.DocumentBrailleLine == nil || got.DocumentBrailleLine.Text != reader.line.Text || reader.calls != 1 { t.Fatalf("raw line not preserved: %+v; calls=%d", got, reader.calls) } reader.err = atspi.ErrStaleObject got = worker.readDocumentBraille(context.Background(), result) if got.DocumentBrailleLine != nil || got.Err != nil || !got.Moved || !errors.Is(got.DocumentBrailleErr, reader.err) { t.Fatalf("braille read failure changed successful navigation: %+v", got) } } func TestDocumentBrailleExpandedLineUsesNativeOffsets(t *testing.T) { reader := &documentBrailleReader{fakeCapturer: &fakeCapturer{}, line: atspi.DocumentCaretText{ Start: 0, End: 5, RawText: "a\ufffc bc", Text: "a bc", Expanded: true, }} worker, err := NewWorker(reader, time.Second) if err != nil { t.Fatal(err) } cursor := newCaretTextCursor(t, caretTextReader{webID("/text"): reader.line.RawText}) object, _ := cursor.tree.Reference(webID("/text")) worker.SyncFocus(object.ID()) worker.SetDocumentBrailleEnabled(true) result := Result{Moved: true, DocumentFocus: object.ID(), DocumentPosition: DocumentPosition{Object: object, Offset: 5}} got := worker.readDocumentBraille(context.Background(), result) if got.DocumentBrailleLine == nil || got.DocumentBrailleErr != nil { t.Fatalf("valid native line end rejected after contraction: %+v", got) } reader.line.Text = "aLong link bc" reader.line.SelectionStart, reader.line.SelectionEnd = 3, 5 reader.line.TextAttributeRuns = []atspi.TextAttributeRun{{Start: 3, End: 5, Values: map[string]string{"weight": "700"}}} result.DocumentPosition.Offset = 3 result.DocumentCaretLine = &atspi.DocumentLine{Contents: []atspi.DocumentContent{{ Reference: object, Snapshot: atspi.Snapshot{BusName: object.ID().BusName, Path: object.ID().Path}, Start: 3, End: 5, RawText: "bc", }}} got = worker.readDocumentBraille(context.Background(), result) snapshot := got.DocumentCaretLine.Contents[0].Snapshot if snapshot.SelectionStart != 3 || snapshot.SelectionEnd != 5 || !reflect.DeepEqual(snapshot.TextAttributeRuns, reader.line.TextAttributeRuns) { t.Fatalf("native suffix indicators lost after display expansion: %+v", snapshot) } if result.DocumentCaretLine.Contents[0].Snapshot.SelectionEnd != 0 { t.Fatal("enrichment mutated the original line") } result.DocumentPosition.Offset = 6 got = worker.readDocumentBraille(context.Background(), result) if got.DocumentBrailleLine != nil || !errors.Is(got.DocumentBrailleErr, atspi.ErrNoDocumentCaret) { t.Fatalf("display length accepted an invalid native offset: %+v", got) } }