133 lines
4.4 KiB
Go
133 lines
4.4 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package review
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
)
|
|
|
|
func TestObjectInterestIsOrderedAndPreservesExplicitReview(t *testing.T) {
|
|
capturer := &fakeCapturer{calls: make(chan captureCall, 1)}
|
|
worker, cancel := startWorker(t, capturer, time.Second)
|
|
defer cancel()
|
|
defer worker.Stop(context.Background())
|
|
worker.SyncFocus(objectID("/first"))
|
|
ctx, stop := context.WithTimeout(context.Background(), time.Second)
|
|
defer stop()
|
|
got, err := worker.ObjectOfInterest(ctx)
|
|
if err != nil || got.Object != objectID("/first") {
|
|
t.Fatalf("focus interest: %+v %v", got, err)
|
|
}
|
|
worker.Move(NextSibling)
|
|
call := <-capturer.calls
|
|
replies := make(chan interestReply, 1)
|
|
go func() { interest, err := worker.ObjectOfInterest(ctx); replies <- interestReply{interest, err} }()
|
|
tree := newTree(t)
|
|
call.result <- captureResult{tree: tree}
|
|
if result := waitResult(t, worker); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
reply := <-replies
|
|
if reply.err != nil || reply.interest.Object != objectID("/second") {
|
|
t.Fatalf("explicit review snapped to focus: %+v", reply)
|
|
}
|
|
worker.SyncFocus(objectID("/grandchild"))
|
|
worker.SyncFocus(objectID("/first"))
|
|
got, err = worker.ObjectOfInterest(ctx)
|
|
if err != nil || got.Object != objectID("/first") {
|
|
t.Fatalf("away/back retained explicit review: %+v %v", got, err)
|
|
}
|
|
worker.SyncFocus(objectID("/grandchild"))
|
|
got, err = worker.ObjectOfInterest(ctx)
|
|
if err != nil || got.Object != objectID("/grandchild") {
|
|
t.Fatalf("changed focus: %+v %v", got, err)
|
|
}
|
|
worker.Move(Parent)
|
|
if result := waitResult(t, worker); result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
tree.Invalidate(objectID("/second"))
|
|
if _, err = worker.ObjectOfInterest(ctx); err == nil {
|
|
t.Fatal("stale explicit interest fell back to focus")
|
|
}
|
|
worker.ClearFocus()
|
|
if _, err = worker.ObjectOfInterest(ctx); !errors.Is(err, ErrNoCurrent) {
|
|
t.Fatalf("clear focus: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFlatInterestUsesZoneAndDoesNotRecapture(t *testing.T) {
|
|
capturer := &fakeCapturer{calls: make(chan captureCall, 1)}
|
|
worker, cancel := startWorker(t, capturer, time.Second)
|
|
defer cancel()
|
|
defer worker.Stop(context.Background())
|
|
worker.SyncFocus(objectID("/b"))
|
|
worker.ReadFlat(Line, 0)
|
|
call := <-capturer.calls
|
|
tree := flatTree(t, flatNode("/a", "First", 0, 0), flatNode("/b", "Second", 0, 30))
|
|
call.result <- captureResult{tree: tree}
|
|
waitResult(t, worker)
|
|
worker.ReadFlat(Line, -1)
|
|
position := waitResult(t, worker).FlatPosition
|
|
ctx, cancelQuery := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancelQuery()
|
|
got, err := worker.ObjectOfInterest(ctx)
|
|
if err != nil || got.Object != objectID("/a") || !got.HasZone || got.Zone != position.Bounds {
|
|
t.Fatalf("zone: %+v %v", got, err)
|
|
}
|
|
tree.InvalidateGeometry(objectID("/a"))
|
|
if _, err = worker.ObjectOfInterest(ctx); !errors.Is(err, atspi.ErrStaleGeometry) {
|
|
t.Fatalf("stale zone: %v", err)
|
|
}
|
|
select {
|
|
case <-capturer.calls:
|
|
t.Fatal("interest recaptured a tree")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestInterestPrefersVirtualCaretThenExplicitReview(t *testing.T) {
|
|
ctx := context.Background()
|
|
reader := caretTextReader{webID("/text"): "First line", webID("/next"): "Last line"}
|
|
document := newCaretTextCursor(t, reader)
|
|
if _, err := document.SyncFocus(ctx, webID("/text")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := document.MoveCaret(ctx, reader, CaretNextLine); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := resolve_interest(ctx, webID("/text"), 0, nil, nil, nil, document)
|
|
if err != nil || got.Object != webID("/next") {
|
|
t.Fatalf("virtual caret: %+v %v", got, err)
|
|
}
|
|
cursor, _ := NewCursor(newTree(t))
|
|
cursor.SyncFocus(objectID("/first"))
|
|
cursor.Move(NextSibling)
|
|
got, err = resolve_interest(ctx, webID("/text"), 1, cursor, nil, nil, document)
|
|
if err != nil || got.Object != objectID("/second") {
|
|
t.Fatalf("explicit precedence: %+v %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestInterestCancelledAndStopped(t *testing.T) {
|
|
capturer := &fakeCapturer{calls: make(chan captureCall, 1)}
|
|
worker, cancel := startWorker(t, capturer, time.Second)
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
stop()
|
|
if _, err := worker.ObjectOfInterest(ctx); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("cancel: %v", err)
|
|
}
|
|
cancel()
|
|
worker.Stop(context.Background())
|
|
if _, err := worker.ObjectOfInterest(context.Background()); !errors.Is(err, ErrNoCurrent) {
|
|
t.Fatalf("stop: %v", err)
|
|
}
|
|
}
|