Thunderbird HTML messages expose readable text on containers around embedded objects. The browse cursor previously considered only text leaves, and native document capture omitted heading/link character counts. This skipped message content, headings and link text. A caret event following Tab could also replace a complete link announcement with its first character. Capture owned hypertext child identities and native placeholder offsets on the GLib thread, releasing native interfaces before returning. Require every child to occupy one embedded-object character so providers exposing duplicate text through static descendants retain their ordinary traversal. Traverse native text ranges before and after embedded children without changing their offset domains. Preserve character, word, line and document-boundary navigation; normalize native caret and Braille routing positions at adjacent embedded links. Keep the Braille cursor on the correct fragment when a parent contributes text more than once to an assembled line. Ignore same-input link caret echoes after focus while retaining subsequent arrow and selection feedback. Add regression tests, extend the private Firefox fixture, and record behavior and remaining acceptance in the parity ledger. Validation: - Full Go test suite with count=1; focused app/atspi/review race checks. - go vet ./...; reader and brlapi helper builds. - Private Firefox baseline, mixed-parent offsets and tab navigation. - Private Chromium fragments, named links, document navigation and selection. - Parity ledger validation and whitespace checks. The existing Firefox tab test intermittently timed out on both an untouched HEAD copy and the changed tree; the final run passed. The unchanged TestRunReachesSoundStopAfterEarlierServiceStopError also failed once during startup cancellation and passed in subsequent full-suite and race runs. Both installed binaries were verified and the reader reloaded in its original desktop session. The maintainer confirmed readable message text and later working links after an earlier intermittent omission. That omission remains untraced; repeat live acceptance and physical Braille verification remain open.
414 lines
18 KiB
Go
414 lines
18 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/braille"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/preferences"
|
|
"git.stormux.org/storm/cthulhu/internal/review"
|
|
)
|
|
|
|
type frameRecorder struct{ frames chan braille.Frame }
|
|
|
|
type flashRecorder struct{ messages []string }
|
|
|
|
func (*flashRecorder) EnqueueFrame(braille.Frame) {}
|
|
func (recorder *flashRecorder) Flash(message string, _ config.BrailleOptions) bool {
|
|
recorder.messages = append(recorder.messages, message)
|
|
return true
|
|
}
|
|
|
|
func TestSystemMessageFlashesOnBraille(t *testing.T) {
|
|
live := &liveSettings{}
|
|
live.Store(config.DefaultSettings())
|
|
braille := &flashRecorder{}
|
|
queue := outputQueue{speech: &remoteSpeechQueue{}, braille: braille, live: live}
|
|
queue.Enqueue(speechRequest{Text: "Copied", Role: "system", Cause: "clipboard"})
|
|
queue.Enqueue(speechRequest{Text: "Current line", Role: "system", Cause: "document-caret-line"})
|
|
if len(braille.messages) != 1 || braille.messages[0] != "Copied" {
|
|
t.Fatalf("flash messages = %v", braille.messages)
|
|
}
|
|
}
|
|
|
|
func TestBrailleOptionsChangePresentation(t *testing.T) {
|
|
options := config.DefaultSettings().Braille
|
|
options.DisableEOL = true
|
|
options.WordWrap = true
|
|
options.SelectionIndicator = 0x40
|
|
entry := atspi.Snapshot{Role: "entry", Name: "Title", BrailleLine: "abcd", BrailleLineStart: 10,
|
|
BrailleCaret: 12, TextCaretKnown: true, SelectionStart: 11, SelectionEnd: 13}
|
|
frame := brailleFocusFrameWithOptions(entry, options)
|
|
if frame.Text != "Title abcd" || !frame.WordWrap || len(frame.Dots) != 10 || frame.Dots[7] != 0x40 || frame.Dots[8] != 0x40 {
|
|
t.Fatalf("entry frame = %+v", frame)
|
|
}
|
|
options.AbbreviateRoles = true
|
|
if got := brailleFocusFrameWithOptions(atspi.Snapshot{Role: "button", Name: "Save"}, options).Text; got != "Save btn" {
|
|
t.Fatalf("abbreviated button = %q", got)
|
|
}
|
|
options.Verbosity = 0
|
|
if got := brailleFocusFrameWithOptions(atspi.Snapshot{Role: "button", Name: "Save"}, options).Text; got != "Save" {
|
|
t.Fatalf("brief button = %q", got)
|
|
}
|
|
options.LinkIndicator = 0x80
|
|
link := brailleFocusFrameWithOptions(atspi.Snapshot{Role: "link", Name: "Help"}, options)
|
|
if len(link.Dots) != 4 || link.Dots[0] != 0x80 || link.Dots[3] != 0x80 {
|
|
t.Fatalf("link dots = %v", link.Dots)
|
|
}
|
|
}
|
|
|
|
func (recorder frameRecorder) EnqueueFrame(frame braille.Frame) { recorder.frames <- frame }
|
|
|
|
func TestBrailleFocusFrameTracksUnicodeCaret(t *testing.T) {
|
|
snapshot := atspi.NewSnapshot("/entry", "Message", "entry", "", "é猫x")
|
|
snapshot.TextCaretKnown = true
|
|
snapshot.BrailleLine = "é猫x"
|
|
snapshot.TextBeforeCaret = "é猫"
|
|
snapshot.BrailleCaret = 2
|
|
if got := brailleFocusFrame(snapshot); !reflect.DeepEqual(got, braille.Frame{Text: "Message é猫x $l", Cursor: 10}) {
|
|
t.Fatalf("braille frame = %+v", got)
|
|
}
|
|
snapshot.BrailleLine = "a é"
|
|
snapshot.TextBeforeCaret = "a "
|
|
snapshot.BrailleCaret = 3
|
|
if got := brailleFocusFrame(snapshot); !reflect.DeepEqual(got, braille.Frame{Text: "Message a é $l", Cursor: 11}) {
|
|
t.Fatalf("braille spacing/caret = %+v", got)
|
|
}
|
|
snapshot.BrailleLine = "é猫x"
|
|
snapshot.TextBeforeCaret = "other"
|
|
snapshot.BrailleCaret = 5
|
|
if got := brailleFocusFrame(snapshot); got.Cursor != -1 {
|
|
t.Fatalf("invalid caret should be hidden: %+v", got)
|
|
}
|
|
snapshot.Role = "password text"
|
|
if got := brailleFocusFrame(snapshot); got.Cursor != 0 || got.Text != "Message password text" {
|
|
t.Fatalf("protected text leaked into braille: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestBrailleFramesFollowAcceptedFocusAndCaretWithoutSpeech(t *testing.T) {
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
|
frames := make(chan braille.Frame, 8)
|
|
queue := outputQueue{speech: discardSpeech{}, braille: frameRecorder{frames: frames}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_ = processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, nil, nil, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
entry := atspi.NewSnapshot("/entry", "Message", "entry", "", "é猫")
|
|
entry.Editable = true
|
|
entry.TextCaretKnown = true
|
|
entry.BrailleLine = "é猫"
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, entry)
|
|
wantFrame(t, frames, braille.Frame{Text: "Message é猫 $l", Cursor: 8})
|
|
entry.TextBeforeCaret = "é"
|
|
entry.BrailleCaret = 1
|
|
adapter.events <- atspi.NewEvent(atspi.CaretEvent, entry)
|
|
wantFrame(t, frames, braille.Frame{Text: "Message é猫 $l", Cursor: 9})
|
|
entry.Text = "é猫x"
|
|
entry.BrailleLine = "é猫x"
|
|
entry.TextBeforeCaret = "é猫x"
|
|
entry.BrailleCaret = 3
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, entry, "x")
|
|
wantFrame(t, frames, braille.Frame{Text: "Message é猫x $l", Cursor: 11})
|
|
// State snapshots may omit text capture. They must retain the displayed
|
|
// line while updating its state suffix.
|
|
entry.TextCaretKnown = false
|
|
entry.BrailleLine = ""
|
|
entry.Required = true
|
|
adapter.events <- atspi.NewEvent("object:state-changed:required", entry)
|
|
wantFrame(t, frames, braille.Frame{Text: "Message é猫x $l required", Cursor: 11})
|
|
check := atspi.NewSnapshot("/check", "Save", "check box", "", "")
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, check)
|
|
wantFrame(t, frames, braille.Frame{Text: "< > Save check box", Cursor: 0})
|
|
check.Checked = true
|
|
adapter.events <- atspi.NewEvent("object:state-changed:checked", check)
|
|
wantFrame(t, frames, braille.Frame{Text: "<x> Save check box", Cursor: 0})
|
|
}
|
|
|
|
func wantFrame(t *testing.T, frames <-chan braille.Frame, want braille.Frame) {
|
|
t.Helper()
|
|
select {
|
|
case got := <-frames:
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("braille frame = %+v, want %+v", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("timed out waiting for braille frame %+v", want)
|
|
}
|
|
}
|
|
|
|
func TestBraillePythonControlFormats(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
snapshot atspi.Snapshot
|
|
text string
|
|
cursor int
|
|
}{
|
|
{"unchecked", atspi.Snapshot{Name: "Save", Role: "check box"}, "< > Save check box", 0},
|
|
{"checked", atspi.Snapshot{Name: "Save", Role: "check box", Checked: true}, "<x> Save check box", 0},
|
|
{"mixed", atspi.Snapshot{Name: "Save", Role: "check box", Indeterminate: true}, "<-> Save check box", 0},
|
|
{"radio", atspi.Snapshot{Name: "First", Role: "radio button", Checked: true}, "&=y First radio button", 0},
|
|
{"toggle", atspi.Snapshot{Name: "Bold", Role: "toggle button", Checked: true}, "&=y Bold toggle button", 0},
|
|
{"button", atspi.Snapshot{Name: "More", Role: "push button", Expandable: true}, "More collapsed push button", 0},
|
|
{"menu", atspi.Snapshot{Name: "Save", Role: "menu item", SensitiveKnown: true}, "Save grayed", 0},
|
|
{"combo", atspi.Snapshot{Name: "Color", Role: "combo box", ValueText: "Blue"}, "Color Blue combo box", 6},
|
|
{"slider", atspi.Snapshot{Name: "Volume", Role: "slider", ValueKnown: true, CurrentValue: 1.25, MinimumIncrement: 0.01}, "Volume 1.25 slider", 0},
|
|
{"progress", atspi.Snapshot{Name: "Copy", Role: "progress bar", ValueKnown: true, CurrentValue: 25, MaximumValue: 100}, "Copy 25% progress bar", 0},
|
|
{"progress range", atspi.Snapshot{Name: "Copy", Role: "progress bar", ValueKnown: true, MinimumValue: 10, CurrentValue: 20, MaximumValue: 30}, "Copy 50% progress bar", 0},
|
|
{"tree", atspi.Snapshot{Name: "Files", Role: "tree item", Expandable: true, Expanded: true}, "Files expanded", 0},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
frame := brailleFocusFrame(tc.snapshot)
|
|
if frame.Text != tc.text || frame.Cursor != tc.cursor {
|
|
t.Fatalf("got %+v, want %q cursor %d", frame, tc.text, tc.cursor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBrailleTextRoutesExcludeLabelsAndStates(t *testing.T) {
|
|
s := atspi.Snapshot{BusName: ":1.42", Path: "/entry", Name: "Message", Role: "entry", Editable: true, TextCaretKnown: true,
|
|
BrailleLine: "é猫\n", BrailleLineStart: 20, BrailleCaret: 22, Invalid: true, Required: true, ReadOnly: true}
|
|
f := brailleFocusFrame(s)
|
|
if f.Text != "Message é猫 $l invalid required rdonly" || f.Cursor != 10 || len(f.Routes) != 1 {
|
|
t.Fatalf("frame %+v", f)
|
|
}
|
|
r := f.Routes[0]
|
|
if r.Start != 8 || r.End != 13 || r.SourceStart != 20 || r.TextLength != 2 || r.SourceText != "é猫\n" {
|
|
t.Fatalf("route %+v", r)
|
|
}
|
|
s.Role = "terminal"
|
|
f = brailleFocusFrame(s)
|
|
if f.Text != "é猫 " || f.Cursor != 2 {
|
|
t.Fatalf("terminal frame %+v", f)
|
|
}
|
|
s.BrailleCaret = 19
|
|
if f = brailleFocusFrame(s); f.Cursor != -1 {
|
|
t.Fatalf("caret outside captured suffix: %+v", f)
|
|
}
|
|
}
|
|
|
|
type tableBrailleCapturer struct{ *documentLineCapturer }
|
|
|
|
type browseBrailleCapturer struct {
|
|
*documentLineCapturer
|
|
text string
|
|
}
|
|
|
|
func (capturer *browseBrailleCapturer) ReadDocumentCaret(_ context.Context, _ atspi.ObjectReference, offset int, unit atspi.DocumentCaretUnit) (atspi.DocumentCaretText, error) {
|
|
characters := []rune(capturer.text)
|
|
if offset < 0 || offset >= len(characters) {
|
|
return atspi.DocumentCaretText{CharacterCount: len(characters)}, nil
|
|
}
|
|
start, end := offset, offset+1
|
|
if unit != atspi.DocumentCaretCharacter {
|
|
for start > 0 && characters[start-1] != '\n' {
|
|
start--
|
|
}
|
|
for end < len(characters) && characters[end-1] != '\n' {
|
|
end++
|
|
}
|
|
}
|
|
return atspi.DocumentCaretText{Start: start, End: end, Text: string(characters[start:end]), CharacterCount: len(characters)}, nil
|
|
}
|
|
|
|
func TestBrowseCaretUpdatesBrailleWithoutNativeCaretEvents(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
capturer := &browseBrailleCapturer{documentLineCapturer: &documentLineCapturer{calls: make(chan documentLineCaptureCall, 2)}, text: "é猫 word\nSecond line"}
|
|
worker, err := review.NewWorker(capturer, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings := config.DefaultSettings()
|
|
settings.EnableBraille = false
|
|
prefsWorker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
|
|
prefsWorker.braille = &recordingBrailleSettings{}
|
|
prefsWorker.documentReview = worker
|
|
values := preferences.FromSettings(settings)
|
|
values.EnableBraille = true
|
|
if response := prefsWorker.apply(ctx, preferences.Request{Values: values}); !response.Accepted {
|
|
t.Fatalf("enabling braille for the running session failed: %+v", response)
|
|
}
|
|
if err = worker.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer worker.Stop(context.Background())
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
|
frames := make(chan braille.Frame, 16)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), outputQueue{speech: discardSpeech{}, braille: frameRecorder{frames: frames}}, adapter, nil, worker, 0, nil, nil, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
doc := atspi.Snapshot{BusName: ":1.42", Path: "/doc", Role: "document web", StatesKnown: true}
|
|
text := atspi.Snapshot{BusName: ":1.42", Path: "/text", Role: "static", StatesKnown: true, CharacterCount: utf8.RuneCountInString(capturer.text)}
|
|
tree, err := atspi.NewAccessibleTree(doc.ID(), []atspi.TreeNode{{Snapshot: doc, Children: []atspi.ObjectID{text.ID()}}, {Snapshot: text}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, doc)
|
|
wantFrame(t, frames, brailleFocusFrame(doc))
|
|
for index, step := range []struct {
|
|
command review.DocumentCaretCommand
|
|
line string
|
|
cursor int
|
|
}{
|
|
{review.CaretDocumentStart, "é猫 word", 0},
|
|
{review.CaretNextCharacter, "é猫 word", 1},
|
|
{review.CaretLineEnd, "é猫 word", 8},
|
|
{review.CaretNextLine, "Second line", 0},
|
|
{review.CaretPreviousLine, "é猫 word", 0},
|
|
{review.CaretDocumentEnd, "Second line", 10},
|
|
} {
|
|
if err = worker.MoveDocumentCaret(step.command); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if index == 0 {
|
|
select {
|
|
case call := <-capturer.calls:
|
|
call.result <- tree
|
|
case <-time.After(time.Second):
|
|
t.Fatal("document capture missing")
|
|
}
|
|
}
|
|
select {
|
|
case frame := <-frames:
|
|
if strings.TrimRight(frame.Text, " ") != step.line || frame.Cursor != step.cursor || len(frame.Routes) != 1 || frame.Document == nil || frame.Routes[0].Path != text.Path {
|
|
t.Fatalf("command %v: frame %+v, want %q cursor %d with document routing", step.command, frame, step.line, step.cursor)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("command %v did not update braille", step.command)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBrowseBrailleUsesVirtualOffsetAndAncestorIndicators(t *testing.T) {
|
|
options := config.DefaultSettings().Braille
|
|
options.LinkIndicator = 0x80
|
|
options.WordWrap = true
|
|
result := review.Result{DocumentPosition: review.DocumentPosition{Offset: 12, Snapshot: atspi.Snapshot{Role: "static"}},
|
|
DocumentBrailleLine: &atspi.DocumentCaretText{Start: 10, Text: "é猫 word\n"},
|
|
DocumentCaretContext: []atspi.Snapshot{{Role: "link"}, {Role: "heading", HeadingLevel: 2}}}
|
|
frame := brailleDocumentFrame(result, options)
|
|
if frame.Text != "é猫 word h2" || frame.Cursor != 2 || !frame.WordWrap || len(frame.Routes) != 0 || len(frame.Dots) != 11 || frame.Dots[0] != 0x80 || frame.Dots[8] != 0 {
|
|
t.Fatalf("browse frame = %+v", frame)
|
|
}
|
|
result.DocumentPosition.Snapshot = atspi.Snapshot{Role: "password text", Name: "Secret"}
|
|
frame = brailleDocumentFrame(result, options)
|
|
if frame.Text != "Secret password text" || len(frame.Routes) != 0 {
|
|
t.Fatalf("protected document text leaked: %+v", frame)
|
|
}
|
|
}
|
|
|
|
func TestBrowseBrailleFragmentLineKeepsNativeCursor(t *testing.T) {
|
|
first := atspi.NewSnapshot("/first", "", "static", "", "")
|
|
first.BusName = ":1.999"
|
|
second := atspi.NewSnapshot("/second", "", "static", "", "")
|
|
second.BusName = first.BusName
|
|
result := review.Result{
|
|
DocumentPosition: review.DocumentPosition{Snapshot: second, Offset: 8},
|
|
DocumentCaretLine: &atspi.DocumentLine{Contents: []atspi.DocumentContent{
|
|
{Snapshot: first, Start: 0, End: 2, RawText: "é猫"},
|
|
{Snapshot: second, Start: 7, End: 11, RawText: "word", Separator: " "},
|
|
}},
|
|
DocumentCaretContext: []atspi.Snapshot{{Role: "heading", HeadingLevel: 2}},
|
|
}
|
|
frame := brailleDocumentFrame(result, config.DefaultSettings().Braille)
|
|
if frame.Text != "é猫 word h2" || frame.Cursor != 4 || len(frame.Routes) != 0 {
|
|
t.Fatalf("fragment frame = %+v", frame)
|
|
}
|
|
}
|
|
|
|
func TestLinkedHeadingBrailleKeepsHeadingAndLinkIndicators(t *testing.T) {
|
|
options := config.DefaultSettings().Braille
|
|
options.LinkIndicator = 0x80
|
|
heading := atspi.NewSnapshot("/heading", "Linked heading", "heading", "", "")
|
|
heading.HeadingLevel = 2
|
|
link := atspi.NewSnapshot("/link", "Linked heading", "link", "", "")
|
|
link.Visited = true
|
|
result := review.Result{DocumentPosition: review.DocumentPosition{Snapshot: heading, LinkedHeading: &link}}
|
|
frame := brailleDocumentFrame(result, options)
|
|
if !strings.Contains(frame.Text, "h2") || !strings.Contains(frame.Text, "vlnk") || len(frame.Dots) != utf8.RuneCountInString(frame.Text) || frame.Dots[0] != 0x80 || frame.Dots[len([]rune(heading.Name))] != 0 {
|
|
t.Fatalf("linked heading frame = %+v", frame)
|
|
}
|
|
result.DocumentBrailleLine = &atspi.DocumentCaretText{Text: "Linked heading"}
|
|
frame = brailleDocumentFrame(result, options)
|
|
if frame.Text != "Linked heading h2 vlnk" || frame.Dots[0] != 0x80 || frame.Dots[len([]rune(heading.Name))] != 0 {
|
|
t.Fatalf("linked heading line frame = %+v", frame)
|
|
}
|
|
}
|
|
|
|
func (*tableBrailleCapturer) ReadDocumentCaret(context.Context, atspi.ObjectReference, int, atspi.DocumentCaretUnit) (atspi.DocumentCaretText, error) {
|
|
return atspi.DocumentCaretText{}, nil
|
|
}
|
|
|
|
func TestTableNavigationUpdatesBrailleWithoutNativeCaretEvent(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
capturer := &tableBrailleCapturer{&documentLineCapturer{calls: make(chan documentLineCaptureCall, 2)}}
|
|
worker, err := review.NewWorker(capturer, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = worker.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer worker.Stop(context.Background())
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
|
frames := make(chan braille.Frame, 8)
|
|
queue := outputQueue{speech: discardSpeech{}, braille: frameRecorder{frames: frames}}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, worker, 0, nil, nil, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
doc := atspi.Snapshot{BusName: ":1.42", Path: "/doc", Role: "document web", StatesKnown: true}
|
|
table := atspi.Snapshot{BusName: ":1.42", Path: "/table", Role: "table", Name: "Probe", TableRowCount: 1, TableColumnCount: 1, StatesKnown: true}
|
|
cell := atspi.Snapshot{BusName: ":1.42", Path: "/cell", Role: "table cell", Name: "Cell contents", TablePositionKnown: true, TableRowSpan: 1, TableColumnSpan: 1, StatesKnown: true}
|
|
tree, err := atspi.NewAccessibleTree(doc.ID(), []atspi.TreeNode{{Snapshot: doc, Children: []atspi.ObjectID{table.ID()}}, {Snapshot: table, Children: []atspi.ObjectID{cell.ID()}}, {Snapshot: cell}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, table)
|
|
wantFrame(t, frames, brailleFocusFrame(table))
|
|
if err = worker.MoveDocumentCaret(review.TableFirst); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case call := <-capturer.calls:
|
|
call.result <- tree
|
|
case <-time.After(time.Second):
|
|
t.Fatal("table capture missing")
|
|
}
|
|
wantFrame(t, frames, brailleFocusFrame(cell))
|
|
}
|
|
|
|
func TestBrowseBrailleRepeatedParentKeepsNativeCursor(t *testing.T) {
|
|
parent := atspi.NewSnapshot("/body", "", "paragraph", "", "")
|
|
parent.BusName = ":1.1"
|
|
child := atspi.NewSnapshot("/span", "", "section", "", "")
|
|
child.BusName = ":1.1"
|
|
result := review.Result{DocumentPosition: review.DocumentPosition{Snapshot: parent, Offset: 0}, DocumentCaretLine: &atspi.DocumentLine{Contents: []atspi.DocumentContent{
|
|
{Snapshot: parent, Start: 0, End: 7, RawText: "Before "},
|
|
{Snapshot: child, Start: 0, End: 9, RawText: "Read more"},
|
|
{Snapshot: parent, Start: 8, End: 15, RawText: " after."},
|
|
}}}
|
|
frame := brailleDocumentFrame(result, config.DefaultSettings().Braille)
|
|
if frame.Cursor != 0 {
|
|
t.Fatalf("text=%q cursor=%d; expected cursor=0", frame.Text, frame.Cursor)
|
|
}
|
|
}
|