Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,162 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user