diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/core | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/tests/core')
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(); + }); +}); + +}); |
