Expand embedded objects in braille document lines

Structural navigation to a heading whose native text is a single embedded-object placeholder for its child link showed only that placeholder in braille while speech used the expanded title. Braille line reads now expand embedded objects like speech capture, preserving RawText and native offsets; expanded lines carry no cell routes or native selection/attribute dots and only a line-start cursor. Frame assembly strips any remaining placeholders and falls back to displayed text so a placeholder never reaches the display. Regression tests cover placeholder-only, mixed, expanded, assembled, and empty headings plus native linked-heading and Firefox mixed-line braille. Maintainer confirms physical braille and speech agree on YouTube headings.
This commit is contained in:
hjozwiak committed 2026-11-03 00:17:08 -05:00
1 parent 60f7f7a2e9
commit 8ce5e5cdad
7 files changed
+245 -21

No files matched your search

+119
View File
@@ -362,6 +362,125 @@ func TestLinkedHeadingBrailleKeepsHeadingAndLinkIndicators(t *testing.T) {
}
}
// A heading whose native text is a single embedded-object placeholder for
// its child link must braille the same displayed title speech uses, never
// the placeholder itself.
func TestBrowseBrailleEmbeddedObjectHeadingUsesDisplayedText(t *testing.T) {
options := config.DefaultSettings().Braille
heading := atspi.NewSnapshot("/heading", "Cool video title", "heading", "", "")
heading.BusName = ":1.999"
heading.HeadingLevel = 3
link := atspi.NewSnapshot("/link", "Cool video title", "link", "", "")
link.BusName = ":1.999"
result := review.Result{
DocumentPosition: review.DocumentPosition{Snapshot: heading, LinkedHeading: &link},
DocumentBrailleLine: &atspi.DocumentCaretText{Start: 0, End: 1, Text: "\ufffc", RawText: "\ufffc"},
}
frame := brailleDocumentFrame(result, options)
if frame.Text != "Cool video title h3 lnk" || frame.Cursor != 0 || len(frame.Routes) != 0 {
t.Fatalf("embedded-object heading frame = %+v", frame)
}
// An expanded line on the snapshot wins over the accessible name.
heading.Text = "Expanded title"
result.DocumentPosition.Snapshot = heading
frame = brailleDocumentFrame(result, options)
if frame.Text != "Expanded title h3 lnk" || len(frame.Routes) != 0 {
t.Fatalf("expanded heading frame = %+v", frame)
}
// No displayed text anywhere: role markers only, no placeholder.
heading.Name, heading.Text = "", ""
result.DocumentPosition.Snapshot = heading
result.DocumentPosition.LinkedHeading = nil
frame = brailleDocumentFrame(result, options)
// The leading cell is the usual empty-line cursor cell.
if strings.ContainsRune(frame.Text, '\ufffc') || strings.TrimSpace(frame.Text) != "h3" {
t.Fatalf("empty heading frame = %+v", frame)
}
}
// An expanded braille line no longer maps cells to native offsets: it must
// carry no routes and no native selection dots, and only a caret at the
// start of the native line can be shown.
func TestBrowseBrailleExpandedLineHasNoNativeMapping(t *testing.T) {
options := config.DefaultSettings().Braille
options.SelectionIndicator, options.LinkIndicator = 0x40, 0x80
heading := atspi.NewSnapshot("/heading", "Cool video title", "heading", "", "")
heading.BusName = ":1.999"
heading.HeadingLevel = 3
link := atspi.NewSnapshot("/link", "Cool video title", "link", "", "")
link.BusName = ":1.999"
doc := atspi.Snapshot{BusName: heading.BusName, Path: "/document", Role: "document web"}
tree, err := atspi.NewAccessibleTree(doc.ID(), []atspi.TreeNode{{Snapshot: doc, Children: []atspi.ObjectID{heading.ID()}}, {Snapshot: heading}})
if err != nil {
t.Fatal(err)
}
docRef, _ := tree.Reference(doc.ID())
result := review.Result{DocumentEpoch: 1, DocumentFocus: doc.ID(),
DocumentPosition: review.DocumentPosition{Document: docRef, Snapshot: heading, LinkedHeading: &link},
DocumentBrailleLine: &atspi.DocumentCaretText{Start: 0, End: 1, Text: "Cool video title", RawText: "\ufffc", Expanded: true, SelectionStart: 0, SelectionEnd: 1}}
frame := brailleDocumentFrame(result, options)
if frame.Text != "Cool video title h3 lnk" || frame.Cursor != 0 || len(frame.Routes) != 0 || frame.Document == nil {
t.Fatalf("expanded frame = %+v", frame)
}
// Link dots are display-relative and survive; selection dots are
// native-offset based and must not appear on expanded text.
if len(frame.Dots) != utf8.RuneCountInString(frame.Text) || frame.Dots[0] != 0x80 || frame.Dots[15] != 0x80 || frame.Dots[16] != 0 {
t.Fatalf("expanded dots = %v", frame.Dots)
}
result.DocumentPosition.Offset = 1
frame = brailleDocumentFrame(result, options)
if frame.Cursor != -1 {
t.Fatalf("interior native offset must hide the cursor on expanded text: %+v", frame)
}
}
func TestBrailleFocusEmbeddedObjectLineUsesDisplayedText(t *testing.T) {
heading := atspi.NewSnapshot("/heading", "Cool video title", "heading", "", "Cool video title")
heading.HeadingLevel = 3
heading.TextCaretKnown = true
heading.BrailleLine = "\ufffc"
heading.BrailleCaret = 0
frame := brailleFocusFrameWithOptions(heading, config.DefaultSettings().Braille)
if frame.Text != "Cool video title h3" || frame.Cursor != 0 || len(frame.Routes) != 0 {
t.Fatalf("focus heading frame = %+v", frame)
}
// Mixed native text keeps its real surrounding text; only the
// placeholder is dropped when no expansion is available.
heading.Text = "Before \ufffc after"
heading.BrailleLine = "Before \ufffc after"
frame = brailleFocusFrameWithOptions(heading, config.DefaultSettings().Braille)
if strings.ContainsRune(frame.Text, '\ufffc') || !strings.Contains(frame.Text, "Before") || !strings.Contains(frame.Text, "after") || len(frame.Routes) != 0 {
t.Fatalf("mixed focus frame = %+v", frame)
}
}
func TestBrowseBrailleAssembledLineDropsEmbeddedObjectPlaceholders(t *testing.T) {
heading := atspi.NewSnapshot("/heading", "Cool video title", "heading", "", "")
heading.BusName = ":1.999"
heading.HeadingLevel = 3
static := atspi.NewSnapshot("/static", "", "static", "", "")
static.BusName = ":1.999"
result := review.Result{
DocumentPosition: review.DocumentPosition{Snapshot: static},
DocumentCaretLine: &atspi.DocumentLine{Contents: []atspi.DocumentContent{
{Snapshot: heading, Start: 0, End: 1, RawText: "\ufffc"},
{Snapshot: static, Start: 0, End: 16, RawText: "Cool video title"},
}},
DocumentCaretContext: []atspi.Snapshot{{Role: "heading", HeadingLevel: 3}},
}
frame := brailleDocumentFrame(result, config.DefaultSettings().Braille)
if strings.ContainsRune(frame.Text, '\ufffc') || strings.Count(frame.Text, "Cool video title") != 1 {
t.Fatalf("assembled frame = %+v", frame)
}
// A placeholder-only assembled line falls back to displayed text.
result.DocumentCaretLine.Contents = result.DocumentCaretLine.Contents[:1]
result.DocumentPosition.Snapshot = heading
frame = brailleDocumentFrame(result, config.DefaultSettings().Braille)
if frame.Text != "Cool video title h3" || len(frame.Routes) != 0 {
t.Fatalf("placeholder-only assembled frame = %+v", frame)
}
}
func (*tableBrailleCapturer) ReadDocumentCaret(context.Context, atspi.ObjectReference, int, atspi.DocumentCaretUnit) (atspi.DocumentCaretText, error) {
return atspi.DocumentCaretText{}, nil
}