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,131 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
"git.stormux.org/storm/cthulhu/internal/interaction"
|
||||
"git.stormux.org/storm/cthulhu/internal/review"
|
||||
)
|
||||
|
||||
type uiTestKeys struct{ session atomic.Uint64 }
|
||||
|
||||
func (keys *uiTestKeys) InternalUISession() uint64 { return keys.session.Load() }
|
||||
func (keys *uiTestKeys) InternalUIActive() bool { return keys.session.Load() != 0 }
|
||||
func (keys *uiTestKeys) BeginInternalUI(context.Context) (uint64, error) {
|
||||
keys.session.Store(1)
|
||||
return 1, nil
|
||||
}
|
||||
func (keys *uiTestKeys) EndInternalUI(_ context.Context, id uint64) error {
|
||||
keys.session.CompareAndSwap(id, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
type uiTestActions struct {
|
||||
keys *uiTestKeys
|
||||
invoked chan string
|
||||
object atspi.ObjectID
|
||||
}
|
||||
|
||||
func (source *uiTestActions) ObjectOfInterest(context.Context) (review.Interest, error) {
|
||||
return review.Interest{Object: source.object}, nil
|
||||
}
|
||||
func (source *uiTestActions) AccessibleActions(_ context.Context, object atspi.ObjectID) (atspi.ActionSet, error) {
|
||||
return atspi.ActionSet{Target: atspi.ActionTarget{Object: object}, Actions: []atspi.AccessibleAction{{Name: "click", Label: "Click"}, {Name: "press", Label: "Press"}}}, nil
|
||||
}
|
||||
func (source *uiTestActions) PerformAccessibleAction(_ context.Context, target atspi.ActionTarget, name string) error {
|
||||
if source.keys.InternalUISession() != 0 || target.Object != source.object {
|
||||
source.invoked <- "wrong target or capture retained"
|
||||
} else {
|
||||
source.invoked <- name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestInternalUIActionsAndFormUseSameHost(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
keys := &uiTestKeys{}
|
||||
keys.session.Store(1)
|
||||
source := &uiTestActions{keys: keys, object: atspi.NewObjectID(":1.42", "/button"), invoked: make(chan string, 1)}
|
||||
output := &ocrTestOutput{speech: make(chan speechRequest, 16)}
|
||||
ui := &internalUI{keys: keys, actions: source, interest: source, speech: output, gate: &focusGate{}, events: make(chan input.CommandEvent, 16), requests: make(chan internalUIRequest, 1), done: make(chan struct{})}
|
||||
go ui.run(ctx)
|
||||
defer func() { cancel(); <-ui.done }()
|
||||
ui.Handle(input.CommandEvent{Command: input.ShowActions, UISession: 1})
|
||||
select {
|
||||
case message := <-output.speech:
|
||||
if message.Cause != "internal-ui" || message.Text != "Actions. Click" {
|
||||
t.Fatal(message)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("menu did not open")
|
||||
}
|
||||
ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff54}})
|
||||
ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff0d}})
|
||||
select {
|
||||
case action := <-source.invoked:
|
||||
if action != "press" {
|
||||
t.Fatal(action)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("action did not run")
|
||||
}
|
||||
select {
|
||||
case message := <-output.speech:
|
||||
if message.Text != "Press" {
|
||||
t.Fatalf("navigation repeated title: %q", message.Text)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("selection was not announced")
|
||||
}
|
||||
result, err := ui.Open(ctx, interaction.Form{Title: "Example", Elements: []interaction.Element{{ID: "entry", Label: "Code", Kind: interaction.Entry}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for keys.InternalUISession() == 0 {
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal("form did not open")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Text: "é", Chord: input.CommandChord{Symbol: 0xe9}})
|
||||
ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff0d}})
|
||||
select {
|
||||
case submitted := <-result:
|
||||
if submitted.Activated != "entry" || submitted.Values["entry"].Text != "é" {
|
||||
t.Fatalf("form=%+v", submitted)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatal("form did not submit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalUIBrailleRestoresNewestApplicationFrame(t *testing.T) {
|
||||
worker := &brailleWorker{wake: make(chan struct{}, 1), cancel: func() {}, connected: true}
|
||||
worker.EnqueueFrame(braille.Frame{Text: "Application", Cursor: 2})
|
||||
worker.SetInternalUIFrame(&braille.Frame{Text: "Code, edit: abc", Cursor: 14})
|
||||
worker.EnqueueFrame(braille.Frame{Text: "Newest application", Cursor: 4})
|
||||
if worker.current().Text != "Code, edit: abc" {
|
||||
t.Fatal("application overwrote UI")
|
||||
}
|
||||
options := config.DefaultSettings().Braille
|
||||
options.FlashMessages = true
|
||||
if worker.Flash("Background", options) {
|
||||
t.Fatal("background flash overwrote UI")
|
||||
}
|
||||
worker.SetInternalUIFrame(nil)
|
||||
if worker.current().Text != "Newest application" || worker.current().Cursor != 4 {
|
||||
t.Fatal("latest application frame not restored")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user