Files
cthulhu/internal/app/submitted_text_test.go

113 lines
4.9 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"
"slices"
"testing"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
)
func TestSubmittedMessageIsAnnouncedOnce(t *testing.T) {
for _, key := range []string{"Return", "KP_Enter"} {
t.Run(key, func(t *testing.T) {
entry := ownerEvent(atspi.FocusEvent, ":1.42", "/window", "/entry")
entry.Source.Role, entry.Source.ApplicationProcess = "text", "pidgin"
entry.Source.Editable, entry.Source.Focused, entry.Source.TextCaretKnown = true, true, true
deletion := atspi.NewEventWithPayload(atspi.TextDeletedEvent, entry.Source, "A sample message.")
deletion.Window = entry.Window
message := pidginMessageEvent("Username: A sample message.")
tracker := &input.IntentTracker{}
tracker.Observe(input.NewKeyEvent(0, true, key))
tracker.Observe(input.NewKeyEvent(0, false, key))
got := submitted_text_speech(t, tracker, entry, deletion,
atspi.NewEvent(atspi.CaretEvent, entry.Source), message)
want := []string{"text", "Username: A sample message."}
if !slices.Equal(got, want) {
t.Fatalf("submission speech = %q, want %q", got, want)
}
})
}
}
func TestSubmissionClearPreservesEditingFeedback(t *testing.T) {
for _, test := range []struct {
name string
keys []string
selected bool
removeFirst bool
insert string
remainingText string
caretKnown bool
want []string
}{
{name: "plain Enter clear", keys: []string{"Return"}, caretKnown: true},
{name: "Backspace", keys: []string{"BackSpace"}, caretKnown: true, want: []string{"old"}},
{name: "Delete", keys: []string{"Delete"}, caretKnown: true, want: []string{"old"}},
{name: "word deletion", keys: []string{"Control_L", "BackSpace"}, caretKnown: true, want: []string{"old"}},
{name: "cut", keys: []string{"Control_L", "x"}, caretKnown: true, want: []string{"cut"}},
{name: "paste replacement", keys: []string{"Control_L", "v"}, insert: "new", caretKnown: true, want: []string{"old", "pasted from clipboard"}},
{name: "undo", keys: []string{"Control_L", "z"}, caretKnown: true, want: []string{"old"}},
{name: "redo", keys: []string{"Control_L", "Shift_L", "z"}, caretKnown: true, want: []string{"old"}},
{name: "typing replaces selection", keys: []string{"x"}, selected: true, insert: "x", caretKnown: true, want: []string{"old", "x"}},
{name: "Enter replaces selection", keys: []string{"Return"}, selected: true, insert: "\n", caretKnown: true, want: []string{"old"}},
{name: "selection removal precedes replacement", keys: []string{"Return"}, selected: true, removeFirst: true, caretKnown: true, want: []string{"selection removed", "old"}},
{name: "Shift Enter", keys: []string{"Shift_L", "Return"}, caretKnown: true, want: []string{"old"}},
{name: "unknown caret", keys: []string{"Return"}, want: []string{"old"}},
{name: "no keyboard evidence", caretKnown: true, want: []string{"old"}},
{name: "Enter leaves text", keys: []string{"Return"}, caretKnown: true, remainingText: "remaining", want: []string{"old"}},
} {
t.Run(test.name, func(t *testing.T) {
tracker := &input.IntentTracker{}
for _, key := range test.keys {
tracker.Observe(input.NewKeyEvent(0, true, key))
}
entry := atspi.Snapshot{BusName: ":1.42", Path: "/entry", Role: "entry", Editable: true, Focused: true, TextCaretKnown: test.caretKnown}
if test.selected {
entry.SelectionStart, entry.SelectionEnd, entry.SelectionText = 0, 3, "old"
}
events := []atspi.Event{atspi.NewEvent(atspi.FocusEvent, entry)}
entry.SelectionStart, entry.SelectionEnd, entry.SelectionText = 0, 0, ""
entry.Text = test.remainingText
if test.removeFirst {
events = append(events, atspi.NewEvent(atspi.TextSelectionChangedEvent, entry))
}
events = append(events, atspi.NewEventWithPayload(atspi.TextDeletedEvent, entry, "old"))
if test.insert != "" {
events = append(events, atspi.NewEventWithPayload(atspi.TextInsertedEvent, entry, test.insert))
}
got := submitted_text_speech(t, tracker, events...)
// Initial focus also presents an existing selection.
got = got[1:]
if !slices.Equal(got, test.want) {
t.Fatalf("editing speech = %q, want %q", got, test.want)
}
})
}
}
func submitted_text_speech(t *testing.T, inputSource any, events ...atspi.Event) []string {
t.Helper()
adapter := &fakeAdapter{events: make(chan atspi.Event, len(events))}
for _, event := range events {
adapter.events <- event
}
close(adapter.events)
queue := &remoteSpeechQueue{}
if err := processEventsWithInput(context.Background(), slog.New(slog.DiscardHandler), queue, adapter,
nil, nil, 0, nil, inputSource, nil, nil); err != nil {
t.Fatal(err)
}
texts := make([]string, 0, len(queue.requests))
for _, request := range queue.requests {
texts = append(texts, request.Text)
}
return texts
}