163 lines
6.1 KiB
Go
163 lines
6.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/ocr"
|
|
)
|
|
|
|
func TestOCRAutomaticReadAllOnceWithCopyAndRefresh(t *testing.T) {
|
|
session, _, _, engine, output := ocr_test_session(t)
|
|
reader := &ocrTestReadAll{}
|
|
copier := &ocrTestCopier{copied: make(chan string, 1)}
|
|
session.readAll, session.copier = reader, copier
|
|
settings := session.settings()
|
|
settings.OCR.AutoReadAll, settings.OCR.CopyFirstResult = true, true
|
|
session.live = &liveSettings{}
|
|
session.live.Store(settings)
|
|
session.Handle(input.CommandEvent{Command: input.OCRWindow, Timestamp: 100})
|
|
ocr_test_receive(t, engine.calls)
|
|
// A pending job must keep the initiating preferences.
|
|
settings.OCR.AutoReadAll, settings.OCR.CopyFirstResult = false, false
|
|
session.live.Store(settings)
|
|
engine.results <- ocr.Result{Text: "é one\n猫 two"}
|
|
request := ocr_test_receive(t, output.speech)
|
|
if request.Cause != "read-all" || len(request.ReadAll) != 2 || request.ReadAll[0].Text != "é one" || reader.timestamp != 100 {
|
|
t.Fatalf("initial reading: %+v", request)
|
|
}
|
|
frame := ocr_test_receive(t, output.frames)
|
|
if frame.Text != "é one" || frame.Cursor != 0 || frame.Virtual == nil {
|
|
t.Fatalf("initial braille: %+v", frame)
|
|
}
|
|
if text := ocr_test_receive(t, copier.copied); text != "é one\n猫 two" {
|
|
t.Fatal(text)
|
|
}
|
|
request.ReadAll[1].Began()
|
|
frame = ocr_test_receive(t, output.frames)
|
|
if frame.Text != "猫 two" || frame.Virtual.SourceStart != 6 {
|
|
t.Fatalf("paced braille: %+v", frame)
|
|
}
|
|
session.Handle(input.CommandEvent{Command: input.OCRRefresh, Timestamp: 101})
|
|
ocr_test_receive(t, engine.calls) // Worker has also finished the optional copy.
|
|
if request.context.Err() != context.Canceled {
|
|
t.Fatal("manual refresh did not cancel old reading")
|
|
}
|
|
engine.results <- ocr.Result{Text: "é one\n猫 two"}
|
|
if got := ocr_test_receive(t, output.speech); got.Text != "OCR text unchanged" || len(got.ReadAll) != 0 {
|
|
t.Fatalf("copy interrupted reading or refresh restarted it: %+v", got)
|
|
}
|
|
ocr_test_receive(t, output.frames) // Status, then current review frame.
|
|
ocr_test_receive(t, output.frames)
|
|
request.ReadAll[0].Began()
|
|
select {
|
|
case got := <-output.frames:
|
|
t.Fatalf("stale reading moved refreshed braille: %+v", got)
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestOCRPeriodicRefreshWaitsForSayAll(t *testing.T) {
|
|
for _, interrupted := range []bool{false, true} {
|
|
t.Run(map[bool]string{false: "completed", true: "interrupted"}[interrupted], func(t *testing.T) {
|
|
session, _, _, engine, output := ocr_test_session(t)
|
|
session.readAll = &ocrTestReadAll{}
|
|
settings := session.settings()
|
|
settings.OCR.AutoReadAll = true
|
|
session.live = &liveSettings{}
|
|
session.live.Store(settings)
|
|
session.Handle(input.CommandEvent{Command: input.OCRWindow})
|
|
ocr_test_receive(t, engine.calls)
|
|
engine.results <- ocr.Result{Text: "first\nsecond"}
|
|
request := ocr_test_receive(t, output.speech)
|
|
ocr_test_receive(t, output.frames)
|
|
settings.OCR.AutoRefresh = true
|
|
session.live.Store(settings)
|
|
session.mu.Lock()
|
|
session.refresh_locked(true)
|
|
if session.refreshing || request.context.Err() != nil {
|
|
session.mu.Unlock()
|
|
t.Fatal("periodic refresh truncated Say All")
|
|
}
|
|
if interrupted {
|
|
ctx, cancel := context.WithCancel(session.job.ctx)
|
|
session.readAllContext = ctx
|
|
cancel()
|
|
} else {
|
|
completeSpeechRequest(request, nil)
|
|
}
|
|
session.refresh_locked(true)
|
|
if !session.refreshing {
|
|
session.mu.Unlock()
|
|
t.Fatal("periodic refresh did not resume after reading")
|
|
}
|
|
session.mu.Unlock()
|
|
ocr_test_receive(t, engine.calls)
|
|
engine.results <- ocr.Result{Text: "changed"}
|
|
if got := ocr_test_receive(t, output.speech); got.Text != "changed" || len(got.ReadAll) != 0 {
|
|
t.Fatalf("automatic refresh restarted Say All: %+v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCRShiftSelectionAnnouncesChangedRanges(t *testing.T) {
|
|
session, _, _, engine, output := ocr_test_session(t)
|
|
session.Handle(input.CommandEvent{Command: input.OCRWindow})
|
|
ocr_test_receive(t, engine.calls)
|
|
engine.results <- ocr.Result{Text: "é猫abc\nnext"}
|
|
ocr_test_receive(t, output.speech)
|
|
ocr_test_receive(t, output.frames)
|
|
steps := []struct {
|
|
command input.Command
|
|
shift bool
|
|
speech string
|
|
offset, start, end int
|
|
}{
|
|
{input.OCRNextCharacter, true, "é selected", 1, 0, 1},
|
|
{input.OCRNextCharacter, true, "猫 selected", 2, 0, 2},
|
|
{input.OCRPreviousCharacter, true, "猫 unselected", 1, 0, 1},
|
|
{input.OCRPreviousCharacter, true, "é unselected", 0, 0, 0},
|
|
{input.OCRPreviousCharacter, true, "OCR text boundary", 0, 0, 0},
|
|
{input.OCRNextCharacter, false, "猫", 1, -1, -1},
|
|
{input.OCRNextCharacter, true, "猫 selected", 2, 1, 2},
|
|
{input.OCRLineStart, true, "猫 unselected, é selected", 0, 0, 1},
|
|
{input.OCRLineEnd, true, "é unselected, 猫abc selected", 5, 1, 5},
|
|
{input.OCRNextCharacter, true, "selected", 6, 1, 6},
|
|
{input.OCRDocumentEnd, true, "next selected", 10, 1, 10},
|
|
{input.OCRNextCharacter, false, "OCR text boundary", 10, -1, -1},
|
|
}
|
|
for _, step := range steps {
|
|
modifiers := uint16(0)
|
|
if step.shift {
|
|
modifiers = 1
|
|
}
|
|
session.Handle(input.CommandEvent{Command: step.command, Chord: input.CommandChord{Modifiers: modifiers}})
|
|
if got := ocr_test_receive(t, output.speech); got.Text != step.speech {
|
|
t.Fatalf("%s shift=%v: %q, want %q", step.command, step.shift, got.Text, step.speech)
|
|
}
|
|
frame := ocr_test_receive(t, output.frames)
|
|
session.mu.Lock()
|
|
start, end := session.selection_locked()
|
|
offset := session.cursor.Offset()
|
|
session.mu.Unlock()
|
|
if offset != step.offset || start != step.start || end != step.end || frame.Virtual == nil {
|
|
t.Fatalf("%s: offset=%d selection=%d:%d frame=%+v", step.command, offset, start, end, frame)
|
|
}
|
|
for index, dots := range frame.Dots {
|
|
source := frame.Virtual.SourceStart + index
|
|
if (dots != 0) != (source >= start && source < end) {
|
|
t.Fatalf("wrong selection dots: %+v", frame)
|
|
}
|
|
}
|
|
if start == end && len(frame.Dots) != 0 {
|
|
t.Fatalf("cleared selection retained dots: %+v", frame)
|
|
}
|
|
}
|
|
}
|