summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive/test/browser/browser_touch_event_iframes.js')
-rw-r--r--devtools/client/responsive/test/browser/browser_touch_event_iframes.js312
1 files changed, 312 insertions, 0 deletions
diff --git a/devtools/client/responsive/test/browser/browser_touch_event_iframes.js b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
new file mode 100644
index 0000000000..11b94d2ab1
--- /dev/null
+++ b/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test simulated touch events can correctly target embedded iframes.
+
+// These tests put a target iframe in a small embedding area, nested
+// different ways. Then a simulated mouse click is made on top of the
+// target iframe. If everything works, the translation done in
+// touch-simulator.js should exactly match the translation done in the
+// Platform code, such that the target is hit by the synthesized tap
+// is at the expected location.
+
+info("--- Starting viewport test output ---");
+
+info(`*** WARNING *** This test will move the mouse pointer to simulate
+native mouse clicks. Do not move the mouse during this test or you may
+cause intermittent failures.`);
+
+// This test could run awhile, so request a 4x timeout duration.
+requestLongerTimeout(4);
+
+// The viewport will be square, set to VIEWPORT_DIMENSION on each axis.
+const VIEWPORT_DIMENSION = 200;
+
+const META_VIEWPORT_CONTENTS = ["width=device-width", "width=400"];
+
+const DPRS = [1, 2, 3];
+
+const URL_ROOT_2 = CHROME_URL_ROOT.replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+const IFRAME_PATHS = [`${URL_ROOT}`, `${URL_ROOT_2}`];
+
+const TESTS = [
+ {
+ description: "untranslated iframe",
+ style: {},
+ },
+ {
+ description: "translated 50% iframe",
+ style: {
+ position: "absolute",
+ left: "50%",
+ top: "50%",
+ transform: "translate(-50%, -50%)",
+ },
+ },
+ {
+ description: "translated 100% iframe",
+ style: {
+ position: "absolute",
+ left: "100%",
+ top: "100%",
+ transform: "translate(-100%, -100%)",
+ },
+ },
+];
+
+let testID = 0;
+
+for (const mvcontent of META_VIEWPORT_CONTENTS) {
+ info(`Starting test series with meta viewport content "${mvcontent}".`);
+
+ const TEST_URL =
+ `data:text/html;charset=utf-8,` +
+ `<html><meta name="viewport" content="${mvcontent}">` +
+ `<body style="margin:0; width:100%; height:200%;">` +
+ `<iframe id="host" ` +
+ `style="margin:0; border:0; width:100%; height:100%"></iframe>` +
+ `</body></html>`;
+
+ addRDMTask(TEST_URL, async function ({ ui, manager, browser }) {
+ await setViewportSize(ui, manager, VIEWPORT_DIMENSION, VIEWPORT_DIMENSION);
+ await setTouchAndMetaViewportSupport(ui, true);
+
+ // Figure out our window origin in screen space, which we'll need as we calculate
+ // coordinates for our simulated click events. These values are in CSS units, which
+ // is weird, but we compensate for that later.
+ const screenToWindowX = window.mozInnerScreenX;
+ const screenToWindowY = window.mozInnerScreenY;
+
+ for (const dpr of DPRS) {
+ await selectDevicePixelRatio(ui, dpr);
+
+ for (const path of IFRAME_PATHS) {
+ for (const test of TESTS) {
+ const { description, style } = test;
+
+ const title = `ID ${testID} - ${description} with DPR ${dpr} and path ${path}`;
+
+ info(`Starting test ${title}.`);
+
+ await spawnViewportTask(
+ ui,
+ {
+ title,
+ style,
+ path,
+ VIEWPORT_DIMENSION,
+ screenToWindowX,
+ screenToWindowY,
+ },
+ async args => {
+ // Define a function that returns a promise for one message that
+ // contains, at least, the supplied prop, and resolves with the
+ // data from that message. If a timeout value is supplied, the
+ // promise will reject if the timeout elapses first.
+ const oneMatchingMessageWithTimeout = (win, prop, timeout) => {
+ return new Promise((resolve, reject) => {
+ let ourTimeoutID = 0;
+
+ const ourListener = win.addEventListener("message", e => {
+ if (typeof e.data[prop] !== "undefined") {
+ if (ourTimeoutID) {
+ win.clearTimeout(ourTimeoutID);
+ }
+ win.removeEventListener("message", ourListener);
+ resolve(e.data);
+ }
+ });
+
+ if (timeout) {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ ourTimeoutID = win.setTimeout(() => {
+ win.removeEventListener("message", ourListener);
+ reject(
+ `Timeout waiting for message with prop ${prop} after ${timeout}ms.`
+ );
+ }, timeout);
+ }
+ });
+ };
+
+ // Our checks are not always precise, due to rounding errors in the
+ // scaling from css to screen and back. For now we use an epsilon and
+ // a locally-defined isfuzzy to compensate. We can't use
+ // SimpleTest.isfuzzy, because it's not bridged to the ContentTask.
+ // If that is ever bridged, we can remove the isfuzzy definition here and
+ // everything should "just work".
+ function isfuzzy(actual, expected, epsilon, msg) {
+ if (
+ actual >= expected - epsilon &&
+ actual <= expected + epsilon
+ ) {
+ ok(true, msg);
+ } else {
+ // This will trigger the usual failure message for is.
+ is(actual, expected, msg);
+ }
+ }
+
+ // This function takes screen coordinates in css pixels.
+ // TODO: This should stop using nsIDOMWindowUtils.sendNativeMouseEvent
+ // directly, and use `EventUtils.synthesizeNativeMouseEvent` in
+ // a message listener in the chrome.
+ function synthesizeNativeMouseClick(win, screenX, screenY) {
+ const utils = win.windowUtils;
+ const scale = win.devicePixelRatio;
+
+ return new Promise(resolve => {
+ utils.sendNativeMouseEvent(
+ screenX * scale,
+ screenY * scale,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN,
+ 0,
+ 0,
+ win.document.documentElement,
+ () => {
+ utils.sendNativeMouseEvent(
+ screenX * scale,
+ screenY * scale,
+ utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP,
+ 0,
+ 0,
+ win.document.documentElement,
+ resolve
+ );
+ }
+ );
+ });
+ }
+
+ // We're done defining functions; start the actual loading of the iframe
+ // and triggering the onclick handler in its content.
+ const host = content.document.getElementById("host");
+
+ // Modify the iframe style by adding the properties in the
+ // provided style object.
+ for (const prop in args.style) {
+ info(`Setting style.${prop} to ${args.style[prop]}.`);
+ host.style[prop] = args.style[prop];
+ }
+
+ // Set the iframe source, and await the ready message.
+ const IFRAME_URL = args.path + "touch_event_target.html";
+ const READY_TIMEOUT_MS = 5000;
+ const iframeReady = oneMatchingMessageWithTimeout(
+ content,
+ "ready",
+ READY_TIMEOUT_MS
+ );
+ host.src = IFRAME_URL;
+ try {
+ await iframeReady;
+ } catch (error) {
+ ok(false, `${args.title} ${error}`);
+ return;
+ }
+
+ info(`iframe has finished loading.`);
+
+ // Await reflow of the parent window.
+ await new Promise(resolve => {
+ content.requestAnimationFrame(() => {
+ content.requestAnimationFrame(resolve);
+ });
+ });
+
+ // Now we're going to calculate screen coordinates for the upper-left
+ // quadrant of the target area. We're going to do that by using the
+ // following sources:
+ // 1) args.screenToWindow: the window position in screen space, in CSS
+ // pixels.
+ // 2) host.getBoxQuadsFromWindowOrigin(): the iframe position, relative
+ // to the window origin, in CSS pixels.
+ // 3) args.VIEWPORT_DIMENSION: the viewport size, in CSS pixels.
+ // We calculate the screen position of the center of the upper-left
+ // quadrant of the iframe, then use sendNativeMouseEvent to dispatch
+ // a click at that position. It should trigger the RDM TouchSimulator
+ // and turn the mouse click into a touch event that hits the onclick
+ // handler in the iframe content. If it's done correctly, the message
+ // we get back should have x,y coordinates that match the center of the
+ // upper left quadrant of the iframe, in CSS units.
+
+ const hostBounds = host
+ .getBoxQuadsFromWindowOrigin()[0]
+ .getBounds();
+ const windowToHostX = hostBounds.left;
+ const windowToHostY = hostBounds.top;
+
+ const screenToHostX = args.screenToWindowX + windowToHostX;
+ const screenToHostY = args.screenToWindowY + windowToHostY;
+
+ const quadrantOffsetDoc = hostBounds.width * 0.25;
+ const hostUpperLeftQuadrantDocX = quadrantOffsetDoc;
+ const hostUpperLeftQuadrantDocY = quadrantOffsetDoc;
+
+ const quadrantOffsetViewport = args.VIEWPORT_DIMENSION * 0.25;
+ const hostUpperLeftQuadrantViewportX = quadrantOffsetViewport;
+ const hostUpperLeftQuadrantViewportY = quadrantOffsetViewport;
+
+ const targetX = screenToHostX + hostUpperLeftQuadrantViewportX;
+ const targetY = screenToHostY + hostUpperLeftQuadrantViewportY;
+
+ // We're going to try a few times to click on the target area. Our method
+ // for triggering a native mouse click is vulnerable to interactive mouse
+ // moves while the test is running. Letting the click timeout gives us a
+ // chance to try again.
+ const CLICK_TIMEOUT_MS = 1000;
+ const CLICK_ATTEMPTS = 3;
+ let eventWasReceived = false;
+
+ for (let attempt = 0; attempt < CLICK_ATTEMPTS; attempt++) {
+ const gotXAndY = oneMatchingMessageWithTimeout(
+ content,
+ "x",
+ CLICK_TIMEOUT_MS
+ );
+ info(
+ `Sending native mousedown and mouseup to screen position ${targetX}, ${targetY} (attempt ${attempt}).`
+ );
+ await synthesizeNativeMouseClick(content, targetX, targetY);
+ try {
+ const { x, y, screenX, screenY } = await gotXAndY;
+ eventWasReceived = true;
+ isfuzzy(
+ x,
+ hostUpperLeftQuadrantDocX,
+ 1,
+ `${args.title} got click at close enough X ${x}, screen is ${screenX}.`
+ );
+ isfuzzy(
+ y,
+ hostUpperLeftQuadrantDocY,
+ 1,
+ `${args.title} got click at close enough Y ${y}, screen is ${screenY}.`
+ );
+ break;
+ } catch (error) {
+ // That click didn't work. The for loop will trigger another attempt,
+ // or give up.
+ }
+ }
+
+ if (!eventWasReceived) {
+ ok(
+ false,
+ `${args.title} failed to get a click after ${CLICK_ATTEMPTS} tries.`
+ );
+ }
+ }
+ );
+
+ testID++;
+ }
+ }
+ }
+ });
+}