tor-browser

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

scroll_support.js (12803B)


      1 async function waitForEvent(eventName, test, target, timeoutMs = 500) {
      2  return new Promise((resolve, reject) => {
      3    const timeoutCallback = test.step_timeout(() => {
      4      reject(`No ${eventName} event received for target ${target}`);
      5    }, timeoutMs);
      6    target.addEventListener(eventName, (evt) => {
      7      clearTimeout(timeoutCallback);
      8      resolve(evt);
      9    }, { once: true });
     10  });
     11 }
     12 
     13 async function waitForScrollendEvent(test, target, timeoutMs = 500) {
     14  return waitForEvent("scrollend", test, target, timeoutMs);
     15 }
     16 
     17 async function waitForScrollendEventNoTimeout(target) {
     18  return new Promise((resolve) => {
     19    target.addEventListener("scrollend", resolve);
     20  });
     21 }
     22 
     23 // Waits until a rAF callback with no "scroll" event in the last 200ms.
     24 function waitForDelayWithoutScrollEvent(eventTarget) {
     25  const TIMEOUT_IN_MS = 200;
     26 
     27  return new Promise(resolve => {
     28    let lastScrollEventTime = performance.now();
     29 
     30    const scrollListener = () => {
     31      lastScrollEventTime = performance.now();
     32    };
     33    eventTarget.addEventListener('scroll', scrollListener);
     34 
     35    const tick = () => {
     36      if (performance.now() - lastScrollEventTime > TIMEOUT_IN_MS) {
     37        eventTarget.removeEventListener('scroll', scrollListener);
     38        resolve();
     39        return;
     40      }
     41      requestAnimationFrame(tick); // wait another frame
     42    }
     43    requestAnimationFrame(tick);
     44  });
     45 }
     46 
     47 // Waits for the end of scrolling. Uses the "scrollend" event if available.
     48 // Otherwise, fall backs to waitForDelayWithoutScrollEvent().
     49 function waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTargets) {
     50  return new Promise(resolve => {
     51    if (!Array.isArray(eventTargets)) {
     52      eventTargets = [eventTargets];
     53    }
     54    let listeners = [];
     55    const cleanup = () => {
     56      for (const [eventTarget, eventName, listener] of listeners) {
     57        eventTarget.removeEventListener(eventName, listener);
     58      }
     59      listeners = [];
     60    }
     61    const addListener = (eventTarget, eventName, listener) => {
     62      listeners.push([eventTarget, eventName, listener]);
     63      eventTarget.addEventListener(eventName, listener);
     64    }
     65    if (window.onscrollend !== undefined) {
     66      // If scrollend is supported, wait for the first scrollend event.
     67      for (const eventTarget of eventTargets) {
     68        addListener(eventTarget, 'scrollend', () => {
     69          cleanup();
     70          resolve(eventTarget);
     71        });
     72      }
     73    } else {
     74      // Otherwise, wait for the first scroll event, then wait until that
     75      // scroller finishes scrolling.
     76      for (const eventTarget of eventTargets) {
     77        addListener(eventTarget, 'scroll', async () => {
     78          cleanup();
     79          await waitForDelayWithoutScrollEvent(eventTarget);
     80          resolve(eventTarget);
     81        });
     82      }
     83    }
     84  });
     85 }
     86 
     87 // Waits for the end of scrolling, but resolves after the given timeout if no
     88 // scroll event occurs.
     89 function waitForScrollEndOrTimeout(eventTarget, timeout) {
     90  const rafTimeout = new Promise(resolve => {
     91    const startTime = performance.now();
     92    const tick = () => {
     93      if (performance.now() - startTime >= timeout) {
     94        resolve();
     95      } else {
     96        requestAnimationFrame(tick);
     97      }
     98    };
     99    requestAnimationFrame(tick);
    100  });
    101 
    102  return Promise.race([
    103    waitForScrollEndFallbackToDelayWithoutScrollEvent(eventTarget),
    104    rafTimeout
    105  ]);
    106 }
    107 
    108 async function waitForPointercancelEvent(test, target, timeoutMs = 500) {
    109  return waitForEvent("pointercancel", test, target, timeoutMs);
    110 }
    111 
    112 // Resets the scroll position to (0,0).  If a scroll is required, then the
    113 // promise is not resolved until the scrollend event is received.
    114 async function waitForScrollReset(test, scroller, x = 0, y = 0) {
    115  return new Promise(resolve => {
    116    if (scroller.scrollLeft == x && scroller.scrollTop == y) {
    117      resolve();
    118    } else {
    119      const eventTarget =
    120        scroller == document.scrollingElement ? document : scroller;
    121      scroller.scrollTo(x, y);
    122      waitForScrollendEventNoTimeout(eventTarget).then(resolve);
    123    }
    124  });
    125 }
    126 
    127 async function createScrollendPromiseForTarget(test,
    128                                               target_div,
    129                                               timeoutMs = 500,
    130                                               targetIsRoot = false) {
    131  return waitForScrollendEvent(test, target_div, timeoutMs).then(evt => {
    132    assert_false(evt.cancelable, 'Event is not cancelable');
    133    if (targetIsRoot) {
    134      assert_true(evt.bubbles, 'Event targeting element does not bubble');
    135    } else {
    136      assert_false(evt.bubbles, 'Event targeting element does not bubble');
    137    }
    138  });
    139 }
    140 
    141 function verifyNoScrollendOnDocument(test) {
    142  const callback =
    143      test.unreached_func("window got unexpected scrollend event.");
    144  window.addEventListener('scrollend', callback);
    145  test.add_cleanup(() => {
    146    window.removeEventListener('scrollend', callback);
    147  });
    148 }
    149 
    150 async function verifyScrollStopped(test, target_div) {
    151  const unscaled_pause_time_in_ms = 100;
    152  const x = target_div.scrollLeft;
    153  const y = target_div.scrollTop;
    154  return new Promise(resolve => {
    155    test.step_timeout(() => {
    156      assert_equals(target_div.scrollLeft, x);
    157      assert_equals(target_div.scrollTop, y);
    158      resolve();
    159    }, unscaled_pause_time_in_ms);
    160  });
    161 }
    162 
    163 async function resetTargetScrollState(test, target_div) {
    164  if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) {
    165    target_div.scrollTop = 0;
    166    target_div.scrollLeft = 0;
    167    return waitForScrollendEvent(test, target_div);
    168  }
    169 }
    170 
    171 const MAX_FRAME = 700;
    172 const MAX_UNCHANGED_FRAMES = 20;
    173 
    174 // Returns a promise that resolves when the given condition is met or rejects
    175 // after MAX_FRAME animation frames.
    176 // TODO(crbug.com/1400399): deprecate. We should not use frame based waits in
    177 // WPT as frame rates may vary greatly in different testing environments.
    178 function waitFor(condition, error_message = 'Reaches the maximum frames.') {
    179  return new Promise((resolve, reject) => {
    180    function tick(frames) {
    181      // We requestAnimationFrame either for MAX_FRAM frames or until condition
    182      // is met.
    183      if (frames >= MAX_FRAME)
    184        reject(error_message);
    185      else if (condition())
    186        resolve();
    187      else
    188        requestAnimationFrame(tick.bind(this, frames + 1));
    189    }
    190    tick(0);
    191  });
    192 }
    193 
    194 // TODO(crbug.com/1400446): Test driver should defer sending events until the
    195 // browser is ready. Also the term compositor-commit is misleading as not all
    196 // user-agents use a compositor process.
    197 function waitForCompositorCommit() {
    198  return new Promise((resolve) => {
    199    // rAF twice.
    200    window.requestAnimationFrame(() => {
    201      window.requestAnimationFrame(resolve);
    202    });
    203  });
    204 }
    205 
    206 // Please don't remove this. This is necessary for chromium-based browsers. It
    207 // can be a no-op on user-agents that do not have a separate compositor thread.
    208 // TODO(crbug.com/1509054): This shouldn't be necessary if the test harness
    209 // deferred running the tests until after paint holding.
    210 async function waitForCompositorReady() {
    211  const animation =
    212      document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 });
    213  return animation.finished;
    214 }
    215 
    216 function waitForNextFrame() {
    217  const startTime = performance.now();
    218  return new Promise(resolve => {
    219    window.requestAnimationFrame((frameTime) => {
    220      if (frameTime < startTime) {
    221        window.requestAnimationFrame(resolve);
    222      } else {
    223        resolve();
    224      }
    225    });
    226  });
    227 }
    228 
    229 // TODO(crbug.com/1400399): Deprecate as frame rates may vary greatly in
    230 // different test environments.
    231 function waitForAnimationEnd(getValue) {
    232  var last_changed_frame = 0;
    233  var last_position = getValue();
    234  return new Promise((resolve, reject) => {
    235    function tick(frames) {
    236    // We requestAnimationFrame either for MAX_FRAME or until
    237    // MAX_UNCHANGED_FRAMES with no change have been observed.
    238      if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAMES) {
    239        resolve();
    240      } else {
    241        current_value = getValue();
    242        if (last_position != current_value) {
    243          last_changed_frame = frames;
    244          last_position = current_value;
    245        }
    246        requestAnimationFrame(tick.bind(this, frames + 1));
    247      }
    248    }
    249    tick(0);
    250  })
    251 }
    252 
    253 // Scrolls in target according to move_path with pauses in between
    254 // The move_path should contains coordinates that are within target boundaries.
    255 // Keep in mind that 0,0 is the center of the target element and is also
    256 // the pointerDown position.
    257 // pointerUp() is fired after sequence of moves.
    258 function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_in_ms = 100) {
    259  const test_driver_actions = new test_driver.Actions()
    260    .addPointer("pointer1", "touch")
    261    .pointerMove(0, 0, {origin: target})
    262    .pointerDown();
    263 
    264  const substeps = 5;
    265  let x = 0;
    266  let y = 0;
    267  // Do each move in 5 steps
    268  for(let move of move_path) {
    269    let step_x = (move.x - x) / substeps;
    270    let step_y = (move.y - y) / substeps;
    271    for(let step = 0; step < substeps; step++) {
    272      x += step_x;
    273      y += step_y;
    274      test_driver_actions.pointerMove(x, y, {origin: target});
    275    }
    276    test_driver_actions.pause(pause_time_in_ms); // To prevent inertial scroll
    277  }
    278 
    279  return test_driver_actions.pointerUp().send();
    280 }
    281 
    282 function touchScrollInTarget(pixels_to_scroll, target, direction, pause_time_in_ms = 100) {
    283  var x_delta = 0;
    284  var y_delta = 0;
    285  const num_movs = 5;
    286  if (direction == "down") {
    287    y_delta = -1 * pixels_to_scroll / num_movs;
    288  } else if (direction == "up") {
    289    y_delta = pixels_to_scroll / num_movs;
    290  } else if (direction == "right") {
    291    x_delta = -1 * pixels_to_scroll / num_movs;
    292  } else if (direction == "left") {
    293    x_delta = pixels_to_scroll / num_movs;
    294  } else {
    295    throw("scroll direction '" + direction + "' is not expected, direction should be 'down', 'up', 'left' or 'right'");
    296  }
    297  return new test_driver.Actions()
    298      .addPointer("pointer1", "touch")
    299      .pointerMove(0, 0, {origin: target})
    300      .pointerDown()
    301      .pointerMove(x_delta, y_delta, {origin: target})
    302      .pointerMove(2 * x_delta, 2 * y_delta, {origin: target})
    303      .pointerMove(3 * x_delta, 3 * y_delta, {origin: target})
    304      .pointerMove(4 * x_delta, 4 * y_delta, {origin: target})
    305      .pointerMove(5 * x_delta, 5 * y_delta, {origin: target})
    306      .pause(pause_time_in_ms)
    307      .pointerUp()
    308      .send();
    309 }
    310 
    311 // Trigger fling by doing pointerUp right after pointerMoves.
    312 function touchFlingInTarget(pixels_to_scroll, target, direction) {
    313  return touchScrollInTarget(pixels_to_scroll, target, direction, 0 /* pause_time */);
    314 }
    315 
    316 function mouseActionsInTarget(target, origin, delta, pause_time_in_ms = 100) {
    317  return new test_driver.Actions()
    318    .addPointer("pointer1", "mouse")
    319    .pointerMove(origin.x, origin.y, { origin: target })
    320    .pointerDown()
    321    .pointerMove(origin.x + delta.x, origin.y + delta.y, { origin: target })
    322    .pointerMove(origin.x + delta.x * 2, origin.y + delta.y * 2, { origin: target })
    323    .pause(pause_time_in_ms)
    324    .pointerUp()
    325    .send();
    326 }
    327 
    328 // Returns a promise that resolves when the given condition holds for 10
    329 // animation frames or rejects if the condition changes to false within 10
    330 // animation frames.
    331 // TODO(crbug.com/1400399): Deprecate as frame rates may very greatly in
    332 // different test environments.
    333 function conditionHolds(condition, error_message = 'Condition is not true anymore.') {
    334  const MAX_FRAME = 10;
    335  return new Promise((resolve, reject) => {
    336    function tick(frames) {
    337      // We requestAnimationFrame either for 10 frames or until condition is
    338      // violated.
    339      if (frames >= MAX_FRAME)
    340        resolve();
    341      else if (!condition())
    342        reject(error_message);
    343      else
    344        requestAnimationFrame(tick.bind(this, frames + 1));
    345    }
    346    tick(0);
    347  });
    348 }
    349 
    350 function scrollElementDown(element, scroll_amount) {
    351  let x = 0;
    352  let y = 0;
    353  let delta_x = 0;
    354  let delta_y = scroll_amount;
    355  let actions = new test_driver.Actions()
    356  .scroll(x, y, delta_x, delta_y, {origin: element});
    357  return  actions.send();
    358 }
    359 
    360 function scrollElementLeft(element, scroll_amount) {
    361  let x = 0;
    362  let y = 0;
    363  let delta_x = scroll_amount;
    364  let delta_y = 0;
    365  let actions = new test_driver.Actions()
    366  .scroll(x, y, delta_x, delta_y, {origin: element});
    367  return  actions.send();
    368 }
    369 
    370 async function scrollElementByKeyboard(key) {
    371  const KEY_CODE_MAP = {
    372    'ArrowLeft':  '\uE012',
    373    'ArrowUp':    '\uE013',
    374    'ArrowRight': '\uE014',
    375    'ArrowDown':  '\uE015',
    376  };
    377 
    378  if (!KEY_CODE_MAP.hasOwnProperty(key)) {
    379    return Promise.reject(`Invalid key for scrollElementByKeyboard: ${key}`);
    380  }
    381  const code = KEY_CODE_MAP[key];
    382  for (let i = 0; i < 10; i++) {
    383    await new test_driver.Actions()
    384      .keyDown(code)
    385      .keyUp(code)
    386      .send();
    387  }
    388 }