tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }