138 lines
6.2 KiB
Go
138 lines
6.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/braille"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
)
|
|
|
|
func TestEditorParagraphUsesConfiguredEcho(t *testing.T) {
|
|
for _, echo := range []string{"disabled", "character", "word", "sentence"} {
|
|
t.Run(echo, func(t *testing.T) {
|
|
options := presentation.DefaultOptions()
|
|
payload, contextText, want := "o", "Hello", "o"
|
|
switch echo {
|
|
case "disabled":
|
|
options.EnableKeyEcho, want = false, ""
|
|
case "character":
|
|
options.EnableEchoByCharacter = true
|
|
case "word":
|
|
options.EnableEchoByWord = true
|
|
payload, contextText, want = " ", "Hello ", "Hello"
|
|
case "sentence":
|
|
options.EnableEchoBySentence = true
|
|
payload, contextText, want = " ", "Hello. ", "Hello."
|
|
}
|
|
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
processEvents(ctx, slog.New(slog.DiscardHandler), queue, adapter, 0, options)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
editor := atspi.Snapshot{BusName: ":1.7", Path: "/editor", Name: "Composer", Role: "entry", Editable: true}
|
|
paragraph := atspi.Snapshot{BusName: editor.BusName, Path: "/paragraph", Role: "paragraph", Editable: true,
|
|
EditableRoot: editor.ID(), Text: contextText, TextBeforeCaret: contextText}
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, editor)
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, payload)
|
|
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/next", Role: "button", Name: "Next"})
|
|
// Once focus leaves, delayed events from that editor stay quiet.
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "delayed edit")
|
|
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/end", Role: "button", Name: "End"})
|
|
wants := []string{"Composer, entry"}
|
|
if want != "" {
|
|
wants = append(wants, want)
|
|
}
|
|
wants = append(wants, "Next, button", "End, button")
|
|
for _, want := range wants {
|
|
select {
|
|
case got := <-queue.spoken:
|
|
if got != want {
|
|
t.Fatalf("speech = %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("missing %q", want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEditableDocumentParagraphEvents(t *testing.T) {
|
|
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, nil, nil, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
editor := atspi.Snapshot{BusName: ":1.7", Path: "/editor", Name: "Message body", Role: "document web", Editable: true}
|
|
paragraph := atspi.Snapshot{BusName: editor.BusName, Path: "/paragraph", Role: "paragraph", Editable: true,
|
|
EditableRoot: editor.ID(), Text: "a", TextCaretKnown: true, BrailleLine: "a", BrailleCaret: 1}
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, editor)
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "a")
|
|
// This caret must not reread the growing line after accepted typing.
|
|
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
|
paragraph.SelectionStart, paragraph.SelectionEnd, paragraph.SelectionText = 0, 1, "a"
|
|
adapter.events <- atspi.NewEvent(atspi.TextSelectionChangedEvent, paragraph)
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextDeletedEvent, paragraph, "a")
|
|
subject := atspi.Snapshot{BusName: editor.BusName, Path: "/subject", Name: "Subject", Role: "entry", Editable: true}
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, subject)
|
|
// A delayed mutation from the old body cannot interrupt a header field.
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "unfocused text")
|
|
other := paragraph
|
|
other.Path, other.EditableRoot = "/other_paragraph", atspi.NewObjectID(editor.BusName, "/other_editor")
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, other, "other editor")
|
|
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/end", Role: "button", Name: "End"})
|
|
for _, want := range []string{"Message body, document web", "a", "a selected", "a", "Subject, entry", "End, button"} {
|
|
select {
|
|
case got := <-queue.spoken:
|
|
if got != want {
|
|
t.Fatalf("speech = %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEditableDocumentCaretAndSelectionBraille(t *testing.T) {
|
|
root := atspi.NewObjectID(":1.7", "/editor")
|
|
frames := make(chan braille.Frame, 2)
|
|
state := &eventLoopState{}
|
|
state.focusedID = root
|
|
handler := accessibilityEventHandler{state: state, logger: slog.New(slog.DiscardHandler), brailleOutput: frameRecorder{frames: frames}}
|
|
source := atspi.Snapshot{BusName: root.BusName, Path: "/paragraph", Role: "paragraph", Editable: true,
|
|
EditableRoot: root, Text: "body", TextCaretKnown: true, BrailleLine: "body", BrailleCaret: 2}
|
|
for _, eventType := range []string{atspi.CaretEvent, atspi.TextSelectionChangedEvent} {
|
|
handler.acceptFocusedEvent(atspi.NewEvent(eventType, source), recentInput(nil))
|
|
if state.focusedID != root || state.current.ID() != source.ID() || state.brailleSnapshot.ID() != source.ID() {
|
|
t.Fatal("paragraph presentation lost native editor focus or Braille ownership")
|
|
}
|
|
select {
|
|
case frame := <-frames:
|
|
if !strings.HasPrefix(frame.Text, "body") || frame.Cursor != 2 || len(frame.Routes) == 0 || frame.Routes[0].Path != source.Path {
|
|
t.Fatalf("paragraph Braille frame = %+v", frame)
|
|
}
|
|
default:
|
|
t.Fatal("missing paragraph Braille frame")
|
|
}
|
|
}
|
|
}
|