// 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) } }