1package daemon
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/require"
7
8 vt "github.com/bnema/vev-vt"
9 renderer "github.com/bnema/vev-vt/ansi"
10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"
12)
13
14func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {
15 d := newTestDaemon(t, nil, stubClock{})
16 sess := addControlSession(d, "work", "tab-1", "pane-1")
17 sess.ephemeral = false
18 sess.incarnation = remoteLifecycleForTest()
19 pane := sess.tabs[0].focusedPane()
20 pane.mu.Lock()
21 pane.screen = vt.NewScreen(2, 3)
22 pane.screen.Frame.Set(0, 0, renderer.Cell{Rune: 'a'})
23 pane.screen.Frame.Set(0, 1, renderer.Cell{Rune: 'b'})
24 pane.screen.Frame.Set(0, 2, renderer.Cell{Rune: 'c'})
25 pane.mu.Unlock()
26
27 target := domain.RemoteSessionTarget{
28 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
29 SessionName: "work", LiveTabID: "tab-1",
30 }
31 preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
32 Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 2,
33 })
34 require.NoError(t, err)
35 require.Equal(t, []rune{'b', 'c'}, []rune{preview.Cells[0].Rune, preview.Cells[1].Rune})
36}
37
38func TestCaptureRemotePreviewCompactsBlankTail(t *testing.T) {
39 type screenCell struct {
40 x, y int
41 cell renderer.Cell
42 }
43 type previewCell struct {
44 index int
45 rune rune
46 style *renderer.Style
47 }
48 styledBlank := renderer.DefaultStyle()
49 styledBlank.Background = 4
50 tests := []struct {
51 name string
52 screenWidth, screenHeight int
53 cells []screenCell
54 width, height uint16
55 wantHeight uint16
56 wantCells []previewCell
57 }{
58 {
59 name: "keeps short screen content visible",
60 screenWidth: 2,
61 screenHeight: 4,
62 cells: []screenCell{{x: 0, y: 0, cell: renderer.Cell{Rune: 'v'}}},
63 width: 2,
64 height: 2,
65 wantHeight: 1,
66 wantCells: []previewCell{{index: 0, rune: 'v'}, {index: 1, rune: ' '}},
67 },
68 {
69 name: "ignores boundary split wide rune",
70 screenWidth: 3,
71 screenHeight: 3,
72 cells: []screenCell{
73 {x: 0, y: 1, cell: renderer.Cell{Rune: 'a'}},
74 {x: 1, y: 2, cell: renderer.Cell{Rune: '界'}},
75 {x: 2, y: 2, cell: renderer.Cell{Continuation: true}},
76 },
77 width: 2,
78 height: 2,
79 wantHeight: 1,
80 wantCells: []previewCell{{index: 0, rune: 'a'}, {index: 1, rune: ' '}},
81 },
82 {
83 name: "preserves styled blank tail",
84 screenWidth: 2,
85 screenHeight: 4,
86 cells: []screenCell{
87 {x: 0, y: 2, cell: renderer.Cell{Rune: 'a'}},
88 {x: 0, y: 3, cell: renderer.Cell{Rune: ' ', Style: styledBlank}},
89 },
90 width: 2,
91 height: 2,
92 wantHeight: 2,
93 wantCells: []previewCell{{index: 0, rune: 'a'}, {index: 2, rune: ' ', style: &styledBlank}},
94 },
95 }
96
97 for _, test := range tests {
98 t.Run(test.name, func(t *testing.T) {
99 d := newTestDaemon(t, nil, stubClock{})
100 sess := addControlSession(d, "work", "tab-1", "pane-1")
101 sess.ephemeral = false
102 sess.incarnation = remoteLifecycleForTest()
103 pane := sess.tabs[0].focusedPane()
104 pane.mu.Lock()
105 pane.screen = vt.NewScreen(test.screenWidth, test.screenHeight)
106 for _, cell := range test.cells {
107 pane.screen.Frame.Set(cell.x, cell.y, cell.cell)
108 }
109 pane.mu.Unlock()
110
111 target := domain.RemoteSessionTarget{
112 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
113 SessionName: "work", LiveTabID: "tab-1",
114 }
115 preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
116 Version: ports.RemotePreviewSchemaVersion, Target: target, Width: test.width, Height: test.height,
117 })
118 require.NoError(t, err)
119 require.Equal(t, test.wantHeight, preview.Height)
120 for _, want := range test.wantCells {
121 require.Equal(t, want.rune, preview.Cells[want.index].Rune)
122 if want.style != nil {
123 require.True(t, preview.Cells[want.index].Style.Equal(*want.style))
124 }
125 }
126 })
127 }
128}
129
130func TestStaticRemotePickerPreviewUsesOnlyItsTextRow(t *testing.T) {
131 preview := staticRemotePickerPreview(8, 4, "loading")
132
133 require.Equal(t, 8, preview.Width)
134 require.Equal(t, 1, preview.Height)
135 require.Equal(t, []rune{'l', 'o', 'a', 'd', 'i', 'n', 'g', ' '}, []rune{
136 preview.Rows[0][0].Rune, preview.Rows[0][1].Rune, preview.Rows[0][2].Rune, preview.Rows[0][3].Rune,
137 preview.Rows[0][4].Rune, preview.Rows[0][5].Rune, preview.Rows[0][6].Rune, preview.Rows[0][7].Rune,
138 })
139}
140
141func TestCaptureRemotePreviewDoesNotSplitWideRuneAtCropBoundary(t *testing.T) {
142 d := newTestDaemon(t, nil, stubClock{})
143 sess := addControlSession(d, "work", "tab-1", "pane-1")
144 sess.ephemeral = false
145 sess.incarnation = remoteLifecycleForTest()
146 pane := sess.tabs[0].focusedPane()
147 pane.mu.Lock()
148 pane.screen = vt.NewScreen(3, 1)
149 style := renderer.DefaultStyle()
150 style.Bold = true
151 pane.screen.Frame.Set(1, 0, renderer.Cell{Rune: '界', Style: style})
152 pane.screen.Frame.Set(2, 0, renderer.Cell{Continuation: true, Style: style})
153 pane.mu.Unlock()
154
155 target := domain.RemoteSessionTarget{
156 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
157 SessionName: "work", LiveTabID: "tab-1",
158 }
159 preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{
160 Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 2, Height: 1,
161 })
162 require.NoError(t, err)
163 require.Equal(t, uint16(2), preview.Width)
164 require.Equal(t, rune(' '), preview.Cells[1].Rune, "a crop cannot expose a dangling wide rune")
165 require.False(t, preview.Cells[1].Continuation)
166 require.True(t, preview.Cells[1].Style.Equal(style), "boundary replacement preserves the cell style")
167 require.NoError(t, ports.ValidateRemotePreview(preview))
168}