Files
cthulhu/internal/app/ocr_reading_test.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)
}
}
}