tor-browser

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

test_interactive_widget.html (12573B)


      1 <!DOCTYPE HTML>
      2 <html>
      3 <head>
      4  <meta charset="utf-8">
      5  <title>interactive-widget tests</title>
      6  <script src="/tests/SimpleTest/SimpleTest.js"></script>
      7  <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
      8  <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
      9  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
     10  <style>
     11  textarea {
     12    height: 100px;
     13    width: 100px;
     14  }
     15  </style>
     16 </head>
     17 <body>
     18 <textarea></textarea>
     19 <p id="display"></p>
     20 <div id="content" style="display: none"></div>
     21 <pre id="test"></pre>
     22 <script>
     23 async function getViewportMetrics() {
     24  return SpecialPowers.spawn(parent, [], () => {
     25    return [ content.window.innerHeight,
     26             content.window.visualViewport.height,
     27             content.window.visualViewport.width ];
     28  });
     29 }
     30 
     31 let initial_window_height, initial_visual_viewport_width, initial_visual_viewport_height;
     32 
     33 // setup a meta viewport tag in the top document.
     34 add_setup(async () => {
     35  // Try to close the software keyboard to invoke `documentElement.focus()`.
     36  document.documentElement.focus();
     37  await SimpleTest.promiseWaitForCondition(
     38    () => document.activeElement == document.documentElement,
     39    "Waiting for focus");
     40 
     41  await SpecialPowers.spawn(parent, [], async () => {
     42    const initial_scale = content.window.visualViewport.scale;
     43 
     44    const meta = content.document.createElement("meta");
     45    meta.setAttribute("id", "interactive-widget");
     46    meta.setAttribute("name", "viewport");
     47    meta.setAttribute("content", "width=device-width, initial-scale=1, user-scalable=no");
     48    content.document.documentElement.appendChild(meta);
     49 
     50    const eventPromise =  new Promise(resolve => content.window.addEventListener("resize", resolve));
     51    // Flush the viewport change.
     52    content.document.documentElement.getBoundingClientRect();
     53 
     54    // If this top level content is rendered as `scale < 1.0`, it means there
     55    // was no meta viewport tag at all, so that adding the above meta viewport
     56    // tag will fire a resize event, thus we need to wait for the event here.
     57    // Otherwise, we will wait for the event in the first `resizes-content`
     58    // test and the test will fail.
     59    //
     60    // NOTE: We need this `scale < 1.0` check for --run-until-failure option.
     61    if (initial_scale < 1.0) {
     62      await eventPromise;
     63    }
     64  });
     65 
     66  SimpleTest.registerCleanupFunction(async () => {
     67    await SpecialPowers.spawn(parent, [], async () => {
     68      const meta = content.document.querySelector("#interactive-widget");
     69      meta.setAttribute("content", "");
     70      // Flush the change above.
     71      content.document.documentElement.getBoundingClientRect();
     72      // A dummy Promise to make sure that SpecialPowers.spawn's Promise will
     73      // never be resolved until this script has run in the parent context.
     74      await new Promise(resolve => resolve());
     75    });
     76  });
     77  [ initial_window_height,
     78    initial_visual_viewport_height, initial_visual_viewport_width ] = await getViewportMetrics();
     79  ok(initial_visual_viewport_width < initial_visual_viewport_height,
     80     `the visual viewport height (${initial_visual_viewport_height}) is less ` +
     81     `than the visual viewport width (${initial_visual_viewport_width}), ` +
     82     `it hightly suspects the virtual keyboard persists there, thus ` +
     83     `we can't run this interactive-widget tests properly`);
     84 });
     85 
     86 async function setupInteractiveWidget(aValue) {
     87  await SpecialPowers.spawn(parent, [aValue], async (value) => {
     88    const meta = content.document.querySelector("#interactive-widget");
     89    meta.setAttribute("content", `width=device-width, initial-scale=1, user-scalable=no, interactive-widget=${value}`);
     90 
     91    // Flush the viewport change.
     92    content.document.documentElement.getBoundingClientRect();
     93 
     94    // A dummy Promise to make sure that SpecialPowers.spawn's Promise will
     95    // never be resolved until these script have run in the parent context.
     96    await new Promise(resolve => resolve());
     97  });
     98 }
     99 
    100 // SpecialPowers.spawn doesn't provide any reasonable way to make sure event
    101 // listeners have been set in the given context (bug 1743857), so here we post
    102 // a message just before setting up a resize event listener and return two
    103 // Promises, one will be resolved when we received the message, the other will
    104 // be resolved when we got a resize event.
    105 function setupResizeEventListener(aInteractiveWidget) {
    106  const ready = new Promise(resolve => {
    107    window.addEventListener("message", msg => {
    108      if (msg.data == "interactive-widget:ready") {
    109        resolve(msg.data)
    110      }
    111    }, { once: true });
    112  });
    113 
    114  const resizePromise = SpecialPowers.spawn(parent, [aInteractiveWidget], async (interactiveWidget) => {
    115    // #testframe is the iframe id where our mochitest harness loads each test
    116    // document, but if this test runs solely just like ./mach test TEST_PATH,
    117    // the test document gets loaded in the top level content.
    118    const target = content.document.querySelector("#testframe") ?
    119      content.document.querySelector("#testframe").contentWindow : content.window;
    120 
    121    let eventPromise;
    122    if (interactiveWidget == "resizes-content") {
    123      eventPromise =  new Promise(resolve => content.window.addEventListener("resize", resolve));
    124    } else if (interactiveWidget == "resizes-visual") {
    125      eventPromise = new Promise(resolve => content.window.visualViewport.addEventListener("resize", resolve));
    126    } else {
    127      ok(false, `Unexpected interactive-widget=${interactiveWidget}`);
    128    }
    129    target.postMessage("interactive-widget:ready", "*");
    130    await eventPromise;
    131  });
    132 
    133  return [ ready, resizePromise ];
    134 }
    135 
    136 // A utility function to hide the software keyboard.
    137 // This function needs to be called while the software keyboard is shown on
    138 // `resizes-content' or `resizes-visual` mode.
    139 async function hideKeyboard() {
    140  const interactiveWidget = await SpecialPowers.spawn(parent, [], () => {
    141    const meta = content.document.querySelector("#interactive-widget");
    142    return meta.getAttribute("content").match(/interactive-widget=([\w-].+?)[,\s]*$/)[1];
    143  });
    144 
    145  let [ readyPromise, resizePromise ] = setupResizeEventListener(interactiveWidget);
    146  await readyPromise;
    147 
    148  // Tap outside the textarea to hide the software keyboard.
    149  await synthesizeNativeTap(document.querySelector("textarea"), 150, 50);
    150  await resizePromise;
    151 
    152  await SimpleTest.promiseWaitForCondition(
    153    async () => {
    154      let [ current_window_height, current_visual_viewport_height ] = await getViewportMetrics();
    155      return current_window_height == initial_window_height &&
    156             current_visual_viewport_height == initial_visual_viewport_height;
    157    },
    158    "Waiting for restoring the initial state");
    159 }
    160 
    161 // `resizes-content` test
    162 add_task(async () => {
    163  await setupInteractiveWidget("resizes-content");
    164 
    165  // Setup a resize event listener in the top level document.
    166  let [ readyPromise, resizePromise ] = setupResizeEventListener("resizes-content");
    167  // Make sure the event listener has been set.
    168  await readyPromise;
    169 
    170  // Tap the textarea to show the software keyboard.
    171  await synthesizeNativeTap(document.querySelector("textarea"), 50, 50);
    172 
    173  await resizePromise;
    174 
    175  // Now the software keyboard has appeared, before running the next test we
    176  // need to hide the keyboard.
    177  SimpleTest.registerCurrentTaskCleanupFunction(async () => await hideKeyboard());
    178 
    179  await SimpleTest.promiseWaitForCondition(
    180    () => document.activeElement == document.querySelector("textarea"),
    181    "Waiting for focus");
    182 
    183  let [ window_height, visual_viewport_height ] = await getViewportMetrics();
    184  ok(window_height < initial_window_height,
    185     `The layout viewport got resized to ${window_height} from ${initial_window_height}`);
    186  ok(visual_viewport_height < initial_visual_viewport_height,
    187     `The visual viewport got resized to ${visual_viewport_height} from ${initial_visual_viewport_height}`);
    188 });
    189 
    190 // `resizes-visual` test
    191 add_task(async () => {
    192  await setupInteractiveWidget("resizes-visual");
    193 
    194  // Setup a resize event listener in the top level document.
    195  let [ readyPromise, resizePromise ] = setupResizeEventListener("resizes-visual");
    196  // Make sure the event listener has been set.
    197  await readyPromise;
    198 
    199  // Tap the textarea to show the software keyboard.
    200  await synthesizeNativeTap(document.querySelector("textarea"), 50, 50);
    201 
    202  await resizePromise;
    203 
    204  // Now the software keyboard has appeared, before running the next test we
    205  // need to hide the keyboard.
    206  SimpleTest.registerCurrentTaskCleanupFunction(async () => await hideKeyboard());
    207 
    208  await SimpleTest.promiseWaitForCondition(
    209    () => document.activeElement == document.querySelector("textarea"),
    210    "Waiting for focus");
    211 
    212  let [ window_height, visual_viewport_height ] = await getViewportMetrics();
    213  is(window_height, initial_window_height,
    214     "The layout viewport is not resized on resizes-visual");
    215  ok(visual_viewport_height < initial_visual_viewport_height,
    216     `The visual viewport got resized to ${visual_viewport_height} from ${initial_visual_viewport_height}`);
    217 });
    218 
    219 // Append an element in the top level document that the element will be the
    220 // underneath the software keyboard.
    221 async function appendSpacer() {
    222  await SpecialPowers.spawn(parent, [], async () => {
    223    const div = content.document.createElement("div");
    224    div.setAttribute("id", "interactive-widget-test-spacer");
    225    div.style = "height: 200vh; position: absolute; top: 90vh;";
    226    content.document.body.appendChild(div);
    227 
    228    // Flush the change.
    229    content.document.documentElement.getBoundingClientRect();
    230 
    231    // A dummy Promise to make sure that SpecialPowers.spawn's Promise will
    232    // never be resolved until these script have run in the parent context.
    233    await new Promise(resolve => resolve());
    234  });
    235 
    236  SimpleTest.registerCurrentTaskCleanupFunction(async () => {
    237    await SpecialPowers.spawn(parent, [], async () => {
    238      const div = content.document.querySelector("#interactive-widget-test-spacer");
    239      div.remove();
    240      // Flush the change.
    241      content.document.documentElement.getBoundingClientRect();
    242 
    243      // A dummy Promise to make sure that SpecialPowers.spawn's Promise will
    244      // never be resolved until these script have run in the parent context.
    245      await new Promise(resolve => resolve());
    246    });
    247  });
    248 }
    249 
    250 // `overlays-content` test
    251 add_task(async () => {
    252  await setupInteractiveWidget("overlays-content");
    253 
    254  await appendSpacer();
    255 
    256  // Tap the textarea to show the software keyboard.
    257  await synthesizeNativeTap(document.querySelector("textarea"), 50, 50);
    258 
    259  // Now the software keyboard has appeared, before running the next test we
    260  // need to hide the keyboard.
    261  SimpleTest.registerCurrentTaskCleanupFunction(async () => {
    262    // Switch back to `resizes-content` mode so that we can receive a resize
    263    // event when the keyboard gets hidden.
    264    await setupInteractiveWidget("resizes-content");
    265    await hideKeyboard();
    266  });
    267 
    268  await SimpleTest.promiseWaitForCondition(
    269    () => document.activeElement == document.querySelector("textarea"),
    270    "Waiting for focus");
    271 
    272  let [ window_height, visual_viewport_height ] = await getViewportMetrics();
    273  is(window_height, initial_window_height,
    274     "The layout viewport is not resized on overlays-content");
    275  is(visual_viewport_height, initial_visual_viewport_height,
    276     "The visual viewport is not resized on overlays-content");
    277 
    278  // Call a scrollIntoView() on an element underneath the keyboard and see if
    279  // the current scroll position changes.
    280  const scrollPosition = await SpecialPowers.spawn(parent, [], () => {
    281    return content.window.scrollY;
    282  });
    283  await SpecialPowers.spawn(parent, [], async () => {
    284    const div = content.document.querySelector("#interactive-widget-test-spacer");
    285    div.scrollIntoView({ behavior: "instant" });
    286 
    287    // Though two rAFs ensure there's at least one scroll event if there is,
    288    // we use two additional rAFs just in case.
    289    await new Promise(resolve => content.window.requestAnimationFrame(resolve));
    290    await new Promise(resolve => content.window.requestAnimationFrame(resolve));
    291    await new Promise(resolve => content.window.requestAnimationFrame(resolve));
    292    await new Promise(resolve => content.window.requestAnimationFrame(resolve));
    293  });
    294 
    295  const newScrollPosition = await SpecialPowers.spawn(parent, [], () => {
    296    return content.window.scrollY;
    297  });
    298  is(scrollPosition, newScrollPosition, "The scrollIntoView() call has no effect");
    299 });
    300 </script>
    301 </body>
    302 </html>