11import { base64Encode } from "@opencode-ai/core/util/encode"
2- import { expect , test } from "@playwright/test"
2+ import { expect , test , type Page } from "@playwright/test"
33import { mockOpenCodeServer } from "../utils/mock-server"
44import { expectSessionTitle } from "../utils/waits"
55
66const directory = "C:/OpenCode/TerminalComposerFocus"
77const projectID = "proj_terminal_composer_focus"
88const sessionID = "ses_terminal_composer_focus"
99const ptyID = "pty_terminal_composer_focus"
10+ const newPtyID = "pty_terminal_composer_focus_new"
1011
1112test . use ( { viewport : { width : 1440 , height : 900 } } )
1213
13- test ( "routes typing to the composer unless the open terminal is focused" , async ( { page } ) => {
14+ test . beforeEach ( async ( { page } ) => {
1415 await mockOpenCodeServer ( page , {
1516 directory,
1617 project : {
@@ -67,7 +68,9 @@ test("routes typing to the composer unless the open terminal is focused", async
6768 await page . addInitScript ( ( ) => {
6869 localStorage . setItem ( "settings.v3" , JSON . stringify ( { general : { newLayoutDesigns : true } } ) )
6970 } )
71+ } )
7072
73+ test ( "routes typing to the composer unless the open terminal is focused" , async ( { page } ) => {
7174 await page . goto ( `/${ base64Encode ( directory ) } /session/${ sessionID } ` )
7275 await expectSessionTitle ( page , "Terminal composer focus" )
7376
@@ -87,3 +90,120 @@ test("routes typing to the composer unless the open terminal is focused", async
8790 await expect ( composer ) . toBeFocused ( )
8891 await expect ( composer ) . toHaveText ( "a" )
8992} )
93+
94+ test ( "keeps composer focus when a cached terminal finishes mounting" , async ( { page } ) => {
95+ const ghostty = Promise . withResolvers < void > ( )
96+ const release = Promise . withResolvers < void > ( )
97+ const created = { count : 0 }
98+ await page . route ( "**/pty" , ( route ) => {
99+ created . count += 1
100+ return route . fulfill ( {
101+ status : 200 ,
102+ contentType : "application/json" ,
103+ body : JSON . stringify ( { id : ptyID , title : "Terminal 1" } ) ,
104+ } )
105+ } )
106+ await page . route ( / g h o s t t y - w e b / , async ( route ) => {
107+ ghostty . resolve ( )
108+ await release . promise
109+ await route . continue ( )
110+ } )
111+ await seedCachedTerminal ( page )
112+
113+ await page . goto ( `/${ base64Encode ( directory ) } /session/${ sessionID } ` , { waitUntil : "commit" } )
114+ await expectSessionTitle ( page , "Terminal composer focus" )
115+
116+ const composer = page . locator ( '[data-component="prompt-input"]' )
117+ const terminal = page . locator ( '[data-component="terminal"]' )
118+ await expect ( terminal ) . toBeVisible ( )
119+ expect ( created . count ) . toBe ( 0 )
120+ await ghostty . promise
121+ await composer . click ( )
122+ await expect ( composer ) . toBeFocused ( )
123+
124+ release . resolve ( )
125+ await expect ( terminal . locator ( "textarea" ) ) . toHaveCount ( 1 )
126+ await page . waitForTimeout ( 300 )
127+ await expect ( composer ) . toBeFocused ( )
128+ } )
129+
130+ test ( "keeps newer composer focus while an explicit terminal open finishes" , async ( { page } ) => {
131+ const ghostty = Promise . withResolvers < void > ( )
132+ const release = Promise . withResolvers < void > ( )
133+ await page . route ( / g h o s t t y - w e b / , async ( route ) => {
134+ ghostty . resolve ( )
135+ await release . promise
136+ await route . continue ( )
137+ } )
138+
139+ await page . goto ( `/${ base64Encode ( directory ) } /session/${ sessionID } ` )
140+ await expectSessionTitle ( page , "Terminal composer focus" )
141+
142+ const composer = page . locator ( '[data-component="prompt-input"]' )
143+ const terminal = page . locator ( '[data-component="terminal"]' )
144+ await page . keyboard . press ( "Control+Backquote" )
145+ await expect ( terminal ) . toBeVisible ( )
146+ await ghostty . promise
147+ await composer . click ( )
148+ await expect ( composer ) . toBeFocused ( )
149+
150+ release . resolve ( )
151+ await expect ( terminal . locator ( "textarea" ) ) . toHaveCount ( 1 )
152+ await page . waitForTimeout ( 50 )
153+ await expect ( composer ) . toBeFocused ( )
154+ } )
155+
156+ test ( "focuses a terminal created from the new-terminal button" , async ( { page } ) => {
157+ const created = { count : 0 }
158+ await page . route ( "**/pty" , ( route ) => {
159+ created . count += 1
160+ const next = created . count === 1 ? { id : ptyID , title : "Terminal 1" } : { id : newPtyID , title : "Terminal 2" }
161+ return route . fulfill ( {
162+ status : 200 ,
163+ contentType : "application/json" ,
164+ body : JSON . stringify ( next ) ,
165+ } )
166+ } )
167+ await page . route ( `**/pty/${ newPtyID } ` , ( route ) =>
168+ route . fulfill ( { status : 200 , contentType : "application/json" , body : "{}" } ) ,
169+ )
170+ await page . route ( `**/pty/${ newPtyID } /connect-token*` , ( route ) =>
171+ route . fulfill ( {
172+ status : 200 ,
173+ contentType : "application/json" ,
174+ headers : { "access-control-allow-origin" : "*" } ,
175+ body : JSON . stringify ( { ticket : "e2e-ticket" } ) ,
176+ } ) ,
177+ )
178+ await page . routeWebSocket ( new RegExp ( `/pty/${ newPtyID } /connect` ) , ( ) => undefined )
179+
180+ await page . goto ( `/${ base64Encode ( directory ) } /session/${ sessionID } ` )
181+ await expectSessionTitle ( page , "Terminal composer focus" )
182+
183+ const composer = page . locator ( '[data-component="prompt-input"]' )
184+ const terminal = page . locator ( '[data-component="terminal"]' )
185+ await page . keyboard . press ( "Control+Backquote" )
186+ await expect ( terminal . locator ( "textarea" ) ) . toHaveCount ( 1 )
187+ await composer . click ( )
188+ await expect ( composer ) . toBeFocused ( )
189+
190+ await page . getByRole ( "button" , { name : "New terminal" } ) . click ( )
191+ await expect ( page . getByRole ( "tab" , { name : "Terminal 2" } ) ) . toHaveAttribute ( "aria-selected" , "true" )
192+ await expect . poll ( ( ) => terminal . evaluate ( ( element ) => element . contains ( document . activeElement ) ) ) . toBe ( true )
193+ } )
194+
195+ function seedCachedTerminal ( page : Page ) {
196+ return page . addInitScript (
197+ ( { terminalKey, ptyID } ) => {
198+ localStorage . setItem ( "opencode.global.dat:layout" , JSON . stringify ( { terminal : { height : 320 , opened : true } } ) )
199+ localStorage . setItem (
200+ terminalKey ,
201+ JSON . stringify ( {
202+ active : ptyID ,
203+ all : [ { id : ptyID , title : "Terminal 1" , titleNumber : 1 } ] ,
204+ } ) ,
205+ )
206+ } ,
207+ { terminalKey : `${ base64Encode ( directory ) } /terminal.v1` , ptyID } ,
208+ )
209+ }
0 commit comments