package review import ( "context" "errors" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) type brailleDocumentNavigator struct { *boundaryNavigator routes chan atspi.BrailleRoute routeErr error } func (navigator *brailleDocumentNavigator) RouteBraille(ctx context.Context, route atspi.BrailleRoute) error { if err := ctx.Err(); err != nil { return err } navigator.routes <- route return navigator.routeErr } func TestDocumentBrailleRoutingUpdatesBrowseCursorAndRejectsStaleRequests(t *testing.T) { text := caretTextReader{webID("/text"): "é猫 word", webID("/next"): "Next line"} navigator := &brailleDocumentNavigator{boundaryNavigator: &boundaryNavigator{fakeCapturer: &fakeCapturer{calls: make(chan captureCall, 1)}, caretTextReader: text}, routes: make(chan atspi.BrailleRoute, 4)} worker, cancel := startWorker(t, navigator, time.Second) defer cancel() worker.SetDocumentBrailleEnabled(true) worker.SyncFocus(webID("/text")) if err := worker.MoveDocumentCaret(CaretDocumentStart); err != nil { t.Fatal(err) } call := <-navigator.calls call.result <- captureResult{tree: newCaretTextCursor(t, text).tree} first := waitResult(t, worker) ctx, stop := context.WithTimeout(context.Background(), time.Second) defer stop() route := func(epoch uint64, focus, document atspi.ObjectID) error { return worker.RouteDocumentBraille(ctx, epoch, focus, document, webID("/text"), "é猫 word", 0, 4) } if err := route(first.DocumentEpoch, first.DocumentFocus, first.DocumentPosition.Document.ID()); err != nil { t.Fatal(err) } routed := waitResult(t, worker) if !routed.DocumentBrailleRoute || !routed.Moved || routed.DocumentPosition.Offset != 4 || routed.DocumentBrailleLine == nil { t.Fatalf("route result = %+v", routed) } if native := <-navigator.routes; !native.Position.Valid() || native.Offset != 4 || native.Focus != first.DocumentFocus { t.Fatalf("native route = %+v", native) } if err := worker.MoveDocumentCaret(CaretNextCharacter); err != nil { t.Fatal(err) } if result := waitResult(t, worker); result.DocumentPosition.Offset != 5 || result.DocumentCaretText != "o" { t.Fatalf("arrow did not follow routed cursor: %+v", result) } navigator.routeErr = atspi.ErrStaleObject if err := route(first.DocumentEpoch, first.DocumentFocus, first.DocumentPosition.Document.ID()); !errors.Is(err, atspi.ErrStaleObject) { t.Fatalf("native refusal = %v", err) } if failed := waitResult(t, worker); failed.Moved { t.Fatal("native refusal moved the virtual cursor") } <-navigator.routes if err := worker.MoveDocumentCaret(CaretNextCharacter); err != nil { t.Fatal(err) } if result := waitResult(t, worker); result.DocumentPosition.Offset != 6 { t.Fatalf("failed routing changed cursor: %+v", result) } navigator.routeErr = nil canceled, stopCanceled := context.WithCancel(ctx) stopCanceled() if err := worker.RouteDocumentBraille(canceled, first.DocumentEpoch, first.DocumentFocus, first.DocumentPosition.Document.ID(), webID("/text"), "é猫 word", 0, 0); !errors.Is(err, context.Canceled) { t.Fatalf("canceled routing = %v", err) } if err := route(first.DocumentEpoch, first.DocumentFocus, webID("/other")); !errors.Is(err, atspi.ErrStaleObject) { t.Fatalf("other page route = %v", err) } waitResult(t, worker) worker.ResetDocument(first.DocumentFocus) if err := route(first.DocumentEpoch, first.DocumentFocus, first.DocumentPosition.Document.ID()); !errors.Is(err, atspi.ErrStaleObject) { t.Fatalf("retired epoch route = %v", err) } if len(navigator.routes) != 0 { t.Fatal("stale route reached native owner") } }