Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

@@ -0,0 +1,125 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
import (
"context"
"os"
"strings"
"testing"
"time"
)
func TestNativeSpeechMetadata(t *testing.T) {
if os.Getenv("CTHULHU_SPEECH_METADATA") != "1" {
t.Skip("requires private GTK speech fixture")
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
adapter := New()
adapter.SetCaptureTextAttributes(true)
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer func() {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second)
defer stopCancel()
if err := adapter.Stop(stopCtx); err != nil {
t.Error(err)
}
}()
for _, probe := range []struct{ key, name string }{{"F9", "Mnemonic probe"}, {"F10", "Indentation probe"}} {
pressTreeProbeKey(t, probe.key)
for {
select {
case <-ctx.Done():
t.Fatal("missing speech metadata focus event", probe.name)
case event := <-adapter.Events():
if !((event.Type == FocusEvent || event.Type == FocusedStateEvent) && event.Source.Name == probe.name) {
continue
}
windowContext := false
for _, ancestor := range event.Source.BrailleContext {
if ancestor.Role == "frame" && ancestor.Name != "" {
windowContext = true
}
}
if !windowContext {
t.Fatalf("missing native braille window context: %+v", event.Source.BrailleContext)
}
if probe.key == "F9" {
if !strings.Contains(strings.ToLower(event.Source.Mnemonic), "a") {
t.Fatalf("missing native mnemonic: %q", event.Source.Mnemonic)
}
} else {
if event.Source.BrailleLine != "\t Indented É" {
t.Fatalf("raw indentation lost: %q", event.Source.BrailleLine)
}
found := false
for _, ancestor := range event.Source.SpeechContext {
if (ancestor.Role == "list" || ancestor.Role == "list box") && ancestor.Name == "Dynamic controls" {
if !ancestor.ChildCountKnown || ancestor.ChildCount != 10 {
t.Fatalf("native list count unavailable: %+v", ancestor)
}
found = true
}
}
if !found {
t.Fatalf("missing native list context: %+v", event.Source.SpeechContext)
}
}
goto nextProbe
}
}
nextProbe:
}
pressTreeProbeKey(t, "F11")
for {
select {
case <-ctx.Done():
t.Fatal("missing color attribute focus event")
case event := <-adapter.Events():
if (event.Type != FocusEvent && event.Type != FocusedStateEvent) || event.Source.Name != "Color attribute probe" {
continue
}
if len(event.Source.TextAttributeRuns) == 0 || event.Source.TextAttributeRuns[0].Values["fg-color"] == "" {
t.Fatalf("native braille attribute runs missing: %+v", event.Source.TextAttributeRuns)
}
if !event.Source.Misspelled || !TextAttributesMisspelled(event.Source.TextAttributeRuns[0].Values) {
t.Fatalf("native error underline lost its spelling indicator: %+v", event.Source.TextAttributeRuns)
}
attributes, err := adapter.ReadCharacterAttributes(ctx, event.Source.ID())
if err != nil {
t.Fatal(err)
}
if !TextAttributesMisspelled(attributes.Values) {
t.Fatalf("character attributes lost spelling indicator: %+v", attributes.Values)
}
if color := attributes.Values["fg-color"]; color == "" {
t.Fatalf("native foreground color missing: %+v", attributes.Values)
} else {
t.Logf("native foreground color: %q", color)
}
goto protectedProbe
}
}
protectedProbe:
pressTreeProbeKey(t, "F7")
for {
select {
case <-ctx.Done():
t.Fatal("missing protected text focus event")
case event := <-adapter.Events():
if (event.Type != FocusEvent && event.Type != FocusedStateEvent) || event.Source.Name != "Protected text probe" {
continue
}
attributes, err := adapter.ReadCharacterAttributes(ctx, event.Source.ID())
if err != nil || len(attributes.Values) != 0 {
t.Fatalf("password attributes: %+v, %v", attributes.Values, err)
}
return
}
}
}