tor-browser

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

on-scroll-behavior.tentative.html (22429B)


      1 <!doctype html>
      2 <meta charset="utf-8" />
      3 <meta name="author" title="Chromium" href="https://chromium.org" />
      4 <meta name="timeout" content="long" />
      5 <link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
      6 <script src="/resources/testharness.js"></script>
      7 <script src="/resources/testharnessreport.js"></script>
      8 <script src="resources/invoker-utils.js"></script>
      9 
     10 <style>
     11  .scroll-container {
     12    width: 200px;
     13    height: 200px;
     14    overflow: auto;
     15    border: 1px solid black;
     16  }
     17 
     18  .scroll-content {
     19    width: 1000px;
     20    height: 1000px;
     21    background: linear-gradient(to bottom right, red, blue);
     22  }
     23 
     24  .scroll-container-horizontal {
     25    width: 200px;
     26    height: 100px;
     27    overflow-x: auto;
     28    overflow-y: hidden;
     29    border: 1px solid black;
     30  }
     31 
     32  .scroll-content-horizontal {
     33    width: 1000px;
     34    height: 100px;
     35    background: linear-gradient(to right, red, blue);
     36  }
     37 
     38  .scroll-container-vertical {
     39    width: 100px;
     40    height: 200px;
     41    overflow-y: auto;
     42    overflow-x: hidden;
     43    border: 1px solid black;
     44  }
     45 
     46  .scroll-content-vertical {
     47    width: 100px;
     48    height: 1000px;
     49    background: linear-gradient(to bottom, red, blue);
     50  }
     51 
     52  .rtl {
     53    direction: rtl;
     54  }
     55 
     56  .vertical-writing {
     57    writing-mode: vertical-rl;
     58  }
     59 </style>
     60 
     61 <!-- Basic scroll container -->
     62 <div id="scrollcontainer" class="scroll-container">
     63  <div class="scroll-content"></div>
     64 </div>
     65 <button id="pageup" commandfor="scrollcontainer" command="page-up">Page Up</button>
     66 <button id="pagedown" commandfor="scrollcontainer" command="page-down">Page Down</button>
     67 <button id="pageleft" commandfor="scrollcontainer" command="page-left">Page Left</button>
     68 <button id="pageright" commandfor="scrollcontainer" command="page-right">Page Right</button>
     69 
     70 <!-- Horizontal only scroll container -->
     71 <div id="horizontalcontainer" class="scroll-container-horizontal">
     72  <div class="scroll-content-horizontal"></div>
     73 </div>
     74 <button id="hpageleft" commandfor="horizontalcontainer" command="page-left">Page Left</button>
     75 <button id="hpageright" commandfor="horizontalcontainer" command="page-right">Page Right</button>
     76 
     77 <!-- Vertical only scroll container -->
     78 <div id="verticalcontainer" class="scroll-container-vertical">
     79  <div class="scroll-content-vertical"></div>
     80 </div>
     81 <button id="vpageup" commandfor="verticalcontainer" command="page-up">Page Up</button>
     82 <button id="vpagedown" commandfor="verticalcontainer" command="page-down">Page Down</button>
     83 
     84 <!-- Logical direction tests -->
     85 <div id="logicalcontainer" class="scroll-container">
     86  <div class="scroll-content"></div>
     87 </div>
     88 <button id="blockstart" commandfor="logicalcontainer" command="page-block-start">Block Start</button>
     89 <button id="blockend" commandfor="logicalcontainer" command="page-block-end">Block End</button>
     90 <button id="inlinestart" commandfor="logicalcontainer" command="page-inline-start">Inline Start</button>
     91 <button id="inlineend" commandfor="logicalcontainer" command="page-inline-end">Inline End</button>
     92 
     93 <!-- RTL container -->
     94 <div id="rtlcontainer" class="scroll-container rtl">
     95  <div class="scroll-content"></div>
     96 </div>
     97 <button id="rtlinlinestart" commandfor="rtlcontainer" command="page-inline-start">Inline Start (RTL)</button>
     98 <button id="rtlinlineend" commandfor="rtlcontainer" command="page-inline-end">Inline End (RTL)</button>
     99 
    100 <!-- Vertical writing mode container -->
    101 <div id="verticalwritingcontainer" class="scroll-container vertical-writing">
    102  <div class="scroll-content"></div>
    103 </div>
    104 <button id="vwblockstart" commandfor="verticalwritingcontainer" command="page-block-start">Block Start (VW)</button>
    105 <button id="vwblockend" commandfor="verticalwritingcontainer" command="page-block-end">Block End (VW)</button>
    106 
    107 <script>
    108  function resetScrollPosition(container) {
    109    container.scrollTop = 0;
    110    container.scrollLeft = 0;
    111  }
    112 
    113  // Test page-up command
    114  test(function (t) {
    115    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    116    scrollcontainer.scrollTop = 400;
    117    const initialScrollTop = scrollcontainer.scrollTop;
    118    pageup.click();
    119    assert_less_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll position should decrease");
    120  }, "page-up command scrolls up");
    121 
    122  // Test page-down command
    123  test(function (t) {
    124    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    125    const initialScrollTop = scrollcontainer.scrollTop;
    126    pagedown.click();
    127    assert_greater_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll position should increase");
    128  }, "page-down command scrolls down");
    129 
    130  // Test page-left command
    131  test(function (t) {
    132    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    133    scrollcontainer.scrollLeft = 400;
    134    const initialScrollLeft = scrollcontainer.scrollLeft;
    135    pageleft.click();
    136    assert_less_than(scrollcontainer.scrollLeft, initialScrollLeft, "Scroll position should decrease");
    137  }, "page-left command scrolls left");
    138 
    139  // Test page-right command
    140  test(function (t) {
    141    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    142    const initialScrollLeft = scrollcontainer.scrollLeft;
    143    pageright.click();
    144    assert_greater_than(scrollcontainer.scrollLeft, initialScrollLeft, "Scroll position should increase");
    145  }, "page-right command scrolls right");
    146 
    147  // Test that page-up doesn't scroll horizontally
    148  test(function (t) {
    149    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    150    scrollcontainer.scrollTop = 400;
    151    scrollcontainer.scrollLeft = 200;
    152    const initialScrollLeft = scrollcontainer.scrollLeft;
    153    pageup.click();
    154    assert_equals(scrollcontainer.scrollLeft, initialScrollLeft, "Horizontal scroll should not change");
    155  }, "page-up command doesn't affect horizontal scroll");
    156 
    157  // Test that page-left doesn't scroll vertically
    158  test(function (t) {
    159    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    160    scrollcontainer.scrollTop = 200;
    161    scrollcontainer.scrollLeft = 400;
    162    const initialScrollTop = scrollcontainer.scrollTop;
    163    pageleft.click();
    164    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Vertical scroll should not change");
    165  }, "page-left command doesn't affect vertical scroll");
    166 
    167  // Test horizontal-only container
    168  test(function (t) {
    169    t.add_cleanup(() => resetScrollPosition(horizontalcontainer));
    170    const initialScrollLeft = horizontalcontainer.scrollLeft;
    171    hpageright.click();
    172    assert_greater_than(horizontalcontainer.scrollLeft, initialScrollLeft, "Horizontal scroll should increase");
    173    assert_equals(horizontalcontainer.scrollTop, 0, "Vertical scroll should remain 0");
    174  }, "page-right works on horizontal-only container");
    175 
    176  // Test vertical-only container
    177  test(function (t) {
    178    t.add_cleanup(() => resetScrollPosition(verticalcontainer));
    179    const initialScrollTop = verticalcontainer.scrollTop;
    180    vpagedown.click();
    181    assert_greater_than(verticalcontainer.scrollTop, initialScrollTop, "Vertical scroll should increase");
    182    assert_equals(verticalcontainer.scrollLeft, 0, "Horizontal scroll should remain 0");
    183  }, "page-down works on vertical-only container");
    184 
    185  // Test page-block-end (should scroll down in horizontal writing mode)
    186  test(function (t) {
    187    t.add_cleanup(() => resetScrollPosition(logicalcontainer));
    188    const initialScrollTop = logicalcontainer.scrollTop;
    189    blockend.click();
    190    assert_greater_than(logicalcontainer.scrollTop, initialScrollTop, "Scroll position should increase");
    191  }, "page-block-end scrolls down in horizontal writing mode");
    192 
    193  // Test page-block-start (should scroll up in horizontal writing mode)
    194  test(function (t) {
    195    t.add_cleanup(() => resetScrollPosition(logicalcontainer));
    196    logicalcontainer.scrollTop = 400;
    197    const initialScrollTop = logicalcontainer.scrollTop;
    198    blockstart.click();
    199    assert_less_than(logicalcontainer.scrollTop, initialScrollTop, "Scroll position should decrease");
    200  }, "page-block-start scrolls up in horizontal writing mode");
    201 
    202  // Test page-inline-end (should scroll right in LTR)
    203  test(function (t) {
    204    t.add_cleanup(() => resetScrollPosition(logicalcontainer));
    205    const initialScrollLeft = logicalcontainer.scrollLeft;
    206    inlineend.click();
    207    assert_greater_than(logicalcontainer.scrollLeft, initialScrollLeft, "Scroll position should increase");
    208  }, "page-inline-end scrolls right in LTR");
    209 
    210  // Test page-inline-start (should scroll left in LTR)
    211  test(function (t) {
    212    t.add_cleanup(() => resetScrollPosition(logicalcontainer));
    213    logicalcontainer.scrollLeft = 400;
    214    const initialScrollLeft = logicalcontainer.scrollLeft;
    215    inlinestart.click();
    216    assert_less_than(logicalcontainer.scrollLeft, initialScrollLeft, "Scroll position should decrease");
    217  }, "page-inline-start scrolls left in LTR");
    218 
    219  // Test RTL inline directions
    220  test(function (t) {
    221    t.add_cleanup(() => resetScrollPosition(rtlcontainer));
    222    // In RTL, inline-end should scroll left (in the visual sense)
    223    const initialScrollLeft = rtlcontainer.scrollLeft;
    224    rtlinlineend.click();
    225    // Note: RTL scrolling behavior can vary, but the command should work
    226    assert_not_equals(rtlcontainer.scrollLeft, initialScrollLeft, "Scroll position should change");
    227  }, "page-inline-end works in RTL container");
    228 
    229  // Test case insensitivity
    230  ["page-up", "PAGE-UP", "PaGe-Up"].forEach((command) => {
    231    test(function (t) {
    232      t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    233      const button = document.createElement("button");
    234      button.setAttribute("commandfor", "scrollcontainer");
    235      button.setAttribute("command", command);
    236      document.body.appendChild(button);
    237      t.add_cleanup(() => button.remove());
    238 
    239      scrollcontainer.scrollTop = 400;
    240      const initialScrollTop = scrollcontainer.scrollTop;
    241      button.click();
    242      assert_less_than(scrollcontainer.scrollTop, initialScrollTop, "Scroll should work with " + command);
    243    }, `scroll command is case-insensitive: ${command}`);
    244  });
    245 
    246  // Test preventDefault
    247  test(function (t) {
    248    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    249    scrollcontainer.addEventListener("command", (e) => e.preventDefault(), { once: true });
    250    const initialScrollTop = scrollcontainer.scrollTop;
    251    pagedown.click();
    252    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Scroll should not change when prevented");
    253  }, "preventDefault stops scroll command");
    254 
    255  // Test that scroll doesn't happen if commandfor is invalid
    256  test(function (t) {
    257    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    258    const button = document.createElement("button");
    259    button.setAttribute("commandfor", "nonexistent");
    260    button.setAttribute("command", "page-down");
    261    document.body.appendChild(button);
    262    t.add_cleanup(() => button.remove());
    263 
    264    const initialScrollTop = scrollcontainer.scrollTop;
    265    button.click();
    266    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Scroll should not happen with invalid commandfor");
    267  }, "scroll command requires valid commandfor target");
    268 
    269  // Test that scroll doesn't happen on non-scrollable element
    270  test(function (t) {
    271    const nonscrollable = document.createElement("div");
    272    nonscrollable.id = "nonscrollable";
    273    nonscrollable.textContent = "Not scrollable";
    274    document.body.appendChild(nonscrollable);
    275    t.add_cleanup(() => nonscrollable.remove());
    276 
    277    const button = document.createElement("button");
    278    button.setAttribute("commandfor", "nonscrollable");
    279    button.setAttribute("command", "page-down");
    280    document.body.appendChild(button);
    281    t.add_cleanup(() => button.remove());
    282 
    283    // Should not throw or cause issues
    284    button.click();
    285    assert_equals(nonscrollable.scrollTop, 0, "Non-scrollable element should remain at 0");
    286  }, "scroll command on non-scrollable element does nothing");
    287 
    288  // Test scroll amount is reasonable (approximately one page)
    289  test(function (t) {
    290    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    291    const initialScrollTop = scrollcontainer.scrollTop;
    292    const containerHeight = scrollcontainer.clientHeight;
    293    pagedown.click();
    294    const scrollDistance = scrollcontainer.scrollTop - initialScrollTop;
    295 
    296    // Scroll should be at least 80% of container height (allowing for some overlap)
    297    assert_greater_than(scrollDistance, containerHeight * 0.8,
    298      "Scroll distance should be approximately one page");
    299    // And not more than 1.2x container height
    300    assert_less_than(scrollDistance, containerHeight * 1.2,
    301      "Scroll distance should not be much more than one page");
    302  }, "scroll amount is approximately one page");
    303 
    304  // Edge case: commandfor references non-existent element
    305  test(function (t) {
    306    const button = document.createElement("button");
    307    button.setAttribute("commandfor", "this-element-does-not-exist");
    308    button.setAttribute("command", "page-down");
    309    document.body.appendChild(button);
    310    t.add_cleanup(() => button.remove());
    311 
    312    // Should not throw
    313    assert_equals(button.click(), undefined, "Click should not throw");
    314  }, "scroll command with non-existent commandfor target doesn't throw");
    315 
    316  // Edge case: commandfor is empty string
    317  test(function (t) {
    318    const button = document.createElement("button");
    319    button.setAttribute("commandfor", "");
    320    button.setAttribute("command", "page-down");
    321    document.body.appendChild(button);
    322    t.add_cleanup(() => button.remove());
    323 
    324    // Should not throw
    325    assert_equals(button.click(), undefined, "Click should not throw");
    326  }, "scroll command with empty commandfor doesn't throw");
    327 
    328  // Edge case: commandfor is whitespace
    329  test(function (t) {
    330    const button = document.createElement("button");
    331    button.setAttribute("commandfor", "   ");
    332    button.setAttribute("command", "page-down");
    333    document.body.appendChild(button);
    334    t.add_cleanup(() => button.remove());
    335 
    336    // Should not throw
    337    assert_equals(button.click(), undefined, "Click should not throw");
    338  }, "scroll command with whitespace commandfor doesn't throw");
    339 
    340  // Edge case: target element is disconnected
    341  test(function (t) {
    342    const disconnected = document.createElement("div");
    343    disconnected.id = "disconnected";
    344    disconnected.className = "scroll-container";
    345    disconnected.innerHTML = '<div class="scroll-content"></div>';
    346 
    347    const button = document.createElement("button");
    348    button.setAttribute("commandfor", "disconnected");
    349    button.setAttribute("command", "page-down");
    350    document.body.appendChild(button);
    351    t.add_cleanup(() => button.remove());
    352 
    353    // Should not throw
    354    assert_equals(button.click(), undefined, "Click should not throw for disconnected target");
    355  }, "scroll command with disconnected target element doesn't throw");
    356 
    357  // Edge case: target element is button itself
    358  test(function (t) {
    359    const button = document.createElement("button");
    360    button.id = "selfbutton";
    361    button.setAttribute("commandfor", "selfbutton");
    362    button.setAttribute("command", "page-down");
    363    document.body.appendChild(button);
    364    t.add_cleanup(() => button.remove());
    365 
    366    // Should not throw
    367    assert_equals(button.click(), undefined, "Click should not throw when targeting self");
    368  }, "scroll command targeting self doesn't throw");
    369 
    370  // Edge case: target element is display:none
    371  test(function (t) {
    372    const hidden = document.createElement("div");
    373    hidden.id = "hiddenscroll";
    374    hidden.className = "scroll-container";
    375    hidden.style.display = "none";
    376    hidden.innerHTML = '<div class="scroll-content"></div>';
    377    document.body.appendChild(hidden);
    378    t.add_cleanup(() => hidden.remove());
    379 
    380    const button = document.createElement("button");
    381    button.setAttribute("commandfor", "hiddenscroll");
    382    button.setAttribute("command", "page-down");
    383    document.body.appendChild(button);
    384    t.add_cleanup(() => button.remove());
    385 
    386    // Should not throw
    387    assert_equals(button.click(), undefined, "Click should not throw for hidden target");
    388    assert_equals(hidden.scrollTop, 0, "Hidden element should not scroll");
    389  }, "scroll command on display:none element does nothing");
    390 
    391  // Edge case: target element has no computed style (e.g., in detached document)
    392  test(function (t) {
    393    const newDoc = document.implementation.createHTMLDocument();
    394    const container = newDoc.createElement("div");
    395    container.id = "detachedcontainer";
    396    container.className = "scroll-container";
    397    newDoc.body.appendChild(container);
    398 
    399    // Add the container to main document so commandfor can find it
    400    document.body.appendChild(container);
    401    t.add_cleanup(() => container.remove());
    402 
    403    const button = document.createElement("button");
    404    button.setAttribute("commandfor", "detachedcontainer");
    405    button.setAttribute("command", "page-down");
    406    document.body.appendChild(button);
    407    t.add_cleanup(() => button.remove());
    408 
    409    // Should not throw
    410    assert_equals(button.click(), undefined, "Click should not throw");
    411  }, "scroll command handles elements with unusual document state");
    412 
    413  // Edge case: button is disabled
    414  test(function (t) {
    415    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    416    const button = document.createElement("button");
    417    button.setAttribute("commandfor", "scrollcontainer");
    418    button.setAttribute("command", "page-down");
    419    button.disabled = true;
    420    document.body.appendChild(button);
    421    t.add_cleanup(() => button.remove());
    422 
    423    const initialScrollTop = scrollcontainer.scrollTop;
    424    button.click();
    425    // Disabled buttons should not trigger commands
    426    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Disabled button should not trigger scroll");
    427  }, "disabled button doesn't trigger scroll command");
    428 
    429  // Edge case: multiple buttons targeting same element
    430  test(function (t) {
    431    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    432    const button1 = document.createElement("button");
    433    button1.setAttribute("commandfor", "scrollcontainer");
    434    button1.setAttribute("command", "page-down");
    435    document.body.appendChild(button1);
    436    t.add_cleanup(() => button1.remove());
    437 
    438    const button2 = document.createElement("button");
    439    button2.setAttribute("commandfor", "scrollcontainer");
    440    button2.setAttribute("command", "page-down");
    441    document.body.appendChild(button2);
    442    t.add_cleanup(() => button2.remove());
    443 
    444    const initialScrollTop = scrollcontainer.scrollTop;
    445    button1.click();
    446    const afterFirst = scrollcontainer.scrollTop;
    447    assert_greater_than(afterFirst, initialScrollTop, "First button should scroll");
    448 
    449    button2.click();
    450    assert_greater_than(scrollcontainer.scrollTop, afterFirst, "Second button should also scroll");
    451  }, "multiple buttons can target same scroll container");
    452 
    453  // Edge case: scroll at boundary (can't scroll further up)
    454  test(function (t) {
    455    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    456    scrollcontainer.scrollTop = 0;
    457 
    458    // Should not throw
    459    pageup.click();
    460    assert_equals(scrollcontainer.scrollTop, 0, "Should remain at top");
    461  }, "scroll command at top boundary doesn't throw");
    462 
    463  // Edge case: scroll at boundary (can't scroll further down)
    464  test(function (t) {
    465    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    466    scrollcontainer.scrollTop = scrollcontainer.scrollHeight - scrollcontainer.clientHeight;
    467    const maxScroll = scrollcontainer.scrollTop;
    468 
    469    // Should not throw
    470    pagedown.click();
    471    assert_equals(scrollcontainer.scrollTop, maxScroll, "Should remain at bottom");
    472  }, "scroll command at bottom boundary doesn't throw");
    473 
    474  // Edge case: invalid command value
    475  test(function (t) {
    476    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    477    const button = document.createElement("button");
    478    button.setAttribute("commandfor", "scrollcontainer");
    479    button.setAttribute("command", "invalid-scroll-command");
    480    document.body.appendChild(button);
    481    t.add_cleanup(() => button.remove());
    482 
    483    const initialScrollTop = scrollcontainer.scrollTop;
    484    button.click();
    485    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Invalid command should not scroll");
    486  }, "invalid scroll command value doesn't trigger scroll");
    487 
    488  // Edge case: command attribute is empty
    489  test(function (t) {
    490    t.add_cleanup(() => resetScrollPosition(scrollcontainer));
    491    const button = document.createElement("button");
    492    button.setAttribute("commandfor", "scrollcontainer");
    493    button.setAttribute("command", "");
    494    document.body.appendChild(button);
    495    t.add_cleanup(() => button.remove());
    496 
    497    const initialScrollTop = scrollcontainer.scrollTop;
    498    button.click();
    499    assert_equals(scrollcontainer.scrollTop, initialScrollTop, "Empty command should not scroll");
    500  }, "empty command attribute doesn't trigger scroll");
    501 
    502  // Edge case: target has overflow:visible (not scrollable)
    503  test(function (t) {
    504    const visible = document.createElement("div");
    505    visible.id = "visibleoverflow";
    506    visible.style.width = "200px";
    507    visible.style.height = "200px";
    508    visible.style.overflow = "visible";
    509    visible.innerHTML = '<div style="width: 1000px; height: 1000px;"></div>';
    510    document.body.appendChild(visible);
    511    t.add_cleanup(() => visible.remove());
    512 
    513    const button = document.createElement("button");
    514    button.setAttribute("commandfor", "visibleoverflow");
    515    button.setAttribute("command", "page-down");
    516    document.body.appendChild(button);
    517    t.add_cleanup(() => button.remove());
    518 
    519    button.click();
    520    assert_equals(visible.scrollTop, 0, "overflow:visible element should not scroll");
    521  }, "scroll command on overflow:visible element does nothing");
    522 
    523  // Edge case: target has overflow:clip
    524  test(function (t) {
    525    const clipped = document.createElement("div");
    526    clipped.id = "clippedoverflow";
    527    clipped.style.width = "200px";
    528    clipped.style.height = "200px";
    529    clipped.style.overflow = "clip";
    530    clipped.innerHTML = '<div style="width: 1000px; height: 1000px;"></div>';
    531    document.body.appendChild(clipped);
    532    t.add_cleanup(() => clipped.remove());
    533 
    534    const button = document.createElement("button");
    535    button.setAttribute("commandfor", "clippedoverflow");
    536    button.setAttribute("command", "page-down");
    537    document.body.appendChild(button);
    538    t.add_cleanup(() => button.remove());
    539 
    540    button.click();
    541    assert_equals(clipped.scrollTop, 0, "overflow:clip element should not scroll");
    542  }, "scroll command on overflow:clip element does nothing");
    543 </script>