1package ui
2
3import (
4 "image/color"
5 "strings"
6 "testing"
7
8 "charm.land/bubbles/v2/key"
9 tea "charm.land/bubbletea/v2"
10
11 "git.secluded.site/keld/internal/theme"
12)
13
14// fakeScreen is a minimal Screen implementation for testing session
15// navigation without depending on real UI components. It returns
16// BackCmd on Esc to signal back-navigation, matching the expected
17// behaviour for screens that don't use Esc internally.
18type fakeScreen struct {
19 title string
20 selection string
21 initCalls int
22}
23
24func newFake(title string) *fakeScreen {
25 return &fakeScreen{title: title}
26}
27
28func (f *fakeScreen) Init() tea.Cmd { f.initCalls++; return nil }
29
30func (f *fakeScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
31 if kp, ok := msg.(tea.KeyPressMsg); ok {
32 if key.Matches(kp, key.NewBinding(key.WithKeys("esc"))) {
33 return f, BackCmd
34 }
35 }
36 return f, nil
37}
38
39func (f *fakeScreen) View() string { return f.title + " view" }
40func (f *fakeScreen) Title() string { return f.title }
41func (f *fakeScreen) KeyBindings() []key.Binding { return nil }
42func (f *fakeScreen) Selection() string { return f.selection }
43
44// complete marks the fake screen as having a selection for breadcrumb
45// display purposes.
46func (f *fakeScreen) complete(sel string) { f.selection = sel }
47
48// selectingScreen completes itself on any non-Esc key press by setting
49// its selection and returning DoneCmd to signal advancement.
50type selectingScreen struct {
51 fakeScreen
52}
53
54func newSelecting(title string) *selectingScreen {
55 return &selectingScreen{fakeScreen: fakeScreen{title: title}}
56}
57
58func (s *selectingScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
59 if kp, ok := msg.(tea.KeyPressMsg); ok {
60 if key.Matches(kp, key.NewBinding(key.WithKeys("esc"))) {
61 return s, BackCmd
62 }
63 s.selection = s.title
64 return s, DoneCmd
65 }
66 return s, nil
67}
68
69// escConsumingScreen handles Esc internally (e.g. like a file picker)
70// and does not return BackCmd.
71type escConsumingScreen struct {
72 fakeScreen
73 escHandled bool
74}
75
76func newEscConsuming(title string) *escConsumingScreen {
77 return &escConsumingScreen{fakeScreen: fakeScreen{title: title}}
78}
79
80func (e *escConsumingScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
81 if kp, ok := msg.(tea.KeyPressMsg); ok {
82 if key.Matches(kp, key.NewBinding(key.WithKeys("esc"))) {
83 e.escHandled = true
84 return e, nil // consume Esc, no BackMsg
85 }
86 }
87 return e, nil
88}
89
90// sizeCaptureScreen records the height from WindowSizeMsg for testing
91// that the session adjusts dimensions correctly.
92type sizeCaptureScreen struct {
93 fakeScreen
94 onSize func(int)
95}
96
97func (s *sizeCaptureScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
98 if wsm, ok := msg.(tea.WindowSizeMsg); ok {
99 if s.onSize != nil {
100 s.onSize(wsm.Height)
101 }
102 }
103 return s.fakeScreen.Update(msg)
104}
105
106// defaultStyles returns a *theme.Styles for use in tests. Callers
107// pass this to both New() and any screen constructors that need it.
108func defaultStyles() *theme.Styles {
109 s := theme.New(true)
110 return &s
111}
112
113func escMsg() tea.Msg {
114 return tea.KeyPressMsg{Code: tea.KeyEscape}
115}
116
117func ctrlCMsg() tea.Msg {
118 return tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}
119}
120
121func enterMsg() tea.Msg {
122 return tea.KeyPressMsg{Code: tea.KeyEnter}
123}
124
125// resizeMsg simulates a window resize — an unrelated background message.
126func resizeMsg() tea.Msg {
127 return tea.WindowSizeMsg{Width: 80, Height: 24}
128}
129
130func TestSessionInit(t *testing.T) {
131 t.Parallel()
132
133 a := newFake("first")
134 b := newFake("second")
135 s := New([]Screen{a, b}, defaultStyles())
136 s.Init()
137
138 if a.initCalls != 1 {
139 t.Errorf("first screen Init called %d times, want 1", a.initCalls)
140 }
141 if b.initCalls != 0 {
142 t.Errorf("second screen Init called %d times, want 0", b.initCalls)
143 }
144}
145
146func TestCtrlCExits(t *testing.T) {
147 t.Parallel()
148
149 s := New([]Screen{newFake("one")}, defaultStyles())
150 s.Init()
151
152 result, cmd := s.Update(ctrlCMsg())
153 sess := result.(Session)
154
155 if !sess.done {
156 t.Error("session should be done after Ctrl+C")
157 }
158 if cmd == nil {
159 t.Error("expected tea.Quit command")
160 }
161}
162
163func TestEscOnFirstScreenExits(t *testing.T) {
164 t.Parallel()
165
166 s := New([]Screen{newFake("one")}, defaultStyles())
167 s.Init()
168
169 // Esc is forwarded to the screen, which returns BackCmd.
170 result, _ := s.Update(escMsg())
171 // BackMsg triggers navigateBack.
172 result, cmd := result.(Session).Update(BackMsg{})
173 sess := result.(Session)
174
175 if !sess.done {
176 t.Error("Esc on first screen should exit")
177 }
178 if cmd == nil {
179 t.Error("expected tea.Quit command")
180 }
181}
182
183func TestEscNavigatesBack(t *testing.T) {
184 t.Parallel()
185
186 a := newFake("first")
187 b := newFake("second")
188 a.complete("picked")
189
190 s := New([]Screen{a, b}, defaultStyles())
191 s.cursor = 1
192
193 // Esc → screen returns BackCmd → session receives BackMsg.
194 result, _ := s.Update(escMsg())
195 result, _ = result.(Session).Update(BackMsg{})
196 sess := result.(Session)
197
198 if sess.cursor != 0 {
199 t.Errorf("cursor = %d, want 0 after Esc", sess.cursor)
200 }
201}
202
203func TestBackNavigationPreservesState(t *testing.T) {
204 t.Parallel()
205
206 a := newFake("first")
207 b := newFake("second")
208 a.complete("picked")
209
210 s := New([]Screen{a, b}, defaultStyles())
211 s.cursor = 1
212
213 result, _ := s.Update(escMsg())
214 result, _ = result.(Session).Update(BackMsg{})
215 sess := result.(Session)
216
217 screen := sess.screens[0].(*fakeScreen)
218 if screen.Selection() != "picked" {
219 t.Errorf("selection = %q, want %q after back navigation", screen.Selection(), "picked")
220 }
221}
222
223func TestBackNavDoesNotReAdvance(t *testing.T) {
224 t.Parallel()
225
226 a := newFake("first")
227 b := newFake("second")
228 a.complete("picked")
229
230 s := New([]Screen{a, b}, defaultStyles())
231 s.cursor = 1
232
233 // Navigate back to the first screen.
234 result, _ := s.Update(escMsg())
235 result, _ = result.(Session).Update(BackMsg{})
236 sess := result.(Session)
237
238 if sess.cursor != 0 {
239 t.Fatalf("cursor = %d, want 0 after back nav", sess.cursor)
240 }
241
242 // Send an unrelated message (window resize). The session should
243 // NOT re-advance because advancement only happens via DoneMsg,
244 // not by polling Selection().
245 result, _ = sess.Update(resizeMsg())
246 sess = result.(Session)
247
248 if sess.cursor != 0 {
249 t.Errorf("cursor = %d after resize on completed screen, want 0 (should not re-advance)", sess.cursor)
250 }
251}
252
253func TestScreenAdvancesOnDoneMsg(t *testing.T) {
254 t.Parallel()
255
256 a := newSelecting("first")
257 b := newFake("second")
258
259 s := New([]Screen{a, b}, defaultStyles())
260 s.Init()
261
262 // Send a key to trigger selection on the first screen. The
263 // selectingScreen returns DoneCmd, which produces DoneMsg.
264 result, cmd := s.Update(enterMsg())
265 // The cmd contains DoneCmd; execute it to produce DoneMsg.
266 if cmd == nil {
267 t.Fatal("expected DoneCmd from selecting screen")
268 }
269 // Simulate the runtime delivering the DoneMsg.
270 result, _ = result.(Session).Update(DoneMsg{})
271 sess := result.(Session)
272
273 if sess.cursor != 1 {
274 t.Errorf("cursor = %d, want 1 after DoneMsg", sess.cursor)
275 }
276 if b.initCalls != 1 {
277 t.Errorf("second screen Init called %d times, want 1", b.initCalls)
278 }
279}
280
281func TestReAdvanceAfterBackAndNewSelection(t *testing.T) {
282 t.Parallel()
283
284 a := newSelecting("first")
285 b := newFake("second")
286
287 s := New([]Screen{a, b}, defaultStyles())
288 s.Init()
289
290 // Advance past the first screen.
291 result, _ := s.Update(enterMsg())
292 result, _ = result.(Session).Update(DoneMsg{})
293 sess := result.(Session)
294
295 if sess.cursor != 1 {
296 t.Fatalf("cursor = %d, want 1 after first advance", sess.cursor)
297 }
298
299 // Navigate back.
300 result, _ = sess.Update(escMsg())
301 result, _ = result.(Session).Update(BackMsg{})
302 sess = result.(Session)
303
304 if sess.cursor != 0 {
305 t.Fatalf("cursor = %d, want 0 after back nav", sess.cursor)
306 }
307
308 // Make a new selection. This should advance again.
309 result, _ = sess.Update(enterMsg())
310 result, _ = result.(Session).Update(DoneMsg{})
311 sess = result.(Session)
312
313 if sess.cursor != 1 {
314 t.Errorf("cursor = %d, want 1 after re-selection", sess.cursor)
315 }
316}
317
318func TestEscConsumedByScreen(t *testing.T) {
319 t.Parallel()
320
321 a := newFake("first")
322 b := newEscConsuming("picker")
323 a.complete("restore")
324
325 s := New([]Screen{a, b}, defaultStyles())
326 s.cursor = 1
327
328 // Esc is forwarded to the screen, which handles it internally
329 // and does NOT return BackCmd.
330 result, _ := s.Update(escMsg())
331 sess := result.(Session)
332
333 // Should still be on the same screen.
334 if sess.cursor != 1 {
335 t.Errorf("cursor = %d, want 1 (screen consumed Esc)", sess.cursor)
336 }
337
338 screen := sess.screens[1].(*escConsumingScreen)
339 if !screen.escHandled {
340 t.Error("screen should have handled Esc internally")
341 }
342}
343
344func TestBreadcrumbEmpty(t *testing.T) {
345 t.Parallel()
346
347 s := New([]Screen{newFake("one")}, defaultStyles())
348 crumb := s.breadcrumb()
349
350 if crumb != "" {
351 t.Errorf("breadcrumb on first screen = %q, want empty", crumb)
352 }
353}
354
355func TestBreadcrumbShowsCompletedSelections(t *testing.T) {
356 t.Parallel()
357
358 a := newFake("cmd")
359 b := newFake("preset")
360 c := newFake("details")
361
362 a.complete("restore")
363 b.complete("home@cloud")
364
365 s := New([]Screen{a, b, c}, defaultStyles())
366 s.cursor = 2
367
368 crumb := s.breadcrumb()
369 if !strings.Contains(crumb, "restore") {
370 t.Errorf("breadcrumb missing 'restore': %q", crumb)
371 }
372 if !strings.Contains(crumb, "home@cloud") {
373 t.Errorf("breadcrumb missing 'home@cloud': %q", crumb)
374 }
375}
376
377func TestBreadcrumbUpdatesOnBack(t *testing.T) {
378 t.Parallel()
379
380 a := newFake("cmd")
381 b := newFake("preset")
382 c := newFake("details")
383
384 a.complete("restore")
385 b.complete("home@cloud")
386
387 s := New([]Screen{a, b, c}, defaultStyles())
388 s.cursor = 2
389
390 result, _ := s.Update(escMsg())
391 result, _ = result.(Session).Update(BackMsg{})
392 sess := result.(Session)
393
394 crumb := sess.breadcrumb()
395 if !strings.Contains(crumb, "restore") {
396 t.Errorf("breadcrumb after back missing 'restore': %q", crumb)
397 }
398 // On screen b now — b's selection should not be in the crumb
399 // because the breadcrumb only shows screens before the current one.
400 if strings.Contains(crumb, "home@cloud") {
401 t.Errorf("breadcrumb after back should not contain current screen's selection: %q", crumb)
402 }
403}
404
405func TestViewIncludesChrome(t *testing.T) {
406 t.Parallel()
407
408 a := newFake("first")
409 a.complete("restore")
410 b := newFake("Second Step")
411
412 s := New([]Screen{a, b}, defaultStyles())
413 s.cursor = 1
414 s.width = 80
415 s.height = 24
416
417 view := s.View()
418
419 if !strings.Contains(view.Content, "Second Step") {
420 t.Error("view should contain the screen title")
421 }
422 if !strings.Contains(view.Content, "restore") {
423 t.Error("view should contain the breadcrumb")
424 }
425 if !strings.Contains(view.Content, "esc") {
426 t.Error("view should contain the help bar with esc binding")
427 }
428}
429
430func TestViewEmptyScreens(t *testing.T) {
431 t.Parallel()
432
433 s := New([]Screen{}, defaultStyles())
434 view := s.View()
435 if view.Content != "" {
436 t.Errorf("view with no screens should be empty, got %q", view.Content)
437 }
438}
439
440func TestBackNavReInitsScreen(t *testing.T) {
441 t.Parallel()
442
443 a := newFake("first")
444 b := newFake("second")
445 a.complete("cmd")
446
447 s := New([]Screen{a, b}, defaultStyles())
448 s.cursor = 1
449
450 result, _ := s.Update(escMsg())
451 result.(Session).Update(BackMsg{})
452
453 // After navigating back, the first screen should be re-inited.
454 if a.initCalls != 1 {
455 t.Errorf("first screen Init called %d times after back nav, want 1", a.initCalls)
456 }
457}
458
459func TestDoneOnLastScreenCompletes(t *testing.T) {
460 t.Parallel()
461
462 a := newSelecting("only")
463 s := New([]Screen{a}, defaultStyles())
464 s.Init()
465
466 // Select on the only screen — DoneMsg should complete the session.
467 result, _ := s.Update(enterMsg())
468 result, cmd := result.(Session).Update(DoneMsg{})
469 sess := result.(Session)
470
471 if !sess.done {
472 t.Error("session should be done after DoneMsg on last screen")
473 }
474 if !sess.Completed() {
475 t.Error("session should be completed (not cancelled)")
476 }
477 if cmd == nil {
478 t.Error("expected tea.Quit command")
479 }
480}
481
482func TestCtrlCIsNotCompleted(t *testing.T) {
483 t.Parallel()
484
485 s := New([]Screen{newFake("one")}, defaultStyles())
486 s.Init()
487
488 result, _ := s.Update(ctrlCMsg())
489 sess := result.(Session)
490
491 if !sess.done {
492 t.Error("session should be done after Ctrl+C")
493 }
494 if sess.Completed() {
495 t.Error("session should NOT be completed after cancellation")
496 }
497}
498
499func TestEscExitIsNotCompleted(t *testing.T) {
500 t.Parallel()
501
502 s := New([]Screen{newFake("one")}, defaultStyles())
503 s.Init()
504
505 result, _ := s.Update(escMsg())
506 result, _ = result.(Session).Update(BackMsg{})
507 sess := result.(Session)
508
509 if !sess.done {
510 t.Error("session should be done after Esc on first screen")
511 }
512 if sess.Completed() {
513 t.Error("session should NOT be completed after Esc exit")
514 }
515}
516
517func TestWindowSizeAdjustedForChrome(t *testing.T) {
518 t.Parallel()
519
520 var receivedHeight int
521 screen := &sizeCaptureScreen{
522 fakeScreen: fakeScreen{title: "test"},
523 onSize: func(h int) { receivedHeight = h },
524 }
525
526 s := New([]Screen{screen}, defaultStyles())
527 s.Init()
528
529 s.Update(tea.WindowSizeMsg{Width: 80, Height: 30})
530
531 wantHeight := 30 - chromeLines
532 if receivedHeight != wantHeight {
533 t.Errorf("screen received height %d, want %d (terminal height minus chrome)", receivedHeight, wantHeight)
534 }
535}
536
537// themeAwareScreen holds a *theme.Styles pointer, as real screen
538// implementations will. This lets us verify that the session's theme
539// update propagates through the shared pointer.
540type themeAwareScreen struct {
541 fakeScreen
542 styles *theme.Styles
543}
544
545// bgCapturingScreen records whether it received a BackgroundColorMsg.
546type bgCapturingScreen struct {
547 fakeScreen
548 onBg func()
549}
550
551func (b *bgCapturingScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
552 if _, ok := msg.(tea.BackgroundColorMsg); ok {
553 if b.onBg != nil {
554 b.onBg()
555 }
556 return b, nil
557 }
558 return b.fakeScreen.Update(msg)
559}
560
561func newThemeAware(title string, styles *theme.Styles) *themeAwareScreen {
562 return &themeAwareScreen{
563 fakeScreen: fakeScreen{title: title},
564 styles: styles,
565 }
566}
567
568func TestThemeUpdatePropagesToScreens(t *testing.T) {
569 t.Parallel()
570
571 // Caller owns the styles allocation and passes the same pointer
572 // to both screen constructors and the session.
573 styles := defaultStyles()
574 screen := newThemeAware("test", styles)
575 s := New([]Screen{screen}, styles)
576
577 if !screen.styles.Dark {
578 t.Fatal("precondition: styles should default to dark")
579 }
580
581 // Simulate a light terminal background detection.
582 result, _ := s.Update(tea.BackgroundColorMsg{Color: color.White})
583 _ = result.(Session)
584
585 // The screen's pointer should now see the updated (light) styles
586 // because the session mutated through the shared pointer.
587 if screen.styles.Dark {
588 t.Error("screen still sees dark styles after light BackgroundColorMsg; session replaced pointer instead of mutating in place")
589 }
590}
591
592func TestBackgroundColorForwardedToScreen(t *testing.T) {
593 t.Parallel()
594
595 // Screens that cache themed state need to see BackgroundColorMsg
596 // so they can rebuild. Verify the session forwards it.
597 var received bool
598 screen := &bgCapturingScreen{
599 fakeScreen: fakeScreen{title: "test"},
600 onBg: func() { received = true },
601 }
602
603 styles := defaultStyles()
604 s := New([]Screen{screen}, styles)
605 s.Init()
606
607 s.Update(tea.BackgroundColorMsg{Color: color.White})
608
609 if !received {
610 t.Error("BackgroundColorMsg was not forwarded to the active screen")
611 }
612}
613
614func TestExtendAppendsScreens(t *testing.T) {
615 t.Parallel()
616
617 a := newSelecting("first")
618 s := New([]Screen{a}, defaultStyles())
619 s.Init()
620
621 // Extend the session with two more screens before the current
622 // screen completes.
623 b := newFake("second")
624 c := newFake("third")
625 result, _ := s.Update(ExtendMsg{Screens: []Screen{b, c}})
626 sess := result.(Session)
627
628 if len(sess.screens) != 3 {
629 t.Fatalf("screens = %d, want 3 after extend", len(sess.screens))
630 }
631
632 // The cursor should still be on the first screen.
633 if sess.cursor != 0 {
634 t.Errorf("cursor = %d, want 0 (extend should not advance)", sess.cursor)
635 }
636}
637
638func TestExtendedScreensReachableViaAdvance(t *testing.T) {
639 t.Parallel()
640
641 a := newSelecting("first")
642 s := New([]Screen{a}, defaultStyles())
643 s.Init()
644
645 // Extend before completing the first screen.
646 b := newFake("second")
647 result, _ := s.Update(ExtendMsg{Screens: []Screen{b}})
648 sess := result.(Session)
649
650 // Complete the first screen. Without extend, this would finish
651 // the session. With extend, it should advance to b.
652 result, _ = sess.Update(enterMsg())
653 result, _ = result.(Session).Update(DoneMsg{})
654 sess = result.(Session)
655
656 if sess.cursor != 1 {
657 t.Errorf("cursor = %d, want 1 after advance to extended screen", sess.cursor)
658 }
659 if sess.done {
660 t.Error("session should not be done — extended screen is still pending")
661 }
662}
663
664func TestExtendedScreensGetSizeOnActivation(t *testing.T) {
665 t.Parallel()
666
667 var receivedHeight int
668 a := newSelecting("first")
669 b := &sizeCaptureScreen{
670 fakeScreen: fakeScreen{title: "extended"},
671 onSize: func(h int) { receivedHeight = h },
672 }
673
674 s := New([]Screen{a}, defaultStyles())
675 s.Init()
676
677 // Set terminal size, then extend, then advance.
678 result, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 30})
679 sess := result.(Session)
680
681 result, _ = sess.Update(ExtendMsg{Screens: []Screen{b}})
682 sess = result.(Session)
683
684 result, _ = sess.Update(enterMsg())
685 result.(Session).Update(DoneMsg{})
686
687 wantHeight := 30 - chromeLines
688 if receivedHeight != wantHeight {
689 t.Errorf("extended screen received height %d, want %d", receivedHeight, wantHeight)
690 }
691}
692
693func TestExtendEmptySliceIsNoop(t *testing.T) {
694 t.Parallel()
695
696 a := newFake("only")
697 s := New([]Screen{a}, defaultStyles())
698 s.Init()
699
700 result, _ := s.Update(ExtendMsg{Screens: nil})
701 sess := result.(Session)
702
703 if len(sess.screens) != 1 {
704 t.Errorf("screens = %d, want 1 after empty extend", len(sess.screens))
705 }
706}
707
708func TestExtendBackNavThroughExtendedScreens(t *testing.T) {
709 t.Parallel()
710
711 a := newSelecting("first")
712 b := newFake("extended")
713
714 s := New([]Screen{a}, defaultStyles())
715 s.Init()
716
717 // Extend, advance, then back-nav.
718 result, _ := s.Update(ExtendMsg{Screens: []Screen{b}})
719 sess := result.(Session)
720
721 // Advance past first screen.
722 result, _ = sess.Update(enterMsg())
723 result, _ = result.(Session).Update(DoneMsg{})
724 sess = result.(Session)
725
726 if sess.cursor != 1 {
727 t.Fatalf("cursor = %d, want 1 before back nav", sess.cursor)
728 }
729
730 // Back nav should return to the first screen.
731 result, _ = sess.Update(escMsg())
732 result, _ = result.(Session).Update(BackMsg{})
733 sess = result.(Session)
734
735 if sess.cursor != 0 {
736 t.Errorf("cursor = %d, want 0 after back through extended screen", sess.cursor)
737 }
738}
739
740func TestExtendTruncatesScreensBeyondCursor(t *testing.T) {
741 t.Parallel()
742
743 // If the user backs up and a new ExtendMsg arrives, screens after
744 // the cursor should be replaced by the new ones (the old future
745 // is no longer valid).
746 a := newSelecting("cmd")
747 b := newFake("old-future")
748
749 s := New([]Screen{a, b}, defaultStyles())
750 s.Init()
751
752 // Advance to b, then back to a.
753 result, _ := s.Update(enterMsg())
754 result, _ = result.(Session).Update(DoneMsg{})
755 sess := result.(Session)
756
757 result, _ = sess.Update(escMsg())
758 result, _ = result.(Session).Update(BackMsg{})
759 sess = result.(Session)
760
761 // Now extend with new screens. The old b should be replaced.
762 c := newFake("new-future-1")
763 d := newFake("new-future-2")
764 result, _ = sess.Update(ExtendMsg{Screens: []Screen{c, d}})
765 sess = result.(Session)
766
767 // Should have a + c + d = 3 screens, not a + b + c + d.
768 if len(sess.screens) != 3 {
769 t.Errorf("screens = %d, want 3 (old future should be replaced)", len(sess.screens))
770 }
771
772 // The screen at index 1 should be c, not b.
773 if sess.screens[1].Title() != "new-future-1" {
774 t.Errorf("screen[1].Title() = %q, want %q", sess.screens[1].Title(), "new-future-1")
775 }
776}
777
778func TestSizeReplayedOnAdvance(t *testing.T) {
779 t.Parallel()
780
781 var receivedHeight int
782 a := newSelecting("first")
783 b := &sizeCaptureScreen{
784 fakeScreen: fakeScreen{title: "second"},
785 onSize: func(h int) { receivedHeight = h },
786 }
787
788 s := New([]Screen{a, b}, defaultStyles())
789 s.Init()
790
791 // Set terminal size so the session caches it.
792 result, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 30})
793 sess := result.(Session)
794
795 // Advance to second screen. The session sends the adjusted size
796 // directly to the new screen during advance().
797 result, _ = sess.Update(enterMsg())
798 result.(Session).Update(DoneMsg{})
799
800 wantHeight := 30 - chromeLines
801 if receivedHeight != wantHeight {
802 t.Errorf("second screen received height %d after advance, want %d", receivedHeight, wantHeight)
803 }
804}