1package tui
2
3import (
4 "strings"
5 "testing"
6)
7
8func TestFloatingPaneComposeCentersAndDimsLiveBackground(t *testing.T) {
9 var pane FloatingPane
10 frame, cursorRow, cursorCol := pane.Compose(Dark, "settings", "settings", []string{"live transcript", "draft input"}, []string{"settings body"}, 100, 30, -1, 0)
11
12 if frame.Rect.Drawer {
13 t.Fatal("100-column terminal unexpectedly used drawer layout")
14 }
15 if want := (100 - frame.Rect.Width) / 2; frame.Rect.X != want {
16 t.Fatalf("pane x = %d, want %d", frame.Rect.X, want)
17 }
18 if !strings.Contains(frame.Background[len(frame.Background)-2], Dim("live transcript")) {
19 t.Fatalf("live background was not dimmed: %q", frame.Background)
20 }
21 if got := stripANSI(frame.Lines[0]); !strings.HasPrefix(got, "╭") {
22 t.Fatalf("pane is missing top border: %q", got)
23 } else if title := strings.Index(got, "settings"); title < 0 {
24 t.Fatalf("pane title is missing from its border: %q", got)
25 } else {
26 left := strings.Count(got[:title], "─")
27 right := strings.Count(got[title+len("settings"):], "─")
28 if left-right > 1 || right-left > 1 {
29 t.Fatalf("pane title is not centered in its border: %q", got)
30 }
31 }
32 if cursorRow != -1 || cursorCol != 0 {
33 t.Fatalf("cursor = (%d, %d), want hidden", cursorRow, cursorCol)
34 }
35}
36
37func TestFloatingPaneUsesBottomDrawerBelowEightyColumns(t *testing.T) {
38 var pane FloatingPane
39 frame, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"settings body"}, 79, 24, -1, 0)
40
41 if !frame.Rect.Drawer {
42 t.Fatal("79-column terminal did not use drawer layout")
43 }
44 if frame.Rect.X != 0 || frame.Rect.Width != 79 || frame.Rect.Y+frame.Rect.Height != 24 {
45 t.Fatalf("drawer geometry = %+v, want full-width bottom drawer", frame.Rect)
46 }
47 if got := stripANSI(frame.Lines[0]); !strings.Contains(got, "settings") {
48 t.Fatalf("drawer title = %q, want centered settings title", got)
49 }
50}
51
52func TestFloatingPaneRecomputesGeometryOnResize(t *testing.T) {
53 var pane FloatingPane
54 wide, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"setting"}, 100, 30, -1, 0)
55 narrow, _, _ := pane.Compose(Dark, "settings", "settings", nil, []string{"setting"}, 79, 24, -1, 0)
56
57 if wide.Rect.Drawer {
58 t.Fatal("wide terminal unexpectedly used a drawer")
59 }
60 if !narrow.Rect.Drawer || narrow.Rect.Width != 79 || narrow.Rect.Y+narrow.Rect.Height != 24 {
61 t.Fatalf("resize did not recompute a bottom drawer: %+v", narrow.Rect)
62 }
63}
64
65func TestFloatingPaneClampsCursorToContentAndHandlesTinyTerminals(t *testing.T) {
66 var pane FloatingPane
67 frame, _, col := pane.Compose(Dark, "search", "search", nil, []string{"query"}, 100, 20, 0, 1000)
68 if want := frame.Rect.contentX() + frame.Rect.ContentWidth() - 1; col != want {
69 t.Fatalf("cursor col = %d, want content edge %d", col, want)
70 }
71
72 tiny, row, tinyCol := pane.Compose(Dark, "search", "search", nil, []string{"x"}, 2, 2, 0, 10)
73 if !tiny.Rect.Borderless {
74 t.Fatalf("tiny terminal rect = %+v, want borderless", tiny.Rect)
75 }
76 if row != 0 || tinyCol != 1 {
77 t.Fatalf("tiny cursor = (%d, %d), want (0, 1)", row, tinyCol)
78 }
79}
80
81func TestFloatingPaneKeepsFocusedContentVisibleAndResetsForOtherView(t *testing.T) {
82 content := make([]string, 30)
83 for row := range content {
84 content[row] = "row " + itoa(row)
85 }
86 var pane FloatingPane
87 frame, cursorRow, _ := pane.Compose(Dark, "settings", "settings", nil, content, 100, 12, 25, 0)
88 if cursorRow < frame.Rect.Y+1 || cursorRow >= frame.Rect.Y+frame.Rect.Height-1 {
89 t.Fatalf("focused cursor row %d is outside pane %+v", cursorRow, frame.Rect)
90 }
91 if !strings.Contains(stripANSI(strings.Join(frame.Lines, "\n")), "row 25") {
92 t.Fatal("focused row was not retained in the pane viewport")
93 }
94
95 other, _, _ := pane.Compose(Dark, "subagents", "subagents", nil, content, 100, 12, -1, 0)
96 if strings.Contains(stripANSI(strings.Join(other.Lines, "\n")), "row 25") {
97 t.Fatal("new view inherited the previous view's scroll position")
98 }
99}
100
101func TestRendererDrawFloatingPaintsBackgroundThenPaneAtAbsolutePosition(t *testing.T) {
102 var out strings.Builder
103 r := NewRenderer(&out)
104 r.Resize(100, 20)
105 var pane FloatingPane
106 frame, _, _ := pane.Compose(Dark, "settings", "settings", []string{"background"}, []string{"foreground"}, 100, 20, -1, 0)
107 r.DrawFloating(frame, -1, 0)
108 got := out.String()
109 if !strings.Contains(got, Dim("background")) || !strings.Contains(got, "foreground") {
110 t.Fatalf("floating render omitted a layer: %q", got)
111 }
112 if !strings.Contains(got, MoveTo(frame.Rect.Y+1, frame.Rect.X+1)) {
113 t.Fatalf("floating pane was not painted at its geometry: %q", got)
114 }
115
116 out.Reset()
117 r.Draw(frame.Background, -1, 0)
118 if !strings.Contains(out.String(), MoveTo(1, 1)) {
119 t.Fatalf("normal frame did not repaint after a floating pane: %q", out.String())
120 }
121}
122
123func TestRendererDrawFloatingPreservesThemeBackgroundAndImageInvalidation(t *testing.T) {
124 var out strings.Builder
125 r := NewRenderer(&out)
126 background := Color256(237)
127 th := Dark
128 th.Background = &background
129 r.SetTheme(th)
130 r.Resize(20, 4)
131
132 pane := FloatingPaneFrame{
133 Background: make([]string, 4),
134 Lines: []string{"\x1b_Ga=T,f=100;image\x1b\\"},
135 Rect: FloatingPaneRect{X: 2, Y: 1, Width: 10, Height: 1, Borderless: true},
136 }
137 r.DrawFloating(pane, -1, 0)
138 if !strings.Contains(out.String(), th.BackgroundStyle()) {
139 t.Fatalf("floating pane omitted theme background: %q", out.String())
140 }
141 if !r.prevHadImage || !r.prevHadKittyImage {
142 t.Fatalf("floating image state = image:%t kitty:%t, want both true", r.prevHadImage, r.prevHadKittyImage)
143 }
144
145 out.Reset()
146 r.Draw([]string{""}, -1, 0)
147 if !strings.Contains(out.String(), SeqDeleteKittyImages) {
148 t.Fatalf("stale kitty image was not deleted: %q", out.String())
149 }
150}