package ui import ( "strings" "testing" ) // framedWindow returns a window on a desktop — which is what gives it a // working maximise box — with the desktop filling the given area. func framedWindow(t *testing.T, area, bounds Rect) (*Desktop, *Window) { t.Helper() d := NewDesktop() d.SetBounds(area) w := NewWindow("main.go", nil) d.Add(w) w.SetBounds(bounds) return d, w } // titleBar draws a window onto a simulated screen and returns its top row. func titleBar(t *testing.T, w *Window, width, height int) string { t.Helper() screen, root := newTestScreen(t, width, height) w.SetActive(true) w.Draw(root, testTheme(t)) screen.Show() return screenLines(t, screen)[0] } func TestTheCloseBoxIsAnX(t *testing.T) { // It closes the window, so it says so. The block character it used to be // now means "maximise" on the other side of the same frame. if closeBoxLabel != "[x]" { t.Errorf("closeBoxLabel = %q, want [x]", closeBoxLabel) } if maximizeBoxLabel != "[■]" { t.Errorf("maximizeBoxLabel = %q, want [■]", maximizeBoxLabel) } } func TestAWindowOnADesktopDrawsBothBoxes(t *testing.T) { _, w := framedWindow(t, Rect{W: 40, H: 12}, Rect{W: 30, H: 8}) bar := titleBar(t, w, 40, 12) if !strings.Contains(bar, closeBoxLabel) { t.Errorf("the title bar is %q, want a close box", bar) } if !strings.Contains(bar, maximizeBoxLabel) { t.Errorf("the title bar is %q, want a maximise box", bar) } } func TestAWindowWithNowhereToMaximiseIntoDrawsNoBox(t *testing.T) { // A window that was never added to a desktop has no OnMaximize, and a // button that could do nothing is worse than no button. w := NewWindow("main.go", nil) w.SetBounds(Rect{W: 30, H: 8}) bar := titleBar(t, w, 40, 12) if strings.Contains(bar, maximizeBoxLabel) { t.Errorf("the title bar is %q, want no maximise box", bar) } if !strings.Contains(bar, closeBoxLabel) { t.Errorf("the title bar is %q, want the close box regardless", bar) } } func TestTheBoxShowsWhatPressingItWillDo(t *testing.T) { area := Rect{W: 40, H: 12} d, w := framedWindow(t, area, Rect{W: 30, H: 8}) if bar := titleBar(t, w, 40, 12); !strings.Contains(bar, maximizeBoxLabel) { t.Errorf("a normal window shows %q, want the maximise box", bar) } d.ToggleMaximize(w) bar := titleBar(t, w, 40, 12) if !strings.Contains(bar, restoreBoxLabel) { t.Errorf("a maximised window shows %q, want the restore box", bar) } if strings.Contains(bar, maximizeBoxLabel) { t.Errorf("a maximised window still shows the maximise box: %q", bar) } } func TestClickingTheMaximiseBoxFillsTheDesktopAndClickingItAgainRestores(t *testing.T) { area := Rect{X: 0, Y: 1, W: 40, H: 12} bounds := Rect{X: 2, Y: 3, W: 30, H: 6} _, w := framedWindow(t, area, bounds) pressMaximizeBox(w) if got := w.Bounds(); got != area { t.Fatalf("Bounds() = %+v, want the whole desktop %+v", got, area) } if !w.Maximized() { t.Error("the window does not know it is maximised") } pressMaximizeBox(w) if got := w.Bounds(); got != bounds { t.Errorf("Bounds() = %+v, want the size it had before %+v", got, bounds) } if w.Maximized() { t.Error("the window still thinks it is maximised") } } func TestTheMaximiseBoxIsNotTheCloseBox(t *testing.T) { // Both are three cells on the same row. Landing on the wrong one would // close a window somebody meant to enlarge. closed := 0 area := Rect{W: 40, H: 12} _, w := framedWindow(t, area, Rect{W: 30, H: 8}) w.OnClose = func() bool { closed++; return true } pressMaximizeBox(w) if closed != 0 { t.Error("pressing the maximise box closed the window") } if !w.Maximized() { t.Error("pressing the maximise box did not maximise the window") } } func TestPressingBesideTheMaximiseBoxMovesTheWindowInstead(t *testing.T) { area := Rect{W: 40, H: 12} _, w := framedWindow(t, area, Rect{W: 30, H: 8}) // One column to the left of the box is title bar, which drags. w.HandleMouse(clickEvent(w.Bounds().Right()-maximizeOffset-1, w.Bounds().Y)) if w.Maximized() { t.Error("a click one cell short of the box maximised the window") } if !w.Dragging() { t.Error("a click on the title bar did not start a move") } } func TestRestoringAWindowThatWasNeverMaximisedDoesNothing(t *testing.T) { bounds := Rect{X: 2, Y: 3, W: 30, H: 6} w := NewWindow("main.go", nil) w.SetBounds(bounds) w.Restore() if got := w.Bounds(); got != bounds { t.Errorf("Bounds() = %+v, want it left alone at %+v", got, bounds) } } func TestMaximisingTwiceKeepsTheOriginalSize(t *testing.T) { // The second call must not record the maximised bounds as the ones to go // back to, or restoring would do nothing at all. area := Rect{W: 40, H: 12} bounds := Rect{X: 2, Y: 3, W: 30, H: 6} w := NewWindow("main.go", nil) w.SetBounds(bounds) w.Maximize(area) w.Maximize(area) w.Restore() if got := w.Bounds(); got != bounds { t.Errorf("Bounds() = %+v, want %+v", got, bounds) } } func TestAMaximisedWindowFollowsATerminalThatShrinks(t *testing.T) { // Without the restore rectangle following the desktop, pressing the box // after a shrink would put the window partly off the screen. before := Rect{W: 80, H: 22} after := Rect{W: 40, H: 12} d, w := framedWindow(t, before, Rect{X: 10, Y: 2, W: 60, H: 18}) d.ToggleMaximize(w) d.SetBounds(after) if got := w.Bounds(); got != after { t.Errorf("a maximised window is %+v, want the new desktop %+v", got, after) } d.ToggleMaximize(w) restored := w.Bounds() if restored.Right() > after.Right() || restored.Bottom() > after.Bottom() { t.Errorf("restored to %+v, which sticks out of the desktop %+v", restored, after) } } func TestTilingAWindowForgetsThatItWasMaximised(t *testing.T) { // Its box must go back to offering to maximise: the rectangle it would // restore to is not where the window is any more. area := Rect{W: 40, H: 12} d, w := framedWindow(t, area, Rect{W: 30, H: 8}) d.ToggleMaximize(w) d.Tile() if w.Maximized() { t.Error("a tiled window still calls itself maximised") } } func TestCascadingAWindowForgetsThatItWasMaximised(t *testing.T) { area := Rect{W: 40, H: 12} d, w := framedWindow(t, area, Rect{W: 30, H: 8}) d.ToggleMaximize(w) d.Cascade() if w.Maximized() { t.Error("a cascaded window still calls itself maximised") } } func TestTheTitleNeverRunsIntoTheFurniture(t *testing.T) { // The margin kept for the boxes and the number is arithmetic that is easy // to get wrong by one, and getting it wrong overwrites the number or the // box with a letter. This checks it rather than trusting the sum. for width := 16; width <= 60; width++ { for _, title := range []string{"a", "main.go", "a-rather-long-file-name.go", strings.Repeat("x", 80)} { w := NewWindow(title, nil) d := NewDesktop() d.SetBounds(Rect{W: width, H: 10}) d.Add(w) w.SetBounds(Rect{W: width, H: 6}) w.SetNumber(7) bar := []rune(titleBar(t, w, width, 10)) assertFurnitureIntact(t, bar, width, title) } } } // assertFurnitureIntact checks that a drawn title bar still has its close box, // its number and its maximise box where they belong — and, the part that // actually catches an off-by-one, that the title stops short of them. // // Checking the furniture alone proves very little: drawNumber runs after // drawTitleBar, so the number is painted over whatever the title left there // and always looks right. What goes wrong is the other way round — the title // loses its last character to the number, reading "main.g7". func assertFurnitureIntact(t *testing.T, bar []rune, width int, title string) { t.Helper() if got := string(bar[2 : 2+boxWidth]); got != closeBoxLabel { t.Fatalf("width %d, title %q: the close box reads %q", width, title, got) } if got := bar[width-numberOffset]; got != '7' { t.Fatalf("width %d, title %q: the number cell holds %q", width, title, string(got)) } from := width - maximizeOffset if got := string(bar[from : from+boxWidth]); got != maximizeBoxLabel { t.Fatalf("width %d, title %q: the maximise box reads %q", width, title, got) } // The title always ends in a space, so the cell before the number is one // of those or bare frame. A letter there means the title reached the // number and lost a character to it. if got := bar[width-numberOffset-1]; !isFrameOrSpace(got) { t.Fatalf("width %d, title %q: %q sits against the number, so the title was clipped by it", width, title, string(got)) } if got := bar[4+1]; !isFrameOrSpace(got) { t.Fatalf("width %d, title %q: %q sits against the close box", width, title, string(got)) } } // isFrameOrSpace reports whether a cell of the top frame holds no title text. func isFrameOrSpace(r rune) bool { return r == ' ' || r == '═' || r == '─' } // pressMaximizeBox clicks the middle of the maximise box. func pressMaximizeBox(w *Window) { bounds := w.Bounds() w.HandleMouse(clickEvent(bounds.Right()-maximizeOffset+1, bounds.Y)) }