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}