tor-browser

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

browser_test_scroll_bounds.js (19717B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* import-globals-from ../../mochitest/role.js */
      8 loadScripts(
      9  { name: "layout.js", dir: MOCHITESTS_DIR },
     10  { name: "role.js", dir: MOCHITESTS_DIR }
     11 );
     12 requestLongerTimeout(2);
     13 
     14 const appUnitsPerDevPixel = 60;
     15 
     16 function testCachedScrollPosition(
     17  acc,
     18  expectedX,
     19  expectedY,
     20  shouldBeEmpty = false
     21 ) {
     22  let cachedPosition = "";
     23  try {
     24    cachedPosition = acc.cache.getStringProperty("scroll-position");
     25  } catch (e) {
     26    info("Cache was not populated");
     27    // If the key doesn't exist, this frame is not scrollable.
     28    return shouldBeEmpty;
     29  }
     30 
     31  // The value we retrieve from the cache is in app units, but the values
     32  // passed in are in pixels. Since the retrieved value is a string,
     33  // and harder to modify, adjust our expected x and y values to match its units.
     34  return (
     35    cachedPosition ==
     36    `${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}`
     37  );
     38 }
     39 
     40 function getCachedBounds(acc) {
     41  let cachedBounds = "";
     42  try {
     43    cachedBounds = acc.cache.getStringProperty("relative-bounds");
     44  } catch (e) {
     45    ok(false, "Unable to fetch cached bounds from cache!");
     46  }
     47  return cachedBounds;
     48 }
     49 
     50 /**
     51 * Test bounds of accessibles after scrolling
     52 */
     53 addAccessibleTask(
     54  `
     55  <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
     56  </div>
     57 
     58  <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
     59  </div>
     60  `,
     61  async function (browser, docAcc) {
     62    ok(docAcc, "iframe document acc is present");
     63    await testBoundsWithContent(docAcc, "square", browser);
     64    await testBoundsWithContent(docAcc, "rect", browser);
     65 
     66    await invokeContentTask(browser, [], () => {
     67      content.document.getElementById("square").scrollIntoView();
     68    });
     69 
     70    await waitForContentPaint(browser);
     71 
     72    await testBoundsWithContent(docAcc, "square", browser);
     73    await testBoundsWithContent(docAcc, "rect", browser);
     74 
     75    // Scroll rect into view, but also make it reflow so we can be sure the
     76    // bounds are correct for reflowed frames.
     77    await invokeContentTask(browser, [], () => {
     78      const rect = content.document.getElementById("rect");
     79      rect.scrollIntoView();
     80      rect.style.width = "300px";
     81      rect.offsetTop; // Flush layout.
     82      rect.style.width = "200px";
     83      rect.offsetTop; // Flush layout.
     84    });
     85 
     86    await waitForContentPaint(browser);
     87    await testBoundsWithContent(docAcc, "square", browser);
     88    await testBoundsWithContent(docAcc, "rect", browser);
     89  },
     90  { iframe: true, remoteIframe: true, chrome: true }
     91 );
     92 
     93 /**
     94 * Test scroll offset on cached accessibles
     95 */
     96 addAccessibleTask(
     97  `
     98  <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
     99  </div>
    100 
    101  <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
    102  </div>
    103  `,
    104  async function (browser, docAcc) {
    105    ok(docAcc, "iframe document acc is present");
    106    await untilCacheOk(
    107      () => testCachedScrollPosition(docAcc, 0, 0),
    108      "Correct initial scroll position."
    109    );
    110    const rectAcc = findAccessibleChildByID(docAcc, "rect");
    111    const rectInitialBounds = getCachedBounds(rectAcc);
    112 
    113    await invokeContentTask(browser, [], () => {
    114      content.document.getElementById("square").scrollIntoView();
    115    });
    116 
    117    await waitForContentPaint(browser);
    118 
    119    // The only content to scroll over is `square`'s top margin
    120    // so our scroll offset here should be 3000px
    121    await untilCacheOk(
    122      () => testCachedScrollPosition(docAcc, 0, 3000),
    123      "Correct scroll position after first scroll."
    124    );
    125 
    126    // Scroll rect into view, but also make it reflow so we can be sure the
    127    // bounds are correct for reflowed frames.
    128    await invokeContentTask(browser, [], () => {
    129      const rect = content.document.getElementById("rect");
    130      rect.scrollIntoView();
    131      rect.style.width = "300px";
    132      rect.offsetTop;
    133      rect.style.width = "200px";
    134    });
    135 
    136    await waitForContentPaint(browser);
    137    // We have to scroll over `square`'s top margin (3000px),
    138    // `square` itself (100px), and `square`'s bottom margin (4000px).
    139    // This should give us a 7100px offset.
    140    await untilCacheOk(
    141      () => testCachedScrollPosition(docAcc, 0, 7100),
    142      "Correct final scroll position."
    143    );
    144    await untilCacheIs(
    145      () => getCachedBounds(rectAcc),
    146      rectInitialBounds,
    147      "Cached relative bounds don't change when scrolling"
    148    );
    149  },
    150  { iframe: true, remoteIframe: true }
    151 );
    152 
    153 /**
    154 * Test scroll offset fixed-pos acc accs
    155 */
    156 addAccessibleTask(
    157  `
    158  <div style="margin-top: 100px; margin-left: 75px; border: 1px solid;">
    159    <div id="d" style="position:fixed;">
    160      <button id="top">top</button>
    161    </div>
    162  </div>
    163  `,
    164  async function (browser, docAcc) {
    165    const origTopBounds = await testBoundsWithContent(docAcc, "top", browser);
    166    const origDBounds = await testBoundsWithContent(docAcc, "d", browser);
    167    const e = waitForEvent(EVENT_REORDER, docAcc);
    168    await invokeContentTask(browser, [], () => {
    169      for (let i = 0; i < 1000; ++i) {
    170        const div = content.document.createElement("div");
    171        div.innerHTML = "<button>${i}</button>";
    172        content.document.body.append(div);
    173      }
    174    });
    175    await e;
    176 
    177    await invokeContentTask(browser, [], () => {
    178      // scroll to the bottom of the page
    179      content.window.scrollTo(0, content.document.body.scrollHeight);
    180    });
    181 
    182    await waitForContentPaint(browser);
    183 
    184    let newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
    185    let newDBounds = await testBoundsWithContent(docAcc, "d", browser);
    186    is(
    187      origTopBounds[0],
    188      newTopBounds[0],
    189      "x of fixed elem is unaffected by scrolling"
    190    );
    191    is(
    192      origTopBounds[1],
    193      newTopBounds[1],
    194      "y of fixed elem is unaffected by scrolling"
    195    );
    196    is(
    197      origTopBounds[2],
    198      newTopBounds[2],
    199      "width of fixed elem is unaffected by scrolling"
    200    );
    201    is(
    202      origTopBounds[3],
    203      newTopBounds[3],
    204      "height of fixed elem is unaffected by scrolling"
    205    );
    206    is(
    207      origDBounds[0],
    208      newTopBounds[0],
    209      "x of fixed elem container is unaffected by scrolling"
    210    );
    211    is(
    212      origDBounds[1],
    213      newDBounds[1],
    214      "y of fixed elem container is unaffected by scrolling"
    215    );
    216    is(
    217      origDBounds[2],
    218      newDBounds[2],
    219      "width of fixed container elem is unaffected by scrolling"
    220    );
    221    is(
    222      origDBounds[3],
    223      newDBounds[3],
    224      "height of fixed container elem is unaffected by scrolling"
    225    );
    226 
    227    await invokeContentTask(browser, [], () => {
    228      // remove position styling
    229      content.document.getElementById("d").style = "";
    230    });
    231 
    232    await waitForContentPaint(browser);
    233 
    234    newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
    235    newDBounds = await testBoundsWithContent(docAcc, "d", browser);
    236    is(
    237      origTopBounds[0],
    238      newTopBounds[0],
    239      "x of non-fixed element remains accurate."
    240    );
    241    Assert.less(
    242      newTopBounds[1],
    243      0,
    244      "y coordinate shows item scrolled off page"
    245    );
    246    is(
    247      origTopBounds[2],
    248      newTopBounds[2],
    249      "width of non-fixed element remains accurate."
    250    );
    251    is(
    252      origTopBounds[3],
    253      newTopBounds[3],
    254      "height of non-fixed element remains accurate."
    255    );
    256    is(
    257      origDBounds[0],
    258      newDBounds[0],
    259      "x of non-fixed container element remains accurate."
    260    );
    261    Assert.less(
    262      newDBounds[1],
    263      0,
    264      "y coordinate shows container scrolled off page"
    265    );
    266    // Removing the position styling on this acc causes it to be bound by
    267    // its parent's bounding box, which alters its width as a block element.
    268    // We don't particularly care about width in this test, so skip it.
    269    is(
    270      origDBounds[3],
    271      newDBounds[3],
    272      "height of non-fixed container element remains accurate."
    273    );
    274 
    275    await invokeContentTask(browser, [], () => {
    276      // re-add position styling
    277      content.document.getElementById("d").style = "position:fixed;";
    278    });
    279 
    280    await waitForContentPaint(browser);
    281 
    282    newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
    283    newDBounds = await testBoundsWithContent(docAcc, "d", browser);
    284    is(
    285      origTopBounds[0],
    286      newTopBounds[0],
    287      "x correct when position:fixed is added."
    288    );
    289    is(
    290      origTopBounds[1],
    291      newTopBounds[1],
    292      "y correct when position:fixed is added."
    293    );
    294    is(
    295      origTopBounds[2],
    296      newTopBounds[2],
    297      "width correct when position:fixed is added."
    298    );
    299    is(
    300      origTopBounds[3],
    301      newTopBounds[3],
    302      "height correct when position:fixed is added."
    303    );
    304    is(
    305      origDBounds[0],
    306      newDBounds[0],
    307      "x of container correct when position:fixed is added."
    308    );
    309    is(
    310      origDBounds[1],
    311      newDBounds[1],
    312      "y of container correct when position:fixed is added."
    313    );
    314    is(
    315      origDBounds[2],
    316      newDBounds[2],
    317      "width of container correct when position:fixed is added."
    318    );
    319    is(
    320      origDBounds[3],
    321      newDBounds[3],
    322      "height of container correct when position:fixed is added."
    323    );
    324  },
    325  { chrome: true, iframe: true, remoteIframe: true }
    326 );
    327 
    328 /**
    329 * Test position: fixed for containers that would otherwise be pruned from the
    330 * a11y tree.
    331 */
    332 addAccessibleTask(
    333  `
    334 <table id="fixed" role="presentation" style="position: fixed;">
    335  <tr><th>fixed</th></tr>
    336 </table>
    337 <div id="mutate" role="presentation">mutate</div>
    338 <hr style="height: 200vh;">
    339 <p>bottom</p>
    340  `,
    341  async function (browser, docAcc) {
    342    const fixed = findAccessibleChildByID(docAcc, "fixed");
    343    ok(fixed, "fixed is accessible");
    344    isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE");
    345    ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
    346    info("Setting position: fixed on mutate");
    347    let shown = waitForEvent(EVENT_SHOW, "mutate");
    348    await invokeContentTask(browser, [], () => {
    349      content.document.getElementById("mutate").style.position = "fixed";
    350    });
    351    await shown;
    352    const origFixedBounds = await testBoundsWithContent(
    353      docAcc,
    354      "fixed",
    355      browser
    356    );
    357    const origMutateBounds = await testBoundsWithContent(
    358      docAcc,
    359      "mutate",
    360      browser
    361    );
    362    info("Scrolling to bottom of page");
    363    await invokeContentTask(browser, [], () => {
    364      content.window.scrollTo(0, content.document.body.scrollHeight);
    365    });
    366    await waitForContentPaint(browser);
    367    const newFixedBounds = await testBoundsWithContent(
    368      docAcc,
    369      "fixed",
    370      browser
    371    );
    372    Assert.deepEqual(
    373      newFixedBounds,
    374      origFixedBounds,
    375      "fixed bounds are unchanged"
    376    );
    377    const newMutateBounds = await testBoundsWithContent(
    378      docAcc,
    379      "mutate",
    380      browser
    381    );
    382    Assert.deepEqual(
    383      newMutateBounds,
    384      origMutateBounds,
    385      "mutate bounds are unchanged"
    386    );
    387  },
    388  { chrome: true, iframe: true, remoteIframe: true }
    389 );
    390 
    391 /**
    392 * Test scroll offset on sticky-pos acc
    393 */
    394 addAccessibleTask(
    395  `
    396  <div id="d" style="margin-top: 100px; margin-left: 75px; position:sticky; top:0px;">
    397    <button id="top">top</button>
    398  </div>
    399  `,
    400  async function (browser, docAcc) {
    401    const containerBounds = await testBoundsWithContent(docAcc, "d", browser);
    402    const e = waitForEvent(EVENT_REORDER, docAcc);
    403    await invokeContentTask(browser, [], () => {
    404      for (let i = 0; i < 1000; ++i) {
    405        const div = content.document.createElement("div");
    406        div.innerHTML = "<button>${i}</button>";
    407        content.document.body.append(div);
    408      }
    409    });
    410    await e;
    411    for (let id of ["d", "top"]) {
    412      info(`Verifying bounds for acc with ID ${id}`);
    413      const origBounds = await testBoundsWithContent(docAcc, id, browser);
    414 
    415      info("Scrolling partially");
    416      await invokeContentTask(browser, [], () => {
    417        // scroll some of the window
    418        content.window.scrollTo(0, 50);
    419      });
    420 
    421      await waitForContentPaint(browser);
    422 
    423      let newBounds = await testBoundsWithContent(docAcc, id, browser);
    424      is(
    425        origBounds[0],
    426        newBounds[0],
    427        `x coord of sticky element is unaffected by scrolling`
    428      );
    429      ok(
    430        origBounds[1] > newBounds[1] && newBounds[1] >= 0,
    431        "sticky element scrolled, but not off the page"
    432      );
    433      is(
    434        origBounds[2],
    435        newBounds[2],
    436        `width of sticky element is unaffected by scrolling`
    437      );
    438      is(
    439        origBounds[3],
    440        newBounds[3],
    441        `height of sticky element is unaffected by scrolling`
    442      );
    443 
    444      info("Scrolling to bottom");
    445      await invokeContentTask(browser, [], () => {
    446        // scroll to the bottom of the page
    447        content.window.scrollTo(0, content.document.body.scrollHeight);
    448      });
    449 
    450      await waitForContentPaint(browser);
    451 
    452      newBounds = await testBoundsWithContent(docAcc, id, browser);
    453      is(
    454        origBounds[0],
    455        newBounds[0],
    456        `x coord of sticky element is unaffected by scrolling`
    457      );
    458      // Subtract margin from container screen coords to get chrome height
    459      // which is where our y pos should be
    460      is(
    461        newBounds[1],
    462        containerBounds[1] - 100,
    463        "Sticky element is top of screen"
    464      );
    465      is(
    466        origBounds[2],
    467        newBounds[2],
    468        `width of sticky element is unaffected by scrolling`
    469      );
    470      is(
    471        origBounds[3],
    472        newBounds[3],
    473        `height of sticky element is unaffected by scrolling`
    474      );
    475 
    476      info("Removing position style on container");
    477      await invokeContentTask(browser, [], () => {
    478        // remove position styling
    479        content.document.getElementById("d").style =
    480          "margin-top: 100px; margin-left: 75px;";
    481      });
    482 
    483      await waitForContentPaint(browser);
    484 
    485      newBounds = await testBoundsWithContent(docAcc, id, browser);
    486 
    487      is(
    488        origBounds[0],
    489        newBounds[0],
    490        `x coord of non-sticky element remains accurate.`
    491      );
    492      Assert.less(newBounds[1], 0, "y coordinate shows item scrolled off page");
    493 
    494      // Removing the position styling on this acc causes it to be bound by
    495      // its parent's bounding box, which alters its width as a block element.
    496      // We don't particularly care about width in this test, so skip it.
    497      is(
    498        origBounds[3],
    499        newBounds[3],
    500        `height of non-sticky element remains accurate.`
    501      );
    502 
    503      info("Adding position style on container");
    504      await invokeContentTask(browser, [], () => {
    505        // re-add position styling
    506        content.document.getElementById("d").style =
    507          "margin-top: 100px; margin-left: 75px; position:sticky; top:0px;";
    508      });
    509 
    510      await waitForContentPaint(browser);
    511 
    512      newBounds = await testBoundsWithContent(docAcc, id, browser);
    513      is(
    514        origBounds[0],
    515        newBounds[0],
    516        `x coord of sticky element is unaffected by scrolling`
    517      );
    518      is(
    519        newBounds[1],
    520        containerBounds[1] - 100,
    521        "Sticky element is top of screen"
    522      );
    523      is(
    524        origBounds[2],
    525        newBounds[2],
    526        `width of sticky element is unaffected by scrolling`
    527      );
    528      is(
    529        origBounds[3],
    530        newBounds[3],
    531        `height of sticky element is unaffected by scrolling`
    532      );
    533 
    534      info("Scrolling back up to test next ID");
    535      await invokeContentTask(browser, [], () => {
    536        // scroll some of the window
    537        content.window.scrollTo(0, 0);
    538      });
    539    }
    540  },
    541  { chrome: false, iframe: false, remoteIframe: false }
    542 );
    543 
    544 /**
    545 * Test position: sticky for containers that would otherwise be pruned from the
    546 * a11y tree.
    547 */
    548 addAccessibleTask(
    549  `
    550 <hr style="height: 100vh;">
    551 <div id="stickyContainer">
    552  <div id="sticky" role="presentation" style="position: sticky; top: 0px;">sticky</div>
    553  <hr style="height: 100vh;">
    554  <p id="stickyEnd">stickyEnd</p>
    555 </div>
    556 <div id="mutateContainer">
    557  <div id="mutate" role="presentation" style="top: 0px;">mutate</div>
    558  <hr style="height: 100vh;">
    559  <p id="mutateEnd">mutateEnd</p>
    560 </div>
    561  `,
    562  async function (browser, docAcc) {
    563    ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible");
    564    info("Scrolling to sticky");
    565    await invokeContentTask(browser, [], () => {
    566      content.document.getElementById("sticky").scrollIntoView();
    567    });
    568    await waitForContentPaint(browser);
    569    const origStickyBounds = await testBoundsWithContent(
    570      docAcc,
    571      "sticky",
    572      browser
    573    );
    574    info("Scrolling to stickyEnd");
    575    await invokeContentTask(browser, [], () => {
    576      content.document.getElementById("stickyEnd").scrollIntoView();
    577    });
    578    await waitForContentPaint(browser);
    579    const newStickyBounds = await testBoundsWithContent(
    580      docAcc,
    581      "sticky",
    582      browser
    583    );
    584    Assert.deepEqual(
    585      newStickyBounds,
    586      origStickyBounds,
    587      "sticky bounds are unchanged"
    588    );
    589 
    590    ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
    591    info("Setting position: sticky on mutate");
    592    let shown = waitForEvent(EVENT_SHOW, "mutate");
    593    await invokeContentTask(browser, [], () => {
    594      content.document.getElementById("mutate").style.position = "sticky";
    595    });
    596    await shown;
    597    info("Scrolling to mutate");
    598    await invokeContentTask(browser, [], () => {
    599      content.document.getElementById("mutate").scrollIntoView();
    600    });
    601    await waitForContentPaint(browser);
    602    const origMutateBounds = await testBoundsWithContent(
    603      docAcc,
    604      "mutate",
    605      browser
    606    );
    607    info("Scrolling to mutateEnd");
    608    await invokeContentTask(browser, [], () => {
    609      content.document.getElementById("mutateEnd").scrollIntoView();
    610    });
    611    await waitForContentPaint(browser);
    612    const newMutateBounds = await testBoundsWithContent(
    613      docAcc,
    614      "mutate",
    615      browser
    616    );
    617    assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds);
    618  },
    619  { chrome: true, iframe: true, remoteIframe: true }
    620 );
    621 
    622 /**
    623 * Test scroll offset on non-scrollable accs
    624 */
    625 addAccessibleTask(
    626  `
    627  <div id='square' style='height:100px; width: 100px; background:green;'>hello world
    628  </div>
    629  `,
    630  async function (browser, docAcc) {
    631    const square = findAccessibleChildByID(docAcc, "square");
    632    await untilCacheOk(
    633      () => testCachedScrollPosition(square, 0, 0, true),
    634      "Square is not scrollable."
    635    );
    636 
    637    info("Adding more text content to square");
    638    await invokeContentTask(browser, [], () => {
    639      const s = content.document.getElementById("square");
    640      s.textContent =
    641        "hello world I am some text and I should overflow this container because I am very long";
    642      s.offsetTop; // Flush layout.
    643    });
    644 
    645    await waitForContentPaint(browser);
    646 
    647    await untilCacheOk(
    648      () => testCachedScrollPosition(square, 0, 0, true),
    649      "Square is not scrollable (still has overflow:visible)."
    650    );
    651 
    652    info("Adding overflow:auto; styling");
    653    await invokeContentTask(browser, [], () => {
    654      const s = content.document.getElementById("square");
    655      s.setAttribute(
    656        "style",
    657        "overflow:auto; height:100px; width: 100px; background:green;"
    658      );
    659      s.offsetTop; // Flush layout.
    660    });
    661 
    662    await waitForContentPaint(browser);
    663 
    664    await untilCacheOk(
    665      () => testCachedScrollPosition(square, 0, 0),
    666      "Square is scrollable."
    667    );
    668  },
    669  { iframe: true, remoteIframe: true }
    670 );