ContentTaskUtils.sys.mjs (8012B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * This module implements a number of utility functions that can be loaded 7 * into content scope. 8 * 9 * All asynchronous helper methods should return promises, rather than being 10 * callback based. 11 */ 12 13 // Disable ownerGlobal use since that's not available on content-privileged elements. 14 15 /* eslint-disable mozilla/use-ownerGlobal */ 16 17 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; 18 19 export var ContentTaskUtils = { 20 /** 21 * Checks if a DOM element is hidden. 22 * 23 * @param {Element} element 24 * The element which is to be checked. 25 * 26 * @return {boolean} 27 */ 28 isHidden(element) { 29 let style = element.ownerDocument.defaultView.getComputedStyle(element); 30 if (style.display == "none") { 31 return true; 32 } 33 if (style.visibility != "visible") { 34 return true; 35 } 36 37 // Hiding a parent element will hide all its children 38 if ( 39 element.parentNode != element.ownerDocument && 40 element.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE 41 ) { 42 return ContentTaskUtils.isHidden(element.parentNode); 43 } 44 45 // Walk up the shadow DOM if we've reached the top of the shadow root 46 if (element.parentNode.host) { 47 return ContentTaskUtils.isHidden(element.parentNode.host); 48 } 49 50 return false; 51 }, 52 53 /** 54 * Checks if a DOM element is visible. 55 * 56 * @param {Element} element 57 * The element which is to be checked. 58 * 59 * @return {boolean} 60 */ 61 isVisible(element) { 62 return !this.isHidden(element); 63 }, 64 65 /** 66 * Will poll a condition function until it returns true. 67 * 68 * @param condition 69 * A condition function that must return true or false. If the 70 * condition ever throws, this is also treated as a false. 71 * @param msg 72 * The message to use when the returned promise is rejected. 73 * This message will be extended with additional information 74 * about the number of tries or the thrown exception. 75 * @param interval 76 * The time interval to poll the condition function. Defaults 77 * to 100ms. 78 * @param maxTries 79 * The number of times to poll before giving up and rejecting 80 * if the condition has not yet returned true. Defaults to 50 81 * (~5 seconds for 100ms intervals) 82 * @return Promise 83 * Resolves when condition is true. 84 * Rejects if timeout is exceeded or condition ever throws. 85 */ 86 async waitForCondition(condition, msg, interval = 100, maxTries = 50) { 87 let startTime = ChromeUtils.now(); 88 for (let tries = 0; tries < maxTries; ++tries) { 89 await new Promise(resolve => setTimeout(resolve, interval)); 90 91 let conditionPassed = false; 92 try { 93 conditionPassed = await condition(); 94 } catch (e) { 95 msg += ` - threw exception: ${e}`; 96 ChromeUtils.addProfilerMarker( 97 "ContentTaskUtils", 98 { startTime, category: "Test" }, 99 `waitForCondition - ${msg}` 100 ); 101 throw msg; 102 } 103 if (conditionPassed) { 104 ChromeUtils.addProfilerMarker( 105 "ContentTaskUtils", 106 { startTime, category: "Test" }, 107 `waitForCondition succeeded after ${tries} retries - ${msg}` 108 ); 109 return conditionPassed; 110 } 111 } 112 113 msg += ` - timed out after ${maxTries} tries.`; 114 ChromeUtils.addProfilerMarker( 115 "ContentTaskUtils", 116 { startTime, category: "Test" }, 117 `waitForCondition - ${msg}` 118 ); 119 throw msg; 120 }, 121 122 /** 123 * Waits for an event to be fired on a specified element. 124 * 125 * Usage: 126 * let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName"); 127 * // Do some processing here that will cause the event to be fired 128 * // ... 129 * // Now yield until the Promise is fulfilled 130 * let receivedEvent = yield promiseEvent; 131 * 132 * @param {Element} subject 133 * The element that should receive the event. 134 * @param {string} eventName 135 * Name of the event to listen to. 136 * @param {bool} capture [optional] 137 * True to use a capturing listener. 138 * @param {function} checkFn [optional] 139 * Called with the Event object as argument, should return true if the 140 * event is the expected one, or false if it should be ignored and 141 * listening should continue. If not specified, the first event with 142 * the specified name resolves the returned promise. 143 * 144 * Note: Because this function is intended for testing, any error in checkFn 145 * will cause the returned promise to be rejected instead of waiting for 146 * the next event, since this is probably a bug in the test. 147 * 148 * @returns {Promise<Event>} 149 */ 150 waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted = false) { 151 return new Promise((resolve, reject) => { 152 let startTime = ChromeUtils.now(); 153 subject.addEventListener( 154 eventName, 155 function listener(event) { 156 try { 157 if (checkFn && !checkFn(event)) { 158 return; 159 } 160 subject.removeEventListener(eventName, listener, capture); 161 setTimeout(() => { 162 ChromeUtils.addProfilerMarker( 163 "ContentTaskUtils", 164 { category: "Test", startTime }, 165 "waitForEvent - " + eventName 166 ); 167 resolve(event); 168 }, 0); 169 } catch (ex) { 170 try { 171 subject.removeEventListener(eventName, listener, capture); 172 } catch (ex2) { 173 // Maybe the provided object does not support removeEventListener. 174 } 175 setTimeout(() => reject(ex), 0); 176 } 177 }, 178 capture, 179 wantsUntrusted 180 ); 181 }); 182 }, 183 184 /** 185 * Wait until DOM mutations cause the condition expressed in checkFn to pass. 186 * Intended as an easy-to-use alternative to waitForCondition. 187 * 188 * @param {Element} subject 189 * The element on which to observe mutations. 190 * @param {object} options 191 * The options to pass to MutationObserver.observe(); 192 * @param {function} checkFn [optional] 193 * Function that returns true when it wants the promise to be resolved. 194 * If not specified, the first mutation will resolve the promise. 195 * 196 * @returns {Promise<void>} 197 */ 198 waitForMutationCondition(subject, options, checkFn) { 199 if (checkFn?.()) { 200 return Promise.resolve(); 201 } 202 return new Promise(resolve => { 203 let obs = new subject.ownerGlobal.MutationObserver(function () { 204 if (checkFn && !checkFn()) { 205 return; 206 } 207 obs.disconnect(); 208 resolve(); 209 }); 210 obs.observe(subject, options); 211 }); 212 }, 213 214 /** 215 * Gets an instance of the `EventUtils` helper module for usage in 216 * content tasks. See https://searchfox.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js 217 * 218 * @param content 219 * The `content` global object from your content task. 220 * 221 * @returns an EventUtils instance. 222 */ 223 getEventUtils(content) { 224 if (content._EventUtils) { 225 return content._EventUtils; 226 } 227 228 let EventUtils = (content._EventUtils = {}); 229 230 EventUtils.window = {}; 231 EventUtils.setTimeout = setTimeout; 232 EventUtils.parent = EventUtils.window; 233 /* eslint-disable camelcase */ 234 EventUtils._EU_Ci = Ci; 235 EventUtils._EU_Cc = Cc; 236 /* eslint-enable camelcase */ 237 // EventUtils' `sendChar` function relies on the navigator to synthetize events. 238 EventUtils.navigator = content.navigator; 239 EventUtils.KeyboardEvent = content.KeyboardEvent; 240 241 Services.scriptloader.loadSubScript( 242 "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", 243 EventUtils 244 ); 245 246 return EventUtils; 247 }, 248 };