summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/core
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/core
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/core')
-rw-r--r--addons/web/static/tests/core/ajax_tests.js35
-rw-r--r--addons/web/static/tests/core/class_tests.js168
-rw-r--r--addons/web/static/tests/core/concurrency_tests.js576
-rw-r--r--addons/web/static/tests/core/data_comparison_utils_tests.js75
-rw-r--r--addons/web/static/tests/core/dialog_tests.js173
-rw-r--r--addons/web/static/tests/core/dom_tests.js133
-rw-r--r--addons/web/static/tests/core/domain_tests.js186
-rw-r--r--addons/web/static/tests/core/math_utils_tests.js56
-rw-r--r--addons/web/static/tests/core/mixins_tests.js36
-rw-r--r--addons/web/static/tests/core/owl_dialog_tests.js332
-rw-r--r--addons/web/static/tests/core/patch_mixin_tests.js994
-rw-r--r--addons/web/static/tests/core/popover_tests.js280
-rw-r--r--addons/web/static/tests/core/py_utils_tests.js1376
-rw-r--r--addons/web/static/tests/core/registry_tests.js90
-rw-r--r--addons/web/static/tests/core/rpc_tests.js316
-rw-r--r--addons/web/static/tests/core/time_tests.js165
-rw-r--r--addons/web/static/tests/core/util_tests.js339
-rw-r--r--addons/web/static/tests/core/widget_tests.js530
18 files changed, 5860 insertions, 0 deletions
diff --git a/addons/web/static/tests/core/ajax_tests.js b/addons/web/static/tests/core/ajax_tests.js
new file mode 100644
index 00000000..f58d7368
--- /dev/null
+++ b/addons/web/static/tests/core/ajax_tests.js
@@ -0,0 +1,35 @@
+odoo.define('web.ajax_tests', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+
+QUnit.module('core', function () {
+
+ var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
+ var test_link_selector = 'link[href="' + test_css_url + '"]';
+
+ QUnit.module('ajax', {
+ beforeEach: function () {
+ $(test_link_selector).remove();
+ },
+ afterEach: function () {
+ $(test_link_selector).remove();
+ }
+ });
+
+ QUnit.test('loadCSS', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+ ajax.loadCSS(test_css_url).then(function () {
+ var $links = $(test_link_selector);
+ assert.strictEqual($links.length, 1, "The css should be added to the dom.");
+ ajax.loadCSS(test_css_url).then(function () {
+ var $links = $(test_link_selector);
+ assert.strictEqual($links.length, 1, "The css should have been added only once.");
+ done();
+ });
+ });
+ });
+});
+
+});
diff --git a/addons/web/static/tests/core/class_tests.js b/addons/web/static/tests/core/class_tests.js
new file mode 100644
index 00000000..438b137e
--- /dev/null
+++ b/addons/web/static/tests/core/class_tests.js
@@ -0,0 +1,168 @@
+odoo.define('web.class_tests', function (require) {
+"use strict";
+
+var Class = require('web.Class');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('Class');
+
+
+ QUnit.test('Basic class creation', function (assert) {
+ assert.expect(2);
+
+ var C = Class.extend({
+ foo: function () {
+ return this.somevar;
+ }
+ });
+ var i = new C();
+ i.somevar = 3;
+
+ assert.ok(i instanceof C);
+ assert.strictEqual(i.foo(), 3);
+ });
+
+ QUnit.test('Class initialization', function (assert) {
+ assert.expect(2);
+
+ var C1 = Class.extend({
+ init: function () {
+ this.foo = 3;
+ }
+ });
+ var C2 = Class.extend({
+ init: function (arg) {
+ this.foo = arg;
+ }
+ });
+
+ var i1 = new C1(),
+ i2 = new C2(42);
+
+ assert.strictEqual(i1.foo, 3);
+ assert.strictEqual(i2.foo, 42);
+ });
+
+ QUnit.test('Inheritance', function (assert) {
+ assert.expect(3);
+
+ var C0 = Class.extend({
+ foo: function () {
+ return 1;
+ }
+ });
+ var C1 = C0.extend({
+ foo: function () {
+ return 1 + this._super();
+ }
+ });
+ var C2 = C1.extend({
+ foo: function () {
+ return 1 + this._super();
+ }
+ });
+
+ assert.strictEqual(new C0().foo(), 1);
+ assert.strictEqual(new C1().foo(), 2);
+ assert.strictEqual(new C2().foo(), 3);
+ });
+
+ QUnit.test('In-place extension', function (assert) {
+ assert.expect(4);
+
+ var C0 = Class.extend({
+ foo: function () {
+ return 3;
+ },
+ qux: function () {
+ return 3;
+ },
+ bar: 3
+ });
+
+ C0.include({
+ foo: function () {
+ return 5;
+ },
+ qux: function () {
+ return 2 + this._super();
+ },
+ bar: 5,
+ baz: 5
+ });
+
+ assert.strictEqual(new C0().bar, 5);
+ assert.strictEqual(new C0().baz, 5);
+ assert.strictEqual(new C0().foo(), 5);
+ assert.strictEqual(new C0().qux(), 5);
+ });
+
+ QUnit.test('In-place extension and inheritance', function (assert) {
+ assert.expect(4);
+
+ var C0 = Class.extend({
+ foo: function () { return 1; },
+ bar: function () { return 1; }
+ });
+ var C1 = C0.extend({
+ foo: function () { return 1 + this._super(); }
+ });
+ assert.strictEqual(new C1().foo(), 2);
+ assert.strictEqual(new C1().bar(), 1);
+
+ C1.include({
+ foo: function () { return 2 + this._super(); },
+ bar: function () { return 1 + this._super(); }
+ });
+ assert.strictEqual(new C1().foo(), 4);
+ assert.strictEqual(new C1().bar(), 2);
+ });
+
+ QUnit.test('In-place extensions alter existing instances', function (assert) {
+ assert.expect(4);
+
+ var C0 = Class.extend({
+ foo: function () { return 1; },
+ bar: function () { return 1; }
+ });
+ var i = new C0();
+ assert.strictEqual(i.foo(), 1);
+ assert.strictEqual(i.bar(), 1);
+
+ C0.include({
+ foo: function () { return 2; },
+ bar: function () { return 2 + this._super(); }
+ });
+ assert.strictEqual(i.foo(), 2);
+ assert.strictEqual(i.bar(), 3);
+ });
+
+ QUnit.test('In-place extension of subclassed types', function (assert) {
+ assert.expect(3);
+
+ var C0 = Class.extend({
+ foo: function () { return 1; },
+ bar: function () { return 1; }
+ });
+ var C1 = C0.extend({
+ foo: function () { return 1 + this._super(); },
+ bar: function () { return 1 + this._super(); }
+ });
+ var i = new C1();
+
+ assert.strictEqual(i.foo(), 2);
+
+ C0.include({
+ foo: function () { return 2; },
+ bar: function () { return 2 + this._super(); }
+ });
+
+ assert.strictEqual(i.foo(), 3);
+ assert.strictEqual(i.bar(), 4);
+ });
+
+
+});
+
+});
diff --git a/addons/web/static/tests/core/concurrency_tests.js b/addons/web/static/tests/core/concurrency_tests.js
new file mode 100644
index 00000000..1e195fde
--- /dev/null
+++ b/addons/web/static/tests/core/concurrency_tests.js
@@ -0,0 +1,576 @@
+odoo.define('web.concurrency_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+var testUtils = require('web.test_utils');
+
+var makeTestPromise = testUtils.makeTestPromise;
+var makeTestPromiseWithAssert = testUtils.makeTestPromiseWithAssert;
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('concurrency');
+
+ QUnit.test('mutex: simple scheduling', async function (assert) {
+ assert.expect(5);
+ var mutex = new concurrency.Mutex();
+
+ var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
+ var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
+
+ mutex.exec(function () { return prom1; });
+ mutex.exec(function () { return prom2; });
+
+ assert.verifySteps([]);
+
+ await prom1.resolve();
+
+ assert.verifySteps(['ok prom1']);
+
+ await prom2.resolve();
+
+ assert.verifySteps(['ok prom2']);
+ });
+
+ QUnit.test('mutex: simpleScheduling2', async function (assert) {
+ assert.expect(5);
+ var mutex = new concurrency.Mutex();
+
+ var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
+ var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
+
+ mutex.exec(function () { return prom1; });
+ mutex.exec(function () { return prom2; });
+
+ assert.verifySteps([]);
+
+ await prom2.resolve();
+
+ assert.verifySteps(['ok prom2']);
+
+ await prom1.resolve();
+
+ assert.verifySteps(['ok prom1']);
+ });
+
+ QUnit.test('mutex: reject', async function (assert) {
+ assert.expect(7);
+ var mutex = new concurrency.Mutex();
+
+ var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
+ var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
+ var prom3 = makeTestPromiseWithAssert(assert, 'prom3');
+
+ mutex.exec(function () { return prom1; }).catch(function () {});
+ mutex.exec(function () { return prom2; }).catch(function () {});
+ mutex.exec(function () { return prom3; }).catch(function () {});
+
+ assert.verifySteps([]);
+
+ prom1.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok prom1']);
+
+ prom2.catch(function () {
+ assert.verifySteps(['ko prom2']);
+ });
+ prom2.reject({name: "sdkjfmqsjdfmsjkdfkljsdq"});
+ await testUtils.nextMicrotaskTick();
+
+ prom3.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok prom3']);
+ });
+
+ QUnit.test('mutex: getUnlockedDef checks', async function (assert) {
+ assert.expect(9);
+
+ var mutex = new concurrency.Mutex();
+
+ var prom1 = makeTestPromiseWithAssert(assert, 'prom1');
+ var prom2 = makeTestPromiseWithAssert(assert, 'prom2');
+
+ mutex.getUnlockedDef().then(function () {
+ assert.step('mutex unlocked (1)');
+ });
+
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['mutex unlocked (1)']);
+
+ mutex.exec(function () { return prom1; });
+ await testUtils.nextMicrotaskTick();
+
+ mutex.getUnlockedDef().then(function () {
+ assert.step('mutex unlocked (2)');
+ });
+
+ assert.verifySteps([]);
+
+ mutex.exec(function () { return prom2; });
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps([]);
+
+ await prom1.resolve();
+
+ assert.verifySteps(['ok prom1']);
+
+ prom2.resolve();
+ await testUtils.nextTick();
+
+ assert.verifySteps(['ok prom2', 'mutex unlocked (2)']);
+ });
+
+ QUnit.test('DropPrevious: basic usecase', async function (assert) {
+ assert.expect(4);
+
+ var dp = new concurrency.DropPrevious();
+
+ var prom1 = makeTestPromise(assert, 'prom1');
+ var prom2 = makeTestPromise(assert, 'prom2');
+
+ dp.add(prom1).then(() => assert.step('should not go here'))
+ .catch(()=> assert.step("rejected dp1"));
+ dp.add(prom2).then(() => assert.step("ok dp2"));
+
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['rejected dp1']);
+
+ prom2.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok dp2']);
+ });
+
+ QUnit.test('DropPrevious: resolve first before last', async function (assert) {
+ assert.expect(4);
+
+ var dp = new concurrency.DropPrevious();
+
+ var prom1 = makeTestPromise(assert, 'prom1');
+ var prom2 = makeTestPromise(assert, 'prom2');
+
+ dp.add(prom1).then(() => assert.step('should not go here'))
+ .catch(()=> assert.step("rejected dp1"));
+ dp.add(prom2).then(() => assert.step("ok dp2"));
+
+
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['rejected dp1']);
+
+ prom1.resolve();
+ prom2.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok dp2']);
+ });
+
+ QUnit.test('DropMisordered: resolve all correctly ordered, sync', async function (assert) {
+ assert.expect(1);
+
+ var dm = new concurrency.DropMisordered(),
+ flag = false;
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ var r1 = dm.add(d1),
+ r2 = dm.add(d2);
+
+ Promise.all([r1, r2]).then(function () {
+ flag = true;
+ });
+
+ d1.resolve();
+ d2.resolve();
+ await testUtils.nextTick();
+
+ assert.ok(flag);
+ });
+
+ QUnit.test("DropMisordered: don't resolve mis-ordered, sync", async function (assert) {
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(),
+ done1 = false,
+ done2 = false,
+ fail1 = false,
+ fail2 = false;
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ dm.add(d1).then(function () { done1 = true; })
+ .catch(function () { fail1 = true; });
+ dm.add(d2).then(function () { done2 = true; })
+ .catch(function () { fail2 = true; });
+
+ d2.resolve();
+ d1.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ // d1 is in limbo
+ assert.ok(!done1);
+ assert.ok(!fail1);
+
+ // d2 is fulfilled
+ assert.ok(done2);
+ assert.ok(!fail2);
+ });
+
+ QUnit.test('DropMisordered: fail mis-ordered flag, sync', async function (assert) {
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(true/* failMisordered */),
+ done1 = false,
+ done2 = false,
+ fail1 = false,
+ fail2 = false;
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ dm.add(d1).then(function () { done1 = true; })
+ .catch(function () { fail1 = true; });
+ dm.add(d2).then(function () { done2 = true; })
+ .catch(function () { fail2 = true; });
+
+ d2.resolve();
+ d1.resolve();
+ await testUtils.nextMicrotaskTick();
+
+ // d1 is in limbo
+ assert.ok(!done1);
+ assert.ok(fail1);
+
+ // d2 is resolved
+ assert.ok(done2);
+ assert.ok(!fail2);
+ });
+
+ QUnit.test('DropMisordered: resolve all correctly ordered, async', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ var dm = new concurrency.DropMisordered();
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ var r1 = dm.add(d1),
+ r2 = dm.add(d2);
+
+ setTimeout(function () { d1.resolve(); }, 10);
+ setTimeout(function () { d2.resolve(); }, 20);
+
+ Promise.all([r1, r2]).then(function () {
+ assert.ok(true);
+ done();
+ });
+ });
+
+ QUnit.test("DropMisordered: don't resolve mis-ordered, async", function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(),
+ done1 = false, done2 = false,
+ fail1 = false, fail2 = false;
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ dm.add(d1).then(function () { done1 = true; })
+ .catch(function () { fail1 = true; });
+ dm.add(d2).then(function () { done2 = true; })
+ .catch(function () { fail2 = true; });
+
+ setTimeout(function () { d1.resolve(); }, 20);
+ setTimeout(function () { d2.resolve(); }, 10);
+
+ setTimeout(function () {
+ // d1 is in limbo
+ assert.ok(!done1);
+ assert.ok(!fail1);
+
+ // d2 is resolved
+ assert.ok(done2);
+ assert.ok(!fail2);
+ done();
+ }, 30);
+ });
+
+ QUnit.test('DropMisordered: fail mis-ordered flag, async', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(true),
+ done1 = false, done2 = false,
+ fail1 = false, fail2 = false;
+
+ var d1 = makeTestPromise();
+ var d2 = makeTestPromise();
+
+ dm.add(d1).then(function () { done1 = true; })
+ .catch(function () { fail1 = true; });
+ dm.add(d2).then(function () { done2 = true; })
+ .catch(function () { fail2 = true; });
+
+ setTimeout(function () { d1.resolve(); }, 20);
+ setTimeout(function () { d2.resolve(); }, 10);
+
+ setTimeout(function () {
+ // d1 is failed
+ assert.ok(!done1);
+ assert.ok(fail1);
+
+ // d2 is resolved
+ assert.ok(done2);
+ assert.ok(!fail2);
+ done();
+ }, 30);
+ });
+
+ QUnit.test('MutexedDropPrevious: simple', async function (assert) {
+ assert.expect(5);
+
+ var m = new concurrency.MutexedDropPrevious();
+ var d1 = makeTestPromise();
+
+ d1.then(function () {
+ assert.step("d1 resolved");
+ });
+ m.exec(function () { return d1; }).then(function (result) {
+ assert.step("p1 done");
+ assert.strictEqual(result, 'd1');
+ });
+
+ assert.verifySteps([]);
+ d1.resolve('d1');
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(["d1 resolved","p1 done"]);
+ });
+
+ QUnit.test('MutexedDropPrevious: d2 arrives after d1 resolution', async function (assert) {
+ assert.expect(8);
+
+ var m = new concurrency.MutexedDropPrevious();
+ var d1 = makeTestPromiseWithAssert(assert, 'd1');
+
+ m.exec(function () { return d1; }).then(function () {
+ assert.step("p1 resolved");
+ });
+
+ assert.verifySteps([]);
+ d1.resolve('d1');
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok d1','p1 resolved']);
+
+ var d2 = makeTestPromiseWithAssert(assert, 'd2');
+ m.exec(function () { return d2; }).then(function () {
+ assert.step("p2 resolved");
+ });
+
+ assert.verifySteps([]);
+ d2.resolve('d2');
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok d2','p2 resolved']);
+ });
+
+ QUnit.test('MutexedDropPrevious: p1 does not return a deferred', async function (assert) {
+ assert.expect(7);
+
+ var m = new concurrency.MutexedDropPrevious();
+
+ m.exec(function () { return 42; }).then(function () {
+ assert.step("p1 resolved");
+ });
+
+ assert.verifySteps([]);
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['p1 resolved']);
+
+ var d2 = makeTestPromiseWithAssert(assert, 'd2');
+ m.exec(function () { return d2; }).then(function () {
+ assert.step("p2 resolved");
+ });
+
+ assert.verifySteps([]);
+ d2.resolve('d2');
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['ok d2','p2 resolved']);
+ });
+
+ QUnit.test('MutexedDropPrevious: p2 arrives before p1 resolution', async function (assert) {
+ assert.expect(8);
+
+ var m = new concurrency.MutexedDropPrevious();
+ var d1 = makeTestPromiseWithAssert(assert, 'd1');
+
+ m.exec(function () { return d1; }).catch(function () {
+ assert.step("p1 rejected");
+ });
+ assert.verifySteps([]);
+
+ var d2 = makeTestPromiseWithAssert(assert, 'd2');
+ m.exec(function () { return d2; }).then(function () {
+ assert.step("p2 resolved");
+ });
+
+ assert.verifySteps([]);
+ d1.resolve('d1');
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['p1 rejected', 'ok d1']);
+
+ d2.resolve('d2');
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['ok d2', 'p2 resolved']);
+ });
+
+ QUnit.test('MutexedDropPrevious: 3 arrives before 2 initialization', async function (assert) {
+ assert.expect(10);
+ var m = new concurrency.MutexedDropPrevious();
+
+ var d1 = makeTestPromiseWithAssert(assert, 'd1');
+ var d3 = makeTestPromiseWithAssert(assert, 'd3');
+
+ m.exec(function () { return d1; }).catch(function () {
+ assert.step('p1 rejected');
+ });
+
+ m.exec(function () {
+ assert.ok(false, "should not execute this function");
+ }).catch(function () {
+ assert.step('p2 rejected');
+ });
+
+ m.exec(function () { return d3; }).then(function (result) {
+ assert.strictEqual(result, 'd3');
+ assert.step('p3 resolved');
+ });
+
+ assert.verifySteps([]);
+
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['p1 rejected', 'p2 rejected']);
+
+ d1.resolve('d1');
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok d1']);
+
+ d3.resolve('d3');
+ await testUtils.nextTick();
+
+
+ assert.verifySteps(['ok d3','p3 resolved']);
+ });
+
+ QUnit.test('MutexedDropPrevious: 3 arrives after 2 initialization', async function (assert) {
+ assert.expect(15);
+ var m = new concurrency.MutexedDropPrevious();
+
+ var d1 = makeTestPromiseWithAssert(assert, 'd1');
+ var d2 = makeTestPromiseWithAssert(assert, 'd2');
+ var d3 = makeTestPromiseWithAssert(assert, 'd3');
+
+ m.exec(function () {
+ assert.step('execute d1');
+ return d1;
+ }).catch(function () {
+ assert.step('p1 rejected');
+ });
+
+ m.exec(function () {
+ assert.step('execute d2');
+ return d2;
+ }).catch(function () {
+ assert.step('p2 rejected');
+ });
+
+ assert.verifySteps(['execute d1']);
+
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['p1 rejected']);
+
+ d1.resolve('d1');
+ await testUtils.nextMicrotaskTick();
+
+ assert.verifySteps(['ok d1', 'execute d2']);
+
+ m.exec(function () {
+ assert.step('execute d3');
+ return d3;
+ }).then(function () {
+ assert.step('p3 resolved');
+ });
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['p2 rejected']);
+
+ d2.resolve();
+ await testUtils.nextMicrotaskTick();
+ assert.verifySteps(['ok d2', 'execute d3']);
+
+ d3.resolve();
+ await testUtils.nextTick();
+ assert.verifySteps(['ok d3', 'p3 resolved']);
+
+ });
+
+ QUnit.test('MutexedDropPrevious: 2 in then of 1 with 3', async function (assert) {
+ assert.expect(9);
+
+ var m = new concurrency.MutexedDropPrevious();
+
+ var d1 = makeTestPromiseWithAssert(assert, 'd1');
+ var d2 = makeTestPromiseWithAssert(assert, 'd2');
+ var d3 = makeTestPromiseWithAssert(assert, 'd3');
+ var p3;
+
+ m.exec(function () { return d1; })
+ .catch(function () {
+ assert.step('p1 rejected');
+ p3 = m.exec(function () {
+ return d3;
+ }).then(function () {
+ assert.step('p3 resolved');
+ });
+ return p3;
+ });
+
+ await testUtils.nextTick();
+ assert.verifySteps([]);
+
+ m.exec(function () {
+ assert.ok(false, 'should not execute this function');
+ return d2;
+ }).catch(function () {
+ assert.step('p2 rejected');
+ });
+
+ await testUtils.nextTick();
+ assert.verifySteps(['p1 rejected', 'p2 rejected']);
+
+ d1.resolve('d1');
+ await testUtils.nextTick();
+
+ assert.verifySteps(['ok d1']);
+
+ d3.resolve('d3');
+ await testUtils.nextTick();
+
+ assert.verifySteps(['ok d3', 'p3 resolved']);
+ });
+
+});
+
+});
diff --git a/addons/web/static/tests/core/data_comparison_utils_tests.js b/addons/web/static/tests/core/data_comparison_utils_tests.js
new file mode 100644
index 00000000..f5058714
--- /dev/null
+++ b/addons/web/static/tests/core/data_comparison_utils_tests.js
@@ -0,0 +1,75 @@
+odoo.define('web.data_comparison_utils_tests', function(require) {
+"use strict";
+
+var dataComparisonUtils = require('web.dataComparisonUtils');
+var DateClasses = dataComparisonUtils.DateClasses;
+
+QUnit.module('dataComparisonUtils', function () {
+
+ QUnit.module('DateClasses');
+
+
+ QUnit.test('main parameters are correctly computed', function(assert) {
+ assert.expect(30);
+
+ var dateClasses;
+
+ dateClasses = new DateClasses([['2019']]);
+ assert.strictEqual(dateClasses.referenceIndex, 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2019'), 0);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']);
+
+ dateClasses = new DateClasses([['2018', '2019']]);
+ assert.strictEqual(dateClasses.referenceIndex, 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2018'), 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2019'), 1);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2018']);
+ assert.deepEqual(dateClasses.dateClassMembers(1), ['2019']);
+
+ dateClasses = new DateClasses([['2019'], []]);
+ assert.strictEqual(dateClasses.referenceIndex, 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2019'), 0);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']);
+
+ dateClasses = new DateClasses([[], ['2019']]);
+ assert.strictEqual(dateClasses.referenceIndex, 1);
+ assert.strictEqual(dateClasses.dateClass(1, '2019'), 0);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']);
+
+ dateClasses = new DateClasses([['2019'],['2018', '2019']]);
+ assert.strictEqual(dateClasses.referenceIndex, 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2019'), 0);
+ assert.strictEqual(dateClasses.dateClass(1, '2018'), 0);
+ assert.strictEqual(dateClasses.dateClass(1, '2019'), 1);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2019', '2018']);
+ assert.deepEqual(dateClasses.dateClassMembers(1), ['2019']);
+
+
+ dateClasses = new DateClasses([['2019'], ['2017', '2018', '2020'], ['2017', '2019']]);
+ assert.strictEqual(dateClasses.referenceIndex, 0);
+ assert.strictEqual(dateClasses.dateClass(0, '2019'), 0);
+ assert.strictEqual(dateClasses.dateClass(1, '2017'), 0);
+ assert.strictEqual(dateClasses.dateClass(1, '2018'), 1);
+ assert.strictEqual(dateClasses.dateClass(1, '2020'), 2);
+ assert.strictEqual(dateClasses.dateClass(2, '2017'), 0);
+ assert.strictEqual(dateClasses.dateClass(2, '2019'), 1);
+ assert.deepEqual(dateClasses.dateClassMembers(0), ['2019', '2017']);
+ assert.deepEqual(dateClasses.dateClassMembers(1), ['2018', '2019']);
+ assert.deepEqual(dateClasses.dateClassMembers(2), ['2020']);
+
+
+ });
+
+ QUnit.test('two overlapping datesets and classes representatives', function(assert) {
+ assert.expect(4);
+
+ var dateClasses = new DateClasses([['March 2017'], ['February 2017', 'March 2017']]);
+
+ assert.strictEqual(dateClasses.representative(0, 0), 'March 2017');
+ assert.strictEqual(dateClasses.representative(0, 1), 'February 2017');
+
+ assert.strictEqual(dateClasses.representative(1, 0), undefined);
+ assert.strictEqual(dateClasses.representative(1, 1), 'March 2017');
+ });
+});
+});
diff --git a/addons/web/static/tests/core/dialog_tests.js b/addons/web/static/tests/core/dialog_tests.js
new file mode 100644
index 00000000..3c83b2ba
--- /dev/null
+++ b/addons/web/static/tests/core/dialog_tests.js
@@ -0,0 +1,173 @@
+odoo.define('web.dialog_tests', function (require) {
+"use strict";
+
+var Dialog = require('web.Dialog');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+var ESCAPE_KEY = $.Event("keyup", { which: 27 });
+
+async function createEmptyParent(debug) {
+ var widget = new Widget();
+
+ await testUtils.mock.addMockEnvironment(widget, {
+ debug: debug || false,
+ });
+ return widget;
+}
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('Dialog');
+
+ QUnit.test("Closing custom dialog using buttons calls standard callback", async function (assert) {
+ assert.expect(3);
+
+ var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback');
+ var parent = await createEmptyParent();
+ new Dialog(parent, {
+ buttons: [
+ {
+ text: "Close",
+ classes: 'btn-primary',
+ close: true,
+ click: testPromise.resolve,
+ },
+ ],
+ $content: $('<main/>'),
+ onForceClose: testPromise.reject,
+ }).open();
+
+ assert.verifySteps([]);
+
+ await testUtils.nextTick();
+ await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
+
+ testPromise.then(() => {
+ assert.verifySteps(['ok custom callback']);
+ });
+
+ parent.destroy();
+ });
+
+ QUnit.test("Closing custom dialog without using buttons calls force close callback", async function (assert) {
+ assert.expect(3);
+
+ var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback');
+ var parent = await createEmptyParent();
+ new Dialog(parent, {
+ buttons: [
+ {
+ text: "Close",
+ classes: 'btn-primary',
+ close: true,
+ click: testPromise.reject,
+ },
+ ],
+ $content: $('<main/>'),
+ onForceClose: testPromise.resolve,
+ }).open();
+
+ assert.verifySteps([]);
+
+ await testUtils.nextTick();
+ await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
+
+ testPromise.then(() => {
+ assert.verifySteps(['ok custom callback']);
+ });
+
+ parent.destroy();
+ });
+
+ QUnit.test("Closing confirm dialog without using buttons calls cancel callback", async function (assert) {
+ assert.expect(3);
+
+ var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'confirm callback');
+ var parent = await createEmptyParent();
+ var options = {
+ confirm_callback: testPromise.reject,
+ cancel_callback: testPromise.resolve,
+ };
+ Dialog.confirm(parent, "", options);
+
+ assert.verifySteps([]);
+
+ await testUtils.nextTick();
+ await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
+
+ testPromise.then(() => {
+ assert.verifySteps(['ok confirm callback']);
+ });
+
+ parent.destroy();
+ });
+
+ QUnit.test("Closing alert dialog without using buttons calls confirm callback", async function (assert) {
+ assert.expect(3);
+
+ var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'alert callback');
+ var parent = await createEmptyParent();
+ var options = {
+ confirm_callback: testPromise.resolve,
+ };
+ Dialog.alert(parent, "", options);
+
+ assert.verifySteps([]);
+
+ await testUtils.nextTick();
+ await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]);
+
+ testPromise.then(() => {
+ assert.verifySteps(['ok alert callback']);
+ });
+
+ parent.destroy();
+ });
+
+ QUnit.test("Ensure on_attach_callback and on_detach_callback are properly called", async function (assert) {
+ assert.expect(4);
+
+ const TestDialog = Dialog.extend({
+ on_attach_callback() {
+ assert.step('on_attach_callback');
+ },
+ on_detach_callback() {
+ assert.step('on_detach_callback');
+ },
+ });
+
+ const parent = await createEmptyParent();
+ const dialog = new TestDialog(parent, {
+ buttons: [
+ {
+ text: "Close",
+ classes: 'btn-primary',
+ close: true,
+ },
+ ],
+ $content: $('<main/>'),
+ }).open();
+
+ await dialog.opened();
+
+ assert.verifySteps(['on_attach_callback']);
+
+ await testUtils.dom.click($('.modal[role="dialog"] .btn-primary'));
+ assert.verifySteps(['on_detach_callback']);
+
+ parent.destroy();
+ });
+
+ QUnit.test("Should not be displayed if parent is destroyed while dialog is being opened", async function (assert) {
+ assert.expect(1);
+ const parent = await createEmptyParent();
+ const dialog = new Dialog(parent);
+ dialog.open();
+ parent.destroy();
+ await testUtils.nextTick();
+ assert.containsNone(document.body, ".modal[role='dialog']");
+ });
+});
+
+});
diff --git a/addons/web/static/tests/core/dom_tests.js b/addons/web/static/tests/core/dom_tests.js
new file mode 100644
index 00000000..c57ced52
--- /dev/null
+++ b/addons/web/static/tests/core/dom_tests.js
@@ -0,0 +1,133 @@
+odoo.define('web.dom_tests', function (require) {
+"use strict";
+
+var dom = require('web.dom');
+var testUtils = require('web.test_utils');
+
+/**
+ * Create an autoresize text area with 'border-box' as box sizing rule.
+ * The minimum height of this autoresize text are is 1px.
+ *
+ * @param {Object} [options={}]
+ * @param {integer} [options.borderBottomWidth=0]
+ * @param {integer} [options.borderTopWidth=0]
+ * @param {integer} [options.padding=0]
+ */
+function prepareAutoresizeTextArea(options) {
+ options = options || {};
+ var $textarea = $('<textarea>');
+ $textarea.css('box-sizing', 'border-box');
+ $textarea.css({
+ padding: options.padding || 0,
+ borderTopWidth: options.borderTopWidth || 0,
+ borderBottomWidth: options.borderBottomWidth || 0,
+ });
+ $textarea.appendTo($('#qunit-fixture'));
+ dom.autoresize($textarea, { min_height: 1 });
+ return $textarea;
+}
+
+QUnit.module('core', {}, function () {
+QUnit.module('dom', {}, function () {
+
+ QUnit.module('autoresize', {
+ afterEach: function () {
+ $('#qunit-fixture').find('textarea').remove();
+ },
+ });
+
+ QUnit.test('autoresize (border-box): no padding + no border', function (assert) {
+ assert.expect(3);
+ var $textarea = prepareAutoresizeTextArea();
+ assert.strictEqual($('textarea').length, 2,
+ "there should be two textareas in the DOM");
+
+ $textarea = $('textarea:eq(0)');
+ var $fixedTextarea = $('textarea:eq(1)');
+ assert.strictEqual($textarea.css('height'),
+ $fixedTextarea[0].scrollHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + padding (0 line)");
+
+ testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
+ assert.strictEqual($textarea.css('height'),
+ $fixedTextarea[0].scrollHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + padding (4 lines)");
+ });
+
+ QUnit.test('autoresize (border-box): padding + no border', function (assert) {
+ assert.expect(3);
+ var $textarea = prepareAutoresizeTextArea({ padding: 10 });
+ assert.strictEqual($('textarea').length, 2,
+ "there should be two textareas in the DOM");
+
+ $textarea = $('textarea:eq(0)');
+ var $fixedTextarea = $('textarea:eq(1)');
+ // twice the padding of 10px
+ var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + 2*10;
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + padding (0 line)");
+
+ testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
+ // twice the padding of 10px
+ expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + 2*10;
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + padding (4 lines)");
+ });
+
+ QUnit.test('autoresize (border-box): no padding + border', function (assert) {
+ assert.expect(3);
+ var $textarea = prepareAutoresizeTextArea({
+ borderTopWidth: 2,
+ borderBottomWidth: 3,
+ });
+ assert.strictEqual($('textarea').length, 2,
+ "there should be two textareas in the DOM");
+
+ $textarea = $('textarea:eq(0)');
+ var $fixedTextarea = $('textarea:eq(1)');
+ // top (2px) + bottom (3px) borders
+ var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2 + 3);
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + border (0 line)");
+
+ testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
+ // top (2px) + bottom (3px) borders
+ expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2 + 3);
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + border (4 lines)");
+ });
+
+ QUnit.test('autoresize (border-box): padding + border', function (assert) {
+ assert.expect(3);
+ var $textarea = prepareAutoresizeTextArea({
+ padding: 10,
+ borderTopWidth: 2,
+ borderBottomWidth: 3,
+ });
+ assert.strictEqual($('textarea').length, 2,
+ "there should be two textareas in the DOM");
+
+ $textarea = $('textarea:eq(0)');
+ var $fixedTextarea = $('textarea:eq(1)');
+ // twice padding (10px) + top (2px) + bottom (3px) borders
+ var expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2*10 + 2 + 3);
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + border (0 line)");
+
+ testUtils.fields.editInput($textarea, 'a\nb\nc\nd');
+ // twice padding (10px) + top (2px) + bottom (3px) borders
+ expectedTextAreaHeight = $fixedTextarea[0].scrollHeight + (2*10 + 2 + 3);
+ assert.strictEqual($textarea.css('height'),
+ expectedTextAreaHeight + 'px',
+ "autoresized textarea should have height of fixed textarea + border (4 lines)");
+ });
+
+});
+
+});
+});
diff --git a/addons/web/static/tests/core/domain_tests.js b/addons/web/static/tests/core/domain_tests.js
new file mode 100644
index 00000000..fd94db6e
--- /dev/null
+++ b/addons/web/static/tests/core/domain_tests.js
@@ -0,0 +1,186 @@
+odoo.define('web.domain_tests', function (require) {
+"use strict";
+
+var Domain = require('web.Domain');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('domain');
+
+ QUnit.test("empty", function (assert) {
+ assert.expect(1);
+ assert.ok(new Domain([]).compute({}));
+ });
+
+ QUnit.test("basic", function (assert) {
+ assert.expect(3);
+
+ var fields = {
+ a: 3,
+ group_method: 'line',
+ select1: 'day',
+ rrule_type: 'monthly',
+ };
+ assert.ok(new Domain([['a', '=', 3]]).compute(fields));
+ assert.ok(new Domain([['group_method','!=','count']]).compute(fields));
+ assert.ok(new Domain([['select1','=','day'], ['rrule_type','=','monthly']]).compute(fields));
+ });
+
+ QUnit.test("or", function (assert) {
+ assert.expect(3);
+
+ var web = {
+ section_id: null,
+ user_id: null,
+ member_ids: null,
+ };
+ var currentDomain = [
+ '|',
+ ['section_id', '=', 42],
+ '|',
+ ['user_id', '=', 3],
+ ['member_ids', 'in', [3]]
+ ];
+ assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {section_id: 42})));
+ assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {user_id: 3})));
+ assert.ok(new Domain(currentDomain).compute(_.extend({}, web, {member_ids: 3})));
+ });
+
+ QUnit.test("not", function (assert) {
+ assert.expect(2);
+
+ var fields = {
+ a: 5,
+ group_method: 'line',
+ };
+ assert.ok(new Domain(['!', ['a', '=', 3]]).compute(fields));
+ assert.ok(new Domain(['!', ['group_method','=','count']]).compute(fields));
+ });
+
+ QUnit.test("domains initialized with a number", function (assert) {
+ assert.expect(2);
+
+ assert.ok(new Domain(1).compute({}));
+ assert.notOk(new Domain(0).compute({}));
+ });
+
+ QUnit.test("invalid domains should not succeed", function (assert) {
+ assert.expect(3);
+ assert.throws(
+ () => new Domain(['|', ['hr_presence_state', '=', 'absent']]),
+ /invalid domain .* \(missing 1 segment/
+ );
+ assert.throws(
+ () => new Domain(['|', '|', ['hr_presence_state', '=', 'absent'], ['attendance_state', '=', 'checked_in']]),
+ /invalid domain .* \(missing 1 segment/
+ );
+ assert.throws(
+ () => new Domain(['&', ['composition_mode', '!=', 'mass_post']]),
+ /invalid domain .* \(missing 1 segment/
+ );
+ });
+
+ QUnit.test("domain <=> condition", function (assert) {
+ assert.expect(3);
+
+ var domain = [
+ '|',
+ '|',
+ '|',
+ '&', ['doc.amount', '>', 33], ['doc.toto', '!=', null],
+ '&', ['doc.bidule.active', '=', true], ['truc', 'in', [2, 3]],
+ ['gogo', '=', 'gogo value'],
+ ['gogo', '!=', false]
+ ];
+ var condition = '((doc.amount > 33 and doc.toto is not None or doc.bidule.active is True and truc in [2,3]) or gogo == "gogo value") or gogo';
+
+ assert.equal(Domain.prototype.domainToCondition(domain), condition);
+ assert.deepEqual(Domain.prototype.conditionToDomain(condition), domain);
+ assert.deepEqual(Domain.prototype.conditionToDomain(
+ 'doc and toto is None or not tata'),
+ ['|', '&', ['doc', '!=', false], ['toto', '=', null], ['tata', '=', false]]);
+ });
+
+ QUnit.test("condition 'a field is set' does not convert to a domain", function (assert) {
+ assert.expect(1);
+ var expected = [["doc.blabla","!=",false]];
+ var condition = "doc.blabla";
+
+ var actual = Domain.prototype.conditionToDomain(condition);
+
+ assert.deepEqual(actual, expected);
+ });
+
+ QUnit.test("condition with a function should fail", function (assert) {
+ assert.expect(1);
+ var condition = "doc.blabla()";
+
+ assert.throws(function() { Domain.prototype.conditionToDomain(condition); });
+ });
+
+ QUnit.test("empty condition should not fail", function (assert) {
+ assert.expect(2);
+ var condition = "";
+ var actual = Domain.prototype.conditionToDomain(condition);
+ assert.strictEqual(typeof(actual),typeof([]));
+ assert.strictEqual(actual.length, 0);
+ });
+ QUnit.test("undefined condition should not fail", function (assert) {
+ assert.expect(2);
+ var condition = undefined;
+ var actual = Domain.prototype.conditionToDomain(condition);
+ assert.strictEqual(typeof(actual),typeof([]));
+ assert.strictEqual(actual.length, 0);
+ });
+
+ QUnit.test("compute true domain", function (assert) {
+ assert.expect(1);
+ assert.ok(new Domain(Domain.TRUE_DOMAIN).compute({}));
+ });
+
+ QUnit.test("compute false domain", function (assert) {
+ assert.expect(1);
+ assert.notOk(new Domain(Domain.FALSE_DOMAIN).compute({}));
+ });
+
+ QUnit.test("arrayToString", function (assert) {
+ assert.expect(7);
+
+ const arrayToString = Domain.prototype.arrayToString;
+
+ // domains containing null, false or true
+ assert.strictEqual(arrayToString([['name', '=', null]]), '[["name","=",None]]');
+ assert.strictEqual(arrayToString([['name', '=', false]]), '[["name","=",False]]');
+ assert.strictEqual(arrayToString([['name', '=', true]]), '[["name","=",True]]');
+ assert.strictEqual(arrayToString([['name', '=', 'null']]), '[["name","=","null"]]');
+ assert.strictEqual(arrayToString([['name', '=', 'false']]), '[["name","=","false"]]');
+ assert.strictEqual(arrayToString([['name', '=', 'true']]), '[["name","=","true"]]');
+
+ assert.strictEqual(arrayToString(), '[]');
+ });
+
+ QUnit.test("like, =like, ilike and =ilike", function (assert) {
+ assert.expect(16);
+
+ assert.ok(new Domain([['a', 'like', 'value']]).compute({ a: 'value' }));
+ assert.ok(new Domain([['a', 'like', 'value']]).compute({ a: 'some value' }));
+ assert.notOk(new Domain([['a', 'like', 'value']]).compute({ a: 'Some Value' }));
+ assert.notOk(new Domain([['a', 'like', 'value']]).compute({ a: false }));
+
+ assert.ok(new Domain([['a', '=like', '%value']]).compute({ a: 'value' }));
+ assert.ok(new Domain([['a', '=like', '%value']]).compute({ a: 'some value' }));
+ assert.notOk(new Domain([['a', '=like', '%value']]).compute({ a: 'Some Value' }));
+ assert.notOk(new Domain([['a', '=like', '%value']]).compute({ a: false }));
+
+ assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'value' }));
+ assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'some value' }));
+ assert.ok(new Domain([['a', 'ilike', 'value']]).compute({ a: 'Some Value' }));
+ assert.notOk(new Domain([['a', 'ilike', 'value']]).compute({ a: false }));
+
+ assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'value' }));
+ assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'some value' }));
+ assert.ok(new Domain([['a', '=ilike', '%value']]).compute({ a: 'Some Value' }));
+ assert.notOk(new Domain([['a', '=ilike', '%value']]).compute({ a: false }));
+ });
+});
+});
diff --git a/addons/web/static/tests/core/math_utils_tests.js b/addons/web/static/tests/core/math_utils_tests.js
new file mode 100644
index 00000000..1029bdd0
--- /dev/null
+++ b/addons/web/static/tests/core/math_utils_tests.js
@@ -0,0 +1,56 @@
+odoo.define('web.math_utils_tests', function(require) {
+"use strict";
+
+var mathUtils = require('web.mathUtils');
+var cartesian = mathUtils.cartesian;
+
+QUnit.module('mathUtils', function () {
+
+ QUnit.module('cartesian');
+
+
+ QUnit.test('cartesian product of zero arrays', function(assert) {
+ assert.expect(1);
+ assert.deepEqual(cartesian(), [undefined],
+ "the unit of the product is a singleton");
+ });
+
+ QUnit.test('cartesian product of a single array', function(assert) {
+ assert.expect(5);
+ assert.deepEqual(cartesian([]), []);
+ assert.deepEqual(cartesian([1]), [1],
+ "we don't want unecessary brackets");
+ assert.deepEqual(cartesian([1, 2]), [1, 2]);
+ assert.deepEqual(cartesian([[1, 2]]), [[1, 2]],
+ "the internal structure of elements should be preserved");
+ assert.deepEqual(cartesian([[1, 2], [3, [2]]]), [[1, 2], [3, [2]]],
+ "the internal structure of elements should be preserved");
+ });
+
+ QUnit.test('cartesian product of two arrays', function(assert) {
+ assert.expect(5);
+ assert.deepEqual(cartesian([], []), []);
+ assert.deepEqual(cartesian([1], []), []);
+ assert.deepEqual(cartesian([1], [2]), [[1, 2]]);
+ assert.deepEqual(cartesian([1, 2], [3]), [[1, 3], [2, 3]]);
+ assert.deepEqual(cartesian([[1], 4], [2, [3]]), [[[1], 2], [[1], [3]], [4, 2], [4, [3]] ],
+ "the internal structure of elements should be preserved");
+ });
+
+ QUnit.test('cartesian product of three arrays', function(assert) {
+ assert.expect(4);
+ assert.deepEqual(cartesian([], [], []), []);
+ assert.deepEqual(cartesian([1], [], [2, 5]), []);
+ assert.deepEqual(cartesian([1], [2], [3]), [[1, 2, 3]],
+ "we should have no unecessary brackets, we want elements to be 'triples'");
+ assert.deepEqual(cartesian([[1], 2], [3], [4]), [[[1], 3, 4], [2, 3, 4]],
+ "the internal structure of elements should be preserved");
+ });
+
+ QUnit.test('cartesian product of four arrays', function(assert) {
+ assert.expect(1);
+ assert.deepEqual(cartesian([1], [2], [3], [4]), [[1, 2, 3, 4]]);
+ });
+
+});
+});
diff --git a/addons/web/static/tests/core/mixins_tests.js b/addons/web/static/tests/core/mixins_tests.js
new file mode 100644
index 00000000..8c71a259
--- /dev/null
+++ b/addons/web/static/tests/core/mixins_tests.js
@@ -0,0 +1,36 @@
+odoo.define('web.mixins_tests', function (require) {
+"use strict";
+
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('mixins');
+
+ QUnit.test('perform a do_action properly', function (assert) {
+ assert.expect(3);
+ var done = assert.async();
+
+ var widget = new Widget();
+
+ testUtils.mock.intercept(widget, 'do_action', function (event) {
+ assert.strictEqual(event.data.action, 'test.some_action_id',
+ "should have sent proper action name");
+ assert.deepEqual(event.data.options, {clear_breadcrumbs: true},
+ "should have sent proper options");
+ event.data.on_success();
+ });
+
+ widget.do_action('test.some_action_id', {clear_breadcrumbs: true}).then(function () {
+ assert.ok(true, 'deferred should have been resolved');
+ widget.destroy();
+ done();
+ });
+ });
+
+
+});
+
+});
+
diff --git a/addons/web/static/tests/core/owl_dialog_tests.js b/addons/web/static/tests/core/owl_dialog_tests.js
new file mode 100644
index 00000000..cddb7ac7
--- /dev/null
+++ b/addons/web/static/tests/core/owl_dialog_tests.js
@@ -0,0 +1,332 @@
+odoo.define('web.owl_dialog_tests', function (require) {
+ "use strict";
+
+ const LegacyDialog = require('web.Dialog');
+ const makeTestEnvironment = require('web.test_env');
+ const Dialog = require('web.OwlDialog');
+ const testUtils = require('web.test_utils');
+
+ const { Component, tags, useState } = owl;
+ const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 };
+ const { xml } = tags;
+
+ QUnit.module('core', {}, function () {
+ QUnit.module('OwlDialog');
+
+ QUnit.test("Rendering of all props", async function (assert) {
+ assert.expect(35);
+
+ class SubComponent extends Component {
+ // Handlers
+ _onClick() {
+ assert.step('subcomponent_clicked');
+ }
+ }
+ SubComponent.template = xml`<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>`;
+
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.state = useState({ textContent: "sup" });
+ }
+ // Handlers
+ _onButtonClicked(ev) {
+ assert.step('button_clicked');
+ }
+ _onDialogClosed() {
+ assert.step('dialog_closed');
+ }
+ }
+ Parent.components = { Dialog, SubComponent };
+ Parent.env = makeTestEnvironment();
+ Parent.template = xml`
+ <Dialog
+ backdrop="state.backdrop"
+ contentClass="state.contentClass"
+ fullscreen="state.fullscreen"
+ renderFooter="state.renderFooter"
+ renderHeader="state.renderHeader"
+ size="state.size"
+ subtitle="state.subtitle"
+ technical="state.technical"
+ title="state.title"
+ t-on-dialog-closed="_onDialogClosed"
+ >
+ <SubComponent text="state.textContent"/>
+ <t t-set="buttons">
+ <button class="btn btn-primary" t-on-click="_onButtonClicked">The Button</button>
+ </t>
+ </Dialog>`;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+ const dialog = document.querySelector('.o_dialog');
+
+ // Helper function
+ async function changeProps(key, value) {
+ parent.state[key] = value;
+ await testUtils.nextTick();
+ }
+
+ // Basic layout with default properties
+ assert.containsOnce(dialog, '.modal.o_technical_modal');
+ assert.hasClass(dialog.querySelector('.modal .modal-dialog'), 'modal-lg');
+ assert.containsOnce(dialog, '.modal-header > button.close');
+ assert.containsOnce(dialog, '.modal-footer > button.btn.btn-primary');
+ assert.strictEqual(dialog.querySelector('.modal-body').innerText.trim(), "sup",
+ "Subcomponent should match with its given text");
+
+ // Backdrop (default: 'static')
+ // Static backdrop click should focus first button
+ // => we need to reset that property
+ dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely
+ assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
+ assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
+ await testUtils.dom.click(dialog.querySelector('.modal'));
+ assert.strictEqual(document.activeElement, dialog.querySelector('.btn-primary'),
+ "Button should be focused when clicking on backdrop");
+ assert.verifySteps([]); // Ensure not closed
+ dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely
+
+ await changeProps('backdrop', false);
+ assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
+ assert.strictEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)');
+ await testUtils.dom.click(dialog.querySelector('.modal'));
+ assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
+ "Button should not be focused when clicking on backdrop 'false'");
+ assert.verifySteps([]); // Ensure not closed
+
+ await changeProps('backdrop', true);
+ assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
+ assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
+ await testUtils.dom.click(dialog.querySelector('.modal'));
+ assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
+ "Button should not be focused when clicking on backdrop 'true'");
+ assert.verifySteps(['dialog_closed']);
+
+ // Dialog class (default: '')
+ await changeProps('contentClass', 'my_dialog_class');
+ assert.hasClass(dialog.querySelector('.modal-content'), 'my_dialog_class');
+
+ // Full screen (default: false)
+ assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_modal_full');
+ await changeProps('fullscreen', true);
+ assert.hasClass(dialog.querySelector('.modal'), 'o_modal_full');
+
+ // Size class (default: 'large')
+ await changeProps('size', 'extra-large');
+ assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-xl',
+ "Modal should have taken the class modal-xl");
+ await changeProps('size', 'medium');
+ assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog',
+ "Modal should not have any additionnal class with 'medium'");
+ await changeProps('size', 'small');
+ assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-sm',
+ "Modal should have taken the class modal-sm");
+
+ // Subtitle (default: '')
+ await changeProps('subtitle', "The Subtitle");
+ assert.strictEqual(dialog.querySelector('span.o_subtitle').innerText.trim(), "The Subtitle",
+ "Subtitle should match with its given text");
+
+ // Technical (default: true)
+ assert.hasClass(dialog.querySelector('.modal'), 'o_technical_modal');
+ await changeProps('technical', false);
+ assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_technical_modal');
+
+ // Title (default: 'Odoo')
+ assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "Odoo" + "The Subtitle",
+ "Title should match with its default text");
+ await changeProps('title', "The Title");
+ assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "The Title" + "The Subtitle",
+ "Title should match with its given text");
+
+ // Reactivity of buttons
+ await testUtils.dom.click(dialog.querySelector('.modal-footer .btn-primary'));
+
+ // Render footer (default: true)
+ await changeProps('renderFooter', false);
+ assert.containsNone(dialog, '.modal-footer');
+
+ // Render header (default: true)
+ await changeProps('renderHeader', false);
+ assert.containsNone(dialog, '.header');
+
+ // Reactivity of subcomponents
+ await changeProps('textContent', "wassup");
+ assert.strictEqual(dialog.querySelector('.o_subcomponent').innerText.trim(), "wassup",
+ "Subcomponent should match with its given text");
+ await testUtils.dom.click(dialog.querySelector('.o_subcomponent'));
+
+ assert.verifySteps(['button_clicked', 'subcomponent_clicked']);
+
+ parent.destroy();
+ });
+
+ QUnit.test("Interactions between multiple dialogs", async function (assert) {
+ assert.expect(22);
+
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.dialogIds = useState([]);
+ }
+ // Handlers
+ _onDialogClosed(id) {
+ assert.step(`dialog_${id}_closed`);
+ this.dialogIds.splice(this.dialogIds.findIndex(d => d === id), 1);
+ }
+ }
+ Parent.components = { Dialog };
+ Parent.env = makeTestEnvironment();
+ Parent.template = xml`
+ <div>
+ <Dialog t-foreach="dialogIds" t-as="dialogId" t-key="dialogId"
+ contentClass="'dialog_' + dialogId"
+ t-on-dialog-closed="_onDialogClosed(dialogId)"
+ />
+ </div>`;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ // Dialog 1 : Owl
+ parent.dialogIds.push(1);
+ await testUtils.nextTick();
+ // Dialog 2 : Legacy
+ new LegacyDialog(null, {}).open();
+ await testUtils.nextTick();
+ // Dialog 3 : Legacy
+ new LegacyDialog(null, {}).open();
+ await testUtils.nextTick();
+ // Dialog 4 : Owl
+ parent.dialogIds.push(4);
+ await testUtils.nextTick();
+ // Dialog 5 : Owl
+ parent.dialogIds.push(5);
+ await testUtils.nextTick();
+ // Dialog 6 : Legacy (unopened)
+ const unopenedModal = new LegacyDialog(null, {});
+ await testUtils.nextTick();
+
+ // Manually closes the last legacy dialog. Should not affect the other
+ // existing dialogs (3 owl and 2 legacy).
+ unopenedModal.close();
+
+ let modals = document.querySelectorAll('.modal');
+ assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
+ "last dialog should have the active class");
+ assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
+ "active dialog should not have the legacy class");
+ assert.containsN(document.body, '.o_dialog', 3);
+ assert.containsN(document.body, '.o_legacy_dialog', 2);
+
+ // Reactivity with owl dialogs
+ await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); // Press Escape
+
+ modals = document.querySelectorAll('.modal');
+ assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
+ "last dialog should have the active class");
+ assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
+ "active dialog should not have the legacy class");
+ assert.containsN(document.body, '.o_dialog', 2);
+ assert.containsN(document.body, '.o_legacy_dialog', 2);
+
+ await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn.btn-primary')); // Click on 'Ok' button
+
+ modals = document.querySelectorAll('.modal');
+ assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
+ "active dialog should have the legacy class");
+ assert.containsOnce(document.body, '.o_dialog');
+ assert.containsN(document.body, '.o_legacy_dialog', 2);
+
+ // Reactivity with legacy dialogs
+ await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey);
+
+ modals = document.querySelectorAll('.modal');
+ assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
+ "active dialog should have the legacy class");
+ assert.containsOnce(document.body, '.o_dialog');
+ assert.containsOnce(document.body, '.o_legacy_dialog');
+
+ await testUtils.dom.click(modals[modals.length - 1].querySelector('.close'));
+
+ modals = document.querySelectorAll('.modal');
+ assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
+ "last dialog should have the active class");
+ assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
+ "active dialog should not have the legacy class");
+ assert.containsOnce(document.body, '.o_dialog');
+ assert.containsNone(document.body, '.o_legacy_dialog');
+
+ parent.unmount();
+
+ assert.containsNone(document.body, '.modal');
+ // dialog 1 is closed through the removal of its parent => no callback
+ assert.verifySteps(['dialog_5_closed', 'dialog_4_closed']);
+
+ parent.destroy();
+ });
+ });
+
+ QUnit.test("Z-index toggling and interactions", async function (assert) {
+ assert.expect(3);
+
+ function createCustomModal(className) {
+ const $modal = $(
+ `<div role="dialog" class="${className}" tabindex="-1">
+ <div class="modal-dialog medium">
+ <div class="modal-content">
+ <main class="modal-body">The modal body</main>
+ </div>
+ </div>
+ </div>`
+ ).appendTo('body').modal();
+ const modal = $modal[0];
+ modal.destroy = function () {
+ $modal.modal('hide');
+ this.remove();
+ };
+ return modal;
+ }
+
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.state = useState({ showSecondDialog: true });
+ }
+ }
+ Parent.components = { Dialog };
+ Parent.env = makeTestEnvironment();
+ Parent.template = xml`
+ <div>
+ <Dialog/>
+ <Dialog t-if="state.showSecondDialog"/>
+ </div>`;
+
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget());
+
+ const frontEndModal = createCustomModal('modal');
+ const backEndModal = createCustomModal('modal o_technical_modal');
+
+ // querySelector will target the first modal (the static one).
+ const owlIndexBefore = getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex;
+ const feZIndexBefore = getComputedStyle(frontEndModal).zIndex;
+ const beZIndexBefore = getComputedStyle(backEndModal).zIndex;
+
+ parent.state.showSecondDialog = false;
+ await testUtils.nextTick();
+
+ assert.ok(owlIndexBefore < getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex,
+ "z-index of the owl dialog should be incremented since the active modal was destroyed");
+ assert.strictEqual(feZIndexBefore, getComputedStyle(frontEndModal).zIndex,
+ "z-index of front-end modals should not be impacted by Owl Dialog activity system");
+ assert.strictEqual(beZIndexBefore, getComputedStyle(backEndModal).zIndex,
+ "z-index of custom back-end modals should not be impacted by Owl Dialog activity system");
+
+ parent.destroy();
+ frontEndModal.destroy();
+ backEndModal.destroy();
+ });
+});
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 00000000..7329a960
--- /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 {});
+ });
+ });
+ });
+});
+});
diff --git a/addons/web/static/tests/core/popover_tests.js b/addons/web/static/tests/core/popover_tests.js
new file mode 100644
index 00000000..4b5002d3
--- /dev/null
+++ b/addons/web/static/tests/core/popover_tests.js
@@ -0,0 +1,280 @@
+odoo.define('web.popover_tests', function (require) {
+ 'use strict';
+
+ const makeTestEnvironment = require('web.test_env');
+ const Popover = require('web.Popover');
+ const testUtils = require('web.test_utils');
+
+ const { Component, tags, hooks } = owl;
+ const { useRef, useState } = hooks;
+ const { xml } = tags;
+
+ QUnit.module('core', {}, function () {
+ QUnit.module('Popover');
+
+ QUnit.test('Basic rendering & props', async function (assert) {
+ assert.expect(11);
+
+ class SubComponent extends Component {}
+ SubComponent.template = xml`
+ <div class="o_subcomponent" style="width: 280px;" t-esc="props.text"/>
+ `;
+
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.state = useState({
+ position: 'right',
+ title: '👋',
+ textContent: 'sup',
+ });
+ this.popoverRef = useRef('popoverRef');
+ }
+ }
+ // Popover should be included as a globally available Component
+ Parent.components = { SubComponent };
+ Parent.env = makeTestEnvironment();
+ Parent.template = xml`
+ <div>
+ <button id="passiveTarget">🚫</button>
+ <Popover t-ref="popoverRef"
+ position="state.position"
+ title="state.title"
+ >
+ <t t-set="opened">
+ <SubComponent text="state.textContent"/>
+ </t>
+ <button id="target">
+ Notice me, senpai 👀
+ </button>
+ </Popover>
+ </div>`;
+
+ const parent = new Parent();
+ const fixture = testUtils.prepareTarget();
+ /**
+ * The component being tested behaves differently based on its
+ * visibility (or not) in the viewport. The qunit fixture has to be
+ * in the view port for these tests to be meaningful.
+ */
+ fixture.style.top = '300px';
+ fixture.style.left = '150px';
+ fixture.style.width = '300px';
+
+ // Helper functions
+ async function changeProps(key, value) {
+ parent.state[key] = value;
+ await testUtils.nextTick();
+ }
+ function pointsTo(popover, element, position) {
+ const hasCorrectClass = popover.classList.contains(
+ `o_popover--${position}`
+ );
+ const expectedPosition = Popover.computePositioningData(
+ popover,
+ element
+ )[position];
+ const correctLeft =
+ parseFloat(popover.style.left) ===
+ Math.round(expectedPosition.left * 100) / 100;
+ const correctTop =
+ parseFloat(popover.style.top) ===
+ Math.round(expectedPosition.top * 100) / 100;
+ return hasCorrectClass && correctLeft && correctTop;
+ }
+
+ await parent.mount(fixture);
+ const body = document.querySelector('body');
+ let popover, title;
+ // Show/hide
+ assert.containsNone(body, '.o_popover');
+ await testUtils.dom.click('#target');
+ assert.containsOnce(body, '.o_popover');
+ assert.containsOnce(body, '.o_subcomponent');
+ assert.containsOnce(body, '.o_popover--right');
+ await testUtils.dom.click('#passiveTarget');
+ assert.containsNone(body, '.o_popover');
+ // Reactivity of title
+ await testUtils.dom.click('#target');
+ popover = document.querySelector('.o_popover');
+ title = popover.querySelector('.o_popover_header').innerText.trim();
+ assert.strictEqual(title, '👋');
+ await changeProps('title', '🤔');
+ title = popover.querySelector('.o_popover_header').innerText.trim();
+ assert.strictEqual(
+ title,
+ '🤔',
+ 'The title of the popover should have changed.'
+ );
+ // Position and target reactivity
+ const element = parent.popoverRef.el;
+ assert.ok(
+ pointsTo(
+ document.querySelector('.o_popover'),
+ element,
+ parent.state.position
+ ),
+ 'Popover should be visually aligned with its target'
+ );
+ await changeProps('position', 'bottom');
+ assert.ok(
+ pointsTo(
+ document.querySelector('.o_popover'),
+ element,
+ parent.state.position
+ ),
+ 'Popover should be bottomed positioned'
+ );
+ // Reactivity of subcomponents
+ await changeProps('textContent', 'wassup');
+ assert.strictEqual(
+ popover.querySelector('.o_subcomponent').innerText.trim(),
+ 'wassup',
+ 'Subcomponent should match with its given text'
+ );
+ await testUtils.dom.click('#passiveTarget');
+ // Requested position not fitting
+ await changeProps('position', 'left');
+ await testUtils.dom.click('#target');
+ assert.ok(
+ pointsTo(document.querySelector('.o_popover'), element, 'right'),
+ "Popover should be right-positioned because it doesn't fit left"
+ );
+ await testUtils.dom.click('#passiveTarget');
+ parent.destroy();
+ });
+
+ QUnit.test('Multiple popovers', async function (assert) {
+ assert.expect(9);
+
+ class Parent extends Component {}
+ Parent.components = { Popover };
+ Parent.env = makeTestEnvironment();
+ Parent.template = xml`
+ <div>
+ <Popover>
+ <button id="firstTarget">👋</button>
+ <t t-set="opened">
+ <p id="firstContent">first popover</p>
+ </t>
+ </Popover>
+ <br/>
+ <Popover>
+ <button id="secondTarget">👏</button>
+ <t t-set="opened">
+ <p id="secondContent">second popover</p>
+ </t>
+ </Popover>
+ <br/>
+ <span id="dismissPopovers">💀</span>
+ </div>`;
+
+ const parent = new Parent();
+ const fixture = testUtils.prepareTarget();
+
+ const body = document.querySelector('body');
+ await parent.mount(fixture);
+ // Show first popover
+ assert.containsNone(body, '.o_popover');
+ await testUtils.dom.click('#firstTarget');
+ assert.containsOnce(body, '#firstContent');
+ assert.containsNone(body, '#secondContent');
+ await testUtils.dom.click('#dismissPopovers');
+ assert.containsNone(body, '.o_popover');
+ // Show first then display second
+ await testUtils.dom.click('#firstTarget');
+ assert.containsOnce(body, '#firstContent');
+ assert.containsNone(body, '#secondContent');
+ await testUtils.dom.click('#secondTarget');
+ assert.containsNone(body, '#firstContent');
+ assert.containsOnce(body, '#secondContent');
+ await testUtils.dom.click('#dismissPopovers');
+ assert.containsNone(body, '.o_popover');
+ parent.destroy();
+ });
+
+ QUnit.test('toggle', async function (assert) {
+ assert.expect(4);
+
+ class Parent extends Component {}
+ // Popover should be included as a globally available Component
+ Object.assign(Parent, {
+ env: makeTestEnvironment(),
+ template: xml`
+ <div>
+ <Popover>
+ <button id="open">Open</button>
+ <t t-set="opened">
+ Opened!
+ </t>
+ </Popover>
+ </div>
+ `,
+ });
+
+ const parent = new Parent();
+ const fixture = testUtils.prepareTarget();
+ await parent.mount(fixture);
+
+ const body = document.querySelector('body');
+ assert.containsOnce(body, '#open');
+ assert.containsNone(body, '.o_popover');
+
+ await testUtils.dom.click('#open');
+ assert.containsOnce(body, '.o_popover');
+
+ await testUtils.dom.click('#open');
+ assert.containsNone(body, '.o_popover');
+
+ parent.destroy();
+ });
+
+ QUnit.test('close event', async function (assert) {
+ assert.expect(7);
+
+ // Needed to trigger the event from inside the Popover slot.
+ class Content extends Component {}
+ Content.template = xml`
+ <button id="close" t-on-click="trigger('o-popover-close')">
+ Close
+ </button>
+ `;
+
+ class Parent extends Component {}
+ // Popover should be included as a globally available Component
+ Object.assign(Parent, {
+ components: { Content },
+ env: makeTestEnvironment(),
+ template: xml`
+ <div>
+ <Popover>
+ <button id="open">Open</button>
+ <t t-set="opened">
+ <Content/>
+ </t>
+ </Popover>
+ </div>
+ `,
+ });
+
+ const parent = new Parent();
+ const fixture = testUtils.prepareTarget();
+ await parent.mount(fixture);
+
+ const body = document.querySelector('body');
+ assert.containsOnce(body, '#open');
+ assert.containsNone(body, '.o_popover');
+ assert.containsNone(body, '#close');
+
+ await testUtils.dom.click('#open');
+ assert.containsOnce(body, '.o_popover');
+ assert.containsOnce(body, '#close');
+
+ await testUtils.dom.click('#close');
+ assert.containsNone(body, '.o_popover');
+ assert.containsNone(body, '#close');
+
+ parent.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js
new file mode 100644
index 00000000..e3cec9ed
--- /dev/null
+++ b/addons/web/static/tests/core/py_utils_tests.js
@@ -0,0 +1,1376 @@
+odoo.define('web.py_utils_tests', function(require) {
+"use strict";
+
+var Context = require('web.Context');
+var pyUtils = require('web.py_utils');
+var time = require('web.time');
+
+const r = String.raw;
+
+QUnit.assert.checkAST = function (expr, message) {
+ var ast = pyUtils._getPyJSAST(expr);
+ var formattedAST = pyUtils._formatAST(ast);
+ this.pushResult({
+ result: expr === formattedAST,
+ actual: formattedAST,
+ expected: expr,
+ message: message
+ });
+};
+
+QUnit.module('core', function () {
+
+ QUnit.module('py_utils');
+
+ QUnit.test('simple python expression', function(assert) {
+ assert.expect(2);
+
+ var result = pyUtils.py_eval("True and False");
+ assert.strictEqual(result, false, "should properly evaluate basic expression");
+ result = pyUtils.py_eval("a + b", {a: 1, b: 41});
+ assert.strictEqual(result, 42, "should properly evaluate basic sum");
+ });
+
+ QUnit.test('simple arithmetic', function(assert) {
+ assert.expect(3);
+
+ var result = pyUtils.py_eval("1 + 2");
+ assert.strictEqual(result, 3, "should properly evaluate sum");
+ result = pyUtils.py_eval("42 % 5");
+ assert.strictEqual(result, 2, "should properly evaluate modulo operator");
+ result = pyUtils.py_eval("2 ** 3");
+ assert.strictEqual(result, 8, "should properly evaluate power operator");
+ });
+
+
+ QUnit.test('not prefix', function (assert) {
+ assert.expect(3);
+
+ assert.ok(py.eval('not False'));
+ assert.ok(py.eval('not foo', {foo: false}));
+ assert.ok(py.eval('not a in b', {a: 3, b: [1, 2, 4, 8]}));
+ });
+
+
+ function makeTimeCheck (assert) {
+ var context = pyUtils.context();
+ return function (expr, func, message) {
+ // evaluate expr between two calls to new Date(), and check that
+ // the result is between the transformed dates
+ var d0 = new Date();
+ var result = py.eval(expr, context);
+ var d1 = new Date();
+ assert.ok(func(d0) <= result && result <= func(d1), message);
+ };
+ }
+
+ // Port from pypy/lib_pypy/test_datetime.py
+ function makeEq(assert, c2) {
+ var ctx = pyUtils.context();
+ var c = _.extend({ td: ctx.datetime.timedelta }, c2 || {});
+ return function (a, b, message) {
+ assert.ok(py.eval(a + ' == ' + b, c), message);
+ };
+ }
+
+ QUnit.test('strftime', function (assert) {
+ assert.expect(3);
+
+ var check = makeTimeCheck(assert);
+
+ check("time.strftime('%Y')", function(d) {
+ return String(d.getFullYear());
+ });
+
+ check("time.strftime('%Y')+'-01-30'", function(d) {
+ return String(d.getFullYear()) + '-01-30';
+ });
+
+ check("time.strftime('%Y-%m-%d %H:%M:%S')", function(d) {
+ return _.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d',
+ d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
+ d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds());
+ });
+ });
+
+ QUnit.test('context_today', function (assert) {
+ assert.expect(1);
+
+ var check = makeTimeCheck(assert, pyUtils);
+
+ check("context_today().strftime('%Y-%m-%d')", function(d) {
+ return String(_.str.sprintf('%04d-%02d-%02d',
+ d.getFullYear(), d.getMonth() + 1, d.getDate()));
+ });
+ });
+
+ QUnit.test('timedelta.test_constructor', function (assert) {
+ assert.expect(16);
+
+ var eq = makeEq(assert);
+
+ // keyword args to constructor
+ eq('td()', 'td(weeks=0, days=0, hours=0, minutes=0, seconds=0, ' +
+ 'milliseconds=0, microseconds=0)');
+ eq('td(1)', 'td(days=1)');
+ eq('td(0, 1)', 'td(seconds=1)');
+ eq('td(0, 0, 1)', 'td(microseconds=1)');
+ eq('td(weeks=1)', 'td(days=7)');
+ eq('td(days=1)', 'td(hours=24)');
+ eq('td(hours=1)', 'td(minutes=60)');
+ eq('td(minutes=1)', 'td(seconds=60)');
+ eq('td(seconds=1)', 'td(milliseconds=1000)');
+ eq('td(milliseconds=1)', 'td(microseconds=1000)');
+
+ // Check float args to constructor
+ eq('td(weeks=1.0/7)', 'td(days=1)');
+ eq('td(days=1.0/24)', 'td(hours=1)');
+ eq('td(hours=1.0/60)', 'td(minutes=1)');
+ eq('td(minutes=1.0/60)', 'td(seconds=1)');
+ eq('td(seconds=0.001)', 'td(milliseconds=1)');
+ eq('td(milliseconds=0.001)', 'td(microseconds=1)');
+ });
+
+ QUnit.test('timedelta.test_computations', function (assert) {
+ assert.expect(28);
+
+ var c = pyUtils.context();
+ var zero = py.float.fromJSON(0);
+ var eq = makeEq(assert, {
+ // one week
+ a: py.PY_call(c.datetime.timedelta, [
+ py.float.fromJSON(7)]),
+ // one minute
+ b: py.PY_call(c.datetime.timedelta, [
+ zero, py.float.fromJSON(60)]),
+ // one millisecond
+ c: py.PY_call(c.datetime.timedelta, [
+ zero, zero, py.float.fromJSON(1000)]),
+ });
+
+ eq('a+b+c', 'td(7, 60, 1000)');
+ eq('a-b', 'td(6, 24*3600 - 60)');
+ eq('-a', 'td(-7)');
+ eq('+a', 'td(7)');
+ eq('-b', 'td(-1, 24*3600 - 60)');
+ eq('-c', 'td(-1, 24*3600 - 1, 999000)');
+ // eq('abs(a)', 'a');
+ // eq('abs(-a)', 'a');
+ eq('td(6, 24*3600)', 'a');
+ eq('td(0, 0, 60*1000000)', 'b');
+ eq('a*10', 'td(70)');
+ eq('a*10', '10*a');
+ // eq('a*10L', '10*a');
+ eq('b*10', 'td(0, 600)');
+ eq('10*b', 'td(0, 600)');
+ // eq('b*10L', 'td(0, 600)');
+ eq('c*10', 'td(0, 0, 10000)');
+ eq('10*c', 'td(0, 0, 10000)');
+ // eq('c*10L', 'td(0, 0, 10000)');
+ eq('a*-1', '-a');
+ eq('b*-2', '-b-b');
+ eq('c*-2', '-c+-c');
+ eq('b*(60*24)', '(b*60)*24');
+ eq('b*(60*24)', '(60*b)*24');
+ eq('c*1000', 'td(0, 1)');
+ eq('1000*c', 'td(0, 1)');
+ eq('a//7', 'td(1)');
+ eq('b//10', 'td(0, 6)');
+ eq('c//1000', 'td(0, 0, 1)');
+ eq('a//10', 'td(0, 7*24*360)');
+ eq('a//3600000', 'td(0, 0, 7*24*1000)');
+
+ // Issue #11576
+ eq('td(999999999, 86399, 999999) - td(999999999, 86399, 999998)', 'td(0, 0, 1)');
+ eq('td(999999999, 1, 1) - td(999999999, 1, 0)',
+ 'td(0, 0, 1)');
+ });
+
+ QUnit.test('timedelta.test_basic_attributes', function (assert) {
+ assert.expect(3);
+
+ var ctx = pyUtils.context();
+ assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).days', ctx), 1);
+ assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).seconds', ctx), 7);
+ assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).microseconds', ctx), 31);
+ });
+
+ QUnit.test('timedelta.test_total_seconds', function (assert) {
+ assert.expect(6);
+
+ var c = { timedelta: pyUtils.context().datetime.timedelta };
+ assert.strictEqual(py.eval('timedelta(365).total_seconds()', c), 31536000);
+ assert.strictEqual(
+ py.eval('timedelta(seconds=123456.789012).total_seconds()', c),
+ 123456.789012);
+ assert.strictEqual(
+ py.eval('timedelta(seconds=-123456.789012).total_seconds()', c),
+ -123456.789012);
+ assert.strictEqual(
+ py.eval('timedelta(seconds=0.123456).total_seconds()', c), 0.123456);
+ assert.strictEqual(py.eval('timedelta().total_seconds()', c), 0);
+ assert.strictEqual(
+ py.eval('timedelta(seconds=1000000).total_seconds()', c), 1e6);
+ });
+
+ QUnit.test('timedelta.test_str', function (assert) {
+ assert.expect(10);
+
+ var c = { td: pyUtils.context().datetime.timedelta };
+
+ assert.strictEqual(py.eval('str(td(1))', c), "1 day, 0:00:00");
+ assert.strictEqual(py.eval('str(td(-1))', c), "-1 day, 0:00:00");
+ assert.strictEqual(py.eval('str(td(2))', c), "2 days, 0:00:00");
+ assert.strictEqual(py.eval('str(td(-2))', c), "-2 days, 0:00:00");
+
+ assert.strictEqual(py.eval('str(td(hours=12, minutes=58, seconds=59))', c),
+ "12:58:59");
+ assert.strictEqual(py.eval('str(td(hours=2, minutes=3, seconds=4))', c),
+ "2:03:04");
+ assert.strictEqual(
+ py.eval('str(td(weeks=-30, hours=23, minutes=12, seconds=34))', c),
+ "-210 days, 23:12:34");
+
+ assert.strictEqual(py.eval('str(td(milliseconds=1))', c), "0:00:00.001000");
+ assert.strictEqual(py.eval('str(td(microseconds=3))', c), "0:00:00.000003");
+
+ assert.strictEqual(
+ py.eval('str(td(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999))', c),
+ "999999999 days, 23:59:59.999999");
+ });
+
+ QUnit.test('timedelta.test_massive_normalization', function (assert) {
+ assert.expect(3);
+
+ var td = py.PY_call(
+ pyUtils.context().datetime.timedelta,
+ {microseconds: py.float.fromJSON(-1)});
+ assert.strictEqual(td.days, -1);
+ assert.strictEqual(td.seconds, 24 * 3600 - 1);
+ assert.strictEqual(td.microseconds, 999999);
+ });
+
+ QUnit.test('timedelta.test_bool', function (assert) {
+ assert.expect(5);
+
+ var c = { td: pyUtils.context().datetime.timedelta };
+ assert.ok(py.eval('bool(td(1))', c));
+ assert.ok(py.eval('bool(td(0, 1))', c));
+ assert.ok(py.eval('bool(td(0, 0, 1))', c));
+ assert.ok(py.eval('bool(td(microseconds=1))', c));
+ assert.ok(py.eval('bool(not td(0))', c));
+ });
+
+ QUnit.test('date.test_computations', function (assert) {
+ assert.expect(31);
+
+ var d = pyUtils.context().datetime;
+
+ var a = d.date.fromJSON(2002, 1, 31);
+ var b = d.date.fromJSON(1956, 1, 31);
+ assert.strictEqual(
+ py.eval('(a - b).days', {a: a, b: b}),
+ 46 * 365 + 12);
+ assert.strictEqual(py.eval('(a - b).seconds', {a: a, b: b}), 0);
+ assert.strictEqual(py.eval('(a - b).microseconds', {a: a, b: b}), 0);
+
+ var day = py.PY_call(d.timedelta, [py.float.fromJSON(1)]);
+ var week = py.PY_call(d.timedelta, [py.float.fromJSON(7)]);
+ a = d.date.fromJSON(2002, 3, 2);
+ var ctx = {
+ a: a,
+ day: day,
+ week: week,
+ date: d.date
+ };
+ assert.ok(py.eval('a + day == date(2002, 3, 3)', ctx));
+ assert.ok(py.eval('day + a == date(2002, 3, 3)', ctx)); // 5
+ assert.ok(py.eval('a - day == date(2002, 3, 1)', ctx));
+ assert.ok(py.eval('-day + a == date(2002, 3, 1)', ctx));
+ assert.ok(py.eval('a + week == date(2002, 3, 9)', ctx));
+ assert.ok(py.eval('a - week == date(2002, 2, 23)', ctx));
+ assert.ok(py.eval('a + 52*week == date(2003, 3, 1)', ctx)); // 10
+ assert.ok(py.eval('a - 52*week == date(2001, 3, 3)', ctx));
+ assert.ok(py.eval('(a + week) - a == week', ctx));
+ assert.ok(py.eval('(a + day) - a == day', ctx));
+ assert.ok(py.eval('(a - week) - a == -week', ctx));
+ assert.ok(py.eval('(a - day) - a == -day', ctx)); // 15
+ assert.ok(py.eval('a - (a + week) == -week', ctx));
+ assert.ok(py.eval('a - (a + day) == -day', ctx));
+ assert.ok(py.eval('a - (a - week) == week', ctx));
+ assert.ok(py.eval('a - (a - day) == day', ctx));
+
+ assert.throws(function () {
+ py.eval('a + 1', ctx);
+ }, /^Error: TypeError:/); //20
+ assert.throws(function () {
+ py.eval('a - 1', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('1 + a', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('1 - a', ctx);
+ }, /^Error: TypeError:/);
+
+ // delta - date is senseless.
+ assert.throws(function () {
+ py.eval('day - a', ctx);
+ }, /^Error: TypeError:/);
+ // mixing date and (delta or date) via * or // is senseless
+ assert.throws(function () {
+ py.eval('day * a', ctx);
+ }, /^Error: TypeError:/); // 25
+ assert.throws(function () {
+ py.eval('a * day', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('day // a', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('a // day', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('a * a', ctx);
+ }, /^Error: TypeError:/);
+ assert.throws(function () {
+ py.eval('a // a', ctx);
+ }, /^Error: TypeError:/); // 30
+ // date + date is senseless
+ assert.throws(function () {
+ py.eval('a + a', ctx);
+ }, /^Error: TypeError:/);
+
+ });
+
+ QUnit.test('add', function (assert) {
+ assert.expect(2);
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2017, 4, 18, 9, 32, 15).add(hours=2, minutes=30, " +
+ "seconds=10)).strftime('%Y-%m-%d %H:%M:%S')", pyUtils.context()),
+ '2017-04-18 12:02:25'
+ );
+ assert.strictEqual(
+ py.eval("(datetime.date(2017, 4, 18).add(months=1, years=3, days=5))" +
+ ".strftime('%Y-%m-%d')", pyUtils.context()),
+ '2020-05-23'
+ );
+ });
+
+ QUnit.test('subtract', function(assert) {
+ assert.expect(2);
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2017, 4, 18, 9, 32, 15).subtract(hours=1, minutes=5, " +
+ "seconds=33)).strftime('%Y-%m-%d %H:%M:%S')", pyUtils.context()),
+ '2017-04-18 08:26:42'
+ );
+ assert.strictEqual(
+ py.eval("(datetime.date(2017, 4, 18).subtract(years=5, months=1, days=1))" +
+ ".strftime('%Y-%m-%d')", pyUtils.context()),
+ '2012-03-17'
+ );
+ })
+
+ QUnit.test('start_of/end_of', function (assert) {
+ assert.expect(26);
+
+ var datetime = pyUtils.context().datetime;
+ // Ain't that a kick in the head?
+ var _date = datetime.date.fromJSON(2281, 10, 11);
+ var _datetime = datetime.datetime.fromJSON(2281, 10, 11, 22, 33, 44);
+ var ctx = {
+ _date: _date,
+ _datetime: _datetime,
+ date: datetime.date,
+ datetime: datetime.datetime
+ };
+
+ // Start of period
+ // Dates first
+ assert.ok(py.eval('_date.start_of("year") == date(2281, 1, 1)', ctx));
+ assert.ok(py.eval('_date.start_of("quarter") == date(2281, 10, 1)', ctx));
+ assert.ok(py.eval('_date.start_of("month") == date(2281, 10, 1)', ctx));
+ assert.ok(py.eval('_date.start_of("week") == date(2281, 10, 10)', ctx));
+ assert.ok(py.eval('_date.start_of("day") == date(2281, 10, 11)', ctx));
+ assert.throws(function () {
+ py.eval('_date.start_of("hour")', ctx);
+ }, /^Error: ValueError:/);
+
+ // Datetimes
+ assert.ok(py.eval('_datetime.start_of("year") == datetime(2281, 1, 1)', ctx));
+ assert.ok(py.eval('_datetime.start_of("quarter") == datetime(2281, 10, 1)', ctx));
+ assert.ok(py.eval('_datetime.start_of("month") == datetime(2281, 10, 1)', ctx));
+ assert.ok(py.eval('_datetime.start_of("week") == datetime(2281, 10, 10)', ctx));
+ assert.ok(py.eval('_datetime.start_of("day") == datetime(2281, 10, 11)', ctx));
+ assert.ok(py.eval('_datetime.start_of("hour") == datetime(2281, 10, 11, 22, 0, 0)', ctx));
+ assert.throws(function () {
+ py.eval('_datetime.start_of("cheese")', ctx);
+ }, /^Error: ValueError:/);
+
+ // End of period
+ // Dates
+ assert.ok(py.eval('_date.end_of("year") == date(2281, 12, 31)', ctx));
+ assert.ok(py.eval('_date.end_of("quarter") == date(2281, 12, 31)', ctx));
+ assert.ok(py.eval('_date.end_of("month") == date(2281, 10, 31)', ctx));
+ assert.ok(py.eval('_date.end_of("week") == date(2281, 10, 16)', ctx));
+ assert.ok(py.eval('_date.end_of("day") == date(2281, 10, 11)', ctx));
+ assert.throws(function () {
+ py.eval('_date.start_of("hour")', ctx);
+ }, /^Error: ValueError:/);
+
+ // Datetimes
+ assert.ok(py.eval('_datetime.end_of("year") == datetime(2281, 12, 31, 23, 59, 59)', ctx));
+ assert.ok(py.eval('_datetime.end_of("quarter") == datetime(2281, 12, 31, 23, 59, 59)', ctx));
+ assert.ok(py.eval('_datetime.end_of("month") == datetime(2281, 10, 31, 23, 59, 59)', ctx));
+ assert.ok(py.eval('_datetime.end_of("week") == datetime(2281, 10, 16, 23, 59, 59)', ctx));
+ assert.ok(py.eval('_datetime.end_of("day") == datetime(2281, 10, 11, 23, 59, 59)', ctx));
+ assert.ok(py.eval('_datetime.end_of("hour") == datetime(2281, 10, 11, 22, 59, 59)', ctx));
+ assert.throws(function () {
+ py.eval('_datetime.end_of("cheese")', ctx);
+ }, /^Error: ValueError:/);
+ });
+
+ QUnit.test('relativedelta', function (assert) {
+ assert.expect(7);
+
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
+ pyUtils.context()),
+ "2012-02-14 23:59:59");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=1)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ "2012-02-16");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ "2012-02-14");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 1) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ '2012-01-31');
+ assert.strictEqual(
+ py.eval("(datetime.date(2015,2,5)+relativedelta(days=-6,weekday=0)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ '2015-02-02');
+ assert.strictEqual(
+ py.eval("(datetime.date(2018, 2, 1) + relativedelta(years=7, months=42, days=42)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ '2028-09-12');
+ assert.strictEqual(
+ py.eval("(datetime.date(2018, 2, 1) + relativedelta(years=-7, months=-42, days=-42)).strftime('%Y-%m-%d')",
+ pyUtils.context()),
+ '2007-06-20');
+ });
+
+
+ QUnit.test('timedelta', function (assert) {
+ assert.expect(4);
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')",
+ pyUtils.context()),
+ "2017-02-16 01:07:31");
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')",
+ pyUtils.context()),
+ "2012-02-15 00:07:31");
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')",
+ pyUtils.context()),
+ "2012-02-15 00:07:31");
+ assert.strictEqual(
+ py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')",
+ pyUtils.context()),
+ "2012-02-15 02:47:31");
+ });
+
+ QUnit.test('datetime.tojson', function (assert) {
+ assert.expect(7);
+
+ var result = py.eval(
+ 'datetime.datetime(2012, 2, 15, 1, 7, 31)',
+ pyUtils.context());
+
+ assert.ok(result instanceof Date);
+ assert.strictEqual(result.getFullYear(), 2012);
+ assert.strictEqual(result.getMonth(), 1);
+ assert.strictEqual(result.getDate(), 15);
+ assert.strictEqual(result.getHours(), 1);
+ assert.strictEqual(result.getMinutes(), 7);
+ assert.strictEqual(result.getSeconds(), 31);
+ });
+
+ QUnit.test('datetime.combine', function (assert) {
+ assert.expect(2);
+
+ var result = py.eval(
+ 'datetime.datetime.combine(datetime.date(2012, 2, 15),' +
+ ' datetime.time(1, 7, 13))' +
+ ' .strftime("%Y-%m-%d %H:%M:%S")',
+ pyUtils.context());
+ assert.strictEqual(result, "2012-02-15 01:07:13");
+
+ result = py.eval(
+ 'datetime.datetime.combine(datetime.date(2012, 2, 15),' +
+ ' datetime.time())' +
+ ' .strftime("%Y-%m-%d %H:%M:%S")',
+ pyUtils.context());
+ assert.strictEqual(result, '2012-02-15 00:00:00');
+ });
+
+ QUnit.test('datetime.replace', function (assert) {
+ assert.expect(1);
+
+ var result = py.eval(
+ 'datetime.datetime(2012, 2, 15, 1, 7, 13)' +
+ ' .replace(hour=0, minute=0, second=0)' +
+ ' .strftime("%Y-%m-%d %H:%M:%S")',
+ pyUtils.context());
+ assert.strictEqual(result, "2012-02-15 00:00:00");
+ });
+
+ QUnit.test('conditional expressions', function (assert) {
+ assert.expect(2);
+ assert.strictEqual(
+ py.eval('1 if a else 2', {a: true}),
+ 1
+ );
+ assert.strictEqual(
+ py.eval('1 if a else 2', {a: false}),
+ 2
+ );
+ });
+
+ QUnit.module('py_utils (eval domain contexts)', {
+ beforeEach: function() {
+ this.user_context = {
+ uid: 1,
+ lang: 'en_US',
+ tz: false,
+ };
+ },
+ });
+
+
+ QUnit.test('empty, basic', function (assert) {
+ assert.expect(3);
+
+ var result = pyUtils.eval_domains_and_contexts({
+ contexts: [this.user_context],
+ domains: [],
+ });
+
+ // default values for new db
+ assert.deepEqual(result.context, {
+ lang: 'en_US',
+ tz: false,
+ uid: 1
+ });
+ assert.deepEqual(result.domain, []);
+ assert.deepEqual(result.group_by, []);
+ });
+
+
+ QUnit.test('context_merge_00', function (assert) {
+ assert.expect(1);
+
+ var ctx = [
+ {
+ "__contexts": [
+ { "lang": "en_US", "tz": false, "uid": 1 },
+ {
+ "active_id": 8,
+ "active_ids": [ 8 ],
+ "active_model": "sale.order",
+ "bin_raw": true,
+ "default_composition_mode": "comment",
+ "default_model": "sale.order",
+ "default_res_id": 8,
+ "default_template_id": 18,
+ "default_use_template": true,
+ "edi_web_url_view": "faaaake",
+ "lang": "en_US",
+ "mark_so_as_sent": null,
+ "show_address": null,
+ "tz": false,
+ "uid": null
+ },
+ {}
+ ],
+ "__eval_context": null,
+ "__ref": "compound_context"
+ },
+ { "active_id": 9, "active_ids": [ 9 ], "active_model": "mail.compose.message" }
+ ];
+ var result = pyUtils.eval_domains_and_contexts({
+ contexts: ctx,
+ domins: [],
+ });
+
+ assert.deepEqual(result.context, {
+ active_id: 9,
+ active_ids: [9],
+ active_model: 'mail.compose.message',
+ bin_raw: true,
+ default_composition_mode: 'comment',
+ default_model: 'sale.order',
+ default_res_id: 8,
+ default_template_id: 18,
+ default_use_template: true,
+ edi_web_url_view: "faaaake",
+ lang: 'en_US',
+ mark_so_as_sent: null,
+ show_address: null,
+ tz: false,
+ uid: null
+ });
+
+ });
+
+ QUnit.test('context_merge_01', function (assert) {
+ assert.expect(1);
+
+ var ctx = [{
+ "__contexts": [
+ {
+ "lang": "en_US",
+ "tz": false,
+ "uid": 1
+ },
+ {
+ "default_attachment_ids": [],
+ "default_body": "",
+ "default_model": "res.users",
+ "default_parent_id": false,
+ "default_res_id": 1
+ },
+ {}
+ ],
+ "__eval_context": null,
+ "__ref": "compound_context"
+ }];
+ var result = pyUtils.eval_domains_and_contexts({
+ contexts: ctx,
+ domains: [],
+ });
+
+ assert.deepEqual(result.context, {
+ "default_attachment_ids": [],
+ "default_body": "",
+ "default_model": "res.users",
+ "default_parent_id": false,
+ "default_res_id": 1,
+ "lang": "en_US",
+ "tz": false,
+ "uid": 1
+ });
+ });
+
+ QUnit.test('domain with time', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval_domains_and_contexts({
+ domains: [
+ [['type', '=', 'contract']],
+ ["|", ["state", "in", ["open", "draft"]], [["type", "=", "contract"], ["state", "=", "pending"]]],
+ "['|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)]",
+ [['user_id', '=', 1]]
+ ],
+ contexts: [],
+ });
+
+ var d = new Date();
+ var today = _.str.sprintf("%04d-%02d-%02d",
+ d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
+ assert.deepEqual(result.domain, [
+ ["type", "=", "contract"],
+ "|", ["state", "in", ["open", "draft"]],
+ [["type", "=", "contract"],
+ ["state", "=", "pending"]],
+ "|",
+ "&", ["date", "!=", false],
+ ["date", "<=", today],
+ ["is_overdue_quantity", "=", true],
+ ["user_id", "=", 1]
+ ]);
+ });
+
+ QUnit.test('conditional context', function (assert) {
+ assert.expect(2);
+
+ var d = {
+ __ref: 'domain',
+ __debug: "[('company_id', '=', context.get('company_id',False))]"
+ };
+
+ var result1 = pyUtils.eval_domains_and_contexts({
+ domains: [d],
+ contexts: [],
+ });
+ assert.deepEqual(result1.domain, [['company_id', '=', false]]);
+
+ var result2 = pyUtils.eval_domains_and_contexts({
+ domains: [d],
+ contexts: [],
+ eval_context: {company_id: 42},
+ });
+ assert.deepEqual(result2.domain, [['company_id', '=', 42]]);
+ });
+
+ QUnit.test('substitution in context', function (assert) {
+ assert.expect(1);
+
+ // setup(session);
+ var c = "{'default_opportunity_id': active_id, 'default_duration': 1.0, 'lng': lang}";
+ var cc = new Context(c);
+ cc.set_eval_context({active_id: 42});
+ var result = pyUtils.eval_domains_and_contexts({
+ domains:[], contexts: [this.user_context, cc]
+ });
+
+ assert.deepEqual(result.context, {
+ lang: "en_US",
+ tz: false,
+ uid: 1,
+ default_opportunity_id: 42,
+ default_duration: 1.0,
+ lng: "en_US"
+ });
+ });
+
+ QUnit.test('date', function (assert) {
+ assert.expect(1);
+
+ var d = "[('state','!=','cancel'),('opening_date','>',context_today().strftime('%Y-%m-%d'))]";
+ var result = pyUtils.eval_domains_and_contexts({
+ domains: [d],
+ contexts: [],
+ });
+
+ var date = new Date();
+ var today = _.str.sprintf("%04d-%02d-%02d",
+ date.getFullYear(), date.getMonth() + 1, date.getDate());
+ assert.deepEqual(result.domain, [
+ ['state', '!=', 'cancel'],
+ ['opening_date', '>', today]
+ ]);
+ });
+
+ QUnit.test('delta', function (assert) {
+ assert.expect(1);
+
+ var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(context_today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]";
+ var result = pyUtils.eval_domains_and_contexts({
+ domains: [d],
+ contexts: [],
+ });
+ var date = new Date();
+ var today = _.str.sprintf("%04d-%02d-%02d",
+ date.getFullYear(), date.getMonth() + 1, date.getDate());
+ date.setDate(date.getDate() - 15);
+ var ago_15_d = _.str.sprintf("%04d-%02d-%02d",
+ date.getFullYear(), date.getMonth() + 1, date.getDate());
+ assert.deepEqual(result.domain, [
+ ['type', '=', 'in'],
+ ['day', '<=', today],
+ ['day', '>', ago_15_d]
+ ]);
+ });
+
+ QUnit.test('horror from the deep', function (assert) {
+ assert.expect(1);
+
+ var cs = [
+ {"__ref": "compound_context",
+ "__contexts": [
+ {"__ref": "context", "__debug": "{'k': 'foo,' + str(context.get('test_key', False))}"},
+ {"__ref": "compound_context",
+ "__contexts": [
+ {"lang": "en_US", "tz": false, "uid": 1},
+ {"lang": "en_US", "tz": false, "uid": 1,
+ "active_model": "sale.order", "default_type": "out",
+ "show_address": 1, "contact_display": "partner_address",
+ "active_ids": [9], "active_id": 9},
+ {}
+ ], "__eval_context": null },
+ {"active_id": 8, "active_ids": [8],
+ "active_model": "stock.picking.out"},
+ {"__ref": "context", "__debug": "{'default_ref': 'stock.picking.out,'+str(context.get('active_id', False))}", "__id": "54d6ad1d6c45"}
+ ], "__eval_context": null}
+ ];
+ var result = pyUtils.eval_domains_and_contexts({
+ domains: [],
+ contexts: cs,
+ });
+
+ assert.deepEqual(result.context, {
+ k: 'foo,False',
+ lang: 'en_US',
+ tz: false,
+ uid: 1,
+ active_model: 'stock.picking.out',
+ active_id: 8,
+ active_ids: [8],
+ default_type: 'out',
+ show_address: 1,
+ contact_display: 'partner_address',
+ default_ref: 'stock.picking.out,8'
+ });
+ });
+
+ QUnit.module('py_utils (contexts)');
+
+ QUnit.test('context_recursive', function (assert) {
+ assert.expect(3);
+
+ var context_to_eval = [{
+ __ref: 'context',
+ __debug: '{"foo": context.get("bar", "qux")}'
+ }];
+ assert.deepEqual(
+ pyUtils.eval('contexts', context_to_eval, {bar: "ok"}),
+ {foo: 'ok'});
+ assert.deepEqual(
+ pyUtils.eval('contexts', context_to_eval, {bar: false}),
+ {foo: false});
+ assert.deepEqual(
+ pyUtils.eval('contexts', context_to_eval),
+ {foo: 'qux'});
+ });
+
+ QUnit.test('context_sequences', function (assert) {
+ assert.expect(1);
+
+ // Context n should have base evaluation context + all of contexts
+ // 0..n-1 in its own evaluation context
+ var active_id = 4;
+ var result = pyUtils.eval('contexts', [
+ {
+ "__contexts": [
+ {
+ "department_id": false,
+ "lang": "en_US",
+ "project_id": false,
+ "section_id": false,
+ "tz": false,
+ "uid": 1
+ },
+ { "search_default_create_uid": 1 },
+ {}
+ ],
+ "__eval_context": null,
+ "__ref": "compound_context"
+ },
+ {
+ "active_id": active_id,
+ "active_ids": [ active_id ],
+ "active_model": "purchase.requisition"
+ },
+ {
+ "__debug": "{'record_id' : active_id}",
+ "__id": "63e8e9bff8a6",
+ "__ref": "context"
+ }
+ ]);
+
+ assert.deepEqual(result, {
+ department_id: false,
+ lang: 'en_US',
+ project_id: false,
+ section_id: false,
+ tz: false,
+ uid: 1,
+ search_default_create_uid: 1,
+ active_id: active_id,
+ active_ids: [active_id],
+ active_model: 'purchase.requisition',
+ record_id: active_id
+ });
+ });
+
+ QUnit.test('non-literal_eval_contexts', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('contexts', [{
+ "__ref": "compound_context",
+ "__contexts": [
+ {"__ref": "context", "__debug": "{'move_type':parent.move_type}",
+ "__id": "462b9dbed42f"}
+ ],
+ "__eval_context": {
+ "__ref": "compound_context",
+ "__contexts": [{
+ "__ref": "compound_context",
+ "__contexts": [
+ {"__ref": "context", "__debug": "{'move_type': move_type}",
+ "__id": "16a04ed5a194"}
+ ],
+ "__eval_context": {
+ "__ref": "compound_context",
+ "__contexts": [
+ {"lang": "en_US", "tz": false, "uid": 1,
+ "journal_type": "sale", "section_id": false,
+ "default_move_type": "out_invoice",
+ "move_type": "out_invoice", "department_id": false},
+ {"id": false, "journal_id": 10,
+ "number": false, "move_type": "out_invoice",
+ "currency_id": 1, "partner_id": 4,
+ "fiscal_position_id": false,
+ "invoice_date": false, "date": false,
+ "payment_term_id": false,
+ "reference": false, "account_id": 440,
+ "name": false, "invoice_line_ids": [],
+ "tax_line_ids": [], "amount_untaxed": 0,
+ "amount_tax": 0, "reconciled": false,
+ "amount_total": 0, "state": "draft",
+ "amount_residual": 0, "company_id": 1,
+ "date_due": false, "user_id": 1,
+ "partner_bank_id": false, "origin": false,
+ "move_id": false, "comment": false,
+ "payment_ids": [[6, false, []]],
+ "active_id": false, "active_ids": [],
+ "active_model": "account.move",
+ "parent": {}}
+ ], "__eval_context": null}
+ }, {
+ "id": false,
+ "product_id": 4,
+ "name": "[PC1] Basic PC",
+ "quantity": 1,
+ "uom_id": 1,
+ "price_unit": 100,
+ "account_id": 853,
+ "discount": 0,
+ "account_analytic_id": false,
+ "company_id": false,
+ "note": false,
+ "invoice_line_tax_ids": [[6, false, [1]]],
+ "active_id": false,
+ "active_ids": [],
+ "active_model": "account.move.line",
+ "parent": {
+ "id": false, "journal_id": 10, "number": false,
+ "move_type": "out_invoice", "currency_id": 1,
+ "partner_id": 4, "fiscal_position_id": false,
+ "invoice_date": false, "date": false,
+ "payment_term_id": false,
+ "reference": false, "account_id": 440, "name": false,
+ "tax_line_ids": [], "amount_untaxed": 0, "amount_tax": 0,
+ "reconciled": false, "amount_total": 0,
+ "state": "draft", "amount_residual": 0, "company_id": 1,
+ "date_due": false, "user_id": 1,
+ "partner_bank_id": false, "origin": false,
+ "move_id": false, "comment": false,
+ "payment_ids": [[6, false, []]]}
+ }],
+ "__eval_context": null
+ }
+ }]);
+
+ assert.deepEqual(result, {move_type: 'out_invoice'});
+ });
+
+ QUnit.test('return-input-value', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('contexts', [{
+ __ref: 'compound_context',
+ __contexts: ["{'line_id': line_id , 'journal_id': journal_id }"],
+ __eval_context: {
+ __ref: 'compound_context',
+ __contexts: [{
+ __ref: 'compound_context',
+ __contexts: [
+ {lang: 'en_US', tz: 'Europe/Paris', uid: 1},
+ {lang: 'en_US', tz: 'Europe/Paris', uid: 1},
+ {}
+ ],
+ __eval_context: null,
+ }, {
+ active_id: false,
+ active_ids: [],
+ active_model: 'account.move',
+ amount: 0,
+ company_id: 1,
+ id: false,
+ journal_id: 14,
+ line_id: [
+ [0, false, {
+ account_id: 55,
+ amount_currency: 0,
+ analytic_account_id: false,
+ credit: 0,
+ currency_id: false,
+ date_maturity: false,
+ debit: 0,
+ name: "dscsd",
+ partner_id: false,
+ tax_line_id: false,
+ }]
+ ],
+ name: '/',
+ narration: false,
+ parent: {},
+ partner_id: false,
+ date: '2011-01-1',
+ ref: false,
+ state: 'draft',
+ to_check: false,
+ }],
+ __eval_context: null,
+ },
+ }]);
+ assert.deepEqual(result, {
+ journal_id: 14,
+ line_id: [[0, false, {
+ account_id: 55,
+ amount_currency: 0,
+ analytic_account_id: false,
+ credit: 0,
+ currency_id: false,
+ date_maturity: false,
+ debit: 0,
+ name: "dscsd",
+ partner_id: false,
+ tax_line_id: false,
+ }]],
+ });
+ });
+
+ QUnit.module('py_utils (domains)');
+
+ QUnit.test('current_date', function (assert) {
+ assert.expect(1);
+
+ var current_date = time.date_to_str(new Date());
+ var result = pyUtils.eval('domains',
+ [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
+ pyUtils.context());
+ assert.deepEqual(result, [
+ ['name', '>=', current_date],
+ ['name', '<=', current_date]
+ ]);
+ });
+
+ QUnit.test('context_freevar', function (assert) {
+ assert.expect(3);
+
+ var domains_to_eval = [{
+ __ref: 'domain',
+ __debug: '[("foo", "=", context.get("bar", "qux"))]'
+ }, [['bar', '>=', 42]]];
+ assert.deepEqual(
+ pyUtils.eval('domains', domains_to_eval, {bar: "ok"}),
+ [['foo', '=', 'ok'], ['bar', '>=', 42]]);
+ assert.deepEqual(
+ pyUtils.eval('domains', domains_to_eval, {bar: false}),
+ [['foo', '=', false], ['bar', '>=', 42]]);
+ assert.deepEqual(
+ pyUtils.eval('domains', domains_to_eval),
+ [['foo', '=', 'qux'], ['bar', '>=', 42]]);
+ });
+
+ QUnit.module('py_utils (groupbys)');
+
+ QUnit.test('groupbys_00', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ {group_by: ['bar', 'qux']},
+ {group_by: null},
+ {group_by: 'grault'}
+ ]);
+ assert.deepEqual(result, ['foo', 'bar', 'qux', 'grault']);
+ });
+
+ QUnit.test('groupbys_01', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ { __ref: 'context', __debug: '{"group_by": "bar"}' },
+ {group_by: 'grault'}
+ ]);
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+ QUnit.test('groupbys_02', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ {
+ __ref: 'compound_context',
+ __contexts: [ {group_by: 'bar'} ],
+ __eval_context: null
+ },
+ {group_by: 'grault'}
+ ]);
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+ QUnit.test('groupbys_03', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ {
+ __ref: 'compound_context',
+ __contexts: [
+ { __ref: 'context', __debug: '{"group_by": value}' }
+ ],
+ __eval_context: { value: 'bar' }
+ },
+ {group_by: 'grault'}
+ ]);
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+ QUnit.test('groupbys_04', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ {
+ __ref: 'compound_context',
+ __contexts: [
+ { __ref: 'context', __debug: '{"group_by": value}' }
+ ],
+ __eval_context: { value: 'bar' }
+ },
+ {group_by: 'grault'}
+ ], { value: 'bar' });
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+ QUnit.test('groupbys_05', function (assert) {
+ assert.expect(1);
+
+ var result = pyUtils.eval('groupbys', [
+ {group_by: 'foo'},
+ { __ref: 'context', __debug: '{"group_by": value}' },
+ {group_by: 'grault'}
+ ], { value: 'bar' });
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+ QUnit.module('pyutils (_formatAST)');
+
+ QUnit.test("basic values", function (assert) {
+ assert.expect(6);
+
+ assert.checkAST("1", "integer value");
+ assert.checkAST("1.4", "float value");
+ assert.checkAST("-12", "negative integer value");
+ assert.checkAST("True", "boolean");
+ assert.checkAST(`"some string"`, "a string");
+ assert.checkAST("None", "None");
+ });
+
+ QUnit.test("dictionary", function (assert) {
+ assert.expect(3);
+
+ assert.checkAST("{}", "empty dictionary");
+ assert.checkAST(`{"a": 1}`, "dictionary with a single key");
+ assert.checkAST(`d["a"]`, "get a value in a dictionary");
+ });
+
+ QUnit.test("list", function (assert) {
+ assert.expect(2);
+
+ assert.checkAST("[]", "empty list");
+ assert.checkAST("[1]", "list with one value");
+ });
+
+ QUnit.test("tuple", function (assert) {
+ assert.expect(2);
+
+ assert.checkAST("()", "empty tuple");
+ assert.checkAST("(1, 2)", "basic tuple");
+ });
+
+ QUnit.test("simple arithmetic", function (assert) {
+ assert.expect(15);
+
+ assert.checkAST("1 + 2", "addition");
+ assert.checkAST("+(1 + 2)", "other addition, prefix");
+ assert.checkAST("1 - 2", "substraction");
+ assert.checkAST("-1 - 2", "other substraction");
+ assert.checkAST("-(1 + 2)", "other substraction");
+ assert.checkAST("1 + 2 + 3", "addition of 3 integers");
+ assert.checkAST("a + b", "addition of two variables");
+ assert.checkAST("42 % 5", "modulo operator");
+ assert.checkAST("a * 10", "multiplication");
+ assert.checkAST("a ** 10", "**");
+ assert.checkAST("~10", "bitwise not");
+ assert.checkAST("~(10 + 3)", "bitwise not");
+ assert.checkAST("a * (1 + 2)", "multiplication and addition");
+ assert.checkAST("(a + b) * 43", "addition and multiplication");
+ assert.checkAST("a // 10", "integer division");
+ });
+
+ QUnit.test("boolean operators", function (assert) {
+ assert.expect(6);
+
+ assert.checkAST("True and False", "boolean operator");
+ assert.checkAST("True or False", "boolean operator or");
+ assert.checkAST("(True or False) and False", "boolean operators and and or");
+ assert.checkAST("not False", "not prefix");
+ assert.checkAST("not foo", "not prefix with variable");
+ assert.checkAST("not a in b", "not prefix with expression");
+ });
+
+ QUnit.test("conditional expression", function (assert) {
+ assert.expect(2);
+
+ assert.checkAST("1 if a else 2");
+ assert.checkAST("[] if a else 2");
+ });
+
+ QUnit.test("other operators", function (assert) {
+ assert.expect(7);
+
+ assert.checkAST("x == y", "== operator");
+ assert.checkAST("x != y", "!= operator");
+ assert.checkAST("x < y", "< operator");
+ assert.checkAST("x is y", "is operator");
+ assert.checkAST("x is not y", "is and not operator");
+ assert.checkAST("x in y", "in operator");
+ assert.checkAST("x not in y", "not in operator");
+ });
+
+ QUnit.test("equality", function (assert) {
+ assert.expect(1);
+ assert.checkAST("a == b", "simple equality");
+ });
+
+ QUnit.test("strftime", function (assert) {
+ assert.expect(3);
+ assert.checkAST(`time.strftime("%Y")`, "strftime with year");
+ assert.checkAST(`time.strftime("%Y") + "-01-30"`, "strftime with year");
+ assert.checkAST(`time.strftime("%Y-%m-%d %H:%M:%S")`, "strftime with year");
+ });
+
+ QUnit.test("context_today", function (assert) {
+ assert.expect(1);
+ assert.checkAST(`context_today().strftime("%Y-%m-%d")`, "context today call");
+ });
+
+
+ QUnit.test("function call", function (assert) {
+ assert.expect(5);
+ assert.checkAST("td()", "simple call");
+ assert.checkAST("td(a, b, c)", "simple call with args");
+ assert.checkAST('td(days = 1)', "simple call with kwargs");
+ assert.checkAST('f(1, 2, days = 1)', "mixing args and kwargs");
+ assert.checkAST('str(td(2))', "function call in function call");
+ });
+
+ QUnit.test("various expressions", function (assert) {
+ assert.expect(3);
+ assert.checkAST('(a - b).days', "substraction and .days");
+ assert.checkAST('a + day == date(2002, 3, 3)');
+
+ var expr = `[("type", "=", "in"), ("day", "<=", time.strftime("%Y-%m-%d")), ("day", ">", (context_today() - datetime.timedelta(days = 15)).strftime("%Y-%m-%d"))]`;
+ assert.checkAST(expr);
+ });
+
+ QUnit.test('escaping support', function (assert) {
+ assert.expect(4);
+ assert.strictEqual(py.eval(r`"\x61"`), "a", "hex escapes");
+ assert.strictEqual(py.eval(r`"\\abc"`), r`\abc`, "escaped backslash");
+ assert.checkAST(r`"\\abc"`, "escaped backslash AST check");
+
+ const {_getPyJSAST, _formatAST} = pyUtils;
+ const a = r`'foo\\abc"\''`;
+ const b = _formatAST(_getPyJSAST(_formatAST(_getPyJSAST(a))));
+ // Our repr uses JSON.stringify which always uses double quotes,
+ // whereas Python's repr is single-quote-biased: strings are repr'd
+ // using single quote delimiters *unless* they contain single quotes and
+ // no double quotes, then they're delimited with double quotes.
+ assert.strictEqual(b, r`"foo\\abc\"'"`);
+ });
+
+ QUnit.module('pyutils (_normalizeDomain)');
+
+ QUnit.assert.checkNormalization = function (domain, normalizedDomain) {
+ normalizedDomain = normalizedDomain || domain;
+ var result = pyUtils.normalizeDomain(domain);
+ this.pushResult({
+ result: result === normalizedDomain,
+ actual: result,
+ expected: normalizedDomain
+ });
+ };
+
+
+ QUnit.test("return simple (normalized) domains", function (assert) {
+ assert.expect(3);
+
+ assert.checkNormalization("[]");
+ assert.checkNormalization(`[("a", "=", 1)]`);
+ assert.checkNormalization(`["!", ("a", "=", 1)]`);
+ });
+
+ QUnit.test("properly add the & in a non normalized domain", function (assert) {
+ assert.expect(1);
+ assert.checkNormalization(
+ `[("a", "=", 1), ("b", "=", 2)]`,
+ `["&", ("a", "=", 1), ("b", "=", 2)]`
+ );
+ });
+
+ QUnit.test("normalize domain with ! operator", function (assert) {
+ assert.expect(1);
+ assert.checkNormalization(
+ `["!", ("a", "=", 1), ("b", "=", 2)]`,
+ `["&", "!", ("a", "=", 1), ("b", "=", 2)]`
+ );
+ });
+
+ QUnit.module('pyutils (assembleDomains)');
+
+ QUnit.assert.checkAssemble = function (domains, operator, domain) {
+ domain = pyUtils.normalizeDomain(domain);
+ var result = pyUtils.assembleDomains(domains, operator);
+ this.pushResult({
+ result: result === domain,
+ actual: result,
+ expected: domain
+ });
+ };
+
+ QUnit.test("assemble domains", function (assert) {
+ assert.expect(7);
+
+ assert.checkAssemble([], '&', "[]");
+ assert.checkAssemble(["[('a', '=', 1)]"], null, "[('a', '=', 1)]");
+ assert.checkAssemble(
+ ["[('a', '=', '1'), ('b', '!=', 2)]"],
+ 'AND',
+ "['&',('a', '=', '1'), ('b', '!=', 2)]"
+ );
+ assert.checkAssemble(
+ ["[('a', '=', '1')]", "[]"],
+ 'AND',
+ "[('a', '=', '1')]"
+ );
+ assert.checkAssemble(
+ ["[('a', '=', '1')]", "[('b', '<=', 3)]"],
+ 'AND',
+ "['&',('a', '=', '1'),('b','<=', 3)]"
+ );
+ assert.checkAssemble(
+ ["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"],
+ 'OR',
+ "['|', '&',('a', '=', '1'),('c', 'in', [4, 5]),('b','<=', 3)]"
+ );
+ assert.checkAssemble(
+ ["[('user_id', '=', uid)]"],
+ null,
+ "[('user_id', '=', uid)]"
+ );
+ });
+});
+});
diff --git a/addons/web/static/tests/core/registry_tests.js b/addons/web/static/tests/core/registry_tests.js
new file mode 100644
index 00000000..eb3389a4
--- /dev/null
+++ b/addons/web/static/tests/core/registry_tests.js
@@ -0,0 +1,90 @@
+odoo.define('web.registry_tests', function (require) {
+"use strict";
+
+var Registry = require('web.Registry');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('Registry');
+
+ QUnit.test('key set', function (assert) {
+ assert.expect(1);
+
+ var registry = new Registry();
+ var foo = {};
+
+ registry
+ .add('foo', foo);
+
+ assert.strictEqual(registry.get('foo'), foo);
+ });
+
+ QUnit.test('get initial keys', function (assert) {
+ assert.expect(1);
+
+ var registry = new Registry({ a: 1, });
+ assert.deepEqual(
+ registry.keys(),
+ ['a'],
+ "keys on prototype should be returned"
+ );
+ });
+
+ QUnit.test('get initial entries', function (assert) {
+ assert.expect(1);
+
+ var registry = new Registry({ a: 1, });
+ assert.deepEqual(
+ registry.entries(),
+ { a: 1, },
+ "entries on prototype should be returned"
+ );
+ });
+
+ QUnit.test('multiget', function (assert) {
+ assert.expect(1);
+
+ var foo = {};
+ var bar = {};
+ var registry = new Registry({
+ foo: foo,
+ bar: bar,
+ });
+ assert.strictEqual(
+ registry.getAny(['qux', 'grault', 'bar', 'foo']),
+ bar,
+ "Registry getAny should find first defined key");
+ });
+
+ QUnit.test('keys and values are properly ordered', function (assert) {
+ assert.expect(2);
+
+ var registry = new Registry();
+
+ registry
+ .add('fred', 'foo', 3)
+ .add('george', 'bar', 2)
+ .add('ronald', 'qux', 4);
+
+ assert.deepEqual(registry.keys(), ['george', 'fred', 'ronald']);
+ assert.deepEqual(registry.values(), ['bar', 'foo', 'qux']);
+ });
+
+ QUnit.test("predicate prevents invalid values", function (assert) {
+ assert.expect(5);
+
+ const predicate = value => typeof value === "number";
+ const registry = new Registry(null, predicate);
+ registry.onAdd((key) => assert.step(key));
+
+ assert.ok(registry.add("age", 23));
+ assert.throws(
+ () => registry.add("name", "Fred"),
+ new Error(`Value of key "name" does not pass the addition predicate.`)
+ );
+ assert.deepEqual(registry.entries(), { age: 23 });
+ assert.verifySteps(["age"]);
+ });
+});
+
+});
diff --git a/addons/web/static/tests/core/rpc_tests.js b/addons/web/static/tests/core/rpc_tests.js
new file mode 100644
index 00000000..06763e38
--- /dev/null
+++ b/addons/web/static/tests/core/rpc_tests.js
@@ -0,0 +1,316 @@
+odoo.define('web.rpc_tests', function (require) {
+"use strict";
+
+var rpc = require('web.rpc');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('RPC Builder');
+
+ QUnit.test('basic rpc (route)', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ route: '/my/route',
+ });
+ assert.strictEqual(query.route, '/my/route', "should have the proper route");
+ });
+
+ QUnit.test('rpc on route with parameters', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ route: '/my/route',
+ params: {hey: 'there', model: 'test'},
+ });
+
+ assert.deepEqual(query.params, {hey: 'there', model: 'test'},
+ "should transfer the proper parameters");
+ });
+
+ QUnit.test('basic rpc, with no context', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'test',
+ kwargs: {},
+ });
+ assert.notOk(query.params.kwargs.context,
+ "does not automatically add a context");
+ });
+
+ QUnit.test('basic rpc, with context', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'test',
+ context: {a: 1},
+ });
+
+ assert.deepEqual(query.params.kwargs.context, {a: 1},
+ "properly transfer the context");
+ });
+
+ QUnit.test('basic rpc, with context, part 2', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'test',
+ kwargs: {context: {a: 1}},
+ });
+
+ assert.deepEqual(query.params.kwargs.context, {a: 1},
+ "properly transfer the context");
+
+ });
+
+ QUnit.test('basic rpc (method of model)', function (assert) {
+ assert.expect(3);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'test',
+ kwargs: {context: {a: 1}},
+ });
+
+ assert.strictEqual(query.route, '/web/dataset/call_kw/partner/test',
+ "should call the proper route");
+ assert.strictEqual(query.params.model, 'partner',
+ "should correctly specify the model");
+ assert.strictEqual(query.params.method, 'test',
+ "should correctly specify the method");
+ });
+
+ QUnit.test('rpc with args and kwargs', function (assert) {
+ assert.expect(4);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'test',
+ args: ['arg1', 2],
+ kwargs: {k: 78},
+ });
+
+ assert.strictEqual(query.route, '/web/dataset/call_kw/partner/test',
+ "should call the proper route");
+ assert.strictEqual(query.params.args[0], 'arg1',
+ "should call with correct args");
+ assert.strictEqual(query.params.args[1], 2,
+ "should call with correct args");
+ assert.strictEqual(query.params.kwargs.k, 78,
+ "should call with correct kargs");
+ });
+
+ QUnit.test('search_read controller', function (assert) {
+ assert.expect(1);
+ var query = rpc.buildQuery({
+ route: '/web/dataset/search_read',
+ model: 'partner',
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ limit: 32,
+ offset: 2,
+ orderBy: [{name: 'yop', asc: true}, {name: 'aa', asc: false}],
+ });
+ assert.deepEqual(query.params, {
+ context: {},
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ limit: 32,
+ offset: 2,
+ model: 'partner',
+ sort: 'yop ASC, aa DESC',
+ }, "should have correct args");
+ });
+
+ QUnit.test('search_read method', function (assert) {
+ assert.expect(1);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'search_read',
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ limit: 32,
+ offset: 2,
+ orderBy: [{name: 'yop', asc: true}, {name: 'aa', asc: false}],
+ });
+ assert.deepEqual(query.params, {
+ args: [],
+ kwargs: {
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ offset: 2,
+ limit: 32,
+ order: 'yop ASC, aa DESC'
+ },
+ method: 'search_read',
+ model: 'partner'
+ }, "should have correct kwargs");
+ });
+
+ QUnit.test('search_read with args', function (assert) {
+ assert.expect(1);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'search_read',
+ args: [
+ ['a', '=', 1],
+ ['name'],
+ 2,
+ 32,
+ 'yop ASC, aa DESC',
+ ]
+ });
+ assert.deepEqual(query.params, {
+ args: [['a', '=', 1], ['name'], 2, 32, 'yop ASC, aa DESC'],
+ kwargs: {},
+ method: 'search_read',
+ model: 'partner'
+ }, "should have correct args");
+ });
+
+ QUnit.test('read_group', function (assert) {
+ assert.expect(2);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ groupBy: ['product_id'],
+ context: {abc: 'def'},
+ lazy: true,
+ });
+
+ assert.deepEqual(query.params, {
+ args: [],
+ kwargs: {
+ context: {abc: 'def'},
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ groupby: ['product_id'],
+ lazy: true,
+ },
+ method: 'read_group',
+ model: 'partner',
+ }, "should have correct args");
+ assert.equal(query.route, '/web/dataset/call_kw/partner/read_group',
+ "should call correct route");
+ });
+
+ QUnit.test('read_group with kwargs', function (assert) {
+ assert.expect(1);
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ groupBy: ['product_id'],
+ lazy: false,
+ kwargs: {context: {abc: 'def'}}
+ });
+
+ assert.deepEqual(query.params, {
+ args: [],
+ kwargs: {
+ context: {abc: 'def'},
+ domain: ['a', '=', 1],
+ fields: ['name'],
+ groupby: ['product_id'],
+ lazy: false,
+ },
+ method: 'read_group',
+ model: 'partner',
+ }, "should have correct args");
+ });
+
+ QUnit.test('read_group with no domain, nor fields', function (assert) {
+ assert.expect(7);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, [], "should have [] as default domain");
+ assert.deepEqual(query.params.kwargs.fields, [], "should have false as default fields");
+ assert.deepEqual(query.params.kwargs.groupby, [], "should have false as default groupby");
+ assert.deepEqual(query.params.kwargs.offset, undefined, "should not enforce a default value for offst");
+ assert.deepEqual(query.params.kwargs.limit, undefined, "should not enforce a default value for limit");
+ assert.deepEqual(query.params.kwargs.orderby, undefined, "should not enforce a default value for orderby");
+ assert.deepEqual(query.params.kwargs.lazy, undefined, "should not enforce a default value for lazy");
+ });
+
+ QUnit.test('read_group with args and kwargs', function (assert) {
+ assert.expect(9);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ kwargs: {
+ domain: ['name', '=', 'saucisse'],
+ fields: ['category_id'],
+ groupby: ['country_id'],
+ },
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, ['name', '=', 'saucisse'], "should have ['name', '=', 'saucisse'] category_id as default domain");
+ assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
+ assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ args: [['name', '=', 'saucisse']],
+ kwargs: {
+ fields: ['category_id'],
+ groupby: ['country_id'],
+ },
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
+ assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
+
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'read_group',
+ args: [['name', '=', 'saucisse'], ['category_id'], ['country_id']],
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
+ assert.deepEqual(query.params.kwargs.groupby, undefined, "should not enforce a default value for groupby");
+ });
+
+ QUnit.test('search_read with no domain, nor fields', function (assert) {
+ assert.expect(5);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ method: 'search_read',
+ });
+
+ assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
+ assert.deepEqual(query.params.kwargs.offset, undefined, "should not enforce a default value for offset");
+ assert.deepEqual(query.params.kwargs.limit, undefined, "should not enforce a default value for limit");
+ assert.deepEqual(query.params.kwargs.order, undefined, "should not enforce a default value for orderby");
+ });
+
+ QUnit.test('search_read controller with no domain, nor fields', function (assert) {
+ assert.expect(5);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ route: '/web/dataset/search_read',
+ });
+
+ assert.deepEqual(query.params.domain, undefined, "should not enforce a default value for domain");
+ assert.deepEqual(query.params.fields, undefined, "should not enforce a default value for fields");
+ assert.deepEqual(query.params.offset, undefined, "should not enforce a default value for groupby");
+ assert.deepEqual(query.params.limit, undefined, "should not enforce a default value for limit");
+ assert.deepEqual(query.params.sort, undefined, "should not enforce a default value for order");
+ });
+});
+
+});
diff --git a/addons/web/static/tests/core/time_tests.js b/addons/web/static/tests/core/time_tests.js
new file mode 100644
index 00000000..54b8ccae
--- /dev/null
+++ b/addons/web/static/tests/core/time_tests.js
@@ -0,0 +1,165 @@
+odoo.define('web.time_tests', function (require) {
+"use strict";
+
+const core = require('web.core');
+var time = require('web.time');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('Time utils');
+
+ QUnit.test('Parse server datetime', function (assert) {
+ assert.expect(4);
+
+ var date = time.str_to_datetime("2009-05-04 12:34:23");
+ assert.deepEqual(
+ [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
+ date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
+ [2009, 5 - 1, 4, 12, 34, 23]);
+ assert.deepEqual(
+ [date.getFullYear(), date.getMonth(), date.getDate(),
+ date.getHours(), date.getMinutes(), date.getSeconds()],
+ [2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]);
+
+ var date2 = time.str_to_datetime('2011-12-10 00:00:00');
+ assert.deepEqual(
+ [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(),
+ date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()],
+ [2011, 12 - 1, 10, 0, 0, 0]);
+
+ var date3 = time.str_to_datetime("2009-05-04 12:34:23.84565");
+ assert.deepEqual(
+ [date3.getUTCFullYear(), date3.getUTCMonth(), date3.getUTCDate(),
+ date3.getUTCHours(), date3.getUTCMinutes(), date3.getUTCSeconds(), date3.getUTCMilliseconds()],
+ [2009, 5 - 1, 4, 12, 34, 23, 845]);
+ });
+
+ QUnit.test('Parse server datetime on 31', function (assert) {
+ assert.expect(1);
+
+ var wDate = window.Date;
+
+ try {
+ window.Date = function (v) {
+ if (_.isUndefined(v)) {
+ v = '2013-10-31 12:34:56';
+ }
+ return new wDate(v);
+ };
+ var date = time.str_to_datetime('2013-11-11 02:45:21');
+
+ assert.deepEqual(
+ [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
+ date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()],
+ [2013, 11 - 1, 11, 2, 45, 21]);
+ }
+ finally {
+ window.Date = wDate;
+ }
+ });
+
+ QUnit.test('Parse server date', function (assert) {
+ assert.expect(1);
+
+ var date = time.str_to_date("2009-05-04");
+ assert.deepEqual(
+ [date.getFullYear(), date.getMonth(), date.getDate()],
+ [2009, 5 - 1, 4]);
+ });
+
+ QUnit.test('Parse server date on 31', function (assert) {
+ assert.expect(1);
+
+ var wDate = window.Date;
+
+ try {
+ window.Date = function (v) {
+ if (_.isUndefined(v)) {
+ v = '2013-10-31 12:34:56';
+ }
+ return new wDate(v);
+ };
+ var date = time.str_to_date('2013-11-21');
+
+ assert.deepEqual(
+ [date.getFullYear(), date.getMonth(), date.getDate()],
+ [2013, 11 - 1, 21]);
+ }
+ finally {
+ window.Date = wDate;
+ }
+ });
+
+ QUnit.test('Parse server time', function (assert) {
+ assert.expect(2);
+
+ var date = time.str_to_time("12:34:23");
+ assert.deepEqual(
+ [date.getHours(), date.getMinutes(), date.getSeconds()],
+ [12, 34, 23]);
+
+ date = time.str_to_time("12:34:23.5467");
+ assert.deepEqual(
+ [date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()],
+ [12, 34, 23, 546]);
+ });
+
+ QUnit.test('Format server datetime', function (assert) {
+ assert.expect(1);
+
+ var date = new Date();
+ date.setUTCFullYear(2009);
+ date.setUTCMonth(5 - 1);
+ date.setUTCDate(4);
+ date.setUTCHours(12);
+ date.setUTCMinutes(34);
+ date.setUTCSeconds(23);
+ assert.strictEqual(time.datetime_to_str(date), "2009-05-04 12:34:23");
+ });
+
+ QUnit.test('Format server date', function (assert) {
+ assert.expect(1);
+
+ var date = new Date();
+ date.setUTCFullYear(2009);
+ date.setUTCMonth(5 - 1);
+ date.setUTCDate(4);
+ date.setUTCHours(0);
+ date.setUTCMinutes(0);
+ date.setUTCSeconds(0);
+ assert.strictEqual(time.date_to_str(date), "2009-05-04");
+ });
+
+ QUnit.test('Format server time', function (assert) {
+ assert.expect(1);
+
+ var date = new Date();
+ date.setUTCFullYear(1970);
+ date.setUTCMonth(1 - 1);
+ date.setUTCDate(1);
+ date.setUTCHours(0);
+ date.setUTCMinutes(0);
+ date.setUTCSeconds(0);
+ date.setHours(12);
+ date.setMinutes(34);
+ date.setSeconds(23);
+ assert.strictEqual(time.time_to_str(date), "12:34:23");
+ });
+
+ QUnit.test("Get lang datetime format", (assert) => {
+ assert.expect(4);
+ const originalParameters = Object.assign({}, core._t.database.parameters);
+ Object.assign(core._t.database.parameters, {
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ });
+ assert.strictEqual(time.getLangDateFormat(), "MM/DD/YYYY");
+ assert.strictEqual(time.getLangDateFormatWoZero(), "M/D/YYYY");
+ assert.strictEqual(time.getLangTimeFormat(), "HH:mm:ss");
+ assert.strictEqual(time.getLangTimeFormatWoZero(), "H:m:s");
+ Object.assign(core._t.database.parameters, originalParameters);
+ });
+
+});
+
+}); \ No newline at end of file
diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js
new file mode 100644
index 00000000..88faab44
--- /dev/null
+++ b/addons/web/static/tests/core/util_tests.js
@@ -0,0 +1,339 @@
+odoo.define('web.util_tests', function (require) {
+"use strict";
+
+var utils = require('web.utils');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('utils');
+
+ QUnit.test('findWhere', function (assert) {
+ assert.expect(7);
+
+ const { findWhere } = utils;
+
+ const list = [
+ undefined,
+ { a: 1, b: 2 },
+ { a: 2, b: 2 },
+ { a: 1, b: 3 },
+ { a: 1, b: 4 },
+ { a: 2, b: 4 },
+ ];
+
+ assert.deepEqual(findWhere(list, { a: 1 }), { a: 1, b: 2 });
+ assert.deepEqual(findWhere(list, { a: 2 }), { a: 2, b: 2 });
+ assert.deepEqual(findWhere(list, { b: 4 }), { a: 1, b: 4 });
+ assert.deepEqual(findWhere(list, { b: 4, a: 2 }), { a: 2, b: 4 });
+ assert.ok(findWhere([], { a: 1 }) === undefined);
+ assert.ok(findWhere(list, { a: 1, b: 5 }) === undefined);
+ assert.ok(findWhere(list, { c: 1 }) === undefined);
+ });
+
+ QUnit.test('groupBy', function (assert) {
+ assert.expect(7);
+
+ const { groupBy } = utils;
+
+ // Invalid
+ assert.throws(
+ () => groupBy({}),
+ new TypeError(`list is not iterable`)
+ );
+ assert.throws(
+ () => groupBy([], true),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`)
+ );
+ assert.throws(
+ () => groupBy([], 3),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'number'`)
+ );
+ assert.throws(
+ () => groupBy([], {}),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'object'`)
+ );
+
+ // criterion = default
+ assert.deepEqual(
+ groupBy(["a", "b", 1, true]),
+ {
+ 1: [1],
+ a: ["a"],
+ b: ["b"],
+ true: [true],
+ }
+ );
+ // criterion = string
+ assert.deepEqual(
+ groupBy([{ x: "a" }, { x: "a" }, { x: "b" }], "x"),
+ {
+ a: [{ x: "a" }, { x: "a" }],
+ b: [{ x: "b" }],
+ }
+ );
+ // criterion = function
+ assert.deepEqual(
+ groupBy(["a", "b", 1, true], (x) => `el${x}`),
+ {
+ ela: ["a"],
+ elb: ["b"],
+ el1: [1],
+ eltrue: [true],
+ }
+ );
+ });
+
+ QUnit.test('intersperse', function (assert) {
+ assert.expect(27);
+
+ var intersperse = utils.intersperse;
+
+ assert.strictEqual(intersperse("", []), "");
+ assert.strictEqual(intersperse("0", []), "0");
+ assert.strictEqual(intersperse("012", []), "012");
+ assert.strictEqual(intersperse("1", []), "1");
+ assert.strictEqual(intersperse("12", []), "12");
+ assert.strictEqual(intersperse("123", []), "123");
+ assert.strictEqual(intersperse("1234", []), "1234");
+ assert.strictEqual(intersperse("123456789", []), "123456789");
+ assert.strictEqual(intersperse("&ab%#@1", []), "&ab%#@1");
+
+ assert.strictEqual(intersperse("0", []), "0");
+ assert.strictEqual(intersperse("0", [1]), "0");
+ assert.strictEqual(intersperse("0", [2]), "0");
+ assert.strictEqual(intersperse("0", [200]), "0");
+
+ assert.strictEqual(intersperse("12345678", [0], '.'), '12345678');
+ assert.strictEqual(intersperse("", [1], '.'), '');
+ assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8');
+ assert.strictEqual(intersperse("12345678", [1], '.'), '1234567.8');
+ assert.strictEqual(intersperse("12345678", [2], '.'), '123456.78');
+ assert.strictEqual(intersperse("12345678", [2, 1], '.'), '12345.6.78');
+ assert.strictEqual(intersperse("12345678", [2, 0], '.'), '12.34.56.78');
+ assert.strictEqual(intersperse("12345678", [-1, 2], '.'), '12345678');
+ assert.strictEqual(intersperse("12345678", [2, -1], '.'), '123456.78');
+ assert.strictEqual(intersperse("12345678", [2, 0, 1], '.'), '12.34.56.78');
+ assert.strictEqual(intersperse("12345678", [2, 0, 0], '.'), '12.34.56.78');
+ assert.strictEqual(intersperse("12345678", [2, 0, -1], '.'), '12.34.56.78');
+ assert.strictEqual(intersperse("12345678", [3,3,3,3], '.'), '12.345.678');
+ assert.strictEqual(intersperse("12345678", [3,0], '.'), '12.345.678');
+ });
+
+ QUnit.test('is_bin_size', function (assert) {
+ assert.expect(3);
+
+ var is_bin_size = utils.is_bin_size;
+
+ assert.strictEqual(is_bin_size('Cg=='), false);
+ assert.strictEqual(is_bin_size('2.5 Mb'), true);
+ // should also work for non-latin languages (e.g. russian)
+ assert.strictEqual(is_bin_size('64.2 Кб'), true);
+ });
+
+ QUnit.test('unaccent', function (assert) {
+ assert.expect(3);
+
+ var singleCharacters = utils.unaccent("ⱮɀꝾƶⱵȥ");
+ var doubledCharacters = utils.unaccent("DZDŽꝎꜩꝡƕ");
+ var caseSensetiveCharacters = utils.unaccent("ⱮɀꝾƶⱵȥ", true);
+
+ assert.strictEqual("mzgzhz", singleCharacters);
+ assert.strictEqual("dzdzootzvyhv", doubledCharacters);
+ assert.strictEqual("MzGzHz", caseSensetiveCharacters);
+ });
+
+ QUnit.test('human_number', function (assert) {
+ assert.expect(26);
+
+ var human_number = utils.human_number;
+
+ assert.strictEqual(human_number(1020, 2, 1), '1.02k');
+ assert.strictEqual(human_number(1020000, 2, 2), '1020k');
+ assert.strictEqual(human_number(10200000, 2, 2), '10.2M');
+ assert.strictEqual(human_number(1020, 2, 1), '1.02k');
+ assert.strictEqual(human_number(1002, 2, 1), '1k');
+ assert.strictEqual(human_number(101, 2, 1), '101');
+ assert.strictEqual(human_number(64.2, 2, 1), '64');
+ assert.strictEqual(human_number(1e+18), '1E');
+ assert.strictEqual(human_number(1e+21, 2, 1), '1e+21');
+ assert.strictEqual(human_number(1.0045e+22, 2, 1), '1e+22');
+ assert.strictEqual(human_number(1.0045e+22, 3, 1), '1.005e+22');
+ assert.strictEqual(human_number(1.012e+43, 2, 1), '1.01e+43');
+ assert.strictEqual(human_number(1.012e+43, 2, 2), '1.01e+43');
+
+ assert.strictEqual(human_number(-1020, 2, 1), '-1.02k');
+ assert.strictEqual(human_number(-1020000, 2, 2), '-1020k');
+ assert.strictEqual(human_number(-10200000, 2, 2), '-10.2M');
+ assert.strictEqual(human_number(-1020, 2, 1), '-1.02k');
+ assert.strictEqual(human_number(-1002, 2, 1), '-1k');
+ assert.strictEqual(human_number(-101, 2, 1), '-101');
+ assert.strictEqual(human_number(-64.2, 2, 1), '-64');
+ assert.strictEqual(human_number(-1e+18), '-1E');
+ assert.strictEqual(human_number(-1e+21, 2, 1), '-1e+21');
+ assert.strictEqual(human_number(-1.0045e+22, 2, 1), '-1e+22');
+ assert.strictEqual(human_number(-1.0045e+22, 3, 1), '-1.004e+22');
+ assert.strictEqual(human_number(-1.012e+43, 2, 1), '-1.01e+43');
+ assert.strictEqual(human_number(-1.012e+43, 2, 2), '-1.01e+43');
+ });
+
+ QUnit.test('patch a class', function(assert) {
+ assert.expect(4);
+
+ class Parent {
+ foo() {
+ return 'Parent foo';
+ }
+ }
+
+ class Child extends Parent {
+ bar() {
+ return 'Child bar';
+ }
+ }
+
+ const removePatch = utils.patch(Child, 'patch', {
+ foo() {
+ return this._super() + ' patch foo';
+ },
+ bar() {
+ return this._super() + ' patch bar';
+ }
+ })
+
+ const child = new Child();
+
+ assert.strictEqual(child.foo(), 'Parent foo patch foo')
+ assert.strictEqual(child.bar(), 'Child bar patch bar')
+
+ removePatch();
+
+ assert.strictEqual(child.foo(), 'Parent foo');
+ assert.strictEqual(child.bar(), 'Child bar');
+ })
+
+ QUnit.test('round_decimals', function (assert) {
+ assert.expect(21);
+
+ var round_di = utils.round_decimals;
+
+ assert.strictEqual(String(round_di(1.0, 0)), '1');
+ assert.strictEqual(String(round_di(1.0, 1)), '1');
+ assert.strictEqual(String(round_di(1.0, 2)), '1');
+ assert.strictEqual(String(round_di(1.0, 3)), '1');
+ assert.strictEqual(String(round_di(1.0, 4)), '1');
+ assert.strictEqual(String(round_di(1.0, 5)), '1');
+ assert.strictEqual(String(round_di(1.0, 6)), '1');
+ assert.strictEqual(String(round_di(1.0, 7)), '1');
+ assert.strictEqual(String(round_di(1.0, 8)), '1');
+ assert.strictEqual(String(round_di(0.5, 0)), '1');
+ assert.strictEqual(String(round_di(-0.5, 0)), '-1');
+ assert.strictEqual(String(round_di(2.6745, 3)), '2.6750000000000003');
+ assert.strictEqual(String(round_di(-2.6745, 3)), '-2.6750000000000003');
+ assert.strictEqual(String(round_di(2.6744, 3)), '2.674');
+ assert.strictEqual(String(round_di(-2.6744, 3)), '-2.674');
+ assert.strictEqual(String(round_di(0.0004, 3)), '0');
+ assert.strictEqual(String(round_di(-0.0004, 3)), '0');
+ assert.strictEqual(String(round_di(357.4555, 3)), '357.456');
+ assert.strictEqual(String(round_di(-357.4555, 3)), '-357.456');
+ assert.strictEqual(String(round_di(457.4554, 3)), '457.455');
+ assert.strictEqual(String(round_di(-457.4554, 3)), '-457.455');
+ });
+
+ QUnit.test('round_precision', function (assert) {
+ assert.expect(26);
+
+ var round_pr = utils.round_precision;
+
+ assert.strictEqual(String(round_pr(1.0, 1)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.1)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.01)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.001)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.0001)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.00001)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.000001)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.0000001)), '1');
+ assert.strictEqual(String(round_pr(1.0, 0.00000001)), '1');
+ assert.strictEqual(String(round_pr(0.5, 1)), '1');
+ assert.strictEqual(String(round_pr(-0.5, 1)), '-1');
+ assert.strictEqual(String(round_pr(2.6745, 0.001)), '2.6750000000000003');
+ assert.strictEqual(String(round_pr(-2.6745, 0.001)), '-2.6750000000000003');
+ assert.strictEqual(String(round_pr(2.6744, 0.001)), '2.674');
+ assert.strictEqual(String(round_pr(-2.6744, 0.001)), '-2.674');
+ assert.strictEqual(String(round_pr(0.0004, 0.001)), '0');
+ assert.strictEqual(String(round_pr(-0.0004, 0.001)), '0');
+ assert.strictEqual(String(round_pr(357.4555, 0.001)), '357.456');
+ assert.strictEqual(String(round_pr(-357.4555, 0.001)), '-357.456');
+ assert.strictEqual(String(round_pr(457.4554, 0.001)), '457.455');
+ assert.strictEqual(String(round_pr(-457.4554, 0.001)), '-457.455');
+ assert.strictEqual(String(round_pr(-457.4554, 0.05)), '-457.45000000000005');
+ assert.strictEqual(String(round_pr(457.444, 0.5)), '457.5');
+ assert.strictEqual(String(round_pr(457.3, 5)), '455');
+ assert.strictEqual(String(round_pr(457.5, 5)), '460');
+ assert.strictEqual(String(round_pr(457.1, 3)), '456');
+ });
+
+ QUnit.test('sortBy', function (assert) {
+ assert.expect(27);
+ const { sortBy } = utils;
+ const bools = [true, false, true];
+ const ints = [2, 1, 5];
+ const strs = ['b', 'a', 'z'];
+ const objbools = [{ x: true }, { x: false }, { x: true }];
+ const objints = [{ x: 2 }, { x: 1 }, { x: 5 }];
+ const objstrss = [{ x: 'b' }, { x: 'a' }, { x: 'z' }];
+
+ // Invalid
+ assert.throws(
+ () => sortBy({}),
+ new TypeError(`array.slice is not a function`)
+ );
+ assert.throws(
+ () => sortBy([Symbol('b'), Symbol('a')]),
+ new TypeError(`Cannot convert a Symbol value to a number`)
+ );
+ assert.throws(
+ () => sortBy(ints, true),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'boolean'`)
+ );
+ assert.throws(
+ () => sortBy(ints, 3),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'number'`)
+ );
+ assert.throws(
+ () => sortBy(ints, {}),
+ new Error(`Expected criterion of type 'string' or 'function' and got 'object'`)
+ );
+ // Do not sort in place
+ const toSort = [2, 3, 1];
+ sortBy(toSort);
+ assert.deepEqual(toSort, [2, 3, 1]);
+ // Sort (no criterion)
+ assert.deepEqual(sortBy([]), []);
+ assert.deepEqual(sortBy(ints), [1, 2, 5]);
+ assert.deepEqual(sortBy(bools), [false, true, true]);
+ assert.deepEqual(sortBy(strs), ['a', 'b', 'z']);
+ assert.deepEqual(sortBy(objbools), [{ x: true }, { x: false }, { x: true }]);
+ assert.deepEqual(sortBy(objints), [{ x: 2 }, { x: 1 }, { x: 5 }]);
+ assert.deepEqual(sortBy(objstrss), [{ x: 'b' }, { x: 'a' }, { x: 'z' }]);
+ // Sort by property
+ const prop = 'x';
+ assert.deepEqual(sortBy([], prop), []);
+ assert.deepEqual(sortBy(ints, prop), [2, 1, 5]);
+ assert.deepEqual(sortBy(bools, prop), [true, false, true]);
+ assert.deepEqual(sortBy(strs, prop), ['b', 'a', 'z']);
+ assert.deepEqual(sortBy(objbools, prop), [{ x: false }, { x: true }, { x: true }]);
+ assert.deepEqual(sortBy(objints, prop), [{ x: 1 }, { x: 2 }, { x: 5 }]);
+ assert.deepEqual(sortBy(objstrss, prop), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]);
+ // Sort by getter
+ const getter = obj => obj.x;
+ assert.deepEqual(sortBy([], getter), []);
+ assert.deepEqual(sortBy(ints, getter), [2, 1, 5]);
+ assert.deepEqual(sortBy(bools, getter), [true, false, true]);
+ assert.deepEqual(sortBy(strs, getter), ['b', 'a', 'z']);
+ assert.deepEqual(sortBy(objbools, getter), [{ x: false }, { x: true }, { x: true }]);
+ assert.deepEqual(sortBy(objints, getter), [{ x: 1 }, { x: 2 }, { x: 5 }]);
+ assert.deepEqual(sortBy(objstrss, getter), [{ x: 'a' }, { x: 'b' }, { x: 'z' }]);
+ });
+});
+
+});
diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js
new file mode 100644
index 00000000..dc704abe
--- /dev/null
+++ b/addons/web/static/tests/core/widget_tests.js
@@ -0,0 +1,530 @@
+odoo.define('web.widget_tests', function (require) {
+"use strict";
+
+var AjaxService = require('web.AjaxService');
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var QWeb = require('web.QWeb');
+var Widget = require('web.Widget');
+var testUtils = require('web.test_utils');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('Widget');
+
+ QUnit.test('proxy (String)', function (assert) {
+ assert.expect(1);
+
+ var W = Widget.extend({
+ exec: function () {
+ this.executed = true;
+ }
+ });
+ var w = new W();
+ var fn = w.proxy('exec');
+ fn();
+ assert.ok(w.executed, 'should execute the named method in the right context');
+ w.destroy();
+ });
+
+ QUnit.test('proxy (String)(*args)', function (assert) {
+ assert.expect(2);
+
+ var W = Widget.extend({
+ exec: function (arg) {
+ this.executed = arg;
+ }
+ });
+ var w = new W();
+ var fn = w.proxy('exec');
+ fn(42);
+ assert.ok(w.executed, "should execute the named method in the right context");
+ assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments");
+ w.destroy();
+ });
+
+ QUnit.test('proxy (String), include', function (assert) {
+ assert.expect(1);
+
+ // the proxy function should handle methods being changed on the class
+ // and should always proxy "by name", to the most recent one
+ var W = Widget.extend({
+ exec: function () {
+ this.executed = 1;
+ }
+ });
+ var w = new W();
+ var fn = w.proxy('exec');
+ W.include({
+ exec: function () { this.executed = 2; }
+ });
+
+ fn();
+ assert.strictEqual(w.executed, 2, "should be lazily resolved");
+ w.destroy();
+ });
+
+ QUnit.test('proxy (Function)', function (assert) {
+ assert.expect(1);
+
+ var w = new (Widget.extend({ }))();
+
+ var fn = w.proxy(function () { this.executed = true; });
+ fn();
+ assert.ok(w.executed, "should set the function's context (like Function#bind)");
+ w.destroy();
+ });
+
+ QUnit.test('proxy (Function)(*args)', function (assert) {
+ assert.expect(1);
+
+ var w = new (Widget.extend({ }))();
+
+ var fn = w.proxy(function (arg) { this.executed = arg; });
+ fn(42);
+ assert.strictEqual(w.executed, 42, "should be passed the proxy's arguments");
+ w.destroy();
+ });
+
+ QUnit.test('renderElement, no template, default', function (assert) {
+ assert.expect(7);
+
+ var widget = new (Widget.extend({ }))();
+
+ assert.strictEqual(widget.$el, undefined, "should not have a root element");
+
+ widget.renderElement();
+
+ assert.ok(widget.$el, "should have generated a root element");
+ assert.strictEqual(widget.$el, widget.$el, "should provide $el alias");
+ assert.ok(widget.$el.is(widget.el), "should provide raw DOM alias");
+
+ assert.strictEqual(widget.el.nodeName, 'DIV', "should have generated the default element");
+ assert.strictEqual(widget.el.attributes.length, 0, "should not have generated any attribute");
+ assert.ok(_.isEmpty(widget.$el.html(), "should not have generated any content"));
+ widget.destroy();
+ });
+
+ QUnit.test('no template, custom tag', function (assert) {
+ assert.expect(1);
+
+
+ var widget = new (Widget.extend({
+ tagName: 'ul'
+ }))();
+ widget.renderElement();
+
+ assert.strictEqual(widget.el.nodeName, 'UL', "should have generated the custom element tag");
+ widget.destroy();
+ });
+
+ QUnit.test('no template, @id', function (assert) {
+ assert.expect(3);
+
+ var widget = new (Widget.extend({
+ id: 'foo'
+ }))();
+ widget.renderElement();
+
+ assert.strictEqual(widget.el.attributes.length, 1, "should have one attribute");
+ assert.hasAttrValue(widget.$el, 'id', 'foo', "should have generated the id attribute");
+ assert.strictEqual(widget.el.id, 'foo', "should also be available via property");
+ widget.destroy();
+ });
+
+ QUnit.test('no template, @className', function (assert) {
+ assert.expect(2);
+
+ var widget = new (Widget.extend({
+ className: 'oe_some_class'
+ }))();
+ widget.renderElement();
+
+ assert.strictEqual(widget.el.className, 'oe_some_class', "should have the right property");
+ assert.hasAttrValue(widget.$el, 'class', 'oe_some_class', "should have the right attribute");
+ widget.destroy();
+ });
+
+ QUnit.test('no template, bunch of attributes', function (assert) {
+ assert.expect(9);
+
+ var widget = new (Widget.extend({
+ attributes: {
+ 'id': 'some_id',
+ 'class': 'some_class',
+ 'data-foo': 'data attribute',
+ 'clark': 'gable',
+ 'spoiler': // don't read the next line if you care about Harry Potter...
+ 'snape kills dumbledore'
+ }
+ }))();
+ widget.renderElement();
+
+ assert.strictEqual(widget.el.attributes.length, 5, "should have all the specified attributes");
+
+ assert.strictEqual(widget.el.id, 'some_id');
+ assert.hasAttrValue(widget.$el, 'id', 'some_id');
+
+ assert.strictEqual(widget.el.className, 'some_class');
+ assert.hasAttrValue(widget.$el, 'class', 'some_class');
+
+ assert.hasAttrValue(widget.$el, 'data-foo', 'data attribute');
+ assert.strictEqual(widget.$el.data('foo'), 'data attribute');
+
+ assert.hasAttrValue(widget.$el, 'clark', 'gable');
+ assert.hasAttrValue(widget.$el, 'spoiler', 'snape kills dumbledore');
+ widget.destroy();
+ });
+
+ QUnit.test('template', function (assert) {
+ assert.expect(3);
+
+ core.qweb.add_template(
+ '<no>' +
+ '<t t-name="test.widget.template">' +
+ '<ol>' +
+ '<li t-foreach="5" t-as="counter" ' +
+ 't-attf-class="class-#{counter}">' +
+ '<input/>' +
+ '<t t-esc="counter"/>' +
+ '</li>' +
+ '</ol>' +
+ '</t>' +
+ '</no>'
+ );
+
+ var widget = new (Widget.extend({
+ template: 'test.widget.template'
+ }))();
+ widget.renderElement();
+
+ assert.strictEqual(widget.el.nodeName, 'OL');
+ assert.strictEqual(widget.$el.children().length, 5);
+ assert.strictEqual(widget.el.textContent, '01234');
+ widget.destroy();
+ });
+
+ QUnit.test('repeated', async function (assert) {
+ assert.expect(4);
+ var $fix = $( "#qunit-fixture");
+
+ core.qweb.add_template(
+ '<no>' +
+ '<t t-name="test.widget.template">' +
+ '<p><t t-esc="widget.value"/></p>' +
+ '</t>' +
+ '</no>'
+ );
+ var widget = new (Widget.extend({
+ template: 'test.widget.template'
+ }))();
+ widget.value = 42;
+
+ await widget.appendTo($fix)
+ .then(function () {
+ assert.strictEqual($fix.find('p').text(), '42', "DOM fixture should contain initial value");
+ assert.strictEqual(widget.$el.text(), '42', "should set initial value");
+ widget.value = 36;
+ widget.renderElement();
+ assert.strictEqual($fix.find('p').text(), '36', "DOM fixture should use new value");
+ assert.strictEqual(widget.$el.text(), '36', "should set new value");
+ });
+ widget.destroy();
+ });
+
+
+ QUnit.module('Widgets, with QWeb', {
+ beforeEach: function() {
+ this.oldQWeb = core.qweb;
+ core.qweb = new QWeb();
+ core.qweb.add_template(
+ '<no>' +
+ '<t t-name="test.widget.template">' +
+ '<ol>' +
+ '<li t-foreach="5" t-as="counter" ' +
+ 't-attf-class="class-#{counter}">' +
+ '<input/>' +
+ '<t t-esc="counter"/>' +
+ '</li>' +
+ '</ol>' +
+ '</t>' +
+ '</no>'
+ );
+ },
+ afterEach: function() {
+ core.qweb = this.oldQWeb;
+ },
+ });
+
+ QUnit.test('basic-alias', function (assert) {
+ assert.expect(1);
+
+
+ var widget = new (Widget.extend({
+ template: 'test.widget.template'
+ }))();
+ widget.renderElement();
+
+ assert.ok(widget.$('li:eq(3)').is(widget.$el.find('li:eq(3)')),
+ "should do the same thing as calling find on the widget root");
+ widget.destroy();
+ });
+
+
+ QUnit.test('delegate', async function (assert) {
+ assert.expect(5);
+
+ var a = [];
+ var widget = new (Widget.extend({
+ template: 'test.widget.template',
+ events: {
+ 'click': function () {
+ a[0] = true;
+ assert.strictEqual(this, widget, "should trigger events in widget");
+ },
+ 'click li.class-3': 'class3',
+ 'change input': function () { a[2] = true; }
+ },
+ class3: function () { a[1] = true; }
+ }))();
+ widget.renderElement();
+
+ await testUtils.dom.click(widget.$el, {allowInvisible: true});
+ await testUtils.dom.click(widget.$('li:eq(3)'), {allowInvisible: true});
+ await testUtils.fields.editAndTrigger(widget.$('input:last'), 'foo', 'change');
+
+ for(var i=0; i<3; ++i) {
+ assert.ok(a[i], "should pass test " + i);
+ }
+ widget.destroy();
+ });
+
+ QUnit.test('undelegate', async function (assert) {
+ assert.expect(4);
+
+ var clicked = false;
+ var newclicked = false;
+
+ var widget = new (Widget.extend({
+ template: 'test.widget.template',
+ events: { 'click li': function () { clicked = true; } }
+ }))();
+
+ widget.renderElement();
+ widget.$el.on('click', 'li', function () { newclicked = true; });
+
+ await testUtils.dom.clickFirst(widget.$('li'), {allowInvisible: true});
+ assert.ok(clicked, "should trigger bound events");
+ assert.ok(newclicked, "should trigger bound events");
+
+ clicked = newclicked = false;
+ widget._undelegateEvents();
+ await testUtils.dom.clickFirst(widget.$('li'), {allowInvisible: true});
+ assert.ok(!clicked, "undelegate should unbind events delegated");
+ assert.ok(newclicked, "undelegate should only unbind events it created");
+ widget.destroy();
+ });
+
+ QUnit.module('Widget, and async stuff');
+
+ QUnit.test("alive(alive)", async function (assert) {
+ assert.expect(1);
+
+ var widget = new (Widget.extend({}));
+
+ await widget.start()
+ .then(function () {return widget.alive(Promise.resolve()) ;})
+ .then(function () { assert.ok(true); });
+
+ widget.destroy();
+ });
+
+ QUnit.test("alive(dead)", function (assert) {
+ assert.expect(1);
+ var widget = new (Widget.extend({}));
+
+ return new Promise(function (resolve, reject) {
+ widget.start()
+ .then(function () {
+ // destroy widget
+ widget.destroy();
+ var promise = Promise.resolve();
+ // leave time for alive() to do its stuff
+ promise.then(function () {
+ return Promise.resolve();
+ }).then(function () {
+ assert.ok(true);
+ resolve();
+ });
+ // ensure that widget.alive() refuses to resolve or reject
+ return widget.alive(promise);
+ }).then(function () {
+ reject();
+ assert.ok(false, "alive() should not terminate by default");
+ }).catch(function() {
+ reject();
+ assert.ok(false, "alive() should not terminate by default");
+ });
+ });
+ });
+
+ QUnit.test("alive(alive, true)", async function (assert) {
+ assert.expect(1);
+ var widget = new (Widget.extend({}));
+ await widget.start()
+ .then(function () { return widget.alive(Promise.resolve(), true); })
+ .then(function () { assert.ok(true); });
+ widget.destroy();
+ });
+
+ QUnit.test("alive(dead, true)", function (assert) {
+ assert.expect(1);
+ var done = assert.async();
+
+ var widget = new (Widget.extend({}));
+
+ widget.start()
+ .then(function () {
+ // destroy widget
+ widget.destroy();
+ return widget.alive(Promise.resolve(), true);
+ }).then(function () {
+ assert.ok(false, "alive(p, true) should fail its promise");
+ done();
+ }, function () {
+ assert.ok(true, "alive(p, true) should fail its promise");
+ done();
+ });
+ });
+
+ QUnit.test("calling _rpc on destroyed widgets", async function (assert) {
+ assert.expect(3);
+
+ var def;
+ var parent = new Widget();
+ await testUtils.mock.addMockEnvironment(parent, {
+ session: {
+ rpc: function () {
+ def = testUtils.makeTestPromise();
+ def.abort = def.reject;
+ return def;
+ },
+ },
+ services: {
+ ajax: AjaxService
+ },
+ });
+ var widget = new Widget(parent);
+
+ widget._rpc({route: '/a/route'}).then(function () {
+ assert.ok(true, "The ajax call should be resolve");
+ });
+ def.resolve();
+ await testUtils.nextMicrotaskTick();
+ def = null;
+
+ widget._rpc({route: '/a/route'}).then(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "promise that remains pending forever");
+ }).catch(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "promise that remains pending forever");
+ });
+ widget.destroy();
+ def.resolve();
+ await testUtils.nextMicrotaskTick();
+ def = null;
+
+ widget._rpc({route: '/a/route'}).then(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "promise that remains pending forever");
+ }).catch(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "promise that remains pending forever");
+ });
+ assert.ok(!def, "trigger_up is not performed and the call returns a " +
+ "promise that remains pending forever");
+
+ assert.ok(true,
+ "there should be no crash when calling _rpc on a destroyed widget");
+ parent.destroy();
+ });
+
+ QUnit.test("calling do_hide on a widget destroyed before being rendered", async function (assert) {
+ assert.expect(1);
+
+ const MyWidget = Widget.extend({
+ willStart() {
+ return new Promise(() => {});
+ }
+ });
+
+ const widget = new MyWidget();
+ widget.appendTo(document.createDocumentFragment());
+ widget.destroy();
+
+ // those calls should not crash
+ widget.do_hide();
+ widget.do_show();
+ widget.do_toggle(true);
+
+ assert.ok(true);
+ });
+
+ QUnit.test('start is not called when widget is destroyed', function (assert) {
+ assert.expect(0);
+ const $fix = $("#qunit-fixture");
+
+ // Note: willStart is always async
+ const MyWidget = Widget.extend({
+ start: function () {
+ assert.ok(false, 'Should not call start method');
+ },
+ });
+
+ const widget = new MyWidget();
+ widget.appendTo($fix);
+ widget.destroy();
+
+ const divEl = document.createElement('div');
+ $fix[0].appendChild(divEl);
+ const widget2 = new MyWidget();
+ widget2.attachTo(divEl);
+ widget2.destroy();
+ });
+
+ QUnit.test("don't destroy twice widget's children", function (assert) {
+ assert.expect(2);
+
+ var parent = new Widget();
+ var child = new (Widget.extend({
+ destroy: function () {
+ assert.step('destroy');
+ }
+ }))(parent);
+
+ parent.destroy();
+ assert.verifySteps(['destroy'], "child should have been detroyed only once");
+ });
+
+
+ QUnit.module('Widgets, Dialog');
+
+ QUnit.test("don't close dialog on backdrop click", async function (assert) {
+ assert.expect(3);
+
+ var dialog = new Dialog(null);
+ dialog.open();
+ await dialog.opened();
+
+ assert.strictEqual($('.modal.show').length, 1, "a dialog should have opened");
+ var $backdrop = $('.modal-backdrop');
+ assert.strictEqual($backdrop.length, 1, "the dialog should have a modal backdrop");
+ testUtils.dom.click('.modal.show'); // Click on backdrop is in fact a direct click on the .modal element
+ assert.strictEqual($('.modal.show').length, 1, "the dialog should still be opened");
+
+ dialog.close();
+ });
+});
+
+});