overlay_test.go

  1package dialog
  2
  3import (
  4	"testing"
  5	"time"
  6
  7	tea "charm.land/bubbletea/v2"
  8	uv "github.com/charmbracelet/ultraviolet"
  9	"github.com/stretchr/testify/require"
 10)
 11
 12// stubDialog is a minimal Dialog for testing Overlay behavior.
 13type stubDialog struct {
 14	id       string
 15	received []tea.Msg
 16}
 17
 18func (s *stubDialog) ID() string { return s.id }
 19func (s *stubDialog) HandleMsg(msg tea.Msg) Action {
 20	s.received = append(s.received, msg)
 21	return nil
 22}
 23func (s *stubDialog) Draw(_ uv.Screen, _ uv.Rectangle) *tea.Cursor { return nil }
 24
 25func keyMsg(r rune) tea.KeyPressMsg {
 26	return tea.KeyPressMsg{Code: r, Text: string(r)}
 27}
 28
 29// TestOverlay_GracePeriodSwallowsKeys verifies that all keystrokes
 30// arriving within the grace period after OpenDialogWithGrace are absorbed
 31// and never forwarded to the dialog.
 32func TestOverlay_GracePeriodSwallowsKeys(t *testing.T) {
 33	t.Parallel()
 34
 35	d := &stubDialog{id: "test"}
 36	o := NewOverlay()
 37	o.OpenDialogWithGrace(d)
 38
 39	for _, r := range []rune{'a', 's', 'd', 'x', 'z'} {
 40		o.Update(keyMsg(r))
 41	}
 42	require.Empty(t, d.received, "no keys should reach the dialog during grace period")
 43}
 44
 45// TestOverlay_GracePeriodArmsAfterQuiet verifies that once input has been
 46// quiet for graceQuietPeriod, subsequent keys are forwarded normally.
 47func TestOverlay_GracePeriodArmsAfterQuiet(t *testing.T) {
 48	t.Parallel()
 49
 50	d := &stubDialog{id: "test"}
 51	o := NewOverlay()
 52	o.OpenDialogWithGrace(d)
 53
 54	// Backdate so both deadlines have elapsed.
 55	o.graceOpenedAt = time.Now().Add(-graceMaxDelay - time.Millisecond)
 56	o.graceLastInputAt = time.Now().Add(-graceQuietPeriod - time.Millisecond)
 57
 58	o.Update(keyMsg('a'))
 59	require.Len(t, d.received, 1, "key after grace period should reach the dialog")
 60}
 61
 62// TestOverlay_GracePeriodBurstExtendsQuietWindow verifies that a sustained
 63// burst of keystrokes keeps resetting the quiet timer, but the fixed
 64// ceiling (graceMaxDelay) eventually arms the dialog regardless.
 65func TestOverlay_GracePeriodBurstExtendsQuietWindow(t *testing.T) {
 66	t.Parallel()
 67
 68	d := &stubDialog{id: "test"}
 69	o := NewOverlay()
 70	o.OpenDialogWithGrace(d)
 71
 72	// Simulate keys arriving every 40ms (within graceQuietPeriod).
 73	// Each one resets the quiet timer, but we stay under graceMaxDelay.
 74	for range 5 {
 75		o.graceLastInputAt = time.Now().Add(-40 * time.Millisecond)
 76		o.Update(keyMsg('a'))
 77	}
 78	require.Empty(t, d.received, "burst within max delay should be absorbed")
 79
 80	// Now exceed the max delay ceiling. Even though lastInputAt is recent,
 81	// the fixed deadline forces arming.
 82	o.graceOpenedAt = time.Now().Add(-graceMaxDelay - time.Millisecond)
 83	o.graceLastInputAt = time.Now() // just typed, but max delay wins
 84	o.Update(keyMsg('b'))
 85	require.Len(t, d.received, 1, "key after max delay should reach dialog even during burst")
 86}
 87
 88// TestOverlay_OpenDialogWithoutGraceHasNoGuard verifies that dialogs
 89// opened via OpenDialog (without grace) receive keys immediately.
 90func TestOverlay_OpenDialogWithoutGraceHasNoGuard(t *testing.T) {
 91	t.Parallel()
 92
 93	d := &stubDialog{id: "test"}
 94	o := NewOverlay()
 95	o.OpenDialog(d)
 96
 97	o.Update(keyMsg('a'))
 98	require.Len(t, d.received, 1, "dialog without grace should receive keys immediately")
 99}
100
101// TestOverlay_GraceClearedOnClose verifies that closing the front dialog
102// clears grace state so a subsequently opened dialog without grace is
103// not affected.
104func TestOverlay_GraceClearedOnClose(t *testing.T) {
105	t.Parallel()
106
107	d1 := &stubDialog{id: "first"}
108	d2 := &stubDialog{id: "second"}
109	o := NewOverlay()
110	o.OpenDialogWithGrace(d1)
111
112	// Close the grace dialog and open a normal one.
113	o.CloseFrontDialog()
114	o.OpenDialog(d2)
115
116	o.Update(keyMsg('a'))
117	require.Len(t, d2.received, 1, "new dialog after grace close should receive keys immediately")
118}
119
120// TestOverlay_NonKeyMessagesPassDuringGrace verifies that non-keypress
121// messages (e.g. mouse, tick) are forwarded even during the grace period.
122func TestOverlay_NonKeyMessagesPassDuringGrace(t *testing.T) {
123	t.Parallel()
124
125	d := &stubDialog{id: "test"}
126	o := NewOverlay()
127	o.OpenDialogWithGrace(d)
128
129	o.Update(tea.MouseWheelMsg{})
130	require.Len(t, d.received, 1, "non-key messages should pass through during grace")
131}