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,226 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
func TestProcessEventsSpeaksChangedCaretLines(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 2)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 3)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
completed := make(chan struct{})
|
||||
go func() {
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0)
|
||||
close(completed)
|
||||
}()
|
||||
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshot("/accessible/terminal", "", "terminal", "", "First line"))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "First line" {
|
||||
t.Fatalf("spoken text = %q, want %q", text, "First line")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for first caret speech")
|
||||
}
|
||||
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshot("/accessible/terminal", "", "terminal", "", "First line"))
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshot("/accessible/terminal", "", "terminal", "", "Second line"))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "Second line" {
|
||||
t.Fatalf("spoken text = %q, want %q", text, "Second line")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for second caret speech")
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case <-completed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for event loop to stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsSuppressesCaretAfterEditableTextChange(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 3)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 4)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
completed := make(chan struct{})
|
||||
go func() {
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0)
|
||||
close(completed)
|
||||
}()
|
||||
|
||||
paragraph := atspi.Snapshot{Path: "/writer/paragraph", Role: "paragraph", Editable: true,
|
||||
Text: "Hello", CaretCharacter: "o"}
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, paragraph)
|
||||
<-speech.spoken
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "o")
|
||||
if got := <-speech.spoken; got != "o" {
|
||||
t.Fatalf("insert speech = %q", got)
|
||||
}
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent,
|
||||
atspi.Snapshot{Path: "/status", Role: "label"}, "status update")
|
||||
|
||||
select {
|
||||
case got := <-speech.spoken:
|
||||
t.Fatalf("unexpected duplicate or status speech: %q", got)
|
||||
case <-time.After(30 * time.Millisecond):
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-completed
|
||||
}
|
||||
|
||||
func TestProcessEventsSpeaksCaretCharacters(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 3)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 3)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
completed := make(chan struct{})
|
||||
go func() {
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0)
|
||||
close(completed)
|
||||
}()
|
||||
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshotWithCaretCharacter(
|
||||
"/accessible/terminal", "", "terminal", "", "prompt ls", "l",
|
||||
))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "l" {
|
||||
t.Fatalf("spoken text = %q, want %q", text, "l")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for first caret character speech")
|
||||
}
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshotWithCaretCharacter(
|
||||
"/accessible/terminal", "", "terminal", "", "prompt ll", "l",
|
||||
))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "l" {
|
||||
t.Fatalf("repeated caret character = %q, want l", text)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for repeated caret character speech")
|
||||
}
|
||||
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshotWithCaretCharacter(
|
||||
"/accessible/terminal", "", "terminal", "", "prompt ls", "s",
|
||||
))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "s" {
|
||||
t.Fatalf("spoken text = %q, want %q", text, "s")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for second caret character speech")
|
||||
}
|
||||
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshotWithCaretCharacter(
|
||||
"/accessible/terminal", "", "terminal", "", "prompt ls", " ",
|
||||
))
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "space" {
|
||||
t.Fatalf("spoken caret space = %q, want %q", text, "space")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for caret space speech")
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case <-completed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for event loop to stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsTextChangeCancelsPendingCaretLine(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 1)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 2)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
completed := make(chan struct{})
|
||||
go func() {
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, time.Hour)
|
||||
close(completed)
|
||||
}()
|
||||
|
||||
terminal := atspi.NewSnapshot("/accessible/terminal", "", "terminal", "", "prompt l")
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, terminal)
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, terminal, "l")
|
||||
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != "l" {
|
||||
t.Fatalf("spoken text = %q, want %q", text, "l")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for inserted text speech")
|
||||
}
|
||||
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
t.Fatalf("unexpected additional speech: %q", text)
|
||||
default:
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case <-completed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for event loop to stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaretEventRequestsCharacterVoice(t *testing.T) {
|
||||
queue := &autocompleteRequestQueue{requests: make(chan speechRequest, 1)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 1)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() { defer close(done); processEvents(ctx, slog.New(slog.DiscardHandler), queue, adapter, 0) }()
|
||||
defer func() { cancel(); <-done }()
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, atspi.NewSnapshotWithCaretCharacter("/terminal", "", "terminal", "", "Ab", "A"))
|
||||
select {
|
||||
case request := <-queue.requests:
|
||||
if request.Text != "A" || !request.Character {
|
||||
t.Fatalf("caret request = %+v", request)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("caret speech missing")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user