extensionUtils: Add InjectionManager
It is fairly common for extensions to monkey-patch existing classes. Add a small helper class that makes this a tad bit more convenient. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2809>
This commit is contained in:
parent
7c4b1d4ae6
commit
f70a75a905
@ -29,3 +29,71 @@ export class Extension extends ExtensionBase {
|
|||||||
export const {
|
export const {
|
||||||
gettext, ngettext, pgettext,
|
gettext, ngettext, pgettext,
|
||||||
} = Extension.defineTranslationFunctions();
|
} = Extension.defineTranslationFunctions();
|
||||||
|
|
||||||
|
export class InjectionManager {
|
||||||
|
#savedMethods = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback CreateOverrideFunc
|
||||||
|
* @param {Function?} originalMethod - the original method if it exists
|
||||||
|
* @returns {Function} - a function to be used as override
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify, replace or inject a method
|
||||||
|
*
|
||||||
|
* @param {object} prototype - the object (or prototype) that is modified
|
||||||
|
* @param {string} methodName - the name of the overwritten method
|
||||||
|
* @param {CreateOverrideFunc} createOverrideFunc - function to call to create the override
|
||||||
|
*/
|
||||||
|
overrideMethod(prototype, methodName, createOverrideFunc) {
|
||||||
|
const originalMethod = this._saveMethod(prototype, methodName);
|
||||||
|
prototype[methodName] = createOverrideFunc(originalMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the original method
|
||||||
|
*
|
||||||
|
* @param {object} prototype - the object (or prototype) that is modified
|
||||||
|
* @param {string} methodName - the name of the method to restore
|
||||||
|
*/
|
||||||
|
restoreMethod(prototype, methodName) {
|
||||||
|
const savedProtoMethods = this.#savedMethods.get(prototype);
|
||||||
|
if (!savedProtoMethods)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const originalMethod = savedProtoMethods.get(methodName);
|
||||||
|
if (originalMethod === undefined)
|
||||||
|
delete prototype[methodName];
|
||||||
|
else
|
||||||
|
prototype[methodName] = originalMethod;
|
||||||
|
|
||||||
|
savedProtoMethods.delete(methodName);
|
||||||
|
if (savedProtoMethods.size === 0)
|
||||||
|
this.#savedMethods.delete(prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore all original methods and clear overrides
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
for (const [proto, map] of this.#savedMethods) {
|
||||||
|
map.forEach(
|
||||||
|
(_, methodName) => this.restoreMethod(proto, methodName));
|
||||||
|
}
|
||||||
|
console.assert(this.#savedMethods.size === 0,
|
||||||
|
`${this.#savedMethods.size} overrides left after clear()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveMethod(prototype, methodName) {
|
||||||
|
let savedProtoMethods = this.#savedMethods.get(prototype);
|
||||||
|
if (!savedProtoMethods) {
|
||||||
|
savedProtoMethods = new Map();
|
||||||
|
this.#savedMethods.set(prototype, savedProtoMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalMethod = prototype[methodName];
|
||||||
|
savedProtoMethods.set(methodName, originalMethod);
|
||||||
|
return originalMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,6 +20,7 @@ dbus_runner = configure_file(
|
|||||||
|
|
||||||
unit_tests = [
|
unit_tests = [
|
||||||
'highlighter',
|
'highlighter',
|
||||||
|
'injectionManager',
|
||||||
'insertSorted',
|
'insertSorted',
|
||||||
'jsParse',
|
'jsParse',
|
||||||
'markup',
|
'markup',
|
||||||
|
93
tests/unit/injectionManager.js
Normal file
93
tests/unit/injectionManager.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
const JsUnit = imports.jsUnit;
|
||||||
|
|
||||||
|
import '../../js/ui/environment.js';
|
||||||
|
|
||||||
|
import {InjectionManager} from '../../js/extensions/extension.js';
|
||||||
|
|
||||||
|
class Object1 {
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
getNumber() {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount() {
|
||||||
|
return ++this.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} object to modify
|
||||||
|
*/
|
||||||
|
function addInjections(object) {
|
||||||
|
// extend original method
|
||||||
|
injectionManager.overrideMethod(
|
||||||
|
object, 'getNumber', originalMethod => {
|
||||||
|
return function () {
|
||||||
|
// eslint-disable-next-line no-invalid-this
|
||||||
|
const num = originalMethod.call(this);
|
||||||
|
return 2 * num;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// override original method
|
||||||
|
injectionManager.overrideMethod(
|
||||||
|
object, 'getCount', () => {
|
||||||
|
return function () {
|
||||||
|
return 42;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// inject new method
|
||||||
|
injectionManager.overrideMethod(
|
||||||
|
object, 'getOtherNumber', () => {
|
||||||
|
return function () {
|
||||||
|
return 42;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const injectionManager = new InjectionManager();
|
||||||
|
let obj;
|
||||||
|
|
||||||
|
// Prototype injections
|
||||||
|
addInjections(Object1.prototype);
|
||||||
|
|
||||||
|
obj = new Object1();
|
||||||
|
|
||||||
|
// new obj is modified
|
||||||
|
JsUnit.assertEquals(obj.getNumber(), 84);
|
||||||
|
JsUnit.assertEquals(obj.getCount(), 42);
|
||||||
|
JsUnit.assertEquals(obj.count, 0);
|
||||||
|
JsUnit.assertEquals(obj.getOtherNumber(), 42);
|
||||||
|
|
||||||
|
injectionManager.clear();
|
||||||
|
|
||||||
|
obj = new Object1();
|
||||||
|
|
||||||
|
// new obj is unmodified
|
||||||
|
JsUnit.assertEquals(obj.getNumber(), 42);
|
||||||
|
JsUnit.assertEquals(obj.getCount(), obj.count);
|
||||||
|
JsUnit.assert(obj.count > 0);
|
||||||
|
JsUnit.assertRaises(() => obj.getOtherNumber());
|
||||||
|
|
||||||
|
// instance injections
|
||||||
|
addInjections(obj);
|
||||||
|
|
||||||
|
// obj is now modified
|
||||||
|
JsUnit.assertEquals(obj.getNumber(), 84);
|
||||||
|
JsUnit.assertEquals(obj.getCount(), 42);
|
||||||
|
JsUnit.assertEquals(obj.count, 1);
|
||||||
|
JsUnit.assertEquals(obj.getOtherNumber(), 42);
|
||||||
|
|
||||||
|
injectionManager.restoreMethod(obj, 'getNumber');
|
||||||
|
JsUnit.assertEquals(obj.getNumber(), 42);
|
||||||
|
|
||||||
|
injectionManager.clear();
|
||||||
|
|
||||||
|
// obj is unmodified again
|
||||||
|
JsUnit.assertEquals(obj.getNumber(), 42);
|
||||||
|
JsUnit.assertEquals(obj.getCount(), obj.count);
|
||||||
|
JsUnit.assert(obj.count > 0);
|
||||||
|
JsUnit.assertRaises(() => obj.getOtherNumber());
|
Loading…
Reference in New Issue
Block a user