package app import ( "testing" "time" ) func TestMarqueeStepFits(t *testing.T) { var m marqueeState now := time.Unix(0, 0) off, animating, _ := m.step("a", 5, 10, now) if animating { t.Fatalf("expected no animation when name fits in budget") } if off != 0 { t.Fatalf("expected offset 0, got %d", off) } } func TestMarqueePhaseProgression(t *testing.T) { var m marqueeState // name 10 runes, budget 5 → maxOffset = 5. const nameLen, budget = 10, 5 t0 := time.Unix(0, 0) // At t0: phaseHoldStart, offset 0, animating. off, anim, wake := m.step("row", nameLen, budget, t0) if off != 0 || !anim || wake != marqueeHoldStart { t.Fatalf("t0: off=%d anim=%v wake=%v", off, anim, wake) } // Just before hold expires: still offset 0. off, anim, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart-time.Millisecond)) if off != 0 || !anim { t.Fatalf("pre-expiry hold: off=%d anim=%v", off, anim) } // At hold expiry + 1 step: should have transitioned to scroll, offset 1. off, anim, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart+marqueeStep)) if !anim || off != 1 { t.Fatalf("first scroll step: off=%d anim=%v", off, anim) } // Mid-scroll: offset == 3. off, _, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart+3*marqueeStep)) if off != 3 { t.Fatalf("mid scroll: off=%d", off) } // Tail reached: offset == maxOffset == 5. off, _, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart+5*marqueeStep+time.Millisecond)) if off != 5 { t.Fatalf("tail: off=%d", off) } // Hold-end window still pegged at maxOffset. off, _, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart+5*marqueeStep+marqueeHoldEnd/2)) if off != 5 { t.Fatalf("hold-end mid: off=%d", off) } // After hold-end: snap back to offset 0. off, _, _ = m.step("row", nameLen, budget, t0.Add(marqueeHoldStart+5*marqueeStep+marqueeHoldEnd+time.Millisecond)) if off != 0 { t.Fatalf("snap back: off=%d", off) } } func TestMarqueeIDChangeResets(t *testing.T) { var m marqueeState t0 := time.Unix(0, 0) _, _, _ = m.step("a", 10, 5, t0) // Advance well into scroll for row "a". _, _, _ = m.step("a", 10, 5, t0.Add(marqueeHoldStart+3*marqueeStep)) // Now focus moves to "b": offset must reset to 0 and phase to hold-start. off, anim, wake := m.step("b", 10, 5, t0.Add(marqueeHoldStart+3*marqueeStep)) if off != 0 || !anim || wake != marqueeHoldStart { t.Fatalf("id reset: off=%d anim=%v wake=%v", off, anim, wake) } } func TestMarqueeActive(t *testing.T) { var m marqueeState if m.active() { t.Fatalf("fresh marquee should not be active") } _, _, _ = m.step("row", 10, 5, time.Unix(0, 0)) if !m.active() { t.Fatalf("expected active after overflow step") } _, _, _ = m.step("row", 4, 5, time.Unix(0, 0)) if m.active() { t.Fatalf("should not be active when name fits") } } func TestMarqueeReset(t *testing.T) { var m marqueeState _, _, _ = m.step("row", 10, 5, time.Unix(0, 0)) m.reset() if m.active() { t.Fatalf("expected inactive after reset") } // After reset, stepping the same id starts fresh. off, _, wake := m.step("row", 10, 5, time.Unix(5, 0)) if off != 0 || wake != marqueeHoldStart { t.Fatalf("post-reset start: off=%d wake=%v", off, wake) } } func TestFitName(t *testing.T) { cases := []struct { name, in string budget int want string }{ {"fits", "abc", 5, "abc"}, {"exact", "abcde", 5, "abcde"}, {"truncate", "abcdef", 5, "abcd…"}, {"budget1", "abcdef", 1, "…"}, {"budget0", "abc", 0, ""}, {"unicode", "αβγδεζη", 4, "αβγ…"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := fitName(c.in, c.budget) if got != c.want { t.Fatalf("fitName(%q, %d) = %q want %q", c.in, c.budget, got, c.want) } }) } } func TestMarqueeWindow(t *testing.T) { got := marqueeWindow("abcdefgh", 4, 2) if got != "cdef" { t.Fatalf("window = %q", got) } // Clamp end-of-string overflow. got = marqueeWindow("abcdef", 4, 10) if got != "cdef" { t.Fatalf("clamped window = %q", got) } } func TestClampVisible(t *testing.T) { // Plain string longer than width. if got := clampVisible("abcdef", 3); visibleLen(got) != 3 { t.Fatalf("plain clamp visible = %d (%q)", visibleLen(got), got) } // Already-fitting string is unchanged. if got := clampVisible("abc", 5); got != "abc" { t.Fatalf("unchanged = %q", got) } // SGR-wrapped string: visible portion must be <= width. in := "\x1b[1mhello\x1b[0m world" got := clampVisible(in, 5) if visibleLen(got) != 5 { t.Fatalf("sgr clamp visible = %d (%q)", visibleLen(got), got) } }