@@ -58,7 +58,7 @@ test("the new session screen does not show session details", async ({ page }) =>
5858 await expect ( page . getByRole ( "button" , { name : "Session details" , exact : true } ) ) . toHaveCount ( 0 )
5959} )
6060
61- test ( "the location shortcut keeps cycling after hovering its icon " , async ( { page } ) => {
61+ test ( "the location shortcuts choose worktrees from every state and keep cycling " , async ( { page } ) => {
6262 const existing = "C:/OpenCode/LocationShortcut/.worktrees/quiet-river"
6363
6464 const view = await openDraft ( page , {
@@ -67,27 +67,133 @@ test("the location shortcut keeps cycling after hovering its icon", async ({ pag
6767 } )
6868
6969 const modifier = await page . evaluate ( ( ) => ( / M a c / i. test ( navigator . platform ) ? "Meta" : "Control" ) )
70+
7071 const path = page . locator ( '[data-slot="new-session-location-path"]' )
71- const icon = page . locator ( '[data-slot="new-session-location-icon"]' )
7272
73- // The worktree trigger already says Local, so the local path has no icon of its own.
73+ const search = page . getByRole ( "combobox" , { name : "Search worktrees" , exact : true } )
74+
7475 await expect ( path ) . toHaveText ( view . directory )
75- await expect ( icon ) . toHaveCount ( 0 )
76- await newSessionWorktree ( page ) . click ( )
76+ await view . editor . press ( ` ${ modifier } +Alt+KeyO` )
77+ await expect ( search ) . toBeFocused ( )
7778 await page . getByRole ( "option" , { name : / ^ q u i e t - r i v e r / } ) . click ( )
7879 await expect ( view . editor ) . toBeFocused ( )
7980 await expect ( path ) . toHaveText ( existing )
80- await icon . hover ( )
81- await expect ( page . getByRole ( "tooltip" ) ) . toContainText ( "Toggle session location" )
82- await view . editor . press ( `${ modifier } +Alt+KeyL` )
83- await expect ( newSessionWorktree ( page ) ) . toHaveText ( "Local" )
84- await expect ( icon ) . toHaveCount ( 0 )
85- await expect ( view . editor ) . toBeFocused ( )
81+ await expect ( page . locator ( '[data-slot="new-session-location-icon"]' ) ) . toHaveCount ( 0 )
82+ await view . editor . press ( `${ modifier } +Alt+KeyO` )
83+ await expect ( search ) . toBeFocused ( )
84+ await page . getByRole ( "option" , { name : / L o c a t i o n S h o r t c u t C : \/ O p e n C o d e \/ L o c a t i o n S h o r t c u t $ / } ) . click ( )
8685 await view . editor . press ( `${ modifier } +Alt+KeyL` )
8786 await expect ( newSessionWorktree ( page ) ) . toContainText ( "New worktree" )
87+ await view . editor . press ( `${ modifier } +Alt+KeyO` )
88+ await expect ( search ) . toBeFocused ( )
89+ await page . getByRole ( "option" , { name : / ^ q u i e t - r i v e r / } ) . click ( )
90+ await expect ( path ) . toHaveText ( existing )
8891 await expect ( view . editor ) . toBeFocused ( )
8992} )
9093
94+ for ( const binding of [ "custom" , "none" ] ) {
95+ test ( `the picker tooltips reflect ${ binding } shortcuts` , async ( { page } ) => {
96+ const view = await openDraft ( page , {
97+ name : "PickerShortcuts" ,
98+ seed : {
99+ settings : {
100+ keybinds : {
101+ "project.select" : binding === "custom" ? "mod+alt+p" : "none" ,
102+ "session.location.select" : binding === "custom" ? "mod+alt+x" : "none" ,
103+ "session.location.cycle" : binding === "custom" ? "mod+alt+k" : "none" ,
104+ } ,
105+ } ,
106+ } ,
107+ } )
108+
109+ const tooltip = page . getByRole ( "tooltip" ) . and ( page . locator ( "[data-expanded]" ) )
110+
111+ const keys = tooltip . locator ( '[data-component="keybind-v2"]' )
112+
113+ await newSessionLocation ( page ) . hover ( )
114+ await expect ( tooltip ) . toContainText ( "Choose project" )
115+
116+ await expect ( keys ) . toHaveText ( binding === "custom" ? [ / P $ / ] : [ ] )
117+ await newSessionWorktree ( page ) . hover ( )
118+ await expect ( tooltip ) . toContainText ( "Isolate sessions with worktrees" )
119+
120+ await expect ( keys ) . toHaveText ( binding === "custom" ? [ / X $ / ] : [ ] )
121+
122+ const modifier = await page . evaluate ( ( ) => ( / M a c / i. test ( navigator . platform ) ? "Meta" : "Control" ) )
123+
124+ if ( binding === "none" ) {
125+ await view . editor . press ( `${ modifier } +Alt+o` )
126+ await expect ( page . getByRole ( "dialog" ) ) . toHaveCount ( 0 )
127+ await expect ( newSessionWorktree ( page ) ) . toHaveText ( "Local" )
128+
129+ return
130+ }
131+
132+ await view . editor . press ( `${ modifier } +Alt+k` )
133+ await expect ( newSessionWorktree ( page ) ) . toContainText ( "New worktree" )
134+ await view . editor . press ( `${ modifier } +Alt+x` )
135+ const search = page . getByRole ( "combobox" , { name : "Search worktrees" , exact : true } )
136+
137+ await expect ( search ) . toBeFocused ( )
138+ await search . press ( "Escape" )
139+ await expect ( page . getByRole ( "dialog" ) ) . toHaveCount ( 0 )
140+ await expect ( newSessionWorktree ( page ) ) . toContainText ( "New worktree" )
141+ await expect ( view . editor ) . toBeFocused ( )
142+ await view . editor . press ( `${ modifier } +Alt+p` )
143+ await expect ( page . getByRole ( "combobox" , { name : "Search projects" , exact : true } ) ) . toBeFocused ( )
144+ } )
145+ }
146+
147+ test ( "the worktree picker shortcut is disabled for non-Git projects" , async ( { page } ) => {
148+ const view = await openDraft ( page , { name : "NoWorktrees" , project : { vcs : undefined } } )
149+
150+ const modifier = await page . evaluate ( ( ) => ( / M a c / i. test ( navigator . platform ) ? "Meta" : "Control" ) )
151+
152+ await expect ( newSessionWorktree ( page ) ) . toHaveCount ( 0 )
153+ await view . editor . press ( `${ modifier } +Alt+o` )
154+ await expect ( page . getByRole ( "dialog" ) ) . toHaveCount ( 0 )
155+ await view . editor . fill ( "Keep working locally" )
156+ await expect ( view . editor ) . toHaveText ( "Keep working locally" )
157+ } )
158+
159+ test ( "picker scrollbars overlay full-width rows and support dragging" , async ( { page } ) => {
160+ const root = "C:/OpenCode/OverlayMenus"
161+
162+ await openDraft ( page , {
163+ name : "OverlayMenus" ,
164+ worktrees : ( ) => [
165+ { directory : root } ,
166+ ...Array . from ( { length : 24 } , ( _ , index ) => ( {
167+ directory : `${ root } /worktree-${ index } ` ,
168+ strategy : "git" as const ,
169+ } ) ) ,
170+ ] ,
171+ } )
172+ await newSessionWorktree ( page ) . click ( )
173+ await expect ( page . getByRole ( "combobox" , { name : "Search worktrees" , exact : true } ) ) . toBeFocused ( )
174+
175+ const menu = page . locator ( '[data-component="menu-scroll"]' )
176+
177+ const viewport = menu . locator ( '[data-slot="menu-scroll-viewport"]' )
178+
179+ const thumb = menu . locator ( '[data-slot="menu-scroll-thumb"]' )
180+
181+ await menu . hover ( )
182+ await expect ( thumb ) . toHaveCSS ( "opacity" , "1" )
183+ await expect . poll ( ( ) => viewport . evaluate ( ( el ) => el . getBoundingClientRect ( ) . width - el . clientWidth ) ) . toBe ( 0 )
184+
185+ const track = ( await menu . boundingBox ( ) ) !
186+
187+ const handle = ( await thumb . boundingBox ( ) ) !
188+
189+ await thumb . hover ( )
190+ await page . mouse . down ( )
191+ await page . mouse . move ( handle . x + handle . width / 2 , track . y + track . height - 4 )
192+ await page . mouse . up ( )
193+ await expect . poll ( ( ) => viewport . evaluate ( ( el ) => el . scrollTop > 0 ) ) . toBe ( true )
194+ await expect ( page . getByRole ( "option" , { name : / ^ w o r k t r e e - 2 3 / } ) ) . toBeInViewport ( )
195+ } )
196+
91197for ( const row of [
92198 {
93199 name : "POSIX" ,
@@ -108,6 +214,21 @@ for (const row of [
108214 await expect . poll ( ( ) => path . evaluate ( ( el ) => el . scrollWidth <= el . clientWidth ) ) . toBe ( true )
109215 await path . hover ( )
110216 await expect ( page . getByRole ( "tooltip" ) ) . toHaveText ( row . directory )
217+
218+ if ( row . name !== "POSIX" ) return
219+ await expect ( path ) . toHaveCSS ( "user-select" , "text" )
220+ await path . click ( )
221+ await path . evaluate ( ( element ) => {
222+ const range = document . createRange ( )
223+
224+ range . selectNodeContents ( element )
225+ window . getSelection ( ) ?. removeAllRanges ( )
226+ window . getSelection ( ) ?. addRange ( range )
227+ } )
228+ const modifier = await page . evaluate ( ( ) => ( / M a c / i. test ( navigator . platform ) ? "Meta" : "Control" ) )
229+
230+ await page . keyboard . press ( `${ modifier } +c` )
231+ await expect . poll ( ( ) => page . evaluate ( ( ) => navigator . clipboard . readText ( ) ) ) . toBe ( row . directory )
111232 } )
112233}
113234
@@ -234,11 +355,7 @@ test("the worktree trigger opens the current project's worktree step", async ({
234355 await local . click ( )
235356 await expect ( search ) . toBeFocused ( )
236357 await expect ( back ) . toHaveText ( "Run session in" )
237- await expect ( options . locator ( ".command-palette-title" ) ) . toHaveText ( [
238- "BranchWorktrees" ,
239- "New worktree" ,
240- "quiet-river" ,
241- ] )
358+ await expect ( options . locator ( ".command-palette-title" ) ) . toHaveText ( [ "BranchWorktrees" , "New worktree" , "quiet-river" ] )
242359 await expect ( options . nth ( 0 ) ) . toHaveAttribute ( "aria-selected" , "true" )
243360
244361 // Stepping back lands on the projects step with the drilled project active, as after drilling in with →.
@@ -247,15 +364,28 @@ test("the worktree trigger opens the current project's worktree step", async ({
247364 await expect ( projects ) . toBeFocused ( )
248365 await expect ( back ) . toHaveCount ( 0 )
249366 await expect ( options . filter ( { hasText : "BranchWorktrees" } ) ) . toHaveAttribute ( "aria-selected" , "true" )
367+ await projects . press ( "ArrowRight" )
368+ await expect ( search ) . toBeFocused ( )
369+ await search . press ( "Escape" )
370+ await expect ( projects ) . toBeFocused ( )
250371 await projects . press ( "Escape" )
251372 await expect ( dialog ) . toHaveCount ( 0 )
252373 await expect ( view . editor ) . toBeFocused ( )
253374
254- // Escape also steps back first, then closes.
375+ // A directly opened worktree picker closes on Escape without showing projects first .
255376 await local . focus ( )
256377 await page . keyboard . press ( "Enter" )
257378 await expect ( search ) . toBeFocused ( )
258379 await search . press ( "Escape" )
380+ await expect ( dialog ) . toHaveCount ( 0 )
381+ await expect ( view . editor ) . toBeFocused ( )
382+
383+ // Drilling in from the project picker still makes Escape step back.
384+ await newSessionLocation ( page ) . click ( )
385+ await expect ( projects ) . toBeFocused ( )
386+ await projects . press ( "ArrowRight" )
387+ await expect ( search ) . toBeFocused ( )
388+ await search . press ( "Escape" )
259389 await expect ( projects ) . toBeFocused ( )
260390 await projects . press ( "Escape" )
261391 await expect ( dialog ) . toHaveCount ( 0 )
@@ -277,15 +407,15 @@ test("the branch shortcut chooses a base branch and stays inactive on Local", as
277407
278408 const modifier = await page . evaluate ( ( ) => ( / M a c / i. test ( navigator . platform ) ? "Meta" : "Control" ) )
279409
280- const shortcut = `${ modifier } +Alt+Period `
410+ const shortcut = `${ modifier } +Alt+KeyB `
281411
282412 const base = page . getByRole ( "button" , { name : "from dev" , exact : true } )
283413
284414 const search = page . getByRole ( "combobox" , { name : "Search branches" , exact : true } )
285415
286416 await base . hover ( )
287417 await expect ( page . getByRole ( "tooltip" ) ) . toContainText ( "Choose base branch" )
288- await expect ( page . getByRole ( "tooltip" ) . locator ( '[data-component="keybind-v2"]' ) ) . toContainText ( ". " )
418+ await expect ( page . getByRole ( "tooltip" ) . locator ( '[data-component="keybind-v2"]' ) ) . toContainText ( "B " )
289419 await expect ( view . editor ) . toBeFocused ( )
290420 await view . editor . press ( shortcut )
291421 await expect ( search ) . toBeFocused ( )
0 commit comments