| 🛟 Updated. 28d5985 k33g 17h ago | 1 | package terminal |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/gdamore/tcell/v2" |
| 9 | |
| 10 | "codeberg.org/turbo-editors/turbo-core/theme" |
| 11 | "codeberg.org/turbo-editors/turbo-core/ui" |
| 12 | ) |
| 13 | |
| 14 | // newTestView starts a view on a bare shell, closed when the test ends. |
| 15 | // newTestViewWith returns a view on a shell, wired to the given callbacks. |
| 16 | // |
| 17 | // They are passed in rather than assigned afterwards because NewView starts the |
| 18 | // goroutine that calls them — assigning them later is the data race this whole |
| 19 | // arrangement exists to prevent. |
| 20 | func newTestViewWith(t *testing.T, width, height int, onChange, onExit func()) *View { |
| 21 | t.Helper() |
| 22 | skipWithoutPTY(t) |
| 23 | |
| 24 | v, err := NewView(ViewOptions{ |
| 25 | Options: Options{Shell: "/bin/sh", Dir: t.TempDir(), Width: width, Height: height}, |
| 26 | OnChange: onChange, |
| 27 | OnExit: onExit, |
| 28 | }) |
| 29 | if err != nil { |
| 30 | t.Fatalf("NewView() error = %v", err) |
| 31 | } |
| 32 | t.Cleanup(func() { v.Close() }) |
| 33 | v.SetBounds(ui.Rect{W: width, H: height}) |
| 34 | return v |
| 35 | } |
| 36 | |
| 37 | func newTestView(t *testing.T, width, height int) *View { |
| 38 | t.Helper() |
| 39 | skipWithoutPTY(t) |
| 40 | |
| 41 | v, err := NewView(ViewOptions{ |
| 42 | Options: Options{Shell: "/bin/sh", Dir: t.TempDir(), Width: width, Height: height}, |
| 43 | }) |
| 44 | if err != nil { |
| 45 | t.Fatalf("NewView() error = %v", err) |
| 46 | } |
| 47 | t.Cleanup(func() { v.Close() }) |
| 48 | v.SetBounds(ui.Rect{W: width, H: height}) |
| 49 | return v |
| 50 | } |
| 51 | |
| 52 | // newOfflineView returns a view with no shell behind it. |
| 53 | // |
| 54 | // Drawing needs no session, and a test about what reaches the screen must not |
| 55 | // race the shell's own startup output for the cell it is looking at — which is |
| 56 | // exactly the flake this helper replaced. |
| 57 | func newOfflineView(t *testing.T, width, height int) *View { |
| 58 | t.Helper() |
| 59 | |
| 60 | v := &View{ |
| 61 | parser: NewParser(NewScreen(width, height)), |
| 62 | dirty: make(chan struct{}, 1), |
| 63 | closed: make(chan struct{}), |
| 64 | } |
| 65 | v.SetFocused(true) |
| 66 | v.FocusBox.SetBounds(ui.Rect{W: width, H: height}) |
| 67 | return v |
| 68 | } |
| 69 | |
| 70 | // waitUntil waits for a condition on the view's screen, which is checked with |
| 71 | // the lock held. |
| 72 | func waitUntil(t *testing.T, v *View, description string, condition func() bool) { |
| 73 | t.Helper() |
| 74 | |
| 75 | deadline := time.After(10 * time.Second) |
| 76 | for { |
| 77 | v.mu.Lock() |
| 78 | met := condition() |
| 79 | shown := screenText(v.parser.Screen()) |
| 80 | v.mu.Unlock() |
| 81 | |
| 82 | if met { |
| 83 | return |
| 84 | } |
| 85 | select { |
| 86 | case <-deadline: |
| 87 | t.Fatalf("the terminal never showed %s; it shows:\n%s", description, shown) |
| 88 | case <-time.After(5 * time.Millisecond): |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // waitForScreen waits until some row of the view's screen holds the text. |
| 94 | func waitForScreen(t *testing.T, v *View, want string) { |
| 95 | t.Helper() |
| 96 | |
| 97 | deadline := time.After(10 * time.Second) |
| 98 | for { |
| 99 | v.mu.Lock() |
| 100 | found := screenContains(v.parser.Screen(), want) |
| 101 | shown := screenText(v.parser.Screen()) |
| 102 | v.mu.Unlock() |
| 103 | |
| 104 | if found { |
| 105 | return |
| 106 | } |
| 107 | select { |
| 108 | case <-deadline: |
| 109 | t.Fatalf("the terminal never showed %q; it shows:\n%s", want, shown) |
| 110 | case <-time.After(5 * time.Millisecond): |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // drawView paints a view onto a simulated screen and returns what it shows. |
| 116 | func drawView(t *testing.T, v *View) (tcell.SimulationScreen, []string) { |
| 117 | t.Helper() |
| 118 | |
| 119 | screen := tcell.NewSimulationScreen("UTF-8") |
| 120 | if err := screen.Init(); err != nil { |
| 121 | t.Fatalf("initialising the simulation screen: %v", err) |
| 122 | } |
| 123 | t.Cleanup(screen.Fini) |
| 124 | |
| 125 | bounds := v.Bounds() |
| 126 | screen.SetSize(bounds.W, bounds.H) |
| 127 | v.Draw(ui.NewPainter(screen).Sub(bounds), theme.Default("")) |
| 128 | screen.Show() |
| 129 | |
| 130 | cells, width, height := screen.GetContents() |
| 131 | lines := make([]string, height) |
| 132 | for row := range height { |
| 133 | var text strings.Builder |
| 134 | for col := range width { |
| 135 | runes := cells[row*width+col].Runes |
| 136 | if len(runes) == 0 { |
| 137 | text.WriteRune(' ') |
| 138 | continue |
| 139 | } |
| 140 | text.WriteRune(runes[0]) |
| 141 | } |
| 142 | lines[row] = strings.TrimRight(text.String(), " ") |
| 143 | } |
| 144 | return screen, lines |
| 145 | } |
| 146 | |
| 147 | func TestAViewShowsWhatTheShellWrites(t *testing.T) { |
| 148 | v := newTestView(t, 40, 8) |
| 149 | |
| 150 | v.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'e', tcell.ModNone)) |
| 151 | typeInto(v, "cho drawn-here\r") |
| 152 | |
| 153 | waitForScreen(t, v, "drawn-here") |
| 154 | |
| 155 | _, lines := drawView(t, v) |
| 156 | if !containsLine(lines, "drawn-here") { |
| 157 | t.Errorf("the drawn view does not show the output:\n%s", strings.Join(lines, "\n")) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func TestTheThemeFillsInColoursTheProgramDidNotChoose(t *testing.T) { |
| 162 | v := newOfflineView(t, 20, 4) |
| 163 | th := theme.Default("") |
| 164 | |
| 165 | screen, _ := drawView(t, v) |
| 166 | cells, width, _ := screen.GetContents() |
| 167 | |
| 168 | // The second row, because the cursor sits on the first one and is drawn in |
| 169 | // a colour of its own. |
| 170 | wantForeground, wantBackground, _ := th.Style(theme.KeyTerminalText).Decompose() |
| 171 | got, gotBackground, _ := cells[width].Style.Decompose() |
| 172 | |
| 173 | if got != wantForeground || gotBackground != wantBackground { |
| 174 | t.Errorf("an untouched cell is %v on %v, want the theme's %v on %v", |
| 175 | got, gotBackground, wantForeground, wantBackground) |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | func TestTheCursorIsDrawnInTheThemesCursorColour(t *testing.T) { |
| 180 | v := newOfflineView(t, 20, 4) |
| 181 | th := theme.Default("") |
| 182 | |
| 183 | screen, _ := drawView(t, v) |
| 184 | cells, _, _ := screen.GetContents() |
| 185 | |
| 186 | wantForeground, wantBackground, _ := th.Style(theme.KeyTerminalCursor).Decompose() |
| 187 | got, gotBackground, _ := cells[0].Style.Decompose() |
| 188 | |
| 189 | if got != wantForeground || gotBackground != wantBackground { |
| 190 | t.Errorf("the cursor cell is %v on %v, want the theme's %v on %v", |
| 191 | got, gotBackground, wantForeground, wantBackground) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | func TestAnUnfocusedTerminalDrawsNoCursor(t *testing.T) { |
| 196 | v := newOfflineView(t, 20, 4) |
| 197 | v.SetFocused(false) |
| 198 | th := theme.Default("") |
| 199 | |
| 200 | screen, _ := drawView(t, v) |
| 201 | cells, _, _ := screen.GetContents() |
| 202 | |
| 203 | // Two terminals both showing a cursor would be two claims on the keyboard. |
| 204 | wantForeground, wantBackground, _ := th.Style(theme.KeyTerminalText).Decompose() |
| 205 | got, gotBackground, _ := cells[0].Style.Decompose() |
| 206 | |
| 207 | if got != wantForeground || gotBackground != wantBackground { |
| 208 | t.Errorf("an unfocused terminal drew its cursor: %v on %v", got, gotBackground) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | func TestAProgramsChosenColoursAreLeftAlone(t *testing.T) { |
| 213 | v := newOfflineView(t, 20, 4) |
| 214 | |
| 215 | // A program that named both halves must get both back untouched, however |
| 216 | // little they resemble the theme. |
| 217 | v.parser.Write([]byte("\x1b[38;5;196;48;5;21mX")) //nolint:errcheck // the parser never fails |
| 218 | |
| 219 | screen, _ := drawView(t, v) |
| 220 | cells, _, _ := screen.GetContents() |
| 221 | |
| 222 | foreground, background, _ := cells[0].Style.Decompose() |
| 223 | if foreground != tcell.PaletteColor(196) || background != tcell.PaletteColor(21) { |
| 224 | t.Errorf("the cell is %v on %v, want the program's own 196 on 21", foreground, background) |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | func TestAProgramsOwnColoursSurvive(t *testing.T) { |
| 229 | v := newTestView(t, 40, 6) |
| 230 | |
| 231 | // Waiting for the word itself would prove nothing: the pseudo-terminal |
| 232 | // echoes the command line, so "red" appears before the command has run. |
| 233 | // The colour is what only the output can produce. |
| 234 | typeInto(v, "printf '\\033[31mred\\033[0m\\n'\r") |
| 235 | |
| 236 | waitUntil(t, v, "a red cell", func() bool { |
| 237 | return screenHasColour(v.parser.Screen(), tcell.ColorMaroon) |
| 238 | }) |
| 239 | } |
| 240 | |
| 241 | func TestTheCursorIsDrawnOnlyWhenTheViewHasTheFocus(t *testing.T) { |
| 242 | v := newTestView(t, 20, 4) |
| 243 | |
| 244 | screen, _ := drawView(t, v) |
| 245 | if _, _, visible := screen.GetCursor(); !visible { |
| 246 | t.Error("a focused terminal did not place the cursor") |
| 247 | } |
| 248 | |
| 249 | v.SetFocused(false) |
| 250 | screen, _ = drawView(t, v) |
| 251 | if _, _, visible := screen.GetCursor(); visible { |
| 252 | t.Error("an unfocused terminal placed the cursor") |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | func TestResizingTellsTheShell(t *testing.T) { |
| 257 | v := newTestView(t, 40, 8) |
| 258 | |
| 259 | v.SetBounds(ui.Rect{W: 100, H: 30}) |
| 260 | typeInto(v, "stty size\r") |
| 261 | |
| 262 | waitForScreen(t, v, "30 100") |
| 263 | } |
| 264 | |
| 265 | func TestTheViewTitleIsTheShellUntilTheProgramSaysOtherwise(t *testing.T) { |
| 266 | v := newTestView(t, 40, 6) |
| 267 | |
| 268 | if got := v.Title(); got != "sh" { |
| 269 | t.Errorf("Title() = %q, want the shell's name", got) |
| 270 | } |
| 271 | |
| 272 | typeInto(v, "printf '\\033]0;my title\\007'\r") |
| 273 | |
| 274 | waitUntil(t, v, "the title the program set", func() bool { |
| 275 | return v.parser.Title() == "my title" |
| 276 | }) |
| 277 | } |
| 278 | |
| 279 | func TestScrollingBackThroughTheHistory(t *testing.T) { |
| 280 | v := newTestView(t, 40, 6) |
| 281 | |
| 282 | typeInto(v, "for i in 1 2 3 4 5 6 7 8 9; do echo line-$i; done\r") |
| 283 | waitForScreen(t, v, "line-9") |
| 284 | |
| 285 | if got := v.ScrollOffset(); got != 0 { |
| 286 | t.Fatalf("ScrollOffset() = %d, want the live screen", got) |
| 287 | } |
| 288 | |
| 289 | v.ScrollBy(4) |
| 290 | if got := v.ScrollOffset(); got == 0 { |
| 291 | t.Fatal("ScrollBy did not move back into the history") |
| 292 | } |
| 293 | |
| 294 | _, lines := drawView(t, v) |
| 295 | if containsLine(lines, "line-9") { |
| 296 | t.Errorf("the view still shows the newest line after scrolling back:\n%s", strings.Join(lines, "\n")) |
| 297 | } |
| 298 | |
| 299 | v.ScrollToBottom() |
| 300 | _, lines = drawView(t, v) |
| 301 | if !containsLine(lines, "line-9") { |
| 302 | t.Errorf("the view did not come back to the live screen:\n%s", strings.Join(lines, "\n")) |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | func TestScrollingStopsAtBothEnds(t *testing.T) { |
| 307 | v := newTestView(t, 40, 6) |
| 308 | |
| 309 | v.ScrollBy(-100) |
| 310 | if got := v.ScrollOffset(); got != 0 { |
| 311 | t.Errorf("ScrollOffset() = %d, want it held at the live screen", got) |
| 312 | } |
| 313 | |
| 314 | v.ScrollBy(10_000) |
| 315 | v.mu.Lock() |
| 316 | history := v.parser.Screen().ScrollbackLen() |
| 317 | v.mu.Unlock() |
| 318 | if got := v.ScrollOffset(); got > history { |
| 319 | t.Errorf("ScrollOffset() = %d, want no more than the %d lines of history", got, history) |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | func TestShiftPageUpReadsBackAndAKeyComesForward(t *testing.T) { |
| 324 | v := newTestView(t, 40, 6) |
| 325 | typeInto(v, "for i in 1 2 3 4 5 6 7 8 9; do echo line-$i; done\r") |
| 326 | waitForScreen(t, v, "line-9") |
| 327 | |
| 328 | v.HandleKey(tcell.NewEventKey(tcell.KeyPgUp, 0, tcell.ModShift)) |
| 329 | if v.ScrollOffset() == 0 { |
| 330 | t.Fatal("Shift-PageUp did not read back through the history") |
| 331 | } |
| 332 | |
| 333 | // Typing must bring the view back, or the user cannot see what they type. |
| 334 | v.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModNone)) |
| 335 | if got := v.ScrollOffset(); got != 0 { |
| 336 | t.Errorf("ScrollOffset() = %d after a key press, want the live screen", got) |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | func TestPlainPageUpGoesToTheShell(t *testing.T) { |
| 341 | // Shift is what scrolls; a bare Page Up belongs to the program, which may |
| 342 | // well be a pager. |
| 343 | v := newTestView(t, 40, 6) |
| 344 | typeInto(v, "for i in 1 2 3 4 5 6 7 8 9; do echo line-$i; done\r") |
| 345 | waitForScreen(t, v, "line-9") |
| 346 | |
| 347 | v.HandleKey(tcell.NewEventKey(tcell.KeyPgUp, 0, tcell.ModNone)) |
| 348 | |
| 349 | if got := v.ScrollOffset(); got != 0 { |
| 350 | t.Errorf("ScrollOffset() = %d, want a bare Page Up to have gone to the shell", got) |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | func TestTheWheelReadsBackThroughTheHistory(t *testing.T) { |
| 355 | v := newTestView(t, 40, 6) |
| 356 | typeInto(v, "for i in 1 2 3 4 5 6 7 8 9; do echo line-$i; done\r") |
| 357 | waitForScreen(t, v, "line-9") |
| 358 | |
| 359 | v.HandleMouse(tcell.NewEventMouse(1, 1, tcell.WheelUp, tcell.ModNone)) |
| 360 | |
| 361 | if got := v.ScrollOffset(); got != wheelStep { |
| 362 | t.Errorf("ScrollOffset() = %d, want %d", got, wheelStep) |
| 363 | } |
| 364 | |
| 365 | v.HandleMouse(tcell.NewEventMouse(1, 1, tcell.WheelDown, tcell.ModNone)) |
| 366 | if got := v.ScrollOffset(); got != 0 { |
| 367 | t.Errorf("ScrollOffset() = %d, want it back at the live screen", got) |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | func TestAMouseEventThatMissesTheViewIsNotItsBusiness(t *testing.T) { |
| 372 | v := newTestView(t, 10, 4) |
| 373 | |
| 374 | if v.HandleMouse(tcell.NewEventMouse(50, 50, tcell.WheelUp, tcell.ModNone)) { |
| 375 | t.Error("the view claimed a wheel event from outside it") |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | func TestAnUnfocusedViewIgnoresKeys(t *testing.T) { |
| 380 | v := newTestView(t, 20, 4) |
| 381 | v.SetFocused(false) |
| 382 | |
| 383 | if v.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModNone)) { |
| 384 | t.Error("an unfocused terminal claimed a key") |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | func TestOutputArrivingWhileReadingHistoryComesForward(t *testing.T) { |
| 389 | v := newTestView(t, 40, 6) |
| 390 | typeInto(v, "for i in 1 2 3 4 5 6 7 8 9; do echo line-$i; done\r") |
| 391 | waitForScreen(t, v, "line-9") |
| 392 | |
| 393 | v.ScrollBy(4) |
| 394 | typeInto(v, "echo pulled-forward\r") |
| 395 | waitForScreen(t, v, "pulled-forward") |
| 396 | |
| 397 | if got := v.ScrollOffset(); got != 0 { |
| 398 | t.Errorf("ScrollOffset() = %d, want new output to have brought the view forward", got) |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | func TestTheEditorIsToldWhenTheShellExits(t *testing.T) { |
| 403 | gone := make(chan struct{}) |
| 404 | v := newTestViewWith(t, 20, 4, nil, func() { close(gone) }) |
| 405 | |
| 406 | typeInto(v, "exit\r") |
| 407 | |
| 408 | select { |
| 409 | case <-gone: |
| 410 | case <-time.After(10 * time.Second): |
| 411 | t.Error("OnExit was never called after the shell left") |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | func TestTheEditorIsAskedToRedraw(t *testing.T) { |
| 416 | drawn := make(chan struct{}, 64) |
| 417 | v := newTestViewWith(t, 20, 4, func() { |
| 418 | select { |
| 419 | case drawn <- struct{}{}: |
| 420 | default: |
| 421 | } |
| 422 | }, nil) |
| 423 | |
| 424 | typeInto(v, "echo something\r") |
| 425 | |
| 426 | select { |
| 427 | case <-drawn: |
| 428 | case <-time.After(10 * time.Second): |
| 429 | t.Error("the editor was never asked to redraw") |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | func TestNewViewFailsWhenTheShellIsNotThere(t *testing.T) { |
| 434 | skipWithoutPTY(t) |
| 435 | |
| 436 | options := ViewOptions{Options: Options{Shell: "/nonexistent/shell", Width: 20, Height: 5}} |
| 437 | if _, err := NewView(options); err == nil { |
| 438 | t.Fatal("NewView() error = nil for a shell that does not exist") |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | // typeInto sends a string to a view one key at a time, as typing would. |
| 443 | func typeInto(v *View, text string) { |
| 444 | for _, r := range text { |
| 445 | key := tcell.KeyRune |
| 446 | if r == '\r' { |
| 447 | key = tcell.KeyEnter |
| 448 | r = 0 |
| 449 | } |
| 450 | v.HandleKey(tcell.NewEventKey(key, r, tcell.ModNone)) |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | // containsLine reports whether any of the lines holds the text. |
| 455 | func containsLine(lines []string, want string) bool { |
| 456 | for _, line := range lines { |
| 457 | if strings.Contains(line, want) { |
| 458 | return true |
| 459 | } |
| 460 | } |
| 461 | return false |
| 462 | } |
| 463 | |
| 464 | // screenHasColour reports whether any cell is painted in a colour. |
| 465 | func screenHasColour(s *Screen, want tcell.Color) bool { |
| 466 | _, height := s.Size() |
| 467 | width, _ := s.Size() |
| 468 | |
| 469 | for row := range height { |
| 470 | for col := range width { |
| 471 | if foreground, _, _ := s.CellAt(row, col).Style.Decompose(); foreground == want { |
| 472 | return true |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | return false |
| 477 | } |
| 478 | |
| 479 | func TestTheNameIsUsedWhenTheProgramSetsNoTitle(t *testing.T) { |
| 480 | // A command run through a shell would otherwise show as "sh", which says |
| 481 | // nothing about what is in the window. |
| 482 | v := newOfflineView(t, 20, 4) |
| 483 | v.name = "go test ./..." |
| 484 | |
| 485 | if got := v.Title(); got != "go test ./..." { |
| 486 | t.Errorf("Title() = %q, want the name the caller gave", got) |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | func TestATitleTheProgramAsksForWinsOverTheName(t *testing.T) { |
| 491 | // vim naming the file it has open is saying something the caller could not |
| 492 | // have known. |
| 493 | v := newOfflineView(t, 20, 4) |
| 494 | v.name = "go run ." |
| 495 | v.parser.Write([]byte("\x1b]0;vim main.go\x07")) //nolint:errcheck // the parser never fails |
| 496 | |
| 497 | if got := v.Title(); got != "vim main.go" { |
| 498 | t.Errorf("Title() = %q, want the program's own title", got) |
| 499 | } |
| 500 | } |
| 501 | |
| 502 | func TestAFinishedTerminalStopsTakingKeys(t *testing.T) { |
| 503 | // Otherwise the write to the dead shell fails silently, the key is consumed |
| 504 | // anyway, and Ctrl-W can never close the window — leaving the mouse as the |
| 505 | // only way out of a command that has finished. |
| 506 | v := newTestView(t, 20, 4) |
| 507 | typeInto(v, "exit\r") |
| 508 | |
| 509 | waitUntil(t, v, "the shell to have gone", func() bool { return v.Exited() }) |
| 510 | |
| 511 | if v.HandleKey(tcell.NewEventKey(tcell.KeyCtrlW, 0, tcell.ModCtrl)) { |
| 512 | t.Error("a finished terminal consumed Ctrl-W") |
| 513 | } |
| 514 | if v.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModNone)) { |
| 515 | t.Error("a finished terminal consumed a printable key") |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | func TestAFinishedTerminalStillScrollsItsOutput(t *testing.T) { |
| 520 | // Reading back through what a command printed is the whole reason the |
| 521 | // window stays open after it exits. |
| 522 | v := newTestView(t, 20, 4) |
| 523 | typeInto(v, "exit\r") |
| 524 | waitUntil(t, v, "the shell to have gone", func() bool { return v.Exited() }) |
| 525 | |
| 526 | if !v.HandleKey(tcell.NewEventKey(tcell.KeyPgUp, 0, tcell.ModShift)) { |
| 527 | t.Error("a finished terminal will not scroll back through its output") |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | func TestALiveTerminalIsNotExited(t *testing.T) { |
| 532 | v := newTestView(t, 20, 4) |
| 533 | |
| 534 | if v.Exited() { |
| 535 | t.Error("a running shell reports itself gone") |
| 536 | } |
| 537 | } |