@@ -9,6 +9,7 @@ import { CONNECTOR_PROTOCOL } from "../apps/connector/src/version";
99import { authenticate , expect , ready , test } from "./room" ;
1010import { createChannel , seedChannel , titleChannel } from "./database" ;
1111import { RELEASE } from "./release" ;
12+ import { failedBuild } from "./build-seed" ;
1213
1314import type { Locator , Page } from "@playwright/test" ;
1415
@@ -911,6 +912,18 @@ for (let height of [1080, 969]) {
911912 await reachable ( page , menu , labels ) ;
912913 await shoot ( page , `creation-${ height } -${ repository } -build` ) ;
913914 await page . keyboard . press ( "Escape" ) ;
915+ let entry = sidebar . getByRole ( "button" , { name : / ^ M a c h i n e s / } ) ;
916+ await entry . click ( ) ;
917+ let defaults = page . getByRole ( "menu" ) . filter ( { hasText : "Default machine" } ) ;
918+ await defaults . getByRole ( "menuitemradio" ) . first ( ) . click ( { trial : true } ) ;
919+ await settledMenu ( defaults ) ;
920+ expect ( ( await defaults . boundingBox ( ) ) ! . y + ( await defaults . boundingBox ( ) ) ! . height )
921+ . toBeLessThanOrEqual ( ( await entry . boundingBox ( ) ) ! . y ) ;
922+ for ( let row of await defaults . locator ( '[role^="menuitem"]' ) . all ( ) ) {
923+ expect ( await shownIn ( row , defaults ) ) . toBe ( true ) ;
924+ await row . click ( { trial : true } ) ;
925+ }
926+ await page . keyboard . press ( "Escape" ) ;
914927 }
915928 } ,
916929 ) ;
@@ -1034,6 +1047,153 @@ test("twelve machines on a phone: the machine sheet scrolls to its last row and
10341047 . toHaveAccessibleName ( `Machine: ${ LONG_NAME } , connected` ) ;
10351048} ) ;
10361049
1050+ /** Wait for actual placement and entrance motion, not an arbitrary delay. */
1051+ async function settledMenu ( menu : Locator ) {
1052+ await menu . evaluate ( async element => {
1053+ await Promise . all (
1054+ element . getAnimations ( { subtree : true } ) . map ( animation => animation . finished ) ,
1055+ ) ;
1056+ await new Promise < void > ( resolve => {
1057+ let previous = "" ;
1058+ let stable = 0 ;
1059+ let measure = ( ) => {
1060+ let box = element . getBoundingClientRect ( ) ;
1061+ let current = `${ box . x } ,${ box . y } ,${ box . width } ,${ box . height } ` ;
1062+ stable = current === previous ? stable + 1 : 0 ;
1063+ previous = current ;
1064+ if ( stable === 3 ) resolve ( ) ;
1065+ else requestAnimationFrame ( measure ) ;
1066+ } ;
1067+ requestAnimationFrame ( measure ) ;
1068+ } ) ;
1069+ } ) ;
1070+ }
1071+
1072+ for (
1073+ let viewport of [
1074+ { width : 1862 , height : 1039 } ,
1075+ { width : 1920 , height : 1080 } ,
1076+ { width : 1280 , height : 800 } ,
1077+ { width : 1280 , height : 600 } ,
1078+ PHONE . viewport ,
1079+ ]
1080+ ) {
1081+ for ( let projects of [ 1 , 3 ] ) {
1082+ test ( `populated sidebar Default machine fits ${ viewport . width } x${ viewport . height } with ${ projects } projects` , async ( { seed, join : enter } ) => {
1083+ let title = `Sidebar ${ viewport . width } x${ viewport . height } ${ projects } ` ;
1084+ await seed ( `# ${ title } \n\nA document open beside Chat.\n` , {
1085+ graph : GRAPH ,
1086+ builds : [ failedBuild ( GRAPH ) ] ,
1087+ } ) ;
1088+ let phone = viewport . width === 390 ;
1089+ let page = await enter ( person ( ) , phone ? PHONE : { viewport } ) ;
1090+ await connected ( page , "devbox" ) ;
1091+ for ( let index = 0 ; index < projects ; index ++ ) {
1092+ let name = index === 0 ? "score" : `archive-${ index } ` ;
1093+ if ( index > 0 ) {
1094+ for ( let document of [ 1 , 2 ] ) {
1095+ let id = crypto . randomUUID ( ) ;
1096+ await createChannel ( Number ( new URL ( page . url ( ) ) . port ) , id , {
1097+ id : `R_archive_${ index } ` ,
1098+ owner : "octo-org" ,
1099+ name,
1100+ } ) ;
1101+ await seedChannel (
1102+ Number ( new URL ( page . url ( ) ) . port ) ,
1103+ id ,
1104+ `# ${ name } ${ document } \n\nContent.\n` ,
1105+ ) ;
1106+ }
1107+ }
1108+ let added = await page . request . post ( "/api/navigation/projects" , {
1109+ headers : { origin : new URL ( page . url ( ) ) . origin } ,
1110+ data : { owner : "octo-org" , repository : name } ,
1111+ } ) ;
1112+ expect ( added . ok ( ) , await added . text ( ) ) . toBe ( true ) ;
1113+ }
1114+ await page . reload ( ) ;
1115+ await page . getByRole ( "button" , { name : "Build" , exact : true } ) . click ( ) ;
1116+ if ( phone ) {
1117+ // Phones intentionally link to Machines from the documents sheet instead of a menu.
1118+ await page . getByRole ( "button" , { name : "Documents" , exact : true } ) . tap ( ) ;
1119+ let link = page . getByRole ( "link" , { name : / ^ M a c h i n e s / } ) ;
1120+ await link . scrollIntoViewIfNeeded ( ) ;
1121+ await expect ( link ) . toBeInViewport ( { ratio : 1 } ) ;
1122+ await link . tap ( ) ;
1123+ await expect ( page ) . toHaveURL ( / \/ m a c h i n e s $ / ) ;
1124+ await expect ( page . getByRole ( "button" , { name : "Favorite devbox" , exact : true } ) )
1125+ . toBeVisible ( ) ;
1126+ return ;
1127+ }
1128+ let sidebar = page . getByRole ( "complementary" , { name : "Projects" } ) ;
1129+ await expect ( sidebar . getByRole ( "link" , { name : title , exact : true } ) ) . toBeVisible ( ) ;
1130+ let entry = sidebar . getByRole ( "button" , { name : / ^ M a c h i n e s / } ) ;
1131+ await expect ( entry ) . toBeInViewport ( { ratio : 1 } ) ;
1132+ await entry . click ( ) ;
1133+ let menu = page . getByRole ( "menu" ) . filter ( { hasText : "Default machine" } ) ;
1134+ await menu . getByRole ( "menuitemradio" ) . first ( ) . click ( { trial : true } ) ;
1135+ await settledMenu ( menu ) ;
1136+ if ( SHOTS ) {
1137+ await mkdir ( SHOTS , { recursive : true } ) ;
1138+ await page . screenshot ( {
1139+ path : `${ SHOTS } /sidebar-${ viewport . width } x${ viewport . height } -${ projects } .png` ,
1140+ } ) ;
1141+ }
1142+ let panel = ( await menu . boundingBox ( ) ) ! ;
1143+ let trigger = ( await entry . boundingBox ( ) ) ! ;
1144+ expect ( panel . y ) . toBeGreaterThanOrEqual ( 0 ) ;
1145+ expect ( panel . y + panel . height ) . toBeLessThanOrEqual ( trigger . y ) ;
1146+ for ( let row of await menu . locator ( '[role^="menuitem"]' ) . all ( ) ) {
1147+ expect ( await shownIn ( row , menu ) ) . toBe ( true ) ;
1148+ await row . click ( { trial : true } ) ;
1149+ }
1150+ } ) ;
1151+ }
1152+ }
1153+
1154+ test ( "the populated sidebar menu scrolls and refits while the viewport shrinks" , async ( { seed, join : enter } ) => {
1155+ test . setTimeout ( 90_000 ) ;
1156+ await seed ( "# Sidebar menu\n\n" + "A long document.\n\n" . repeat ( 80 ) , {
1157+ graph : GRAPH ,
1158+ builds : [ failedBuild ( GRAPH ) ] ,
1159+ } ) ;
1160+ let page = await enter ( person ( ) , { viewport : { width : 1862 , height : 1039 } } ) ;
1161+ await machines ( page , 30 ) ;
1162+ let added = await page . request . post ( "/api/navigation/projects" , {
1163+ headers : { origin : new URL ( page . url ( ) ) . origin } ,
1164+ data : { owner : "octo-org" , repository : "score" } ,
1165+ } ) ;
1166+ expect ( added . ok ( ) , await added . text ( ) ) . toBe ( true ) ;
1167+ await page . reload ( ) ;
1168+ await page . getByRole ( "button" , { name : "Build" , exact : true } ) . click ( ) ;
1169+ let sidebar = page . getByRole ( "complementary" , { name : "Projects" } ) ;
1170+ let entry = sidebar . getByRole ( "button" , { name : / ^ M a c h i n e s / } ) ;
1171+ await entry . click ( ) ;
1172+ let menu = page . getByRole ( "menu" ) . filter ( { hasText : "Default machine" } ) ;
1173+ await menu . getByRole ( "menuitemradio" ) . first ( ) . click ( { trial : true } ) ;
1174+ await page . keyboard . press ( "End" ) ;
1175+ let last = menu . getByRole ( "menuitem" , { name : "Manage machines" , exact : true } ) ;
1176+ await expect ( last ) . toBeFocused ( ) ;
1177+ expect ( await shownIn ( last , menu ) ) . toBe ( true ) ;
1178+ await page . setViewportSize ( { width : 1280 , height : 600 } ) ;
1179+ await settledMenu ( menu ) ;
1180+ if ( SHOTS ) {
1181+ await mkdir ( SHOTS , { recursive : true } ) ;
1182+ await page . screenshot ( { path : `${ SHOTS } /sidebar-default-machine-resize.png` } ) ;
1183+ }
1184+ expect ( await shownIn ( last , menu ) ) . toBe ( true ) ;
1185+ let panel = ( await menu . boundingBox ( ) ) ! ;
1186+ let trigger = ( await entry . boundingBox ( ) ) ! ;
1187+ expect ( trigger . y + trigger . height ) . toBeLessThanOrEqual ( 600 ) ;
1188+ expect ( panel . y ) . toBeGreaterThanOrEqual ( 0 ) ;
1189+ expect ( panel . y + panel . height ) . toBeLessThanOrEqual ( trigger . y ) ;
1190+ for ( let row of await menu . locator ( '[role^="menuitem"]' ) . all ( ) ) {
1191+ await row . scrollIntoViewIfNeeded ( ) ;
1192+ await row . click ( { trial : true } ) ;
1193+ expect ( await shownIn ( row , menu ) ) . toBe ( true ) ;
1194+ }
1195+ } ) ;
1196+
10371197for ( let width of [ 1280 , 600 , 390 ] ) {
10381198 test ( `the sidebar's default machine control is never clipped at ${ width } px` , async ( { join : enter } ) => {
10391199 let phone = width === 390 ;
0 commit comments