1package daemon
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/require"
7
8 renderer "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"
10 scopy "github.com/bnema/vev/internal/usecase/copy"
11 promptui "github.com/bnema/vev/internal/usecase/prompt"
12 "github.com/bnema/vev/internal/usecase/visualsearch"
13)
14
15func TestOverlayRuntimeActive(t *testing.T) {
16 ac := &attachedClient{}
17 ac.initOverlays()
18 require.False(t, ac.overlays.Active())
19
20 ac.overlays.promptMu.Lock()
21 ac.overlays.prompt = promptui.New(" Test ", "")
22 ac.overlays.promptMu.Unlock()
23 require.True(t, ac.overlays.Active())
24}
25
26func TestOverlayRuntimeHandleInputPrecedence(t *testing.T) {
27 cases := []struct {
28 name string
29 setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient)
30 input []byte
31 check func(t *testing.T, ac *attachedClient)
32 }{
33 {
34 name: "prompt before palette picker copy",
35 setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
36 t.Helper()
37 d.enterCopyMode(sess, ac)
38 d.enterPicker(sess, ac)
39 d.enterPalette(sess, ac)
40 d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil })
41 },
42 input: []byte("x"),
43 check: func(t *testing.T, ac *attachedClient) {
44 t.Helper()
45 require.Equal(t, "x", ac.overlays.prompt.Value())
46 require.Equal(t, "", ac.overlays.palette.Query())
47 require.True(t, ac.overlays.pickerActive())
48 require.True(t, ac.overlays.copyActive())
49 },
50 },
51 {
52 name: "palette before picker copy",
53 setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
54 t.Helper()
55 d.enterCopyMode(sess, ac)
56 d.enterPicker(sess, ac)
57 d.enterPalette(sess, ac)
58 },
59 input: []byte("x"),
60 check: func(t *testing.T, ac *attachedClient) {
61 t.Helper()
62 require.Equal(t, "x", ac.overlays.palette.Query())
63 require.True(t, ac.overlays.pickerActive())
64 require.True(t, ac.overlays.copyActive())
65 },
66 },
67 {
68 name: "picker before copy",
69 setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
70 t.Helper()
71 d.enterCopyMode(sess, ac)
72 d.enterPicker(sess, ac)
73 },
74 input: []byte("j"),
75 check: func(t *testing.T, ac *attachedClient) {
76 t.Helper()
77 target, ok := ac.overlays.picker.Selected()
78 require.True(t, ok)
79 require.Equal(t, 1, target.TabIndex)
80 require.True(t, ac.overlays.copyActive())
81 },
82 },
83 {
84 name: "copy",
85 setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
86 t.Helper()
87 d.enterCopyMode(sess, ac)
88 },
89 input: []byte("q"),
90 check: func(t *testing.T, ac *attachedClient) {
91 t.Helper()
92 require.False(t, ac.overlays.copyActive())
93 },
94 },
95 }
96
97 for _, tc := range cases {
98 t.Run(tc.name, func(t *testing.T) {
99 p1, releasePTY1 := newBlockingPTY(t)
100 p2, releasePTY2 := newBlockingPTY(t)
101 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
102 defer releasePTY1()
103 defer releasePTY2()
104 tc.setup(t, d, sess, ac)
105
106 require.True(t, ac.overlays.HandleInput(d, tc.input))
107
108 tc.check(t, ac)
109 })
110 }
111}
112
113func TestOverlayRuntimeSnapshotDoesNotAliasCopySearchState(t *testing.T) {
114 ac := &attachedClient{}
115 ac.initOverlays()
116 snap := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("alpha"), testRow("beta alpha")}, 16, 2)
117 mode := scopy.NewMode(scopy.NewDocument(snap, domain.DefaultWordSeparators))
118 require.True(t, mode.Search("alpha"))
119 search := visualsearch.New(snap)
120 for _, r := range "alpha" {
121 search.Insert(r)
122 }
123
124 ac.overlays.copyMu.Lock()
125 ac.overlays.copyMode = mode
126 ac.overlays.copySearch = search
127 ac.overlays.copyMu.Unlock()
128
129 renderSnap := ac.overlays.SnapshotForRender()
130 defer renderSnap.Unlock()
131
132 ac.overlays.copyMu.Lock()
133 ac.overlays.copyMode.Searches[0].Row = 99
134 ac.overlays.copySearch.Insert('z')
135 ac.overlays.copyMu.Unlock()
136
137 require.Equal(t, 0, renderSnap.copyMode.Searches[0].Row)
138 require.Equal(t, "alpha", renderSnap.copySearchModel.Query())
139}
140
141func TestOverlayRuntimeHandleInputInactive(t *testing.T) {
142 ac := &attachedClient{}
143 ac.initOverlays()
144
145 require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x")))
146}
147
148func TestOverlayRuntimeSnapshotCarriesCopyModeDocumentAndClearReleasesIt(t *testing.T) {
149 ac := &attachedClient{}
150 ac.initOverlays()
151 p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2})
152 document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2), domain.DefaultWordSeparators)
153
154 ac.overlays.copyMu.Lock()
155 ac.overlays.copyMode = scopy.NewMode(document)
156 ac.overlays.copyPane = p
157 ac.overlays.copyDocument = document
158 ac.overlays.copyMu.Unlock()
159
160 renderSnap := ac.overlays.SnapshotForRender()
161 require.Same(t, p, renderSnap.copyPane)
162 require.NotNil(t, renderSnap.copyMode)
163 require.Same(t, document, renderSnap.copyMode.Document())
164 renderSnap.Unlock()
165
166 ac.overlays.copyMu.Lock()
167 ac.overlays.clearCopyModeLocked()
168 require.Nil(t, ac.overlays.copyPane)
169 require.Nil(t, ac.overlays.copyMode)
170 require.Nil(t, ac.overlays.copyDocument)
171 ac.overlays.copyMu.Unlock()
172}
173
174func TestOverlayRuntimePointerEpochInvalidatesTransferAndPaneClose(t *testing.T) {
175 ac := &attachedClient{}
176 ac.initOverlays()
177 p := newPane("pane", nil, domain.Size{Cols: 4, Rows: 1})
178 document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 1), domain.DefaultWordSeparators)
179
180 ac.overlays.copyMu.Lock()
181 ac.overlays.beginCopyPointerLocked(copyPointerState{pane: p, document: document, press: scopy.Pos{}})
182 staleEpoch := ac.overlays.copyPointer.epoch
183 ac.overlays.clearCopyPointerForTransferLocked()
184 ac.overlays.invalidateCopyPointerLocked(true) // release/close while publish revalidates
185 require.NotEqual(t, staleEpoch, ac.overlays.copyPointerEpoch)
186 require.False(t, ac.overlays.copyPointer.valid)
187 ac.overlays.copyCandidate = scopy.NewMode(document)
188 ac.overlays.copyDocument = document
189 ac.overlays.copyPane = p
190 ac.overlays.copyMu.Unlock()
191
192 require.True(t, ac.overlays.clearCopyModeForPane(p))
193 ac.overlays.copyMu.Lock()
194 require.False(t, ac.overlays.copyPointer.valid)
195 ac.overlays.copyMu.Unlock()
196}
197
198func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
199 ac := &attachedClient{}
200 ac.initOverlays()
201 history := newTestHistory(1)
202 appendHistoryRow(t, history, testRow("before"))
203 document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1), nil, nil), domain.DefaultWordSeparators)
204
205 ac.overlays.copyMu.Lock()
206 ac.overlays.copyMode = scopy.NewMode(document)
207 ac.overlays.copyDocument = document
208 ac.overlays.copyMu.Unlock()
209
210 appendHistoryRow(t, history, testRow("after"))
211 renderSnap := ac.overlays.SnapshotForRender()
212 defer renderSnap.Unlock()
213 require.NotNil(t, renderSnap.copyMode)
214 require.Same(t, document, renderSnap.copyMode.Document())
215 require.Equal(t, "before", rowText(renderSnap.copyMode.Document().Snapshot().Row(0)))
216}