Toasts now render three content rows with word-wrapped bodies. The in-toast "Ctrl-N · N more" hint is replaced by a short "Ctrl-N · dismiss" entry on the status strip that only appears while a notification is live. The box stops flickering while the focused child repaints its TUI: the overlay is stitched onto the per-chunk PTY write under outMu and bracketed by DECSET 2026 so supporting terminals buffer the child's redraw and the box paint into a single frame.
165 lines
4.3 KiB
Go
165 lines
4.3 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestToastStackPushAndOrder(t *testing.T) {
|
|
var s toastStack
|
|
s.push(toastInfo, "one")
|
|
s.push(toastError, "two")
|
|
s.push(toastAttention, "three")
|
|
|
|
snap := s.snapshot()
|
|
if len(snap) != 3 {
|
|
t.Fatalf("snapshot len = %d, want 3", len(snap))
|
|
}
|
|
if snap[0].text != "one" || snap[1].text != "two" || snap[2].text != "three" {
|
|
t.Fatalf("snapshot order wrong: %#v", snap)
|
|
}
|
|
if snap[0].kind != toastInfo || snap[1].kind != toastError || snap[2].kind != toastAttention {
|
|
t.Fatalf("snapshot kinds wrong: %#v", snap)
|
|
}
|
|
// IDs strictly increase.
|
|
if !(snap[0].id < snap[1].id && snap[1].id < snap[2].id) {
|
|
t.Fatalf("ids not increasing: %#v", snap)
|
|
}
|
|
}
|
|
|
|
func TestToastStackCapDropsOldest(t *testing.T) {
|
|
var s toastStack
|
|
for i := 0; i < toastStackCap+3; i++ {
|
|
s.push(toastInfo, "msg")
|
|
}
|
|
snap := s.snapshot()
|
|
if len(snap) != toastStackCap {
|
|
t.Fatalf("len = %d, want %d", len(snap), toastStackCap)
|
|
}
|
|
// The earliest IDs should have been dropped, leaving the highest
|
|
// toastStackCap IDs.
|
|
for i := 1; i < len(snap); i++ {
|
|
if snap[i].id <= snap[i-1].id {
|
|
t.Fatalf("ordering broken after cap: %#v", snap)
|
|
}
|
|
}
|
|
// First retained id should be 4 (1,2,3 dropped; cap=5 leaves 4..8).
|
|
want := uint64(toastStackCap + 3 - toastStackCap + 1)
|
|
if snap[0].id != want {
|
|
t.Fatalf("first retained id = %d, want %d", snap[0].id, want)
|
|
}
|
|
}
|
|
|
|
func TestToastStackDismissTop(t *testing.T) {
|
|
var s toastStack
|
|
if s.dismissTop() {
|
|
t.Fatalf("dismissTop on empty stack returned true")
|
|
}
|
|
s.push(toastInfo, "a")
|
|
s.push(toastError, "b")
|
|
if !s.dismissTop() {
|
|
t.Fatalf("dismissTop returned false with items present")
|
|
}
|
|
snap := s.snapshot()
|
|
if len(snap) != 1 || snap[0].text != "a" {
|
|
t.Fatalf("after dismissTop: %#v", snap)
|
|
}
|
|
if !s.dismissTop() {
|
|
t.Fatalf("dismissTop on last item returned false")
|
|
}
|
|
if s.length() != 0 {
|
|
t.Fatalf("length after final dismiss = %d, want 0", s.length())
|
|
}
|
|
}
|
|
|
|
func TestToastStackClear(t *testing.T) {
|
|
var s toastStack
|
|
if s.clear() {
|
|
t.Fatalf("clear on empty returned true")
|
|
}
|
|
s.push(toastInfo, "a")
|
|
s.push(toastError, "b")
|
|
s.push(toastAttention, "c")
|
|
if !s.clear() {
|
|
t.Fatalf("clear returned false with items present")
|
|
}
|
|
if s.length() != 0 {
|
|
t.Fatalf("length after clear = %d, want 0", s.length())
|
|
}
|
|
if snap := s.snapshot(); snap != nil {
|
|
t.Fatalf("snapshot after clear = %#v, want nil", snap)
|
|
}
|
|
}
|
|
|
|
func TestToastStackSnapshotIsCopy(t *testing.T) {
|
|
var s toastStack
|
|
s.push(toastInfo, "a")
|
|
snap := s.snapshot()
|
|
snap[0].text = "mutated"
|
|
again := s.snapshot()
|
|
if again[0].text != "a" {
|
|
t.Fatalf("snapshot is not an independent copy: %#v", again)
|
|
}
|
|
}
|
|
|
|
func TestWrapToastBodyFixedHeight(t *testing.T) {
|
|
got := wrapToastBody("short", 20)
|
|
if len(got) != toastContentRows {
|
|
t.Fatalf("len = %d, want %d", len(got), toastContentRows)
|
|
}
|
|
if got[0] != "short" {
|
|
t.Fatalf("line 0 = %q, want \"short\"", got[0])
|
|
}
|
|
if got[1] != "" || got[2] != "" {
|
|
t.Fatalf("trailing pads not empty: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestWrapToastBodyWrapsOnWordBoundary(t *testing.T) {
|
|
got := wrapToastBody("the quick brown fox jumps over", 10)
|
|
// Expect greedy fill: "the quick" (9), "brown fox" (9), "jumps over" (10).
|
|
want := []string{"the quick", "brown fox", "jumps over"}
|
|
for i, w := range want {
|
|
if got[i] != w {
|
|
t.Fatalf("line %d = %q, want %q (full=%#v)", i, got[i], w, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWrapToastBodyEllipsizesOverflow(t *testing.T) {
|
|
got := wrapToastBody("alpha beta gamma delta epsilon zeta eta theta", 6)
|
|
if len(got) != toastContentRows {
|
|
t.Fatalf("len = %d, want %d", len(got), toastContentRows)
|
|
}
|
|
last := got[toastContentRows-1]
|
|
if !strings.HasSuffix(last, "…") {
|
|
t.Fatalf("overflow should ellipsize last line, got %q (full=%#v)", last, got)
|
|
}
|
|
if visibleLen(last) > 6 {
|
|
t.Fatalf("last line %q exceeds width 6", last)
|
|
}
|
|
}
|
|
|
|
func TestWrapToastBodyBreaksOverlongWord(t *testing.T) {
|
|
got := wrapToastBody("supercalifragilistic", 6)
|
|
if got[0] != "superc" {
|
|
t.Fatalf("line 0 = %q, want \"superc\"", got[0])
|
|
}
|
|
if got[1] != "alifra" {
|
|
t.Fatalf("line 1 = %q, want \"alifra\"", got[1])
|
|
}
|
|
// Third line should hold the rest (possibly ellipsized).
|
|
if got[2] == "" {
|
|
t.Fatalf("line 2 unexpectedly empty: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestWrapToastBodyEmptyInput(t *testing.T) {
|
|
got := wrapToastBody("", 20)
|
|
for i, l := range got {
|
|
if l != "" {
|
|
t.Fatalf("line %d = %q, want \"\"", i, l)
|
|
}
|
|
}
|
|
}
|