function waitForRender() { return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); } function waitForTick() { return new Promise(resolve => step_timeout(resolve, 0)); } async function clickOn(element) { const actions = new test_driver.Actions(); await waitForRender(); await actions.pointerMove(0, 0, {origin: element}) .pointerDown({button: actions.ButtonType.LEFT}) .pointerUp({button: actions.ButtonType.LEFT}) .send(); await waitForRender(); } async function sendTab() { await waitForRender(); const kTab = '\uE004'; await new test_driver.send_keys(document.documentElement,kTab); await waitForRender(); } // Waiting for crbug.com/893480: // async function sendShiftTab() { // await waitForRender(); // const kShift = '\uE008'; // const kTab = '\uE004'; // await new test_driver.Actions() // .keyDown(kShift) // .keyDown(kTab) // .keyUp(kTab) // .keyUp(kShift) // .send(); // await waitForRender(); // } async function sendEscape() { await waitForRender(); await new test_driver.send_keys(document.documentElement,'\uE00C'); // Escape await waitForRender(); } async function sendEnter() { await waitForRender(); await new test_driver.send_keys(document.documentElement,'\uE007'); // Enter await waitForRender(); } function isElementVisible(el) { return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); } function isTopLayer(el) { // A bit of a hack. Just test a few properties of the ::backdrop pseudo // element that change when in the top layer. const properties = ['right','background']; const testEl = document.createElement('div'); document.body.appendChild(testEl); const computedStyle = getComputedStyle(testEl, '::backdrop'); const nonTopLayerValues = properties.map(p => computedStyle[p]); testEl.remove(); for(let i=0;i animation.finish()); await waitForRender(); } let mousemoveInfo; function mouseOver(element) { mousemoveInfo?.controller?.abort(); const controller = new AbortController(); mousemoveInfo = {element, controller, moved: false, started: performance.now()}; return (new test_driver.Actions()) .pointerMove(0, 0, {origin: element}) .send() .then(() => { document.addEventListener("mousemove", (e) => {mousemoveInfo.moved = true;}, {signal: controller.signal}); }) } function msSinceMouseOver() { return performance.now() - mousemoveInfo.started; } function assertMouseStillOver(element) { assert_equals(mousemoveInfo.element, element, 'Broken test harness'); assert_false(mousemoveInfo.moved,'Broken test harness'); } async function waitForHoverTime(hoverWaitTimeMs) { await new Promise(resolve => step_timeout(resolve,hoverWaitTimeMs)); await waitForRender(); }; async function mouseHover(element,hoverWaitTimeMs) { await mouseOver(element); await waitForHoverTime(hoverWaitTimeMs); assertMouseStillOver(element); } async function blessTopLayer(visibleElement) { // The normal "bless" function doesn't work well when there are top layer // elements blocking clicks. Additionally, since the normal test_driver.bless // function just adds a button to the main document and clicks it, we can't // call that in the presence of open popovers, since that click will close them. const button = document.createElement('button'); button.innerHTML = "Click me to activate"; visibleElement.appendChild(button); let wait_click = new Promise(resolve => button.addEventListener("click", resolve, {once: true})); await test_driver.click(button); await wait_click; button.remove(); } // This is a "polyfill" of sorts for the `defaultopen` attribute. // It can be called before window.load is complete, and it will // show defaultopen popovers according to the rules previously part // of the popover API: any popover=manual popover can be shown this // way, and only the first popover=auto popover. function showDefaultopenPopoversOnLoad() { function show() { const popovers = Array.from(document.querySelectorAll('[popover][defaultopen]')); popovers.forEach((p) => { // The showPopover calls below aren't guarded by a check on the popover // open/closed status. If they throw exceptions, this function was // probably called at a bad time. However, a check is made for open // elements. if (p instanceof HTMLDialogElement && p.hasAttribute('open')) return; switch (p.popover) { case 'auto': if (!document.querySelector('[popover]:popover-open')) p.showPopover(); return; case 'manual': p.showPopover(); return; default: assert_unreached(`Unknown popover type ${p.popover}`); } }); } if (document.readyState === 'complete') { show(); } else { window.addEventListener('load',show,{once:true}); } } function popoverHintSupported() { // TODO(crbug.com/1416284): This function should be removed, and // any calls replaced with `true`, once popover=hint ships. const testElement = document.createElement('div'); testElement.popover = 'hint'; return testElement.popover === 'hint'; } function assertPopoverVisibility(popover, isPopover, expectedVisibility, message) { const isVisible = isElementVisible(popover); assert_equals(isVisible, expectedVisibility,`${message}: Expected this element to be ${expectedVisibility ? "visible" : "not visible"}`); // Check other things related to being visible or not: if (isVisible) { assert_not_equals(window.getComputedStyle(popover).display,'none'); assert_equals(popover.matches(':popover-open'),isPopover,`${message}: Visible popovers should match :popover-open`); } else { assert_equals(window.getComputedStyle(popover).display,'none',`${message}: Non-showing popovers should have display:none`); assert_false(popover.matches(':popover-open'),`${message}: Non-showing popovers should *not* match :popover-open`); } } function assertIsFunctionalPopover(popover, checkVisibility) { assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'A popover should start out hidden'); popover.showPopover(); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After showPopover(), a popover should be visible'); popover.showPopover(); // Calling showPopover on a showing popover should not throw. popover.hidePopover(); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After hidePopover(), a popover should be hidden'); popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. popover.togglePopover(); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover() on hidden popover, it should be visible'); popover.togglePopover(); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover() on visible popover, it should be hidden'); popover.togglePopover(/*force=*/true); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on hidden popover, it should be visible'); popover.togglePopover(/*force=*/true); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on visible popover, it should be visible'); popover.togglePopover(/*force=*/false); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on visible popover, it should be hidden'); popover.togglePopover(/*force=*/false); if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on hidden popover, it should be hidden'); const parent = popover.parentElement; popover.remove(); assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a disconnected popover should throw InvalidStateError'); popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw. assert_throws_dom("InvalidStateError",() => popover.togglePopover(),'Calling hidePopover on a disconnected popover should throw InvalidStateError'); parent.appendChild(popover); } function assertNotAPopover(nonPopover) { // If the non-popover element nonetheless has a 'popover' attribute, it should // be invisible. Otherwise, it should be visible. const expectVisible = !nonPopover.hasAttribute('popover'); assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'A non-popover should start out visible'); assert_throws_dom("NotSupportedError",() => nonPopover.showPopover(),'Calling showPopover on a non-popover should throw NotSupported'); assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling showPopover on a non-popover should leave it visible'); assert_throws_dom("NotSupportedError",() => nonPopover.hidePopover(),'Calling hidePopover on a non-popover should throw NotSupported'); assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling hidePopover on a non-popover should leave it visible'); assert_throws_dom("NotSupportedError",() => nonPopover.togglePopover(),'Calling togglePopover on a non-popover should throw NotSupported'); assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling togglePopover on a non-popover should leave it visible'); }