113 lines
4.9 KiB
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
|
|
}
|