popover-utils.js (8492B)
1 function waitForRender() { 2 return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); 3 } 4 5 async function clickOn(element, useTouch) { 6 await waitForRender(); 7 let rect = element.getBoundingClientRect(); 8 let actions = new test_driver.Actions(); 9 // FIXME: Switch to pointerMove(0, 0, {origin: element}) once 10 // https://github.com/web-platform-tests/wpt/issues/41257 is fixed. 11 if (useTouch) { 12 actions.addPointer('touch1', 'touch'); 13 } 14 await actions 15 .pointerMove(Math.round(rect.x + rect.width / 2), Math.round(rect.y + rect.height / 2), {}) 16 .pointerDown({button: actions.ButtonType.LEFT}) 17 .pointerUp({button: actions.ButtonType.LEFT}) 18 .send(); 19 await waitForRender(); 20 } 21 async function sendTab() { 22 await waitForRender(); 23 const kTab = '\uE004'; 24 await test_driver.send_keys(document.activeElement || document.documentElement, kTab); 25 await waitForRender(); 26 } 27 async function sendShiftTab() { 28 await waitForRender(); 29 const kShift = '\uE008'; 30 const kTab = '\uE004'; 31 await new test_driver.Actions() 32 .keyDown(kShift) 33 .keyDown(kTab) 34 .keyUp(kTab) 35 .keyUp(kShift) 36 .send(); 37 await waitForRender(); 38 } 39 async function sendEscape() { 40 await waitForRender(); 41 await test_driver.send_keys(document.activeElement || document.documentElement,'\uE00C'); // Escape 42 await waitForRender(); 43 } 44 async function sendEnter() { 45 await waitForRender(); 46 await test_driver.send_keys(document.activeElement || document.documentElement,'\uE007'); // Enter 47 await waitForRender(); 48 } 49 function isElementVisible(el) { 50 return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); 51 } 52 async function finishAnimations(popover) { 53 popover.getAnimations({subtree: true}).forEach(animation => animation.finish()); 54 await waitForRender(); 55 } 56 57 // This is a "polyfill" of sorts for the `defaultopen` attribute. 58 // It can be called before window.load is complete, and it will 59 // show defaultopen popovers according to the rules previously part 60 // of the popover API: any popover=manual popover can be shown this 61 // way, and only the first popover=auto popover. 62 function showDefaultopenPopoversOnLoad() { 63 function show() { 64 const popovers = Array.from(document.querySelectorAll('[popover][defaultopen]')); 65 popovers.forEach((p) => { 66 // The showPopover calls below aren't guarded by a check on the popover 67 // open/closed status. If they throw exceptions, this function was 68 // probably called at a bad time. However, a check is made for open 69 // <dialog open> elements. 70 if (p instanceof HTMLDialogElement && p.hasAttribute('open')) 71 return; 72 switch (p.popover) { 73 case 'auto': 74 if (!document.querySelector('[popover]:popover-open')) 75 p.showPopover(); 76 return; 77 case 'manual': 78 p.showPopover(); 79 return; 80 default: 81 assert_unreached(`Unknown popover type ${p.popover}`); 82 } 83 }); 84 } 85 if (document.readyState === 'complete') { 86 show(); 87 } else { 88 window.addEventListener('load',show,{once:true}); 89 } 90 } 91 92 function assertPopoverVisibility(popover, isPopover, expectedVisibility, message) { 93 const isVisible = isElementVisible(popover); 94 assert_equals(isVisible, expectedVisibility,`${message}: Expected this element to be ${expectedVisibility ? "visible" : "not visible"}`); 95 // Check other things related to being visible or not: 96 if (isVisible) { 97 assert_not_equals(window.getComputedStyle(popover).display,'none'); 98 assert_equals(popover.matches(':popover-open'),isPopover,`${message}: Visible popovers should match :popover-open`); 99 } else { 100 assert_equals(window.getComputedStyle(popover).display,'none',`${message}: Non-showing popovers should have display:none`); 101 assert_false(popover.matches(':popover-open'),`${message}: Non-showing popovers should *not* match :popover-open`); 102 } 103 } 104 105 function assertIsFunctionalPopover(popover, checkVisibility) { 106 assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'A popover should start out hidden'); 107 popover.showPopover(); 108 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After showPopover(), a popover should be visible'); 109 popover.showPopover(); // Calling showPopover on a showing popover should not throw. 110 popover.hidePopover(); 111 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After hidePopover(), a popover should be hidden'); 112 popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. 113 popover.togglePopover(); 114 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover() on hidden popover, it should be visible'); 115 popover.togglePopover(); 116 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover() on visible popover, it should be hidden'); 117 popover.togglePopover(/*force=*/true); 118 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on hidden popover, it should be visible'); 119 popover.togglePopover(/*force=*/true); 120 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/true, 'After togglePopover(true) on visible popover, it should be visible'); 121 popover.togglePopover(/*force=*/false); 122 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on visible popover, it should be hidden'); 123 popover.togglePopover(/*force=*/false); 124 if (checkVisibility) assertPopoverVisibility(popover, /*isPopover*/true, /*expectedVisibility*/false, 'After togglePopover(false) on hidden popover, it should be hidden'); 125 const parent = popover.parentElement; 126 popover.remove(); 127 assert_throws_dom("InvalidStateError",() => popover.showPopover(),'Calling showPopover on a disconnected popover should throw InvalidStateError'); 128 popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw. 129 assert_throws_dom("InvalidStateError",() => popover.togglePopover(),'Calling hidePopover on a disconnected popover should throw InvalidStateError'); 130 parent.appendChild(popover); 131 } 132 133 function assertNotAPopover(nonPopover) { 134 // If the non-popover element nonetheless has a 'popover' attribute, it should 135 // be invisible. Otherwise, it should be visible. 136 const expectVisible = !nonPopover.hasAttribute('popover'); 137 assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'A non-popover should start out visible'); 138 assert_throws_dom("NotSupportedError",() => nonPopover.showPopover(),'Calling showPopover on a non-popover should throw NotSupported'); 139 assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling showPopover on a non-popover should leave it visible'); 140 assert_throws_dom("NotSupportedError",() => nonPopover.hidePopover(),'Calling hidePopover on a non-popover should throw NotSupported'); 141 assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling hidePopover on a non-popover should leave it visible'); 142 assert_throws_dom("NotSupportedError",() => nonPopover.togglePopover(),'Calling togglePopover on a non-popover should throw NotSupported'); 143 assertPopoverVisibility(nonPopover, /*isPopover*/false, expectVisible, 'Calling togglePopover on a non-popover should leave it visible'); 144 } 145 146 async function verifyFocusOrder(order,description) { 147 order[0].focus(); 148 for(let i=0;i<order.length;++i) { 149 // Press tab between each check, excluding first (because it should already be focused) 150 // and the last (because tabbing after the last element may send focus into browser chrome). 151 if (i != 0) { 152 await sendTab(); 153 } 154 const control = order[i]; 155 assert_equals(document.activeElement,control,`${description}: Step ${i+1}`); 156 } 157 for(let i=order.length-1;i>=0;--i) { 158 const control = order[i]; 159 assert_equals(document.activeElement,control,`${description}: Step ${i+1} (backwards)`); 160 // Press shift+tab between each check, excluding last (because it should already be focused) 161 // and the first (because shift+tabbing after the last element may send focus into browser chrome). 162 if (i != 0) { 163 await sendShiftTab(); 164 } 165 } 166 }