package ui import ( "strings" "testing" "github.com/gdamore/tcell/v2" ) // addWindows puts n windows on a desktop and returns them. func addWindows(d *Desktop, n int) []*Window { windows := make([]*Window, n) for i := range n { windows[i] = NewWindow("w", &stubContent{consume: true}) windows[i].SetBounds(Rect{X: i * 3, Y: i * 2, W: 20, H: 8}) d.Add(windows[i]) } return windows } func TestAddMakesTheNewWindowActiveAndNumbersThemAll(t *testing.T) { d := NewDesktop() windows := addWindows(d, 3) if d.Active() != windows[2] { t.Error("the last window added is not the active one") } for i, w := range windows { if got := w.Number(); got != i+1 { t.Errorf("window %d has number %d, want %d", i, got, i+1) } } if windows[0].Active() || windows[1].Active() { t.Error("more than one window is marked active") } } func TestRemoveRenumbersTheRest(t *testing.T) { d := NewDesktop() windows := addWindows(d, 3) if !d.Remove(windows[0]) { t.Fatal("Remove() = false for a window that was on the desktop") } if d.Count() != 2 { t.Errorf("Count() = %d, want 2", d.Count()) } if got := windows[1].Number(); got != 1 { t.Errorf("the remaining windows were not renumbered: got %d, want 1", got) } if d.Remove(NewWindow("stranger", nil)) { t.Error("Remove() = true for a window that was never added") } } func TestRemovingTheActiveWindowActivatesTheOneBehind(t *testing.T) { d := NewDesktop() windows := addWindows(d, 2) d.Remove(windows[1]) if d.Active() != windows[0] || !windows[0].Active() { t.Error("the window behind did not become active") } } func TestFocusBringsAWindowToTheFront(t *testing.T) { d := NewDesktop() windows := addWindows(d, 3) if !d.Focus(windows[0]) { t.Fatal("Focus() = false for a window on the desktop") } if d.Active() != windows[0] { t.Error("the focused window is not at the front") } if got := windows[0].Number(); got != 3 { t.Errorf("the raised window has number %d, want 3 — it is now last", got) } } func TestFocusNumber(t *testing.T) { d := NewDesktop() windows := addWindows(d, 3) if !d.FocusNumber(2) { t.Fatal("FocusNumber(2) = false") } if d.Active() != windows[1] { t.Error("FocusNumber raised the wrong window") } for _, n := range []int{0, -1, 4, 99} { if d.FocusNumber(n) { t.Errorf("FocusNumber(%d) = true, want false", n) } } } func TestNextCyclesThroughTheWindows(t *testing.T) { d := NewDesktop() windows := addWindows(d, 3) // front to back: 2, 1, 0 d.Next() if d.Active() != windows[1] { t.Error("Next() did not bring the window behind forward") } d.Next() if d.Active() != windows[0] { t.Error("a second Next() did not carry on round") } d.Next() if d.Active() != windows[2] { t.Error("Next() did not wrap back to the start") } } func TestNextWithFewerThanTwoWindowsDoesNothing(t *testing.T) { d := NewDesktop() d.Next() // must not panic on an empty desktop windows := addWindows(d, 1) d.Next() if d.Active() != windows[0] { t.Error("Next() disturbed a desktop holding one window") } } func TestDrawPaintsTheBackdropThenTheWindowsBackToFront(t *testing.T) { screen, root := newTestScreen(t, 40, 12) d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 12}) back := NewWindow("back", nil) back.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6}) front := NewWindow("front", nil) front.SetBounds(Rect{X: 5, Y: 0, W: 20, H: 6}) d.Add(back) d.Add(front) d.Draw(root, testTheme(t)) screen.Show() lines := screenLines(t, screen) if !strings.Contains(lines[11], string(desktopPattern)) { t.Errorf("the bottom row is %q, want the desktop pattern", lines[11]) } if !strings.Contains(lines[0], "front") { t.Errorf("the top row is %q, want the front window's title visible", lines[0]) } if strings.Contains(lines[0], "back") { t.Errorf("the top row is %q, want the back window covered", lines[0]) } } func TestAClickRaisesTheWindowItLandsOn(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 12}) back := NewWindow("back", &stubContent{consume: true}) back.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 6}) front := NewWindow("front", &stubContent{consume: true}) front.SetBounds(Rect{X: 20, Y: 0, W: 10, H: 6}) d.Add(back) d.Add(front) if !d.HandleMouse(clickEvent(3, 3)) { t.Fatal("the click was not handled") } if d.Active() != back { t.Error("clicking a window did not bring it to the front") } } func TestAClickOnTheBareDesktopIsNotHandled(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 12}) addWindows(d, 1) if d.HandleMouse(clickEvent(38, 11)) { t.Error("a click on the backdrop was reported as handled") } } func TestADraggedWindowKeepsReceivingEventsThatLeaveIt(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 0, W: 60, H: 20}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 5}) d.Add(w) d.HandleMouse(clickEvent(5, 0)) // grab the title bar // The pointer jumps far outside the window, as a fast drag does. d.HandleMouse(tcell.NewEventMouse(40, 12, tcell.Button1, tcell.ModNone)) if got := w.Bounds(); got.X != 35 || got.Y != 12 { t.Errorf("Bounds() = %+v, want the window to have followed the pointer", got) } } func TestKeysGoToTheActiveWindow(t *testing.T) { d := NewDesktop() back := &stubContent{consume: true} front := &stubContent{consume: true} d.Add(NewWindow("back", back)) d.Add(NewWindow("front", front)) d.HandleKey(runeEvent('x')) if front.keys != 1 || back.keys != 0 { t.Errorf("the active window saw %d keys and the other %d, want 1 and 0", front.keys, back.keys) } } func TestKeysOnAnEmptyDesktopAreNotHandled(t *testing.T) { if NewDesktop().HandleKey(runeEvent('x')) { t.Error("an empty desktop claimed a key") } } func TestTilePutsEveryWindowInItsOwnCell(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 22}) windows := addWindows(d, 4) d.Tile() for i, a := range windows { for _, b := range windows[i+1:] { if !a.Bounds().Intersect(b.Bounds()).IsEmpty() { t.Errorf("tiled windows %+v and %+v overlap", a.Bounds(), b.Bounds()) } } } } func TestTileOnAnEmptyDesktopDoesNothing(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 10}) d.Tile() // must not panic } func TestCascadeOffsetsEachWindowAndKeepsItsTitleVisible(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 22}) windows := addWindows(d, 3) d.Cascade() for i := 1; i < len(windows); i++ { previous, current := windows[i-1].Bounds(), windows[i].Bounds() if current.X <= previous.X || current.Y <= previous.Y { t.Errorf("window %d at %+v is not offset from %+v", i, current, previous) } } } func TestMaximizeFillsTheDesktop(t *testing.T) { d := NewDesktop() area := Rect{X: 0, Y: 1, W: 80, H: 22} d.SetBounds(area) w := addWindows(d, 1)[0] d.ToggleMaximize(w) if got := w.Bounds(); got != area { t.Errorf("Bounds() = %+v, want the whole desktop %+v", got, area) } } func TestAWindowFollowsTheDesktopWhenTheTerminalGrows(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 0, Y: 1, W: 78, H: 23}) // filling it, less the shadow d.Add(w) d.SetBounds(Rect{X: 0, Y: 1, W: 100, H: 30}) if got := w.Bounds(); got != (Rect{X: 0, Y: 1, W: 98, H: 30}) { t.Errorf("Bounds() = %+v, want the window grown by the same amount as the desktop", got) } } func TestAWindowFollowsTheDesktopWhenTheTerminalShrinks(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 0, Y: 1, W: 78, H: 23}) d.Add(w) d.SetBounds(Rect{X: 0, Y: 1, W: 60, H: 15}) if got := w.Bounds(); got != (Rect{X: 0, Y: 1, W: 58, H: 15}) { t.Errorf("Bounds() = %+v, want the window shrunk with the desktop", got) } } func TestACascadedWindowKeepsItsOffsetWhileFollowingTheFarCorner(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 4, Y: 5, W: 74, H: 19}) // offset from the top left d.Add(w) d.SetBounds(Rect{X: 0, Y: 1, W: 100, H: 30}) got := w.Bounds() if got.X != 4 || got.Y != 5 { t.Errorf("Bounds() = %+v, want the top-left corner left alone", got) } if got.Right() != 98 || got.Bottom() != 31 { t.Errorf("Bounds() = %+v, want the far corner to have kept its distance", got) } } func TestAWindowThatDoesNotGrowIsOnlyMoved(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetGrow(GrowNone) w.SetBounds(Rect{X: 60, Y: 15, W: 20, H: 8}) d.Add(w) d.SetBounds(Rect{X: 0, Y: 1, W: 40, H: 12}) got := w.Bounds() if got.W != 20 { t.Errorf("Bounds() = %+v, want its width kept", got) } if got.Right() > 40 || got.Bottom() > 13 { t.Errorf("Bounds() = %+v, want it pulled back inside the smaller desktop", got) } } func TestNoWindowEndsUpLargerThanTheDesktop(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetGrow(GrowNone) // it does not follow, so only the clamp can save it w.SetBounds(Rect{X: 0, Y: 1, W: 78, H: 23}) d.Add(w) d.SetBounds(Rect{X: 0, Y: 1, W: 30, H: 10}) got := w.Bounds() if got.W > 30 || got.H > 10 { t.Errorf("Bounds() = %+v, want it held to the desktop's own size", got) } } func TestAWindowIsNotShrunkBelowItsMinimumWhileTheDesktopCanHoldIt(t *testing.T) { d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 0, Y: 1, W: 20, H: 6}) d.Add(w) // Shrinking by more than the window is wide would leave it at nothing. d.SetBounds(Rect{X: 0, Y: 1, W: 40, H: 20}) got := w.Bounds() if got.W < MinWindowWidth || got.H < MinWindowHeight { t.Errorf("Bounds() = %+v, want at least the minimum window size", got) } } func TestOnATerminalSmallerThanTheMinimumTheDesktopWins(t *testing.T) { // A window may be no larger than the desktop even when that means going // below the size it could be dragged down to: overflowing a terminal this // small helps nobody, and the painter clips the rest away anyway. d := NewDesktop() d.SetBounds(Rect{X: 0, Y: 1, W: 80, H: 23}) w := NewWindow("t", nil) w.SetBounds(Rect{X: 0, Y: 1, W: 78, H: 23}) d.Add(w) tiny := Rect{X: 0, Y: 1, W: 4, H: 2} d.SetBounds(tiny) if got := w.Bounds(); got.W > tiny.W || got.H > tiny.H { t.Errorf("Bounds() = %+v, want it to fit inside %+v", got, tiny) } } func TestALayoutThatChangesNothingLeavesTheWindowsAlone(t *testing.T) { // SetBounds runs on every turn of the event loop, so it must be inert when // the desktop has not moved. A window the user resized by hand would // otherwise creep. d := NewDesktop() area := Rect{X: 0, Y: 1, W: 80, H: 23} d.SetBounds(area) w := NewWindow("t", nil) w.SetBounds(Rect{X: 5, Y: 5, W: 30, H: 10}) d.Add(w) for range 10 { d.SetBounds(area) } if got := w.Bounds(); got != (Rect{X: 5, Y: 5, W: 30, H: 10}) { t.Errorf("Bounds() = %+v, want it untouched", got) } } func TestADocumentWindowFollowsBothEdgesByDefault(t *testing.T) { if got := NewWindow("t", nil).Grow(); got != GrowBoth { t.Errorf("Grow() = %v, want GrowBoth", got) } }