utils.js (11403B)
1 /** 2 * GlobalOverrider - Utility that allows you to override properties on the global object. 3 * See unit-entry.js for example usage. 4 */ 5 export class GlobalOverrider { 6 constructor() { 7 this.originalGlobals = new Map(); 8 this.sandbox = sinon.createSandbox(); 9 } 10 11 /** 12 * _override - Internal method to override properties on the global object. 13 * The first time a given key is overridden, we cache the original 14 * value in this.originalGlobals so that later it can be restored. 15 * 16 * @param {string} key The identifier of the property 17 * @param {any} value The value to which the property should be reassigned 18 */ 19 _override(key, value) { 20 if (!this.originalGlobals.has(key)) { 21 this.originalGlobals.set(key, global[key]); 22 } 23 global[key] = value; 24 } 25 26 /** 27 * set - Override a given property, or all properties on an object 28 * 29 * @param {string|object} key If a string, the identifier of the property 30 * If an object, a number of properties and values to which they should be reassigned. 31 * @param {any} value The value to which the property should be reassigned 32 * @return {type} description 33 */ 34 set(key, value) { 35 if (!value && typeof key === "object") { 36 const overrides = key; 37 Object.keys(overrides).forEach(k => this._override(k, overrides[k])); 38 } else { 39 this._override(key, value); 40 } 41 return value; 42 } 43 44 /** 45 * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared. 46 * You probably want to call this after each test. 47 */ 48 reset() { 49 this.sandbox.reset(); 50 } 51 52 /** 53 * restore - Restore the global sandbox and reset all overriden properties to 54 * their original values. You should call this after all tests have completed. 55 */ 56 restore() { 57 this.sandbox.restore(); 58 this.originalGlobals.forEach((value, key) => { 59 global[key] = value; 60 }); 61 } 62 } 63 64 /** 65 * A map of mocked preference names and values, used by `FakensIPrefBranch`, 66 * `FakensIPrefService`, and `FakePrefs`. 67 * 68 * Tests should add entries to this map for any preferences they'd like to set, 69 * and remove any entries during teardown for preferences that shouldn't be 70 * shared between tests. 71 */ 72 export const FAKE_GLOBAL_PREFS = new Map(); 73 74 /** 75 * Very simple fake for the most basic semantics of nsIPrefBranch. Lots of 76 * things aren't yet supported. Feel free to add them in. 77 * 78 * @param {object} args - optional arguments 79 * @param {Function} args.initHook - if present, will be called back 80 * inside the constructor. Typically used from tests 81 * to save off a pointer to the created instance so that 82 * stubs and spies can be inspected by the test code. 83 */ 84 export class FakensIPrefBranch { 85 PREF_INVALID = "invalid"; 86 PREF_INT = "integer"; 87 PREF_BOOL = "boolean"; 88 PREF_STRING = "string"; 89 90 constructor(args) { 91 if (args) { 92 if ("initHook" in args) { 93 args.initHook.call(this); 94 } 95 if (args.defaultBranch) { 96 this.prefs = new Map(); 97 } else { 98 this.prefs = FAKE_GLOBAL_PREFS; 99 } 100 } else { 101 this.prefs = FAKE_GLOBAL_PREFS; 102 } 103 this._prefBranch = {}; 104 this.observers = new Map(); 105 } 106 addObserver(prefix, callback) { 107 this.observers.set(prefix, callback); 108 } 109 removeObserver(prefix, callback) { 110 this.observers.delete(prefix, callback); 111 } 112 setStringPref(prefName, value) { 113 this.set(prefName, value); 114 } 115 getStringPref(prefName, defaultValue) { 116 return this.get(prefName, defaultValue); 117 } 118 setBoolPref(prefName, value) { 119 this.set(prefName, value); 120 } 121 getBoolPref(prefName, defaultValue) { 122 return this.get(prefName, defaultValue); 123 } 124 setIntPref(prefName, value) { 125 this.set(prefName, value); 126 } 127 getIntPref(prefName) { 128 return this.get(prefName); 129 } 130 setCharPref(prefName, value) { 131 this.set(prefName, value); 132 } 133 getCharPref(prefName) { 134 return this.get(prefName); 135 } 136 clearUserPref(prefName) { 137 this.prefs.delete(prefName); 138 } 139 get(prefName, defaultValue) { 140 let value = this.prefs.get(prefName); 141 return typeof value === "undefined" ? defaultValue : value; 142 } 143 getPrefType(prefName) { 144 let value = this.prefs.get(prefName); 145 switch (typeof value) { 146 case "number": 147 return this.PREF_INT; 148 149 case "boolean": 150 return this.PREF_BOOL; 151 152 case "string": 153 return this.PREF_STRING; 154 155 default: 156 return this.PREF_INVALID; 157 } 158 } 159 set(prefName, value) { 160 this.prefs.set(prefName, value); 161 162 // Trigger all observers for prefixes of the changed pref name. This matches 163 // the semantics of `nsIPrefBranch`. 164 let observerPrefixes = [...this.observers.keys()].filter(prefix => 165 prefName.startsWith(prefix) 166 ); 167 for (let observerPrefix of observerPrefixes) { 168 this.observers.get(observerPrefix)("", "", prefName); 169 } 170 } 171 getChildList(prefix) { 172 return [...this.prefs.keys()].filter(prefName => 173 prefName.startsWith(prefix) 174 ); 175 } 176 prefHasUserValue(prefName) { 177 return this.prefs.has(prefName); 178 } 179 prefIsLocked(_prefName) { 180 return false; 181 } 182 } 183 184 /** 185 * A fake `Services.prefs` implementation that extends `FakensIPrefBranch` 186 * with methods specific to `nsIPrefService`. 187 */ 188 export class FakensIPrefService extends FakensIPrefBranch { 189 getBranch() {} 190 getDefaultBranch(_prefix) { 191 return { 192 setBoolPref() {}, 193 setIntPref() {}, 194 setStringPref() {}, 195 clearUserPref() {}, 196 }; 197 } 198 } 199 200 /** 201 * Very simple fake for the most basic semantics of Preferences.sys.mjs. 202 * Extends FakensIPrefBranch. 203 */ 204 export class FakePrefs extends FakensIPrefBranch { 205 observe(prefName, callback) { 206 super.addObserver(prefName, callback); 207 } 208 ignore(prefName, callback) { 209 super.removeObserver(prefName, callback); 210 } 211 observeBranch(_listener) {} 212 ignoreBranch(_listener) {} 213 set(prefName, value) { 214 this.prefs.set(prefName, value); 215 216 // Trigger observers for just the changed pref name, not any of its 217 // prefixes. This matches the semantics of `Preferences.sys.mjs`. 218 if (this.observers.has(prefName)) { 219 this.observers.get(prefName)(value); 220 } 221 } 222 } 223 224 /** 225 * Slimmed down version of toolkit/modules/EventEmitter.sys.mjs 226 */ 227 export function EventEmitter() {} 228 EventEmitter.decorate = function (objectToDecorate) { 229 let emitter = new EventEmitter(); 230 objectToDecorate.on = emitter.on.bind(emitter); 231 objectToDecorate.off = emitter.off.bind(emitter); 232 objectToDecorate.once = emitter.once.bind(emitter); 233 objectToDecorate.emit = emitter.emit.bind(emitter); 234 }; 235 EventEmitter.prototype = { 236 on(event, listener) { 237 if (!this._eventEmitterListeners) { 238 this._eventEmitterListeners = new Map(); 239 } 240 if (!this._eventEmitterListeners.has(event)) { 241 this._eventEmitterListeners.set(event, []); 242 } 243 this._eventEmitterListeners.get(event).push(listener); 244 }, 245 off(event, listener) { 246 if (!this._eventEmitterListeners) { 247 return; 248 } 249 let listeners = this._eventEmitterListeners.get(event); 250 if (listeners) { 251 this._eventEmitterListeners.set( 252 event, 253 listeners.filter( 254 l => l !== listener && l._originalListener !== listener 255 ) 256 ); 257 } 258 }, 259 once(event, listener) { 260 return new Promise(resolve => { 261 let handler = (_, first, ...rest) => { 262 this.off(event, handler); 263 if (listener) { 264 listener(event, first, ...rest); 265 } 266 resolve(first); 267 }; 268 269 handler._originalListener = listener; 270 this.on(event, handler); 271 }); 272 }, 273 // All arguments to this method will be sent to listeners 274 emit(event, ...args) { 275 if ( 276 !this._eventEmitterListeners || 277 !this._eventEmitterListeners.has(event) 278 ) { 279 return; 280 } 281 let originalListeners = this._eventEmitterListeners.get(event); 282 for (let listener of this._eventEmitterListeners.get(event)) { 283 // If the object was destroyed during event emission, stop 284 // emitting. 285 if (!this._eventEmitterListeners) { 286 break; 287 } 288 // If listeners were removed during emission, make sure the 289 // event handler we're going to fire wasn't removed. 290 if ( 291 originalListeners === this._eventEmitterListeners.get(event) || 292 this._eventEmitterListeners.get(event).some(l => l === listener) 293 ) { 294 try { 295 listener(event, ...args); 296 } catch (ex) { 297 // error with a listener 298 } 299 } 300 } 301 }, 302 }; 303 304 export function FakePerformance() {} 305 FakePerformance.prototype = { 306 marks: new Map(), 307 now() { 308 return window.performance.now(); 309 }, 310 timing: { navigationStart: 222222.123 }, 311 get timeOrigin() { 312 return 10000.234; 313 }, 314 // XXX assumes type == "mark" 315 getEntriesByName(name, _type) { 316 if (this.marks.has(name)) { 317 return this.marks.get(name); 318 } 319 return []; 320 }, 321 callsToMark: 0, 322 323 /** 324 * Note: The "startTime" for each mark is simply the number of times mark 325 * has been called in this object. 326 */ 327 mark(name) { 328 let markObj = { 329 name, 330 entryType: "mark", 331 startTime: ++this.callsToMark, 332 duration: 0, 333 }; 334 335 if (this.marks.has(name)) { 336 this.marks.get(name).push(markObj); 337 return; 338 } 339 340 this.marks.set(name, [markObj]); 341 }, 342 }; 343 344 /** 345 * addNumberReducer - a simple dummy reducer for testing that adds a number 346 */ 347 export function addNumberReducer(prevState = 0, action) { 348 return action.type === "ADD" ? prevState + action.data : prevState; 349 } 350 351 export class FakeConsoleAPI { 352 static LOG_LEVELS = { 353 all: Number.MIN_VALUE, 354 debug: 2, 355 log: 3, 356 info: 3, 357 clear: 3, 358 trace: 3, 359 timeEnd: 3, 360 time: 3, 361 assert: 3, 362 group: 3, 363 groupEnd: 3, 364 profile: 3, 365 profileEnd: 3, 366 dir: 3, 367 dirxml: 3, 368 warn: 4, 369 error: 5, 370 off: Number.MAX_VALUE, 371 }; 372 373 constructor({ prefix = "", maxLogLevel = "all" } = {}) { 374 this.prefix = prefix; 375 this.prefixStr = prefix ? `${prefix}: ` : ""; 376 this.maxLogLevel = maxLogLevel; 377 378 for (const level of Object.keys(FakeConsoleAPI.LOG_LEVELS)) { 379 // eslint-disable-next-line no-console 380 if (typeof console[level] === "function") { 381 this[level] = this.shouldLog(level) 382 ? this._log.bind(this, level) 383 : () => {}; 384 } 385 } 386 } 387 shouldLog(level) { 388 return ( 389 FakeConsoleAPI.LOG_LEVELS[this.maxLogLevel] <= 390 FakeConsoleAPI.LOG_LEVELS[level] 391 ); 392 } 393 _log(level, ...args) { 394 console[level](this.prefixStr, ...args); // eslint-disable-line no-console 395 } 396 } 397 398 export function FakeNimbusFeature() { 399 return { 400 getEnrollmentMetadata() {}, 401 getVariable() {}, 402 getAllVariables() {}, 403 getAllEnrollments() {}, 404 onUpdate() {}, 405 offUpdate() {}, 406 }; 407 } 408 409 export function FakeNimbusFeatures(featureIds) { 410 return Object.fromEntries( 411 featureIds.map(featureId => [featureId, FakeNimbusFeature()]) 412 ); 413 } 414 415 export class FakeLogger extends FakeConsoleAPI { 416 constructor() { 417 super({ 418 // Don't use a prefix because the first instance gets cached and reused by 419 // other consumers that would otherwise pass their own identifying prefix. 420 maxLogLevel: "off", // Change this to "debug" or "all" to get more logging in tests 421 }); 422 } 423 }