Files
ZacharyZhang-NY 455f5f2a05 Add chat components 321-345; named List rows, inert scroll views
Speech bubbles, a chat log that follows its end, a composer that sends
on Enter, typing dots, chat and channel lists, read receipts,
reactions, emoji and sticker pickers, mentions, date and unread rules,
system lines, member lists, user cards, a mailbox, mail reader and
letter, a notification bell, call controls, a voice meter, attachment
chips, link previews and polls.

pixel's editor takes EnterSends; display's Bubble takes a Fill and
exports EmojiSprite. data.List names its rows, which also lets typing
choose one. layout.ScrollArea and input.TextArea hold still under
Inert, and an inert scroll view leaves the Tab order until it wakes.
2026-10-12 05:02:36 -04:00

250 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package chat_test
import (
"math"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/chat"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestReactionsToggleYours(t *testing.T) {
wd := &world{clk: newClock()}
rs := []chat.Reaction{{Emoji: "love", Count: 3}, {Emoji: "fire", Count: 1, Mine: true}}
toggled := -1
tt := view(300, 100, wd, func(c *ui.Context) {
if r := chat.Reactions(c, "Reactions", rs, chat.ReactionsOptions{}); r.Toggled >= 0 {
toggled = r.Toggled
}
})
click(t, tt, "love 3")
if !rs[0].Mine || rs[0].Count != 4 || toggled != 0 {
t.Fatalf("after a click: %+v, toggled %d", rs[0], toggled)
}
click(t, tt, "fire 1")
if rs[1].Mine || rs[1].Count != 0 || toggled != 1 {
t.Fatalf("taking yours back: %+v", rs[1])
}
for name, bad := range map[string]chat.Reaction{"negative": {Emoji: "love", Count: -1}, "mine at zero": {Emoji: "love", Mine: true}, "no such emoji": {Emoji: "nope", Count: 1}} {
mustPanic(t, name, func() {
view(300, 100, wd, func(c *ui.Context) { chat.Reactions(c, "R", []chat.Reaction{bad}, chat.ReactionsOptions{}) })
})
}
}
func TestEmojiPickerPicksByKeysAcrossCategories(t *testing.T) {
wd := &world{clk: newClock()}
picked := ""
tt := view(400, 200, wd, func(c *ui.Context) {
if r := chat.EmojiPicker(c, "Emoji", chat.EmojiPickerOptions{}); r.Picked != "" {
picked = r.Picked
}
})
// A click picks the face under it: the grid's middle.
click(t, tt, "Emoji")
if picked != "dead" {
t.Fatalf("the click picked %q", picked)
}
tt.Key(0, ui.KeyHome)
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
// A row of eight below grin is angry.
if picked != "angry" {
t.Fatalf("picked %q", picked)
}
// From the last face, a shorter category starts over.
tt.Key(0, ui.KeyEnd)
click(t, tt, "ITEMS")
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnter)
if picked != "coin" {
t.Fatalf("the items tab picked %q", picked)
}
}
func TestStickerPickerPicksBigFaces(t *testing.T) {
wd := &world{clk: newClock()}
picked := ""
tt := view(400, 400, wd, func(c *ui.Context) {
if r := chat.StickerPicker(c, "Stickers", chat.StickerPickerOptions{}); r.Picked != "" {
picked = r.Picked
}
})
if r := find(t, tt, "Stickers"); r.W != 2*4*40 {
t.Fatalf("four 40-dot stickers a row, got %v wide", r.W)
}
click(t, tt, "Stickers")
if picked != "sleep" {
t.Fatalf("the click picked %q", picked)
}
tt.Key(0, ui.KeyHome)
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyEnter)
if picked != "love" {
t.Fatalf("picked %q", picked)
}
}
func TestMentionInputCompletesMembers(t *testing.T) {
wd := &world{clk: newClock()}
v, mentioned := "", ""
tt := view(400, 200, wd, func(c *ui.Context) {
if r := chat.MentionInput(c, "Mention", &v, []string{"Aria", "Arthur", "Brom"}, chat.MentionInputOptions{}); r.Mentioned != "" {
mentioned = r.Mentioned
}
})
click(t, tt, "Mention")
tt.Type("hi @ar")
find(t, tt, "@Aria @Arthur")
tt.Key(0, ui.KeyTab)
if v != "hi @Aria " || mentioned != "Aria" {
t.Fatalf("value %q, mentioned %q", v, mentioned)
}
// Past a space the word is done: nothing is suggested.
tt.Type("ok")
if _, ok := tt.Find("@Aria @Arthur"); ok {
t.Fatal("suggestions outlived the mention")
}
}
func TestNotificationBellRingsOnlyWhenNewsArrives(t *testing.T) {
for _, reduce := range []bool{false, true} {
wd := &world{clk: newClock(), reduce: reduce}
n, clicks := 0, 0
tt := view(200, 100, wd, func(c *ui.Context) {
if chat.NotificationBell(c, "Bell", n, chat.NotificationBellOptions{}).Clicked {
clicks++
}
})
bell := find(t, tt, "Bell")
rest := count(tt.Image(), int(bell.X/2), int(bell.Y/2)+5, 3, 12, pixel.Ink)
swung := func() bool { return count(tt.Image(), int(bell.X/2), int(bell.Y/2)+5, 3, 12, pixel.Ink) != rest }
n = 3
tt.Frame()
if swung() == reduce {
t.Fatalf("reduce %v: the bell swung %v on news", reduce, !reduce)
}
wd.clk.t = wd.clk.t.Add(time.Second)
tt.Frame()
n = 1
tt.Frame()
if swung() {
t.Fatalf("reduce %v: fewer notes rang the bell", reduce)
}
click(t, tt, "Bell")
if clicks != 1 {
t.Fatal("the bell did not report its click")
}
}
mustPanic(t, "negative", func() {
view(100, 100, &world{clk: newClock()}, func(c *ui.Context) { chat.NotificationBell(c, "Bell", -1, chat.NotificationBellOptions{}) })
})
}
func TestCallControlsToggleAndHangUp(t *testing.T) {
wd := &world{clk: newClock()}
muted, camera, hung := false, true, 0
tt := view(300, 100, wd, func(c *ui.Context) {
if chat.CallControls(c, "Call", &muted, &camera, chat.CallControlsOptions{}).HungUp {
hung++
}
})
click(t, tt, "MUTE")
click(t, tt, "CAMERA")
click(t, tt, "HANG UP")
if !muted || camera || hung != 1 {
t.Fatalf("muted %v, camera %v, hung up %d", muted, camera, hung)
}
}
func TestVoiceMeterLightsBars(t *testing.T) {
wd := &world{clk: newClock()}
for _, tc := range []struct {
level float64
good, warn, hot int
}{{0, 0, 0, 0}, {0.5, 4, 0, 0}, {1, 6, 1, 1}} {
tt := view(100, 50, wd, func(c *ui.Context) { chat.VoiceMeter(c, "Voice", tc.level, chat.VoiceMeterOptions{}) })
img := tt.Image()
// Each lit bar shows one dot on the bottom row.
if g, w, b := count(img, 0, 11, 23, 1, pixel.Good)/2, count(img, 0, 11, 23, 1, pixel.Warn)/2, count(img, 0, 11, 23, 1, pixel.Bad)/2; g != tc.good || w != tc.warn || b != tc.hot {
t.Fatalf("level %v lit %d, %d, %d bars", tc.level, g, w, b)
}
}
for _, bad := range []float64{-0.1, 1.1, math.NaN()} {
mustPanic(t, "level", func() {
view(100, 50, wd, func(c *ui.Context) { chat.VoiceMeter(c, "Voice", bad, chat.VoiceMeterOptions{}) })
})
}
}
func TestAttachmentChipSizesAndRemoves(t *testing.T) {
wd := &world{clk: newClock()}
removed := 0
tt := view(500, 200, wd, func(c *ui.Context) {
for _, f := range []struct {
name string
size int64
}{{"map.png", 2_516_582}, {"song.mp3", 12 << 20}, {"a.txt", 812}, {"b.bin", 1024}} {
if chat.AttachmentChip(c, f.name, f.size, chat.AttachmentChipOptions{}).Removed {
removed++
}
}
})
for _, s := range []string{"2.4 MB", "12 MB", "812 B", "1.0 KB"} {
find(t, tt, s)
}
click(t, tt, "REMOVE a.txt")
if removed != 1 {
t.Fatal("× did not remove")
}
}
func TestLinkPreviewOpensItsURL(t *testing.T) {
wd := &world{clk: newClock()}
tt := view(500, 200, wd, func(c *ui.Context) {
chat.LinkPreview(c, "https://example.com/a", "A long title that wraps over two lines here", "example.com", chat.LinkPreviewOptions{})
})
r := find(t, tt, "A long title that wraps over two lines here")
if r.H < 2*44 {
t.Fatalf("a two-line title needs room for the site: %v high", r.H)
}
click(t, tt, "A long title that wraps over two lines here")
tt.Key(0, ui.KeyEnter)
if got := tt.OpenedURLs(); len(got) != 2 || got[0] != "https://example.com/a" {
t.Fatalf("opened %q", got)
}
}
func TestPollMovesYourVote(t *testing.T) {
wd := &world{clk: newClock()}
opts := []chat.PollOption{{Text: "Mage", Votes: 2}, {Text: "Knight", Votes: 1}}
mine, voted := -1, -1
tt := view(400, 200, wd, func(c *ui.Context) {
if r := chat.Poll(c, "Class?", opts, &mine, chat.PollOptions{}); r.Voted >= 0 {
voted = r.Voted
}
})
click(t, tt, "Knight, 1")
if mine != 1 || opts[1].Votes != 2 || voted != 1 {
t.Fatalf("first vote: mine %d, %+v", mine, opts)
}
click(t, tt, "Mage, 2")
if mine != 0 || opts[0].Votes != 3 || opts[1].Votes != 1 {
t.Fatalf("moved vote: mine %d, %+v", mine, opts)
}
voted = -1
click(t, tt, "Mage, 3")
if voted != -1 || opts[0].Votes != 3 {
t.Fatal("voting again for your choice counted twice")
}
find(t, tt, "4 VOTES")
mustPanic(t, "lone option", func() {
one := -1
view(300, 100, wd, func(c *ui.Context) { chat.Poll(c, "Q", opts[:1], &one, chat.PollOptions{}) })
})
}