utils.js (8024B)
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) { 122 return this.get(prefName); 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 export class FakeConsoleAPI { 225 static LOG_LEVELS = { 226 all: Number.MIN_VALUE, 227 debug: 2, 228 log: 3, 229 info: 3, 230 clear: 3, 231 trace: 3, 232 timeEnd: 3, 233 time: 3, 234 assert: 3, 235 group: 3, 236 groupEnd: 3, 237 profile: 3, 238 profileEnd: 3, 239 dir: 3, 240 dirxml: 3, 241 warn: 4, 242 error: 5, 243 off: Number.MAX_VALUE, 244 }; 245 246 constructor({ prefix = "", maxLogLevel = "all" } = {}) { 247 this.prefix = prefix; 248 this.prefixStr = prefix ? `${prefix}: ` : ""; 249 this.maxLogLevel = maxLogLevel; 250 251 for (const level of Object.keys(FakeConsoleAPI.LOG_LEVELS)) { 252 // eslint-disable-next-line no-console 253 if (typeof console[level] === "function") { 254 this[level] = this.shouldLog(level) 255 ? this._log.bind(this, level) 256 : () => {}; 257 } 258 } 259 } 260 shouldLog(level) { 261 return ( 262 FakeConsoleAPI.LOG_LEVELS[this.maxLogLevel] <= 263 FakeConsoleAPI.LOG_LEVELS[level] 264 ); 265 } 266 _log(level, ...args) { 267 console[level](this.prefixStr, ...args); // eslint-disable-line no-console 268 } 269 } 270 271 export function FakeNimbusFeature() { 272 return { 273 getEnrollmentMetadata() {}, 274 getVariable() {}, 275 getAllVariables() {}, 276 onUpdate() {}, 277 offUpdate() {}, 278 recordExposureEvent() {}, 279 }; 280 } 281 282 export function FakeNimbusFeatures(featureIds) { 283 return Object.fromEntries( 284 featureIds.map(featureId => [featureId, FakeNimbusFeature()]) 285 ); 286 } 287 288 export class FakeLogger extends FakeConsoleAPI { 289 constructor() { 290 super({ 291 // Don't use a prefix because the first instance gets cached and reused by 292 // other consumers that would otherwise pass their own identifying prefix. 293 maxLogLevel: "off", // Change this to "debug" or "all" to get more logging in tests 294 }); 295 } 296 }