-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathcursed_renderer_test.go
More file actions
203 lines (174 loc) Β· 5.36 KB
/
Copy pathcursed_renderer_test.go
File metadata and controls
203 lines (174 loc) Β· 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package tea
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/charmbracelet/x/ansi"
)
type mouseRaceModel struct {
i int
}
func (m *mouseRaceModel) Init() Cmd { return nil }
func (m *mouseRaceModel) Update(msg Msg) (Model, Cmd) {
switch msg.(type) {
case MouseClickMsg, MouseMotionMsg, MouseWheelMsg:
m.i++
}
return m, nil
}
func (m *mouseRaceModel) View() View {
return View{
Content: fmt.Sprintf("tick-%d\n", m.i),
MouseMode: MouseModeCellMotion,
}
}
// Fixes: https://github.com/charmbracelet/bubbletea/issues/1690
func TestCursedRenderer_mouseVsFlush(t *testing.T) {
t.Parallel()
pr, pw := io.Pipe()
defer func() { _ = pw.Close() }()
m := &mouseRaceModel{}
p := NewProgram(
m,
WithContext(t.Context()),
WithInput(pr),
WithOutput(io.Discard),
WithEnvironment([]string{
"TERM=xterm-256color",
"TERM_PROGRAM=Apple_Terminal",
}),
WithoutSignals(),
WithWindowSize(80, 24),
)
runDone := make(chan struct{})
go func() {
defer close(runDone)
_, _ = p.Run()
}()
time.Sleep(150 * time.Millisecond)
const iterations = 100
for i := range iterations {
switch i % 4 {
case 0:
p.Send(MouseClickMsg{X: i % 80, Y: i % 24, Button: MouseLeft})
case 1:
p.Send(MouseMotionMsg{X: i % 80, Y: i % 24})
case 2:
p.Send(MouseWheelMsg{X: 0, Y: 0, Button: MouseWheelUp})
default:
p.Send(MouseReleaseMsg{X: i % 80, Y: i % 24, Button: MouseLeft})
}
}
p.Quit()
select {
case <-runDone:
case <-time.After(5 * time.Second):
t.Fatal("program did not exit after Quit")
}
}
func assertInOrder(t *testing.T, got string, wants ...string) {
t.Helper()
rest := got
for _, want := range wants {
idx := strings.Index(rest, want)
if idx < 0 {
t.Fatalf("expected %q to appear after the previous sequences in %q", want, got)
}
rest = rest[idx+len(want):]
}
}
func TestCursedRenderer_restoresKittyKeyboardStack(t *testing.T) {
t.Parallel()
var out bytes.Buffer
r := newCursedRenderer(&out, []string{"TERM=xterm-256color"}, 80, 24)
r.start()
view := NewView("hello")
view.KeyboardEnhancements.ReportEventTypes = true
pushMain := ansi.PushKittyKeyboard(keyboardEnhancementsFlags(view.KeyboardEnhancements))
pop := ansi.PopKittyKeyboard(1)
render := func(v View) {
t.Helper()
r.render(v)
if err := r.flush(false); err != nil {
t.Fatal(err)
}
}
render(view)
// Stop the renderer (as on suspend or ExecProcess) and start it again:
// close pops the stack entry, start pushes it back.
if err := r.close(); err != nil {
t.Fatal(err)
}
r.start()
// Enter and leave the alt screen. The terminal keeps a separate Kitty
// keyboard stack per screen, so each screen gets its own push and pop.
view.AltScreen = true
render(view)
view.AltScreen = false
render(view)
if err := r.close(); err != nil {
t.Fatal(err)
}
got := out.String()
// The flags are pushed once per screen activation: the first flush,
// the flush after the renderer was restarted, and on each screen
// switch. start() itself does not write to [out].
if n := strings.Count(got, pushMain); n != 4 {
t.Fatalf("expected kitty keyboard protocol to be pushed 4 times with %q (%d times), got %q", pushMain, n, got)
}
// One pop per stop/start cycle and per screen switch: closing pops the
// current screen's entry, and switching screens pops the entry of the
// screen being left.
if n := strings.Count(got, pop); n != 4 {
t.Fatalf("expected kitty keyboard protocol to be popped 4 times with %q (%d times), got %q", pop, n, got)
}
// Every pop must come after a push: the stack is balanced when pushes
// and pops alternate. The resumed flush pushes twice in a row (once in
// start(), once in flush()), and both entries are popped afterwards.
assertInOrder(t, got,
pushMain, pop, // close pops the entry pushed by the first flush
pop, // entering the alt screen pops the resumed entry
pushMain, pop, // leaving the alt screen
pushMain, pop, // the resumed main screen entry and the final close
)
if strings.Contains(got, ansi.KittyKeyboard(0, 1)) {
t.Fatalf("expected kitty keyboard protocol not to be reset in-place with %q, got %q", ansi.KittyKeyboard(0, 1), got)
}
}
func TestCursedRenderer_updatesKittyKeyboardFlagsInPlace(t *testing.T) {
t.Parallel()
var out bytes.Buffer
r := newCursedRenderer(&out, []string{"TERM=xterm-256color"}, 80, 24)
render := func(v View) {
t.Helper()
r.render(v)
if err := r.flush(false); err != nil {
t.Fatal(err)
}
}
view := NewView("hello")
render(view)
// Changing the enhancement flags without switching screens updates the
// current stack entry in place instead of pushing a new one.
changed := view
changed.KeyboardEnhancements.ReportEventTypes = true
render(changed)
wantUpdate := ansi.KittyKeyboard(keyboardEnhancementsFlags(changed.KeyboardEnhancements), 1)
got := out.String()
if !strings.Contains(got, wantUpdate) {
t.Fatalf("expected kitty keyboard flags to be updated in place with %q, got %q", wantUpdate, got)
}
assertInOrder(t, got,
ansi.PushKittyKeyboard(keyboardEnhancementsFlags(view.KeyboardEnhancements)),
wantUpdate,
)
if strings.Contains(got, ansi.PopKittyKeyboard(1)) {
t.Fatalf("expected kitty keyboard protocol not to be popped with %q, got %q", ansi.PopKittyKeyboard(1), got)
}
if n := strings.Count(got, ansi.PushKittyKeyboard(0)); n > 1 {
t.Fatalf("expected kitty keyboard protocol to be pushed once, got %d pushes in %q", n, got)
}
}