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