summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive/test/browser/browser_touch_event_iframes.js
blob: 6ad963f4c0785c390ebb7a6a1cf8ae28b0270e03 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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 }) {
    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++;
        }
      }
    }
  });
}