@@ -1066,9 +1066,21 @@ async function touchPinballControl(page, cdp, button, selector, control, touchId
10661066 return released ;
10671067}
10681068
1069- async function exerciseMobileClippyAndPinball ( browser , baseUrl ) {
1069+ const PINBALL_BOARD_WIDTH = 520 ;
1070+ const PINBALL_BOARD_HEIGHT = 700 ;
1071+ const PINBALL_RATIO_TOLERANCE = 0.005 ;
1072+ const PINBALL_MOBILE_VIEWPORTS = [
1073+ { width : 375 , height : 600 } ,
1074+ { width : 390 , height : 664 } ,
1075+ { width : 390 , height : 745 } ,
1076+ { width : 430 , height : 739 , exerciseTouch : true } ,
1077+ { width : 430 , height : 824 } ,
1078+ { width : 390 , height : 844 }
1079+ ] ;
1080+
1081+ async function auditMobilePinballViewport ( browser , baseUrl , viewportCase ) {
10701082 const context = await browser . newContext ( {
1071- viewport : { width : 390 , height : 844 } ,
1083+ viewport : { width : viewportCase . width , height : viewportCase . height } ,
10721084 isMobile : true ,
10731085 hasTouch : true ,
10741086 deviceScaleFactor : 2
@@ -1087,15 +1099,17 @@ async function exerciseMobileClippyAndPinball(browser, baseUrl) {
10871099 await page . waitForSelector ( '.desktop' , { state : 'visible' , timeout : 12000 } ) ;
10881100 await page . waitForFunction ( ( ) => Boolean ( window . zarateXP ?. appManager ?. windowManager ) , null , { timeout : 12000 } ) ;
10891101
1090- const trayClippy = page . locator ( '.tray-clippy-icon' ) ;
1091- ensure ( await trayClippy . count ( ) === 1 && await trayClippy . isHidden ( ) , 'El icono de Clippy en la bandeja no quedo oculto en un dispositivo movil real' ) ;
1102+ if ( viewportCase . exerciseTouch ) {
1103+ const trayClippy = page . locator ( '.tray-clippy-icon' ) ;
1104+ ensure ( await trayClippy . count ( ) === 1 && await trayClippy . isHidden ( ) , 'El icono de Clippy en la bandeja no quedo oculto en un dispositivo movil real' ) ;
10921105
1093- await page . evaluate ( ( ) => window . zarateXP . clippyManager . showWelcome ( ) ) ;
1094- await page . waitForTimeout ( 650 ) ;
1095- ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'showWelcome creo un host de Clippy en movil' ) ;
1096- await page . evaluate ( ( ) => window . zarateXP . clippyManager . showTip ( 0 ) ) ;
1097- await page . waitForTimeout ( 650 ) ;
1098- ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'showTip creo un host de Clippy en movil' ) ;
1106+ await page . evaluate ( ( ) => window . zarateXP . clippyManager . showWelcome ( ) ) ;
1107+ await page . waitForTimeout ( 650 ) ;
1108+ ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'showWelcome creo un host de Clippy en movil' ) ;
1109+ await page . evaluate ( ( ) => window . zarateXP . clippyManager . showTip ( 0 ) ) ;
1110+ await page . waitForTimeout ( 650 ) ;
1111+ ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'showTip creo un host de Clippy en movil' ) ;
1112+ }
10991113
11001114 const appWindow = await openApp ( page , 'pinball' ) ;
11011115 const root = appWindow . locator ( '[data-pinball-root]' ) ;
@@ -1109,79 +1123,179 @@ async function exerciseMobileClippyAndPinball(browser, baseUrl) {
11091123 } , null , { timeout : 12000 } ) ;
11101124 await waitForWindowAnimation ( page , 'pinball' ) ;
11111125
1112- const mobileLayout = await appWindow . evaluate ( ( windowNode ) => {
1126+ const mobileLayout = await appWindow . evaluate ( ( windowNode , board ) => {
11131127 const rootNode = windowNode . querySelector ( '[data-pinball-root]' ) ;
11141128 const windowBody = windowNode . querySelector ( '.window-body' ) ;
1129+ const tableWrap = rootNode ?. querySelector ( '.xp-pinball-table-wrap' ) ;
1130+ const canvasStage = rootNode ?. querySelector ( '.xp-pinball-canvas-stage' ) ;
11151131 const canvasNode = rootNode ?. querySelector ( '[data-pinball-canvas]' ) ;
11161132 const pad = rootNode ?. querySelector ( '.xp-pinball-pad' ) ;
1133+ const app = rootNode ?. _pinballApp || rootNode ?. closest ( '.window' ) ?. _pinballApp ;
11171134 const requiredButtons = [
11181135 rootNode ?. querySelector ( '[data-pinball-left]' ) ,
11191136 rootNode ?. querySelector ( '[data-pinball-plunger]' ) ,
11201137 rootNode ?. querySelector ( '[data-pinball-right]' )
11211138 ] ;
1122- if ( ! rootNode || ! windowBody || ! canvasNode || ! pad || requiredButtons . some ( ( button ) => ! button ) ) {
1139+ if ( ! rootNode || ! windowBody || ! tableWrap || ! canvasStage || ! canvasNode || ! pad || ! app || requiredButtons . some ( ( button ) => ! button ) ) {
11231140 return { complete : false } ;
11241141 }
11251142
11261143 const bodyRect = windowBody . getBoundingClientRect ( ) ;
1144+ const wrapRect = tableWrap . getBoundingClientRect ( ) ;
1145+ const stageRect = canvasStage . getBoundingClientRect ( ) ;
11271146 const canvasRect = canvasNode . getBoundingClientRect ( ) ;
11281147 const padRect = pad . getBoundingClientRect ( ) ;
11291148 const taskbarTop = document . querySelector ( '.taskbar' ) ?. getBoundingClientRect ( ) . top ?? window . innerHeight ;
1130- const usableBottom = Math . min ( window . innerHeight , taskbarTop ) ;
1131- const inViewport = ( rect ) => rect . width > 0 && rect . height > 0
1132- && rect . left >= - 1 && rect . right <= window . innerWidth + 1
1133- && rect . top >= - 1 && rect . bottom <= usableBottom + 1 ;
1134- const inWindowBody = ( rect ) => rect . left >= bodyRect . left - 1
1135- && rect . right <= bodyRect . right + 1
1136- && rect . top >= bodyRect . top - 1
1137- && rect . bottom <= bodyRect . bottom + 1 ;
1149+ const visualViewportRect = {
1150+ left : window . visualViewport ?. offsetLeft ?? 0 ,
1151+ top : window . visualViewport ?. offsetTop ?? 0 ,
1152+ right : ( window . visualViewport ?. offsetLeft ?? 0 ) + ( window . visualViewport ?. width ?? window . innerWidth ) ,
1153+ bottom : ( window . visualViewport ?. offsetTop ?? 0 ) + ( window . visualViewport ?. height ?? window . innerHeight )
1154+ } ;
1155+ const usableViewportRect = {
1156+ ...visualViewportRect ,
1157+ bottom : Math . min ( visualViewportRect . bottom , taskbarTop )
1158+ } ;
1159+ const contains = ( outer , inner , tolerance = 1 ) => inner . width > 0 && inner . height > 0
1160+ && inner . left >= outer . left - tolerance
1161+ && inner . right <= outer . right + tolerance
1162+ && inner . top >= outer . top - tolerance
1163+ && inner . bottom <= outer . bottom + tolerance ;
1164+ const clipRect = {
1165+ left : Math . max ( stageRect . left , wrapRect . left , bodyRect . left , usableViewportRect . left ) ,
1166+ top : Math . max ( stageRect . top , wrapRect . top , bodyRect . top , usableViewportRect . top ) ,
1167+ right : Math . min ( stageRect . right , wrapRect . right , bodyRect . right , usableViewportRect . right ) ,
1168+ bottom : Math . min ( stageRect . bottom , wrapRect . bottom , bodyRect . bottom , usableViewportRect . bottom )
1169+ } ;
1170+
1171+ const canvasStyle = getComputedStyle ( canvasNode ) ;
1172+ const borderLeft = Number . parseFloat ( canvasStyle . borderLeftWidth ) || 0 ;
1173+ const borderRight = Number . parseFloat ( canvasStyle . borderRightWidth ) || 0 ;
1174+ const borderTop = Number . parseFloat ( canvasStyle . borderTopWidth ) || 0 ;
1175+ const borderBottom = Number . parseFloat ( canvasStyle . borderBottomWidth ) || 0 ;
1176+ const canvasContentRect = {
1177+ left : canvasRect . left + borderLeft ,
1178+ top : canvasRect . top + borderTop ,
1179+ right : canvasRect . right - borderRight ,
1180+ bottom : canvasRect . bottom - borderBottom ,
1181+ width : canvasRect . width - borderLeft - borderRight ,
1182+ height : canvasRect . height - borderTop - borderBottom
1183+ } ;
1184+ const intrinsicRatio = canvasNode . width / canvasNode . height ;
1185+ const renderedRatio = canvasContentRect . width / canvasContentRect . height ;
1186+ const ratioError = Math . abs ( ( renderedRatio / intrinsicRatio ) - 1 ) ;
1187+ const visibleContentBottom = Math . min ( canvasContentRect . bottom , clipRect . bottom ) ;
1188+ const logicalVisibleBottom = Math . max ( 0 , Math . min (
1189+ board . height ,
1190+ ( ( visibleContentBottom - canvasContentRect . top ) / canvasContentRect . height ) * board . height
1191+ ) ) ;
1192+ const logicalToPixelRect = ( bounds ) => ( {
1193+ left : canvasContentRect . left + ( bounds . left / board . width ) * canvasContentRect . width ,
1194+ right : canvasContentRect . left + ( bounds . right / board . width ) * canvasContentRect . width ,
1195+ top : canvasContentRect . top + ( bounds . top / board . height ) * canvasContentRect . height ,
1196+ bottom : canvasContentRect . top + ( bounds . bottom / board . height ) * canvasContentRect . height ,
1197+ width : ( ( bounds . right - bounds . left ) / board . width ) * canvasContentRect . width ,
1198+ height : ( ( bounds . bottom - bounds . top ) / board . height ) * canvasContentRect . height
1199+ } ) ;
1200+ const flipperBounds = [ 'left' , 'right' ] . map ( ( side ) => {
1201+ const segment = app . flipperSegment ( side ) ;
1202+ return logicalToPixelRect ( {
1203+ left : Math . min ( segment . a . x , segment . b . x ) - segment . thickness ,
1204+ right : Math . max ( segment . a . x , segment . b . x ) + segment . thickness ,
1205+ top : Math . min ( segment . a . y , segment . b . y ) - segment . thickness ,
1206+ bottom : Math . max ( segment . a . y , segment . b . y ) + segment . thickness
1207+ } ) ;
1208+ } ) ;
11381209 const scrollingElement = document . scrollingElement ;
11391210 const scrollSurfaces = [ scrollingElement , windowBody , rootNode ] . filter ( Boolean ) ;
1211+ const containment = {
1212+ stage : contains ( stageRect , canvasRect ) ,
1213+ wrap : contains ( wrapRect , canvasRect ) ,
1214+ body : contains ( bodyRect , canvasRect ) ,
1215+ visualViewport : contains ( usableViewportRect , canvasRect )
1216+ } ;
11401217
11411218 return {
11421219 complete : true ,
1143- viewport : { width : window . innerWidth , height : window . innerHeight , usableBottom } ,
1220+ viewport : {
1221+ width : window . innerWidth ,
1222+ height : window . innerHeight ,
1223+ visualWidth : window . visualViewport ?. width ?? window . innerWidth ,
1224+ visualHeight : window . visualViewport ?. height ?? window . innerHeight ,
1225+ usableBottom : usableViewportRect . bottom
1226+ } ,
11441227 hasTouch : navigator . maxTouchPoints > 0 && matchMedia ( '(pointer: coarse)' ) . matches ,
11451228 buttonCount : pad . querySelectorAll ( 'button' ) . length ,
1146- padVisible : getComputedStyle ( pad ) . display !== 'none' && inViewport ( padRect ) && inWindowBody ( padRect ) ,
1147- canvasVisible : getComputedStyle ( canvasNode ) . display !== 'none' && inViewport ( canvasRect ) && inWindowBody ( canvasRect ) ,
1229+ padVisible : getComputedStyle ( pad ) . display !== 'none'
1230+ && contains ( wrapRect , padRect )
1231+ && contains ( bodyRect , padRect )
1232+ && contains ( usableViewportRect , padRect ) ,
1233+ canvasContained : Object . values ( containment ) . every ( Boolean ) ,
1234+ containment,
1235+ ratioError,
1236+ ratioWithinTolerance : Number . isFinite ( ratioError ) && ratioError <= board . ratioTolerance ,
1237+ logicalVisibleBottom,
1238+ bottomEdgeVisible : logicalVisibleBottom >= board . height - 1 ,
1239+ flippersComplete : flipperBounds . every ( ( bounds ) => contains ( clipRect , bounds ) ) ,
11481240 targetsLargeEnough : requiredButtons . every ( ( button ) => {
11491241 const rect = button . getBoundingClientRect ( ) ;
11501242 return rect . width >= 43.5 && rect . height >= 43.5 ;
11511243 } ) ,
11521244 noVerticalScroll : scrollSurfaces . every ( ( surface ) => surface . scrollHeight <= surface . clientHeight + 1 && Math . abs ( surface . scrollTop ) <= 1 ) ,
11531245 canvas : { top : canvasRect . top , bottom : canvasRect . bottom , width : canvasRect . width , height : canvasRect . height } ,
1246+ canvasContent : canvasContentRect ,
1247+ clip : clipRect ,
11541248 pad : { top : padRect . top , bottom : padRect . bottom , width : padRect . width , height : padRect . height } ,
11551249 body : { top : bodyRect . top , bottom : bodyRect . bottom , scrollHeight : windowBody . scrollHeight , clientHeight : windowBody . clientHeight } ,
11561250 root : { scrollHeight : rootNode . scrollHeight , clientHeight : rootNode . clientHeight } ,
1251+ flippers : flipperBounds ,
11571252 buttons : requiredButtons . map ( ( button ) => {
11581253 const rect = button . getBoundingClientRect ( ) ;
11591254 return { width : rect . width , height : rect . height } ;
11601255 } )
11611256 } ;
1257+ } , {
1258+ width : PINBALL_BOARD_WIDTH ,
1259+ height : PINBALL_BOARD_HEIGHT ,
1260+ ratioTolerance : PINBALL_RATIO_TOLERANCE
11621261 } ) ;
11631262
1164- ensure ( mobileLayout . complete && mobileLayout . hasTouch , `El contexto movil no expuso touch real (${ JSON . stringify ( mobileLayout ) } )` ) ;
1165- ensure ( mobileLayout . buttonCount === 3 && mobileLayout . targetsLargeEnough , `El dock de Pinball no expuso tres controles de al menos 44 px (${ JSON . stringify ( mobileLayout ) } )` ) ;
1166- ensure ( mobileLayout . padVisible && mobileLayout . canvasVisible && mobileLayout . noVerticalScroll , `Canvas y dock de Pinball no quedaron visibles simultaneamente sin scroll vertical (${ JSON . stringify ( mobileLayout ) } )` ) ;
1167-
1168- const cdp = await context . newCDPSession ( page ) ;
1169- await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-left]' ) , '[data-pinball-left]' , 'left' , 11 ) ;
1170- await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-right]' ) , '[data-pinball-right]' , 'right' , 12 ) ;
1171- await root . evaluate ( ( rootNode ) => {
1172- const app = rootNode . _pinballApp || rootNode . closest ( '.window' ) ?. _pinballApp ;
1173- app . resetGame ( { announce : false } ) ;
1174- } ) ;
1175- const plungerReleased = await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-plunger]' ) , '[data-pinball-plunger]' , 'plunger' , 13 , 160 ) ;
1176- ensure ( plungerReleased . gameState === 'playing' && plungerReleased . launchPower >= 0.3 && plungerReleased . ballInLauncherLane , `El lanzador no disparo la bola al soltar el control tactil (${ JSON . stringify ( plungerReleased ) } )` ) ;
1177- ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'Clippy reaparecio durante la sesion movil' ) ;
1263+ const caseLabel = `${ viewportCase . width } x${ viewportCase . height } ` ;
1264+ ensure ( mobileLayout . complete && mobileLayout . hasTouch , `El contexto movil ${ caseLabel } no expuso touch real (${ JSON . stringify ( mobileLayout ) } )` ) ;
1265+ ensure ( mobileLayout . buttonCount === 3 && mobileLayout . targetsLargeEnough , `El dock de Pinball no expuso tres controles de al menos 44 px en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1266+ ensure ( mobileLayout . padVisible && mobileLayout . canvasContained , `Canvas o dock de Pinball quedaron recortados en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1267+ ensure ( mobileLayout . ratioWithinTolerance , `El canvas de Pinball deformo la relacion 520:700 en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1268+ ensure ( mobileLayout . logicalVisibleBottom >= PINBALL_BOARD_HEIGHT - 1 && mobileLayout . bottomEdgeVisible , `El borde inferior del tablero no quedo visible en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1269+ ensure ( mobileLayout . flippersComplete , `Los flippers no quedaron completamente visibles en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1270+ ensure ( mobileLayout . noVerticalScroll , `Pinball genero scroll vertical en ${ caseLabel } (${ JSON . stringify ( mobileLayout ) } )` ) ;
1271+
1272+ if ( viewportCase . exerciseTouch ) {
1273+ const cdp = await context . newCDPSession ( page ) ;
1274+ await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-left]' ) , '[data-pinball-left]' , 'left' , 11 ) ;
1275+ await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-right]' ) , '[data-pinball-right]' , 'right' , 12 ) ;
1276+ await root . evaluate ( ( rootNode ) => {
1277+ const app = rootNode . _pinballApp || rootNode . closest ( '.window' ) ?. _pinballApp ;
1278+ app . resetGame ( { announce : false } ) ;
1279+ } ) ;
1280+ const plungerReleased = await touchPinballControl ( page , cdp , appWindow . locator ( '[data-pinball-plunger]' ) , '[data-pinball-plunger]' , 'plunger' , 13 , 160 ) ;
1281+ ensure ( plungerReleased . gameState === 'playing' && plungerReleased . launchPower >= 0.3 && plungerReleased . ballInLauncherLane , `El lanzador no disparo la bola al soltar el control tactil (${ JSON . stringify ( plungerReleased ) } )` ) ;
1282+ ensure ( await page . locator ( 'clippy-character' ) . count ( ) === 0 , 'Clippy reaparecio durante la sesion movil' ) ;
1283+ }
11781284
1179- return 'Movil real: Clippy deshabilitado y Pinball con dock tactil, touch press/release y lanzamiento' ;
1285+ return caseLabel ;
11801286 } finally {
11811287 await context . close ( ) ;
11821288 }
11831289}
11841290
1291+ async function exerciseMobileClippyAndPinball ( browser , baseUrl ) {
1292+ const auditedViewports = [ ] ;
1293+ for ( const viewportCase of PINBALL_MOBILE_VIEWPORTS ) {
1294+ auditedViewports . push ( await auditMobilePinballViewport ( browser , baseUrl , viewportCase ) ) ;
1295+ }
1296+ return `Movil real: Clippy deshabilitado y Pinball completo en ${ auditedViewports . join ( ', ' ) } , con touch press/release y lanzamiento` ;
1297+ }
1298+
11851299async function main ( ) {
11861300 const server = await createStaticServer ( ) ;
11871301 const { port } = server . address ( ) ;
0 commit comments