diff --git a/addons/web/static/src/core/utils/patch.js b/addons/web/static/src/core/utils/patch.js index 5ed6818fcfa..7c268e7a600 100644 --- a/addons/web/static/src/core/utils/patch.js +++ b/addons/web/static/src/core/utils/patch.js @@ -1,22 +1,60 @@ /** @odoo-module **/ /** - * @typedef {{ - * name: string; - * patch: object; - * pure: boolean; - * }} PatchDescription + * @typedef {{ + * originalProperties: Map; + * skeleton: object; + * extensions: Set; + * }} PatchDescription */ +/** @type {WeakMap} */ +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} */ -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} 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 }); - } - } + }; } diff --git a/addons/web/static/tests/core/utils/patch_tests.js b/addons/web/static/tests/core/utils/patch_tests.js index 4012d6ac505..b7befbbe656 100644 --- a/addons/web/static/tests/core/utils/patch_tests.js +++ b/addons/web/static/tests/core/utils/patch_tests.js @@ -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); - }); }); });