422 lines
11 KiB
JavaScript
422 lines
11 KiB
JavaScript
/**
|
|
* GlobalOverrider - Utility that allows you to override properties on the global object.
|
|
* See unit-entry.js for example usage.
|
|
*/
|
|
export class GlobalOverrider {
|
|
constructor() {
|
|
this.originalGlobals = new Map();
|
|
this.sandbox = sinon.createSandbox();
|
|
}
|
|
|
|
/**
|
|
* _override - Internal method to override properties on the global object.
|
|
* The first time a given key is overridden, we cache the original
|
|
* value in this.originalGlobals so that later it can be restored.
|
|
*
|
|
* @param {string} key The identifier of the property
|
|
* @param {any} value The value to which the property should be reassigned
|
|
*/
|
|
_override(key, value) {
|
|
if (!this.originalGlobals.has(key)) {
|
|
this.originalGlobals.set(key, global[key]);
|
|
}
|
|
global[key] = value;
|
|
}
|
|
|
|
/**
|
|
* set - Override a given property, or all properties on an object
|
|
*
|
|
* @param {string|object} key If a string, the identifier of the property
|
|
* If an object, a number of properties and values to which they should be reassigned.
|
|
* @param {any} value The value to which the property should be reassigned
|
|
* @return {type} description
|
|
*/
|
|
set(key, value) {
|
|
if (!value && typeof key === "object") {
|
|
const overrides = key;
|
|
Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
|
|
} else {
|
|
this._override(key, value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
|
|
* You probably want to call this after each test.
|
|
*/
|
|
reset() {
|
|
this.sandbox.reset();
|
|
}
|
|
|
|
/**
|
|
* restore - Restore the global sandbox and reset all overriden properties to
|
|
* their original values. You should call this after all tests have completed.
|
|
*/
|
|
restore() {
|
|
this.sandbox.restore();
|
|
this.originalGlobals.forEach((value, key) => {
|
|
global[key] = value;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A map of mocked preference names and values, used by `FakensIPrefBranch`,
|
|
* `FakensIPrefService`, and `FakePrefs`.
|
|
*
|
|
* Tests should add entries to this map for any preferences they'd like to set,
|
|
* and remove any entries during teardown for preferences that shouldn't be
|
|
* shared between tests.
|
|
*/
|
|
export const FAKE_GLOBAL_PREFS = new Map();
|
|
|
|
/**
|
|
* Very simple fake for the most basic semantics of nsIPrefBranch. Lots of
|
|
* things aren't yet supported. Feel free to add them in.
|
|
*
|
|
* @param {Object} args - optional arguments
|
|
* @param {Function} args.initHook - if present, will be called back
|
|
* inside the constructor. Typically used from tests
|
|
* to save off a pointer to the created instance so that
|
|
* stubs and spies can be inspected by the test code.
|
|
*/
|
|
export class FakensIPrefBranch {
|
|
PREF_INVALID = "invalid";
|
|
PREF_INT = "integer";
|
|
PREF_BOOL = "boolean";
|
|
PREF_STRING = "string";
|
|
|
|
constructor(args) {
|
|
if (args) {
|
|
if ("initHook" in args) {
|
|
args.initHook.call(this);
|
|
}
|
|
if (args.defaultBranch) {
|
|
this.prefs = new Map();
|
|
} else {
|
|
this.prefs = FAKE_GLOBAL_PREFS;
|
|
}
|
|
} else {
|
|
this.prefs = FAKE_GLOBAL_PREFS;
|
|
}
|
|
this._prefBranch = {};
|
|
this.observers = new Map();
|
|
}
|
|
addObserver(prefix, callback) {
|
|
this.observers.set(prefix, callback);
|
|
}
|
|
removeObserver(prefix, callback) {
|
|
this.observers.delete(prefix, callback);
|
|
}
|
|
setStringPref(prefName, value) {
|
|
this.set(prefName, value);
|
|
}
|
|
getStringPref(prefName, defaultValue) {
|
|
return this.get(prefName, defaultValue);
|
|
}
|
|
setBoolPref(prefName, value) {
|
|
this.set(prefName, value);
|
|
}
|
|
getBoolPref(prefName) {
|
|
return this.get(prefName);
|
|
}
|
|
setIntPref(prefName, value) {
|
|
this.set(prefName, value);
|
|
}
|
|
getIntPref(prefName) {
|
|
return this.get(prefName);
|
|
}
|
|
setCharPref(prefName, value) {
|
|
this.set(prefName, value);
|
|
}
|
|
getCharPref(prefName) {
|
|
return this.get(prefName);
|
|
}
|
|
clearUserPref(prefName) {
|
|
this.prefs.delete(prefName);
|
|
}
|
|
get(prefName, defaultValue) {
|
|
let value = this.prefs.get(prefName);
|
|
return typeof value === "undefined" ? defaultValue : value;
|
|
}
|
|
getPrefType(prefName) {
|
|
let value = this.prefs.get(prefName);
|
|
switch (typeof value) {
|
|
case "number":
|
|
return this.PREF_INT;
|
|
|
|
case "boolean":
|
|
return this.PREF_BOOL;
|
|
|
|
case "string":
|
|
return this.PREF_STRING;
|
|
|
|
default:
|
|
return this.PREF_INVALID;
|
|
}
|
|
}
|
|
set(prefName, value) {
|
|
this.prefs.set(prefName, value);
|
|
|
|
// Trigger all observers for prefixes of the changed pref name. This matches
|
|
// the semantics of `nsIPrefBranch`.
|
|
let observerPrefixes = [...this.observers.keys()].filter(prefix =>
|
|
prefName.startsWith(prefix)
|
|
);
|
|
for (let observerPrefix of observerPrefixes) {
|
|
this.observers.get(observerPrefix)("", "", prefName);
|
|
}
|
|
}
|
|
getChildList(prefix) {
|
|
return [...this.prefs.keys()].filter(prefName =>
|
|
prefName.startsWith(prefix)
|
|
);
|
|
}
|
|
prefHasUserValue(prefName) {
|
|
return this.prefs.has(prefName);
|
|
}
|
|
prefIsLocked(_prefName) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A fake `Services.prefs` implementation that extends `FakensIPrefBranch`
|
|
* with methods specific to `nsIPrefService`.
|
|
*/
|
|
export class FakensIPrefService extends FakensIPrefBranch {
|
|
getBranch() {}
|
|
getDefaultBranch(_prefix) {
|
|
return {
|
|
setBoolPref() {},
|
|
setIntPref() {},
|
|
setStringPref() {},
|
|
clearUserPref() {},
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Very simple fake for the most basic semantics of Preferences.sys.mjs.
|
|
* Extends FakensIPrefBranch.
|
|
*/
|
|
export class FakePrefs extends FakensIPrefBranch {
|
|
observe(prefName, callback) {
|
|
super.addObserver(prefName, callback);
|
|
}
|
|
ignore(prefName, callback) {
|
|
super.removeObserver(prefName, callback);
|
|
}
|
|
observeBranch(_listener) {}
|
|
ignoreBranch(_listener) {}
|
|
set(prefName, value) {
|
|
this.prefs.set(prefName, value);
|
|
|
|
// Trigger observers for just the changed pref name, not any of its
|
|
// prefixes. This matches the semantics of `Preferences.sys.mjs`.
|
|
if (this.observers.has(prefName)) {
|
|
this.observers.get(prefName)(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Slimmed down version of toolkit/modules/EventEmitter.sys.mjs
|
|
*/
|
|
export function EventEmitter() {}
|
|
EventEmitter.decorate = function (objectToDecorate) {
|
|
let emitter = new EventEmitter();
|
|
objectToDecorate.on = emitter.on.bind(emitter);
|
|
objectToDecorate.off = emitter.off.bind(emitter);
|
|
objectToDecorate.once = emitter.once.bind(emitter);
|
|
objectToDecorate.emit = emitter.emit.bind(emitter);
|
|
};
|
|
EventEmitter.prototype = {
|
|
on(event, listener) {
|
|
if (!this._eventEmitterListeners) {
|
|
this._eventEmitterListeners = new Map();
|
|
}
|
|
if (!this._eventEmitterListeners.has(event)) {
|
|
this._eventEmitterListeners.set(event, []);
|
|
}
|
|
this._eventEmitterListeners.get(event).push(listener);
|
|
},
|
|
off(event, listener) {
|
|
if (!this._eventEmitterListeners) {
|
|
return;
|
|
}
|
|
let listeners = this._eventEmitterListeners.get(event);
|
|
if (listeners) {
|
|
this._eventEmitterListeners.set(
|
|
event,
|
|
listeners.filter(
|
|
l => l !== listener && l._originalListener !== listener
|
|
)
|
|
);
|
|
}
|
|
},
|
|
once(event, listener) {
|
|
return new Promise(resolve => {
|
|
let handler = (_, first, ...rest) => {
|
|
this.off(event, handler);
|
|
if (listener) {
|
|
listener(event, first, ...rest);
|
|
}
|
|
resolve(first);
|
|
};
|
|
|
|
handler._originalListener = listener;
|
|
this.on(event, handler);
|
|
});
|
|
},
|
|
// All arguments to this method will be sent to listeners
|
|
emit(event, ...args) {
|
|
if (
|
|
!this._eventEmitterListeners ||
|
|
!this._eventEmitterListeners.has(event)
|
|
) {
|
|
return;
|
|
}
|
|
let originalListeners = this._eventEmitterListeners.get(event);
|
|
for (let listener of this._eventEmitterListeners.get(event)) {
|
|
// If the object was destroyed during event emission, stop
|
|
// emitting.
|
|
if (!this._eventEmitterListeners) {
|
|
break;
|
|
}
|
|
// If listeners were removed during emission, make sure the
|
|
// event handler we're going to fire wasn't removed.
|
|
if (
|
|
originalListeners === this._eventEmitterListeners.get(event) ||
|
|
this._eventEmitterListeners.get(event).some(l => l === listener)
|
|
) {
|
|
try {
|
|
listener(event, ...args);
|
|
} catch (ex) {
|
|
// error with a listener
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
export function FakePerformance() {}
|
|
FakePerformance.prototype = {
|
|
marks: new Map(),
|
|
now() {
|
|
return window.performance.now();
|
|
},
|
|
timing: { navigationStart: 222222.123 },
|
|
get timeOrigin() {
|
|
return 10000.234;
|
|
},
|
|
// XXX assumes type == "mark"
|
|
getEntriesByName(name, _type) {
|
|
if (this.marks.has(name)) {
|
|
return this.marks.get(name);
|
|
}
|
|
return [];
|
|
},
|
|
callsToMark: 0,
|
|
|
|
/**
|
|
* @note The "startTime" for each mark is simply the number of times mark
|
|
* has been called in this object.
|
|
*/
|
|
mark(name) {
|
|
let markObj = {
|
|
name,
|
|
entryType: "mark",
|
|
startTime: ++this.callsToMark,
|
|
duration: 0,
|
|
};
|
|
|
|
if (this.marks.has(name)) {
|
|
this.marks.get(name).push(markObj);
|
|
return;
|
|
}
|
|
|
|
this.marks.set(name, [markObj]);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* addNumberReducer - a simple dummy reducer for testing that adds a number
|
|
*/
|
|
export function addNumberReducer(prevState = 0, action) {
|
|
return action.type === "ADD" ? prevState + action.data : prevState;
|
|
}
|
|
|
|
export class FakeConsoleAPI {
|
|
static LOG_LEVELS = {
|
|
all: Number.MIN_VALUE,
|
|
debug: 2,
|
|
log: 3,
|
|
info: 3,
|
|
clear: 3,
|
|
trace: 3,
|
|
timeEnd: 3,
|
|
time: 3,
|
|
assert: 3,
|
|
group: 3,
|
|
groupEnd: 3,
|
|
profile: 3,
|
|
profileEnd: 3,
|
|
dir: 3,
|
|
dirxml: 3,
|
|
warn: 4,
|
|
error: 5,
|
|
off: Number.MAX_VALUE,
|
|
};
|
|
|
|
constructor({ prefix = "", maxLogLevel = "all" } = {}) {
|
|
this.prefix = prefix;
|
|
this.prefixStr = prefix ? `${prefix}: ` : "";
|
|
this.maxLogLevel = maxLogLevel;
|
|
|
|
for (const level of Object.keys(FakeConsoleAPI.LOG_LEVELS)) {
|
|
// eslint-disable-next-line no-console
|
|
if (typeof console[level] === "function") {
|
|
this[level] = this.shouldLog(level)
|
|
? this._log.bind(this, level)
|
|
: () => {};
|
|
}
|
|
}
|
|
}
|
|
shouldLog(level) {
|
|
return (
|
|
FakeConsoleAPI.LOG_LEVELS[this.maxLogLevel] <=
|
|
FakeConsoleAPI.LOG_LEVELS[level]
|
|
);
|
|
}
|
|
_log(level, ...args) {
|
|
console[level](this.prefixStr, ...args); // eslint-disable-line no-console
|
|
}
|
|
}
|
|
|
|
export function FakeNimbusFeature() {
|
|
return {
|
|
getEnrollmentMetadata() {},
|
|
getVariable() {},
|
|
getAllVariables() {},
|
|
onUpdate() {},
|
|
offUpdate() {},
|
|
};
|
|
}
|
|
|
|
export function FakeNimbusFeatures(featureIds) {
|
|
return Object.fromEntries(
|
|
featureIds.map(featureId => [featureId, FakeNimbusFeature()])
|
|
);
|
|
}
|
|
|
|
export class FakeLogger extends FakeConsoleAPI {
|
|
constructor() {
|
|
super({
|
|
// Don't use a prefix because the first instance gets cached and reused by
|
|
// other consumers that would otherwise pass their own identifying prefix.
|
|
maxLogLevel: "off", // Change this to "debug" or "all" to get more logging in tests
|
|
});
|
|
}
|
|
}
|