function waitForRender() {
  return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
async function clickOn(element) {
  await waitForRender();
  let rect = element.getBoundingClientRect();
  let actions = new test_driver.Actions();
  // FIXME: Switch to pointerMove(0, 0, {origin: element}) once
  // https://github.com/web-platform-tests/wpt/issues/41257 is fixed.
  await actions
      .pointerMove(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2), {})
      .pointerDown({button: actions.ButtonType.LEFT})
      .pointerUp({button: actions.ButtonType.LEFT})
      .send();
  await waitForRender();
}
async function focusOn(element) {
  element.focus();
  await waitForRender();
  assert_equals(document.activeElement,element,'focus should be on element');
}
async function hoverOver(element) {
  await waitForRender();
  let rect = element.getBoundingClientRect();
  let actions = new test_driver.Actions();
  // FIXME: Switch to pointerMove(0, 0, {origin: element}) once
  // https://github.com/web-platform-tests/wpt/issues/41257 is fixed.
  await actions
      .pointerMove(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2), {})
      .send();
  await waitForRender();
}
function mouseOverAndRecord(t,element) {
  let timingInfo = {element, started: performance.now()};
  return (new test_driver.Actions())
      .pointerMove(0, 0, {origin: element})
      .send()
      .then(() => timingInfo);
}
function focusAndRecord(t,element) {
  let timingInfo = {element, started: performance.now()};
  element.focus();
  return timingInfo;
}
async function hoverOrFocus(invokerMethod,element) {
  if (invokerMethod === 'hover') {
    await hoverOver(element);
  } else {
    assert_equals(invokerMethod,'focus');
    element.focus();
    await waitForRender();
  }
}
async function mouseOverOrFocusAndRecord(t,invokerMethod,element) {
  if (invokerMethod === 'hover') {
    return await mouseOverAndRecord(t,element);
  } else {
    assert_equals(invokerMethod,'focus');
    return focusAndRecord(t,element);
  }
}
// Note that this may err on the side of being too large (reporting a number
// that is larger than the actual time since the mouseover happened), due to how
// `timingInfo.started` is initialized, on first mouse move. However, this
// function is intended to be used as a detector for the test harness taking too
// long for some tests, so it's ok to be conservative.
function msSinceMouseOver(timingInfo) {
  return performance.now() - timingInfo.started;
}
async function waitForHoverTime(hoverWaitTimeMs) {
  await new Promise(resolve => step_timeout(resolve,hoverWaitTimeMs));
  await waitForRender();
};

async function createPopoverAndInvokerForHoverTests(test, showdelayMs, hideDelayMs) {
  const unrelated = document.createElement('div');
  unrelated.tabIndex = 0;
  document.body.appendChild(unrelated);
  unrelated.textContent = 'Unrelated';
  unrelated.setAttribute('style','position:fixed; top:0;');
  // Ensure we never hover over or focus on an active interesttarget element.
  unrelated.focus();
  await hoverOver(unrelated);
  const popover = document.createElement('div');
  popover.popover = 'auto';
  popover.setAttribute('style','inset:auto; top: 100px;');
  popover.textContent = 'Popover';
  document.body.appendChild(popover);
  let invoker = document.createElement('button');
  invoker.interestTargetElement = popover;
  invoker.setAttribute('style',`
    interest-target-show-delay: ${showdelayMs}ms;
    interest-target-hide-delay: ${hideDelayMs}ms;
    position:fixed;
    top:200px;
    width:fit-content;
    height:fit-content;
    `);
  invoker.innerText = 'Invoker';
  document.body.appendChild(invoker);
  const actualShowDelay = Number(getComputedStyle(invoker).interestTargetShowDelay.slice(0,-1))*1000;
  assert_equals(actualShowDelay,showdelayMs,'interest-target-show-delay is incorrect');
  const actualHideDelay = Number(getComputedStyle(invoker).interestTargetHideDelay.slice(0,-1))*1000;
  assert_equals(actualHideDelay,hideDelayMs,'interest-target-hide-delay is incorrect');
  test.add_cleanup(() => {
    popover.remove();
    invoker.remove();
    unrelated.remove();
  });
  assert_false(popover.matches(':popover-open'),'The popover should start out closed');
  return {popover, invoker, unrelated};
}
async function sendLoseInterestHotkey() {
  const kEscape = '\uE00C';
  await new test_driver.Actions()
    .keyDown(kEscape)
    .keyUp(kEscape)
    .send();
  await waitForRender();
}
async function sendShowInterestHotkey() {
  const kAlt = "\uE00A";
  const kArrowUp = '\uE013';
  await new test_driver.Actions()
    .keyDown(kAlt)
    .keyDown(kArrowUp)
    .keyUp(kArrowUp)
    .keyUp(kAlt)
    .send();
  await waitForRender();
}
