// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "testing" "time" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/interaction" ) type emojiTestSource struct { keys *uiTestKeys calls chan string err error } func (source *emojiTestSource) TypeText(_ context.Context, text string) error { if source.keys.InternalUISession() != 0 { source.calls <- "capture retained" } else { source.calls <- text } return source.err } func TestEmojiHostCancelCheckboxMemoryAndClipboard(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() keys := &uiTestKeys{} source := &emojiTestSource{keys: keys, calls: make(chan string, 4)} copier := &ocrTestCopier{copied: make(chan string, 4)} output := &ocrTestOutput{speech: make(chan speechRequest, 64)} ui := &internalUI{keys: keys, emojiSource: source, emojiCopier: copier, speech: output, gate: &focusGate{}, events: make(chan input.CommandEvent, 32), done: make(chan struct{})} go ui.run(ctx) defer func() { cancel(); <-ui.done }() wait := func(prefix string) { t.Helper() for { select { case message := <-output.speech: if len(message.Text) >= len(prefix) && message.Text[:len(prefix)] == prefix { return } case <-ctx.Done(): t.Fatalf("missing %q", prefix) } } } open := func() { t.Helper() ui.Handle(input.CommandEvent{Command: input.EmojiMenu}) wait("Emoji Menu. All tab") } key := func(symbol uint32, text string) { ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: symbol}, Text: text}) } open() key(0xff09, "") key(0, "goat") key(0xff0d, "") wait("Inserted Goat") if got := <-source.calls; got != "🐐" { t.Fatal(got) } select { case got := <-copier.copied: t.Fatalf("default copied: %q", got) default: } // Toggle copying on, cancel, and verify the next invocation retains the choice. open() key(0xff09, "") key(0xff09, "") key(0, " ") key(0xff1b, "") for keys.InternalUISession() != 0 { if ctx.Err() != nil { t.Fatal("cancel not processed") } time.Sleep(time.Millisecond) } open() key(0xff09, "") key(0, "astronaut") key(0xff0d, "") wait("Copied Astronaut to clipboard") if got := <-copier.copied; got != "🧑‍🚀" { t.Fatal(got) } select { case got := <-source.calls: t.Fatalf("copy mode typed: %q", got) default: } } func TestEmojiTypingFailure(t *testing.T) { source := &emojiTestSource{keys: &uiTestKeys{}, calls: make(chan string, 1), err: errors.New("unsupported")} output := &ocrTestOutput{speech: make(chan speechRequest, 1)} ui := &internalUI{keys: source.keys, emojiSource: source, speech: output, gate: &focusGate{}} ui.select_emoji(context.Background(), interaction.Value{Selected: "🐐", Text: "Goat"}, false) if got := (<-output.speech).Text; got != "Emoji insertion unavailable" { t.Fatal(got) } } type emojiBrowseKeys struct{ uiTestKeys } func (*emojiBrowseKeys) BrowseNavigationEnabled() bool { return true } func TestEmojiBrowseModeBlocksTypingAndAllowsCopy(t *testing.T) { keys := &emojiBrowseKeys{} source := &emojiTestSource{keys: &keys.uiTestKeys, calls: make(chan string, 1)} copier := &ocrTestCopier{copied: make(chan string, 1)} output := &ocrTestOutput{speech: make(chan speechRequest, 2)} ui := &internalUI{keys: keys, emojiSource: source, emojiCopier: copier, speech: output, gate: &focusGate{}} ui.select_emoji(context.Background(), interaction.Value{Selected: "🐐", Text: "Goat"}, false) if got := (<-output.speech).Text; got != "Emoji insertion unavailable in browse mode" { t.Fatal(got) } ui.select_emoji(context.Background(), interaction.Value{Selected: "🐐", Text: "Goat"}, true) if got := (<-output.speech).Text; got != "Copied Goat to clipboard" { t.Fatal(got) } select { case got := <-source.calls: t.Fatalf("typed in browse mode: %q", got) default: } if got := <-copier.copied; got != "🐐" { t.Fatal(got) } }