discordo/internal/ui/chat/composer/model_test.go

147 lines
4.8 KiB
Go

package composer
import (
"maps"
"path/filepath"
"testing"
"github.com/ayn2op/arikawa/v3/discord"
"github.com/ayn2op/arikawa/v3/gateway"
"github.com/ayn2op/arikawa/v3/state"
"github.com/ayn2op/arikawa/v3/utils/sendpart"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/ningen/v3"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
)
// newTestModel returns an enabled composer with the default config.
func newTestModel(t *testing.T) (*Model, *config.Config) {
t.Helper()
cfg, err := config.Load(filepath.Join(t.TempDir(), "config.toml"))
if err != nil {
t.Fatal(err)
}
c := NewModel(cfg, ningen.FromState(state.New("")))
c.disabled = false
return &c, cfg
}
func TestModelUpdate(t *testing.T) {
up := tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)
for _, tt := range []struct {
name string
setup func(*Model, *config.Config) tview.KeyMsg
want bool
}{
{"empty", func(*Model, *config.Config) tview.KeyMsg { return up }, true},
{"draft", func(c *Model, _ *config.Config) tview.KeyMsg { c.setText("draft"); return up }, false},
{"reply", func(c *Model, _ *config.Config) tview.KeyMsg {
c.StartReply(discord.Message{ID: 3}, "name", false)
return up
}, false},
{"attachment", func(c *Model, _ *config.Config) tview.KeyMsg {
c.sendMessageData.Files = []sendpart.File{{Name: "file"}}
return up
}, false},
{"editing", func(c *Model, _ *config.Config) tview.KeyMsg { c.StartEdit(discord.Message{}); return up }, false},
{"disabled", func(c *Model, _ *config.Config) tview.KeyMsg { c.disabled = true; return up }, false},
{"remapped", func(_ *Model, cfg *config.Config) tview.KeyMsg {
cfg.Keybinds.Composer.EditLast.Keybind = keybind.New("ctrl+p")
return tcell.NewEventKey(tcell.KeyCtrlP, "", tcell.ModNone)
}, true},
{"unbound", func(_ *Model, cfg *config.Config) tview.KeyMsg {
cfg.Keybinds.Composer.EditLast.Keybind = keybind.New()
return up
}, false},
} {
t.Run("edit last message: "+tt.name, func(t *testing.T) {
c, cfg := newTestModel(t)
// setup changes c, so it runs before Update copies it.
msg := tt.setup(c, cfg)
var cmd tview.Cmd
*c, cmd = c.Update(msg)
if got := cmd != nil && cmd() == (EditLastMsg{}); got != tt.want {
t.Fatalf("asked to edit the last message = %v, want %v", got, tt.want)
}
})
}
}
func TestModelHandle(t *testing.T) {
c, _ := newTestModel(t)
area := tview.Rectangle{Width: 20, Height: 3}
t.Run("typing edits the text", func(t *testing.T) {
msg := c.View(true).Handle(tcell.NewEventKey(tcell.KeyRune, "a", tcell.ModNone), area)
if _, ok := msg.(editMsg); !ok {
t.Fatalf("got %v, want an editMsg", msg)
}
*c, _ = c.Update(msg)
if c.editState.Value() != "a" {
t.Fatalf("text = %q", c.editState.Value())
}
})
t.Run("the newline keybind inserts a newline", func(t *testing.T) {
msg := c.View(true).Handle(tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModShift), area)
if _, ok := msg.(editMsg); !ok {
t.Fatalf("got %v, want an editMsg", msg)
}
*c, _ = c.Update(msg)
if c.editState.Value() != "a\n" {
t.Fatalf("text = %q", c.editState.Value())
}
})
t.Run("bound keys are left to Update", func(t *testing.T) {
enter := tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone)
if got := c.View(true).Handle(enter, area); got != enter {
t.Fatalf("got %v", got)
}
})
}
func TestModelSearchMember(t *testing.T) {
c, _ := newTestModel(t)
c.state.MemberState.SearchLimit = 2
c.CacheMemberSearch(&gateway.GuildMembersChunkEvent{
Nonce: memberSearchNonce + "1 ab",
Members: []discord.Member{{}},
})
if cmd := c.searchMember(1, "ab"); cmd != nil {
t.Fatal("cached query returned a network command")
}
if cmd := c.searchMember(1, "abc"); cmd != nil || c.memberSearchCache["1 abc"] != 1 {
t.Fatal("incomplete prefix results were not reused")
}
c.memberSearchCache["1 a"] = 2
c.memberSearchCache["2 a"] = 2
c.OnGuildMemberRemove(&gateway.GuildMemberRemoveEvent{
GuildID: 1,
User: discord.User{Username: "abc"},
})
want := map[string]uint{"1 ab": 1, "1 abc": 1, "2 a": 2}
if !maps.Equal(c.memberSearchCache, want) {
t.Fatalf("cache after invalidation = %v, want %v", c.memberSearchCache, want)
}
}
func TestModelTabSuggest(t *testing.T) {
c, _ := newTestModel(t)
if err := c.state.Cabinet.MyselfSet(discord.User{ID: 1, Username: "1"}, false); err != nil {
t.Fatal(err)
}
channel := discord.Channel{ID: 1, Type: discord.DirectMessage}
for id, author := range []discord.UserID{2, 3, 2, 1} {
message := discord.Message{ID: discord.MessageID(id + 1), ChannelID: 1, Author: discord.User{ID: author, Username: author.String()}}
if err := c.state.Cabinet.MessageSet(&message, false); err != nil {
t.Fatal(err)
}
}
c.SetChannel(&channel)
c.setText("@")
c.tabSuggest()
if got := c.mentionsList.ItemCount(); got != 2 {
t.Fatalf("suggested %d authors, want 2", got)
}
}