package chat import ( "path/filepath" "testing" "time" "github.com/ayn2op/arikawa/v3/discord" "github.com/ayn2op/arikawa/v3/gateway" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/ui/chat/emojipicker" "github.com/ayn2op/discordo/internal/ui/chat/messageslist" "github.com/ayn2op/tview" "github.com/gdamore/tcell/v3" ) func newTestModel(t *testing.T) *Model { t.Helper() cfg, err := config.Load(filepath.Join(t.TempDir(), "config.toml")) if err != nil { t.Fatal(err) } m := NewModel(cfg, "") return &m } func TestModelHandle(t *testing.T) { t.Run("typing in the channels picker", func(t *testing.T) { m := newTestModel(t) m.openPicker() area := tview.Rectangle{Width: 120, Height: 40} // The picker's action passes through the panes below it, which must leave it alone. msg := m.View().Handle(tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone), area) if msg == nil { t.Fatal("typing produced no message") } if _, ok := msg.(paneMsg); ok { t.Fatalf("the picker's action was marked for a pane: %v", msg) } *m, _ = m.Update(msg) }) } func TestModelUpdate(t *testing.T) { t.Run("typing expires unless extended", func(t *testing.T) { m := newTestModel(t) m.addTyper(1) m.addTyper(1) *m, _ = m.Update(typingExpiredMsg{userID: 1}) if _, ok := m.typers[1]; !ok { t.Fatal("an extended typing indicator expired early") } m.typers[1] = time.Now().Add(-time.Second) *m, _ = m.Update(typingExpiredMsg{userID: 1}) if _, ok := m.typers[1]; ok { t.Fatal("the typing indicator did not expire") } }) } func TestModelAddMessageOrNotify(t *testing.T) { for _, tt := range []struct { name string focused bool whenUnfocused bool want bool }{ {"focused", true, true, false}, {"unfocused", false, true, true}, {"unfocused but disabled", false, false, false}, } { t.Run(tt.name, func(t *testing.T) { m := newTestModel(t) m.cfg.Notifications.WhenUnfocused = tt.whenUnfocused // Loading an avatar is a command too. m.cfg.UI.MessagesList.Avatars.Preview = false m.setSelectedChannel(&discord.Channel{ID: 1}) *m, _ = m.Update(tcell.NewEventFocus(tt.focused)) cmd := m.addMessageOrNotify(&gateway.MessageCreateEvent{ID: 1, ChannelID: 1}) if got := cmd != nil; got != tt.want { t.Fatalf("notified = %v, want %v", got, tt.want) } }) } } func TestModelCycleFocus(t *testing.T) { m := newTestModel(t) m.setSelectedChannel(&discord.Channel{ID: 1, GuildID: 2, Type: discord.GuildText}) m.focused = paneGuildsTree // The composer is disabled without permission to send messages, so it is skipped. for _, want := range []pane{paneMessagesList, paneMembersTree, paneGuildsTree} { if m.cycleFocus(1); m.focused != want { t.Fatalf("focused = %v, want %v", m.focused, want) } } for _, want := range []pane{paneMembersTree, paneMessagesList, paneGuildsTree} { if m.cycleFocus(-1); m.focused != want { t.Fatalf("focused = %v, want %v", m.focused, want) } } } func TestModelReactions(t *testing.T) { m := newTestModel(t) channel := discord.Channel{ID: 1, GuildID: 2, Type: discord.GuildText} m.setSelectedChannel(&channel) msg := discord.Message{ID: 10, ChannelID: 1} *m, _ = m.Update(messageslist.ReactMsg{Message: msg}) if m.overlay != overlayEmojiPicker { t.Fatalf("overlay = %v, want overlayEmojiPicker", m.overlay) } if m.reactingMessage == nil || m.reactingMessage.ID != 10 { t.Fatalf("reactingMessage = %v, want ID 10", m.reactingMessage) } // Cancel closes overlay *m, _ = m.Update(emojipicker.CancelMsg{}) if m.overlay != overlayNone { t.Fatalf("overlay after cancel = %v, want overlayNone", m.overlay) } // Select triggers react command and closes overlay *m, _ = m.Update(messageslist.ReactMsg{Message: msg}) var cmd tview.Cmd *m, cmd = m.Update(emojipicker.SelectedMsg{Emoji: discord.APIEmoji("😄")}) if m.overlay != overlayNone { t.Fatalf("overlay after select = %v, want overlayNone", m.overlay) } if cmd == nil { t.Fatal("expected non-nil command for reaction action") } }