<!DOCTYPE html>
<meta charset="utf-8" />
<title>The popovertargetaction=hover behavior</title>
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popover.research.explainer">
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/popover-utils.js"></script>

<body>
<style>
.unrelated {top:0;}
.invoker {top:100px; width:fit-content; height:fit-content;}
[popover] {top: 200px;}
.offset-child {top:300px; left:300px;}
</style>

<script>
const popoverShowDelay = 100; // The CSS delay setting.
const hoverWaitTime = 200; // How long to wait to cover the delay for sure.
async function makePopoverAndInvoker(test, popoverType, invokerType, delayMs) {
  delayMs = delayMs || popoverShowDelay;
  const popover = Object.assign(document.createElement('div'),{popover: popoverType});
  document.body.appendChild(popover);
  popover.textContent = 'Popover';
  // Set popover-show-delay on the popover to 0 - it should be ignored.
  popover.setAttribute('style',`popover-show-delay: 0; popover-hide-delay: 1000s;`);
  let invoker = document.createElement('button');
  invoker.setAttribute('class','invoker');
  invoker.popoverTargetElement = popover;
  invoker.popoverTargetAction = "hover";
  // Set popover-hide-delay on the invoker to 0 - it should be ignored.
  invoker.setAttribute('style',`popover-show-delay: ${delayMs}ms; popover-hide-delay: 0;`);
  document.body.appendChild(invoker);
  const actualHoverDelay = Number(getComputedStyle(invoker)['popoverShowDelay'].slice(0,-1))*1000;
  assert_equals(actualHoverDelay,delayMs,'popover-show-delay is incorrect');
  const originalInvoker = invoker;
  const reassignPopoverFn = (p) => {originalInvoker.popoverTargetElement = p};
  switch (invokerType) {
    case 'plain':
      // Invoker is just a button.
      invoker.textContent = 'Invoker';
      break;
    case 'nested':
      // Invoker is just a button containing a div.
      const child1 = invoker.appendChild(document.createElement('div'));
      child1.textContent = 'Invoker';
      break;
    case 'nested-offset':
      // Invoker is a child of the invoking button, and is not contained within
      // the bounds of the popovertarget element.
      invoker.textContent = 'Invoker';
      // Reassign invoker to the child:
      invoker = invoker.appendChild(document.createElement('div'));
      invoker.textContent = 'Invoker child';
      invoker.setAttribute('class','offset-child');
      break;
    case 'none':
      // No invoker.
      invoker.remove();
      break;
    default:
      assert_unreached(`Invalid invokerType ${invokerType}`);
  }
  const unrelated = document.createElement('div');
  document.body.appendChild(unrelated);
  unrelated.textContent = 'Unrelated';
  unrelated.setAttribute('class','unrelated');
  test.add_cleanup(async () => {
    popover.remove();
    invoker.remove();
    originalInvoker.remove();
    unrelated.remove();
    await waitForRender();
  });
  await mouseOver(unrelated); // Start by mousing over the unrelated element
  await waitForRender();
  return {popover,invoker,reassignPopoverFn};
}

// NOTE about testing methodology:
// This test checks whether popovers are triggered *after* the appropriate hover
// delay. The delay used for testing is kept low, to avoid this test taking too
// long, but that means that sometimes on a slow bot/client, the hover delay can
// elapse before we are able to check the popover status. And that can make this
// test flaky. To avoid that, the msSinceMouseOver() function is used to check
// that not-too-much time has passed, and if it has, the test is simply skipped.

["auto","hint","manual"].forEach(type => {
  ["plain","nested","nested-offset"].forEach(invokerType => {
    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      assert_false(popover.matches(':popover-open'));
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      // See NOTE above.
      if (msSinceMouseOver() < popoverShowDelay)
        assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_true(msSinceMouseOver() >= hoverWaitTime,'waitForHoverTime should wait the specified time');
      assert_true(popover.matches(':popover-open'),'popover should show after delay');
      assert_true(hoverWaitTime > popoverShowDelay,'popoverShowDelay is the CSS setting, hoverWaitTime should be longer than that');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover shows a popover with popover=${type}, invokerType=${invokerType}`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      assert_false(popover.matches(':popover-open'));
      invoker.click(); // Click the invoker
      assert_true(popover.matches(':popover-open'),'Clicking the invoker should show the popover, even when popovertargetaction=hover');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover should also allow click activation, for popover=${type}, invokerType=${invokerType}`);

    promise_test(async (t) => {
      const longerHoverDelay = hoverWaitTime*2;
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType,longerHoverDelay);
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      // See NOTE above.
      if (msSinceMouseOver() >= longerHoverDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      showing = popover.matches(':popover-open');
      if (msSinceMouseOver() >= longerHoverDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show after not long enough of a delay');
    },`popovertargetaction=hover popover-show-delay is respected (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      popover.showPopover();
      assert_true(popover.matches(':popover-open'));
      await mouseOver(invoker);
      assert_true(popover.matches(':popover-open'),'popover should stay showing on mouseover');
      await waitForHoverTime(hoverWaitTime);
      assert_true(popover.matches(':popover-open'),'popover should stay showing after delay');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover does nothing when popover is already showing (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      popover.remove();
      // See NOTE above.
      if (msSinceMouseOver() >= popoverShowDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover should not show even after a delay');
      // Now put it back in the document and make sure it doesn't trigger.
      document.body.appendChild(popover);
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover should not show even when returned to the document');
    },`popovertargetaction=hover does nothing when popover is moved out of the document (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker,reassignPopoverFn} = await makePopoverAndInvoker(t,type,invokerType);
      const popover2 = Object.assign(document.createElement('div'),{popover: type});
      document.body.appendChild(popover2);
      t.add_cleanup(() => popover2.remove());
      await mouseOver(invoker);
      let eitherShowing = popover.matches(':popover-open') || popover2.matches(':popover-open');
      reassignPopoverFn(popover2);
      // See NOTE above.
      if (msSinceMouseOver() >= popoverShowDelay)
        return; // The WPT runner was too slow.
      assert_false(eitherShowing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover #1 should not show since popovertarget was reassigned');
      assert_false(popover2.matches(':popover-open'),'popover #2 should not show since popovertarget was reassigned');
    },`popovertargetaction=hover does nothing when target changes (popover=${type}, invokerType=${invokerType})`);
  });
});
</script>
