| <!DOCTYPE html> |
| <meta charset="utf-8" /> |
| <title>The hoverpopup attribute</title> |
| <link rel="author" href="mailto:masonf@chromium.org"> |
| <link rel=help href="https://open-ui.org/components/popup.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/popup-utils.js"></script> |
| |
| <body> |
| <style> |
| .unrelated {top:0;} |
| .invoker {top:100px; width:fit-content; height:fit-content;} |
| [popup] {top: 200px;} |
| .offset-child {top:300px; left:300px;} |
| </style> |
| <script> |
| const hoverPopUpDelay = 100; // The CSS delay setting. |
| const hoverWaitTime = 200; // How long to wait to cover the delay for sure. |
| let nextId = 0; |
| async function makePopUpAndInvoker(test, popUpType, invokerType, delayMs) { |
| delayMs = delayMs || hoverPopUpDelay; |
| const popUp = Object.assign(document.createElement('div'),{popUp: popUpType, id: `pop-up-${nextId++}`}); |
| document.body.appendChild(popUp); |
| popUp.textContent = 'Pop-up'; |
| let invoker = document.createElement('div'); |
| invoker.setAttribute('class','invoker'); |
| invoker.setAttribute('hoverpopup',popUp.id); |
| invoker.setAttribute('style',`hover-pop-up-delay: ${delayMs}ms;`); |
| document.body.appendChild(invoker); |
| const originalInvoker = invoker; |
| const reassignPopupFn = (p) => originalInvoker.setAttribute('hoverpopup',p.id); |
| switch (invokerType) { |
| case 'plain': |
| // Invoker is just a div. |
| invoker.textContent = 'Invoker'; |
| break; |
| case 'nested': |
| // Invoker is just a div containing a div. |
| const child1 = invoker.appendChild(document.createElement('div')); |
| child1.textContent = 'Invoker'; |
| break; |
| case 'nested-offset': |
| // Invoker is a child of the element wearing the 'hoverpopup' attribute, |
| // and is not contained within the bounds of the hoverpopup 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 () => { |
| popUp.remove(); |
| invoker.remove(); |
| originalInvoker.remove(); |
| unrelated.remove(); |
| await waitForRender(); |
| }); |
| await mouseOver(unrelated); // Start by mousing over the unrelated element |
| await waitForRender(); |
| return {popUp,invoker,reassignPopupFn}; |
| } |
| let mouseOverStarted; |
| function mouseOver(element) { |
| mouseOverStarted = performance.now(); |
| return (new test_driver.Actions()) |
| .pointerMove(0, 0, {origin: element}) |
| .send(); |
| } |
| function msSinceMouseOver() { |
| return performance.now() - mouseOverStarted; |
| } |
| async function waitForHoverTime() { |
| await new Promise(resolve => step_timeout(resolve,hoverWaitTime)); |
| await waitForRender(); |
| }; |
| |
| // NOTE about testing methodology: |
| // This test checks whether pop-ups 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 pop-up 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 {popUp,invoker} = await makePopUpAndInvoker(t,type,invokerType); |
| assert_false(popUp.matches(':top-layer')); |
| await mouseOver(invoker); |
| let showing = popUp.matches(':top-layer'); |
| // See NOTE above. |
| if (msSinceMouseOver() < hoverPopUpDelay) |
| assert_false(showing,'pop-up should not show immediately'); |
| await waitForHoverTime(); |
| assert_true(hoverWaitTime > hoverPopUpDelay,'hoverPopUpDelay is the CSS setting, hoverWaitTime should be longer than that'); |
| assert_true(msSinceMouseOver() >= hoverPopUpDelay,'waitForHoverTime should wait longer than hoverPopUpDelay'); |
| assert_true(popUp.matches(':top-layer'),'pop-up should show after delay'); |
| popUp.hidePopUp(); // Cleanup |
| },`hoverpopup attribute shows a pop-up with popup=${type}, invokerType=${invokerType}`); |
| |
| promise_test(async (t) => { |
| const longerHoverDelay = hoverWaitTime*2; |
| const {popUp,invoker} = await makePopUpAndInvoker(t,type,invokerType,longerHoverDelay); |
| await mouseOver(invoker); |
| let showing = popUp.matches(':top-layer'); |
| // See NOTE above. |
| if (msSinceMouseOver() >= longerHoverDelay) |
| return; // The WPT runner was too slow. |
| assert_false(showing,'pop-up should not show immediately'); |
| await waitForHoverTime(); |
| showing = popUp.matches(':top-layer'); |
| if (msSinceMouseOver() >= longerHoverDelay) |
| return; // The WPT runner was too slow. |
| assert_false(showing,'pop-up should not show after not long enough of a delay'); |
| },`hoverpopup hover-pop-up-delay is respected (popup=${type}, invokerType=${invokerType})`); |
| |
| promise_test(async (t) => { |
| const {popUp,invoker} = await makePopUpAndInvoker(t,type,invokerType); |
| popUp.showPopUp(); |
| assert_true(popUp.matches(':top-layer')); |
| await mouseOver(invoker); |
| assert_true(popUp.matches(':top-layer'),'pop-up should stay showing on mouseover'); |
| await waitForHoverTime(); |
| assert_true(popUp.matches(':top-layer'),'pop-up should stay showing after delay'); |
| popUp.hidePopUp(); // Cleanup |
| },`hoverpopup attribute does nothing when pop-up is already showing (popup=${type}, invokerType=${invokerType})`); |
| |
| promise_test(async (t) => { |
| const {popUp,invoker} = await makePopUpAndInvoker(t,type,invokerType); |
| await mouseOver(invoker); |
| let showing = popUp.matches(':top-layer'); |
| popUp.remove(); |
| // See NOTE above. |
| if (msSinceMouseOver() >= hoverPopUpDelay) |
| return; // The WPT runner was too slow. |
| assert_false(showing,'pop-up should not show immediately'); |
| await waitForHoverTime(); |
| assert_false(popUp.matches(':top-layer'),'pop-up should not show even after a delay'); |
| // Now put it back in the document and make sure it doesn't trigger. |
| document.body.appendChild(popUp); |
| await waitForHoverTime(); |
| assert_false(popUp.matches(':top-layer'),'pop-up should not show even when returned to the document'); |
| },`hoverpopup attribute does nothing when pop-up is moved out of the document (popup=${type}, invokerType=${invokerType})`); |
| |
| promise_test(async (t) => { |
| const {popUp,invoker,reassignPopupFn} = await makePopUpAndInvoker(t,type,invokerType); |
| const popUp2 = Object.assign(document.createElement('div'),{popUp: type, id: 'foobar'}); |
| document.body.appendChild(popUp2); |
| t.add_cleanup(() => popUp2.remove()); |
| await mouseOver(invoker); |
| let eitherShowing = popUp.matches(':top-layer') || popUp2.matches(':top-layer'); |
| reassignPopupFn(popUp2); |
| // See NOTE above. |
| if (msSinceMouseOver() >= hoverPopUpDelay) |
| return; // The WPT runner was too slow. |
| assert_false(eitherShowing,'pop-up should not show immediately'); |
| await waitForHoverTime(); |
| assert_false(popUp.matches(':top-layer'),'pop-up #1 should not show since hoverpopup was reassigned'); |
| assert_false(popUp2.matches(':top-layer'),'pop-up #2 should not show since hoverpopup was reassigned'); |
| },`hoverpopup attribute does nothing when target changes (popup=${type}, invokerType=${invokerType})`); |
| }); |
| }); |
| </script> |