browser_touch_event_iframes.js (12464B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test simulated touch events can correctly target embedded iframes. 7 8 // These tests put a target iframe in a small embedding area, nested 9 // different ways. Then a simulated mouse click is made on top of the 10 // target iframe. If everything works, the translation done in 11 // touch-simulator.js should exactly match the translation done in the 12 // Platform code, such that the target is hit by the synthesized tap 13 // is at the expected location. 14 15 info("--- Starting viewport test output ---"); 16 17 info(`*** WARNING *** This test will move the mouse pointer to simulate 18 native mouse clicks. Do not move the mouse during this test or you may 19 cause intermittent failures.`); 20 21 // This test could run awhile, so request a 4x timeout duration. 22 requestLongerTimeout(4); 23 24 // The viewport will be square, set to VIEWPORT_DIMENSION on each axis. 25 const VIEWPORT_DIMENSION = 200; 26 27 const META_VIEWPORT_CONTENTS = ["width=device-width", "width=400"]; 28 29 const DPRS = [1, 2, 3]; 30 31 const URL_ROOT_2 = CHROME_URL_ROOT.replace( 32 "chrome://mochitests/content/", 33 "http://mochi.test:8888/" 34 ); 35 const IFRAME_PATHS = [`${URL_ROOT}`, `${URL_ROOT_2}`]; 36 37 const TESTS = [ 38 { 39 description: "untranslated iframe", 40 style: {}, 41 }, 42 { 43 description: "translated 50% iframe", 44 style: { 45 position: "absolute", 46 left: "50%", 47 top: "50%", 48 transform: "translate(-50%, -50%)", 49 }, 50 }, 51 { 52 description: "translated 100% iframe", 53 style: { 54 position: "absolute", 55 left: "100%", 56 top: "100%", 57 transform: "translate(-100%, -100%)", 58 }, 59 }, 60 ]; 61 62 let testID = 0; 63 64 for (const mvcontent of META_VIEWPORT_CONTENTS) { 65 info(`Starting test series with meta viewport content "${mvcontent}".`); 66 67 const TEST_URL = 68 `data:text/html;charset=utf-8,` + 69 `<html><meta name="viewport" content="${mvcontent}">` + 70 `<body style="margin:0; width:100%; height:200%;">` + 71 `<iframe id="host" ` + 72 `style="margin:0; border:0; width:100%; height:100%"></iframe>` + 73 `</body></html>`; 74 75 addRDMTask(TEST_URL, async function ({ ui, manager }) { 76 await setViewportSize(ui, manager, VIEWPORT_DIMENSION, VIEWPORT_DIMENSION); 77 await setTouchAndMetaViewportSupport(ui, true); 78 79 // Figure out our window origin in screen space, which we'll need as we calculate 80 // coordinates for our simulated click events. These values are in CSS units, which 81 // is weird, but we compensate for that later. 82 const screenToWindowX = window.mozInnerScreenX; 83 const screenToWindowY = window.mozInnerScreenY; 84 85 for (const dpr of DPRS) { 86 await selectDevicePixelRatio(ui, dpr); 87 88 for (const path of IFRAME_PATHS) { 89 for (const test of TESTS) { 90 const { description, style } = test; 91 92 const title = `ID ${testID} - ${description} with DPR ${dpr} and path ${path}`; 93 94 info(`Starting test ${title}.`); 95 96 await spawnViewportTask( 97 ui, 98 { 99 title, 100 style, 101 path, 102 VIEWPORT_DIMENSION, 103 screenToWindowX, 104 screenToWindowY, 105 }, 106 async args => { 107 // Define a function that returns a promise for one message that 108 // contains, at least, the supplied prop, and resolves with the 109 // data from that message. If a timeout value is supplied, the 110 // promise will reject if the timeout elapses first. 111 const oneMatchingMessageWithTimeout = (win, prop, timeout) => { 112 return new Promise((resolve, reject) => { 113 let ourTimeoutID = 0; 114 115 const ourListener = win.addEventListener("message", e => { 116 if (typeof e.data[prop] !== "undefined") { 117 if (ourTimeoutID) { 118 win.clearTimeout(ourTimeoutID); 119 } 120 win.removeEventListener("message", ourListener); 121 resolve(e.data); 122 } 123 }); 124 125 if (timeout) { 126 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 127 ourTimeoutID = win.setTimeout(() => { 128 win.removeEventListener("message", ourListener); 129 reject( 130 `Timeout waiting for message with prop ${prop} after ${timeout}ms.` 131 ); 132 }, timeout); 133 } 134 }); 135 }; 136 137 // Our checks are not always precise, due to rounding errors in the 138 // scaling from css to screen and back. For now we use an epsilon and 139 // a locally-defined isfuzzy to compensate. We can't use 140 // SimpleTest.isfuzzy, because it's not bridged to the ContentTask. 141 // If that is ever bridged, we can remove the isfuzzy definition here and 142 // everything should "just work". 143 function isfuzzy(actual, expected, epsilon, msg) { 144 if ( 145 actual >= expected - epsilon && 146 actual <= expected + epsilon 147 ) { 148 ok(true, msg); 149 } else { 150 // This will trigger the usual failure message for is. 151 is(actual, expected, msg); 152 } 153 } 154 155 // This function takes screen coordinates in css pixels. 156 // TODO: This should stop using nsIDOMWindowUtils.sendNativeMouseEvent 157 // directly, and use `EventUtils.synthesizeNativeMouseEvent` in 158 // a message listener in the chrome. 159 function synthesizeNativeMouseClick(win, screenX, screenY) { 160 const utils = win.windowUtils; 161 const scale = win.devicePixelRatio; 162 163 return new Promise(resolve => { 164 utils.sendNativeMouseEvent( 165 screenX * scale, 166 screenY * scale, 167 utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, 168 0, 169 0, 170 win.document.documentElement, 171 () => { 172 utils.sendNativeMouseEvent( 173 screenX * scale, 174 screenY * scale, 175 utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, 176 0, 177 0, 178 win.document.documentElement, 179 resolve 180 ); 181 } 182 ); 183 }); 184 } 185 186 // We're done defining functions; start the actual loading of the iframe 187 // and triggering the onclick handler in its content. 188 const host = content.document.getElementById("host"); 189 190 // Modify the iframe style by adding the properties in the 191 // provided style object. 192 for (const prop in args.style) { 193 info(`Setting style.${prop} to ${args.style[prop]}.`); 194 host.style[prop] = args.style[prop]; 195 } 196 197 // Set the iframe source, and await the ready message. 198 const IFRAME_URL = args.path + "touch_event_target.html"; 199 const READY_TIMEOUT_MS = 5000; 200 const iframeReady = oneMatchingMessageWithTimeout( 201 content, 202 "ready", 203 READY_TIMEOUT_MS 204 ); 205 host.src = IFRAME_URL; 206 try { 207 await iframeReady; 208 } catch (error) { 209 ok(false, `${args.title} ${error}`); 210 return; 211 } 212 213 info(`iframe has finished loading.`); 214 215 // Await reflow of the parent window. 216 await new Promise(resolve => { 217 content.requestAnimationFrame(() => { 218 content.requestAnimationFrame(resolve); 219 }); 220 }); 221 // Ensure that we get correct coordinates in the iframe. 222 await SpecialPowers.spawn(host, [], async () => { 223 await SpecialPowers.contentTransformsReceived(content); 224 }); 225 226 // Now we're going to calculate screen coordinates for the upper-left 227 // quadrant of the target area. We're going to do that by using the 228 // following sources: 229 // 1) args.screenToWindow: the window position in screen space, in CSS 230 // pixels. 231 // 2) host.getBoxQuadsFromWindowOrigin(): the iframe position, relative 232 // to the window origin, in CSS pixels. 233 // 3) args.VIEWPORT_DIMENSION: the viewport size, in CSS pixels. 234 // We calculate the screen position of the center of the upper-left 235 // quadrant of the iframe, then use sendNativeMouseEvent to dispatch 236 // a click at that position. It should trigger the RDM TouchSimulator 237 // and turn the mouse click into a touch event that hits the onclick 238 // handler in the iframe content. If it's done correctly, the message 239 // we get back should have x,y coordinates that match the center of the 240 // upper left quadrant of the iframe, in CSS units. 241 242 const hostBounds = host 243 .getBoxQuadsFromWindowOrigin()[0] 244 .getBounds(); 245 const windowToHostX = hostBounds.left; 246 const windowToHostY = hostBounds.top; 247 248 const screenToHostX = args.screenToWindowX + windowToHostX; 249 const screenToHostY = args.screenToWindowY + windowToHostY; 250 251 const quadrantOffsetDoc = hostBounds.width * 0.25; 252 const hostUpperLeftQuadrantDocX = quadrantOffsetDoc; 253 const hostUpperLeftQuadrantDocY = quadrantOffsetDoc; 254 255 const quadrantOffsetViewport = args.VIEWPORT_DIMENSION * 0.25; 256 const hostUpperLeftQuadrantViewportX = quadrantOffsetViewport; 257 const hostUpperLeftQuadrantViewportY = quadrantOffsetViewport; 258 259 const targetX = screenToHostX + hostUpperLeftQuadrantViewportX; 260 const targetY = screenToHostY + hostUpperLeftQuadrantViewportY; 261 262 // We're going to try a few times to click on the target area. Our method 263 // for triggering a native mouse click is vulnerable to interactive mouse 264 // moves while the test is running. Letting the click timeout gives us a 265 // chance to try again. 266 const CLICK_TIMEOUT_MS = 1000; 267 const CLICK_ATTEMPTS = 3; 268 let eventWasReceived = false; 269 270 for (let attempt = 0; attempt < CLICK_ATTEMPTS; attempt++) { 271 const gotXAndY = oneMatchingMessageWithTimeout( 272 content, 273 "x", 274 CLICK_TIMEOUT_MS 275 ); 276 info( 277 `Sending native mousedown and mouseup to screen position ${targetX}, ${targetY} (attempt ${attempt}).` 278 ); 279 await synthesizeNativeMouseClick(content, targetX, targetY); 280 try { 281 const { x, y, screenX, screenY } = await gotXAndY; 282 eventWasReceived = true; 283 isfuzzy( 284 x, 285 hostUpperLeftQuadrantDocX, 286 1, 287 `${args.title} got click at close enough X ${x}, screen is ${screenX}.` 288 ); 289 isfuzzy( 290 y, 291 hostUpperLeftQuadrantDocY, 292 1, 293 `${args.title} got click at close enough Y ${y}, screen is ${screenY}.` 294 ); 295 break; 296 } catch (error) { 297 // That click didn't work. The for loop will trigger another attempt, 298 // or give up. 299 } 300 } 301 302 if (!eventWasReceived) { 303 ok( 304 false, 305 `${args.title} failed to get a click after ${CLICK_ATTEMPTS} tries.` 306 ); 307 } 308 } 309 ); 310 311 testID++; 312 } 313 } 314 } 315 }); 316 }