Files
cthulhu/internal/app/ocr_refresh_activation_test.go

194 lines
6.9 KiB
Go

package app
import (
"context"
"image"
"image/color"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/ocr"
)
func TestOCRClipboardRefreshKeepsSnapshotAndCursor(t *testing.T) {
session, keys, capture, engine, output := ocr_test_session(t)
session.Handle(input.CommandEvent{Command: input.OCRClipboard, Timestamp: 100})
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, capture.calls)
ocr_test_receive(t, engine.calls)
engine.results <- ocr.Result{Text: "one two", Words: []ocr.Word{{Text: "one", Span: ocr.Span{Start: 0, End: 3}}, {Text: "two", Span: ocr.Span{Start: 4, End: 7}}}}
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, output.speech)
ocr_test_receive(t, output.frames)
session.Handle(input.CommandEvent{Command: input.OCRNextWord, Timestamp: 101})
ocr_test_receive(t, output.speech)
ocr_test_receive(t, output.frames)
session.Handle(input.CommandEvent{Command: input.OCRRefresh, Timestamp: 102})
ocr_test_receive(t, engine.calls)
select {
case <-capture.calls:
t.Fatal("clipboard refresh fetched a changed clipboard")
default:
}
engine.results <- ocr.Result{Text: "one three", Words: []ocr.Word{{Text: "one", Span: ocr.Span{Start: 0, End: 3}}, {Text: "three", Span: ocr.Span{Start: 4, End: 9}}}}
if got := ocr_test_receive(t, output.speech); got.Text != "one three" {
t.Fatal(got)
}
session.mu.Lock()
defer session.mu.Unlock()
if session.cursor == nil || session.cursor.Offset() != 4 || session.cursor.Text() != "one three" || session.refreshFailed {
t.Fatal("refresh lost cursor or snapshot")
}
}
func TestOCRRefreshRetainsUnchangedSuffixAcrossInsertion(t *testing.T) {
if got := remapOCROffset("é one", "new é one", 2); got != 6 {
t.Fatalf("cursor moved to inserted text: %d", got)
}
if got := remapOCROffset("é one", "é new one", 2); got != 6 {
t.Fatalf("cursor missed original suffix: %d", got)
}
if got := remapOCROffset("old text", "old", 8); got != 3 {
t.Fatalf("deleted suffix was not clamped: %d", got)
}
}
func TestOCRStaleActivationCannotRetireReplacement(t *testing.T) {
session, keys, _, engine, _ := ocr_test_session(t)
defer func() { engine.results <- ocr.Result{}; engine.results <- ocr.Result{} }()
session.Handle(input.CommandEvent{Command: input.OCRWindow, Timestamp: 100})
ocr_test_receive(t, keys.calls)
session.mu.Lock()
old := session.job
session.mu.Unlock()
session.Handle(input.CommandEvent{Command: input.OCRDesktop, Timestamp: 101})
session.mu.Lock()
current := session.job.generation
session.mu.Unlock()
session.activate(ocrActivateRequest{job: old})
session.mu.Lock()
defer session.mu.Unlock()
if session.cancel == nil || session.job.generation != current {
t.Fatal("late activation retired replacement job")
}
}
func TestOCRAutoRefreshWaitsForEachJob(t *testing.T) {
session, keys, capture, engine, output := ocr_test_session(t)
live := &liveSettings{}
settings := config.DefaultSettings()
settings.OCR.AutoRefresh = true
settings.OCR.RefreshInterval = 500
live.Store(settings)
session.live = live
session.Handle(input.CommandEvent{Command: input.OCRWindow, Timestamp: 100})
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, capture.calls)
ocr_test_receive(t, engine.calls)
engine.results <- ocr.Result{Text: "ready"}
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, output.speech)
ocr_test_receive(t, output.frames)
ocr_test_receive(t, capture.calls)
ocr_test_receive(t, engine.calls)
select {
case <-capture.calls:
t.Fatal("automatic OCR jobs overlapped")
case <-time.After(600 * time.Millisecond):
}
engine.results <- ocr.Result{Text: "ready"}
ocr_test_receive(t, output.frames)
ocr_test_receive(t, capture.calls)
ocr_test_receive(t, engine.calls)
}
type ocrTestClicker struct{ points chan image.Point }
func (clicker *ocrTestClicker) ClickOCR(ctx context.Context, _ uint64, _ image.Rectangle, point image.Point) error {
clicker.points <- point
return ctx.Err()
}
type ocrTestConfirmer struct {
answer bool
called chan string
}
func (confirm *ocrTestConfirmer) Confirm(_ context.Context, word string) (bool, error) {
confirm.called <- word
return confirm.answer, nil
}
func TestOCRActivationChecksFreshPixelsBeforeClick(t *testing.T) {
for _, test := range []struct {
name string
command input.Command
changed bool
}{
{"object/unchanged", input.OCRObject, false},
{"object/changed", input.OCRObject, true},
{"inaccessible-window/unchanged", input.OCRWindow, false},
{"inaccessible-window/changed", input.OCRWindow, true},
} {
t.Run(test.name, func(t *testing.T) {
session, keys, capture, engine, output := ocr_test_session(t)
if test.command == input.OCRWindow {
session.focus, session.window = atspi.ObjectID{}, atspi.ObjectID{}
session.geometry, session.interest = nil, nil
}
clicker := &ocrTestClicker{points: make(chan image.Point, 1)}
confirmer := &ocrTestConfirmer{answer: true, called: make(chan string, 1)}
session.clicker, session.confirmer = clicker, confirmer
paint := color.RGBA{R: 20, A: 255}
capture.produce = func(ctx context.Context, request ocr.CaptureRequest) (ocr.Capture, error) {
pixels := image.NewRGBA(image.Rect(0, 0, 100, 50))
for index := range pixels.Pix {
pixels.Pix[index] = 20
}
pixels.SetRGBA(0, 0, paint)
bounds := image.Rect(10, 20, 110, 70)
return ocr.Capture{CaptureHeader: ocr.CaptureHeader{Request: request, RootBounds: image.Rect(0, 0, 200, 200), SourceBounds: bounds, ScreenBounds: bounds, SourceWindow: request.Foreground}, Pixels: pixels,
Transform: ocr.Transform{ImageContent: image.Rect(0, 0, 100, 50), ScreenBounds: bounds, HasScreenMapping: true}}, ctx.Err()
}
session.Handle(input.CommandEvent{Command: test.command, Timestamp: 100})
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, capture.calls)
ocr_test_receive(t, engine.calls)
engine.results <- ocr.Result{Text: "go", Width: 100, Height: 50, Words: []ocr.Word{{Text: "go", Bounds: image.Rect(10, 10, 30, 20), Span: ocr.Span{Start: 0, End: 2}}}}
ocr_test_receive(t, keys.calls)
ocr_test_receive(t, output.speech)
ocr_test_receive(t, output.frames)
if test.changed {
paint = color.RGBA{R: 30, A: 255}
}
session.Handle(input.CommandEvent{Command: input.OCRActivate, Timestamp: 101})
if got := ocr_test_receive(t, confirmer.called); got != "go" {
t.Fatal(got)
}
ocr_test_receive(t, capture.calls)
if test.changed {
if got := ocr_test_receive(t, output.speech); got.Text != "OCR source changed; refresh before activation" {
t.Fatal(got)
}
select {
case <-clicker.points:
t.Fatal("clicked changed image")
default:
}
} else {
select {
case point := <-clicker.points:
if point != image.Pt(29, 34) {
t.Fatal(point)
}
case <-time.After(3 * time.Second):
t.Fatal("valid word was not clicked")
}
}
})
}
}