@@ -34,8 +34,9 @@ function ui(page: Page) {
3434 const settings = page . getByTestId ( "settings-screen" )
3535 return {
3636 settings,
37- search : settings . getByRole ( "combobox" , { name : "Search settings " , exact : true } ) ,
37+ search : settings . getByRole ( "combobox" , { name : "Search" , exact : true } ) ,
3838 results : settings . getByRole ( "listbox" , { name : "Settings results" , exact : true } ) ,
39+ viewport : settings . locator ( ".settings-search-scroll > .scroll-view__viewport" ) ,
3940 }
4041}
4142
@@ -53,14 +54,14 @@ test.beforeEach(async ({ page }) => {
5354 if ( ( page . viewportSize ( ) ?. width ?? 1280 ) < 800 ) await page . getByRole ( "button" , { name : "Tabs" , exact : true } ) . click ( )
5455 await page . getByRole ( "button" , { name : "Settings" , exact : true } ) . click ( )
5556 const view = ui ( page )
56- await expect ( view . search ) . toBeFocused ( )
57+ await expect ( view . settings ) . toBeFocused ( )
5758 // Readiness includes the server-backed project inventory, not just the settings shell.
5859 await view . search . fill ( "OpenCode" )
5960 await expect ( view . results . getByRole ( "option" ) ) . toHaveCount ( 1 )
6061 await view . search . clear ( )
6162} )
6263
63- test ( "autofocus, pointer selection, keyboard navigation, and local find shortcut" , async ( { page } ) => {
64+ test ( "pointer selection, keyboard navigation, and local find shortcut" , async ( { page } ) => {
6465 const view = ui ( page )
6566 await view . search . fill ( "font" )
6667 const code = view . results . getByRole ( "option" , { name : "Code Font, Appearance" , exact : true } )
@@ -104,7 +105,7 @@ test("page priority, compact icons, section context, and the minimal empty state
104105 await view . search . fill ( "work" )
105106 await expect ( view . results . getByRole ( "option" ) ) . toHaveText ( [ "Worktrees" , "Default environmentPreferences / General" ] )
106107 const pageResult = view . results . getByRole ( "option" , { name : / ^ W o r k t r e e s , / } )
107- await expect ( pageResult ) . toHaveCSS ( "min- height" , "32px " )
108+ await expect ( pageResult ) . toHaveCSS ( "height" , "28px " )
108109 await expect ( pageResult . locator ( "svg" ) ) . toHaveCount ( 1 )
109110 await expect ( view . settings . getByText ( "App settings" , { exact : true } ) ) . toHaveCount ( 0 )
110111 await view . search . fill ( "Agent" )
@@ -116,13 +117,45 @@ test("page priority, compact icons, section context, and the minimal empty state
116117 )
117118 await view . search . fill ( "zzzzzzzzzz" )
118119 await expect ( view . results . getByRole ( "option" ) ) . toHaveCount ( 0 )
119- await expect ( view . settings . getByRole ( "status" ) ) . toHaveText ( " No setting found" )
120+ await expect ( view . settings . getByRole ( "status" ) ) . toHaveText ( ' No results for "zzzzzzzzzz"' )
120121 await expect ( view . settings . getByRole ( "heading" , { name : "Preferences" , exact : true } ) ) . toBeVisible ( )
121122 await view . search . press ( "Escape" )
122123 await expect ( view . search ) . toHaveValue ( "" )
123124 await expect ( view . settings . getByRole ( "tab" , { name : "Preferences" , exact : true } ) ) . toBeVisible ( )
124125} )
125126
127+ test ( "empty queries keep the closing quote beside the ellipsis while typing and resizing" , async ( { page } ) => {
128+ const view = ui ( page )
129+ const query = "zzzz 🧑🏽💻 " . repeat ( 40 )
130+ await view . search . fill ( query )
131+ const status = view . settings . getByRole ( "status" )
132+ const quoted = status . locator ( "bdi" )
133+ await expect ( status ) . toHaveAccessibleName ( `No results for "${ query } "` )
134+ await expect ( quoted ) . toHaveText ( / ^ " .+ … " $ / )
135+ await expect ( status ) . toHaveCSS ( "height" , "28px" )
136+ await expect
137+ . poll ( ( ) =>
138+ quoted . evaluate ( ( element ) => element . getBoundingClientRect ( ) . width <= element . parentElement ! . clientWidth ) ,
139+ )
140+ . toBe ( true )
141+
142+ const text = await quoted . textContent ( )
143+ await page . setViewportSize ( { width : 390 , height : 844 } )
144+ await expect ( quoted ) . toHaveText ( / ^ " .+ … " $ / )
145+ await expect ( quoted ) . not . toHaveText ( text ! )
146+ await expect
147+ . poll ( ( ) =>
148+ quoted . evaluate ( ( element ) => element . getBoundingClientRect ( ) . width <= element . parentElement ! . clientWidth ) ,
149+ )
150+ . toBe ( true )
151+ expect ( await view . settings . evaluate ( ( root ) => root . scrollWidth <= root . clientWidth ) ) . toBe ( true )
152+
153+ await view . search . fill ( "zzzzzzzzzz" )
154+ await expect ( status ) . toHaveText ( 'No results for "zzzzzzzzzz"' )
155+ await view . search . pressSequentially ( "x" )
156+ await expect ( status ) . toHaveText ( 'No results for "zzzzzzzzzzx"' )
157+ } )
158+
126159test ( "Models and Shortcuts autofocus their filters on normal navigation" , async ( { page } ) => {
127160 const view = ui ( page )
128161 for ( const entry of [
@@ -214,8 +247,8 @@ test("qualified project results preserve query and selection on return", async (
214247 await expect ( view . results . locator ( '[data-setting-target="settings-project-name"]' ) ) . toHaveCount ( 0 )
215248 await view . search . fill ( "OpenCode" )
216249 const project = view . results . getByRole ( "option" )
217- await expect ( project ) . toHaveText ( "OOpenCodeProjects " )
218- await expect ( project . locator ( '[data-component="project-avatar-v2"]' ) ) . toHaveCSS ( "width" , "14px " )
250+ await expect ( project ) . toHaveText ( "OOpenCode " )
251+ await expect ( project . locator ( '[data-component="project-avatar-v2"]' ) ) . toHaveCSS ( "width" , "16px " )
219252 await expect ( view . settings . locator ( ".settings-search-group" ) ) . toHaveCount ( 0 )
220253 await view . search . fill ( "OpenCode name" )
221254 const name = view . results . getByRole ( "option" )
@@ -241,16 +274,29 @@ test("returning from a project restores a scrolled result list", async ({ page }
241274 const view = ui ( page )
242275 await view . search . fill ( "OpenCode" )
243276 await expect ( view . results . getByRole ( "option" ) ) . toHaveCount ( 30 )
277+ const scrollbar = view . settings . locator ( '.settings-search-scroll > .scroll-view__thumb[data-orientation="vertical"]' )
278+ await view . viewport . hover ( )
279+ await expect ( scrollbar ) . toHaveAttribute ( "data-visible" , "true" )
280+ const bounds = ( await scrollbar . boundingBox ( ) ) !
281+ await page . mouse . move ( bounds . x + bounds . width / 2 , bounds . y + bounds . height / 2 )
282+ await page . mouse . down ( )
283+ await expect ( scrollbar ) . toHaveAttribute ( "data-dragging" , "true" )
284+ await page . mouse . move ( bounds . x + bounds . width / 2 , bounds . y + bounds . height / 2 + 80 , { steps : 4 } )
285+ await page . mouse . up ( )
286+ await expect . poll ( ( ) => view . viewport . evaluate ( ( list ) => list . scrollTop ) ) . toBeGreaterThan ( 0 )
244287 const project = view . results . getByRole ( "option" , { name : / ^ O p e n C o d e 2 5 , / } )
245288 await project . scrollIntoViewIfNeeded ( )
246- await expect . poll ( ( ) => view . results . evaluate ( ( list ) => list . scrollTop ) ) . toBeGreaterThan ( 0 )
247- const scroll = await view . results . evaluate ( ( list ) => list . scrollTop )
289+ await expect . poll ( ( ) => view . viewport . evaluate ( ( list ) => list . scrollTop ) ) . toBeGreaterThan ( 0 )
290+ const scroll = await view . viewport . evaluate ( ( list ) => list . scrollTop )
248291 await project . click ( )
249292 await expect ( view . settings . getByRole ( "heading" , { name : "OpenCode 25" , exact : true } ) ) . toBeVisible ( )
250293 await view . settings . getByRole ( "button" , { name : "Back to settings" , exact : true } ) . click ( )
251294 await expect ( view . search ) . toBeFocused ( )
252295 await expect ( project ) . toHaveAttribute ( "aria-selected" , "true" )
253- await expect . poll ( ( ) => view . results . evaluate ( ( list ) => list . scrollTop ) ) . toBe ( scroll )
296+ await expect . poll ( ( ) => view . viewport . evaluate ( ( list ) => list . scrollTop ) ) . toBe ( scroll )
297+ await view . search . fill ( "about" )
298+ await expect ( view . results . getByRole ( "option" , { name : "About" , exact : true } ) ) . toBeVisible ( )
299+ await expect ( scrollbar ) . toHaveCount ( 0 )
254300} )
255301
256302test ( "IME confirmation does not activate a search result" , async ( { page } ) => {
@@ -361,6 +407,44 @@ for (const direction of ["ltr", "rtl"] as const) {
361407 colorScheme : direction === "rtl" ? "dark" : "light" ,
362408 contextOptions : { reducedMotion : "reduce" } ,
363409 } )
410+ test ( "search input fades track typing, caret scrolling, resizing, and clearing" , async ( { page } ) => {
411+ const view = ui ( page )
412+ await page . setViewportSize ( { width : 1280 , height : 900 } )
413+ await page . evaluate ( ( direction ) => {
414+ document . documentElement . dir = direction
415+ } , direction )
416+ await view . search . fill ( direction === "rtl" ? "غيرموجود " . repeat ( 30 ) : "zzzz " . repeat ( 30 ) )
417+ await view . search . press ( "End" )
418+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-start" , "true" )
419+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-end" , "false" )
420+ await expect ( view . search ) . not . toHaveCSS ( "mask-image" , "none" )
421+
422+ await view . search . press ( "Home" )
423+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-start" , "false" )
424+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-end" , "true" )
425+ await view . search . evaluate ( ( input : HTMLInputElement , direction ) => {
426+ input . scrollLeft = ( ( input . scrollWidth - input . clientWidth ) / 2 ) * ( direction === "rtl" ? - 1 : 1 )
427+ } , direction )
428+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-start" , "true" )
429+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-end" , "true" )
430+
431+ await view . search . fill ( "z" . repeat ( 50 ) )
432+ await expect ( view . search ) . not . toHaveCSS ( "mask-image" , "none" )
433+ await page . setViewportSize ( { width : 600 , height : 844 } )
434+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-start" , "false" )
435+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-end" , "false" )
436+ await expect ( view . search ) . toHaveCSS ( "mask-image" , "none" )
437+
438+ await view . search . fill ( "zzzz " . repeat ( 30 ) )
439+ await expect ( view . search ) . not . toHaveCSS ( "mask-image" , "none" )
440+ await view . settings . getByRole ( "button" , { name : "Clear" , exact : true } ) . click ( )
441+ await expect ( view . search ) . toHaveValue ( "" )
442+ await expect ( view . search ) . toBeFocused ( )
443+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-start" , "false" )
444+ await expect ( view . search ) . toHaveAttribute ( "data-overflow-end" , "false" )
445+ await expect ( view . search ) . toHaveCSS ( "mask-image" , "none" )
446+ } )
447+
364448 test ( "keeps input/content stable and hides the active descendant when results collapse" , async ( { page } ) => {
365449 const view = ui ( page )
366450 await page . evaluate ( ( direction ) => {
0 commit comments