[IMP] web: improve patch function
This commit refactors the patch function in order to simplify its code but also and most of all remove the `this._super` bound on each patched method call. The patch function now supports the javascript keyword `super` to get the parent's property. This removes constraints we had with `this._super`. We had to create a rather complicated intermediate function to bind `this._super` and this function was called each time before we called a patched method. It was annoying for debugging but necessary. Because of this intermediate function `this._super` was also had to re-bind each time we entered a patched method which was annoying with async method, we had to save the parent method in the patch before awaiting its execution. Finally, not a big issue but still, objects could not have a `_super` property as it would be hidden in the patch. task 3410198 Part-of: odoo/odoo#125716
This commit is contained in:
@@ -1,22 +1,60 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string;
|
||||
* patch: object;
|
||||
* pure: boolean;
|
||||
* }} PatchDescription
|
||||
* @typedef {{
|
||||
* originalProperties: Map<string, PropertyDescriptor>;
|
||||
* skeleton: object;
|
||||
* extensions: Set<object>;
|
||||
* }} PatchDescription
|
||||
*/
|
||||
|
||||
/** @type {WeakMap<object, PatchDescription>} */
|
||||
const patchDescriptions = new WeakMap();
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* original: object;
|
||||
* patches: PatchDescription[];
|
||||
* }} ObjectPatchDescription
|
||||
* Create or get the patch description for the given `objToPatch`.
|
||||
* @param {object} objToPatch
|
||||
* @returns {PatchDescription}
|
||||
*/
|
||||
function getPatchDescription(objToPatch) {
|
||||
if (!patchDescriptions.has(objToPatch)) {
|
||||
patchDescriptions.set(objToPatch, {
|
||||
originalProperties: new Map(),
|
||||
skeleton: Object.create(Object.getPrototypeOf(objToPatch)),
|
||||
extensions: new Set(),
|
||||
});
|
||||
}
|
||||
return patchDescriptions.get(objToPatch);
|
||||
}
|
||||
|
||||
/** @type {WeakMap<any, ObjectPatchDescription>} */
|
||||
const patchMap = new WeakMap();
|
||||
/**
|
||||
* @param {object} objToPatch
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isClassPrototype(objToPatch) {
|
||||
// class A {}
|
||||
// isClassPrototype(A) === false
|
||||
// isClassPrototype(A.prototype) === true
|
||||
// isClassPrototype(new A()) === false
|
||||
// isClassPrototype({}) === false
|
||||
return Object.hasOwn(objToPatch, "constructor") && objToPatch.constructor?.prototype === objToPatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the prototype chain to find a potential property.
|
||||
* @param {object} objToPatch
|
||||
* @param {string} key
|
||||
* @returns {object}
|
||||
*/
|
||||
function findAncestorPropertyDescriptor(objToPatch, key) {
|
||||
let descriptor = null;
|
||||
let prototype = objToPatch;
|
||||
do {
|
||||
descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
||||
prototype = Object.getPrototypeOf(prototype);
|
||||
} while (!descriptor && prototype);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch an object
|
||||
@@ -25,135 +63,74 @@ const patchMap = new WeakMap();
|
||||
* you want to patch static properties/methods.
|
||||
*
|
||||
* @template T
|
||||
* @template {Partial<T>} U
|
||||
* @param {T} obj Object to patch
|
||||
* @param {string} patchName
|
||||
* @param {U} patchValue
|
||||
* @param {{pure?: boolean}} [options]
|
||||
* @template {T} U
|
||||
* @param {T} objToPatch The object to patch
|
||||
* @param {U} extension The object containing the patched properties
|
||||
* @returns {() => void} Returns an unpatch function
|
||||
*/
|
||||
export function patch(obj, patchName, patchValue, options = {}) {
|
||||
if (typeof patchName !== "string") {
|
||||
throw new Error("Incorrect use of patch: second argument should be the patchName");
|
||||
export function patch(objToPatch, extension) {
|
||||
if (typeof extension === "string") {
|
||||
throw new Error(`Patch "${extension}": Second argument is not the patch name anymore, it should be the object containing the patched properties`);
|
||||
}
|
||||
const pure = Boolean(options.pure);
|
||||
if (!patchMap.has(obj)) {
|
||||
patchMap.set(obj, {
|
||||
original: {},
|
||||
patches: [],
|
||||
});
|
||||
}
|
||||
const objDesc = patchMap.get(obj);
|
||||
if (objDesc.patches.some((p) => p.name === patchName)) {
|
||||
throw new Error(`Class ${obj.name} already has a patch ${patchName}`);
|
||||
}
|
||||
objDesc.patches.push({
|
||||
name: patchName,
|
||||
patch: patchValue,
|
||||
pure,
|
||||
});
|
||||
|
||||
for (const k in patchValue) {
|
||||
let prevDesc = null;
|
||||
let proto = obj;
|
||||
do {
|
||||
prevDesc = Object.getOwnPropertyDescriptor(proto, k);
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
} while (!prevDesc && proto);
|
||||
const description = getPatchDescription(objToPatch);
|
||||
description.extensions.add(extension);
|
||||
|
||||
let newDesc = Object.getOwnPropertyDescriptor(patchValue, k);
|
||||
if (!Object.hasOwnProperty.call(objDesc.original, k)) {
|
||||
objDesc.original[k] = Object.getOwnPropertyDescriptor(obj, k);
|
||||
const properties = Object.getOwnPropertyDescriptors(extension);
|
||||
for (const [key, newProperty] of Object.entries(properties)) {
|
||||
const oldProperty = Object.getOwnPropertyDescriptor(objToPatch, key);
|
||||
if (oldProperty) {
|
||||
// Store the old property on the skeleton.
|
||||
Object.defineProperty(description.skeleton, key, oldProperty);
|
||||
}
|
||||
|
||||
if (prevDesc) {
|
||||
const patchedFnName = `${k} (patch ${patchName})`;
|
||||
if (!description.originalProperties.has(key)) {
|
||||
// Keep a trace of original property (prop before first patch), useful for unpatching.
|
||||
description.originalProperties.set(key, oldProperty);
|
||||
}
|
||||
|
||||
if (prevDesc.value && typeof newDesc.value === "function") {
|
||||
newDesc = { ...prevDesc, value: newDesc.value };
|
||||
makeIntermediateFunction("value", prevDesc, newDesc, patchedFnName);
|
||||
}
|
||||
if ((newDesc.get || newDesc.set) && (prevDesc.get || prevDesc.set)) {
|
||||
// get and set are defined together. If they are both defined
|
||||
// in the previous descriptor but only one in the new descriptor
|
||||
// then the other will be undefined so we need to apply the
|
||||
// previous descriptor in the new one.
|
||||
newDesc = {
|
||||
...prevDesc,
|
||||
get: newDesc.get || prevDesc.get,
|
||||
set: newDesc.set || prevDesc.set,
|
||||
};
|
||||
if (prevDesc.get && typeof newDesc.get === "function") {
|
||||
makeIntermediateFunction("get", prevDesc, newDesc, patchedFnName);
|
||||
}
|
||||
if (prevDesc.set && typeof newDesc.set === "function") {
|
||||
makeIntermediateFunction("set", prevDesc, newDesc, patchedFnName);
|
||||
}
|
||||
if (isClassPrototype(objToPatch)) {
|
||||
// A property is enumerable on POJO ({ prop: 1 }) but not on classes (class A {}).
|
||||
// Here, we only check if we patch a class prototype.
|
||||
newProperty.enumerable = false;
|
||||
}
|
||||
|
||||
if ((newProperty.get && 1) ^ (newProperty.set && 1)) {
|
||||
// get and set are defined together. If they are both defined
|
||||
// in the previous descriptor but only one in the new descriptor
|
||||
// then the other will be undefined so we need to apply the
|
||||
// previous descriptor in the new one.
|
||||
const ancestorProperty = findAncestorPropertyDescriptor(objToPatch, key);
|
||||
newProperty.get = newProperty.get ?? ancestorProperty?.get;
|
||||
newProperty.set = newProperty.set ?? ancestorProperty?.set;
|
||||
}
|
||||
|
||||
// Replace the old property by the new one.
|
||||
Object.defineProperty(objToPatch, key, newProperty);
|
||||
}
|
||||
|
||||
// Sets the current skeleton as the extension's prototype to make
|
||||
// `super` keyword working and then set extension as the new skeleton.
|
||||
description.skeleton = Object.setPrototypeOf(extension, description.skeleton);
|
||||
|
||||
return () => {
|
||||
// Remove the description to start with a fresh base.
|
||||
patchDescriptions.delete(objToPatch);
|
||||
|
||||
for (const [key, property] of description.originalProperties) {
|
||||
if (property) {
|
||||
// Restore the original property on the `objToPatch` object.
|
||||
Object.defineProperty(objToPatch, key, property);
|
||||
} else {
|
||||
// Or remove the property if it did not exist at first.
|
||||
delete objToPatch[key];
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(obj, k, newDesc);
|
||||
}
|
||||
|
||||
function makeIntermediateFunction(key, prevDesc, newDesc, patchedFnName) {
|
||||
const _superFn = prevDesc[key];
|
||||
const patchFn = newDesc[key];
|
||||
if (pure) {
|
||||
newDesc[key] = patchFn;
|
||||
} else {
|
||||
newDesc[key] = {
|
||||
[patchedFnName](...args) {
|
||||
let prevSuper;
|
||||
if (this) {
|
||||
prevSuper = this._super;
|
||||
Object.defineProperty(this, "_super", {
|
||||
value: _superFn.bind(this),
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
const result = patchFn.call(this, ...args);
|
||||
if (this) {
|
||||
Object.defineProperty(this, "_super", {
|
||||
value: prevSuper,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}[patchedFnName];
|
||||
// Re-apply the patches without the current one.
|
||||
description.extensions.delete(extension);
|
||||
for (const extension of description.extensions) {
|
||||
patch(objToPatch, extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We define here an unpatch function. This is mostly useful if we want to
|
||||
* remove a patch. For example, for testing purposes
|
||||
*
|
||||
* @template T
|
||||
* @param {T} obj
|
||||
* @param {string} patchName
|
||||
*/
|
||||
export function unpatch(obj, patchName) {
|
||||
const objDesc = patchMap.get(obj);
|
||||
if (!objDesc.patches.some((p) => p.name === patchName)) {
|
||||
throw new Error(`Class ${obj.name} does not have any patch ${patchName}`);
|
||||
}
|
||||
patchMap.delete(obj);
|
||||
|
||||
// Restore original methods on the prototype and the class.
|
||||
for (const k in objDesc.original) {
|
||||
if (objDesc.original[k] === undefined) {
|
||||
delete obj[k];
|
||||
} else {
|
||||
Object.defineProperty(obj, k, objDesc.original[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-apply the patches except the one to remove.
|
||||
for (const patchDesc of objDesc.patches) {
|
||||
if (patchDesc.name !== patchName) {
|
||||
patch(obj, patchDesc.name, patchDesc.patch, { pure: patchDesc.pure });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import legacyUtils from "@web/legacy/js/core/utils";
|
||||
|
||||
function makeBaseClass(assert, assertInSetup) {
|
||||
@@ -51,13 +51,13 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -72,24 +72,24 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
@@ -105,31 +105,18 @@ QUnit.module("utils", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("two patches with same name on same base class", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const A = class {};
|
||||
|
||||
patch(A.prototype, "patch");
|
||||
|
||||
// keys should be unique
|
||||
assert.throws(() => {
|
||||
patch(A.prototype, "patch");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("unpatch", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -137,7 +124,7 @@ QUnit.module("utils", () => {
|
||||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch.setup", "base.fn", "patch.fn"]);
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
@@ -149,24 +136,24 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
const unpatch1 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
const unpatch2 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
@@ -182,13 +169,13 @@ QUnit.module("utils", () => {
|
||||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch1");
|
||||
unpatch1();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch2.setup", "base.fn", "patch2.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch2");
|
||||
unpatch2();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
@@ -202,24 +189,24 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
const unpatch1 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
const unpatch2 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
@@ -235,13 +222,13 @@ QUnit.module("utils", () => {
|
||||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch1");
|
||||
unpatch1();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch2.setup", "base.fn", "patch2.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch2");
|
||||
unpatch2();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
@@ -249,18 +236,6 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("unpatch twice the same patch name", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const A = class {};
|
||||
patch(A.prototype, "patch");
|
||||
|
||||
unpatch(A.prototype, "patch");
|
||||
assert.throws(() => {
|
||||
unpatch(A.prototype, "patch");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("patch for specialization", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -275,9 +250,9 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
};
|
||||
|
||||
patch(A.prototype, "patch", {
|
||||
patch(A.prototype, {
|
||||
setup() {
|
||||
this._super("patch", ...arguments);
|
||||
super.setup("patch", ...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -290,9 +265,9 @@ QUnit.module("utils", () => {
|
||||
assert.expect(3);
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
super.setup(...arguments);
|
||||
|
||||
this.str += "patch";
|
||||
this.arr.push("patch");
|
||||
@@ -313,7 +288,7 @@ QUnit.module("utils", () => {
|
||||
|
||||
assert.notOk(new BaseClass().f);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
f() {
|
||||
assert.step("patch.f");
|
||||
},
|
||||
@@ -322,7 +297,7 @@ QUnit.module("utils", () => {
|
||||
new BaseClass().f();
|
||||
assert.verifySteps(["patch.f"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.notOk(new BaseClass().f);
|
||||
});
|
||||
@@ -335,9 +310,9 @@ QUnit.module("utils", () => {
|
||||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn"]);
|
||||
|
||||
patch(BaseClass, "patch", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.staticFn");
|
||||
},
|
||||
});
|
||||
@@ -345,7 +320,7 @@ QUnit.module("utils", () => {
|
||||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "patch.staticFn"]);
|
||||
|
||||
unpatch(BaseClass, "patch");
|
||||
unpatch();
|
||||
|
||||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn"]);
|
||||
@@ -356,7 +331,7 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass, "patch", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
@@ -366,7 +341,7 @@ QUnit.module("utils", () => {
|
||||
assert.deepEqual(BaseClass.staticArr, ["base", "patch"]);
|
||||
assert.deepEqual(BaseClass.staticObj, { base: "base", patch: "patch" });
|
||||
|
||||
unpatch(BaseClass, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(BaseClass.staticStr, "base");
|
||||
assert.deepEqual(BaseClass.staticArr, ["base"]);
|
||||
@@ -379,14 +354,14 @@ QUnit.module("utils", () => {
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
const instance = new BaseClass();
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
// will not be called
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -394,7 +369,7 @@ QUnit.module("utils", () => {
|
||||
instance.fn();
|
||||
assert.verifySteps(["base.setup", "base.fn", "patch.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.fn();
|
||||
assert.verifySteps(["base.fn"]);
|
||||
@@ -405,16 +380,16 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
get dynamic() {
|
||||
return this._super() + "patch";
|
||||
return super.dynamic + "patch";
|
||||
},
|
||||
});
|
||||
|
||||
const instance = new BaseClass();
|
||||
assert.strictEqual(instance.dynamic, "basepatch");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
assert.strictEqual(instance.dynamic, "base");
|
||||
});
|
||||
|
||||
@@ -423,9 +398,9 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
set dynamic(value) {
|
||||
this._super("patch:" + value);
|
||||
super.dynamic = "patch:" + value;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -435,7 +410,7 @@ QUnit.module("utils", () => {
|
||||
instance.dynamic = "patch";
|
||||
assert.strictEqual(instance.dynamic, "patch:patch");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.dynamic = "base";
|
||||
assert.strictEqual(instance.dynamic, "base");
|
||||
@@ -450,7 +425,7 @@ QUnit.module("utils", () => {
|
||||
BaseClass.prototype,
|
||||
"dynamic"
|
||||
);
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
dynamic: "patched",
|
||||
});
|
||||
|
||||
@@ -460,11 +435,11 @@ QUnit.module("utils", () => {
|
||||
value: "patched",
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
enumerable: false, // class properties are not enumerable
|
||||
});
|
||||
assert.equal(instance.dynamic, "patched");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.dynamic = "base";
|
||||
assert.deepEqual(
|
||||
@@ -479,11 +454,10 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
assert.step("patch.asyncFn");
|
||||
},
|
||||
});
|
||||
@@ -500,20 +474,18 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
// also check this binding
|
||||
assert.step(`patch1.${this.str}`);
|
||||
},
|
||||
});
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
// also check this binding
|
||||
assert.step(`patch2.${this.str}`);
|
||||
},
|
||||
@@ -526,6 +498,25 @@ QUnit.module("utils", () => {
|
||||
assert.verifySteps(["base.asyncFn", "patch1.asyncFn", "patch2.asyncFn"]);
|
||||
});
|
||||
|
||||
QUnit.test("call another super method", async function (assert) {
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
assert.step("patch.setup");
|
||||
super.fn();
|
||||
},
|
||||
fn() {
|
||||
assert.step("patch.fn"); // should not called
|
||||
},
|
||||
});
|
||||
|
||||
new BaseClass();
|
||||
assert.verifySteps([
|
||||
"patch.setup",
|
||||
"base.fn",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.module("inheritance");
|
||||
|
||||
QUnit.test("inherit a patched class (extends before patch)", async function (assert) {
|
||||
@@ -547,13 +538,13 @@ QUnit.module("utils", () => {
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -574,13 +565,13 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -627,13 +618,13 @@ QUnit.module("utils", () => {
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -654,13 +645,13 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -676,13 +667,13 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
@@ -700,6 +691,50 @@ QUnit.module("utils", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("patch an inherited patched class 2", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
class Extension extends BaseClass {
|
||||
// nothing in the prototype
|
||||
}
|
||||
|
||||
// First patch Extension
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
|
||||
// Then patch BaseClass
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("unpatch base class", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
@@ -716,13 +751,13 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -737,7 +772,7 @@ QUnit.module("utils", () => {
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
@@ -759,13 +794,13 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
const unpatch = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -780,7 +815,7 @@ QUnit.module("utils", () => {
|
||||
"patch.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
@@ -793,13 +828,13 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -815,13 +850,13 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
@@ -838,7 +873,7 @@ QUnit.module("utils", () => {
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
@@ -850,7 +885,7 @@ QUnit.module("utils", () => {
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
@@ -864,13 +899,13 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -886,13 +921,13 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
@@ -909,7 +944,7 @@ QUnit.module("utils", () => {
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
@@ -921,7 +956,7 @@ QUnit.module("utils", () => {
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
@@ -940,16 +975,16 @@ QUnit.module("utils", () => {
|
||||
}
|
||||
}
|
||||
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.staticFn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(Extension, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.extension.staticFn");
|
||||
},
|
||||
});
|
||||
@@ -962,12 +997,12 @@ QUnit.module("utils", () => {
|
||||
"patch.extension.staticFn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
Extension.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "extension.staticFn", "patch.extension.staticFn"]);
|
||||
|
||||
unpatch(Extension, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
Extension.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "extension.staticFn"]);
|
||||
@@ -978,7 +1013,7 @@ QUnit.module("utils", () => {
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
@@ -997,7 +1032,7 @@ QUnit.module("utils", () => {
|
||||
extension: "extension",
|
||||
});
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatch();
|
||||
|
||||
// /!\ WARNING /!\
|
||||
// If inherit comes after the patch then extension will still have
|
||||
@@ -1024,7 +1059,7 @@ QUnit.module("utils", () => {
|
||||
// /!\ WARNING /!\
|
||||
// If patch comes after the inherit then extension won't have
|
||||
// the patched data.
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
@@ -1034,7 +1069,7 @@ QUnit.module("utils", () => {
|
||||
assert.deepEqual(Extension.staticArr, ["base", "extension"]);
|
||||
assert.deepEqual(Extension.staticObj, { base: "base", extension: "extension" });
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(Extension.staticStr, "baseextension");
|
||||
assert.deepEqual(Extension.staticArr, ["base", "extension"]);
|
||||
@@ -1058,14 +1093,14 @@ QUnit.module("utils", () => {
|
||||
|
||||
const instance = new Extension();
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
// will not be called
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
@@ -1079,7 +1114,7 @@ QUnit.module("utils", () => {
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.fn();
|
||||
assert.verifySteps(["base.fn", "extension.fn"]);
|
||||
@@ -1097,7 +1132,7 @@ QUnit.module("utils", () => {
|
||||
assert.strictEqual(descriptor.configurable, true);
|
||||
assert.strictEqual(descriptor.enumerable, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
// getter declared in object are enumerable
|
||||
get getter() {
|
||||
return true;
|
||||
@@ -1121,10 +1156,10 @@ QUnit.module("utils", () => {
|
||||
},
|
||||
};
|
||||
|
||||
patch(obj, "patch", {
|
||||
const unpatch = patch(obj, {
|
||||
var: obj.var + "patch",
|
||||
fn() {
|
||||
this._super(...arguments);
|
||||
super.fn(...arguments);
|
||||
assert.step("patch");
|
||||
},
|
||||
});
|
||||
@@ -1134,7 +1169,7 @@ QUnit.module("utils", () => {
|
||||
obj.fn();
|
||||
assert.verifySteps(["obj", "patch"]);
|
||||
|
||||
unpatch(obj, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(obj.var, "obj");
|
||||
|
||||
@@ -1153,7 +1188,7 @@ QUnit.module("utils", () => {
|
||||
};
|
||||
|
||||
const originalFn = obj.fn;
|
||||
patch(obj, "patch", {
|
||||
patch(obj, {
|
||||
fn() {
|
||||
assert.step("patched");
|
||||
originalFn();
|
||||
@@ -1178,28 +1213,14 @@ QUnit.module("utils", () => {
|
||||
assert.step("a.patch.legacy");
|
||||
},
|
||||
});
|
||||
patch(a, "a.patch", {
|
||||
patch(a, {
|
||||
doSomething() {
|
||||
this._super();
|
||||
super.doSomething();
|
||||
assert.step("a.patch");
|
||||
},
|
||||
});
|
||||
a.doSomething();
|
||||
assert.verifySteps(["a", "a.patch.legacy", "a.patch"]);
|
||||
});
|
||||
|
||||
QUnit.module("patch 'pure' option");
|
||||
|
||||
QUnit.test("function objects are preserved with 'pure' patch", async function (assert) {
|
||||
const obj1 = { a: () => {} };
|
||||
const obj2 = { a: () => {} };
|
||||
function someValue() {}
|
||||
|
||||
patch(obj1, "patch1", { a: someValue });
|
||||
assert.notStrictEqual(obj1.a, someValue);
|
||||
|
||||
patch(obj2, "patch2", { a: someValue }, { pure: true });
|
||||
assert.strictEqual(obj2.a, someValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user