@@ -0,0 +1,131 @@
1+package dialog
2+
3+import (
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.
13+type stubDialog struct {
14+ id string
15+ received []tea.Msg
16+}
17+
18+func (s *stubDialog) ID() string { return s.id }
19+func (s *stubDialog) HandleMsg(msg tea.Msg) Action {
20+ s.received = append(s.received, msg)
21+ return nil
22+}
23+func (s *stubDialog) Draw(_ uv.Screen, _ uv.Rectangle) *tea.Cursor { return nil }
24+
25+func 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.
32+func 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.
47+func 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.
65+func 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.
90+func 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.
104+func 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.
122+func 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+}