| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package ui |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // framedWindow returns a window on a desktop — which is what gives it a |
| 9 | // working maximise box — with the desktop filling the given area. |
| 10 | func framedWindow(t *testing.T, area, bounds Rect) (*Desktop, *Window) { |
| 11 | t.Helper() |
| 12 | |
| 13 | d := NewDesktop() |
| 14 | d.SetBounds(area) |
| 15 | |
| 16 | w := NewWindow("main.go", nil) |
| 17 | d.Add(w) |
| 18 | w.SetBounds(bounds) |
| 19 | return d, w |
| 20 | } |
| 21 | |
| 22 | // titleBar draws a window onto a simulated screen and returns its top row. |
| 23 | func titleBar(t *testing.T, w *Window, width, height int) string { |
| 24 | t.Helper() |
| 25 | |
| 26 | screen, root := newTestScreen(t, width, height) |
| 27 | w.SetActive(true) |
| 28 | w.Draw(root, testTheme(t)) |
| 29 | screen.Show() |
| 30 | |
| 31 | return screenLines(t, screen)[0] |
| 32 | } |
| 33 | |
| 34 | func TestTheCloseBoxIsAnX(t *testing.T) { |
| 35 | // It closes the window, so it says so. The block character it used to be |
| 36 | // now means "maximise" on the other side of the same frame. |
| 37 | if closeBoxLabel != "[x]" { |
| 38 | t.Errorf("closeBoxLabel = %q, want [x]", closeBoxLabel) |
| 39 | } |
| 40 | if maximizeBoxLabel != "[■]" { |
| 41 | t.Errorf("maximizeBoxLabel = %q, want [■]", maximizeBoxLabel) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func TestAWindowOnADesktopDrawsBothBoxes(t *testing.T) { |
| 46 | _, w := framedWindow(t, Rect{W: 40, H: 12}, Rect{W: 30, H: 8}) |
| 47 | |
| 48 | bar := titleBar(t, w, 40, 12) |
| 49 | |
| 50 | if !strings.Contains(bar, closeBoxLabel) { |
| 51 | t.Errorf("the title bar is %q, want a close box", bar) |
| 52 | } |
| 53 | if !strings.Contains(bar, maximizeBoxLabel) { |
| 54 | t.Errorf("the title bar is %q, want a maximise box", bar) |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestAWindowWithNowhereToMaximiseIntoDrawsNoBox(t *testing.T) { |
| 59 | // A window that was never added to a desktop has no OnMaximize, and a |
| 60 | // button that could do nothing is worse than no button. |
| 61 | w := NewWindow("main.go", nil) |
| 62 | w.SetBounds(Rect{W: 30, H: 8}) |
| 63 | |
| 64 | bar := titleBar(t, w, 40, 12) |
| 65 | |
| 66 | if strings.Contains(bar, maximizeBoxLabel) { |
| 67 | t.Errorf("the title bar is %q, want no maximise box", bar) |
| 68 | } |
| 69 | if !strings.Contains(bar, closeBoxLabel) { |
| 70 | t.Errorf("the title bar is %q, want the close box regardless", bar) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | func TestTheBoxShowsWhatPressingItWillDo(t *testing.T) { |
| 75 | area := Rect{W: 40, H: 12} |
| 76 | d, w := framedWindow(t, area, Rect{W: 30, H: 8}) |
| 77 | |
| 78 | if bar := titleBar(t, w, 40, 12); !strings.Contains(bar, maximizeBoxLabel) { |
| 79 | t.Errorf("a normal window shows %q, want the maximise box", bar) |
| 80 | } |
| 81 | |
| 82 | d.ToggleMaximize(w) |
| 83 | |
| 84 | bar := titleBar(t, w, 40, 12) |
| 85 | if !strings.Contains(bar, restoreBoxLabel) { |
| 86 | t.Errorf("a maximised window shows %q, want the restore box", bar) |
| 87 | } |
| 88 | if strings.Contains(bar, maximizeBoxLabel) { |
| 89 | t.Errorf("a maximised window still shows the maximise box: %q", bar) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | func TestClickingTheMaximiseBoxFillsTheDesktopAndClickingItAgainRestores(t *testing.T) { |
| 94 | area := Rect{X: 0, Y: 1, W: 40, H: 12} |
| 95 | bounds := Rect{X: 2, Y: 3, W: 30, H: 6} |
| 96 | _, w := framedWindow(t, area, bounds) |
| 97 | |
| 98 | pressMaximizeBox(w) |
| 99 | if got := w.Bounds(); got != area { |
| 100 | t.Fatalf("Bounds() = %+v, want the whole desktop %+v", got, area) |
| 101 | } |
| 102 | if !w.Maximized() { |
| 103 | t.Error("the window does not know it is maximised") |
| 104 | } |
| 105 | |
| 106 | pressMaximizeBox(w) |
| 107 | if got := w.Bounds(); got != bounds { |
| 108 | t.Errorf("Bounds() = %+v, want the size it had before %+v", got, bounds) |
| 109 | } |
| 110 | if w.Maximized() { |
| 111 | t.Error("the window still thinks it is maximised") |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func TestTheMaximiseBoxIsNotTheCloseBox(t *testing.T) { |
| 116 | // Both are three cells on the same row. Landing on the wrong one would |
| 117 | // close a window somebody meant to enlarge. |
| 118 | closed := 0 |
| 119 | area := Rect{W: 40, H: 12} |
| 120 | _, w := framedWindow(t, area, Rect{W: 30, H: 8}) |
| 121 | w.OnClose = func() bool { closed++; return true } |
| 122 | |
| 123 | pressMaximizeBox(w) |
| 124 | |
| 125 | if closed != 0 { |
| 126 | t.Error("pressing the maximise box closed the window") |
| 127 | } |
| 128 | if !w.Maximized() { |
| 129 | t.Error("pressing the maximise box did not maximise the window") |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | func TestPressingBesideTheMaximiseBoxMovesTheWindowInstead(t *testing.T) { |
| 134 | area := Rect{W: 40, H: 12} |
| 135 | _, w := framedWindow(t, area, Rect{W: 30, H: 8}) |
| 136 | |
| 137 | // One column to the left of the box is title bar, which drags. |
| 138 | w.HandleMouse(clickEvent(w.Bounds().Right()-maximizeOffset-1, w.Bounds().Y)) |
| 139 | |
| 140 | if w.Maximized() { |
| 141 | t.Error("a click one cell short of the box maximised the window") |
| 142 | } |
| 143 | if !w.Dragging() { |
| 144 | t.Error("a click on the title bar did not start a move") |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | func TestRestoringAWindowThatWasNeverMaximisedDoesNothing(t *testing.T) { |
| 149 | bounds := Rect{X: 2, Y: 3, W: 30, H: 6} |
| 150 | w := NewWindow("main.go", nil) |
| 151 | w.SetBounds(bounds) |
| 152 | |
| 153 | w.Restore() |
| 154 | |
| 155 | if got := w.Bounds(); got != bounds { |
| 156 | t.Errorf("Bounds() = %+v, want it left alone at %+v", got, bounds) |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | func TestMaximisingTwiceKeepsTheOriginalSize(t *testing.T) { |
| 161 | // The second call must not record the maximised bounds as the ones to go |
| 162 | // back to, or restoring would do nothing at all. |
| 163 | area := Rect{W: 40, H: 12} |
| 164 | bounds := Rect{X: 2, Y: 3, W: 30, H: 6} |
| 165 | w := NewWindow("main.go", nil) |
| 166 | w.SetBounds(bounds) |
| 167 | |
| 168 | w.Maximize(area) |
| 169 | w.Maximize(area) |
| 170 | w.Restore() |
| 171 | |
| 172 | if got := w.Bounds(); got != bounds { |
| 173 | t.Errorf("Bounds() = %+v, want %+v", got, bounds) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | func TestAMaximisedWindowFollowsATerminalThatShrinks(t *testing.T) { |
| 178 | // Without the restore rectangle following the desktop, pressing the box |
| 179 | // after a shrink would put the window partly off the screen. |
| 180 | before := Rect{W: 80, H: 22} |
| 181 | after := Rect{W: 40, H: 12} |
| 182 | d, w := framedWindow(t, before, Rect{X: 10, Y: 2, W: 60, H: 18}) |
| 183 | |
| 184 | d.ToggleMaximize(w) |
| 185 | d.SetBounds(after) |
| 186 | |
| 187 | if got := w.Bounds(); got != after { |
| 188 | t.Errorf("a maximised window is %+v, want the new desktop %+v", got, after) |
| 189 | } |
| 190 | |
| 191 | d.ToggleMaximize(w) |
| 192 | restored := w.Bounds() |
| 193 | if restored.Right() > after.Right() || restored.Bottom() > after.Bottom() { |
| 194 | t.Errorf("restored to %+v, which sticks out of the desktop %+v", restored, after) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | func TestTilingAWindowForgetsThatItWasMaximised(t *testing.T) { |
| 199 | // Its box must go back to offering to maximise: the rectangle it would |
| 200 | // restore to is not where the window is any more. |
| 201 | area := Rect{W: 40, H: 12} |
| 202 | d, w := framedWindow(t, area, Rect{W: 30, H: 8}) |
| 203 | d.ToggleMaximize(w) |
| 204 | |
| 205 | d.Tile() |
| 206 | |
| 207 | if w.Maximized() { |
| 208 | t.Error("a tiled window still calls itself maximised") |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | func TestCascadingAWindowForgetsThatItWasMaximised(t *testing.T) { |
| 213 | area := Rect{W: 40, H: 12} |
| 214 | d, w := framedWindow(t, area, Rect{W: 30, H: 8}) |
| 215 | d.ToggleMaximize(w) |
| 216 | |
| 217 | d.Cascade() |
| 218 | |
| 219 | if w.Maximized() { |
| 220 | t.Error("a cascaded window still calls itself maximised") |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | func TestTheTitleNeverRunsIntoTheFurniture(t *testing.T) { |
| 225 | // The margin kept for the boxes and the number is arithmetic that is easy |
| 226 | // to get wrong by one, and getting it wrong overwrites the number or the |
| 227 | // box with a letter. This checks it rather than trusting the sum. |
| 228 | for width := 16; width <= 60; width++ { |
| 229 | for _, title := range []string{"a", "main.go", "a-rather-long-file-name.go", strings.Repeat("x", 80)} { |
| 230 | w := NewWindow(title, nil) |
| 231 | d := NewDesktop() |
| 232 | d.SetBounds(Rect{W: width, H: 10}) |
| 233 | d.Add(w) |
| 234 | w.SetBounds(Rect{W: width, H: 6}) |
| 235 | w.SetNumber(7) |
| 236 | |
| 237 | bar := []rune(titleBar(t, w, width, 10)) |
| 238 | assertFurnitureIntact(t, bar, width, title) |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | // assertFurnitureIntact checks that a drawn title bar still has its close box, |
| 244 | // its number and its maximise box where they belong — and, the part that |
| 245 | // actually catches an off-by-one, that the title stops short of them. |
| 246 | // |
| 247 | // Checking the furniture alone proves very little: drawNumber runs after |
| 248 | // drawTitleBar, so the number is painted over whatever the title left there |
| 249 | // and always looks right. What goes wrong is the other way round — the title |
| 250 | // loses its last character to the number, reading "main.g7". |
| 251 | func assertFurnitureIntact(t *testing.T, bar []rune, width int, title string) { |
| 252 | t.Helper() |
| 253 | |
| 254 | if got := string(bar[2 : 2+boxWidth]); got != closeBoxLabel { |
| 255 | t.Fatalf("width %d, title %q: the close box reads %q", width, title, got) |
| 256 | } |
| 257 | if got := bar[width-numberOffset]; got != '7' { |
| 258 | t.Fatalf("width %d, title %q: the number cell holds %q", width, title, string(got)) |
| 259 | } |
| 260 | from := width - maximizeOffset |
| 261 | if got := string(bar[from : from+boxWidth]); got != maximizeBoxLabel { |
| 262 | t.Fatalf("width %d, title %q: the maximise box reads %q", width, title, got) |
| 263 | } |
| 264 | |
| 265 | // The title always ends in a space, so the cell before the number is one |
| 266 | // of those or bare frame. A letter there means the title reached the |
| 267 | // number and lost a character to it. |
| 268 | if got := bar[width-numberOffset-1]; !isFrameOrSpace(got) { |
| 269 | t.Fatalf("width %d, title %q: %q sits against the number, so the title was clipped by it", |
| 270 | width, title, string(got)) |
| 271 | } |
| 272 | if got := bar[4+1]; !isFrameOrSpace(got) { |
| 273 | t.Fatalf("width %d, title %q: %q sits against the close box", width, title, string(got)) |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | // isFrameOrSpace reports whether a cell of the top frame holds no title text. |
| 278 | func isFrameOrSpace(r rune) bool { |
| 279 | return r == ' ' || r == '═' || r == '─' |
| 280 | } |
| 281 | |
| 282 | // pressMaximizeBox clicks the middle of the maximise box. |
| 283 | func pressMaximizeBox(w *Window) { |
| 284 | bounds := w.Bounds() |
| 285 | w.HandleMouse(clickEvent(bounds.Right()-maximizeOffset+1, bounds.Y)) |
| 286 | } |