@@ -2,6 +2,7 @@ import { Topics } from "@coder-studio/core";
22import { act , fireEvent , render , screen , waitFor , within } from "@testing-library/react" ;
33import userEvent from "@testing-library/user-event" ;
44import { createStore , Provider } from "jotai" ;
5+ import { type ReactNode , useState } from "react" ;
56import { MemoryRouter } from "react-router-dom" ;
67import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
78import { localeAtom } from "../../../atoms/app-ui" ;
@@ -602,7 +603,7 @@ describe("TerminalPanel", () => {
602603 expect ( consoleSpy ) . not . toHaveBeenCalled ( ) ;
603604 } ) ;
604605
605- it ( "renders a lighter mobile fullscreen terminal toolbar when requested by the mobile sheet " , async ( ) => {
606+ it ( "leaves the mobile fullscreen title slot blank when no terminal exists " , async ( ) => {
606607 const store = createStore ( ) ;
607608 const subscribe = vi . fn ( ( topics : string [ ] , handler : EventHandler ) => {
608609 handlers . push ( handler ) ;
@@ -629,14 +630,90 @@ describe("TerminalPanel", () => {
629630 store . set ( bottomPanelHeightAtom , 240 ) ;
630631 setEnglishLocale ( store ) ;
631632 store . set ( wsClientAtom , { subscribe, sendCommand } as never ) ;
632- store . set ( terminalMetaAtomFamily ( "term_1" ) , {
633- id : "term_1" ,
634- workspaceId : "ws-test" ,
635- kind : "shell" ,
636- alive : true ,
637- title : "Workspace Shell" ,
633+ render (
634+ < Provider store = { store } >
635+ < MemoryRouter >
636+ < div className = "mobile-terminal-sheet mobile-terminal-sheet--fullscreen" >
637+ < TerminalPanel chrome = "mobile-fullscreen" />
638+ </ div >
639+ </ MemoryRouter >
640+ </ Provider >
641+ ) ;
642+
643+ expect (
644+ document . querySelector ( ".bottom-terminal--mobile-fullscreen .terminal-toolbar" )
645+ ) . toBeTruthy ( ) ;
646+ expect (
647+ document . querySelector ( ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row" )
648+ ) . toBeTruthy ( ) ;
649+ expect (
650+ document . querySelector (
651+ ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder"
652+ )
653+ ) . toBeTruthy ( ) ;
654+ expect ( screen . queryByRole ( "button" , { name : "Close Terminal" } ) ) . not . toBeInTheDocument ( ) ;
655+ expect ( screen . getAllByRole ( "button" , { name : "New Terminal" } ) . length ) . toBeGreaterThan ( 0 ) ;
656+ } ) ;
657+
658+ it ( "renders the mobile fullscreen terminal toolbar as a single compact chrome row" , async ( ) => {
659+ const store = createStore ( ) ;
660+ const subscribe = vi . fn ( ( topics : string [ ] , handler : EventHandler ) => {
661+ handlers . push ( handler ) ;
662+ return ( ) => {
663+ handlers = handlers . filter ( ( candidate ) => candidate !== handler ) ;
664+ } ;
665+ } ) ;
666+ const sendCommand = vi . fn ( ) . mockImplementation ( ( op : string ) => {
667+ if ( op === "terminal.list" ) {
668+ return Promise . resolve ( [
669+ {
670+ id : "term_1" ,
671+ workspaceId : "ws-test" ,
672+ kind : "shell" ,
673+ title : "Workspace Shell" ,
674+ cwd : "/tmp/ws-test" ,
675+ argv : [ "/bin/bash" ] ,
676+ cols : 120 ,
677+ rows : 30 ,
678+ alive : true ,
679+ createdAt : 1 ,
680+ } ,
681+ {
682+ id : "term_2" ,
683+ workspaceId : "ws-test" ,
684+ kind : "shell" ,
685+ title : "Workspace Shell 2" ,
686+ cwd : "/tmp/ws-test" ,
687+ argv : [ "/bin/bash" ] ,
688+ cols : 120 ,
689+ rows : 30 ,
690+ alive : true ,
691+ createdAt : 2 ,
692+ } ,
693+ ] ) ;
694+ }
695+
696+ return Promise . resolve ( [ ] ) ;
638697 } ) ;
639698
699+ seedReadyWorkspaceState ( store , {
700+ "ws-test" : {
701+ id : "ws-test" ,
702+ path : "/tmp/ws-test" ,
703+ targetRuntime : "native" ,
704+ openedAt : 1 ,
705+ lastActiveAt : 1 ,
706+ uiState : {
707+ leftPanelWidth : 280 ,
708+ bottomPanelHeight : 200 ,
709+ focusMode : false ,
710+ } ,
711+ } ,
712+ } ) ;
713+ store . set ( bottomPanelHeightAtom , 240 ) ;
714+ setEnglishLocale ( store ) ;
715+ store . set ( wsClientAtom , { subscribe, sendCommand } as never ) ;
716+
640717 render (
641718 < Provider store = { store } >
642719 < MemoryRouter >
@@ -647,11 +724,216 @@ describe("TerminalPanel", () => {
647724 </ Provider >
648725 ) ;
649726
650- expect ( screen . queryByText ( "TERMINAL" ) ) . not . toBeInTheDocument ( ) ;
651- expect ( screen . queryByText ( "Workspace Shell" ) ) . not . toBeInTheDocument ( ) ;
727+ await waitFor ( ( ) => {
728+ expect ( screen . getByTestId ( "xterm-host" ) ) . toHaveTextContent ( "term_1" ) ;
729+ } ) ;
730+
731+ const toolbar = document . querySelector ( ".bottom-terminal--mobile-fullscreen .terminal-toolbar" ) ;
732+ const mobileRow = document . querySelector (
733+ ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-row"
734+ ) ;
735+ const selector = document . querySelector (
736+ ".bottom-terminal--mobile-fullscreen .terminal-selector"
737+ ) ;
738+ const actions = document . querySelector (
739+ ".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions"
740+ ) ;
741+
742+ expect ( toolbar ) . toBeTruthy ( ) ;
743+ expect ( mobileRow ) . toBeTruthy ( ) ;
744+ expect ( toolbar ?. querySelector ( ".terminal-toolbar-left" ) ) . toBeNull ( ) ;
745+ expect ( toolbar ?. querySelector ( ".terminal-toolbar-right" ) ) . toBeNull ( ) ;
746+ expect ( selector ) . toBeTruthy ( ) ;
747+ expect ( actions ) . toBeTruthy ( ) ;
748+ } ) ;
749+
750+ it ( "renders a static mobile fullscreen title when only one terminal exists" , async ( ) => {
751+ const store = createStore ( ) ;
752+ const subscribe = vi . fn ( ( topics : string [ ] , handler : EventHandler ) => {
753+ handlers . push ( handler ) ;
754+ return ( ) => {
755+ handlers = handlers . filter ( ( candidate ) => candidate !== handler ) ;
756+ } ;
757+ } ) ;
758+ const sendCommand = vi . fn ( ) . mockImplementation ( ( op : string ) => {
759+ if ( op === "terminal.list" ) {
760+ return Promise . resolve ( [
761+ {
762+ id : "term_1" ,
763+ workspaceId : "ws-test" ,
764+ kind : "shell" ,
765+ title : "Workspace Shell" ,
766+ cwd : "/tmp/ws-test" ,
767+ argv : [ "/bin/bash" ] ,
768+ cols : 120 ,
769+ rows : 30 ,
770+ alive : true ,
771+ createdAt : 1 ,
772+ } ,
773+ ] ) ;
774+ }
775+
776+ return Promise . resolve ( [ ] ) ;
777+ } ) ;
778+
779+ seedReadyWorkspaceState ( store , {
780+ "ws-test" : {
781+ id : "ws-test" ,
782+ path : "/tmp/ws-test" ,
783+ targetRuntime : "native" ,
784+ openedAt : 1 ,
785+ lastActiveAt : 1 ,
786+ uiState : {
787+ leftPanelWidth : 280 ,
788+ bottomPanelHeight : 200 ,
789+ focusMode : false ,
790+ } ,
791+ } ,
792+ } ) ;
793+ store . set ( bottomPanelHeightAtom , 240 ) ;
794+ setEnglishLocale ( store ) ;
795+ store . set ( wsClientAtom , { subscribe, sendCommand } as never ) ;
796+
797+ render (
798+ < Provider store = { store } >
799+ < MemoryRouter >
800+ < div className = "mobile-terminal-sheet mobile-terminal-sheet--fullscreen" >
801+ < TerminalPanel chrome = "mobile-fullscreen" />
802+ </ div >
803+ </ MemoryRouter >
804+ </ Provider >
805+ ) ;
806+
807+ await waitFor ( ( ) => {
808+ expect ( screen . getByTestId ( "xterm-host" ) ) . toHaveTextContent ( "term_1" ) ;
809+ } ) ;
810+
811+ expect ( screen . getByText ( "Workspace Shell" ) ) . toBeInTheDocument ( ) ;
812+ expect (
813+ document . querySelector ( ".bottom-terminal--mobile-fullscreen .terminal-selector-btn--static" )
814+ ) . toBeTruthy ( ) ;
815+ expect (
816+ document . querySelector (
817+ ".bottom-terminal--mobile-fullscreen .terminal-toolbar-mobile-placeholder"
818+ )
819+ ) . toBeNull ( ) ;
820+ expect ( screen . getByRole ( "button" , { name : "Close Terminal" } ) ) . toBeInTheDocument ( ) ;
652821 expect ( screen . getAllByRole ( "button" , { name : "New Terminal" } ) . length ) . toBeGreaterThan ( 0 ) ;
653822 } ) ;
654823
824+ it ( "keeps mobile fullscreen terminal actions together in the toolbar when header actions are available" , async ( ) => {
825+ const store = createStore ( ) ;
826+ const subscribe = vi . fn ( ( topics : string [ ] , handler : EventHandler ) => {
827+ handlers . push ( handler ) ;
828+ return ( ) => {
829+ handlers = handlers . filter ( ( candidate ) => candidate !== handler ) ;
830+ } ;
831+ } ) ;
832+ const sendCommand = vi . fn ( ) . mockImplementation ( ( op : string ) => {
833+ if ( op === "terminal.list" ) {
834+ return Promise . resolve ( [
835+ {
836+ id : "term_1" ,
837+ workspaceId : "ws-test" ,
838+ kind : "shell" ,
839+ title : "Workspace Shell" ,
840+ cwd : "/tmp/ws-test" ,
841+ argv : [ "/bin/bash" ] ,
842+ cols : 120 ,
843+ rows : 30 ,
844+ alive : true ,
845+ createdAt : 1 ,
846+ } ,
847+ {
848+ id : "term_2" ,
849+ workspaceId : "ws-test" ,
850+ kind : "shell" ,
851+ title : "Workspace Shell 2" ,
852+ cwd : "/tmp/ws-test" ,
853+ argv : [ "/bin/bash" ] ,
854+ cols : 120 ,
855+ rows : 30 ,
856+ alive : true ,
857+ createdAt : 2 ,
858+ } ,
859+ ] ) ;
860+ }
861+
862+ return Promise . resolve ( [ ] ) ;
863+ } ) ;
864+
865+ seedReadyWorkspaceState ( store , {
866+ "ws-test" : {
867+ id : "ws-test" ,
868+ path : "/tmp/ws-test" ,
869+ targetRuntime : "native" ,
870+ openedAt : 1 ,
871+ lastActiveAt : 1 ,
872+ uiState : {
873+ leftPanelWidth : 280 ,
874+ bottomPanelHeight : 200 ,
875+ focusMode : false ,
876+ } ,
877+ } ,
878+ } ) ;
879+ store . set ( bottomPanelHeightAtom , 240 ) ;
880+ setEnglishLocale ( store ) ;
881+ store . set ( wsClientAtom , { subscribe, sendCommand } as never ) ;
882+
883+ function HeaderActionHarness ( ) {
884+ const [ headerActions , setHeaderActions ] = useState < ReactNode > ( null ) ;
885+
886+ return (
887+ < >
888+ < div data-testid = "terminal-header-actions" > { headerActions } </ div >
889+ < div className = "mobile-terminal-sheet mobile-terminal-sheet--fullscreen" >
890+ < TerminalPanel
891+ chrome = "mobile-fullscreen"
892+ onMobileHeaderActionsChange = { setHeaderActions }
893+ />
894+ </ div >
895+ </ >
896+ ) ;
897+ }
898+
899+ render (
900+ < Provider store = { store } >
901+ < MemoryRouter >
902+ < HeaderActionHarness />
903+ </ MemoryRouter >
904+ </ Provider >
905+ ) ;
906+
907+ await waitFor ( ( ) => {
908+ expect ( screen . getByTestId ( "xterm-host" ) ) . toHaveTextContent ( "term_1" ) ;
909+ } ) ;
910+
911+ const headerActions = screen . getByTestId ( "terminal-header-actions" ) ;
912+ const toolbar = document . querySelector (
913+ ".bottom-terminal--mobile-fullscreen .terminal-toolbar"
914+ ) as HTMLElement | null ;
915+
916+ expect (
917+ within ( headerActions ) . queryByRole ( "button" , { name : "New Terminal" } )
918+ ) . not . toBeInTheDocument ( ) ;
919+ expect (
920+ within ( headerActions ) . queryByRole ( "button" , { name : "Close Terminal" } )
921+ ) . not . toBeInTheDocument ( ) ;
922+ expect ( toolbar ) . toBeTruthy ( ) ;
923+ expect (
924+ within ( toolbar as HTMLElement ) . getByRole ( "button" , { name : "Close Terminal" } )
925+ ) . toBeInTheDocument ( ) ;
926+ expect (
927+ within ( toolbar as HTMLElement ) . getByRole ( "button" , { name : "New Terminal" } )
928+ ) . toBeInTheDocument ( ) ;
929+ expect (
930+ document . querySelector ( ".bottom-terminal--mobile-fullscreen .terminal-selector" )
931+ ) . toBeTruthy ( ) ;
932+ expect (
933+ document . querySelectorAll ( ".bottom-terminal--mobile-fullscreen .terminal-toolbar-actions" )
934+ ) . toHaveLength ( 2 ) ;
935+ } ) ;
936+
655937 it ( "keeps only one terminal switcher in mobile fullscreen mode" , async ( ) => {
656938 const store = createStore ( ) ;
657939 const subscribe = vi . fn ( ( topics : string [ ] , handler : EventHandler ) => {
0 commit comments