194 lines
6.9 KiB
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")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|