1(function() { 2 "use strict"; 3 var idCounter = 0; 4 5 function getInViewCenterPoint(rect) { 6 var left = Math.max(0, rect.left); 7 var right = Math.min(window.innerWidth, rect.right); 8 var top = Math.max(0, rect.top); 9 var bottom = Math.min(window.innerHeight, rect.bottom); 10 11 var x = 0.5 * (left + right); 12 var y = 0.5 * (top + bottom); 13 14 return [x, y]; 15 } 16 17 function getPointerInteractablePaintTree(element) { 18 if (!window.document.contains(element)) { 19 return []; 20 } 21 22 var rectangles = element.getClientRects(); 23 24 if (rectangles.length === 0) { 25 return []; 26 } 27 28 var centerPoint = getInViewCenterPoint(rectangles[0]); 29 30 if ("elementsFromPoint" in document) { 31 return document.elementsFromPoint(centerPoint[0], centerPoint[1]); 32 } else if ("msElementsFromPoint" in document) { 33 var rv = document.msElementsFromPoint(centerPoint[0], centerPoint[1]); 34 return Array.prototype.slice.call(rv ? rv : []); 35 } else { 36 throw new Error("document.elementsFromPoint unsupported"); 37 } 38 } 39 40 function inView(element) { 41 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 42 return pointerInteractablePaintTree.indexOf(element) !== -1; 43 } 44 45 46 /** 47 * @namespace 48 */ 49 window.test_driver = { 50 /** 51 * Trigger user interaction in order to grant additional privileges to 52 * a provided function. 53 * 54 * https://html.spec.whatwg.org/#triggered-by-user-activation 55 * 56 * @param {String} intent - a description of the action which much be 57 * triggered by user interaction 58 * @param {Function} action - code requiring escalated privileges 59 * 60 * @returns {Promise} fulfilled following user interaction and 61 * execution of the provided `action` function; 62 * rejected if interaction fails or the provided 63 * function throws an error 64 */ 65 bless: function(intent, action) { 66 var button = document.createElement("button"); 67 button.innerHTML = "This test requires user interaction.<br />" + 68 "Please click here to allow " + intent + "."; 69 button.id = "wpt-test-driver-bless-" + (idCounter += 1); 70 const elem = document.body || document.documentElement; 71 elem.appendChild(button); 72 73 return new Promise(function(resolve, reject) { 74 button.addEventListener("click", resolve); 75 76 test_driver.click(button).catch(reject); 77 }).then(function() { 78 button.remove(); 79 80 if (typeof action === "function") { 81 return action(); 82 } 83 }); 84 }, 85 86 /** 87 * Triggers a user-initiated click 88 * 89 * This matches the behaviour of the {@link 90 * https://w3c.github.io/webdriver/webdriver-spec.html#element-click|WebDriver 91 * Element Click command}. 92 * 93 * @param {Element} element - element to be clicked 94 * @returns {Promise} fulfilled after click occurs, or rejected in 95 * the cases the WebDriver command errors 96 */ 97 click: function(element) { 98 if (window.top !== window) { 99 return Promise.reject(new Error("can only click in top-level window")); 100 } 101 102 if (!window.document.contains(element)) { 103 return Promise.reject(new Error("element in different document or shadow tree")); 104 } 105 106 if (!inView(element)) { 107 element.scrollIntoView({behavior: "instant", 108 block: "end", 109 inline: "nearest"}); 110 } 111 112 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 113 if (pointerInteractablePaintTree.length === 0 || 114 !element.contains(pointerInteractablePaintTree[0])) { 115 return Promise.reject(new Error("element click intercepted error")); 116 } 117 118 var rect = element.getClientRects()[0]; 119 var centerPoint = getInViewCenterPoint(rect); 120 return window.test_driver_internal.click(element, 121 {x: centerPoint[0], 122 y: centerPoint[1]}); 123 }, 124 125 /** 126 * Send keys to an element 127 * 128 * This matches the behaviour of the {@link 129 * https://w3c.github.io/webdriver/webdriver-spec.html#element-send-keys|WebDriver 130 * Send Keys command}. 131 * 132 * @param {Element} element - element to send keys to 133 * @param {String} keys - keys to send to the element 134 * @returns {Promise} fulfilled after keys are sent, or rejected in 135 * the cases the WebDriver command errors 136 */ 137 send_keys: function(element, keys) { 138 if (window.top !== window) { 139 return Promise.reject(new Error("can only send keys in top-level window")); 140 } 141 142 if (!window.document.contains(element)) { 143 return Promise.reject(new Error("element in different document or shadow tree")); 144 } 145 146 if (!inView(element)) { 147 element.scrollIntoView({behavior: "instant", 148 block: "end", 149 inline: "nearest"}); 150 } 151 152 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 153 if (pointerInteractablePaintTree.length === 0 || 154 !element.contains(pointerInteractablePaintTree[0])) { 155 return Promise.reject(new Error("element send_keys intercepted error")); 156 } 157 158 return window.test_driver_internal.send_keys(element, keys); 159 }, 160 161 /** 162 * Freeze the current page 163 * 164 * The freeze function transitions the page from the HIDDEN state to 165 * the FROZEN state as described in {@link 166 * https://github.com/WICG/page-lifecycle/blob/master/README.md|Lifecycle API 167 * for Web Pages} 168 * 169 * @returns {Promise} fulfilled after the freeze request is sent, or rejected 170 * in case the WebDriver command errors 171 */ 172 freeze: function() { 173 return window.test_driver_internal.freeze(); 174 }, 175 176 /** 177 * Send a sequence of actions 178 * 179 * This function sends a sequence of actions to the top level window 180 * to perform. It is modeled after the behaviour of {@link 181 * https://w3c.github.io/webdriver/#actions|WebDriver Actions Command} 182 * 183 * @param {Array} actions - an array of actions. The format is the same as the actions 184 property of the WebDriver command {@link 185 https://w3c.github.io/webdriver/#perform-actions|Perform 186 Actions} command. Each element is an object representing an 187 input source and each input source itself has an actions 188 property detailing the behaviour of that source at each timestep 189 (or tick). Authors are not expected to construct the actions 190 sequence by hand, but to use the builder api provided in 191 testdriver-actions.js 192 * @returns {Promise} fufiled after the actions are performed, or rejected in 193 * the cases the WebDriver command errors 194 */ 195 action_sequence: function(actions) { 196 return window.test_driver_internal.action_sequence(actions); 197 }, 198 199 /** 200 * Generates a test report on the current page 201 * 202 * The generate_test_report function generates a report (to be observed 203 * by ReportingObserver) for testing purposes, as described in 204 * {@link https://w3c.github.io/reporting/#generate-test-report-command} 205 * 206 * @returns {Promise} fulfilled after the report is generated, or 207 * rejected if the report generation fails 208 */ 209 generate_test_report: function(message) { 210 return window.test_driver_internal.generate_test_report(message); 211 } 212 }; 213 214 window.test_driver_internal = { 215 /** 216 * This flag should be set to `true` by any code which implements the 217 * internal methods defined below for automation purposes. Doing so 218 * allows the library to signal failure immediately when an automated 219 * implementation of one of the methods is not available. 220 */ 221 in_automation: false, 222 223 /** 224 * Waits for a user-initiated click 225 * 226 * @param {Element} element - element to be clicked 227 * @param {{x: number, y: number} coords - viewport coordinates to click at 228 * @returns {Promise} fulfilled after click occurs 229 */ 230 click: function(element, coords) { 231 if (this.in_automation) { 232 return Promise.reject(new Error('Not implemented')); 233 } 234 235 return new Promise(function(resolve, reject) { 236 element.addEventListener("click", resolve); 237 }); 238 }, 239 240 /** 241 * Waits for an element to receive a series of key presses 242 * 243 * @param {Element} element - element which should receve key presses 244 * @param {String} keys - keys to expect 245 * @returns {Promise} fulfilled after keys are received or rejected if 246 * an incorrect key sequence is received 247 */ 248 send_keys: function(element, keys) { 249 if (this.in_automation) { 250 return Promise.reject(new Error('Not implemented')); 251 } 252 253 return new Promise(function(resolve, reject) { 254 var seen = ""; 255 256 function remove() { 257 element.removeEventListener("keydown", onKeyDown); 258 } 259 260 function onKeyDown(event) { 261 if (event.key.length > 1) { 262 return; 263 } 264 265 seen += event.key; 266 267 if (keys.indexOf(seen) !== 0) { 268 reject(new Error("Unexpected key sequence: " + seen)); 269 remove(); 270 } else if (seen === keys) { 271 resolve(); 272 remove(); 273 } 274 } 275 276 element.addEventListener("keydown", onKeyDown); 277 }); 278 }, 279 280 /** 281 * Freeze the current page 282 * 283 * @returns {Promise} fulfilled after freeze request is sent, otherwise 284 * it gets rejected 285 */ 286 freeze: function() { 287 return Promise.reject(new Error("unimplemented")); 288 }, 289 290 /** 291 * Send a sequence of pointer actions 292 * 293 * @returns {Promise} fufilled after actions are sent, rejected if any actions 294 * fail 295 */ 296 action_sequence: function(actions) { 297 return Promise.reject(new Error("unimplemented")); 298 }, 299 300 /** 301 * Generates a test report on the current page 302 * 303 * @param {String} message - the message to be contained in the report 304 * @returns {Promise} fulfilled after the report is generated, or 305 * rejected if the report generation fails 306 */ 307 generate_test_report: function(message) { 308 return Promise.reject(new Error("unimplemented")); 309 } 310 }; 311})(); 312