diff options
| author | Michael Mattiello (mcm) <mcm@odoo.com> | 2020-03-16 08:28:17 +0000 |
|---|---|---|
| committer | Michael Mattiello (mcm) <mcm@odoo.com> | 2020-04-16 07:19:25 +0000 |
| commit | dcf9a4c38da5d50a6e597d3d68c3d46fdef61df5 (patch) | |
| tree | d17339ec7d9a543deb5937052415fe768d1298d7 /addons/web/static/tests | |
| parent | 5cc42022fc159e71ccd2dde122ddde80dd66e23e (diff) | |
[IMP] web: add es6 class patcher
This commit implements a new way to monkey-patch ES6
classes (for instance, Owl Components).
Usage
-----
odoo.module('module.BaseClass', function (require) {
const patchMixin = require('web.patchMixin');
// Define your class as usual
class BaseClass {
constructor() {
this.foo = 'foo';
}
bar() {
console.log(this.foo);
}
}
// but export it like this to have a "patchable" version
return patchMixin(BaseClass);
});
odoo.module('module.Patch', function (require) {
// imports the "patchable" version
const BaseClass = require('module.BaseClass');
BaseClass.patch('key', T =>
class extends T {
constructor() {
super();
this.foo = 'patched foo';
}
bar() {
super.bar();
console.log('patched bar()');
}
}
);
});
odoo.module('module.instance', function (require) {
// imports the "patchable" version
const BaseClass = require('module.BaseClass');
const obj = new BaseClass();
obj.bar();
// if module.patch is loaded before this module the patch
// will be apply and calling obj.bar() will log:
// patched foo
// patched bar()
// You can unpatch a patchable class
BaseClass.unpatch('key');
});
Diffstat (limited to 'addons/web/static/tests')
| -rw-r--r-- | addons/web/static/tests/core/patch_mixin_tests.js | 994 |
1 files changed, 994 insertions, 0 deletions
diff --git a/addons/web/static/tests/core/patch_mixin_tests.js b/addons/web/static/tests/core/patch_mixin_tests.js new file mode 100644 index 00000000000..7329a9607d4 --- /dev/null +++ b/addons/web/static/tests/core/patch_mixin_tests.js @@ -0,0 +1,994 @@ +odoo.define("web.patchMixin_tests", function (require) { +"use strict"; + +const patchMixin = require('web.patchMixin'); + +QUnit.module('core', {}, function () { + + QUnit.module('patchMixin', {}, function () { + + QUnit.test('basic use', function (assert) { + assert.expect(4); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + const a = new A(); + a.f(); + + assert.ok(a instanceof A); + assert.verifySteps([ + 'A.constructor', + 'A.f', + ]); + }); + + QUnit.test('simple patch', function (assert) { + assert.expect(5); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch.constructor', + 'A.f', + 'patch.f', + ]); + }); + + QUnit.test('two patches on same base class', function (assert) { + assert.expect(7); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch1', T => + class extends T { + constructor() { + super(); + assert.step('patch1.constructor'); + } + f() { + super.f(); + assert.step('patch1.f'); + } + } + ); + + A.patch('patch2', T => + class extends T { + constructor() { + super(); + assert.step('patch2.constructor'); + } + f() { + super.f(); + assert.step('patch2.f'); + } + } + ); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch1.constructor', + 'patch2.constructor', + 'A.f', + 'patch1.f', + 'patch2.f', + ]); + }); + + QUnit.test('two patches with same name on same base class', function (assert) { + assert.expect(1); + + const A = patchMixin(class {}); + + A.patch('patch', T => class extends T {}); + + // keys should be unique + assert.throws(() => { + A.patch('patch', T => class extends T {}); + }); + }); + + QUnit.test('unpatch', function (assert) { + assert.expect(8); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch.constructor', + 'A.f', + 'patch.f', + ]); + + A.unpatch('patch'); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'A.f', + ]); + }); + + QUnit.test('unpatch 2', function (assert) { + assert.expect(12); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch1', T => + class extends T { + constructor() { + super(); + assert.step('patch1.constructor'); + } + f() { + super.f(); + assert.step('patch1.f'); + } + } + ); + + A.patch('patch2', T => + class extends T { + constructor() { + super(); + assert.step('patch2.constructor'); + } + f() { + super.f(); + assert.step('patch2.f'); + } + } + ); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch1.constructor', + 'patch2.constructor', + 'A.f', + 'patch1.f', + 'patch2.f', + ]); + + A.unpatch('patch1'); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch2.constructor', + 'A.f', + 'patch2.f', + ]); + }); + + QUnit.test('unpatch inexistent', function (assert) { + assert.expect(1); + + const A = patchMixin(class {}); + A.patch('patch', T => class extends T {}); + + A.unpatch('patch'); + assert.throws(() => { + A.unpatch('inexistent-patch'); + }); + }); + + QUnit.test('patch for specialization', function (assert) { + assert.expect(1); + + let args = []; + + const A = patchMixin( + class { + constructor() { + args = ['A', ...arguments]; + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super('patch', ...arguments); + } + } + ); + + new A('instantiation'); + + assert.deepEqual(args, ['A', 'patch', 'instantiation']); + }); + + QUnit.test('instance fields', function (assert) { + assert.expect(1); + + const A = patchMixin( + class { + constructor() { + this.x = ['A']; + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + this.x.push('patch'); + } + } + ); + + const a = new A(); + assert.deepEqual(a.x, ['A', 'patch']); + }); + + QUnit.test('call instance method defined in patch', function (assert) { + assert.expect(3); + + const A = patchMixin( + class {} + ); + + assert.notOk((new A()).f); + + A.patch('patch', T => + class extends T { + f() { + assert.step('patch.f'); + } + } + ); + + (new A()).f(); + assert.verifySteps(['patch.f']); + }); + + QUnit.test('class methods', function (assert) { + assert.expect(7); + + const A = patchMixin( + class { + static f() { + assert.step('A'); + } + } + ); + + A.f(); + assert.verifySteps(['A']); + + A.patch('patch', T => + class extends T { + static f() { + super.f(); + assert.step('patch'); + } + } + ); + + A.f(); + assert.verifySteps(['A', 'patch']); + + A.unpatch('patch'); + + A.f(); + assert.verifySteps(['A']); + }); + + QUnit.test('class fields', function (assert) { + assert.expect(4); + + class A {} + A.foo = ['A']; + A.bar = 'A'; + + const PatchableA = patchMixin(A); + + PatchableA.patch('patch', T => { + class Patch extends T {} + + Patch.foo = [...T.foo, 'patched A']; + Patch.bar = 'patched A'; + + return Patch; + }); + + assert.deepEqual(PatchableA.foo, ['A', 'patched A']); + assert.strictEqual(PatchableA.bar, 'patched A'); + + PatchableA.unpatch('patch'); + + assert.deepEqual(PatchableA.foo, ['A']); + assert.strictEqual(PatchableA.bar, 'A'); + }); + + QUnit.test('lazy patch', function (assert) { + assert.expect(4); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + const a = new A(); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + // will not be called + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + a.f(); + + assert.verifySteps([ + 'A.constructor', + 'A.f', + 'patch.f', + ]); + }); + + + QUnit.module('inheritance'); + + QUnit.test('inheriting a patchable class', function (assert) { + assert.expect(8); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + class B extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'A.f', + ]); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'A.f', + 'B.f', + ]); + }); + + QUnit.test('inheriting a patchable class that has patch', function (assert) { + assert.expect(12); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + class B extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch.constructor', + 'A.f', + 'patch.f', + ]); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'patch.constructor', + 'B.constructor', + 'A.f', + 'patch.f', + 'B.f', + ]); + }); + + QUnit.test('patch inherited patchable class', function (assert) { + assert.expect(10); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + const B = patchMixin( + class extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + ); + + B.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'A.f', + ]); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'patch.constructor', + 'A.f', + 'B.f', + 'patch.f', + ]); + }); + + QUnit.test('patch inherited patched class', function (assert) { + assert.expect(14); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('A.patch.constructor'); + } + f() { + super.f(); + assert.step('A.patch.f'); + } + } + ); + + /** + * /!\ WARNING /!\ + * + * If you want to patch class B, make it patchable + * otherwise it will patch class A! + */ + const B = patchMixin( + class extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + ); + + B.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('B.patch.constructor'); + } + f() { + super.f(); + assert.step('B.patch.f'); + } + } + ); + + const a = new A(); + a.f(); + + assert.verifySteps([ + 'A.constructor', + 'A.patch.constructor', + 'A.f', + 'A.patch.f', + ]); + + const b = new B(); + b.f(); + + assert.verifySteps([ + 'A.constructor', + 'A.patch.constructor', + 'B.constructor', + 'B.patch.constructor', + 'A.f', + 'A.patch.f', + 'B.f', + 'B.patch.f', + ]); + }); + + QUnit.test('unpatch inherited patched class', function (assert) { + assert.expect(15); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('A.patch.constructor'); + } + f() { + super.f(); + assert.step('A.patch.f'); + } + } + ); + + const B = patchMixin( + class extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + ); + + B.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('B.patch.constructor'); + } + f() { + super.f(); + assert.step('B.patch.f'); + } + } + ); + + A.unpatch('patch'); + + (new A()).f(); + + assert.verifySteps([ + 'A.constructor', + 'A.f', + ]); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'B.patch.constructor', + 'A.f', + 'B.f', + 'B.patch.f', + ]); + + B.unpatch('patch'); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'A.f', + 'B.f', + ]); + }); + + QUnit.test('unpatch inherited patched class 2', function (assert) { + assert.expect(12); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('A.patch.constructor'); + } + f() { + super.f(); + assert.step('A.patch.f'); + } + } + ); + + const B = patchMixin( + class extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + ); + + B.patch('patch', T => + class extends T { + constructor() { + super(); + assert.step('B.patch.constructor'); + } + f() { + super.f(); + assert.step('B.patch.f'); + } + } + ); + + B.unpatch('patch'); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'A.patch.constructor', + 'B.constructor', + 'A.f', + 'A.patch.f', + 'B.f', + ]); + + A.unpatch('patch'); + + (new B()).f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'A.f', + 'B.f', + ]); + }); + + QUnit.test('class methods', function (assert) { + assert.expect(12); + + const A = patchMixin( + class { + static f() { + assert.step('A'); + } + } + ); + + const B = patchMixin( + class extends A { + static f() { + super.f(); + assert.step('B'); + } + } + ); + + A.patch('patch', T => + class extends T { + static f() { + super.f(); + assert.step('A.patch'); + } + } + ); + + B.patch('patch', T => + class extends T { + static f() { + super.f(); + assert.step('B.patch'); + } + } + ); + + B.f(); + assert.verifySteps(['A', 'A.patch', 'B', 'B.patch']); + + A.unpatch('patch'); + + B.f(); + assert.verifySteps(['A', 'B', 'B.patch']); + + B.unpatch('patch'); + + B.f(); + assert.verifySteps(['A', 'B']); + }); + + QUnit.test('class fields', function (assert) { + assert.expect(3); + + class A {} + A.foo = ['A']; + A.bar = 'A'; + + const PatchableA = patchMixin(A); + + class B extends PatchableA {} + // /!\ This is not dynamic + // so if A.foo is patched after this assignment + // B.foo won't have the patches of A.foo + B.foo = [...PatchableA.foo, 'B']; + B.bar = 'B'; + + const PatchableB = patchMixin(B); + + PatchableA.patch('patch', T => { + class Patch extends T {} + + Patch.foo = [...T.foo, 'patched A']; + Patch.bar = 'patched A'; + + return Patch; + }); + + PatchableB.patch('patch', T => { + class Patch extends T {} + + Patch.foo = [...T.foo, 'patched B']; + Patch.bar = 'patched B'; + + return Patch; + }); + + assert.deepEqual(PatchableB.foo, [ 'A', /* 'patched A', */ 'B', 'patched B' ]); + assert.deepEqual(PatchableA.foo, [ 'A', 'patched A' ]); + assert.strictEqual(PatchableB.bar, 'patched B'); + }); + + QUnit.test('inheritance and lazy patch', function (assert) { + assert.expect(6); + + const A = patchMixin( + class { + constructor() { + assert.step('A.constructor'); + } + f() { + assert.step('A.f'); + } + } + ); + + class B extends A { + constructor() { + super(); + assert.step('B.constructor'); + } + f() { + super.f(); + assert.step('B.f'); + } + } + + const b = new B(); + + A.patch('patch', T => + class extends T { + constructor() { + super(); + // will not be called + assert.step('patch.constructor'); + } + f() { + super.f(); + assert.step('patch.f'); + } + } + ); + + b.f(); + + assert.verifySteps([ + 'A.constructor', + 'B.constructor', + 'A.f', + 'patch.f', + 'B.f', + ]); + }); + + QUnit.test('patch not patchable class that inherits patchable class', function (assert) { + assert.expect(1); + + const A = patchMixin(class {}); + class B extends A {} + + // class B is not patchable + assert.throws(() => { + B.patch('patch', T => class extends T {}); + }); + }); + }); +}); +}); |
