@@ -363,4 +363,70 @@ test.describe("Converter Registry", () => {
363363 const bounds = await listbox . boundingBox ( ) ;
364364 expect ( bounds ?. height ) . toBeGreaterThan ( 300 ) ;
365365 } ) ;
366+
367+ for ( const { name, viewport, deviceScaleFactor } of [
368+ { name : "wide short screen" , viewport : { width : 1920 , height : 600 } , deviceScaleFactor : 1 } ,
369+ { name : "scaled laptop" , viewport : { width : 1280 , height : 640 } , deviceScaleFactor : 1.5 } ,
370+ { name : "narrow screen" , viewport : { width : 390 , height : 640 } , deviceScaleFactor : 2 } ,
371+ ] ) {
372+ test . describe ( name , ( ) => {
373+ test . use ( { viewport, deviceScaleFactor } ) ;
374+
375+ test ( "keeps a long converter list vertical, stable, and scrollable" , async ( { page } ) => {
376+ await page . getByRole ( "button" , { name : "New Converter" } ) . click ( ) ;
377+ const trigger = page . getByRole ( "combobox" , { name : "Converter type" } ) ;
378+ await trigger . click ( ) ;
379+
380+ const listbox = page . getByRole ( "listbox" ) ;
381+ await expect ( listbox ) . toBeVisible ( ) ;
382+ const anchor = await trigger . evaluate ( ( button ) => {
383+ const { x, y, width, height } = button . parentElement ! . getBoundingClientRect ( ) ;
384+ return { x, y, width, height } ;
385+ } ) ;
386+ await expect . poll ( async ( ) => {
387+ const box = ( await listbox . boundingBox ( ) ) ! ;
388+ return Math . abs ( box . x - anchor . x ) ;
389+ } ) . toBeLessThanOrEqual ( 1 ) ;
390+
391+ // Sample successive frames: a single bounding box can miss position oscillation.
392+ const samples = await listbox . evaluate ( async ( element ) => {
393+ const bounds = [ ] ;
394+ for ( let frame = 0 ; frame < 60 ; frame ++ ) {
395+ await new Promise < void > ( ( resolve ) => requestAnimationFrame ( ( ) => resolve ( ) ) ) ;
396+ const { x, y, width, height } = element . getBoundingClientRect ( ) ;
397+ bounds . push ( { x, y, width, height } ) ;
398+ }
399+ return bounds ;
400+ } ) ;
401+
402+ for ( const box of samples ) {
403+ expect ( Math . abs ( box . x - anchor . x ) ) . toBeLessThanOrEqual ( 1 ) ;
404+ expect ( Math . abs ( box . width - anchor . width ) ) . toBeLessThanOrEqual ( 1 ) ;
405+ expect ( box . height ) . toBeGreaterThan ( 0 ) ;
406+ expect ( box . x ) . toBeGreaterThanOrEqual ( - 1 ) ;
407+ expect ( box . y ) . toBeGreaterThanOrEqual ( - 1 ) ;
408+ expect ( box . x + box . width ) . toBeLessThanOrEqual ( viewport . width + 1 ) ;
409+ expect ( box . y + box . height ) . toBeLessThanOrEqual ( viewport . height + 1 ) ;
410+ expect (
411+ box . y >= anchor . y + anchor . height - 1
412+ || box . y + box . height <= anchor . y + 1 ,
413+ ) . toBe ( true ) ;
414+ expect ( Math . abs ( box . y - samples [ 0 ] . y ) ) . toBeLessThanOrEqual ( 1 ) ;
415+ expect ( Math . abs ( box . height - samples [ 0 ] . height ) ) . toBeLessThanOrEqual ( 1 ) ;
416+ }
417+
418+ const scroll = await listbox . evaluate ( ( element ) => ( {
419+ height : element . clientHeight ,
420+ contentHeight : element . scrollHeight ,
421+ } ) ) ;
422+ expect ( scroll . contentHeight ) . toBeGreaterThan ( scroll . height ) ;
423+ const lastOption = listbox . getByRole ( "option" ) . last ( ) ;
424+ await lastOption . scrollIntoViewIfNeeded ( ) ;
425+ await expect ( lastOption ) . toBeInViewport ( ) ;
426+ await lastOption . click ( ) ;
427+ await expect ( listbox ) . toBeHidden ( ) ;
428+ await expect ( trigger ) . toHaveText ( "ViewportConverter9" ) ;
429+ } ) ;
430+ } ) ;
431+ }
366432} ) ;
0 commit comments