summaryrefslogtreecommitdiff
path: root/addons/web/static/tests
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/tests')
-rw-r--r--addons/web/static/tests/boot_tests.js21
-rw-r--r--addons/web/static/tests/chrome/pager_tests.js94
-rw-r--r--addons/web/static/tests/chrome/search_tests.js172
-rw-r--r--addons/web/static/tests/core/class_tests.js168
-rw-r--r--addons/web/static/tests/core/concurrency_tests.js256
-rw-r--r--addons/web/static/tests/core/domain_tests.js62
-rw-r--r--addons/web/static/tests/core/pyeval_tests.js1009
-rw-r--r--addons/web/static/tests/core/registry_tests.js83
-rw-r--r--addons/web/static/tests/core/rpc_tests.js217
-rw-r--r--addons/web/static/tests/core/time_tests.js150
-rw-r--r--addons/web/static/tests/core/util_tests.js49
-rw-r--r--addons/web/static/tests/core/widget_tests.js395
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js2718
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js81
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js3742
-rw-r--r--addons/web/static/tests/helpers/mock_server.js941
-rw-r--r--addons/web/static/tests/helpers/qunit_config.js110
-rw-r--r--addons/web/static/tests/helpers/test_utils.js442
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js1220
-rw-r--r--addons/web/static/tests/views/calendar_tests.js340
-rw-r--r--addons/web/static/tests/views/form_tests.js2688
-rw-r--r--addons/web/static/tests/views/graph_tests.js256
-rw-r--r--addons/web/static/tests/views/kanban_model_tests.js253
-rw-r--r--addons/web/static/tests/views/kanban_tests.js1068
-rw-r--r--addons/web/static/tests/views/list_tests.js1264
-rw-r--r--addons/web/static/tests/views/pivot_tests.js536
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js63
-rw-r--r--addons/web/static/tests/widgets/domain_selector_tests.js171
-rw-r--r--addons/web/static/tests/widgets/model_field_selector_tests.js137
29 files changed, 18706 insertions, 0 deletions
diff --git a/addons/web/static/tests/boot_tests.js b/addons/web/static/tests/boot_tests.js
new file mode 100644
index 00000000000..f317e77d2e5
--- /dev/null
+++ b/addons/web/static/tests/boot_tests.js
@@ -0,0 +1,21 @@
+(function() {
+"use strict";
+
+odoo.__DEBUG__.didLogInfo.then(function() {
+
+ var modulesInfo = odoo.__DEBUG__.js_modules;
+
+ QUnit.module('Odoo JS Modules');
+
+ QUnit.test('all modules are properly loaded', function(assert) {
+ assert.expect(2);
+
+ assert.deepEqual(modulesInfo.missing, [],
+ "no js module should be missing");
+ assert.deepEqual(modulesInfo.failed, [],
+ "no js module should have failed");
+ });
+
+});
+
+})(); \ No newline at end of file
diff --git a/addons/web/static/tests/chrome/pager_tests.js b/addons/web/static/tests/chrome/pager_tests.js
new file mode 100644
index 00000000000..48c4ce6ed89
--- /dev/null
+++ b/addons/web/static/tests/chrome/pager_tests.js
@@ -0,0 +1,94 @@
+odoo.define('web.pager_tests', function (require) {
+"use strict";
+
+var Pager = require('web.Pager');
+var concurrency = require('web.concurrency');
+
+QUnit.module('chrome', {}, function () {
+
+ QUnit.module('Pager');
+
+ QUnit.test('basic stuff', function (assert) {
+ assert.expect(2);
+
+ var pager = new Pager(null, 10, 1, 4);
+ pager.appendTo($('#qunit-fixture'));
+
+ assert.strictEqual(pager.state.current_min, 1,
+ "current_min should be set to 1");
+
+ // click on next
+ pager.$('.o_pager_next').click();
+ assert.strictEqual(pager.state.current_min, 5,
+ "current_min should now be 5");
+
+ pager.destroy();
+ });
+
+ QUnit.test('edit the pager', function (assert) {
+ assert.expect(5);
+
+ var pager = new Pager(null, 10, 1, 4);
+ pager.appendTo($('#qunit-fixture'));
+
+ // enter edition
+ pager.$('.o_pager_value').click();
+ assert.strictEqual(pager.$('input').length, 1,
+ "the pager should contain an input");
+ assert.strictEqual(pager.$('input').val(), '1-4',
+ "the input should have correct value");
+
+ // change the limit
+ pager.$('input')
+ .val('1-6')
+ .trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); // trigger the change
+ assert.strictEqual(pager.state.limit, 6,
+ "the limit should have been updated");
+ assert.strictEqual(pager.state.current_min, 1,
+ "the current_min should not have changed");
+ assert.strictEqual(pager.$('.o_pager_value').text(), '1-6',
+ "the input should have correct value");
+
+ pager.destroy();
+ });
+
+ QUnit.test('disabling the pager', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var pager = new Pager(null, 10, 1, 4);
+ pager.appendTo($('#qunit-fixture'));
+
+ pager.disable();
+
+ // try to go to the next or previous pages
+ pager.$('.o_pager_next').click();
+ assert.strictEqual(pager.state.current_min, 1,
+ "current_min should still be 1");
+ pager.$('.o_pager_previous').click();
+ assert.strictEqual(pager.state.current_min, 1,
+ "current_min should still be 1");
+
+ // try to change the limit
+ pager.$('.o_pager_value').click();
+ assert.strictEqual(pager.$('input').length, 0,
+ "the pager should not contain an input");
+
+ // a common use is to disable the pager before reloading the data, and
+ // re-enable it once they have been loaded
+ // the following imulates this situation
+ pager.on('pager_changed', null, function () {
+ pager.disable();
+ concurrency.delay(0).then(function () {
+ assert.ok(pager.disabled, "pager should still be disabled");
+ pager.destroy();
+ done();
+ });
+ });
+ pager.enable();
+ pager.$('.o_pager_next').click();
+
+ });
+});
+
+}); \ No newline at end of file
diff --git a/addons/web/static/tests/chrome/search_tests.js b/addons/web/static/tests/chrome/search_tests.js
new file mode 100644
index 00000000000..c2d290134ed
--- /dev/null
+++ b/addons/web/static/tests/chrome/search_tests.js
@@ -0,0 +1,172 @@
+odoo.define('web.search_tests', function (require) {
+"use strict";
+
+var SearchView = require('web.SearchView');
+
+QUnit.module('chrome', {}, function () {
+
+ QUnit.module('Search View');
+
+ QUnit.test('Adding a facet to the query creates a facet and a value', function (assert) {
+ assert.expect(3);
+
+ var query = new SearchView.SearchQuery();
+ var field = {};
+ query.add({
+ category: 'Foo',
+ field: field,
+ values: [{label: 'Value', value: 3}]
+ });
+
+ var facet = query.at(0);
+ assert.strictEqual(facet.get('category'), 'Foo');
+ assert.strictEqual(facet.get('field'), field);
+ assert.deepEqual(facet.get('values'), [{label: 'Value', value: 3}]);
+ });
+
+ QUnit.test('Adding two facets', function (assert) {
+ assert.expect(3);
+
+ var query = new SearchView.SearchQuery();
+ query.add([
+ { category: 'Foo', field: {}, values: [{label: 'Value', value: 3}] },
+ { category: 'Bar', field: {}, values: [{label: 'Value 2', value: 4}] }
+ ]);
+
+ assert.strictEqual(query.length, 2);
+ assert.strictEqual(query.at(0).values.length, 1);
+ assert.strictEqual(query.at(1).values.length, 1);
+ });
+
+ QUnit.test('If a facet already exists, add values to it', function (assert) {
+ assert.expect(2);
+
+ var query = new SearchView.SearchQuery();
+ var field = {};
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+ query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
+
+ assert.strictEqual(query.length, 1, "adding an existing facet should merge new values into old facet");
+ var facet = query.at(0);
+ assert.deepEqual(facet.get('values'), [
+ {label: 'V1', value: 0},
+ {label: 'V2', value: 1}
+ ]);
+ });
+
+ QUnit.test('Facet being implicitly changed should trigger change, not add', function (assert) {
+ assert.expect(2);
+
+ var query = new SearchView.SearchQuery();
+ var field = {}, added = false, changed = false;
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+ query.on('add', function () { added = true; })
+ .on('change', function () { changed = true; });
+ query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
+
+ assert.ok(!added, "query.add adding values to a facet should not trigger an add");
+ assert.ok(changed, "query.add adding values to a facet should not trigger a change");
+ });
+
+ QUnit.test('Toggling a facet, value which does not exist should add it', function (assert) {
+ assert.expect(3);
+
+ var query = new SearchView.SearchQuery();
+ var field = {};
+ query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+
+ assert.strictEqual(query.length, 1, "Should have created a single facet");
+ var facet = query.at(0);
+ assert.strictEqual(facet.values.length, 1, "Facet should have a single value");
+ assert.deepEqual(facet.get('values'), [{label: 'V1', value: 0}],
+ "Facet's value should match input");
+ });
+
+ QUnit.test('Toggling a facet which exists with a value which does not should add the value to the facet', function (assert) {
+ assert.expect(3);
+
+ var field = {};
+ var query = new SearchView.SearchQuery();
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+ query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
+
+ assert.strictEqual(query.length, 1, "Should have edited the existing facet");
+ var facet = query.at(0);
+ assert.strictEqual(facet.values.length, 2, "Should have added the value to the existing facet");
+ assert.deepEqual(facet.get('values'), [
+ {label: 'V1', value: 0},
+ {label: 'V2', value: 1}
+ ]);
+ });
+
+ QUnit.test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function (assert) {
+ assert.expect(3);
+
+ var field = {};
+ var query = new SearchView.SearchQuery();
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+ query.add({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
+
+ query.toggle({category: 'A', field: field, values: [{label: 'V2', value: 1}]});
+
+ assert.strictEqual(query.length, 1, 'Should have the same single facet');
+ var facet = query.at(0);
+ assert.strictEqual(facet.values.length, 1, "Should only have one value left in the facet");
+ assert.deepEqual(facet.get('values'), [
+ {label: 'V1', value: 0}
+ ]);
+ });
+
+ QUnit.test('Toggling off the last value of a facet should remove the facet', function (assert) {
+ assert.expect(1);
+
+ var field = {};
+ var query = new SearchView.SearchQuery();
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+
+ query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+
+ assert.strictEqual(query.length, 0, 'Should have removed the facet');
+ });
+
+ QUnit.test('Intermediate emptiness should not remove the facet', function (assert) {
+ assert.expect(3);
+
+ var field = {};
+ var query = new SearchView.SearchQuery();
+ query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]});
+
+ query.toggle({category: 'A', field: field, values: [
+ {label: 'V1', value: 0},
+ {label: 'V2', value: 1}
+ ]});
+
+ assert.strictEqual(query.length, 1, 'Should not have removed the facet');
+ var facet = query.at(0);
+ assert.strictEqual(facet.values.length, 1, "Should have one value");
+ assert.deepEqual(facet.get('values'), [
+ {label: 'V2', value: 1}
+ ]);
+ });
+
+ QUnit.test('Reseting with multiple facets should still work to load defaults', function (assert) {
+ assert.expect(3);
+
+ var query = new SearchView.SearchQuery();
+ var field = {};
+ query.reset([
+ {category: 'A', field: field, values: [{label: 'V1', value: 0}]},
+ {category: 'A', field: field, values: [{label: 'V2', value: 1}]}]);
+
+ assert.strictEqual(query.length, 1, 'Should have created a single facet');
+ assert.strictEqual(query.at(0).values.length, 2, 'the facet should have merged two values');
+ assert.deepEqual(query.at(0).get('values'), [
+ {label: 'V1', value: 0},
+ {label: 'V2', value: 1}
+ ]);
+ });
+
+ // TODO add a test to check that combining two filters with a or normalizes the two filters before
+ // Note: maybe domain.js will be improved to make this easier
+});
+});
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 00000000000..438b137e6a1
--- /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 00000000000..62b9f90bcfb
--- /dev/null
+++ b/addons/web/static/tests/core/concurrency_tests.js
@@ -0,0 +1,256 @@
+odoo.define('web.concurrency_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('concurrency');
+
+
+ QUnit.test('mutex: simple scheduling', function (assert) {
+ assert.expect(6);
+
+ var m = new concurrency.Mutex();
+
+ var def1 = $.Deferred(),
+ def2 = $.Deferred();
+
+ var p1 = m.exec(function () { return def1; });
+ var p2 = m.exec(function () { return def2; });
+
+ assert.strictEqual(p1.state(), "pending");
+ assert.strictEqual(p2.state(), "pending");
+
+ def1.resolve();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "pending");
+
+ def2.resolve();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "resolved");
+ });
+
+ QUnit.test('mutex: simpleScheduling2', function (assert) {
+ assert.expect(6);
+
+ var m = new concurrency.Mutex();
+
+ var def1 = $.Deferred(),
+ def2 = $.Deferred();
+
+ var p1 = m.exec(function() { return def1; });
+ var p2 = m.exec(function() { return def2; });
+
+ assert.strictEqual(p1.state(), "pending");
+ assert.strictEqual(p2.state(), "pending");
+ def2.resolve();
+
+ assert.strictEqual(p1.state(), "pending");
+ assert.strictEqual(p2.state(), "pending");
+
+ def1.resolve();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "resolved");
+ });
+
+ QUnit.test('mutex: reject', function (assert) {
+ assert.expect(12);
+
+ var m = new concurrency.Mutex();
+
+ var def1 = $.Deferred(),
+ def2 = $.Deferred(),
+ def3 = $.Deferred();
+
+ var p1 = m.exec(function() {return def1;});
+ var p2 = m.exec(function() {return def2;});
+ var p3 = m.exec(function() {return def3;});
+
+ assert.strictEqual(p1.state(), "pending");
+ assert.strictEqual(p2.state(), "pending");
+ assert.strictEqual(p3.state(), "pending");
+
+ def1.resolve();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "pending");
+ assert.strictEqual(p3.state(), "pending");
+
+ def2.reject();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "rejected");
+ assert.strictEqual(p3.state(), "pending");
+
+ def3.resolve();
+ assert.strictEqual(p1.state(), "resolved");
+ assert.strictEqual(p2.state(), "rejected");
+ assert.strictEqual(p3.state(), "resolved");
+ });
+
+ QUnit.test('DropMisordered: resolve all correctly ordered, sync', function (assert) {
+ assert.expect(1);
+
+ var dm = new concurrency.DropMisordered(),
+ flag = false;
+
+ var d1 = $.Deferred(),
+ d2 = $.Deferred();
+
+ var r1 = dm.add(d1),
+ r2 = dm.add(d2);
+
+ $.when(r1, r2).done(function () {
+ flag = true;
+ });
+
+ d1.resolve();
+ d2.resolve();
+
+ assert.ok(flag);
+ });
+
+ QUnit.test("DropMisordered: don't resolve mis-ordered, sync", function (assert) {
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(),
+ done1 = false,
+ done2 = false,
+ fail1 = false,
+ fail2 = false;
+
+ var d1 = $.Deferred(),
+ d2 = $.Deferred();
+
+ dm.add(d1).done(function () { done1 = true; })
+ .fail(function () { fail1 = true; });
+ dm.add(d2).done(function () { done2 = true; })
+ .fail(function () { fail2 = true; });
+
+ d2.resolve();
+ d1.resolve();
+
+ // d1 is in limbo
+ assert.ok(!done1);
+ assert.ok(!fail1);
+
+ // d2 is resolved
+ assert.ok(done2);
+ assert.ok(!fail2);
+ });
+
+ QUnit.test('DropMisordered: fail mis-ordered flag, sync', function (assert) {
+ assert.expect(4);
+
+ var dm = new concurrency.DropMisordered(true),
+ done1 = false,
+ done2 = false,
+ fail1 = false,
+ fail2 = false;
+
+ var d1 = $.Deferred(),
+ d2 = $.Deferred();
+
+ dm.add(d1).done(function () { done1 = true; })
+ .fail(function () { fail1 = true; });
+ dm.add(d2).done(function () { done2 = true; })
+ .fail(function () { fail2 = true; });
+
+ d2.resolve();
+ d1.resolve();
+
+ // 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 = $.Deferred(),
+ d2 = $.Deferred();
+
+ var r1 = dm.add(d1),
+ r2 = dm.add(d2);
+
+ setTimeout(function () { d1.resolve(); }, 10);
+ setTimeout(function () { d2.resolve(); }, 20);
+
+ $.when(r1, r2).done(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 = $.Deferred(),
+ d2 = $.Deferred();
+
+ dm.add(d1).done(function () { done1 = true; })
+ .fail(function () { fail1 = true; });
+ dm.add(d2).done(function () { done2 = true; })
+ .fail(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 = $.Deferred(),
+ d2 = $.Deferred();
+
+ dm.add(d1).done(function () { done1 = true; })
+ .fail(function () { fail1 = true; });
+ dm.add(d2).done(function () { done2 = true; })
+ .fail(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);
+ });
+
+});
+
+}); \ No newline at end of file
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 00000000000..001eae2d6b9
--- /dev/null
+++ b/addons/web/static/tests/core/domain_tests.js
@@ -0,0 +1,62 @@
+odoo.define('web.domain_tests', function (require) {
+"use strict";
+
+var Domain = require('web.Domain');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('domain');
+
+ 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({}));
+ });
+});
+});
diff --git a/addons/web/static/tests/core/pyeval_tests.js b/addons/web/static/tests/core/pyeval_tests.js
new file mode 100644
index 00000000000..0bd9a7151dd
--- /dev/null
+++ b/addons/web/static/tests/core/pyeval_tests.js
@@ -0,0 +1,1009 @@
+odoo.define('web.pyEval_tests', function(require) {
+"use strict";
+
+var Context = require('web.Context');
+var pyEval = require('web.pyeval');
+var time = require('web.time');
+
+QUnit.module('core', function () {
+
+ QUnit.module('PY.eval');
+
+ QUnit.test('simple python expression', function(assert) {
+ assert.expect(2);
+
+ var result = pyEval.py_eval("True and False");
+ assert.strictEqual(result, false, "should properly evaluate basic expression");
+ result = pyEval.py_eval("a + b", {a: 1, b: 41});
+ assert.strictEqual(result, 42, "should properly evaluate basic sum");
+ });
+
+ 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 = pyEval.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 = pyEval.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, pyEval);
+
+ 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 = pyEval.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 = pyEval.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: pyEval.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: pyEval.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(
+ pyEval.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: pyEval.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 = pyEval.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('relativedelta', function (assert) {
+ assert.expect(5);
+
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
+ pyEval.context()),
+ "2012-02-14 23:59:59");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=1)).strftime('%Y-%m-%d')",
+ pyEval.context()),
+ "2012-02-16");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
+ pyEval.context()),
+ "2012-02-14");
+ assert.strictEqual(
+ py.eval("(datetime.date(2012, 2, 1) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
+ pyEval.context()),
+ '2012-01-31');
+ assert.strictEqual(
+ py.eval("(datetime.date(2015,2,5)+relativedelta(days=-6,weekday=0)).strftime('%Y-%m-%d')",
+ pyEval.context()),
+ '2015-02-02');
+ });
+
+ QUnit.test('datetime.tojson', function (assert) {
+ assert.expect(7);
+
+ var result = py.eval(
+ 'datetime.datetime(2012, 2, 15, 1, 7, 31)',
+ pyEval.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")',
+ pyEval.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")',
+ pyEval.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")',
+ pyEval.context());
+ assert.strictEqual(result, "2012-02-15 00:00:00");
+ });
+
+ QUnit.module('pyeval (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 = pyEval.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 = pyEval.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_content_subtype": "html",
+ "default_model": "res.users",
+ "default_parent_id": false,
+ "default_res_id": 1
+ },
+ {}
+ ],
+ "__eval_context": null,
+ "__ref": "compound_context"
+ }];
+ var result = pyEval.eval_domains_and_contexts({
+ contexts: ctx,
+ domains: [],
+ });
+
+ assert.deepEqual(result.context, {
+ "default_attachment_ids": [],
+ "default_body": "",
+ "default_content_subtype": "html",
+ "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 = pyEval.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 = pyEval.eval_domains_and_contexts({
+ domains: [d],
+ contexts: [],
+ });
+ assert.deepEqual(result1.domain, [['company_id', '=', false]]);
+
+ var result2 = pyEval.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 = pyEval.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 = pyEval.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 = pyEval.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 = pyEval.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('pyeval (contexts)');
+
+ QUnit.test('context_recursive', function (assert) {
+ assert.expect(3);
+
+ var context_to_eval = [{
+ __ref: 'context',
+ __debug: '{"foo": context.get("bar", "qux")}'
+ }];
+ assert.deepEqual(
+ pyEval.eval('contexts', context_to_eval, {bar: "ok"}),
+ {foo: 'ok'});
+ assert.deepEqual(
+ pyEval.eval('contexts', context_to_eval, {bar: false}),
+ {foo: false});
+ assert.deepEqual(
+ pyEval.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 = pyEval.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 = pyEval.eval('contexts', [{
+ "__ref": "compound_context",
+ "__contexts": [
+ {"__ref": "context", "__debug": "{'type':parent.type}",
+ "__id": "462b9dbed42f"}
+ ],
+ "__eval_context": {
+ "__ref": "compound_context",
+ "__contexts": [{
+ "__ref": "compound_context",
+ "__contexts": [
+ {"__ref": "context", "__debug": "{'type': type}",
+ "__id": "16a04ed5a194"}
+ ],
+ "__eval_context": {
+ "__ref": "compound_context",
+ "__contexts": [
+ {"lang": "en_US", "tz": false, "uid": 1,
+ "journal_type": "sale", "section_id": false,
+ "default_type": "out_invoice",
+ "type": "out_invoice", "department_id": false},
+ {"id": false, "journal_id": 10,
+ "number": false, "type": "out_invoice",
+ "currency_id": 1, "partner_id": 4,
+ "fiscal_position_id": false,
+ "date_invoice": false, "date": false,
+ "payment_term_id": false, "reference_type": "none",
+ "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",
+ "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.invoice",
+ "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.invoice.line",
+ "parent": {
+ "id": false, "journal_id": 10, "number": false,
+ "type": "out_invoice", "currency_id": 1,
+ "partner_id": 4, "fiscal_position_id": false,
+ "date_invoice": false, "date": false,
+ "payment_term_id": false, "reference_type": "none",
+ "reference": false, "account_id": 440, "name": false,
+ "tax_line_ids": [], "amount_untaxed": 0, "amount_tax": 0,
+ "reconciled": false, "amount_total": 0,
+ "state": "draft", "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, {type: 'out_invoice'});
+ });
+
+ QUnit.test('return-input-value', function (assert) {
+ assert.expect(1);
+
+ var result = pyEval.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('pyeval (domains)');
+
+ QUnit.test('current_date', function (assert) {
+ assert.expect(1);
+
+ var current_date = time.date_to_str(new Date());
+ var result = pyEval.eval('domains',
+ [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}],
+ pyEval.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(
+ pyEval.eval('domains', domains_to_eval, {bar: "ok"}),
+ [['foo', '=', 'ok'], ['bar', '>=', 42]]);
+ assert.deepEqual(
+ pyEval.eval('domains', domains_to_eval, {bar: false}),
+ [['foo', '=', false], ['bar', '>=', 42]]);
+ assert.deepEqual(
+ pyEval.eval('domains', domains_to_eval),
+ [['foo', '=', 'qux'], ['bar', '>=', 42]]);
+ });
+
+ QUnit.module('pyeval (groupbys)');
+
+ QUnit.test('groupbys_00', function (assert) {
+ assert.expect(1);
+
+ var result = pyEval.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 = pyEval.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 = pyEval.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 = pyEval.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 = pyEval.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 = pyEval.eval('groupbys', [
+ {group_by: 'foo'},
+ { __ref: 'context', __debug: '{"group_by": value}' },
+ {group_by: 'grault'}
+ ], { value: 'bar' });
+ assert.deepEqual(result, ['foo', 'bar', 'grault']);
+ });
+
+});
+
+});
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 00000000000..d28e429da24
--- /dev/null
+++ b/addons/web/static/tests/core/registry_tests.js
@@ -0,0 +1,83 @@
+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('extension', function (assert) {
+ assert.expect(2);
+
+ var foo = {};
+ var foo2 = {};
+ var registry = new Registry({
+ foo: foo,
+ });
+ var registry2 = registry.extend({foo: foo2});
+ assert.strictEqual(registry.get('foo'), foo);
+ assert.strictEqual(registry2.get('foo'), foo2);
+ });
+
+ QUnit.test('remain-linked', function (assert) {
+ assert.expect(2);
+
+ var foo = {};
+ var foo2 = {};
+ var registry = new Registry({
+ foo: foo,
+ });
+
+ var registry2 = registry.extend();
+
+ registry.add('foo2', foo2);
+
+ assert.strictEqual(registry.get('foo2'), foo2);
+ assert.strictEqual(registry2.get('foo2'), foo2);
+ });
+
+ 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('extended-multiget', function (assert) {
+ assert.expect(1);
+
+ var foo = {};
+ var bar = {};
+ var registry = new Registry({
+ foo: foo,
+ bar: bar,
+ });
+ var registry2 = registry.extend();
+ assert.strictEqual(registry2.getAny(['qux', 'grault', 'bar', 'foo']), bar);
+ });
+
+});
+
+});
+
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 00000000000..c19f1007417
--- /dev/null
+++ b/addons/web/static/tests/core/rpc_tests.js
@@ -0,0 +1,217 @@
+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: [['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,
+ orderby: false,
+ },
+ 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,
+ orderby: false,
+ },
+ method: 'read_group',
+ model: 'partner',
+ }, "should have correct args");
+ });
+
+ QUnit.test('search_read with no domain, nor fields', function (assert) {
+ assert.expect(2);
+ var query = rpc.buildQuery({
+ model: 'partner',
+ route: '/web/dataset/search_read',
+ });
+
+ assert.deepEqual(query.params.domain, [], "should have [] as default domain");
+ assert.strictEqual(query.params.fields, false, "should have false as default fields");
+ });
+});
+
+}); \ No newline at end of file
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 00000000000..3407f5f4276
--- /dev/null
+++ b/addons/web/static/tests/core/time_tests.js
@@ -0,0 +1,150 @@
+odoo.define('web.time_tests', function (require) {
+"use strict";
+
+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");
+ });
+
+});
+
+}); \ 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 00000000000..204503117e6
--- /dev/null
+++ b/addons/web/static/tests/core/util_tests.js
@@ -0,0 +1,49 @@
+odoo.define('web.util_tests', function (require) {
+"use strict";
+
+var utils = require('web.utils');
+
+QUnit.module('core', {}, function () {
+
+ QUnit.module('utils');
+
+
+ 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');
+ });
+
+});
+
+}); \ No newline at end of file
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 00000000000..8988002734b
--- /dev/null
+++ b/addons/web/static/tests/core/widget_tests.js
@@ -0,0 +1,395 @@
+odoo.define('web.widget_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+var core = require('web.core');
+var QWeb = require('web.QWeb');
+var Widget = require('web.Widget');
+
+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');
+ });
+
+ 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");
+ });
+
+ 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");
+ });
+
+ 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)");
+ });
+
+ 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");
+ });
+
+
+
+ QUnit.test('renderElement, no template, default', function (assert) {
+ assert.expect(8);
+
+ var widget = new (Widget.extend({ }))();
+
+ var $original = widget.$el;
+ assert.ok($original, "should initially have a root element");
+
+ widget.renderElement();
+
+ assert.ok(widget.$el, "should have generated a root element");
+ assert.ok($original !== widget.$el, "should have generated a new 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"));
+ });
+
+ 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");
+ });
+
+ 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.strictEqual(widget.$el.attr('id'), 'foo', "should have generated the id attribute");
+ assert.strictEqual(widget.el.id, 'foo', "should also be available via property");
+ });
+
+ 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.strictEqual(widget.$el.attr('class'), 'oe_some_class', "should have the right attribute");
+ });
+
+ 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.strictEqual(widget.$el.attr('id'), 'some_id');
+
+ assert.strictEqual(widget.el.className, 'some_class');
+ assert.strictEqual(widget.$el.attr('class'), 'some_class');
+
+ assert.strictEqual(widget.$el.attr('data-foo'), 'data attribute');
+ assert.strictEqual(widget.$el.data('foo'), 'data attribute');
+
+ assert.strictEqual(widget.$el.attr('clark'), 'gable');
+ assert.strictEqual(widget.$el.attr('spoiler'), 'snape kills dumbledore');
+ });
+
+ 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');
+ });
+
+ QUnit.test('repeated', 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;
+
+ return widget.appendTo($fix)
+ .done(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");
+ });
+ });
+
+
+ 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");
+ });
+
+
+ QUnit.test('delegate', 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();
+
+ widget.$el.click();
+ widget.$('li:eq(3)').click();
+ widget.$('input:last').val('foo').change();
+
+ for(var i=0; i<3; ++i) {
+ assert.ok(a[i], "should pass test " + i);
+ }
+ });
+
+ QUnit.test('undelegate', 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; });
+
+ widget.$('li').click();
+ assert.ok(clicked, "should trigger bound events");
+ assert.ok(newclicked, "should trigger bound events");
+
+ clicked = newclicked = false;
+ widget.undelegateEvents();
+ widget.$('li').click();
+ assert.ok(!clicked, "undelegate should unbind events delegated");
+ assert.ok(newclicked, "undelegate should only unbind events it created");
+ });
+
+ QUnit.module('Widget, and async stuff');
+
+ QUnit.test("alive(alive)", function (assert) {
+ assert.expect(1);
+
+ var widget = new (Widget.extend({}));
+
+ return concurrency.asyncWhen(widget.start())
+ .then(function () { return widget.alive(concurrency.asyncWhen()); })
+ .then(function () { assert.ok(true); });
+ });
+
+ QUnit.test("alive(dead)", function (assert) {
+ assert.expect(1);
+ var widget = new (Widget.extend({}));
+
+ return $.Deferred(function (d) {
+ concurrency.asyncWhen(widget.start())
+ .then(function () {
+ // destroy widget
+ widget.destroy();
+ var promise = concurrency.asyncWhen();
+ // leave time for alive() to do its stuff
+ promise.then(function () {
+ return concurrency.asyncWhen();
+ }).then(function () {
+ assert.ok(true);
+ d.resolve();
+ });
+ // ensure that widget.alive() refuses to resolve or reject
+ return widget.alive(promise);
+ }).always(function () {
+ d.reject();
+ assert.ok(false, "alive() should not terminate by default");
+ });
+ });
+ });
+
+ QUnit.test("alive(alive, true)", function (assert) {
+ assert.expect(1);
+ var widget = new (Widget.extend({}));
+ return concurrency.asyncWhen(widget.start())
+ .then(function () { return widget.alive(concurrency.asyncWhen(), true) })
+ .then(function () { assert.ok(true); });
+ });
+
+ QUnit.test("alive(dead, true)", function (assert) {
+ assert.expect(1);
+ var done = assert.async();
+
+ var widget = new (Widget.extend({}));
+
+ concurrency.asyncWhen(widget.start())
+ .then(function () {
+ // destroy widget
+ widget.destroy();
+ return widget.alive(concurrency.asyncWhen(), 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", function (assert) {
+ assert.expect(1);
+
+ var widget = new Widget();
+ widget.destroy();
+ widget._rpc({route: '/a/route'}).always(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "deferred that is never resolved nor rejected");
+ });
+ assert.ok(true,
+ "there should be no crash when calling _rpc on a destroyed widget");
+ });
+});
+
+}); \ No newline at end of file
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
new file mode 100644
index 00000000000..c56f694e1fe
--- /dev/null
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -0,0 +1,2718 @@
+odoo.define('web.basic_fields_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+var FormView = require('web.FormView');
+var KanbanView = require('web.KanbanView');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('basic_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ date: {string: "A date", type: "date", searchable: true},
+ datetime: {string: "A datetime", type: "datetime", searchable: true},
+ display_name: {string: "Displayed name", type: "char", searchable: true},
+ foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true},
+ bar: {string: "Bar", type: "boolean", default: true, searchable: true},
+ int_field: {string: "int_field", type: "integer", sortable: true, searchable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1], searchable: true},
+ p: {string: "one2many field", type: "one2many", relation: 'partner', searchable: true},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner', searchable: true},
+ timmy: {string: "pokemon", type: "many2many", relation: 'partner_type', searchable: true},
+ product_id: {string: "Product", type: "many2one", relation: 'product', searchable: true},
+ sequence: {type: "integer", string: "Sequence", searchable: true},
+ currency_id: {string: "Currency", type: "many2one", relation: "currency", searchable: true},
+ selection: {string: "Selection", type: "selection", searchable:true,
+ selection: [['normal', 'Normal'],['blocked', 'Blocked'],['done', 'Done']]},
+ },
+ records: [{
+ id: 1,
+ date: "2017-02-03",
+ datetime: "2017-02-08 10:00:00",
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44444,
+ p: [],
+ timmy: [],
+ trululu: 4,
+ selection: 'blocked',
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 0,
+ qux: 0,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ sequence: 4,
+ currency_id: 2,
+ selection: 'normal',
+ }, {
+ id: 4,
+ display_name: "aaa",
+ foo: "abc",
+ sequence: 9,
+ int_field: false,
+ qux: false,
+ selection: 'done',
+ },
+ {id: 3, bar: true, foo: "gnap", int_field: 80, qux: -3.89859, m2o: 1, m2m: []},
+ {id: 5, bar: false, foo: "blop", int_field: -4, qux: 9.1, m2o: 1, m2m: [1], currency_id: 1}],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char", searchable: true}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char", searchable: true},
+ color: {string: "Color index", type: "integer", searchable: true},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ currency: {
+ fields: {
+ symbol: {string: "Currency Sumbol", type: "char", searchable: true},
+ position: {string: "Currency Position", type: "char", searchable: true},
+ },
+ records: [{
+ id: 1,
+ display_name: "$",
+ symbol: "$",
+ position: "before",
+ }, {
+ id: 2,
+ display_name: "€",
+ symbol: "€",
+ position: "after",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('FieldBoolean');
+
+ QUnit.test('boolean field in form view', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="bar"/></form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 1,
+ "checkbox should be checked");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 1,
+ "checkbox should still be checked");
+
+ // uncheck the checkbox
+ form.$('.o_field_boolean input:checked').click();
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 0,
+ "checkbox should no longer be checked");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 0,
+ "checkbox should still no longer be checked");
+
+ // switch to edit mode and test the opposite change
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 0,
+ "checkbox should still be unchecked");
+ // check the checkbox by hitting the "enter" key after focusing it
+ form.$('.o_field_boolean input')
+ .trigger("focusin")
+ .trigger({type: "keydown", which: $.ui.keyCode.ENTER})
+ .trigger({type: "keyup", which: $.ui.keyCode.ENTER});
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 1,
+ "checkbox should now be checked");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_field_boolean input:checked').length, 1,
+ "checkbox should still be checked");
+ form.destroy();
+ });
+
+ QUnit.test('boolean field in editable list view', function (assert) {
+ assert.expect(11);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="bar"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input').length, 5,
+ "should have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 4,
+ "should have 4 checked input");
+
+ // Edit a line
+ var $cell = list.$('tr.o_data_row:has(.o_checkbox input:checked) td:not(.o_list_record_selector)').first();
+ assert.ok($cell.find('.o_checkbox input:checked').prop('disabled'),
+ "input should be disabled in readonly mode")
+ $cell.click();
+ assert.ok(!$cell.find('.o_checkbox input:checked').prop('disabled'),
+ "input should not have the disabled property in edit mode")
+ $cell.find('.o_checkbox input:checked').click();
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tr.o_data_row:has(.o_checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
+ assert.ok($cell.find('.o_checkbox input:not(:checked)').prop('disabled'),
+ "input should be disabled again")
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 3,
+ "should now have only 3 checked input");
+
+ // Re-Edit the line and fake-check the checkbox
+ $cell.click();
+ var $checkbox = $cell.find('.o_checkbox input:not(:checked)');
+ $checkbox.click(); // Change the checkbox
+ $checkbox.click(); // Undo the change
+
+ // Save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 3,
+ "should still have only 3 checked input");
+
+ // Re-Edit the line to check the checkbox back
+ $cell = list.$('tr.o_data_row:has(.o_checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
+ $cell.click();
+ $cell.find('.o_checkbox input:not(:checked)').click();
+
+ // save
+ list.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 4,
+ "should now have 4 checked input back");
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldBooleanButton');
+
+ QUnit.test('use custom terminology in form view', function (assert) {
+ assert.expect(2);
+
+ var terminology = {
+ string_true: "Production Environment",
+ hover_true: "Switch to test environment",
+ string_false: "Test Environment",
+ hover_false: "Switch to production environment"
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<div name="button_box" class="oe_button_box">' +
+ '<button type="object" class="oe_stat_button" icon="fa-check-square">' +
+ '<field name="bar" widget="boolean_button" options=\'{"terminology": ' +
+ JSON.stringify(terminology) + '}\'/>' +
+ '</button>' +
+ '</div>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_stat_text.o_not_hover:contains(Production Environment)').length, 1,
+ "button should contain correct string");
+ assert.strictEqual(form.$('.o_stat_text.o_hover:contains(Switch to test environment)').length, 1,
+ "button should display correct string when hovering");
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldFloat');
+
+ QUnit.test('float field when unset', function(assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 4,
+ });
+
+ assert.ok(form.$('.o_form_field').hasClass('o_form_field_empty'),
+ 'Non-set float field should be recognized as unset.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float fields use correct digit precision', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('span.o_form_field_number:contains(0.4)').length, 1,
+ "should contain a number rounded to 1 decimal");
+ form.destroy();
+ });
+
+ QUnit.test('float field in form view', function(assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.ok(!form.$('.o_form_field').hasClass('o_form_field_empty'),
+ 'Float field should be considered set for value 0.');
+ assert.strictEqual(form.$('.o_form_field.o_form_field_number').first().text(), '0.000',
+ 'The value should be displayed properly.')
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_input').val(), '0.000',
+ 'The value should be rendered with correct precision.')
+
+ form.$('input.o_form_input').val('108.2458938598598').trigger('input');
+ assert.strictEqual(form.$('input.o_form_input').val(), '108.2458938598598',
+ 'The value should not be formated yet.')
+
+ form.$('input.o_form_input').val('18.8958938598598').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field.o_form_field_number').first().text(), '18.896',
+ 'The new value should be rounded properly.')
+
+ form.destroy();
+ });
+
+ QUnit.test('float field in editable list view', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</tree>',
+ });
+
+ var zeroValues = list.$('td').filter(function() {return $(this).text() === '0.000'});
+ assert.strictEqual(zeroValues.length, 2,
+ 'Unset float values should be rendered as zeros.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ $cell.click();
+
+ assert.strictEqual(list.$('input.o_form_input').length, 1,
+ 'The view should have 1 input for editable float.')
+
+ list.$('input.o_form_input').val('108.2458938598598').trigger('input');
+ assert.strictEqual(list.$('input.o_form_input').val(), '108.2458938598598',
+ 'The value should not be formated yet.')
+
+ list.$('input.o_form_input').val('18.8958938598598').trigger('input');
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('.o_form_field.o_form_field_number').first().text(), '18.896',
+ 'The new value should be rounded properly.')
+
+ list.destroy();
+ });
+
+
+ QUnit.module('EmailWidget');
+
+ QUnit.test('email field in form view', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="email"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $mailtoLink = form.$('a.o_form_uri.o_form_field.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 1,
+ "should have a anchor with correct classes");
+ assert.strictEqual($mailtoLink.text(), 'yop',
+ "the value should be displayed properly");
+ assert.strictEqual($mailtoLink.attr('href'), 'mailto:yop',
+ "should have proper mailto prefix");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').length, 1,
+ "should have an input for the email field");
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ form.$('input[type="text"].o_form_input.o_form_field').val('new').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ $mailtoLink = form.$('a.o_form_uri.o_form_field.o_text_overflow');
+ assert.strictEqual($mailtoLink.text(), 'new',
+ "new value should be displayed properly");
+ assert.strictEqual($mailtoLink.attr('href'), 'mailto:new',
+ "should still have proper mailto prefix");
+
+ form.destroy();
+ });
+
+ QUnit.test('email field in editable list view', function(assert) {
+ assert.expect(10);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="email"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ var $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 5,
+ "should have anchors with correct classes");
+ assert.strictEqual($mailtoLink.first().attr('href'), 'mailto:yop',
+ "should have proper mailto prefix");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ $cell.find('input').val('new').trigger('input');;
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new',
+ "value should be properly updated");
+ $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 5,
+ "should still have anchors with correct classes");
+ assert.strictEqual($mailtoLink.first().attr('href'), 'mailto:new',
+ "should still have proper mailto prefix");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldChar');
+
+ QUnit.test('char widget isValid method works', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.required = true;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var charField = form.renderer.widgets[0];
+ assert.strictEqual(charField.isValid(), true);
+ form.destroy();
+ });
+
+ QUnit.test('char field in form view', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field').text(), 'yop',
+ "the value should be displayed properly");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').length, 1,
+ "should have an input for the char field");
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ form.$('input[type="text"].o_form_input.o_form_field').val('limbo').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field').text(), 'limbo',
+ 'the new value should be displayed');
+ form.destroy();
+ });
+
+ QUnit.test('char field in editable list view', function(assert) {
+ assert.expect(6);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ $cell.find('input').val('brolo').trigger('input');;
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'brolo',
+ "value should be properly updated");
+ list.destroy();
+ });
+
+
+ QUnit.module('UrlWidget');
+
+ QUnit.test('url widget in form view', function (assert) {
+ assert.expect(8);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="url"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').length, 1,
+ "should have a anchor with correct classes");
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').attr('href'), 'yop',
+ "should have proper href link");
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').text(), 'yop',
+ "the value should be displayed properly");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').length, 1,
+ "should have an input for the char field");
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ form.$('input[type="text"].o_form_input.o_form_field').val('limbo').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').length, 1,
+ "should still have a anchor with correct classes");
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').attr('href'), 'limbo',
+ "should have proper new href link");
+ assert.strictEqual(form.$('a.o_form_uri.o_form_field.o_text_overflow').text(), 'limbo',
+ 'the new value should be displayed');
+
+ form.destroy();
+ });
+
+ QUnit.test('char field in editable list view', function(assert) {
+ assert.expect(10);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="url"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').length, 5,
+ "should have 5 anchors with correct classes");
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().attr('href'), 'yop',
+ "should have proper href link");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ $cell.find('input').val('brolo').trigger('input');;
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').length, 5,
+ "should still have 5 anchors with correct classes");
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().attr('href'), 'brolo',
+ "should have proper new href link");
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().text(), 'brolo',
+ "value should be properly updated");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldText');
+
+ QUnit.test('text fields are correctly rendered', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.foo.type = 'text';
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('div.o_form_textarea').length, "should have a text area");
+ assert.strictEqual(form.$('div.o_form_textarea').text(), 'yop', 'should be "yop" in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ var $textarea = form.$('.o_form_textarea textarea');
+ assert.ok($textarea.length, "should have a text area");
+ assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit');
+
+ $textarea.val('hello').trigger('input');
+ assert.strictEqual($textarea.val(), 'hello', 'should be "hello" after first edition');
+
+ $textarea.val('hello world').trigger('input');
+ assert.strictEqual($textarea.val(), 'hello world', 'should be "hello world" after second edition');
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('div.o_form_textarea').text(), 'hello world',
+ 'should be "hello world" after save');
+ form.destroy();
+ });
+
+ QUnit.test('text fields in edit mode have correct height', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.type = 'text';
+ this.data.partner.records[0].foo = "f\nu\nc\nk\nm\ni\nl\ng\nr\no\nm";
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $field = form.$('div.o_list_text');
+
+ assert.strictEqual($field.outerHeight(), $field[0].scrollHeight,
+ "text field should not have a scroll bar");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ var $textarea = form.$('textarea:first');
+
+ // the difference is to take small calculation errors into account
+ assert.strictEqual($textarea.innerHeight(), $textarea[0].scrollHeight,
+ "textarea should not have a scroll bar");
+ form.destroy();
+ });
+
+
+ QUnit.test('text field rendering in list view', function (assert) {
+ assert.expect(1);
+
+ var data = {
+ foo: {
+ fields: {foo: {string: "F", type: "text"}},
+ records: [{id: 1, foo: "some text"}]
+ },
+ };
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td.o_list_text:contains(some text)').length, 1,
+ "should have a td with the .o_list_text class");
+ list.destroy();
+ });
+
+ QUnit.test('field text in editable list view', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.type = 'text';
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>',
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.strictEqual(list.$('textarea').first().get(0), document.activeElement,
+ "text area should have the focus");
+ list.destroy();
+ });
+
+ QUnit.test('field changes are correctly debounced in text fields', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ this.data.partner.fields.foo.type = 'text';
+
+ var def = $.Deferred();
+ var nbNotifyChanges = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 2,
+ fieldDebounce: 3,
+ });
+
+ var _onFieldChanged = form._onFieldChanged;
+ form._onFieldChanged = function () {
+ _onFieldChanged.apply(form, arguments);
+ nbNotifyChanges++;
+ def.resolve();
+ };
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('textarea').val("1").trigger('input');
+ assert.strictEqual(nbNotifyChanges, 0,
+ "no event should have been triggered");
+ form.$('textarea').val("12").trigger('input');
+ assert.strictEqual(nbNotifyChanges, 0,
+ "no event should have been triggered");
+
+ return waitForChangeTriggered().then(function () {
+ assert.strictEqual(nbNotifyChanges, 1,
+ "one event should have been triggered");
+
+ // add something in the textarea, then focus another input
+ form.$('textarea').first().val("123").trigger('input');
+ form.$('textarea').first().change();
+ assert.strictEqual(nbNotifyChanges, 2,
+ "one event should have been triggered immediately");
+
+ return waitForChangeTriggered();
+ }).then(function () {
+ assert.strictEqual(nbNotifyChanges, 2,
+ "no extra event should have been triggered");
+
+ form.destroy();
+ done();
+ });
+
+ function waitForChangeTriggered() {
+ return def.then(function () {
+ def = $.Deferred();
+ return concurrency.delay(0);
+ });
+ }
+ });
+
+ QUnit.module('JournalDashboardGraph');
+
+ QUnit.test('graph dashboard widget is rendered correctly', function (assert) {
+ assert.expect(2);
+
+ _.extend(this.data.partner.fields, {
+ graph_data: { string: "Graph Data", type: "text" },
+ graph_type: {
+ string: "Graph Type",
+ type: "selection",
+ selection: [['line', 'Line'], ['bar', 'Bar']]
+ },
+ });
+ this.data.partner.records[0].graph_type = "bar";
+ this.data.partner.records[1].graph_type = "line";
+ var graph_values = [
+ {'value': 300, 'label': '5-11 Dec'},
+ {'value': 500, 'label': '12-18 Dec'},
+ {'value': 100, 'label': '19-25 Dec'},
+ ];
+ this.data.partner.records[0].graph_data = JSON.stringify([{
+ color: 'red',
+ title: 'Partner 0',
+ values: graph_values,
+ key: 'A key',
+ area: true,
+ }]);
+ this.data.partner.records[1].graph_data = JSON.stringify([{
+ color: 'blue',
+ title: 'Partner 1',
+ values: graph_values,
+ key: 'A key',
+ area: true,
+ }]);
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="graph_type"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates></kanban>',
+ domain: [['id', 'in', [1, 2]]],
+ });
+
+ // nvd3 seems to do a setTimeout(0) each time the addGraph function is
+ // called, which is done twice in this case as there are 2 records.
+ // for that reason, we need to do two setTimeout(0) as well here to ensure
+ // that both graphs are rendered before starting to check if the rendering
+ // is correct.
+ var done = assert.async();
+ return concurrency.delay(0).then(function () {
+ return concurrency.delay(0);
+ }).then(function () {
+ assert.ok(kanban.$('.o_kanban_record:first() .o_graph_barchart').length,
+ "graph of first record should be a barchart");
+ assert.ok(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart').length,
+ "graph of second record should be a linechart");
+ kanban.destroy();
+ done();
+ });
+
+ });
+
+ QUnit.module('AceEditor');
+
+ QUnit.test('ace widget on text fields works', function (assert) {
+ assert.expect(3);
+ var done = assert.async();
+
+ assert.notOk('ace' in window, "the ace library should not be loaded");
+ this.data.partner.fields.foo.type = 'text';
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" widget="ace"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ assert.ok('ace' in window, "the ace library should now be loaded");
+ assert.ok(form.$('div.ace_content').length, "should have rendered something with ace editor");
+ delete window.require;
+ delete window.ace;
+ form.destroy();
+ done();
+ });
+ });
+
+ QUnit.module('HandleWidget');
+
+ QUnit.test('handle widget', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].p = [2, 4];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="sequence" widget="handle"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('td span.o_row_handle').text(), "",
+ "handle should not have any content");
+
+ assert.notOk(form.$('td span.o_row_handle').is(':visible'),
+ "handle should be invisible in readonly mode");
+
+ assert.strictEqual(form.$('span.o_row_handle').length, 2, "should have 2 handles");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(form.$('td:first').hasClass('o_readonly'),
+ "column should be displayed as readonly");
+
+ assert.ok(form.$('td:first').hasClass('o_handle_cell'),
+ "column widget should be displayed in css class");
+
+ assert.ok(form.$('td span.o_row_handle').is(':visible'),
+ "handle should be visible in readonly mode");
+
+ form.$('td').eq(1).click();
+ assert.strictEqual(form.$('td:first span.o_row_handle').length, 1,
+ "content of the cell should have been replaced");
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldDate');
+
+ QUnit.test('date field is empty if no date is set', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 4,
+ });
+ var $span = form.$('span.o_form_field');
+ assert.strictEqual($span.length, 1, "should have one span in the form view");
+ assert.strictEqual($span.text(), "", "and it should be empty");
+ form.destroy();
+ });
+
+ QUnit.test('date field in form view', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].date, '2017-02-22', 'the correct value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ }
+ });
+
+ assert.strictEqual(form.$('.o_form_field_date').text(), '02/03/2017',
+ 'the date should be correctly displayed in readonly');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ // click on the input and select another value
+ form.$('.o_datepicker_input').click();
+ assert.ok(form.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ form.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
+ form.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
+ form.$('.bootstrap-datetimepicker-widget .year:contains(2017)').click();
+ form.$('.bootstrap-datetimepicker-widget .month').eq(1).click(); // February
+ form.$('.day:contains(22)').click(); // select the 22 February
+ assert.ok(!form.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/22/2017',
+ 'the selected date should be displayed in the input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field_date').text(), '02/22/2017',
+ 'the selected date should be displayed after saving');
+ form.destroy();
+ });
+
+ QUnit.test('date field in editable list view', function (assert) {
+ assert.expect(8);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="date"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ }
+ });
+
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ assert.strictEqual($cell.text(), '02/03/2017',
+ 'the date should be displayed correctly in readonly');
+ $cell.click();
+
+ assert.strictEqual(list.$('input.o_datepicker_input').length, 1,
+ "the view should have a date input for editable mode");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement,
+ "date input should have the focus");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ // click on the input and select another value
+ list.$('input.o_datepicker_input').click();
+ assert.ok(list.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ list.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
+ list.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
+ list.$('.bootstrap-datetimepicker-widget .year:contains(2017)').click();
+ list.$('.bootstrap-datetimepicker-widget .month').eq(1).click(); // February
+ list.$('.day:contains(22)').click(); // select the 22 February
+ assert.ok(!list.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+ assert.strictEqual(list.$('.o_datepicker_input').val(), '02/22/2017',
+ 'the selected date should be displayed in the input');
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), '02/22/2017',
+ 'the selected date should be displayed after saving');
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldDatetime');
+
+ QUnit.test('datetime field in form view', function(assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="datetime"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ }
+ });
+
+ var expectedDateString = "02/08/2017 10:00:00";
+ assert.strictEqual(form.$('.o_form_field_date').text(), expectedDateString,
+ 'the datetime should be correctly displayed in readonly');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
+ 'the datetime should be correct in edit mode');
+ // click on the input and select 22 February at 8:23:33
+ form.$('.o_datepicker_input').click();
+ assert.ok(form.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ form.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
+ form.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
+ form.$('.bootstrap-datetimepicker-widget .year:contains(2017)').click();
+ form.$('.bootstrap-datetimepicker-widget .month').eq(3).click(); // April
+ form.$('.bootstrap-datetimepicker-widget .day:contains(22)').click();
+ form.$('.bootstrap-datetimepicker-widget .fa-clock-o').click();
+ form.$('.bootstrap-datetimepicker-widget .timepicker-hour').click();
+ form.$('.bootstrap-datetimepicker-widget .hour:contains(08)').click();
+ form.$('.bootstrap-datetimepicker-widget .timepicker-minute').click();
+ form.$('.bootstrap-datetimepicker-widget .minute:contains(25)').click();
+ form.$('.bootstrap-datetimepicker-widget .timepicker-second').click();
+ form.$('.bootstrap-datetimepicker-widget .second:contains(35)').click();
+ form.$('.bootstrap-datetimepicker-widget .fa-times').click(); // close
+ assert.ok(!form.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+
+ var newExpectedDateString = "04/22/2017 08:25:35";
+ assert.strictEqual(form.$('.o_datepicker_input').val(), newExpectedDateString,
+ 'the selected date should be displayed in the input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field_date').text(), newExpectedDateString,
+ 'the selected date should be displayed after saving');
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field in editable list view', function (assert) {
+ assert.expect(8);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="datetime"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ }
+ });
+
+ var expectedDateString = "02/08/2017 10:00:00";
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ assert.strictEqual($cell.text(), expectedDateString,
+ 'the datetime should be correctly displayed in readonly');
+
+ // switch to edit mode
+ $cell.click();
+ assert.strictEqual(list.$('input.o_datepicker_input').length, 1,
+ "the view should have a date input for editable mode");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement,
+ "date input should have the focus");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
+ 'the date should be correct in edit mode');
+
+ // click on the input and select 22 February at 8:23:33
+ list.$('input.o_datepicker_input').click();
+ assert.ok(list.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ list.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
+ list.$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
+ list.$('.bootstrap-datetimepicker-widget .year:contains(2017)').click();
+ list.$('.bootstrap-datetimepicker-widget .month').eq(3).click(); // April
+ list.$('.bootstrap-datetimepicker-widget .day:contains(22)').click();
+ list.$('.bootstrap-datetimepicker-widget .fa-clock-o').click();
+ list.$('.bootstrap-datetimepicker-widget .timepicker-hour').click();
+ list.$('.bootstrap-datetimepicker-widget .hour:contains(08)').click();
+ list.$('.bootstrap-datetimepicker-widget .timepicker-minute').click();
+ list.$('.bootstrap-datetimepicker-widget .minute:contains(25)').click();
+ list.$('.bootstrap-datetimepicker-widget .timepicker-second').click();
+ list.$('.bootstrap-datetimepicker-widget .second:contains(35)').click();
+ list.$('.bootstrap-datetimepicker-widget .fa-times').click(); // close
+ assert.ok(!list.$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+
+ var newExpectedDateString = "04/22/2017 08:25:35";
+ assert.strictEqual(list.$('.o_datepicker_input').val(), newExpectedDateString,
+ 'the selected datetime should be displayed in the input');
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), newExpectedDateString,
+ 'the selected datetime should be displayed after saving');
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldMonetary');
+
+ QUnit.test('monetary field in form view', function(assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_form_field').first().text(), '$\u00a09.10',
+ 'The value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_input').val(), '9.10',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('input.o_form_input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ form.$('input.o_form_input').val('108.2458938598598').trigger('input');
+ assert.strictEqual(form.$('input.o_form_input').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ form.$buttons.find('.o_form_button_save').click();
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_form_field').first().text(), '$\u00a0108.25',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field with currency symbol after', function(assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_form_field').first().text(), '0.00\u00a0€',
+ 'The value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_input').val(), '0.00',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('input.o_form_input').parent().children().eq(1).text(), '€',
+ 'The input should be followed by a span containing the currency symbol.');
+
+ form.$('input.o_form_input').val('108.2458938598598').trigger('input');
+ assert.strictEqual(form.$('input.o_form_input').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ form.$buttons.find('.o_form_button_save').click();
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_form_field').first().text(), '108.25\u00a0€',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field in editable list view', function (assert) {
+ assert.expect(9);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ var dollarValues = list.$('td').filter(function() {return _.str.include($(this).text(), '$')});
+ assert.strictEqual(dollarValues.length, 1,
+ 'Only one line has dollar as a currency.');
+
+ var euroValues = list.$('td').filter(function() {return _.str.include($(this).text(), '€')});
+ assert.strictEqual(euroValues.length, 1,
+ 'One one line has euro as a currency.');
+
+ var zeroValues = list.$('td').filter(function() {return _.str.include($(this).text(), '0.00')});
+ assert.strictEqual(zeroValues.length, 2,
+ 'Unset float values should be rendered as zeros.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)');
+ $cell.click();
+
+ assert.strictEqual($cell.children().length, 1,
+ 'The cell td should only contain the special div of monetary widget.');
+ assert.strictEqual(list.$('input.o_form_input').length, 1,
+ 'The view should have 1 input for editable monetary float.');
+ assert.strictEqual(list.$('input.o_form_input').val(), '9.10',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(list.$('input.o_form_input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ list.$('input.o_form_input').val('108.2458938598598').trigger('input');
+ assert.strictEqual(list.$('input.o_form_input').val(), '108.2458938598598',
+ 'The typed value should be correctly displayed.');
+
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)').text(), '$\u00a0108.25',
+ 'The new value should be rounded properly.');
+
+ list.destroy();
+ });
+
+ QUnit.test('monetary field with real monetary field in model', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.qux.type = "monetary";
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.qux = obj.bar ? 100 : obj.qux;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux"/>' +
+ '<field name="currency_id"/>' +
+ '<field name="bar"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ assert.strictEqual(form.$('.o_form_field_monetary').html(), "$&nbsp;9.10",
+ "readonly value should contain the currency");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_form_field_monetary > input').val(), "9.10",
+ "input value in edition should only contain the value, without the currency");
+
+ form.$('input[type="checkbox"]').click(); // Change the field on which the monetary depends
+ assert.strictEqual(form.$('.o_form_field_monetary > input').length, 1,
+ "After the onchange, the monetary <input/> should not have been duplicated");
+
+ var $dropdown = form.$('.o_form_field_many2one input').autocomplete('widget');
+ form.$('.o_form_field_many2one input').click();
+ $dropdown.find('li:not(.o_m2o_dropdown_option):last').mouseenter().click();
+ assert.strictEqual(form.$('.o_form_field_monetary > span').html(), "€",
+ "After currency change, the monetary field currency should have been updated");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldInteger');
+
+ QUnit.test('integer field when unset', function(assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="int_field"/></form>',
+ res_id: 4,
+ });
+
+ assert.ok(form.$('.o_form_field').hasClass('o_form_field_empty'),
+ 'Non-set integer field should be recognized as unset.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field in form view', function(assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="int_field"/></form>',
+ res_id: 2,
+ });
+
+ assert.ok(!form.$('.o_form_field').hasClass('o_form_field_empty'),
+ 'Integer field should be considered set for value 0.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_input').val(), '0',
+ 'The value should be rendered correctly in edit mode.')
+
+ form.$('input.o_form_input').val('-18').trigger('input');
+ assert.strictEqual(form.$('input.o_form_input').val(), '-18',
+ 'The value should be correctly displayed in the input.')
+
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field').text(), '-18',
+ 'The new value should be saved and displayed properly.')
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field in editable list view', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ });
+
+ var zeroValues = list.$('td').filter(function() {return $(this).text() === '0'});
+ assert.strictEqual(zeroValues.length, 1,
+ 'Unset integer values should not be rendered as zeros.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ $cell.click();
+
+ assert.strictEqual(list.$('input.o_form_input').length, 1,
+ 'The view should have 1 input for editable integer.')
+
+ list.$('input.o_form_input').val('-28').trigger('input');
+ assert.strictEqual(list.$('input.o_form_input').val(), '-28',
+ 'The value should be displayed properly in the input.')
+
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), '-28',
+ 'The new value should be saved and displayed properly.')
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldFloatTime');
+
+ QUnit.test('float_time field in form view', function(assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float_time"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 48 / 60 = 0.8
+ assert.strictEqual(args.args[1].qux, -11.8, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 5,
+ });
+
+ // 9 + 0.1 * 60 = 9.06
+ assert.strictEqual(form.$('.o_form_field').first().text(), '09:06',
+ 'The formatted time value should be displayed properly.')
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_input').val(), '09:06',
+ 'The value should be rendered correctly in the input.')
+
+ form.$('input.o_form_input').val('-11:48').trigger('input');
+ assert.strictEqual(form.$('input.o_form_input').val(), '-11:48',
+ 'The new value should be displayed properly in the input.')
+
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field').first().text(), '-11:48',
+ 'The new value should be saved and displayed properly.')
+
+ form.destroy();
+ });
+
+
+ QUnit.module('PhoneWidget');
+
+ QUnit.test('phone field in form view on extra small screens', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ config: {
+ device: {
+ size_class: 0, // Screen XS
+ SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
+ }
+ },
+ });
+
+ var $phoneLink = form.$('a.o_form_uri.o_form_field.o_text_overflow');
+ assert.strictEqual($phoneLink.length, 1,
+ "should have a anchor with correct classes");
+ assert.strictEqual($phoneLink.text(), 'y\u00ADop',
+ "value should be displayed properly as text with the skype obfuscation");
+ assert.strictEqual($phoneLink.attr('href'), 'tel:yop',
+ "should have proper tel prefix");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').length, 1,
+ "should have an input for the phone field");
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ form.$('input[type="text"].o_form_input.o_form_field').val('new').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ $phoneLink = form.$('a.o_form_uri.o_form_field.o_text_overflow');
+ assert.strictEqual($phoneLink.text(), 'n\u00ADew',
+ "new value should be displayed properly as text with the skype obfuscation");
+ assert.strictEqual($phoneLink.attr('href'), 'tel:new',
+ "should still have proper tel prefix");
+
+ form.destroy();
+ });
+
+ QUnit.test('phone field in editable list view on extra small screens', function(assert) {
+ assert.expect(10);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
+ config: {
+ device: {
+ size_class: 0, // Screen XS
+ SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
+ }
+ },
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'y\u00ADop',
+ "value should be displayed properly as text with the skype obfuscation");
+
+ var $phoneLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($phoneLink.length, 5,
+ "should have anchors with correct classes");
+ assert.strictEqual($phoneLink.first().attr('href'), 'tel:yop',
+ "should have proper tel prefix");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ $cell.find('input').val('new').trigger('input');;
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'n\u00ADew',
+ "value should be properly updated");
+ $phoneLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($phoneLink.length, 5,
+ "should still have anchors with correct classes");
+ assert.strictEqual($phoneLink.first().attr('href'), 'tel:new',
+ "should still have proper tel prefix");
+
+ list.destroy();
+ });
+
+ QUnit.test('phone field in form view on normal screens', function (assert) {
+ // The behavior of this widget is completely altered by crm_voip so this
+ // test is irrelevant and fails if crm_voip is installed. The enterprise
+ // module is responsible for testing its own behavior in its own tests.
+ if ('crm.voip' in odoo.__DEBUG__.services) {
+ assert.expect(0);
+ return;
+ }
+
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ config: {
+ device: {
+ size_class: 1, // Screen SM
+ SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
+ }
+ },
+ });
+
+ var $phone = form.$('span.o_form_field.o_text_overflow:not(.o_form_uri)');
+ assert.strictEqual($phone.length, 1,
+ "should have a simple span rather than a link");
+ assert.strictEqual($phone.text(), 'yop',
+ "value should be displayed properly as text without skype obfuscation");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').length, 1,
+ "should have an input for the phone field");
+ assert.strictEqual(form.$('input[type="text"].o_form_input.o_form_field').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ form.$('input[type="text"].o_form_input.o_form_field').val('new').trigger('input');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('span.o_form_field.o_text_overflow:not(.o_form_uri)').text(), 'new',
+ "new value should be displayed properly as text without skype obfuscation");
+
+ form.destroy();
+ });
+
+ QUnit.test('phone field in editable list view on normal screens', function(assert) {
+ // The behavior of this widget is completely altered by crm_voip so this
+ // test is irrelevant and fails if crm_voip is installed. The enterprise
+ // module is responsible for testing its own behavior in its own tests.
+ if ('crm.voip' in odoo.__DEBUG__.services) {
+ assert.expect(0);
+ return;
+ }
+
+ assert.expect(8);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
+ config: {
+ device: {
+ size_class: 1, // Screen SM
+ SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 },
+ }
+ },
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text without skype obfuscation");
+
+ assert.strictEqual(list.$('span.o_field_widget.o_text_overflow:not(.o_form_uri)').length, 5,
+ "should have spans with correct classes");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.ok($cell.hasClass('o_edit_mode'), 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ $cell.find('input').val('new').trigger('input');;
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.ok(!$cell.hasClass('o_edit_mode'), 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new',
+ "value should be properly updated");
+ assert.strictEqual(list.$('span.o_field_widget.o_text_overflow:not(.o_form_uri)').length, 5,
+ "should still have spans with correct classes");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('PriorityWidget');
+
+ QUnit.test('priority widget when not set', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="priority"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_form_field.o_priority:not(.o_form_field_empty)').length, 1,
+ "widget should be considered set, even though there is no value for this field");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 0,
+ "should have no full star since there is no value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 2,
+ "should have two empty stars since there is no value");
+
+ form.destroy();
+ });
+
+ QUnit.test('priority widget in form view', function (assert) {
+ assert.expect(22);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="priority"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field.o_priority:not(.o_form_field_empty)').length, 1,
+ "widget should be considered set");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // hover last star
+ form.$('.o_form_field.o_priority a.o_priority_star.fa-star-o').last().trigger('mouseover');
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should temporary have two full stars since we are hovering the third value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should temporary have no empty star since we are hovering the third value");
+
+ // Here we should test with mouseout, but currently the effect associated with it
+ // occurs in a setTimeout after 200ms so it's not trivial to test it here.
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // switch to edit mode to check that the new value was properly written
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // click on the second star in edit mode
+ form.$('.o_form_field.o_priority a.o_priority_star.fa-star-o').last().click();
+
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(form.$('.o_form_field.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ form.destroy();
+ });
+
+ QUnit.test('priority widget in editable list view', function(assert) {
+ assert.expect(25);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="selection" widget="priority"/></tree>',
+ });
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority:not(.o_form_field_empty)').length, 1,
+ "widget should be considered set");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // Here we should test with mouseout, but currently the effect associated with it
+ // occurs in a setTimeout after 200ms so it's not trivial to test it here.
+
+ // switch to edit mode and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // hover last star
+ list.$('.o_data_row .o_priority a.o_priority_star.fa-star-o').first().trigger('mouseenter');
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star').length, 2,
+ "should temporary have two full stars since we are hovering the third value");
+ assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star-o').length, 0,
+ "should temporary have no empty star since we are hovering the third value");
+
+ // click on the first star in readonly mode
+ list.$('.o_priority a.o_priority_star.fa-star').first().click();
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0,
+ "should now have no full star since the value is the first value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2,
+ "should now have two empty stars since the value is the first value");
+
+ // re-enter edit mode to force re-rendering the widget to check if the value was correctly saved
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ $cell.click();
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0,
+ "should now only have no full star since the value is the first value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2,
+ "should now have two empty stars since the value is the first value");
+
+ // Click on second star in edit mode
+ list.$('.o_priority a.o_priority_star.fa-star-o').last().click();
+
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('StateSelection Widget');
+
+ QUnit.test('state_selection widget in form view', function (assert) {
+ assert.expect(21);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="state_selection"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_red').length, 1,
+ "should have one red status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_green').length, 0,
+ "should not have one green status since selection is the second, blocked state");
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ form.$('.o_form_field.o_selection .o_status').first().click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 1,
+ "there should be a dropdown");
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible li').length, 2,
+ "there should be two options in the dropdown");
+
+ // Click on the first option, "Normal"
+ form.$('ul.dropdown-menu.state:visible li').first().click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown anymore");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_red').length, 0,
+ "should not have one red status since selection is the first, normal state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_green').length, 0,
+ "should not have one green status since selection is the first, normal state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status').length, 1,
+ "should have one grey status since selection is the first, normal state");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should still not be a dropdown");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_red').length, 0,
+ "should still not have one red status since selection is the first, normal state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_green').length, 0,
+ "should still not have one green status since selection is the first, normal state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status').length, 1,
+ "should still have one grey status since selection is the first, normal state");
+
+ // Click on the status button to make the dropdown appear
+ form.$('.o_form_field.o_selection .o_status').first().click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 1,
+ "there should be a dropdown");
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible li').length, 2,
+ "there should be two options in the dropdown");
+
+ // Click on the last option, "Done"
+ form.$('ul.dropdown-menu.state:visible li').last().click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown anymore");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_red').length, 0,
+ "should not have one red status since selection is the third, done state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_green').length, 1,
+ "should have one green status since selection is the third, done state");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should still not be a dropdown anymore");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_red').length, 0,
+ "should still not have one red status since selection is the third, done state");
+ assert.strictEqual(form.$('.o_form_field.o_selection > a span.o_status.o_status_green').length, 1,
+ "should still have one green status since selection is the third, done state");
+
+ form.destroy();
+ });
+
+ QUnit.test('state_selection widget in editable list view', function(assert) {
+ assert.expect(34);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="selection" widget="state_selection"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
+ "should have five status selection widgets");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 1,
+ "should have one red status");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_green').length, 1,
+ "should have one green status");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ var $cell = list.$('tbody td.o_state_selection_cell').first();
+ list.$('.o_state_selection_cell .o_selection > a span.o_status').first().click();
+ assert.ok(!$cell.hasClass('o_edit_mode'),
+ 'should not be in edit mode since we clicked on the widget and not the cell itself');
+ assert.ok(!$cell.siblings().hasClass('o_edit_mode'),
+ 'siblings should not be in edit mode since we clicked on the widget and not the cell itself');
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 1,
+ "there should be a dropdown");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible li').length, 2,
+ "there should be two options in the dropdown");
+
+ // Click on the first option, "Normal"
+ list.$('ul.dropdown-menu.state:visible li').first().click();
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
+ "should still have five status selection widgets");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 0,
+ "should now have no red status");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_green').length, 1,
+ "should still have one green status");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ // switch to edit mode and check the result
+ $cell = list.$('tbody td.o_state_selection_cell').first();
+ $cell.click();
+ assert.ok(!$cell.hasClass('o_edit_mode'),
+ 'should still not be in edit mode since widget is readonly');
+ assert.ok($cell.siblings().hasClass('o_edit_mode'),
+ 'siblings should be in edit mode since we clicked on the cell and not the widget itself');
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
+ "should still have five status selection widgets");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 0,
+ "should now have no red status");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_green').length, 1,
+ "should still have one green status");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ list.$('.o_state_selection_cell .o_selection > a span.o_status').first().click();
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 1,
+ "there should be a dropdown");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible li').length, 2,
+ "there should be two options in the dropdown");
+
+ // Click on another row
+ var $firstCell = list.$('tbody td.o_state_selection_cell').first();
+ var $lastCell = list.$('tbody td.o_state_selection_cell').last();
+ $lastCell.click();
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown anymore");
+ assert.ok(!$firstCell.siblings().hasClass('o_edit_mode'),
+ 'first row should not be in edit mode anymore');
+ assert.ok($lastCell.siblings().hasClass('o_edit_mode'),
+ 'last row should be in edit mode');
+
+ // Click on the last status button to make the dropdown appear
+ list.$('.o_state_selection_cell .o_selection > a span.o_status').last().click();
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 1,
+ "there should be a dropdown");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible li').length, 2,
+ "there should be two options in the dropdown");
+
+ // Click on the last option, "Done"
+ list.$('ul.dropdown-menu.state:visible li').last().click();
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown anymore");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
+ "should still have five status selection widgets");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 0,
+ "should still have no red status");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_green').length, 2,
+ "should now have two green status");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ // save
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status').length, 5,
+ "should have five status selection widgets");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_red').length, 0,
+ "should have no red status");
+ assert.strictEqual(list.$('.o_state_selection_cell .o_selection > a span.o_status.o_status_green').length, 2,
+ "should have two green status");
+ assert.strictEqual(list.$('ul.dropdown-menu.state:visible').length, 0,
+ "there should not be a dropdown");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('LabelSelectionWidget');
+
+ QUnit.test('label_selection widget in form view', function (assert) {
+ assert.expect(12);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="label_selection" ' +
+ ' options="{\'classes\': {\'normal\': \'default\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').length, 1,
+ "should have a warning status label since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-default').length, 0,
+ "should not have a default status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-success').length, 0,
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ // // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').length, 1,
+ "should have a warning status label since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-default').length, 0,
+ "should not have a default status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-success').length, 0,
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').length, 1,
+ "should have a warning status label since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-default').length, 0,
+ "should not have a default status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-success').length, 0,
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_form_field.label.label-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ form.destroy();
+ });
+
+ QUnit.test('label_selection widget in editable list view', function(assert) {
+ assert.expect(21);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="selection" widget="label_selection"' +
+ ' options="{\'classes\': {\'normal\': \'default\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('.o_field_widget.label:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').length, 1,
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').length, 1,
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').length, 1,
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ // switch to edit mode and check the result
+ list.$('tbody td:not(.o_list_record_selector)').first().click();
+ assert.strictEqual(list.$('.o_field_widget.label:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').length, 1,
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').length, 1,
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').length, 1,
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ // save and check the result
+ list.$buttons.find('.o_list_button_save').click();
+ assert.strictEqual(list.$('.o_field_widget.label:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').length, 1,
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').length, 1,
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-default').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').length, 1,
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.label.label-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('StatInfo');
+
+ QUnit.test('statinfo widget in form view', function (assert) {
+ assert.expect(9);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field name="int_field" widget="statinfo"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ form.destroy();
+ });
+
+ QUnit.test('statinfo widget in form view with specific label_field', function (assert) {
+ assert.expect(9);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field string="Useful stat button" name="int_field" widget="statinfo" ' +
+ 'options="{\'label_field\': \'foo\'}"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ form.destroy();
+ });
+
+ QUnit.test('statinfo widget in form view with no label', function (assert) {
+ assert.expect(9);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field string="Useful stat button" name="int_field" widget="statinfo" ' +
+ 'options="{\'nolabel\': \'true\'}"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info').length, 1,
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_form_field.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ form.destroy();
+ });
+
+
+ QUnit.module('PercentPie');
+
+ QUnit.test('percentpie widget in form view with value < 50%', function (assert) {
+ assert.expect(12);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field" widget="percentpie"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ form.destroy();
+ });
+
+ QUnit.test('percentpie widget in form view with value > 50%', function (assert) {
+ assert.expect(12);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field" widget="percentpie"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 3,
+ });
+
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.ok(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().hasClass('o_full'),
+ "right mask should be hidden since the value > 50%");
+
+ // switch to edit mode and check the result
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.ok(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().hasClass('o_full'),
+ "right mask should be hidden since the value > 50%");
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie').length, 1,
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.ok(form.$('.o_form_field_percent_pie.o_form_field .o_pie .o_mask').last().hasClass('o_full'),
+ "right mask should be hidden since the value > 50%");
+
+ form.destroy();
+ });
+
+ // TODO: This test would pass without any issue since all the classes and
+ // custom style attributes are correctly set on the widget in list
+ // view, but since the less itself for this widget currently only
+ // applies inside the form view, the widget is unusable. This test can
+ // be uncommented when we refactor the less files so that this widget
+ // stylesheet applies in both form and list view.
+ // QUnit.test('percentpie widget in editable list view', function(assert) {
+ // assert.expect(10);
+ //
+ // var list = createView({
+ // View: ListView,
+ // model: 'partner',
+ // data: this.data,
+ // arch: '<tree editable="bottom">' +
+ // '<field name="foo"/>' +
+ // '<field name="int_field" widget="percentpie"/>' +
+ // '</tree>',
+ // });
+ //
+ // assert.strictEqual(list.$('.o_form_field_percent_pie .o_pie').length, 5,
+ // "should have five pie charts");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole left side of the pie");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // // switch to edit mode and check the result
+ // list.$('tbody td:not(.o_list_record_selector)').first().click();
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // // save
+ // list.$buttons.find('.o_list_button_save').click();
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie");
+ // assert.strictEqual(list.$('.o_form_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // list.destroy();
+ // });
+
+
+ QUnit.module('FieldDomain');
+
+ QUnit.test('basic domain field usage is ok', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].foo = "[]";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // As the domain is empty, there should be a button to add the first
+ // domain part
+ var $domain = form.$(".o_form_field_domain");
+ var $domainAddFirstNodeButton = $domain.find(".o_domain_add_first_node_button");
+ assert.equal($domainAddFirstNodeButton.length, 1,
+ "there should be a button to create first domain element");
+
+ // Clicking on the button should add the [["id", "=", "1"]] domain, so
+ // there should be a field selector in the DOM
+ $domainAddFirstNodeButton.click();
+ var $fieldSelector = $domain.find(".o_field_selector");
+ assert.equal($fieldSelector.length, 1,
+ "there should be a field selector");
+
+ // Focusing the field selector input should open the field selector
+ // popover
+ $fieldSelector.trigger('focusin');
+ var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // The popover should contain the list of partner_type fields and so
+ // there should be the "Color index" field
+ var $lis = $fieldSelectorPopover.find("li");
+ var $colorIndex = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Color index") >= 0) {
+ $colorIndex = $li;
+ }
+ });
+ assert.equal($colorIndex.length, 1,
+ "field selector popover should contain 'Color index' field");
+
+ // Clicking on this field should close the popover, then changing the
+ // associated value should reveal one matched record
+ $colorIndex.click();
+ $domain.find(".o_domain_leaf_value_input").val(2).change();
+ assert.equal($domain.find(".o_domain_show_selection_button").text().trim().substr(0, 2), "1 ",
+ "changing color value to 2 should reveal only one record");
+
+ // Saving the form view should show a readonly domain containing the
+ // "color" field
+ form.$buttons.find('.o_form_button_save').click();
+ $domain = form.$(".o_form_field_domain");
+ assert.ok($domain.html().indexOf("Color index") >= 0,
+ "field selector readonly value should now contain 'Color index'");
+ form.destroy();
+ });
+
+ QUnit.test('domain field is correctly reset on every view change', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].foo = '[["id","=",1]]';
+ this.data.partner.fields.bar.type = "char";
+ this.data.partner.records[0].bar = "product";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="bar"/>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // As the domain is equal to [["id", "=", 1]] there should be a field
+ // selector to change this
+ var $domain = form.$(".o_form_field_domain");
+ var $fieldSelector = $domain.find(".o_field_selector");
+ assert.equal($fieldSelector.length, 1,
+ "there should be a field selector");
+
+ // Focusing its input should open the field selector popover
+ $fieldSelector.trigger('focusin');
+ var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // As the value of the "bar" field is "product", the field selector
+ // popover should contain the list of "product" fields
+ var $lis = $fieldSelectorPopover.find("li");
+ var $sampleLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Product Name") >= 0) {
+ $sampleLi = $li;
+ }
+ });
+ assert.strictEqual($lis.length, 1,
+ "field selector popover should contain only one field");
+ assert.strictEqual($sampleLi.length, 1,
+ "field selector popover should contain 'Product Name' field");
+
+ // Now change the value of the "bar" field to "partner_type"
+ form.$(".o_field_widget.o_form_input").click().val("partner_type").trigger("input");
+
+ // Refocusing the field selector input should open the popover again
+ $fieldSelector = form.$(".o_field_selector");
+ $fieldSelector.trigger('focusin');
+ $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // Now the list of fields should be the ones of the "partner_type" model
+ $lis = $fieldSelectorPopover.find("li");
+ $sampleLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Color index") >= 0) {
+ $sampleLi = $li;
+ }
+ });
+ assert.strictEqual($lis.length, 2,
+ "field selector popover should contain two fields");
+ assert.strictEqual($sampleLi.length, 1,
+ "field selector popover should contain 'Color index' field");
+ form.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
new file mode 100644
index 00000000000..5b3eb267714
--- /dev/null
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -0,0 +1,81 @@
+odoo.define('web.field_utils_tests', function (require) {
+"use strict";
+
+var core = require('web.core');
+var fieldUtils = require('web.field_utils');
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('field_utils');
+
+QUnit.test('format integer', function(assert) {
+ assert.expect(5);
+
+ var originalGrouping = core._t.database.parameters.grouping;
+
+ core._t.database.parameters.grouping = [3, 3, 3, 3];
+ assert.strictEqual(fieldUtils.format.integer(1000000), '1,000,000');
+
+ core._t.database.parameters.grouping = [3, 2, -1];
+ assert.strictEqual(fieldUtils.format.integer(106500), '1,06,500');
+
+ core._t.database.parameters.grouping = [1, 2, -1];
+ assert.strictEqual(fieldUtils.format.integer(106500), '106,50,0');
+
+ assert.strictEqual(fieldUtils.format.integer(0), "0");
+ assert.strictEqual(fieldUtils.format.integer(undefined), "");
+
+ core._t.database.parameters.grouping = originalGrouping;
+});
+
+QUnit.test('format float', function(assert) {
+ assert.expect(4);
+
+ var originalParameters = $.extend(true, {}, core._t.database.parameters);
+
+ core._t.database.parameters.grouping = [3, 3, 3, 3];
+ assert.strictEqual(fieldUtils.format.float(1000000), '1,000,000.00');
+
+ core._t.database.parameters.grouping = [3, 2, -1];
+ assert.strictEqual(fieldUtils.format.float(106500), '1,06,500.00');
+
+ core._t.database.parameters.grouping = [1, 2, -1];
+ assert.strictEqual(fieldUtils.format.float(106500), '106,50,0.00');
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+ assert.strictEqual(fieldUtils.format.float(6000), '6.000,00');
+
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test("format_datetime", function (assert) {
+ assert.expect(1);
+
+ var date_string = "2009-05-04 12:34:23";
+ var date = fieldUtils.parse.datetime(date_string);
+ var str = fieldUtils.format.datetime(date);
+ assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss"));
+});
+
+QUnit.test("format_many2one", function (assert) {
+ assert.expect(2);
+
+ assert.strictEqual('', fieldUtils.format.many2one(null));
+ assert.strictEqual('A M2O value', fieldUtils.format.many2one({
+ data: { display_name: 'A M2O value' },
+ }));
+});
+
+
+QUnit.test('formatting chars', function(assert) {
+ assert.expect(1);
+
+ assert.strictEqual(fieldUtils.format.char(), '',
+ "undefined char should be formatted as an empty string");
+});
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
new file mode 100644
index 00000000000..86e569fdbb5
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -0,0 +1,3742 @@
+odoo.define('web.relational_fields_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('relational_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ bar: {string: "Bar", type: "boolean", default: true},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1] },
+ p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'},
+ turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle'},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
+ product_id: {string: "Product", type: "many2one", relation: 'product'},
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ },
+ date: {string: "Some Date", type: "date"},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ user_id: {string: "User", type: 'many2one', relation: 'user'},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44,
+ p: [],
+ turtles: [2],
+ timmy: [],
+ trululu: 4,
+ user_id: 17,
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ qux: 13,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ product_id: 37,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ bar: false,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char"},
+ color: {string: "Color index", type: "int"},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ turtle_bar: {string: "Bar", type: "boolean", default: true},
+ turtle_int: {string: "int", type: "integer", sortable: true},
+ turtle_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5},
+ turtle_description: {string: "Description", type: "text"},
+ turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
+ partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_bar: true,
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_bar: true,
+ turtle_foo: "blip",
+ turtle_int: 9,
+ partner_ids: [2,4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ turtle_bar: false,
+ turtle_foo: "kawa",
+ turtle_int: 21,
+ turtle_qux: 9.8,
+ partner_ids: [],
+ }],
+ },
+ user: {
+ fields: {
+ name: {string: "Name", type: "char"}
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('FieldMany2One');
+
+ QUnit.test('many2ones in form views', function (assert) {
+ assert.expect(4);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_action') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_action with correct id");
+ return $.when({
+ res_id: 17,
+ type: 'ir.actions.act_window',
+ target: 'current',
+ res_model: 'res.partner'
+ });
+ }
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ testUtils.intercept(form, 'do_action', function (event) {
+ assert.strictEqual(event.data.action.res_id, 17,
+ "should do a do_action with correct parameters");
+ });
+ testUtils.intercept(form, 'load_views', function (event) {
+ // temporarily prevent the fields_view from being loaded (to prevent a crash)
+ // TODO: specify a fields_view and test that we can edit the record in the dialog
+ event.stopPropagation();
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri:contains(aaa)').length, 1,
+ "should contain a link");
+ form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action)
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('.o_external_button').click(); // click on the external button (should do an RPC)
+
+ // TODO: test that we can edit the record in the dialog, and that the value is correctly
+ // updated on close
+ form.destroy();
+ });
+
+ QUnit.test('many2one readonly fields with option "no_open"', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu" options="{&quot;no_open&quot;: True}" />' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri').length, 0, "should not have an anchor");
+ form.destroy();
+ });
+
+ QUnit.test('many2one in edit mode', function (assert) {
+ assert.expect(16);
+
+ // create 10 partners to have the 'Search More' option in the autocomplete dropdown
+ for (var i=0; i<10; i++) {
+ var id = 20 + i;
+ this.data.partner.records.push({id: id, display_name: "Partner " + id});
+ }
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree string="Partners"><field name="display_name"/></tree>',
+ 'partner,false,search': '<search string="Partners">' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].trululu, 20, "should write the correct id");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // the SelectCreateDialog requests the session, so intercept its custom
+ // event to specify a fake session to prevent it from crashing
+ testUtils.intercept(form, 'get_session', function (event) {
+ event.data.callback({user_context: {}});
+ });
+
+ form._toEditMode();
+ var $dropdown = form.$('.o_form_field_many2one input').autocomplete('widget');
+
+ form.$('.o_form_field_many2one input').click();
+ assert.ok($dropdown.is(':visible'),
+ 'clicking on the m2o input should open the dropdown if it is not open yet');
+ assert.strictEqual($dropdown.find('li:not(.o_m2o_dropdown_option)').length, 7,
+ 'autocomplete should contains 7 suggestions');
+ assert.strictEqual($dropdown.find('li.o_m2o_dropdown_option').length, 2,
+ 'autocomplete should contain "Search More" and Create and Edit..."');
+
+ form.$('.o_form_field_many2one input').click();
+ assert.ok(!$dropdown.is(':visible'),
+ 'clicking on the m2o input should close the dropdown if it is open');
+
+ // change the value of the m2o with a suggestion of the dropdown
+ form.$('.o_form_field_many2one input').click();
+ $dropdown.find('li:first()').click();
+ assert.ok(!$dropdown.is(':visible'), 'clicking on a value should close the dropdown');
+ assert.strictEqual(form.$('.o_form_field_many2one input').val(), 'first record',
+ 'value of the m2o should have been correctly updated');
+
+ // change the value of the m2o with a record in the 'Search More' modal
+ form.$('.o_form_field_many2one input').click();
+ // click on 'Search More' (mouseenter required by ui-autocomplete)
+ $dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click();
+ assert.ok($('.modal .o_list_view').length, "should have opened a list view in a modal");
+ assert.ok(!$('.modal .o_list_view .o_list_record_selector').length,
+ "there should be no record selector in the list view");
+ assert.ok(!$('.modal .modal-footer .o_select_button').length,
+ "there should be no 'Select' button in the footer");
+ assert.ok($('.modal tbody tr').length > 10, "list should contain more than 10 records");
+ // filter the list using the searchview
+ $('.modal .o_searchview_input').trigger({type: 'keypress', which: 80}); // P
+ $('.modal .o_searchview_input').trigger({type: 'keydown', which: 13}); // enter
+ assert.strictEqual($('.modal tbody tr').length, 10,
+ "list should be restricted to records containing a P (10 records)");
+ // choose a record
+ $('.modal tbody tr:contains(Partner 20)').click(); // choose record 'Partner 20'
+ assert.ok(!$('.modal').length, "should have closed the modal");
+ assert.ok(!$dropdown.is(':visible'), 'should have closed the dropdown');
+ assert.strictEqual(form.$('.o_form_field_many2one input').val(), 'Partner 20',
+ 'value of the m2o should have been correctly updated');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('a.o_form_uri').text(), 'Partner 20',
+ "should display correct value after save");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one field with option always_reload', function (assert) {
+ assert.expect(4);
+ var count = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ count++;
+ return $.when([[1, "first record\nand some address"]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(count, 1, "an extra name_get should have been done");
+ assert.ok(form.$('a:contains(and some address)').length,
+ "should display additional result");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('input').val(), "first record",
+ "actual field value should be displayed to be edited");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.ok(form.$('a:contains(and some address)').length,
+ "should still display additional result");
+ form.destroy();
+ });
+
+ // QUnit.test('onchange on a many2one to a different model', function (assert) {
+ // This test is commented because the mock server does not give the correct response.
+ // It should return a couple [id, display_name], but I don't know the logic used
+ // by the server, so it's hard to emulate it correctly
+ // assert.expect(2);
+
+ // this.data.partner.records[0].product_id = 41;
+ // this.data.partner.onchanges = {
+ // foo: function(obj) {
+ // obj.product_id = 37;
+ // },
+ // };
+
+ // var form = createView({
+ // View: FormView,
+ // model: 'partner',
+ // data: this.data,
+ // arch: '<form>' +
+ // '<field name="foo"/>' +
+ // '<field name="product_id"/>' +
+ // '</form>',
+ // res_id: 1,
+ // });
+ // form.$buttons.find('.o_form_button_edit').click();
+ // assert.strictEqual(form.$('input').eq(1).val(), 'xpad', "initial product_id val should be xpad");
+
+ // form.$('input').eq(0).val("let us trigger an onchange").trigger('input');
+
+ // assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied");
+ // });
+
+ QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ domain: [['trululu', '=', 4]]
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [], "should not have a domain");
+ }
+ return this._super(route, args);
+ }
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('input').click();
+ form.destroy();
+ });
+
+ QUnit.test('autocompletion in a many2one, in form view with a date field', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="date"/>' +
+ '<field name="trululu" domain="[(\'bar\',\'=\',True)]"/>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [["bar", "=", true]], "should not have a domain");
+ }
+ return this._super(route, args);
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('input:eq(2)').click();
+ form.destroy();
+ });
+
+ QUnit.test('creating record with many2one with option always_reload', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.trululu.default = 1;
+ this.data.partner.onchanges = {
+ trululu: function (obj) {
+ obj.trululu = 2; //[2, "second record"];
+ },
+ };
+
+ var count = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ count++;
+ if (args.method === 'name_get' && args.args[0][0] === 2) {
+ return $.when([[2, "hello world\nso much noise"]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(count, 3, "should have done 3 rpcs (default_get, onchange, name_get)");
+ assert.strictEqual(form.$('input').val(), 'hello world',
+ "should have taken the correct display name");
+ form.destroy();
+ });
+
+ QUnit.test('selecting a many2one, then discarding', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty');
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('.o_form_field_many2one input').click();
+ form.$('.o_form_field_many2one input').autocomplete('widget').find('a').first().click();
+
+
+ assert.strictEqual(form.$('input').val(), "xphone", "should have selected xphone");
+
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty');
+ form.destroy();
+ });
+
+ QUnit.test('domain and context are correctly used when doing a name_search in a m2o', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="product_id" ' +
+ 'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' +
+ 'context="{\'hello\': \'world\', \'test\': foo}"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ session: {user_context: {hey: "ho"}},
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/name_search') {
+ assert.deepEqual(
+ args.kwargs.args,
+ [['foo', '=', 'bar'], ['foo', '=', 'yop']],
+ 'the field attr domain should have been used for the RPC (and evaluated)');
+ assert.deepEqual(
+ args.kwargs.context,
+ {hey: "ho", hello: "world", test: "yop"},
+ 'the field attr context should have been used for the '
+ + 'RPC (evaluated and merged with the session one)');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').first().click();
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldOne2Many');
+
+ QUnit.test('one2many basic properties', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+
+ assert.strictEqual(form.$('td.o_list_record_selector').length, 0,
+ "embedded one2many should not have a selector");
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ "embedded one2many should not be editable");
+ assert.ok(!form.$('td.o_list_record_delete').length,
+ "embedded one2many records should not have a trash icon");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(form.$('.o_form_field_x2many_list_row_add').length,
+ "embedded one2many should now be editable");
+
+ assert.strictEqual(form.$('.o_form_field_x2many_list_row_add').attr('colspan'), "2",
+ "should have colspan 2 (one for field foo, one for being below trash icon)");
+
+ assert.ok(form.$('td.o_list_record_delete').length,
+ "embedded one2many records should have a trash icon");
+ form.destroy();
+ });
+
+ QUnit.test('one2many with date and datetime', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="date"/>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ // FIXME: this test is locale dependant. we need to do it right.
+ assert.strictEqual(form.$('td:contains(01/25/2017)').length, 1,
+ "should have formatted the date");
+ assert.strictEqual(form.$('td:contains(12/12/2016 10:55:05)').length, 1,
+ "should have formatted the datetime");
+ form.destroy();
+ });
+
+ QUnit.test('rendering with embedded one2many', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('th:contains(Foo)').length, 1,
+ "embedded one2many should have a column titled according to foo");
+ assert.strictEqual(form.$('td:contains(blip)').length, 1,
+ "embedded one2many should have a cell with relational value");
+ form.destroy();
+ });
+
+ QUnit.test('embedded one2many with widget', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="int_field" widget="handle"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('span.o_row_handle').length, 1, "should have 1 handles");
+ form.destroy();
+ });
+
+ QUnit.test('one2many field when using the pager', function (assert) {
+ assert.expect(13);
+
+ var ids = [];
+ for (var i=0; i<45; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.partner.records.push({
+ id: id,
+ display_name: "relational record " + id,
+ });
+ }
+ this.data.partner.records[0].p = ids.slice(0, 42);
+ this.data.partner.records[1].p = ids.slice(42);
+
+ var count = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><t t-esc="record.display_name"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ mockRPC: function () {
+ count++;
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ // we are on record 1, which has 90 related record (first 40 should be
+ // displayed), 2 RPCs (read) should have been done, one on the main record
+ // and one for the O2M
+ assert.strictEqual(count, 2, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'one2many kanban should contain 40 cards for record 1');
+
+ // move to record 2, which has 3 related records (and shouldn't contain the
+ // related records of record 1 anymore). Two additional RPCs should have
+ // been done
+ form.pager.next();
+ assert.strictEqual(count, 4, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3,
+ 'one2many kanban should contain 3 cards for record 2');
+
+ // move back to record 1, which should contain again its first 40 related
+ // records
+ form.pager.previous();
+ assert.strictEqual(count, 6, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'one2many kanban should contain 40 cards for record 1');
+
+ // move to the second page of the o2m: 1 RPC should have been done to fetch
+ // the 2 subrecords of page 2, and those records should now be displayed
+ form.$('.o_x2m_control_panel .o_pager_next').click();
+ assert.strictEqual(count, 7, 'one RPC should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2,
+ 'one2many kanban should contain 2 cards for record 1 at page 2');
+
+ // move to record 2 again and check that everything is correctly updated
+ form.pager.next();
+ assert.strictEqual(count, 9, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3,
+ 'one2many kanban should contain 3 cards for record 2');
+
+ // move back to record 1 and move to page 2 again: all data should have
+ // been correctly reloaded
+ form.pager.previous();
+ assert.strictEqual(count, 11, 'two RPCs should have been done');
+ form.$('.o_x2m_control_panel .o_pager_next').click();
+ assert.strictEqual(count, 12, 'one RPC should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2,
+ 'one2many kanban should contain 2 cards for record 1 at page 2');
+ form.destroy();
+ });
+
+ QUnit.test('sorting one2many fields', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.foo.sortable = true;
+ this.data.partner.records.push({id: 23, foo: "abc"});
+ this.data.partner.records.push({id: 24, foo: "xyz"});
+ this.data.partner.records.push({id: 25, foo: "def"});
+ this.data.partner.records[0].p = [23,24,25];
+
+ var rpcCount = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ rpcCount = 0;
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(def)').length,
+ "the 3rd record is the one with 'def' value");
+ form.renderer._render = function () {
+ throw "should not render the whole form";
+ };
+
+ form.$('table thead th:contains(Foo)').click();
+ assert.strictEqual(rpcCount, 0,
+ 'sort should be in memory, no extra RPCs should have been done');
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(xyz)').length,
+ "the 3rd record is the one with 'xyz' value");
+
+ form.$('table thead th:contains(Foo)').click();
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(abc)').length,
+ "the 3rd record is the one with 'abc' value");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list field edition', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records.push({
+ id: 3,
+ display_name: "relational record 1",
+ });
+ this.data.partner.records[1].p = [3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit the first line of the o2m
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1',
+ "display name of first record in o2m list should be 'relational record 1'");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_one2many tbody td').first().click();
+ assert.ok(form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ "first row of o2m should be in edition");
+ form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input');
+ assert.ok(form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ "first row of o2m should still be in edition");
+
+ // // leave o2m edition
+ form.$el.click();
+ assert.ok(!form.$('.o_field_one2many tbody td').first().hasClass('o_edit_mode'),
+ "first row of o2m should be readonly again");
+
+ // discard changes
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
+ "changes shouldn't have been discarded yet, waiting for user confirmation");
+ $('.modal .modal-footer .btn-primary').click();
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1',
+ "display name of first record in o2m list should be 'relational record 1'");
+
+ // edit again and save
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_one2many tbody td').first().click();
+ form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input');
+ form.$el.click();
+ form.$buttons.find('.o_form_button_save').click();
+ // FIXME: this next test doesn't pass as the save of updates of
+ // relational data is temporarily disabled
+ // assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
+ // "display name of first record in o2m list should be 'new value'");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: create action disabled', function (assert) {
+ assert.expect(2);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree create="0">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: deleting one record', function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].p = [2, 4];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].p;
+ assert.strictEqual(commands.length, 2,
+ 'should have generated two commands');
+ assert.ok(commands[0][0] === 4 && commands[0][1] === 4,
+ 'should have generated the command 4 (LINK_TO) with id 4');
+ assert.ok(commands[1][0] === 2 && commands[1][1] === 2,
+ 'should have generated the command 2 (DELETE) with id 2');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('td.o_list_record_delete span').length, 2,
+ "should have 2 delete buttons");
+
+ form.$('td.o_list_record_delete span').first().click();
+
+ assert.strictEqual(form.$('td.o_list_record_delete span').length, 1,
+ "should have 1 delete button (a record is supposed to have been deleted)");
+
+ // save and check that the correct command has been generated
+ form.$buttons.find('.o_form_button_save').click();
+
+ // FIXME: it would be nice to test that the view is re-rendered correctly,
+ // but as the relational data isn't re-fetched, the rendering is ok even
+ // if the changes haven't been saved
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban: edition', function (assert) {
+ assert.expect(14);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ // color will be in the kanban but not in the form
+ '<field name="color"/>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '<span><t t-esc="record.color.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ // foo will be in the form but not in the kanban
+ '<field name="foo"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_form_field_one2many .o-kanban-button-new').length,
+ '"Create" button should not be visible in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ 'should contain 1 record');
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'second record',
+ 'display_name of subrecord should be the one in DB');
+ assert.strictEqual(form.$('.o_kanban_record span:nth(1)').text(), 'Red',
+ 'color of subrecord should be the one in DB');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_form_field_one2many .o-kanban-button-new').length,
+ '"Create" button should be visible in edit');
+
+ // edit existing subrecord
+ form.$('.oe_kanban_global_click').click();
+
+ $('.modal .o_form_view .o_form_input').val('new name').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // create a new subrecord
+ form.$('.o-kanban-button-new').click();
+ $('.modal .o_form_view .o_form_input').val('new subrecord 1').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save and close
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1',
+ 'value of newly created subrecord should be "new subrecord 1"');
+
+ // create two new subrecords
+ form.$('.o-kanban-button-new').click();
+ $('.modal .o_form_view .o_form_input').val('new subrecord 2').trigger('input');
+ $('.modal .modal-footer .btn-primary:nth(1)').click(); // save and new
+ $('.modal .o_form_view .o_form_input').val('new subrecord 3').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save and close
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ 'should contain 4 records');
+
+ // delete subrecords
+ form.$('.o_kanban_view .delete_icon:first()').click();
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3,
+ 'should contain 3 records');
+ form.$('.o_kanban_view .delete_icon:first()').click();
+ form.$('.o_kanban_view .delete_icon:first()').click();
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ 'should contain 1 records');
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new subrecord 3',
+ 'the remaining subrecord should be "new subrecord 3"');
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban: create action disabled', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [4];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban create="0">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in edit');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (non editable): edition', function (assert) {
+ assert.expect(12);
+
+ var nbWrite = 0;
+ this.data.partner.records[0].p = [2, 4];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/><field name="qux"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ nbWrite++;
+ assert.deepEqual(args.args[1], {
+ p: [[1, 2, {display_name: 'new name'}], [2, 4, false]]
+ }, "should have sent the correct commands");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!form.$('.o_list_record_delete').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'second record',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_delete').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should not visible in edit');
+
+ // edit existing subrecord
+ form.$('.o_list_view tbody tr:first() td:eq(1)').click();
+
+ $('.modal .o_form_view .o_form_input').val('new name').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+ assert.strictEqual(nbWrite, 0, "should not have write anything in DB");
+
+ // create new subrecords
+ // TODO when 'Add an item' will be implemented
+
+ // delete subrecords
+ form.$('.o_list_record_delete:nth(1)').click();
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 1,
+ 'should contain 1 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'the remaining subrecord should be "new name"');
+
+ form.$buttons.find('.o_form_button_save').click(); // save the record
+ assert.strictEqual(nbWrite, 1, "should have write the changes in DB");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].p = [2, 4];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/><field name="qux"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ form.$('.o_list_view tbody td:first()').click();
+ assert.ok($('.modal .o_form_readonly').length,
+ 'in readonly, clicking on a subrecord should open it in readonly in a dialog');
+ $('.modal .o_form_button_cancel').click(); // close the dialog
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should be available in edit');
+
+ // edit existing subrecord
+ form.$('.o_list_view tbody td:first()').click();
+ assert.strictEqual($('.modal').length, 0,
+ 'in edit, clicking on a subrecord should not open a dialog');
+ assert.ok(form.$('.o_list_view tbody tr:first()').hasClass('o_selected_row'),
+ 'first row should be in edition');
+ form.$('.o_list_view .o_form_input:first()').val('new name').trigger('input');
+
+ form.$('.o_list_view tbody tr:nth(1) td:first').click(); // click on second record to validate the first one
+ assert.ok(!form.$('.o_list_view tbody tr:first').hasClass('o_selected_row'),
+ 'first row should not be in edition anymore');
+ assert.strictEqual(form.$('.o_list_view tbody td:first').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // create new subrecords
+ // TODO when 'Add an item' will be implemented
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 2', function (assert) {
+ assert.expect(8);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].p[0][0], 0,
+ "should send a 0 command for field p");
+ assert.strictEqual(args.args[1].p[1][0], 0,
+ "should send a second 0 command for field p");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item and enter a value
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ form.$('td.o_edit_mode input').val('kartoffel').trigger('input');
+
+ // click again on Add an item
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
+ "should have one td with the new value");
+ assert.strictEqual(form.$('td.o_edit_mode input').length, 1,
+ "should have one other new td");
+ assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows");
+
+ // enter another value and save
+ form.$('td.o_edit_mode input').val('gemuse').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows");
+ assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
+ "should have one td with the new value");
+ assert.strictEqual(form.$('td:contains(gemuse)').length, 1,
+ "should have one td with the new value");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 3', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // edit mode, then click on Add an item 2 times
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ "should have 1 data rows");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 3,
+ "should have 3 data rows");
+
+ // cancel the edition
+ form.$buttons.find('.o_form_button_cancel').click();
+ $('.modal-footer button.btn-primary').first().click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ "should have 1 data rows");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 4', function (assert) {
+ assert.expect(3);
+
+ this.data.turtle.onchanges = {
+ turtle_trululu: function (obj) {
+ if (obj.turtle_trululu) {
+ obj.turtle_description = "Some Description";
+ }
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_trululu"/>' +
+ '<field name="turtle_description"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit mode, then click on Add an item
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should have 0 data rows");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('textarea').val(), "",
+ "field turtle_description should be empty");
+
+ // add a value in the turtle_trululu field to trigger an onchange
+ var $dropdown = form.$('.o_form_field_many2one[name=turtle_trululu] input')
+ .autocomplete('widget');
+ form.$('.o_form_field_many2one[name=turtle_trululu] input').click();
+ $dropdown.find('a:contains(first record)').mouseenter().click();
+ assert.strictEqual(form.$('textarea').val(), "Some Description",
+ "field turtle_description should be set to the result of the onchange");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): discarding required empty data', function (assert) {
+ assert.expect(7);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item, then click elsewhere
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should have 0 data rows");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ form.$('label.o_form_label').first().click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should still have 0 data rows");
+
+ // click on Add an item again, then click on save
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should still have 0 data rows");
+
+ assert.verifySteps(['read', 'default_get', 'default_get']);
+ form.destroy();
+ });
+
+ QUnit.test('unselecting a line with missing required data', function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit mode, then click on Add an item, then click elsewhere
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should have 0 data rows");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ "should have 1 data rows");
+
+ // adding a value in the non required field, so it is dirty, but with
+ // a missing required field
+ form.$('input[name="turtle_int"]').val('12345').trigger('input');
+
+ // click elsewhere,
+ form.$('label.o_form_label').click();
+ assert.strictEqual($('.modal').length, 1,
+ 'a confirmation model should be opened');
+
+ // click on cancel, the line should still be selected
+ $('.modal .modal-footer button.btn-default').click();
+ assert.strictEqual(form.$('tr.o_data_row.o_selected_row').length, 1,
+ "should still have 1 selected data row");
+
+ // click elsewhere, and click on ok (on the confirmation dialog)
+ form.$('label.o_form_label').click();
+ $('.modal .modal-footer button.btn-primary').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should have 0 data rows (invalid line has been discarded");
+
+ form.destroy();
+ });
+
+ QUnit.test('editing a o2m, with required field and onchange', function (assert) {
+ assert.expect(12);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+ this.data.turtle.onchanges = {
+ turtle_foo: function (obj) {
+ obj.turtle_int = obj.turtle_foo.length;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should have 0 data rows");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+
+ // input some text in required turtle_foo field
+ form.$('input[name="turtle_foo"]').val('aubergine').trigger('input');
+ assert.strictEqual(form.$('input[name="turtle_int"]').val(), "9",
+ "onchange should have been triggered");
+
+ // save and check everything is fine
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1,
+ "should have one row with turtle_foo value");
+ assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1,
+ "should have one row with turtle_int value");
+
+ assert.verifySteps(['read', 'default_get', 'onchange', 'onchange', 'write', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('onchange in a one2many', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records.push({
+ id: 3,
+ foo: "relational record 1",
+ });
+ this.data.partner.records[1].p = [3];
+ this.data.partner.onchanges = {p: true};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({value: { p: [
+ [5], // delete all
+ [0, 0, {foo: "from onchange"}], // create new
+ ]}});
+ }
+ return this._super(route, args);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_one2many tbody td').first().click();
+ form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange',
+ "display name of first record in o2m list should be 'new value'");
+ form.destroy();
+ });
+
+ QUnit.test('one2many, default_get and onchange (basic)', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [
+ [6, 0, []], // replace with zero ids
+ ];
+ this.data.partner.onchanges = {p: true};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({value: { p: [
+ [5], // delete all
+ [0, 0, {foo: "from onchange"}], // create new
+ ]}});
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.ok(form.$('td:contains(from onchange)').length,
+ "should have 'from onchange' value in one2many");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2, 4];
+ this.data.partner.records[1].product_id = false;
+ this.data.partner.records[2].product_id = 37;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name" attrs=\'{"readonly": [["product_id", "=", false]]}\'/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(form.$('.o_list_view tbody tr:eq(0) td:first').hasClass('o_readonly'),
+ "first record should have display_name in readonly mode");
+
+ assert.notOk(form.$('.o_list_view tbody tr:eq(1) td:first').hasClass('o_readonly'),
+ "second record should not have display_name in readonly mode");
+ form.destroy();
+ });
+
+ QUnit.test('pager of one2many field in new record', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner"><field name="foo"/></form>',
+ },
+ });
+
+ assert.ok(!form.$('.o_x2m_control_panel .o_cp_pager div').is(':visible'),
+ 'o2m pager should be hidden');
+
+ // click to create a subrecord
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ $('.modal .o_form_input').val('new record').trigger('input');
+ $('.modal .modal-footer button:eq(0)').click(); // save and close
+
+ assert.ok(!form.$('.o_x2m_control_panel .o_cp_pager div').is(':visible'),
+ 'o2m pager should be hidden');
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with a many2one', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+ this.data.partner.onchanges.p = function (obj) {
+ obj.p = [
+ [5], // delete all
+ [1, 2, {product_id: [37, "xphone"]}], // update existing record
+ [0, 0, {product_id: [41, "xpad"]}]
+ ];
+ //
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner"><field name="product_id"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.deepEqual(args.args[1].p, [[4, 2, false], [0, false, {product_id: 41}]],
+ "should trigger onchange with correct parameters");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should have properly fetched the many2one nameget");
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 0,
+ "should not display 'xpad' anywhere");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+
+ $('.modal .o_form_field_many2one input').click();
+
+ var $dropdown = $('.modal .o_form_field_many2one input').autocomplete('widget');
+
+ $dropdown.find('li:eq(1) a').mouseenter();
+ $dropdown.find('li:eq(1) a').click();
+
+ $('.modal .modal-footer button:eq(0)').click(); // save and close
+
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1,
+ "should display 'xpad' on a td");
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should still display xphone");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with inline form view', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].p = [];
+
+ var rpcCount = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<field name="product_id"/>' +
+ '<field name="int_field"/>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '<field name="foo"/>' + // don't remove this, it is
+ // useful to make sure the foo fieldwidget
+ // does not crash because the foo field
+ // is not in the form view
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [[0, false, {
+ display_name: false, int_field: 123, product_id: 41
+ }]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+
+ // write in the many2one field, value = 37 (xphone)
+ $('.modal .o_form_field_many2one input').click();
+ var $dropdown = $('.modal .o_form_field_many2one input').autocomplete('widget');
+ $dropdown.find('li:eq(0) a').mouseenter();
+ $dropdown.find('li:eq(0) a').click();
+
+ // write in the integer field
+ $('.modal .modal-body input.o_field_widget').val('123').trigger('input');
+
+ // save and close
+ $('.modal .modal-footer button:eq(0)').click();
+
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should display 'xphone' in a td");
+
+ // reopen the record in form view
+ form.$('tbody td:contains(xphone)').click();
+
+ assert.strictEqual($('.modal .modal-body input').val(), "xphone",
+ "should display 'xphone' in an input");
+
+ $('.modal .modal-body input.o_field_widget').val('456').trigger('input');
+
+ // discard
+ $('.modal .modal-footer span:contains(Discard)').click();
+
+ // reopen the record in form view
+ form.$('tbody td:contains(xphone)').click();
+
+ assert.strictEqual($('.modal .modal-body input.o_field_widget').val(), "123",
+ "should display 123 (previous change has been discarded)");
+
+ // write in the many2one field, value = 41 (xpad)
+ $('.modal .o_form_field_many2one input').click();
+ $dropdown = $('.modal .o_form_field_many2one input').autocomplete('widget');
+ $dropdown.find('li:eq(1) a').mouseenter();
+ $dropdown.find('li:eq(1) a').click();
+
+ // save and close
+ $('.modal .modal-footer button:eq(0)').click();
+
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1,
+ "should display 'xpad' in a td");
+
+ // save the record
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with inline form view with context with parent key', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[0].product_id = 41;
+ this.data.partner.records[1].product_id = 37;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<field name="product_id" context="{\'partner_foo\':parent.foo, \'lalala\': parent.product_id}"/>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.kwargs.context.partner_foo, "yop",
+ "should have correctly evaluated parent foo field");
+ assert.strictEqual(args.kwargs.context.lalala, 41,
+ "should have correctly evaluated parent product_id field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ // open a modal
+ form.$('tr.o_data_row:eq(0) td:contains(xphone)').click();
+
+ // write in the many2one field
+ $('.modal .o_form_field_many2one input').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list, editable, with many2one and with context with parent key', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="product_id" context="{\'partner_foo\':parent.foo}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.kwargs.context.partner_foo, "yop",
+ "should have correctly evaluated parent foo field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('tr.o_data_row:eq(0) td:contains(xphone)').click();
+
+ // trigger a name search
+ form.$('table td input.o_form_input').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list edition, some basic functionality', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.foo.default = false;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('td input.o_field_widget.o_form_input').length, 1,
+ "should have created a row in edit mode");
+
+ form.$('td input.o_field_widget.o_form_input').val('a').trigger('input');
+
+ assert.strictEqual(form.$('td input.o_field_widget.o_form_input').length, 1,
+ "should not have unselected the row after edition");
+
+ form.$('td input.o_field_widget.o_form_input').val('abc').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('td:contains(abc)').length, 1,
+ "should have a row with the correct value");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list, the context is properly evaluated and sent', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p" context="{\'hello\': \'world\', \'abc\': int_field}">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ var context = args.kwargs.context;
+ assert.strictEqual(context.hello, "world");
+ assert.strictEqual(context.abc, 10);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many widget: create', function (assert) {
+ assert.expect(10);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles" widget="many2many">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '<form>' +
+ '<group>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="display_name"/><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></tree>',
+ 'turtle,false,search': '<search><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></search>',
+ },
+ session: {},
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/turtle/create') {
+ assert.ok(args.args, "should write on the turtle record");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1");
+ assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+
+ assert.strictEqual($('.modal .o_data_row').length, 2,
+ "sould have 2 records in the select view (the last one is not displayed because it is already selected)");
+
+ $('.modal .o_data_row:first .o_list_record_selector input').click();
+ $('.modal .o_select_button').click();
+ $('.o_form_button_save').click();
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "sould have 1 record in the select view");
+
+ $('.modal-footer button:eq(1)').click();
+ $('.modal input.o_form_field[name="turtle_foo"]').val('tototo').trigger('input');
+ $('.modal input.o_form_field[name="turtle_int"]').val(50).trigger('input');
+ var $many2one = $('.modal [name="product_id"] input').click();
+ var $dropdown = $many2one.autocomplete('widget');
+ $dropdown.find('li:first a').mouseenter();
+ $dropdown.find('li:first a').click();
+
+ $('.modal-footer button:contains(&):first').click();
+
+ assert.strictEqual($('.modal').length, 0, "sould close the modals");
+
+ assert.strictEqual(form.$('.o_data_row').length, 3,
+ "sould have 3 records in one2many list");
+ assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.5tototo1.550xphone",
+ "sould display the record values in one2many list");
+
+ $('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many widget: edition', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles" widget="many2many">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '<form>' +
+ '<group>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_trululu"/>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="display_name"/><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></tree>',
+ 'turtle,false,search': '<search><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></search>',
+ },
+ session: {},
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/turtle/write') {
+ assert.strictEqual(args.args[0].length, 1, "should write on the turtle record");
+ assert.deepEqual(args.args[1], {"product_id":37}, "should write only the product_id on the turtle record");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1");
+ assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$('.o_data_row:first').click();
+ $('.modal .o_form_button_cancel').click();
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // edit the first one2many record
+ form.$('.o_data_row:first').click();
+ var $many2one = $('.modal [name="product_id"] input').click();
+ var $dropdown = $many2one.autocomplete('widget');
+ $dropdown.find('li:first a').mouseenter();
+ $dropdown.find('li:first a').click();
+ $('.modal-footer button:first').click();
+
+ $('.o_form_button_save').click(); // don't save anything because the one2many does not change
+
+ // add a one2many record
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ $('.modal .o_data_row:first .o_list_record_selector input').click();
+ $('.modal .o_select_button').click();
+
+ // edit the second one2many record
+ form.$('.o_data_row:eq(1)').click();
+ $many2one = $('.modal [name="product_id"] input').click();
+ $dropdown = $many2one.autocomplete('widget');
+ $dropdown.find('li:first a').mouseenter();
+ $dropdown.find('li:first a').click();
+ $('.modal-footer button:first').click();
+
+ $('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('new record, the context is properly evaluated and sent', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.int_field.default = 17;
+ var n = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p" context="{\'hello\': \'world\', \'abc\': int_field}">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ n++;
+ if (n === 2) {
+ var context = args.kwargs.context;
+ assert.strictEqual(context.hello, "world");
+ assert.strictEqual(context.abc, 17);
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ form.destroy();
+ });
+
+ QUnit.test('parent data is properly sent on an onchange rpc', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = {bar: function () {}};
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var fieldValues = args.args[1];
+ assert.strictEqual(fieldValues.trululu.foo, "yop",
+ "should have properly sent the parent foo value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ form.destroy();
+ });
+
+ QUnit.test('parent data is properly sent on an onchange rpc, new record', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = {bar: function () {}};
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var fieldValues = args.args[1];
+ assert.strictEqual(fieldValues.trululu.foo, "My little Foo Value",
+ "should have properly sent the parent foo value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ form.destroy();
+ });
+
+ QUnit.test('sub form view with a required field', function (assert) {
+ assert.expect(2);
+ this.data.partner.fields.foo.required = true;
+ this.data.partner.fields.foo.default = null;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<group><field name="foo"/></group>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('tbody td.o_form_field_x2many_list_row_add a').click();
+ $('.modal-footer button.btn-primary').first().click();
+
+ assert.strictEqual($('.modal').length, 1, "should still have an open modal");
+ assert.strictEqual($('.modal tbody label.o_form_invalid').length, 1,
+ "should have displayed invalid fields");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with action button', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<button name="method_name" type="object" icon="fa-plus"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.record_id, 2,
+ 'should call with correct id');
+ assert.strictEqual(event.data.model, 'partner',
+ 'should call with correct model');
+ assert.strictEqual(event.data.action_data.name, 'method_name',
+ "should call correct method");
+ assert.strictEqual(event.data.action_data.type, 'object',
+ 'should have correct type');
+ },
+ },
+ });
+
+ form.$('.o_list_button button').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban with action button', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="foo"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<span><t t-esc="record.foo.value"/></span>' +
+ '<button name="method_name" type="object" class="fa fa-plus"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.record_id, 2,
+ 'should call with correct id');
+ assert.strictEqual(event.data.model, 'partner',
+ 'should call with correct model');
+ assert.strictEqual(event.data.action_data.name, 'method_name',
+ "should call correct method");
+ assert.strictEqual(event.data.action_data.type, 'object',
+ 'should have correct type');
+ },
+ },
+ });
+
+ form.$('.oe_kanban_action_button').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many without inline tree arch', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [2,3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="p" widget="many2many_tags"/>' + // check if the view don not call load view (widget without useSubview)
+ '<field name="turtles"/>' +
+ '<field name="timmy" invisible="1"/>' + // check if the view don not call load view in invisible
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,list": '<tree string="Turtles"><field name="turtle_bar"/><field name="display_name"/><field name="partner_ids"/></tree>',
+ }
+ });
+
+ assert.strictEqual(form.$('.o_form_field[name="turtles"] .o_list_view').length, 1,
+ 'should display one2many list view in the modal');
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'should display the 2 turtles');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one and many2many in one2many', function (assert) {
+ assert.expect(11);
+
+ this.data.turtle.records[1].product_id = 37;
+ this.data.partner.records[0].turtles = [2, 3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<form string="Turtles">' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '<field name="product_id"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands.length, 2,
+ "should have generated 2 commands");
+ assert.deepEqual(commands[0], [1, 2, {
+ partner_ids: [[6, false, [2, 1]]],
+ product_id: 41,
+ }], "generated commands should be correct");
+ assert.deepEqual(commands[1], [4, 3, false],
+ "generated commands should be correct");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'should display the 2 turtles');
+ assert.strictEqual(form.$('.o_data_row:first td:nth(1)').text(), 'xphone',
+ "should correctly display the m2o");
+ assert.strictEqual(form.$('.o_data_row:first td:nth(2) .badge').length, 2,
+ "m2m should contain two tags");
+ assert.strictEqual(form.$('.o_data_row:first td:nth(2) .badge:first span').text(),
+ 'second record', "m2m values should have been correctly fetched");
+
+ form.$('.o_data_row:first').click();
+
+ assert.strictEqual($('.modal .o_form_field').text(), "xphone",
+ 'should display the form view dialog with the many2one value');
+ $('.modal-footer button').click(); // close the modal
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // edit the m2m of first row
+ form.$('.o_list_view tbody td:first()').click();
+ // remove a tag
+ form.$('.o_form_field_many2manytags .badge:contains(aaa) .o_delete').click();
+ assert.strictEqual(form.$('.o_selected_row .o_form_field_many2manytags .o_badge_text:contains(aaa)').length, 0,
+ "tag should have been correctly removed");
+ // add a tag
+ var $m2mInput = form.$('.o_selected_row .o_form_field_many2manytags input');
+ $m2mInput.click();
+ $m2mInput.autocomplete('widget').find('li:first()').click();
+ assert.strictEqual(form.$('.o_selected_row .o_form_field_many2manytags .o_badge_text:contains(first record)').length, 1,
+ "tag should have been correctly added");
+
+ // edit the m2o of first row
+ var $m2oInput = form.$('.o_selected_row .o_form_field_many2one:first input');
+ $m2oInput.click();
+ $m2oInput.autocomplete('widget').find('li:contains(xpad)').mouseover().click();
+ assert.strictEqual(form.$('.o_selected_row .o_form_field_many2one:first input').val(), 'xpad',
+ "m2o value should have been updated");
+
+ // save (should correctly generate the commands)
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('load view for x2many in one2many', function (assert) {
+ assert.expect(2);
+
+ this.data.turtle.records[1].product_id = 37;
+ this.data.partner.records[0].turtles = [2,3];
+ this.data.partner.records[2].turtles = [1,3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<form string="Turtles">' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '<field name="partner_ids"/>' +
+ '</group>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "partner,false,list": '<tree string="Partners"><field name="display_name"/></tree>',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ 'should display the 2 turtles');
+
+ form.$('.o_data_row:first').click();
+
+ assert.strictEqual($('.modal .o_form_field[name="partner_ids"] .o_list_view').length, 1,
+ 'should display many2many list view in the modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many (who contains a one2many) with tree view and without form view', function (assert) {
+ assert.expect(1);
+
+ // avoid error in _postprocess
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="partner_ids"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="Turtles"><field name="turtle_foo"/></form>',
+ },
+ });
+
+ form.$('.o_data_row:first').click();
+
+ assert.strictEqual($('.modal .o_form_field[name="turtle_foo"]').text(), 'blip',
+ 'should open the modal and display the form field');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) {
+ assert.expect(1);
+
+ // avoid error in _fetchX2Manys
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="Turtles"><field name="turtle_foo"/></form>',
+ },
+ });
+
+ form.$('.o_data_row:first').click();
+
+ assert.strictEqual($('.modal .o_form_field[name="turtle_foo"]').text(), 'blip',
+ 'should open the modal and display the form field');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many field with virtual ids', function (assert) {
+ assert.expect(11);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<notebook>' +
+ '<page>' +
+ '<field name="p" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_details">' +
+ '<div class="o_test_id">' +
+ '<field name="id"/>' +
+ '</div>' +
+ '<div class="o_test_foo">' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Associated partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ },
+ res_id: 4,
+ });
+
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view').length, 1,
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 0,
+ "should not have kanban records yet");
+
+ // // switch to edit mode and create a new kanban record
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field .o-kanban-button-new').click();
+
+ // save & close the modal
+ assert.strictEqual($('.modal-content input.o_form_field').val(), 'My little Foo Value',
+ "should already have the default value for field foo");
+ $('.modal-content .btn-primary').first().click();
+
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view').length, 1,
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ "should now have one kanban record");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_id').text(),
+ '', "should not have a value for the id field");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_foo').text(),
+ 'My little Foo Value', "should have a value for the foo field");
+
+ // save the view to force a create of the new record in the one2many
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view').length, 1,
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ "should now have one kanban record");
+ assert.notEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_id').text(),
+ '', "should now have a value for the id field");
+ assert.strictEqual(form.$('.o_form_field .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_foo').text(),
+ 'My little Foo Value', "should still have a value for the foo field");
+
+ form.destroy();
+ });
+
+ QUnit.test('focusing fields in one2many list', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('.o_data_row:first td:first').click();
+ assert.strictEqual(form.$('.o_form_input[name="turtle_foo"]')[0], document.activeElement,
+ "turtle foo field should have focus");
+
+ form.$('.o_form_input[name="turtle_foo"]').trigger({type: 'keydown', which: $.ui.keyCode.TAB});
+ assert.strictEqual(form.$('.o_form_input[name="turtle_int"]')[0], document.activeElement,
+ "turtle int field should have focus");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable = top', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands[0][0], 0,
+ "first command is a create");
+ assert.strictEqual(commands[1][0], 4,
+ "second command is a link to");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should start with one data row");
+
+ form.$('.o_form_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ "should have 2 data rows");
+ assert.ok(form.$('tr.o_data_row:first:contains(My little Foo Value)').length, 1,
+ "first row should be the new value");
+ assert.ok(form.$('tr.o_data_row:first').hasClass('o_selected_row'),
+ "first row should be selected");
+
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable = bottom', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands[0][0], 4,
+ "first command is a link to");
+ assert.strictEqual(commands[1][0], 0,
+ "second command is a create");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should start with one data row");
+
+ form.$('.o_form_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ "should have 2 data rows");
+ assert.ok(form.$('tr.o_data_row:eq(1):contains(My little Foo Value)').length, 1,
+ "second row should be the new value");
+ assert.ok(form.$('tr.o_data_row:eq(1)').hasClass('o_selected_row'),
+ "second row should be selected");
+
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2Many');
+
+ QUnit.test('many2many kanban: edition', function (assert) {
+ assert.expect(28);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({id: 15, display_name: "red", color: 6});
+ this.data.partner_type.records.push({id: 18, display_name: "yellow", color: 4});
+ this.data.partner_type.records.push({id: 21, display_name: "blue", color: 1});
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,form': '<form string="Types"><field name="display_name"/></form>',
+ 'partner_type,false,list': '<tree string="Types"><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search string="Types">' +
+ '<field name="name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner_type/write') {
+ assert.strictEqual(args.args[1].display_name, "new name", "should write 'new_name'");
+ }
+ if (route === '/web/dataset/call_kw/partner_type/create') {
+ assert.strictEqual(args.args[0].display_name, "A new type", "should create 'A new type'");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ // get the created type's id
+ var createdType = _.findWhere(this.data.partner_type.records, {
+ display_name: "A new type"
+ });
+ var ids = _.sortBy([12, 15, 18, 21].concat(createdType.id), _.identity.bind(_));
+ assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
+ "new value should be " + ids);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // the SelectCreateDialog requests the session, so intercept its custom
+ // event to specify a fake session to prevent it from crashing
+ testUtils.intercept(form, 'get_session', function (event) {
+ event.data.callback({user_context: {}});
+ });
+
+ assert.ok(!form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_form_field_many2many .o-kanban-button-new').length,
+ '"Add" button should not be visible in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'gold',
+ 'display_name of subrecord should be the one in DB');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_form_field_many2many .o-kanban-button-new').length,
+ '"Add" button should be visible in edit');
+
+ // edit existing subrecord
+ form.$('.oe_kanban_global_click:first()').click();
+
+ $('.modal .o_form_view .o_form_input').val('new name').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add subrecords
+ // -> single select
+ form.$('.o_form_field_many2many .o-kanban-button-new').click();
+ assert.ok($('.modal .o_list_view').length, "should have opened a list view in a modal");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 3,
+ "list view should contain 3 records");
+ $('.modal .o_list_view tbody tr:contains(red)').click(); // select red
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3,
+ 'kanban should now contain 3 records');
+ assert.ok(form.$('.o_kanban_record:contains(red)').length,
+ 'record "red" should be in the kanban');
+
+ // -> multiple select
+ form.$('.o_form_field_many2many .o-kanban-button-new').click();
+ assert.ok($('.modal .o_select_button').prop('disabled'), "select button should be disabled");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 2,
+ "list view should contain 2 records");
+ $('.modal .o_list_view thead .o_list_record_selector input').click(); // select all
+ $('.modal .o_select_button').click(); // validate selection
+ assert.ok(!$('.modal .o_select_button').prop('disabled'), "select button should be enabled");
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'kanban should now contain 5 records');
+ // -> created record
+ form.$('.o_form_field_many2many .o-kanban-button-new').click();
+ $('.modal .modal-footer .btn-primary:nth(1)').click(); // click on 'Create'
+ assert.ok($('.modal .o_form_view.o_form_editable').length,
+ "should have opened a form view in edit mode, in a modal");
+ $('.modal .o_form_view input').val('A new type').trigger('input');
+ $('.modal:nth(1) .modal-footer .btn-primary:first()').click(); // click on 'Save & Close'
+ assert.ok(!$('.modal').length, "should have closed both modals");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 6,
+ 'kanban should now contain 6 records');
+ assert.ok(form.$('.o_kanban_record:contains(A new type)').length,
+ 'the newly created type should be in the kanban');
+
+ // delete subrecords
+ form.$('.o_kanban_record:contains(silver) .delete_icon').click();
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'should contain 5 records');
+ assert.ok(!form.$('.o_kanban_record:contains(silver)').length,
+ 'the removed record should not be in kanban anymore');
+
+ // save the record
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban: create action disabled', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban create="0">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ session: {user_context: {}},
+ });
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(form.$('.o-kanban-button-new').length,
+ '"Add" button should be available in edit');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+
+ form.$('.o-kanban-button-new').click(); // click on 'Add'
+ assert.strictEqual($('.modal .modal-footer .btn-primary').length, 1, // only button 'Select'
+ '"Create" button should not be available in the modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (non editable): edition', function (assert) {
+ assert.expect(9);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.fields.float_field = {string: 'Float', type: 'float'};
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_list_record_delete').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_delete').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should be visible in edit');
+
+ // edit existing subrecord
+ form.$('.o_list_view tbody tr:first()').click();
+
+ $('.modal .o_form_view .o_form_input').val('new name').trigger('input');
+ $('.modal .modal-footer .btn-primary').click(); // save
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // create new subrecords
+ // TODO when 'Add an item' will be implemented
+
+ // delete subrecords
+ form.$('.o_list_record_delete:first()').click();
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 1,
+ 'should contain 1 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'silver',
+ 'the remaining subrecord should be "aaa"');
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (editable): edition', function (assert) {
+ assert.expect(11);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.fields.float_field = {string: 'Float', type: 'float'};
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_list_record_delete').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_delete').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" should not visible in edit');
+
+ // edit existing subrecord
+ form.$('.o_list_view tbody td:first()').click();
+ assert.ok(!$('.modal').length,
+ 'in edit, clicking on a subrecord should not open a dialog');
+ assert.ok(form.$('.o_list_view tbody tr:first()').hasClass('o_selected_row'),
+ 'first row should be in edition');
+ form.$('.o_list_view .o_form_input:first()').val('new name').trigger('input');
+ // FIXME: this doesn"t work for now as the x2many fields are reset on field changed
+ // form.$('.o_list_view tbody td:nth(1)').click(); // click on second record to validate the first one
+ // assert.ok(!form.$('.o_list_view tbody tr:first()').hasClass('o_selected_row'),
+ // 'first row should not be in edition anymore');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add new subrecords
+ // TODO when 'Add an item' will be implemented
+
+ // delete subrecords
+ form.$('.o_list_record_delete:first()').click();
+ assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 1,
+ 'should contain 1 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'silver',
+ 'the remaining subrecord should be "aaa"');
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: create action disabled', function (assert) {
+ assert.expect(2);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="0">' +
+ '<field name="name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.ok(!form.$('.o_form_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in edit either');
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldStatus');
+
+ QUnit.test('static statusbar widget on many2one field', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+ this.data.partner.records[1].bar = false;
+
+ var count = 0;
+ var nb_fields_fetched;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ // the following field seem useless, but its presence was the
+ // cause of a crash when evaluating the field domain.
+ '<field name="timmy" invisible="1"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ count++;
+ nb_fields_fetched = args.args[1].length;
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(count, 1, 'once search_read should have been done to fetch the relational values');
+ assert.strictEqual(nb_fields_fetched, 1, 'search_read should only fetch field id');
+ assert.strictEqual(form.$('.o_statusbar_status button:not(.dropdown-toggle)').length, 2, "should have 2 status");
+ assert.strictEqual(form.$('.o_statusbar_status button:disabled').length, 2,
+ "all status should be disabled");
+ assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary'),
+ "selected status should be btn-primary");
+
+ form.destroy();
+ });
+
+ QUnit.test('static statusbar widget on many2one field with domain', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" domain="[(\'user_id\',\'=\',uid)]" widget="statusbar"/></header>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ assert.deepEqual(args.args[0], ['|', ['id', '=', 4], ['user_id', '=', 17]],
+ "search_read should sent the correct domain");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ session: {user_context: {uid: 17}},
+ });
+
+ form.destroy();
+ });
+
+ QUnit.test('clickable statusbar widget on many2one field', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" clickable="True"/></header>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary disabled'),
+ "selected status should be btn-primary and disabled");
+ var $clickable = form.$('.o_statusbar_status button.btn-default:not(.dropdown-toggle):not(:disabled)');
+ assert.strictEqual($clickable.length, 2,
+ "other status should be btn-default and not disabled");
+ $clickable.last().click(); // (last is visually the first here (css))
+ assert.ok(form.$('.o_statusbar_status button[data-value="1"]').hasClass("btn-primary disabled"),
+ "value should have been updated");
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with no status', function (assert) {
+ assert.expect(2);
+
+ this.data.product.records = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="product_id" widget="statusbar"/></header>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('.o_statusbar_status').hasClass('o_form_field_empty'),
+ 'statusbar widget should have class o_form_field_empty');
+ assert.strictEqual(form.$('.o_statusbar_status').children().length, 0,
+ 'statusbar widget should be empty');
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with domain but no value (create mode)', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_statusbar_status button:disabled').length, 2, "should have 2 status");
+
+ form.destroy();
+ });
+
+ QUnit.test('clickable statusbar should change m2o fetching domain in edit mode', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" clickable="True"/></header>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ var $buttons = form.$('.o_statusbar_status button:not(.dropdown-toggle)');
+ assert.strictEqual($buttons.length, 3, "there should be 3 status");
+ $buttons.last().click(); // (last is visually the first here (css))
+ assert.strictEqual(form.$('.o_statusbar_status button:not(.dropdown-toggle)').length, 2,
+ "there should be 2 status left");
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar fold_field option and statusbar_visible attribute', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].bar = false;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" options="{\'fold_field\': \'bar\'}"/>' +
+ '<field name="color" widget="statusbar" statusbar_visible="red"/></header>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_statusbar_status:first .dropdown-menu button.disabled').length, 1, "should have 1 folded status");
+ assert.strictEqual(form.$('.o_statusbar_status:last button.disabled').length, 1, "should have 1 status (other discarded)");
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with dynamic domain', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.trululu.domain = "[('int_field', '>', qux)]";
+ this.data.partner.records[2].int_field = 0;
+
+ var rpcCount = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ '<field name="qux"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ rpcCount++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_statusbar_status button.disabled').length, 3, "should have 3 status");
+ assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc");
+ form.$('input:first').val(9.5).trigger("input").trigger("change");
+ assert.strictEqual(form.$('.o_statusbar_status button.disabled').length, 2, "should have 2 status");
+ assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc");
+ form.$('input:last').val("hey").trigger("input").trigger("change");
+ assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldSelection');
+
+ QUnit.test('widget selection in a list view', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records.forEach(function (r) {
+ r.color = 'red';
+ });
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Colors" editable="top">' +
+ '<field name="color"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('td:contains(Red)').length, 3,
+ "should have 3 rows with correct value");
+ list.$('td:contains(Red):first').click();
+
+ var $td = list.$('tbody tr.o_selected_row td:not(.o_list_record_selector)');
+
+ assert.strictEqual($td.find('select').length, 1, "td should have a child 'select'");
+ assert.strictEqual($td.contents().length, 1, "select tag should be only child of td");
+ list.destroy();
+ });
+
+ QUnit.test('widget selection, edition and on many2one field', function (assert) {
+ assert.expect(15);
+
+ this.data.partner.onchanges = {product_id: function () {}};
+ this.data.partner.records[0].product_id = 37;
+ this.data.partner.records[0].trululu = false;
+
+ var count = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id" widget="selection"/>' +
+ '<field name="trululu" widget="selection"/>' +
+ '<field name="color" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ count++;
+ assert.step(args.method);
+ return this._super(route, args);
+ },
+ });
+
+ assert.ok(!form.$('select').length, "should not have a select tag in dom");
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('select').length, 3,
+ "should have 3 select tag in dom");
+ assert.strictEqual(form.$('select[name="product_id"] option:contains(xphone)').length, 1,
+ "should have fetched xphone option");
+ assert.strictEqual(form.$('select[name="product_id"] option:contains(xpad)').length, 1,
+ "should have fetched xpad option");
+ assert.strictEqual(form.$('select[name="product_id"]').val(), "37",
+ "should have correct product_id value");
+ assert.strictEqual(form.$('select[name="trululu"]').val(), "false",
+ "should not have any value in trululu field");
+ form.$('select[name="product_id"]').val(41).trigger('change');
+
+ assert.strictEqual(form.$('select[name="product_id"]').val(), "41",
+ "should have a value of xphone");
+
+ assert.strictEqual(form.$('select[name="color"]').val(), "\"red\"",
+ "should have correct value in color field");
+
+ assert.verifySteps(['read', 'name_search', 'name_search', 'onchange']);
+ count = 0;
+ form.reload();
+ assert.strictEqual(count, 1, "should not reload product_id relation");
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2ManyTags');
+
+ QUnit.test('fieldmany2many tags: rendering and edition', function (assert) {
+ assert.expect(17);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({id: 13, display_name: "red", color: 8});
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), [12, 13]),
+ "new value should be [12, 13]");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 2,
+ "should contain 2 tags");
+ assert.ok(form.$('span:contains(gold)').length,
+ 'should have fetched and rendered gold partner tag');
+ assert.ok(form.$('span:contains(silver)').length,
+ 'should have fetched and rendered silver partner tag');
+ assert.strictEqual(form.$('span:first()').data('color'), 2,
+ 'should have correctly fetched the color');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 2,
+ "should still contain 2 tags in edit mode");
+ assert.ok(form.$('.o_tag_color_2 .o_badge_text:contains(gold)').length,
+ 'first tag should still contain "gold" and be color 2 in edit mode');
+ assert.strictEqual(form.$('.o_form_field_many2manytags .o_delete').length, 2,
+ "tags should contain a delete button");
+
+ // add an other existing tag
+ var $input = form.$('.o_form_field_many2manytags input');
+ $input.click(); // opens the dropdown
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 1,
+ "autocomplete dropdown should have 1 entry");
+ assert.strictEqual($input.autocomplete('widget').find('li a:contains("red")').length, 1,
+ "autocomplete dropdown should contain 'red'");
+ $input.autocomplete('widget').find('li').click(); // add 'red'
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 3,
+ "should contain 3 tags");
+ assert.ok(form.$('.o_form_field_many2manytags > span:contains("red")').length,
+ "should contain newly added tag 'red'");
+ assert.ok(form.$('.o_form_field_many2manytags > span[data-color=8]:contains("red")').length,
+ "should have fetched the color of added tag");
+
+ // remove tag with id 14
+ form.$('.o_form_field_many2manytags span[data-id=14] .o_delete').click();
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 2,
+ "should contain 2 tags");
+ assert.ok(!form.$('.o_form_field_many2manytags > span:contains("silver")').length,
+ "should not contain tag 'silver' anymore");
+
+ // save the record (should do the write RPC with the correct commands)
+ form.$buttons.find('.o_form_button_save').click();
+
+ // TODO: it would be nice to test the behaviors of the autocomplete dropdown
+ // (like refining the research, creating new tags...), but ui-autocomplete
+ // makes it difficult to test
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags view a domain', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.timmy.domain = [['id', '<', 50]];
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner_type.records.push({id: 99, display_name: "red", color: 8});
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [['id', '<', 50], ['id', 'not in', [12]]],
+ "domain sent to name_search should be correct");
+ return $.when([[14, 'silver']]);
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 1,
+ "should contain 1 tag");
+ assert.ok(form.$('span:contains(gold)').length,
+ 'should have fetched and rendered gold partner tag');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // add an other existing tag
+ var $input = form.$('.o_form_field_many2manytags input');
+ $input.click(); // opens the dropdown
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 1,
+ "autocomplete dropdown should have 1 entry");
+ assert.strictEqual($input.autocomplete('widget').find('li a:contains("silver")').length, 1,
+ "autocomplete dropdown should contain 'silver'");
+ $input.autocomplete('widget').find('li').click(); // add 'silver'
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 2,
+ "should contain 2 tags");
+ assert.ok(form.$('.o_form_field_many2manytags > span:contains("silver")').length,
+ "should contain newly added tag 'silver'");
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags in a new record', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/create') {
+ var commands = args.args[0].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), "form should be in edit mode");
+
+ var $input = form.$('.o_form_field_many2manytags input');
+ $input.click(); // opens the dropdown
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 3,
+ "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')");
+ $input.autocomplete('widget').find('li:first()').click(); // adds a tag
+ assert.strictEqual(form.$('.o_form_field_many2manytags > span').length, 1,
+ "should contain 1 tag");
+ assert.ok(form.$('.o_form_field_many2manytags > span:contains("gold")').length,
+ "should contain newly added tag 'gold'");
+
+ // save the record (should do the write RPC with the correct commands)
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags: update color', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // Update the color in readonly
+ form.$('span:first()').click();
+ $('.o_colorpicker span[data-color="1"]').trigger('mousedown'); // choose color 1
+ assert.strictEqual(form.$('span:first()').data('color'), 1,
+ 'should have correctly updated the color (in readonly)');
+
+ // Update the color in edit
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('span:first()').click();
+ $('.o_colorpicker span[data-color="6"]').trigger('mousedown'); // choose color 6
+ assert.strictEqual(form.$('span:first()').data('color'), 6,
+ 'should have correctly updated the color (in edit)');
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags in editable list', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch:'<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('.o_data_row:first .o_form_field_many2manytags .badge').length, 1,
+ "m2m field should contain one tag");
+
+ // edit first row
+ list.$('.o_data_row:first td:nth(2)').click();
+
+ var $m2o = list.$('.o_data_row:first .o_form_field_many2manytags .o_form_field_many2one');
+ assert.strictEqual($m2o.length, 1, "a many2one widget should have been instantiated");
+
+ // add a tag
+ var $input = $m2o.find('input');
+ $input.click();
+ $input.autocomplete('widget').find('li:first()').click(); // adds a tag
+
+ assert.strictEqual(list.$('.o_data_row:first .o_form_field_many2manytags .badge').length, 2,
+ "m2m field should contain 2 tags");
+ assert.ok(list.$('.o_data_row:first .o_many2many_tags_cell').hasClass('o_field_dirty'),
+ "edited cell should have class o_field_dirty");
+
+ // leave edition
+ list.$('.o_data_row:nth(1) td:nth(2)').click();
+
+ assert.strictEqual(list.$('.o_data_row:first .o_form_field_many2manytags .badge').length, 2,
+ "m2m field should contain 2 tags");
+
+ list.destroy();
+ });
+
+ QUnit.module('FieldRadio');
+
+ QUnit.test('fieldradio widget on a many2one in a new record', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id" widget="radio"/>' +
+ '</form>',
+ });
+
+ assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div");
+ assert.strictEqual(form.$('input.o_form_radio').length, 2, "should have 2 possible choices");
+ assert.ok(form.$('label.o_form_label:contains(xphone)').length, "one of them should be xphone");
+ assert.strictEqual(form.$('input:checked').length, 0, "none of the input should be checked");
+
+ form.$("input.o_form_radio:first").click();
+
+ assert.strictEqual(form.$('input:checked').length, 1, "one of the input should be checked");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.test('fieldradio change value by onchange', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {bar: function (obj) {
+ obj.product_id = obj.bar ? 41 : 37;
+ obj.color = obj.bar ? 'red' : 'black';
+ }};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="product_id" widget="radio"/>' +
+ '<field name="color" widget="radio"/>' +
+ '</form>',
+ });
+
+ form.$("input[type='checkbox']").click();
+ assert.strictEqual(form.$('input.o_form_radio[data-value="37"]:checked').length, 1, "one of the input should be checked");
+ assert.strictEqual(form.$('input.o_form_radio[data-value="black"]:checked').length, 1, "the other of the input should be checked");
+ form.$("input[type='checkbox']").click();
+ assert.strictEqual(form.$('input.o_form_radio[data-value="41"]:checked').length, 1, "the other of the input should be checked");
+ assert.strictEqual(form.$('input.o_form_radio[data-value="red"]:checked').length, 1, "one of the input should be checked");
+
+ form.destroy();
+ });
+
+
+ QUnit.test('fieldradio widget on a selection in a new record', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="color" widget="radio"/>' +
+ '</form>',
+ });
+
+
+ assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div");
+ assert.strictEqual(form.$('input.o_form_radio').length, 2, "should have 2 possible choices");
+ assert.ok(form.$('label.o_form_label:contains(Red)').length, "one of them should be Red");
+
+ // click on 2nd option
+ form.$("input.o_form_radio").eq(1).click();
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.color, 'black', "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2ManyCheckBoxes');
+
+ QUnit.test('widget many2many_checkboxes', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].timmy = [12];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<group><field name="timmy" widget="many2many_checkboxes"/></group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('div.o_form_field div.o_checkbox').length, 2,
+ "should have fetched and displayed the 2 values of the many2many");
+
+ assert.ok(form.$('div.o_form_field div.o_checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('div.o_form_field div.o_checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ assert.ok(form.$('div.o_form_field div.o_checkbox input').prop('disabled'),
+ "the checkboxes should be disabled");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.notOk(form.$('div.o_form_field div.o_checkbox input').prop('disabled'),
+ "the checkboxes should not be disabled");
+
+ form.$('div.o_form_field div.o_checkbox input').eq(1).click();
+ form.$buttons.find('.o_form_button_save').click();
+ assert.deepEqual(this.data.partner.records[0].timmy, [12, 14],
+ "should have added the second element to the many2many");
+
+ form.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
new file mode 100644
index 00000000000..de5e02934f0
--- /dev/null
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -0,0 +1,941 @@
+odoo.define('web.MockServer', function (require) {
+"use strict";
+
+var Class = require('web.Class');
+var data_manager = require('web.data_manager');
+var Domain = require('web.Domain');
+var pyeval = require('web.pyeval');
+var utils = require('web.utils');
+
+var MockServer = Class.extend({
+ /**
+ * @constructor
+ * @param {Object} data
+ * @param {Object} options
+ * @param {integer} [options.logLevel=0]
+ * @param {string} [options.currentDate] formatted string, default to
+ * current day
+ */
+ init: function (data, options) {
+ this.data = data;
+ for (var modelName in this.data) {
+ var model = this.data[modelName];
+ if (!('id' in model.fields)) {
+ model.fields.id = {string: "ID", type: "integer"};
+ }
+ if (!('display_name' in model.fields)) {
+ model.fields.display_name = {string: "Display Name", type: "char"};
+ }
+ if (!('name' in model.fields)) {
+ model.fields.name = {string: "Name", type: "char", default: "name"};
+ }
+ for (var fieldName in model.onchanges) {
+ model.fields[fieldName].onChange = "1";
+ }
+ model.records = model.records || [];
+
+ for (var i = 0; i < model.records.length; i++) {
+ var record = model.records[i];
+ this._applyDefaults(model, record);
+ }
+ }
+
+ // 0 is for no log
+ // 1 is for short
+ // 2 is for detailed
+ this.logLevel = (options && options.logLevel) || 0;
+
+ this.currentDate = options.currentDate || moment().format("YYYY-MM-DD");
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * helper: read a string describing an arch, and returns a simulated
+ * 'field_view_get' call to the server. Calls processViews() of data_manager
+ * to mimick the real behavior of a call to loadViews().
+ *
+ * @param {string|Object} arch a string OR a parsed xml document
+ * @param {string} model a model name (that should be in this.data)
+ * @returns {Object} an object with 2 keys: arch and fields
+ */
+ fieldsViewGet: function (arch, model) {
+ var fields = $.extend(true, {}, this.data[model].fields);
+ var fvg = this._fieldsViewGet(arch, model, fields);
+ var fields_views = {};
+ fields_views[fvg.type] = fvg;
+ data_manager.processViews(fields_views, fields);
+ return fields_views[fvg.type];
+ },
+ /**
+ * Simulate a complete RPC call. This is the main method for this class.
+ *
+ * This method also log incoming and outgoing data, and stringify/parse data
+ * to simulate a barrier between the server and the client. It also simulate
+ * server errors.
+ *
+ * @param {string} route
+ * @param {Object} args
+ * @returns {Deferred<any>}
+ * Resolved with the result of the RPC, stringified then parsed.
+ * If the RPC should fail, the deferred will be rejected with the
+ * error object, stringified then parsed.
+ */
+ performRpc: function (route, args) {
+ var logLevel = this.logLevel;
+ args = JSON.parse(JSON.stringify(args));
+ if (logLevel === 2) {
+ console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args);
+ }
+ var def;
+ try {
+ def = this._performRpc(route, args);
+ } catch (e) {
+ var error = {code: 200, data: {}, message: e.message};
+ if (logLevel === 1) {
+ console.warn('Mock: ' + route, error.message);
+ } else if (logLevel === 2) {
+ console.warn('%c[rpc] error response:', 'color: blue; font-weight: bold;', error.message);
+ }
+ return $.Deferred().reject(error, $.Event());
+ }
+ return def.then(function (result) {
+ var resultString = JSON.stringify(result || false);
+ if (logLevel === 1) {
+ console.log('Mock: ' + route, JSON.parse(resultString));
+ } else if (logLevel === 2) {
+ console.log('%c[rpc] response:', 'color: blue; font-weight: bold;', JSON.parse(resultString));
+ }
+ return JSON.parse(resultString);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Apply the default values when creating an object in the local database.
+ *
+ * @private
+ * @param {Object} model a model object from the local database
+ * @param {Object} record
+ */
+ _applyDefaults: function (model, record) {
+ record.display_name = record.display_name || record.name;
+ for (var fieldName in model.fields) {
+ if (fieldName === 'id') {
+ continue;
+ }
+ if (!(fieldName in record)) {
+ if ('default' in model.fields[fieldName]) {
+ record[fieldName] = model.fields[fieldName].default;
+ } else if (_.contains(['one2many', 'many2many'], model.fields[fieldName].type)) {
+ record[fieldName] = [];
+ } else {
+ record[fieldName] = false;
+ }
+ }
+ }
+ },
+ /**
+ * helper to evaluate a domain for given field values.
+ * Currently, this is only a wrapper of the Domain.compute function in
+ * "web.Domain".
+ *
+ * @param {Array} domain
+ * @param {Object} fieldValues
+ * @returns {boolean}
+ */
+ _evaluateDomain: function (domain, fieldValues) {
+ return new Domain(domain).compute(fieldValues);
+ },
+ /**
+ * helper: read a string describing an arch, and returns a simulated
+ * 'field_view_get' call to the server.
+ *
+ * @private
+ * @param {string|Object} arch a string OR a parsed xml document
+ * @param {string} model a model name (that should be in this.data)
+ * @param {Object} fields
+ * @returns {Object} an object with 2 keys: arch and fields (the fields
+ * appearing in the views)
+ */
+ _fieldsViewGet: function (arch, model, fields) {
+ var self = this;
+ var onchanges = this.data[model].onchanges || {};
+ var fieldNodes = {};
+
+ if (typeof arch === 'string') {
+ var doc = $.parseXML(arch).documentElement;
+ arch = utils.xml_to_json(doc, true);
+ }
+
+ this._traverse(arch, function (node) {
+ if (typeof node === "string") {
+ return false;
+ }
+ var modifiers = {};
+ if (node.attrs.attrs) {
+ var attrs = pyeval.py_eval(node.attrs.attrs);
+ _.extend(modifiers, attrs);
+ delete node.attrs.attrs;
+ }
+ if (node.attrs.states) {
+ if (!modifiers.invisible) {
+ modifiers.invisible = [];
+ }
+ modifiers.invisible.push(["state", "not in", node.attrs.states.split(",")]);
+ }
+ _.each(["invisible", "readonly", "required"], function (a) {
+ if (node.attrs[a]) {
+ var v = pyeval.py_eval(node.attrs[a]);
+ if (v || modifiers[a] === undefined) {
+ modifiers[a] = v;
+ }
+ }
+ });
+ node.attrs.modifiers = JSON.stringify(modifiers);
+ if (node.tag === 'field') {
+ fieldNodes[node.attrs.name] = node;
+ return false;
+ }
+ return true;
+ });
+
+ var relModel, relFields;
+ _.each(fieldNodes, function (node, name) {
+ var field = fields[name];
+ if (field.type === "one2many" || field.type === "many2many") {
+ field.views = {};
+ _.each(node.children, function (children) {
+ relModel = field.relation;
+ relFields = $.extend(true, {}, self.data[relModel].fields);
+ field.views[children.tag] = self._fieldsViewGet(children, relModel, relFields);
+ });
+ }
+
+ // add onchanges
+ if (name in onchanges) {
+ field.onChange="1";
+ }
+ });
+ return {
+ arch: arch,
+ fields: _.pick(fields, _.keys(fieldNodes)),
+ model: model,
+ type: arch.tag === 'tree' ? 'list' : arch.tag,
+ };
+ },
+ /**
+ * Get all records from a model matching a domain. The only difficulty is
+ * that if we have an 'active' field, we implicitely add active = true in
+ * the domain.
+ *
+ * @private
+ * @param {string} model a model name
+ * @param {any[]} domain
+ * @returns {Object[]} a list of records
+ */
+ _getRecords: function (model, domain) {
+ if (!_.isArray(domain)) {
+ throw new Error("MockServer._getRecords: given domain has to be an array.");
+ }
+
+ var self = this;
+ var records = this.data[model].records;
+
+ if ('active' in this.data[model].fields) {
+ // add ['active', '=', true] to the domain if 'active' is not yet present in domain
+ var activeInDomain = false;
+ _.each(domain, function (subdomain) {
+ activeInDomain = activeInDomain || subdomain[0] === 'active';
+ });
+ if (!activeInDomain) {
+ domain.unshift(['active', '=', true]);
+ }
+ }
+
+ if (domain.length) {
+ records = _.filter(records, function (record) {
+ var fieldValues = _.mapObject(record, function (value) {
+ return value instanceof Array ? value[0] : value;
+ });
+ return self._evaluateDomain(domain, fieldValues);
+ });
+ }
+
+ return records;
+ },
+ /**
+ * Helper function, to find an available ID. The current algorithm is to add
+ * all other IDS.
+ *
+ * @private
+ * @param {string} modelName
+ * @returns {integer} a valid ID (> 0)
+ */
+ _getUnusedID: function (modelName) {
+ var model = this.data[modelName];
+ return _.reduce(model.records, function (acc, record){
+ return acc + record.id;
+ }, 1);
+ },
+ /**
+ * Simulate a 'copy' operation, so we simply try to duplicate a record in
+ * memory
+ *
+ * @private
+ * @param {string} modelName
+ * @param {integer} id the ID of a valid record
+ * @returns {integer} the ID of the duplicated record
+ */
+ _mockCopy: function (modelName, id) {
+ var model = this.data[modelName];
+ var newID = this._getUnusedID(modelName);
+ var originalRecord = _.findWhere(model.records, {id: id});
+ var duplicateRecord = _.extend({}, originalRecord, {id: newID});
+ duplicateRecord.display_name = originalRecord.display_name + ' (copy)';
+ model.records.push(duplicateRecord);
+ return newID;
+ },
+ /**
+ * Simulate a 'create' operation. This is basically a 'write' with the
+ * added work of getting a valid ID and applying default values.
+ *
+ * @private
+ * @param {string} modelName
+ * @param {Object} values
+ * @returns {integer}
+ */
+ _mockCreate: function (modelName, values) {
+ if ('id' in values) {
+ throw "Cannot create a record with a predefinite id";
+ }
+ var model = this.data[modelName];
+ var id = this._getUnusedID(modelName);
+ var record = {id: id};
+ model.records.push(record);
+ this._applyDefaults(model, values);
+ this._mockWrite(modelName, [[id], values]);
+ return id;
+ },
+ /**
+ * Simulate a 'default_get' operation
+ *
+ * @private
+ * @param {string} modelName
+ * @param {array[]} args a list with a list of fields in the first position
+ * @param {Object} [kwargs]
+ * @param {Object} [kwargs.context] the context to eventually read default
+ * values
+ * @returns {Object}
+ */
+ _mockDefaultGet: function (modelName, args, kwargs) {
+ var result = {};
+ var fields = args[0];
+ var model = this.data[modelName];
+ _.each(fields, function (name) {
+ var field = model.fields[name];
+ if ('default' in field) {
+ result[name] = field.default;
+ }
+ });
+ if (kwargs && kwargs.context)
+ _.each(kwargs.context, function (value, key) {
+ if ('default_' === key.slice(0, 8)) {
+ result[key.slice(8)] = value;
+ }
+ });
+ return result;
+ },
+ /**
+ * Simulate a 'field_get' operation
+ *
+ * @private
+ * @param {string} modelName
+ * @param {any} args
+ * @returns {Object}
+ */
+ _mockFieldsGet: function (modelName, args) {
+ var modelFields = this.data[modelName].fields;
+ // Get only the asked fields (args[0] could be the field names)
+ if (args[0] && args[0].length) {
+ modelFields = _.pick.apply(_, [modelFields].concat(args[0]));
+ }
+ // Get only the asked attributes (args[1] could be the attribute names)
+ if (args[1] && args[1].length) {
+ modelFields = _.mapObject(modelFields, function (field) {
+ return _.pick.apply(_, [field].concat(args[1]));
+ });
+ }
+ return modelFields;
+ },
+ /**
+ * Simulate a 'name_get' operation
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @returns {Array[]} a list of [id, display_name]
+ */
+ _mockNameGet: function (model, args) {
+ var ids = args[0];
+ if (!_.isArray(ids)) {
+ ids = [ids];
+ }
+ var records = this.data[model].records;
+ var names = _.map(ids, function (id) {
+ return [id, _.findWhere(records, {id: id}).display_name];
+ });
+ return names;
+ },
+ /**
+ * Simulate a 'name_create' operation
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @returns {Array} a couple [id, name]
+ */
+ _mockNameCreate: function (model, args) {
+ var name = args[0];
+ var values = {
+ name: name,
+ display_name: name,
+ };
+ var id = this._mockCreate(model, values);
+ return [id, name];
+ },
+ /**
+ * Simulate a 'name_search' operation.
+ *
+ * not yet fully implemented (missing: limit, and evaluate operators)
+ * domain works but only to filter on ids
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @param {string} args[0]
+ * @param {Array} args[1], search domain
+ * @param {Object} _kwargs
+ * @returns {Array[]} a list of [id, display_name]
+ */
+ _mockNameSearch: function (model, args, _kwargs) {
+ var str = args && typeof args[0] === 'string' ? args[0] : _kwargs.name;
+ var domain = (args && args[1]) || _kwargs.args || [];
+ var dom = domain[0];
+ var records = this.data[model].records;
+ if (dom) {
+ records = _.filter(records, function (record) {
+ var value = record[dom[0]];
+ if (value instanceof Array) {
+ value = value[0];
+ }
+ if (dom[1] === 'not in') {
+ return !_.contains(dom[2], value);
+ } else if (dom[1] === 'in') {
+ return _.contains(dom[2], value);
+ } else if (dom[1] === '==') {
+ return dom[2] == value;
+ } else if (dom[1] === '!=') {
+ return dom[2] != value;
+ }
+ });
+ }
+ if (str.length) {
+ records = _.filter(records, function (record) {
+ return record.display_name.indexOf(str) !== -1;
+ });
+ }
+ return _.map(records, function (record) {
+ return [record.id, record.display_name];
+ });
+ },
+ /**
+ * Simulate an 'onchange' rpc
+ *
+ * @private
+ * @param {string} model
+ * @param {string|string[]} args a list of field names, or just a field name
+ * @returns {Object}
+ */
+ _mockOnchange: function (model, args) {
+ var onchanges = this.data[model].onchanges || {};
+ var record = args[1];
+ var fields = args[2];
+ if (!(fields instanceof Array)) {
+ fields = [fields];
+ }
+ var result = {};
+ _.each(fields, function (field) {
+ if (field in onchanges) {
+ var changes = _.clone(record);
+ onchanges[field](changes);
+ _.each(changes, function (value, key) {
+ if (record[key] !== value) {
+ result[key] = value;
+ }
+ });
+ }
+ });
+ return {value: result};
+ },
+ /**
+ * Simulate a 'read' operation.
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @param {Object} _kwargs ignored... is that correct?
+ * @returns {Object}
+ */
+ _mockRead: function (model, args, _kwargs) {
+ var self = this;
+ var ids = args[0];
+ if (!_.isArray(ids)) {
+ ids = [ids];
+ }
+ var fields = args[1] && args[1].length ? _.uniq(args[1].concat(['id'])) : Object.keys(this.data[model].fields);
+ var results = _.map(ids, function (id) {
+ var record = _.findWhere(self.data[model].records, {id: id});
+ if (!record) {
+ throw "mock read: id does not exist...";
+ }
+ var result = {};
+ for (var i = 0; i < fields.length; i++) {
+ var field = self.data[model].fields[fields[i]];
+ if (!field) {
+ // the field doens't exist on the model, so skip it
+ continue;
+ }
+ if (field.type === 'many2one') {
+ var relatedRecord = _.findWhere(self.data[field.relation].records, {
+ id: record[fields[i]]
+ });
+ if (relatedRecord) {
+ result[fields[i]] =
+ [record[fields[i]], relatedRecord.display_name];
+ } else {
+ result[fields[i]] = false;
+ }
+ } else {
+ result[fields[i]] = record[fields[i]];
+ }
+ }
+ return result;
+ });
+ return results;
+ },
+ /**
+ * Simulate a 'read_group' call to the server.
+ *
+ * Note: most of the keys in kwargs are still ignored
+ *
+ * @private
+ * @param {string} model a string describing an existing model
+ * @param {Object} kwargs various options supported by read_group
+ * @param {string[]} kwargs.groupby fields that we are grouping
+ * @param {string[]} kwargs.fields fields that we are aggregating
+ * @param {Array} kwargs.domain the domain used for the read_group
+ * @param {boolean} kwargs.lazy still mostly ignored
+ * @param {integer} kwargs.limit ignored as well
+ * @returns {Object[]}
+ */
+ _mockReadGroup: function (model, kwargs) {
+ if (!('lazy' in kwargs)) {
+ kwargs.lazy = true;
+ }
+ var self = this;
+ var fields = this.data[model].fields;
+ var aggregatedFields = _.clone(kwargs.fields);
+ var groupByField = kwargs.groupby[0];
+ var result = [];
+
+ // if no fields have been given, the server picks all stored fields
+ if (aggregatedFields.length === 0) {
+ aggregatedFields = _.keys(this.data[model].fields);
+ }
+
+ // filter out non existing fields
+ aggregatedFields = _.filter(aggregatedFields, function (name) {
+ return name in self.data[model].fields;
+ });
+
+
+ function aggregateFields(group, records) {
+ var type;
+ for (var i = 0; i < aggregatedFields.length; i++) {
+ type = fields[aggregatedFields[i]].type;
+ if (type === 'float' || type === 'integer') {
+ group[aggregatedFields[i]] = 0;
+ for (var j = 0; j < records.length; j++) {
+ group[aggregatedFields[i]] += records[j][aggregatedFields[i]];
+ }
+ }
+ }
+ }
+
+ var records = this._getRecords(model, kwargs.domain);
+ if (groupByField) {
+ var groupByFieldDescr = fields[groupByField.split(':')[0]];
+ var groupByFunction, formatValue;
+ if (groupByFieldDescr.type === 'date') {
+
+ var aggregateFunction = groupByField.split(':')[1] || 'month';
+
+ groupByField = groupByField.split(':')[0];
+ groupByFunction = function (obj) {
+ if (aggregateFunction === 'day') {
+ return moment(obj[groupByField]).format('YYYY-MM-DD');
+ } else {
+ return moment(obj[groupByField]).format('MMMM YYYY');
+ }
+ };
+ formatValue = function (val) {
+ if (aggregateFunction === 'day') {
+ return moment(val).format('YYYY-MM-DD');
+ } else {
+ return moment(val).format('MMMM YYYY');
+ }
+ };
+ } else {
+ groupByFunction = function (obj) {
+ return obj[groupByField];
+ };
+ formatValue = function (val) {
+ return val instanceof Array ? val[0] : (val || false);
+ };
+ }
+ _.each(_.groupBy(records, groupByFunction), function (g, val) {
+ val = formatValue(g[0][groupByField]);
+ var group = {
+ __domain: [[
+ groupByField, "=",
+ val instanceof Array ? val[0] : (val || false)
+ ]].concat(kwargs.domain || []),
+ };
+ var field = self.data[model].fields[groupByField];
+ if (field.type === 'many2one' && !_.isArray(val)) {
+ var related_record = _.findWhere(self.data[field.relation].records, {
+ id: val
+ });
+ if (related_record) {
+ group[groupByField] = [val, related_record.display_name];
+ } else {
+ group[groupByField] = false;
+ }
+ } else {
+ group[groupByField] = val;
+ }
+
+ // compute count key to match dumb server logic...
+ var countKey;
+ if (kwargs.lazy) {
+ countKey = groupByField + "_count";
+ } else {
+ countKey = "__count";
+ }
+ group[countKey] = g.length;
+ aggregateFields(group, g);
+ result.push($.extend(true, {}, group));
+ });
+ } else {
+ var group = { __count: records.length };
+ aggregateFields(group, records);
+ result.push(group);
+ }
+ return result;
+ },
+ /**
+ * Simulate a 'search_count' operation
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @returns {integer}
+ */
+ _mockSearchCount: function (model, args) {
+ return this._getRecords(model, args[0]).length;
+ },
+ /**
+ * Simulate a 'search_read' operation on a model
+ *
+ * @private
+ * @param {Object} args
+ * @param {Array} args.domain
+ * @param {string} args.model
+ * @param {Array} [args.fields] defaults to the list of all fields
+ * @param {integer} [args.limit]
+ * @param {integer} [args.offset=0]
+ * @param {string[]} [args.sort]
+ * @returns {Object}
+ */
+ _mockSearchRead: function (model, args, kwargs) {
+ var result = this._mockSearchReadController({
+ model: model,
+ domain: args[0],
+ fields: args[1],
+ offset: args[2],
+ limit: args[3],
+ sort: args[4],
+ context: kwargs.context,
+ });
+ return result.records;
+ },
+ /**
+ * Simulate a 'search_read' operation, from the controller point of view
+ *
+ * @private
+ * @private
+ * @param {Object} args
+ * @param {Array} args.domain
+ * @param {string} args.model
+ * @param {Array} [args.fields] defaults to the list of all fields
+ * @param {integer} [args.limit]
+ * @param {integer} [args.offset=0]
+ * @param {string[]} [args.sort]
+ * @returns {Object}
+ */
+ _mockSearchReadController: function (args) {
+ var self = this;
+ var records = this._getRecords(args.model, args.domain);
+ var fields = args.fields || _.keys(this.data[args.model].fields);
+ var nbRecords = records.length;
+ var offset = args.offset || 0;
+ records = records.slice(offset, args.limit ? (offset + args.limit) : nbRecords);
+ var processedRecords = _.map(records, function (r) {
+ var result = {};
+ _.each(_.uniq(fields.concat(['id'])), function (fieldName) {
+ var field = self.data[args.model].fields[fieldName];
+ if (field.type === 'many2one') {
+ var related_record = _.findWhere(self.data[field.relation].records, {
+ id: r[fieldName]
+ });
+ result[fieldName] =
+ related_record ? [r[fieldName], related_record.display_name] : false;
+ } else {
+ result[fieldName] = r[fieldName];
+ }
+ });
+ return result;
+ });
+ if (args.sort) {
+ var fieldName = args.sort.split(' ')[0];
+ var order = args.sort.split(' ')[1];
+ processedRecords.sort(function (r1, r2) {
+ if (r1[fieldName] < r2[fieldName]) {
+ return order === 'ASC' ? -1 : 1;
+ }
+ if (r1[fieldName] > r2[fieldName]) {
+ return order === 'ASC' ? 1 : -1;
+ }
+ return 0;
+ });
+ }
+ var result = {
+ length: nbRecords,
+ records: processedRecords,
+ };
+ return $.extend(true, {}, result);
+ },
+ /**
+ * Simulate a 'unlink' operation
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @returns {boolean} currently, always returns true
+ */
+ _mockUnlink: function (model, args) {
+ var ids = args[0];
+ if (!_.isArray(ids)) {
+ ids = [ids];
+ }
+ this.data[model].records = _.reject(this.data[model].records, function (record) {
+ return _.contains(ids, record.id);
+ });
+
+ // update value of one2many fields pointing to the deleted records
+ _.each(this.data, function (d) {
+ var relatedFields = _.pick(d.fields, function (field) {
+ return field.type === 'one2many' && field.relation === model;
+ });
+ _.each(Object.keys(relatedFields), function (relatedField) {
+ _.each(d.records, function (record) {
+ record[relatedField] = _.difference(record[relatedField], ids);
+ });
+ });
+ });
+
+ return true;
+ },
+ /**
+ * Simulate a 'write' operation
+ *
+ * @private
+ * @param {string} model
+ * @param {Array} args
+ * @returns {boolean} currently, always return 'true'
+ */
+ _mockWrite: function (model, args) {
+ _.each(args[0], this._writeRecord.bind(this, model, args[1]));
+ return true;
+ },
+ /**
+ * Dispatch a RPC call to the correct helper function
+ *
+ * @see performRpc
+ *
+ * @private
+ * @param {string} route
+ * @param {Object} args
+ * @returns {Deferred<any>}
+ * Resolved with the result of the RPC. If the RPC should fail, the
+ * deferred should either be rejected or the call should throw an
+ * exception (@see performRpc for error handling).
+ */
+ _performRpc: function (route, args) {
+ switch (route) {
+ case '/web/action/load':
+ return $.when(this._mockLoadAction(args));
+
+ case '/web/dataset/search_read':
+ return $.when(this._mockSearchReadController(args));
+ }
+ if (route.indexOf('/web/image/') === 0) {
+ return $.when();
+ }
+ switch (args.method) {
+ case 'copy':
+ return $.when(this._mockCopy(args.model, args.args[0]));
+
+ case 'create':
+ return $.when(this._mockCreate(args.model, args.args[0]));
+
+ case 'default_get':
+ return $.when(this._mockDefaultGet(args.model, args.args, args.kwargs));
+
+ case 'fields_get':
+ return $.when(this._mockFieldsGet(args.model, args.args));
+
+ case 'name_get':
+ return $.when(this._mockNameGet(args.model, args.args));
+
+ case 'name_create':
+ return $.when(this._mockNameCreate(args.model, args.args));
+
+ case 'name_search':
+ return $.when(this._mockNameSearch(args.model, args.args, args.kwargs));
+
+ case 'onchange':
+ return $.when(this._mockOnchange(args.model, args.args));
+
+ case 'read':
+ return $.when(this._mockRead(args.model, args.args, args.kwargs));
+
+ case 'read_group':
+ return $.when(this._mockReadGroup(args.model, args.kwargs));
+
+ case 'search_count':
+ return $.when(this._mockSearchCount(args.model, args.args));
+
+ case 'search_read':
+ return $.when(this._mockSearchRead(args.model, args.args, args.kwargs));
+
+ case 'unlink':
+ return $.when(this._mockUnlink(args.model, args.args));
+
+ case 'write':
+ return $.when(this._mockWrite(args.model, args.args));
+ }
+ var model = this.data[args.model];
+ if (model && typeof model[args.method] === 'function') {
+ return this.data[args.model][args.method](args.args, args.kwargs);
+ }
+
+ console.error("Unimplemented route", route, args);
+ return $.when();
+ },
+ /**
+ * helper function: traverse a tree and apply the function f to each of its
+ * nodes.
+ *
+ * Note: this should be abstracted somewhere in web.utils, or in
+ * web.tree_utils
+ *
+ * @param {Object} tree object with a 'children' key, which contains an
+ * array of trees.
+ * @param {function} f
+ */
+ _traverse: function (tree, f) {
+ var self = this;
+ if (f(tree)) {
+ _.each(tree.children, function (c) { self._traverse(c, f); });
+ }
+ },
+ /**
+ * Write a record. The main difficulty is that we have to apply x2many
+ * commands
+ *
+ * @private
+ * @param {string} model
+ * @param {Object} values
+ * @param {integer} id
+ */
+ _writeRecord: function (model, values, id) {
+ var self = this;
+ var record = _.findWhere(this.data[model].records, {id: id});
+ for (var field_changed in values) {
+ var field = this.data[model].fields[field_changed];
+ var value = values[field_changed];
+ if (!field) {
+ console.warn("Mock: Can't write on field '" + field_changed + "' on model '" + model + "' (field is undefined)");
+ continue;
+ }
+ if (_.contains(['one2many', 'many2many'], field.type)) {
+ var ids = [];
+ // convert commands
+ _.each(value, function (command) {
+ if (command[0] === 0) { // CREATE
+ var id = self._mockCreate(field.relation, command[2]);
+ ids.push(id);
+ } else if (command[0] === 1) { // UPDATE
+ self._mockWrite(field.relation, [[command[1]], command[2]]);
+ } else if (command[0] === 2) { // DELETE
+ } else if (command[0] === 4) { // LINK_TO
+ ids.push(command[1]);
+ } else if (command[0] === 5) { // DELETE ALL
+ ids = [];
+ } else if (command[0] === 6) { // REPLACE WITH
+ ids = command[2];
+ } else {
+ console.error('Command ' + JSON.stringify(command) + ' not supported by the MockServer');
+ }
+ });
+ record[field_changed] = ids;
+ } else if (field.type === 'many2one') {
+ if (value) {
+ var relatedRecord = _.findWhere(this.data[field.relation].records, {
+ id: value
+ });
+ if (!relatedRecord) {
+ throw "Wrong id for a many2one";
+ } else {
+ record[field_changed] = value;
+ }
+ } else {
+ record[field_changed] = false;
+ }
+ } else {
+ record[field_changed] = value;
+ }
+ }
+ },
+});
+
+return MockServer;
+
+});
diff --git a/addons/web/static/tests/helpers/qunit_config.js b/addons/web/static/tests/helpers/qunit_config.js
new file mode 100644
index 00000000000..aa679c60614
--- /dev/null
+++ b/addons/web/static/tests/helpers/qunit_config.js
@@ -0,0 +1,110 @@
+(function() {
+"use strict";
+
+/**
+ * QUnit Config
+ *
+ * The Odoo javascript test framework is based on QUnit (http://qunitjs.com/).
+ * This file is necessary to setup Qunit and to prepare its interactions with
+ * Odoo. It has to be loaded before any tests are defined.
+ *
+ * Note that it is not an Odoo module, because we want this code to be executed
+ * as soon as possible, not whenever the Odoo module system feels like it.
+ */
+
+
+/**
+ * This configuration variable is not strictly necessary, but it ensures more
+ * safety for asynchronous tests. With it, each test has to explicitely tell
+ * QUnit how many assertion it expects, otherwise the test will fail.
+ */
+QUnit.config.requireExpects = true;
+
+/**
+ * not important in normal mode, but in debug=assets, the files are loaded
+ * asynchroneously, which can lead to various issues with QUnit... Notice that
+ * this is done outside of odoo modules, otherwise the setting would not take
+ * effect on time.
+ */
+QUnit.config.autostart = false;
+
+/**
+ * A test timeout of 1 min, before an async test is considered failed.
+ */
+QUnit.config.testTimeout = 1 * 60 * 1000;
+
+
+var sortButtonAppended = false;
+
+/**
+ * This is the way the testing framework knows that tests passed or failed. It
+ * only look in the phantomJS console and check if there is a ok or an error.
+ *
+ * Someday, we should devise a safer strategy...
+ */
+QUnit.done(function(result) {
+ if (!result.failed) {
+ console.log('ok');
+ } else {
+ console.log('error');
+ }
+
+ if (!sortButtonAppended) {
+ _addSortButton();
+ }
+});
+
+/**
+ * This logs various data in the console, which will be available in the log
+ * .txt file generated by the runbot.
+ */
+QUnit.log(function( result) {
+ if (!result.result) {
+ console.log('"' + result.name + '"',
+ 'in section', '"' + result.module + '"',
+ 'failed:', result.message);
+ }
+});
+
+/**
+ * This is done mostly for the .txt log file generated by the runbot.
+ */
+QUnit.moduleDone(function(result) {
+ if (!result.failed) {
+ console.log('"' + result.name + '"', "passed", result.total, "tests.");
+ } else {
+ console.log('"' + result.name + '"',
+ "failed", result.failed,
+ "tests out of", result.total, ".");
+ }
+
+});
+
+/**
+ * Add a sort button on top of the QUnit result page, so we can see which tests
+ * take the most time.
+ */
+function _addSortButton() {
+ sortButtonAppended = true;
+ var $sort = $('<label> sort by time (desc)</label>').css({float: 'right'});
+ $('h2#qunit-userAgent').append($sort);
+ $sort.click(function() {
+ var $ol = $('ol#qunit-tests');
+ var $results = $ol.children('li').get();
+ $results.sort(function (a, b) {
+ var timeA = Number($(a).find('span.runtime').first().text().split(" ")[0]);
+ var timeB = Number($(b).find('span.runtime').first().text().split(" ")[0]);
+ if (timeA < timeB) {
+ return 1;
+ } else if (timeA > timeB) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+ $.each($results, function(idx, $itm) { $ol.append($itm); });
+
+ });
+}
+
+})();
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
new file mode 100644
index 00000000000..442c67b1954
--- /dev/null
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -0,0 +1,442 @@
+odoo.define('web.test_utils', function (require) {
+"use strict";
+
+/**
+ * Test Utils
+ *
+ * In this module, we define various utility functions to help simulate a mock
+ * environment as close as possible as a real environment. The main function is
+ * certainly createView, which takes a bunch of parameters and give you back an
+ * instance of a view, appended in the dom, ready to be tested.
+ */
+
+var AbstractField = require('web.AbstractField');
+var config = require('web.config');
+var core = require('web.core');
+var session = require('web.session');
+var MockServer = require('web.MockServer');
+var Widget = require('web.Widget');
+
+
+/**
+ * intercepts an event bubbling up the widget hierarchy. The event intercepted
+ * must be a "custom event", i.e. an event generated by the method 'trigger_up'.
+ *
+ * Note that this method really intercepts the event. It will not be propagated
+ * further, and even the handlers on the target will not fire.
+ *
+ * @param {Widget} widget the target widget (any Odoo widget)
+ * @param {string} eventName description of the event
+ * @param {function} fn callback executed when the even is intercepted
+ */
+function intercept(widget, eventName, fn) {
+ var _trigger_up = widget._trigger_up.bind(widget);
+ widget._trigger_up = function (event) {
+ if (event.name === eventName) {
+ fn(event);
+ } else {
+ _trigger_up(event);
+ }
+ };
+}
+
+/**
+ * logs all event going through the target widget.
+ *
+ * @param {Widget} widget
+ */
+function observe(widget) {
+ var _trigger_up = widget._trigger_up.bind(widget);
+ widget._trigger_up = function (event) {
+ console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event);
+ _trigger_up(event);
+ };
+}
+
+/**
+ * create a view synchronously. This method uses the createAsyncView method.
+ * Most views are synchronous, so the deferred can be resolved immediately and
+ * this method will work.
+ *
+ * Be careful, if for some reason a view is async, this method will crash.
+ * @see createAsyncView
+ *
+ * @param {Object} params will be given to createAsyncView
+ * @returns {AbstractView}
+ */
+function createView(params) {
+ var view;
+ createAsyncView(params).then(function (result) {
+ view = result;
+ });
+ if (!view) {
+ throw "The view that you are trying to create is async. Please use createAsyncView instead";
+ }
+ return view;
+}
+
+/**
+ * create a view from various parameters. Here, a view means a javascript
+ * instance of an AbstractView class, such as a form view, a list view or a
+ * kanban view.
+ *
+ * It returns the instance of the view, properly created, with all rpcs going
+ * through a mock method using the data object as source, and already loaded/
+ * started (with a do_search). The buttons/pager should also be created, if
+ * appropriate.
+ *
+ * Most views can be tested synchronously (@see createView), but some view have
+ * external dependencies (like lazy loaded libraries). In that case, it is
+ * necessary to use this method.
+ *
+ * @param {Object} params
+ * @param {string} params.arch the xml (arch) of the view to be instantiated
+ * @param {any[]} [params.domain] the initial domain for the view
+ * @param {Object} [params.context] the initial context for the view
+ * @param {Object} [params.debug=false] if true, the widget will be appended in
+ * the DOM. Also, the logLevel will be forced to 2 and the uncaught OdooEvent
+ * will be logged
+ * @param {string[]} [params.groupBy] the initial groupBy for the view
+ * @param {integer} [params.fieldDebounce=0] the debounce value to use for the
+ * duration of the test.
+ * @param {AbstractView} params.View the class that will be instantiated
+ * @param {string} params.model a model name, will be given to the view
+ * @param {Object} params.intercepts an object with event names as key, and
+ * callback as value. Each key,value will be used to intercept the event.
+ * Note that this is particularly useful if you want to intercept events going
+ * up in the init process of the view, because there are no other way to do it
+ * after this method returns
+ * @returns {Deferred<AbstractView>} resolves with the instance of the view
+ */
+function createAsyncView(params) {
+ var $target = $('#qunit-fixture');
+ var widget = new Widget();
+
+ // handle debug parameter: render target, log stuff, ...
+ if (params.debug) {
+ $target = $('body');
+ params.logLevel = 2;
+ observe(widget);
+ var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
+ var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId;
+ console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
+ $target.addClass('debug');
+ }
+
+ // add mock environment: mock server, session, fieldviewget, ...
+ var mockServer = addMockEnvironment(widget, params);
+ var viewInfo = mockServer.fieldsViewGet(params.arch, params.model);
+
+ // create the view
+ var viewOptions = {
+ modelName: params.model || 'foo',
+ ids: 'res_id' in params ? [params.res_id] : undefined,
+ currentId: 'res_id' in params ? params.res_id : undefined,
+ domain: params.domain || [],
+ context: params.context || {},
+ groupBy: params.groupBy || [],
+ };
+
+ _.extend(viewOptions, params.viewOptions);
+
+ var view = new params.View(viewInfo, viewOptions);
+
+ // make sure images do not trigger a GET on the server
+ $('#qunit-fixture').on('DOMNodeInserted.removeSRC', function () {
+ removeSrcAttribute($(this), widget);
+ });
+
+ // reproduce the DOM environment of views
+ var $web_client = $('<div>').addClass('o_web_client').appendTo($target);
+ var $control_panel = $('<div>').addClass('o_control_panel').appendTo($web_client);
+ var $content = $('<div>').addClass('o_content').appendTo($web_client);
+ var $view_manager = $('<div>').addClass('o_view_manager_content').appendTo($content);
+
+ // make sure all Odoo events bubbling up are intercepted
+ if (params.intercepts) {
+ _.each(params.intercepts, function (cb, name) {
+ intercept(widget, name, cb);
+ });
+ }
+
+ return view.getController(widget).then(function (view) {
+ // override the view's 'destroy' so that it calls 'destroy' on the widget
+ // instead, as the widget is the parent of the view and the mockServer.
+ view.destroy = function () {
+ // remove the override to properly destroy the view and its children
+ // when it will be called the second time (by its parent)
+ delete view.destroy;
+ widget.destroy();
+ $('#qunit-fixture').off('DOMNodeInserted.removeSRC');
+ };
+ return view.appendTo($view_manager).then(function () {
+ view.$el.on('click', 'a', function (ev) {
+ ev.preventDefault();
+ });
+ }).then(function () {
+ var $buttons = $('<div>');
+ view.renderButtons($buttons);
+ $buttons.contents().appendTo($control_panel);
+
+ var $sidebar = $('<div>');
+ view.renderSidebar($sidebar);
+ $sidebar.contents().appendTo($control_panel);
+
+ var $pager = $('<div>');
+ view.renderPager($pager);
+ $pager.contents().appendTo($control_panel);
+
+ return view;
+ });
+ });
+}
+
+/**
+ * Add a mock environment to a widget. This helper function can simulate
+ * various kind of side effects, such as mocking RPCs, changing the session,
+ * or the translation settings.
+ *
+ * The simulated environment lasts for the lifecycle of the widget, meaning it
+ * disappears when the widget is destroyed. It is particularly relevant for the
+ * session mocks, because the previous session is restored during the destroy
+ * call. So, it means that you have to be careful and make sure that it is
+ * properly destroyed before another test is run, otherwise you risk having
+ * interferences between tests.
+ *
+ * @param {Widget} widget
+ * @param {Object} params
+ * @param {Object} [params.archs] a map of string [model,view_id,view_type] to
+ * a arch object. It is used to mock answers to 'load_views' custom events.
+ * This is useful when the widget instantiate a formview dialog that needs
+ * to load a particular arch.
+ * @param {string} [params.currentDate] a string representation of the current
+ * date. It is given to the mock server.
+ * @param {Object} params.data the data given to the created mock server. It is
+ * used to generate mock answers for every kind of routes supported by odoo
+ * @param {number} [params.logLevel] the log level. If it is 0, no logging is
+ * done, if 1, some light logging is done, if 2, detailed logs will be
+ * displayed for all rpcs. Most of the time, when working on a test, it is
+ * frequent to set this parameter to 2
+ * @param {function} [params.mockRPC] a function that will be used to override
+ * the _performRpc method from the mock server. It is really useful to add
+ * some custom rpc mocks, or to check some assertions.
+ * @param {Object} [params.session] if it is given, it will be used as answer
+ * for all calls to this.getSession() by the widget, of its children. Also,
+ * it will be used to extend the current, real session. This side effect is
+ * undone when the widget is destroyed.
+ * @param {Object} [params.translateParameters] if given, it will be used to
+ * extend the core._t.database.parameters object. After the widget
+ * destruction, the original parameters will be restored.
+ *
+ * @returns {MockServer} the instance of the mock server, created by this
+ * function. It is necessary for createAsyncView so that method can call some
+ * other methods on it.
+ */
+function addMockEnvironment(widget, params) {
+ var Server = MockServer;
+ if (params.mockRPC) {
+ Server = MockServer.extend({_performRpc: params.mockRPC});
+ }
+ var mockServer = new Server(params.data, {
+ logLevel: params.logLevel,
+ currentDate: params.currentDate,
+ });
+ var widgetDestroy;
+
+ // make sure the debounce value for input fields is set to 0
+ var initialDebounce = AbstractField.prototype.DEBOUNCE;
+ AbstractField.prototype.DEBOUNCE = params.fieldDebounce || 0;
+
+ if ('session' in params) {
+ var initialSession = _.extend({}, session);
+ _.extend(session, params.session);
+ widgetDestroy = widget.destroy;
+ widget.destroy = function () {
+ for (var key in session) {
+ delete session[key];
+ }
+ _.extend(session, initialSession);
+ widgetDestroy.call(this);
+ };
+ }
+
+ if ('config' in params) {
+ var initialConfig = _.extend({}, config);
+ _.extend(config, params.config);
+ widgetDestroy = widget.destroy;
+ widget.destroy = function () {
+ for (var key in config) {
+ delete config[key];
+ }
+ _.extend(config, initialConfig);
+ AbstractField.prototype.DEBOUNCE = initialDebounce;
+ widgetDestroy.call(this);
+ };
+ }
+
+ if ('translateParameters' in params) {
+ var initialParameters = _.extend({}, core._t.database.parameters);
+ _.extend(core._t.database.parameters, params.translateParameters);
+ var oldDestroy = widget.destroy;
+ widget.destroy = function () {
+ for (var key in core._t.database.parameters) {
+ delete core._t.database.parameters[key];
+ }
+ _.extend(core._t.database.parameters, initialParameters);
+ oldDestroy.call(this);
+ };
+ }
+
+
+ intercept(widget, 'call_service', function (event) {
+ if (event.data.service === 'ajax') {
+ var result = mockServer.performRpc(event.data.args[0], event.data.args[1]);
+ event.data.callback(result);
+ }
+ });
+
+ intercept(widget, "load_views", function (event) {
+ if (params.logLevel === 2) {
+ console.log('[mock] load_views', event.data);
+ }
+ var views = {};
+ var model = event.data.modelName;
+ _.each(event.data.views, function (view_descr) {
+ var view_id = view_descr[0] || false;
+ var view_type = view_descr[1];
+ var key = [model, view_id, view_type].join(',');
+ var arch = params.archs[key];
+ if (!arch) {
+ throw new Error('No arch found for key ' + key);
+ }
+ views[view_type] = mockServer.fieldsViewGet(arch, model);
+ });
+
+ event.data.on_success(views);
+ });
+
+ intercept(widget, "get_session", function (event) {
+ event.data.callback(params.session || session);
+ });
+
+ intercept(widget, "load_filters", function (event) {
+ if (params.logLevel === 2) {
+ console.log('[mock] load_filters', event.data);
+ }
+ event.data.on_success([]);
+ });
+
+ return mockServer;
+}
+
+/**
+ * create a model from given parameters.
+ *
+ * @param {Object} params This object will be given to addMockEnvironment, so
+ * any parameters from that method applies
+ * @param {Class} params.Model the model class to use
+ * @returns {Model}
+ */
+function createModel(params) {
+ var widget = new Widget();
+
+ var model = new params.Model(widget);
+
+ addMockEnvironment(widget, params);
+
+ return model;
+}
+
+/**
+ * simulate a drag and drop operation between 2 jquery nodes: $el and $to.
+ * This is a crude simulation, with only the mousedown, mousemove and mouseup
+ * events, but it is enough to help test drag and drop operations with jqueryUI
+ * sortable.
+ *
+ * @param {jqueryElement} $el
+ * @param {jqueryElement} $to
+ */
+function dragAndDrop($el, $to) {
+ var elementCenter = $el.offset();
+ elementCenter.left += $el.outerWidth()/2;
+ elementCenter.top += $el.outerHeight()/2;
+
+ var toCenter = $to.offset();
+ toCenter.left += $to.outerWidth()/2;
+ toCenter.top += $to.outerHeight()/2;
+
+ $el.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: elementCenter.left,
+ pageY: elementCenter.top
+ }));
+
+ $el.trigger($.Event("mousemove", {
+ which: 1,
+ pageX: toCenter.left,
+ pageY: toCenter.top
+ }));
+
+ $el.trigger($.Event("mouseup", {
+ which: 1,
+ pageX: toCenter.left,
+ pageY: toCenter.top
+ }));
+}
+
+/**
+ * simulate a mouse event with a custom event who add the item position. This is
+ * sometimes necessary because the basic way to trigger an event (such as
+ * $el.trigger('mousemove')); ) is too crude for some uses.
+ *
+ * @param {jqueryElement} $el
+ * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
+ */
+function triggerMouseEvent($el, type) {
+ var pos = $el.offset();
+ var e = new jQuery.Event(type);
+ e.pageX = pos.left;
+ e.pageY = pos.top;
+ e.which = 1;
+ $el.trigger(e);
+}
+
+/**
+ * Removes the src attribute on images and iframes to prevent not found errors,
+ * and optionally triggers an rpc with the src url as route on a widget.
+ *
+ * @param {JQueryElement} $el
+ * @param {[Widget]} widget the widget on which the rpc should be performed
+ */
+function removeSrcAttribute($el, widget) {
+ $el.find('img, iframe[src]').each(function () {
+ var src = $(this).attr('src');
+ if (src[0] !== '#') {
+ $(this).attr('src', '#test:' + $(this).attr('src'));
+ if (widget) {
+ widget._rpc({route: src});
+ }
+ }
+ });
+}
+
+return session.is_bound.then(function () {
+ setTimeout(function () {
+ // this is done with the hope that tests are
+ // only started all together...
+ QUnit.start();
+ }, 0);
+ return {
+ intercept: intercept,
+ observe: observe,
+ createView: createView,
+ createAsyncView: createAsyncView,
+ createModel: createModel,
+ addMockEnvironment: addMockEnvironment,
+ dragAndDrop: dragAndDrop,
+ triggerMouseEvent: triggerMouseEvent,
+ removeSrcAttribute: removeSrcAttribute,
+ };
+});
+
+});
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
new file mode 100644
index 00000000000..15947103e6e
--- /dev/null
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -0,0 +1,1220 @@
+odoo.define('web.basic_model_tests', function (require) {
+"use strict";
+
+var BasicModel = require('web.BasicModel');
+var testUtils = require('web.test_utils');
+
+var createModel = testUtils.createModel;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: {string: "STRING", type: 'char'},
+ total: {string: "Total", type: 'integer'},
+ foo: {string: "Foo", type: 'char'},
+ bar: {string: "Bar", type: 'integer'},
+ qux: {string: "Qux", type: 'many2one', relation: 'partner'},
+ product_id: {string: "Favorite product", type: 'many2one', relation: 'product'},
+ product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
+ category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
+ date: {string: "Date Field", type: 'date'},
+ },
+ records: [
+ {id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner", date: "2017-01-25"},
+ {id: 2, foo: 'gnap', bar: 2, product_id: 41, display_name: "second partner"},
+ ],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [
+ {id: 37, display_name: "xphone"},
+ {id: 41, display_name: "xpad"}
+ ]
+ },
+ partner_type: {
+ fields: {
+ display_name: {string: "Partner Type", type: "char"},
+ date: {string: "Date Field", type: 'date'},
+ },
+ records: [
+ {id: 12, display_name: "gold", date: "2017-01-25"},
+ {id: 14, display_name: "silver"},
+ {id: 15, display_name: "bronze"}
+ ]
+ },
+ };
+
+ // add related fields to category.
+ this.data.partner.fields.category.relatedFields =
+ $.extend(true, {}, this.data.partner_type.fields);
+ this.params = {
+ res_id: 2,
+ modelName: 'partner',
+ fields: this.data.partner.fields,
+ };
+ }
+}, function () {
+ QUnit.module('BasicModel');
+
+ QUnit.test('simple functionality', function (assert) {
+ assert.expect(6);
+
+ this.params.fieldNames = ['foo'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ assert.strictEqual(model.get(1), null, "should return null for non existing key");
+
+ model.load(this.params).then(function (resultID) {
+ // it is a string, because it is used as a key in an object
+ assert.strictEqual(typeof resultID, 'string', "result should be a valid id");
+
+ var record = model.get(resultID);
+ assert.strictEqual(record.res_id, 2, "res_id read should be the same as asked");
+ assert.strictEqual(record.type, 'record', "should be of type 'record'");
+ assert.strictEqual(record.data.foo, "gnap", "should correctly read value");
+ assert.strictEqual(record.data.bar, undefined, "should not fetch the field 'bar'");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('notify change with many2one', function (assert) {
+ assert.expect(2);
+
+ this.params.fieldNames = ['foo', 'qux'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.qux, false, "qux field should be false");
+ model.notifyChanges(resultID, {qux: {id: 1, display_name: "hello"}});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.qux.data.id, 1, "qux field should be 1");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('write on a many2one', function (assert) {
+ assert.expect(4);
+ var self = this;
+
+ this.params.fieldNames = ['product_id'];
+
+ var rpcCount = 0;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, 'xpad',
+ "should be initialized with correct value");
+
+ model.notifyChanges(resultID, {product_id: {id: 37, display_name: 'xphone'}});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, 'xphone',
+ "should be changed with correct value");
+
+ model.save(resultID);
+
+ assert.strictEqual(self.data.partner.records[1].product_id, 37,
+ "should have really saved the data");
+ assert.strictEqual(rpcCount, 3, "should have done 3 rpc: 1 read, 1 write, 1 read");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('basic onchange', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.bar = obj.foo.length;
+ };
+
+ this.params.fieldNames = ['foo', 'bar'];
+ this.params.context = {hello: 'world'};
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var context = args.args[4];
+ assert.deepEqual(context, {hello: 'world'},
+ "context should be sent by the onchange");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'gnap', "foo field is properly initialized");
+ assert.strictEqual(record.data.bar, 2, "bar field is properly initialized");
+
+ model.notifyChanges(resultID, {foo: 'mary poppins'});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'mary poppins', "onchange has been applied");
+ assert.strictEqual(record.data.bar, 12, "onchange has been applied");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('onchange with a many2one', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges.product_id = function (obj) {
+ if (obj.product_id === 37) {
+ obj.foo = "space lollipop";
+ }
+ };
+
+ this.params.fieldNames = ['foo', 'product_id'];
+
+ var rpcCount = 0;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.strictEqual(args.args[2], "product_id",
+ "should send the only changed field as a string, not a list");
+ }
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'gnap', "foo field is properly initialized");
+ assert.strictEqual(record.data.product_id.data.id, 41, "product_id field is properly initialized");
+
+ model.notifyChanges(resultID, {product_id: {id: 37, display_name: 'xphone'}});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'space lollipop', "onchange has been applied");
+ assert.strictEqual(rpcCount, 2, "should have done 2 rpc: 1 read and 1 onchange");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('notifyChange on a many2one, without display_name', function (assert) {
+ assert.expect(3);
+
+ this.params.fieldNames = ['product_id'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.strictEqual(args.model, 'product');
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, 'xpad',
+ "product_id field is set to xpad");
+
+ model.notifyChanges(resultID, {product_id: {id: 37}});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, 'xphone',
+ "display_name should have been fetched");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('onchange on a char with an unchanged many2one', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.foo = obj.foo + " alligator";
+ };
+
+ this.params.fieldNames = ['foo', 'product_id'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.strictEqual(args.args[1].product_id, 41, "should send correct value");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ model.notifyChanges(resultID, {foo: 'cookie'});
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'cookie alligator', "onchange has been applied");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('onchange on a char with another many2one not set to a value', function (assert) {
+ assert.expect(2);
+ this.data.partner.records[0].product_id = false;
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.foo = obj.foo + " alligator";
+ };
+
+ this.params.fieldNames = ['foo', 'product_id'];
+ this.params.res_id = 1;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id, false, "product_id is not set");
+
+ model.notifyChanges(resultID, {foo: 'cookie'});
+ record = model.get(resultID);
+ assert.strictEqual(record.data.foo, 'cookie alligator', "onchange has been applied");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('can get a many2many', function (assert) {
+ assert.expect(3);
+
+ this.params.res_id = 1;
+ this.params.fieldsInfo = {
+ default: {
+ category: {
+ fieldsInfo: {default: {display_name: {}}},
+ relatedFields: {display_name: {type: "char"}},
+ viewType: 'default',
+ },
+ },
+ };
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.category.data[0].res_id, 12,
+ "should have loaded many2many res_ids");
+ assert.strictEqual(record.data.category.data[0].data.display_name, "gold",
+ "should have loaded many2many display_name");
+ record = model.get(resultID, {raw: true});
+ assert.deepEqual(record.data.category, [12],
+ "with option raw, category should only return ids");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('can use command add and get many2many value with date field', function (assert) {
+ assert.expect(2);
+
+ this.params.fieldsInfo = {
+ default: {
+ category: {
+ fieldsInfo: {default: {date: {}}},
+ relatedFields: {date: {type: "date"}},
+ viewType: 'default',
+ },
+ },
+ };
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var changes = {
+ category: {operation: 'ADD_M2M', ids: [{id: 12}]}
+ };
+ model.notifyChanges(resultID, changes).then(function () {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.category.data.length, 1, "should have added one category");
+ assert.strictEqual(record.data.category.data[0].data.date instanceof moment,
+ true, "should have a date parsed in a moment object");
+ });
+ });
+ model.destroy();
+ });
+
+ QUnit.test('can fetch a list', function (assert) {
+ assert.expect(4);
+
+ this.params.fieldNames = ['foo'];
+ this.params.domain = [];
+ this.params.groupedBy = [];
+ this.params.res_id = undefined;
+
+ var rpcCount = 0;
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+
+ assert.strictEqual(record.type, 'list', "record fetched should be a list");
+ assert.strictEqual(record.data.length, 2, "should have fetched 2 records");
+ assert.strictEqual(record.data[0].data.foo, 'blip', "first record should have 'blip' in foo field");
+ assert.strictEqual(rpcCount, 1, "should have done 1 rpc (searchread)");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('fetch x2manys in list, with not too many rpcs', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].category = [12, 15];
+ this.data.partner.records[1].category = [12, 14];
+
+ this.params.fieldNames = ['category'];
+ this.params.domain = [];
+ this.params.groupedBy = [];
+ this.params.res_id = undefined;
+
+ var rpcCount = 0;
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+
+ assert.strictEqual(record.data[0].data.category.data.length, 2,
+ "first record should have 2 categories loaded");
+ assert.strictEqual(rpcCount, 2, "should have done 2 rpc (searchread and read category)");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('can make a default_record, no onchange', function (assert) {
+ assert.expect(3);
+
+ this.params.context = {};
+ this.params.fieldNames = ['product_id', 'category', 'product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id, false, "m2o default value should be false");
+ assert.deepEqual(record.data.product_ids.data, [], "o2m default should be []");
+ assert.deepEqual(record.data.category.data, [], "m2m default should be []");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('default_record, with onchange on many2one', function (assert) {
+ assert.expect(1);
+
+ // the onchange is done by the mockRPC because we want to return a value
+ // of 'false', which does not work with the mockserver mockOnChange method.
+ this.data.partner.onchanges.product_id = true;
+
+ this.params.context = {};
+ this.params.fieldNames = ['product_id'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({value: { product_id: false }});
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id, false, "m2o default value should be false");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('default record: batch namegets on same model and res_id', function (assert) {
+ assert.expect(3);
+
+ var rpcCount = 0;
+ var fields = this.data.partner.fields;
+ fields.other_product_id = _.extend({}, fields.product_id);
+ fields.product_id.default = 37;
+ fields.other_product_id.default = 41;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ var params = {
+ context: {},
+ fieldNames: ['other_product_id', 'product_id'],
+ fields: fields,
+ modelName: 'partner',
+ type: 'record',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, "xphone",
+ "should have fetched correct name");
+ assert.strictEqual(record.data.other_product_id.data.display_name, "xpad",
+ "should have fetched correct name");
+ assert.strictEqual(rpcCount, 2, "should have done 2 rpcs: default_get and 1 name_get");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('undoing a change makes the record not dirty', function (assert) {
+ assert.expect(4);
+
+ this.params.fieldNames = ['foo'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.foo, "gnap", "foo field should properly be set");
+ assert.ok(!model.isDirty(resultID), "record should not be dirty");
+ model.notifyChanges(resultID, {foo: "hello"});
+ assert.ok(model.isDirty(resultID), "record should be dirty");
+ model.notifyChanges(resultID, {foo: "gnap"});
+ assert.ok(!model.isDirty(resultID), "record should not be dirty");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('isDirty works correctly on list made empty', function (assert) {
+ assert.expect(3);
+
+ this.params.fieldNames = ['category'];
+ this.params.res_id = 1;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ var category_value = record.data.category;
+ assert.ok(_.isObject(category_value), "category field should have been fetched");
+ assert.strictEqual(category_value.data.length, 1, "category field should contain one record");
+ model.notifyChanges(resultID, {category: {
+ operation: 'REMOVE',
+ id: category_value.data[0].id
+ }});
+ assert.ok(model.isDirty(resultID), "record should be considered dirty");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('can duplicate a record', function (assert) {
+ assert.expect(4);
+
+ this.params.fieldNames = ['foo'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.display_name, "second partner",
+ "record should have correct display name");
+ assert.strictEqual(record.data.foo, "gnap", "foo should be set to correct value");
+ model.duplicateRecord(resultID).then(function (duplicateID) {
+ var duplicate = model.get(duplicateID);
+ assert.strictEqual(duplicate.data.display_name, "second partner (copy)",
+ "record should have been duplicated");
+ assert.strictEqual(duplicate.data.foo, "gnap", "foo should be set to correct value");
+ });
+ });
+ model.destroy();
+ });
+
+ QUnit.test('record with many2one set to some value, then set it to none', function (assert) {
+ assert.expect(3);
+
+ this.params.fieldNames = ['product_id'];
+
+ var self = this;
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_id.data.display_name, 'xpad', "product_id should be set");
+ model.notifyChanges(resultID, {product_id: false});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_id, false, "product_id should not be set");
+
+ model.save(resultID);
+
+ assert.strictEqual(self.data.partner.records[1].product_id, false,
+ "should have saved the new product_id value");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('internal state of groups remains when reloading', function (assert) {
+ assert.expect(9);
+
+ this.params.fieldNames = ['foo'];
+ this.params.domain = [];
+ this.params.limit = 80;
+ this.params.groupedBy = ['product_id'];
+ this.params.res_id = undefined;
+
+ var filterEnabled = false;
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group' && filterEnabled) {
+ // as this is not yet supported by the MockServer, simulates
+ // a read_group that returns empty groups
+ // this is the case for several models (e.g. project.task
+ // grouped by stage_id)
+ return this._super.apply(this, arguments).then(function (result) {
+ // artificially filter out records of first group
+ result[0].product_id_count = 0;
+ return result;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.length, 2, "should have 2 groups");
+ var groupID = record.data[0].id;
+ model.toggleGroup(groupID);
+
+ record = model.get(resultID);
+ assert.ok(record.data[0].isOpen, "first group should be open");
+ assert.strictEqual(record.data[0].data.length, 1,
+ "first group should have one record");
+ assert.strictEqual(record.data[0].limit, 80,
+ "limit should be 80 by default");
+
+ // change the limit and offset of the first group
+ model.localData[record.data[0].id].limit = 10;
+
+ model.reload(resultID);
+ record = model.get(resultID);
+ assert.ok(record.data[0].isOpen, "first group should still be open");
+ assert.strictEqual(record.data[0].data.length, 1,
+ "first group should still have one record");
+ assert.strictEqual(record.data[0].limit, 10,
+ "new limit should have been kept");
+
+ // filter some records out: the open group should stay open but now
+ // be empty
+ filterEnabled = true;
+ model.reload(resultID);
+ record = model.get(resultID);
+ assert.strictEqual(record.data[0].count, 0,
+ "first group's count should be 0");
+ assert.strictEqual(record.data[0].data.length, 0,
+ "first group's data should be empty'");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('create record, then save', function (assert) {
+ assert.expect(4);
+
+ this.params.fieldNames = ['product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var id;
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ // has to be done before the call to _super
+ assert.notOk('product_ids' in args.args[0], "should not have any value");
+ assert.notOk('category' in args.args[0], "should not have other fields");
+ }
+ var result = this._super(route, args);
+ if (args.method === 'create') {
+ result.then(function (res) {
+ id = res;
+ });
+ }
+ return result;
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ model.save(record.id, {reload: false});
+ record = model.get(resultID);
+ assert.strictEqual(record.res_id, id, "should have correct id from server");
+ assert.strictEqual(record.data.id, id, "should have correct id from server");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('write commands on a one2many', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[1].product_ids = [37];
+
+ this.params.fieldNames = ['product_ids'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[0], [2], "should write on res_id = 2");
+ var commands = args.args[1].product_ids;
+ assert.deepEqual(commands[0], [4, 37, false], "first command should be a 4");
+ // TO DO: uncomment next line
+ // assert.strictEqual(commands[1], [0, false, {name: "toy"}], "second command should be a 0");
+ assert.strictEqual(commands[1][0], 0, "second command should be a 0");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID, {raw: true});
+ assert.deepEqual(record.data.product_ids, [37], "should have correct initial value");
+
+ model.makeRecord('product', [{
+ name: 'name',
+ string: "Product Name",
+ type: "char",
+ value: "xpod"
+ }
+ ]).then(function (relatedRecordID) {
+ model.notifyChanges(record.id, {
+ product_ids: {operation: "ADD", id: relatedRecordID}
+ });
+ model.save(record.id);
+ });
+ });
+ model.destroy();
+ });
+
+ QUnit.test('onchange with a one2many on a new record', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.total.default = 50;
+ this.data.partner.onchanges.product_ids = function (obj) {
+ obj.total += 100;
+ };
+
+ this.params.fieldNames = ['total', 'product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange' && args.args[1].total === 150) {
+ assert.deepEqual(args.args[1].product_ids, [[0, false, {name: "xpod"}]],
+ "Should have sent the create command in the onchange");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.data.length, 0,
+ "one2many should start with a list of length 0");
+
+ model.makeRecord('product', [{
+ name: 'name',
+ string: "Product Name",
+ type: "char",
+ value: "xpod"
+ }
+ ]).then(function (relatedRecordID) {
+ model.notifyChanges(resultID, {
+ product_ids: {operation: "ADD", id: relatedRecordID}
+ });
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.data.length, 1,
+ "one2many should be a list of length 1");
+ assert.strictEqual(record.data.product_ids.data[0].data.name, "xpod",
+ "one2many should have correct data");
+ });
+ });
+ model.destroy();
+ });
+
+ QUnit.test('dates are properly loaded and parsed (record)', function (assert) {
+ assert.expect(2);
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['date'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ res_id: 1,
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.ok(record.data.date instanceof moment,
+ "fetched date field should have been formatted");
+ });
+
+ params.res_id = 2;
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.date, false,
+ "unset date field should be false");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('dates are properly loaded and parsed (list)', function (assert) {
+ assert.expect(2);
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['date'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ type: 'list',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ var firstRecord = record.data[0];
+ var secondRecord = record.data[1];
+ assert.ok(firstRecord.data.date instanceof moment,
+ "fetched date field should have been formatted");
+ assert.strictEqual(secondRecord.data.date, false,
+ "if date is not set, it should be false");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('dates are properly loaded and parsed (default_get)', function (assert) {
+ assert.expect(1);
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['date'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ type: 'record',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.date, false, "date default value should be false");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('default_get on x2many may return a list of ids', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.category.default = [12, 14];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['category'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ type: 'record',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.ok(_.isEqual(record.data.category.res_ids, [12, 14]),
+ "category field should have correct default value");
+ });
+
+ model.destroy();
+ });
+
+ QUnit.test('contexts and domains can be properly fetched', function (assert) {
+ assert.expect(8);
+
+ this.data.partner.fields.product_id.context = "{'hello': 'world', 'test': foo}";
+ this.data.partner.fields.product_id.domain = "[['hello', 'like', 'world'], ['test', 'like', foo]]";
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ this.params.fieldNames = ['product_id', 'foo'];
+
+ model.load(this.params).then(function (resultID) {
+ var recordPartner = model.get(resultID);
+ assert.strictEqual(typeof recordPartner.getContext, "function",
+ "partner record should have a getContext function");
+ assert.strictEqual(typeof recordPartner.getDomain, "function",
+ "partner record should have a getDomain function");
+ assert.deepEqual(recordPartner.getContext(), {},
+ "asking for a context without a field name should fetch the session/user/view context");
+ assert.deepEqual(recordPartner.getDomain(), [],
+ "asking for a domain without a field name should fetch the session/user/view domain");
+ assert.deepEqual(
+ recordPartner.getContext({fieldName: "product_id"}),
+ {hello: "world", test: "gnap"},
+ "asking for a context with a field name should fetch the field context (evaluated)");
+ assert.deepEqual(
+ recordPartner.getDomain({fieldName: "product_id"}),
+ [["hello", "like", "world"], ["test", "like", "gnap"]],
+ "asking for a domain with a field name should fetch the field domain (evaluated)");
+ });
+ model.destroy();
+
+ // Try again with xml override of field domain and context
+ model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ this.params.fieldsInfo = {
+ default: {
+ foo: {},
+ product_id: {
+ context: "{'hello2': 'world', 'test2': foo}",
+ domain: "[['hello2', 'like', 'world'], ['test2', 'like', foo]]",
+ },
+ }
+ };
+
+ model.load(this.params).then(function (resultID) {
+ var recordPartner = model.get(resultID);
+ assert.deepEqual(
+ recordPartner.getContext({fieldName: "product_id"}),
+ {hello2: "world", test2: "gnap"},
+ "field context should have been overriden by xml attribute");
+ assert.deepEqual(
+ recordPartner.getDomain({fieldName: "product_id"}),
+ [["hello2", "like", "world"], ["test2", "like", "gnap"]],
+ "field domain should have been overriden by xml attribute");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('dont write on readonly fields (except if create)', function (assert) {
+ assert.expect(6);
+
+ this.params.fieldNames = ['foo', 'bar'];
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.bar = obj.foo.length;
+ };
+ this.data.partner.fields.bar.readonly = true;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {foo: "verylongstring"},
+ "should only save foo field");
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ foo: "anotherverylongstring",
+ bar: 21,
+ }, "should also save bar field in create mode");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 2,
+ "should be initialized with correct value");
+
+ model.notifyChanges(resultID, {foo: "verylongstring"});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 14,
+ "should be changed with correct value");
+
+ model.save(resultID);
+ });
+
+ // start again, but with a new record
+ delete this.params.res_id;
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.bar, false,
+ "should be initialized with correct value");
+
+ model.notifyChanges(resultID, {foo: "anotherverylongstring"});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 21,
+ "should be changed with correct value");
+
+ model.save(resultID);
+ });
+ model.destroy();
+ });
+
+ QUnit.test('default_get with one2many values', function (assert) {
+ assert.expect(1);
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ product_ids: [[0, 0, {"name": "xdroid"}]]
+ });
+ }
+ return this._super(route, args);
+ },
+ });
+ var params = {
+ fieldNames: ['product_ids'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ type: 'record',
+ };
+ model.load(params).then(function (resultID) {
+ assert.strictEqual(typeof resultID, 'string', "result should be a valid id");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('get method works on unset x2many, after save without reload', function (assert) {
+ assert.expect(3);
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['product_ids'],
+ fields: this.data.partner.fields,
+ modelName: 'partner',
+ type: 'record',
+ };
+
+ model.load(params).then(function (resultID) {
+ assert.ok(model.isDirty(resultID), "datapoint should be dirty");
+ model.save(resultID, {reload: false});
+ var record = model.get(resultID, {raw: true});
+ assert.deepEqual(record.data.product_ids, [],
+ "should have correct value for product_ids field");
+ assert.notOk(model.isDirty(resultID), "datapoint should not be dirty");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('call makeRecord with a pre-fetched many2one field', function (assert) {
+ assert.expect(3);
+ var rpcCount = 0;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.makeRecord('coucou', [{
+ name: 'partner_id',
+ relation: 'partner',
+ type: 'many2one',
+ value: [1, 'first partner'],
+ }], {
+ partner_id: {
+ options: {
+ no_open: true,
+ },
+ },
+ }).then(function (recordID) {
+ var record = model.get(recordID);
+ assert.deepEqual(record.fieldsInfo.default.partner_id, {options: {no_open: true}},
+ "makeRecord should have generated the fieldsInfo");
+ assert.deepEqual(record.data.partner_id.data, {id: 1, display_name: 'first partner'},
+ "many2one should contain the partner with id 1");
+ assert.strictEqual(rpcCount, 0, "makeRecord should not have done any rpc");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('call makeRecord with a many2many field', function (assert) {
+ assert.expect(5);
+ var rpcCount = 0;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.makeRecord('coucou', [{
+ name: 'partner_ids',
+ fields: [{
+ name: 'id',
+ type: 'integer',
+ }, {
+ name: 'display_name',
+ type: 'char',
+ }],
+ relation: 'partner',
+ type: 'many2many',
+ value: [1, 2],
+ }]).then(function (recordID) {
+ var record = model.get(recordID);
+ assert.deepEqual(record.fieldsInfo.default.partner_ids, {},
+ "makeRecord should have generated the fieldsInfo");
+ assert.strictEqual(record.data.partner_ids.count, 2,
+ "there should be 2 elements in the many2many");
+ assert.strictEqual(record.data.partner_ids.data.length, 2,
+ "many2many should be a list of length 2");
+ assert.deepEqual(record.data.partner_ids.data[0].data, {id: 1, display_name: 'first partner'},
+ "many2many should contain the partner with id 1");
+ assert.strictEqual(rpcCount, 1, "makeRecord should have done 1 rpc");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('call makeRecord with a pre-fetched many2many field', function (assert) {
+ assert.expect(5);
+ var rpcCount = 0;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+
+ model.makeRecord('coucou', [{
+ name: 'partner_ids',
+ fields: [{
+ name: 'id',
+ type: 'integer',
+ }, {
+ name: 'display_name',
+ type: 'char',
+ }],
+ relation: 'partner',
+ type: 'many2many',
+ value: [{
+ id: 1,
+ display_name: "first partner",
+ }, {
+ id: 2,
+ display_name: "second partner",
+ }],
+ }]).then(function (recordID) {
+ var record = model.get(recordID);
+ assert.deepEqual(record.fieldsInfo.default.partner_ids, {},
+ "makeRecord should have generated the fieldsInfo");
+ assert.strictEqual(record.data.partner_ids.count, 2,
+ "there should be 2 elements in the many2many");
+ assert.strictEqual(record.data.partner_ids.data.length, 2,
+ "many2many should be a list of length 2");
+ assert.deepEqual(record.data.partner_ids.data[0].data, {id: 1, display_name: 'first partner'},
+ "many2many should contain the partner with id 1");
+ assert.strictEqual(rpcCount, 0, "makeRecord should not have done any rpc");
+ });
+ model.destroy();
+ });
+
+});});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
new file mode 100644
index 00000000000..33e989c5cee
--- /dev/null
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -0,0 +1,340 @@
+odoo.define('web.calendar_tests', function (require) {
+"use strict";
+
+var CalendarView = require('web.CalendarView');
+var testUtils = require('web.test_utils');
+var session = require('web.session');
+
+var createView = testUtils.createView;
+
+var initialDate = new Date("2016-12-12T08:00:00Z");
+
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ session.uid = -1; // TO CHECK
+ this.data = {
+ event: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ user_id: {string: "user", type: "many2one", relation: 'user'},
+ parnter_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.parnter_id'},
+ name: {string: "name", type: "char"},
+ start: {string: "start", type: "datetime"},
+ stop: {string: "stop", type: "datetime"},
+ allday: {string: "allday", type: "boolean"},
+ partner_ids: {string: "attendees", type: "one2many", relation: 'partner'},
+ type: {string: "type", type: "integer"},
+ },
+ records: [
+ {id: 1, user_id: session.uid, partner_id: 1, name: "event 1", start: "2016-12-11 00:00:00", stop: "2016-12-11 00:00:00", allday: false, partner_ids: [1,2,3], type: 1},
+ {id: 2, user_id: session.uid, partner_id: 1, name: "event 2", start: "2016-12-12 10:55:05", stop: "2016-12-12 14:55:05", allday: false, partner_ids: [1,2], type: 3},
+ {id: 3, user_id: 4, partner_id: 4, name: "event 3", start: "2016-12-12 15:55:05", stop: "2016-12-12 16:55:05", allday: false, partner_ids: [1], type: 2},
+ {id: 4, user_id: session.uid, partner_id: 1, name: "event 4", start: "2016-12-14 15:55:05", stop: "2016-12-14 18:55:05", allday: true, partner_ids: [1], type: 2},
+ {id: 5, user_id: 4, partner_id: 4, name: "event 5", start: "2016-12-13 15:55:05", stop: "2016-12-20 18:55:05", allday: false, partner_ids: [2,3], type: 2},
+ {id: 6, user_id: session.uid, partner_id: 1, name: "event 6", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [3], type: 3}
+ ],
+ check_access_rights: function () {
+ return $.when(true);
+ }
+ },
+ user: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ display_name: {string: "Displayed name", type: "char"},
+ parnter_id: {string: "partner", type: "many2one", relation: 'partner'},
+ image: {string: "image", type: "integer"},
+ },
+ records: [
+ {id: session.uid, display_name: "user 1", partner_id: 1},
+ {id: 4, display_name: "user 4", partner_id: 4},
+ ]
+ },
+ partner: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ display_name: {string: "Displayed name", type: "char"},
+ image: {string: "image", type: "integer"},
+ },
+ records: [
+ {id: 1, display_name: "partner 1", image: 'AAA'},
+ {id: 2, display_name: "partner 2", image: 'BBB'},
+ {id: 3, display_name: "partner 3", image: 'CCC'},
+ {id: 4, display_name: "partner 4", image: 'DDD'}
+ ]
+ },
+ filter_partner: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ user_id: {string: "user", type: "many2one", relation: 'user'},
+ partner_id: {string: "partner", type: "many2one", relation: 'partner'},
+ },
+ records: [
+ {id: 1, user_id: session.uid, partner_id: 1},
+ {id: 2, user_id: session.uid, partner_id: 2},
+ {id: 3, user_id: 4, partner_id: 3}
+ ]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('CalendarView');
+
+ var archs = {
+ "event,false,form": {
+ attrs: {},
+ children: [
+ {
+ attrs: {name: "name"},
+ children: [],
+ tag: 'field'
+ },
+ {
+ attrs: {name: "allday"},
+ children: [],
+ tag: 'field'
+ },
+ {
+ attrs: {name: "start"},
+ children: [],
+ tag: 'field'
+ },
+ {
+ attrs: {name: "stop"},
+ children: [],
+ tag: 'field'
+ }
+ ],
+ tag: "form"
+ },
+ "event,1,form": {
+ attrs: {},
+ children: [
+ {
+ attrs: {
+ modifiers: '{"invisible": true}',
+ name: "allday"
+ },
+ children: [],
+ tag: 'field'
+ },
+ {
+ attrs: {
+ modifiers: '{"invisible": [["allday","=",false]]}',
+ name: "start"
+ },
+ children: [],
+ tag: 'field'
+ },
+ {
+ attrs: {
+ modifiers: '{"invisible": [["allday","=",true]]}',
+ name: "stop"
+ },
+ children: [],
+ tag: 'field'
+ }
+ ],
+ tag: "form"
+ }
+ };
+
+ QUnit.test('simple calendar rendering', function (assert) {
+ assert.expect(19);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'scale_zoom="week" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'all_day="allday" '+
+ 'mode="week" '+
+ 'attendee="partner_ids" '+
+ 'color="parnter_id">'+
+ '<field name="name"/>'+
+ '<filter name="user_id" avatar_field="image"/>'+
+ '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ assert.ok(calendar.$('.o_calendar_view').find('.fc-view-container').length, "should instance of fullcalendar");
+
+ var $sidebar = calendar.$('.o_calendar_sidebar');
+
+ assert.strictEqual($sidebar.find('.ui-state-active').text(), "12", "should highlight the target day");
+
+ // test view scales
+
+ assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)");
+ assert.strictEqual($sidebar.find('.o_selected_range').length, 7, "week scale should highlight 7 days in mini calendar");
+
+ calendar.$buttons.find('.o_calendar_button_day').trigger('click'); // display only one day
+ assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
+ assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar");
+
+ calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month
+ assert.strictEqual(calendar.$('.fc-event').length, 6, "should display 6 events on the month (5 events + 2 week event - 1 'event 6' is filtered)");
+ assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar");
+
+ // test filters
+
+ assert.strictEqual($sidebar.find('.o_calendar_filter').length, 2, "should display 3 filters");
+
+ var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))');
+ assert.ok($typeFilter.length, "should display 'user' filter");
+ assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 1, "should display 1 filter items for 'user'");
+
+ var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))');
+ assert.ok($attendeesFilter.length, "should display 'attendees' filter");
+ assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
+ assert.ok($attendeesFilter.find('.o_form_field_many2one').length, "should display one2many search bar for 'attendees' filter");
+
+ // test search bar in filter
+
+ $sidebar.find('input.o_form_input').trigger('click');
+ assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2, "should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
+ $('ul.ui-autocomplete li:first').trigger('click');
+ assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'");
+ $sidebar.find('input.o_form_input').trigger('click');
+ assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
+ $sidebar.find('.o_calendar_filter_item[data-id="1"] .o_remove').trigger('click');
+ assert.ok($('.modal button.btn:contains(Ok)').length, "should display the confirm message");
+ $('.modal button.btn:contains(Ok)').trigger('click');
+ assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'");
+ calendar.destroy();
+ });
+
+ QUnit.test('create and change events', function (assert) {
+ assert.expect(19);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'scale_zoom="week" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'all_day="allday" '+
+ 'mode="month" '+
+ 'readonly_form_view_id="1">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ assert.ok(calendar.$('.fc-month-view').length, "should display in month mode");
+
+ // click on an existing event to open the formViewDialog
+
+ calendar.$('.fc-event:contains(event 4) .fc-content').trigger('click');
+
+ assert.ok($('.modal-body').length, "should open the form view in dialog when click on event");
+ assert.ok($('.modal button.btn:contains(Edit)').length, "formViewDialog should be in readonly mode");
+ assert.ok($('.modal button.btn:contains(Delete)').length, "formViewDialog should have a delete button");
+
+ $('.modal button.btn:contains(Edit)').trigger('click');
+
+ assert.ok($('.modal-body').length, "should switch the modal in edit mode");
+ assert.notOk($('.modal button.btn:contains(Delete)').length, "formViewDialog should not have a delete button in edit mode");
+
+ $('.modal-body input:first').val('event 4 modified').trigger('input');
+ $('.modal button.btn:contains(Save)').trigger('click');
+
+ assert.notOk($('.modal-body').length, "save button should close the modal");
+ assert.ok(calendar.$('.fc-event:contains(event 4 modified)').length, "should display the updated records");
+
+ // create a new event, quick create only
+
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
+
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+
+ assert.ok($('.modal-dialog.modal-sm').length, "should open the quick create dialog");
+
+ $('.modal-body input:first').val('new event in quick create').trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click');
+
+ assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1, "should display the new record");
+ assert.strictEqual(calendar.$('td.fc-event-container[colspan]').length, 2, "should the new record have only one day");
+
+ // create a new event with 2 days
+
+ $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)');
+
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell.next(), "mousemove");
+ testUtils.triggerMouseEvent($cell.next(), "mouseup");
+
+ $('.modal input:first').val('new event in quick create 2').trigger('input');
+ $('.modal button.btn:contains(Edit)').trigger('click');
+
+ assert.strictEqual($('.modal-lg input:first').val(), 'new event in quick create 2', "should open the formViewDialog with default values");
+
+ $('.modal-lg button.btn:contains(Save)').trigger('click');
+
+ assert.notOk($('.modal').length, "should close dialogs");
+ var $newevent2 = calendar.$('.fc-event:contains(new event in quick create 2)');
+ assert.ok($newevent2.length, "should display the 2 days new record");
+ assert.strictEqual($newevent2.closest('.fc-event-container').attr('colspan'), "2", "the new record should have 2 days");
+
+ // delete the a record
+
+ calendar.$('.fc-event:contains(event 4) .fc-content').trigger('click');
+ $('.modal button.btn:contains(Delete)').trigger('click');
+ assert.ok($('.modal button.btn:contains(Ok)').length, "should display the confirm message");
+ $('.modal button.btn:contains(Ok)').trigger('click');
+ assert.notOk(calendar.$('.fc-event:contains(event 4) .fc-content').length, "the record should be deleted");
+
+ assert.strictEqual(calendar.$('.fc-event-container .fc-event').length, 8, "should display 8 events");
+ // move to next month
+ calendar.$buttons.find('.o_calendar_button_next').click();
+
+ assert.strictEqual(calendar.$('.fc-event-container .fc-event').length, 0, "should display 0 events");
+ calendar.destroy();
+ });
+
+ QUnit.test('rendering, with many2many', function (assert) {
+ assert.expect(1);
+
+ this.data.event.fields.partner_ids.type = 'many2many';
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'all_day="allday"> '+
+ '<field name="partner_ids" avatar_field="image" write_model="filter_partner" write_field="partner_id"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ assert.strictEqual(calendar.$('.o_calendar_filter_items .o_cal_avatar').length, 3,
+ "should have 3 avatars in the side bar");
+
+ calendar.destroy();
+ });
+});
+
+});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
new file mode 100644
index 00000000000..fdd5fd84c64
--- /dev/null
+++ b/addons/web/static/tests/views/form_tests.js
@@ -0,0 +1,2688 @@
+odoo.define('web.form_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+var core = require('web.core');
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var _t = core._t;
+var createView = testUtils.createView;
+var createAsyncView = testUtils.createAsyncView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ bar: {string: "Bar", type: "boolean"},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1] },
+ p: {string: "one2many field", type: "one2many", relation: 'partner'},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
+ product_id: {string: "Product", type: "many2one", relation: 'product'},
+ priority: {
+ string: "Priority",
+ type: "selection",
+ selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
+ default: 1,
+ },
+ state: {string: "State", type: "selection", selection: [["ab", "AB"], ["cd", "CD"], ["ef", "EF"]]},
+ date: {string: "Some Date", type: "date"},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ product_ids: {string: "one2many product", type: "one2many", relation: "product"},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44,
+ p: [],
+ timmy: [],
+ trululu: 4,
+ state: "ab",
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ qux: 13,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ state: "cd",
+ }, {
+ id: 4,
+ display_name: "aaa",
+ state: "ef",
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"},
+ partner_type_id: {string: "Partner type", type: "many2one", relation: "partner_type"},
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char"},
+ color: {string: "Color index", type: "int"},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('FormView');
+
+ QUnit.test('simple form rendering', function (assert) {
+ assert.expect(10);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<div class="test" style="opacity: 0.5;">some html<span>aa</span></div>' +
+ '<sheet>' +
+ '<group>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '<field name="int_field" string="f3_description"/>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '<group>' +
+ '<div class="hello"></div>' +
+ '</group>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner Yo">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+ assert.strictEqual(form.$('div.test').length, 1,
+ "should contain a div with some html");
+ assert.strictEqual(form.$('div.test').css('opacity'), "0.5",
+ "should keep the inline style on html elements");
+ assert.strictEqual(form.$('label:contains(Foo)').length, 1,
+ "should contain label Foo");
+ assert.strictEqual(form.$('span:contains(blip)').length, 1,
+ "should contain span with field value");
+
+ assert.strictEqual(form.$('label:contains(something_id)').length, 0,
+ "should not contain f3 string description");
+ assert.strictEqual(form.$('label:contains(f3_description)').length, 1,
+ "should contain custom f3 string description");
+ assert.strictEqual(form.$('div.o_field_one2many table').length, 1,
+ "should render a one2many relation");
+
+ assert.strictEqual(form.$('tbody td:not(.o_list_record_selector) .o_checkbox input:checked').length, 1,
+ "1 checkboxes should be checked");
+
+ assert.strictEqual(form.get('title'), "second record",
+ "title should be display_name of record");
+ assert.strictEqual(form.$('label.o_form_label_empty:contains(timmy)').length, 0,
+ "the many2many label shouldn't be marked as empty");
+ form.destroy();
+ });
+
+ QUnit.test('only necessary fields are fetched', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ // NOTE: actually, the current web client always request the __last_update
+ // field, not sure why. Maybe this test should be modified.
+ assert.deepEqual(args.args[1], ["foo", "display_name"],
+ "should only fetch requested fields");
+ return this._super(route, args);
+ }
+ });
+ form.destroy();
+ });
+
+ QUnit.test('group rendering', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('table.o_inner_group').length, 1,
+ "should contain an inner group");
+ form.destroy();
+ });
+
+ QUnit.test('invisible fields are not rendered', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '<field name="qux" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('label.o_form_invisible:contains(Foo)').length, 1,
+ "should not contain label Foo");
+ assert.strictEqual(form.$('span.o_form_invisible:contains(yop)').length, 1,
+ "should not contain span with field value");
+ assert.strictEqual(form.$('.o_form_field.o_form_invisible:contains(0.4)').length, 1,
+ "field qux should be invisible");
+ form.destroy();
+ });
+
+ QUnit.test('invisible elements are properly hidden', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<group string="invgroup" invisible="1">' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="visible">' +
+ '</page>' +
+ '<page string="invisible" invisible="1">' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_notebook li.o_form_invisible a:contains(invisible)').length, 1,
+ "should not display tab invisible");
+ assert.strictEqual(form.$('table.o_inner_group.o_form_invisible td:contains(invgroup)').length, 1,
+ "should not display invisible groups");
+ form.destroy();
+ });
+
+ QUnit.test('invisible attrs are re-evaluated on field changed', function (assert) {
+ assert.expect(3);
+
+ // we set the value bar to simulate a falsy boolean value.
+ this.data.partner.records[0].bar = false;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet><group>' +
+ '<field name="product_id" invisible="1"/>' +
+ '<field name="timmy" invisible="1"/>' +
+ '<field name="foo" class="foo_field" attrs=\'{"invisible": [["product_id", "=", false]]}\'/>' +
+ '<field name="bar" class="bar_field" attrs=\'{"invisible":[("bar","=",False),("timmy","=",[])]}\'/>' +
+ '</group></sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.ok(form.$('.foo_field').hasClass('o_form_invisible'), 'should not display foo field');
+ assert.ok(form.$('.bar_field').hasClass('o_form_invisible'), 'should not display bar field');
+
+ // set a value on the m2o
+ var $dropdown = form.$('.o_form_field_many2one input').autocomplete('widget');
+ form.$('.o_form_field_many2one input').click();
+ $dropdown.find('li:first()').click();
+ assert.ok(!form.$('.foo_field').hasClass('o_form_invisible'), 'should not display foo field');
+ form.destroy();
+ });
+
+ QUnit.test('invisible attrs on first notebook page', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id"/>' +
+ '<notebook>' +
+ '<page string="Foo" attrs=\'{"invisible": [["product_id", "!=", false]]}\'>' +
+ '<field name="foo"/>' +
+ '</page>' +
+ '<page string="Bar">' +
+ '<field name="bar"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.ok(form.$('.o_notebook .nav li:first()').hasClass('active'),
+ 'first tab should be active');
+ assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('o_form_invisible'),
+ 'first tab should be visible');
+
+ // set a value on the m2o
+ var $dropdown = form.$('.o_form_field_many2one input').autocomplete('widget');
+ form.$('.o_form_field_many2one input').click();
+ $dropdown.find('li:first()').click();
+ assert.ok(!form.$('.o_notebook .nav li:first()').hasClass('active'),
+ 'first tab should not be active');
+ assert.ok(form.$('.o_notebook .nav li:first()').hasClass('o_form_invisible'),
+ 'first tab should be invisible');
+ assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'),
+ 'second tab should be active');
+ assert.ok(form.$('.o_notebook .tab-content .tab-pane:nth(1)').hasClass('active'),
+ 'second page should be active');
+ form.destroy();
+ });
+
+ QUnit.test('invisible attrs on group', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="bar"/>' +
+ '<group attrs=\'{"invisible": [["bar", "!=", true]]}\'>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1
+ });
+
+ assert.strictEqual(form.$('div.o_group:visible').length, 1, "should display the group");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('div.o_group:hidden').length, 1, "should not display the group");
+ form.destroy();
+ });
+
+ QUnit.test('invisible attrs with zero value in domain and unset value in data', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.int_field.type = 'monetary';
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="foo"/>' +
+ '<group attrs=\'{"invisible": [["int_field", "=", 0.0]]}\'>' +
+ '<div class="hello">this should be invisible</div>' +
+ '<field name="int_field"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ assert.notOk(form.$('div.hello').is(':visible'),
+ "attrs invisible should have been computed and applied");
+ form.destroy();
+ });
+
+ QUnit.test('rendering stat buttons', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<div name="button_box">' +
+ '<button class="oe_stat_button">' +
+ '<field name="int_field"/>' +
+ '</button>' +
+ '<button class="oe_stat_button" attrs=\'{"invisible": [["bar", "=", true]]}\'>' +
+ '<field name="bar"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('button.oe_stat_button').length, 2,
+ "should have 2 stat buttons");
+ assert.strictEqual(form.$('button.oe_stat_button.o_form_invisible').length, 1,
+ "should have 1 invisible stat buttons");
+
+ var count = 0;
+ testUtils.intercept(form, "execute_action", function () {
+ count++;
+ });
+ form.$('.oe_stat_button').first().click();
+ assert.strictEqual(count, 1, "should have triggered a execute action");
+ form.destroy();
+ });
+
+
+ QUnit.test('label uses the string attribute', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<label for="bar" string="customstring"/>' +
+ '<div><field name="bar"/></div>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('label.o_form_label:contains(customstring)').length, 1,
+ "should have 1 label with correct string");
+ form.destroy();
+ });
+
+
+ QUnit.test('empty fields have o_form_empty class in readonly mode', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.foo.default = false; // no default value for this test
+ this.data.partner.records[1].foo = false; // 1 is record with id=2
+ this.data.partner.records[1].trululu = false; // 1 is record with id=2
+ this.data.partner.fields.trululu.readonly = true;
+ this.data.partner.fields.int_field.readonly = true;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="trululu"/>' +
+ '<field name="int_field"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_form_field.o_form_field_empty').length, 2,
+ "should have 2 empty fields with correct class");
+ assert.strictEqual(form.$('.o_form_label_empty').length, 2,
+ "should have 2 muted labels (for the empty fieds) in readonly");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_form_field_empty').length, 1,
+ "in edit mode, only readonly fields should have .o_form_field_empty class");
+ assert.strictEqual(form.$('.o_form_label_empty').length, 1,
+ "in edit mode, only readonly fields should have .o_form_label_empty class");
+ form.destroy();
+ });
+
+ QUnit.test('empty inner readonly fields don\'t have o_form_empty class in "create" mode', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.product_id.readonly = true;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+ assert.strictEqual(form.$('.o_form_label_empty').length, 0,
+ "no empty class on label");
+ assert.strictEqual(form.$('.o_form_field_empty').length, 0,
+ "no empty class on field");
+ form.destroy();
+ });
+
+ QUnit.test('form view can switch to edit mode', function (assert) {
+ assert.expect(9);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'),
+ 'form view should have .o_form_readonly');
+ assert.ok(form.$buttons.find('.o_form_buttons_view').is(':visible'),
+ 'readonly buttons should be visible');
+ assert.ok(!form.$buttons.find('.o_form_buttons_edit').is(':visible'),
+ 'edit buttons should not be visible');
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
+ assert.ok(form.$el.hasClass('o_form_editable'),
+ 'form view should have .o_form_editable');
+ assert.ok(!form.$el.hasClass('o_form_readonly'),
+ 'form view should not have .o_form_readonly');
+ assert.ok(!form.$buttons.find('.o_form_buttons_view').is(':visible'),
+ 'readonly buttons should not be visible');
+ assert.ok(form.$buttons.find('.o_form_buttons_edit').is(':visible'),
+ 'edit buttons should be visible');
+ form.destroy();
+ });
+
+ QUnit.test('required fields should have o_form_required in readonly mode', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.required = true;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('span.o_form_required').length, 1,
+ "should have 1 span with o_form_required class");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input.o_form_required').length, 1,
+ "in edit mode, should have 1 input with o_form_required");
+ form.destroy();
+ });
+
+ QUnit.test('separators', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<separator string="Geolocation"/>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('div.o_horizontal_separator').length, 1,
+ "should contain a separator div");
+ form.destroy();
+ });
+
+ QUnit.test('buttons in form view', function (assert) {
+ assert.expect(7);
+
+ var rpcCount = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="state" invisible="1"/>' +
+ '<header>' +
+ '<button name="post" class="p" states="ab,ef" string="Confirm" type="object"/>' +
+ '<button name="some_method" class="s" string="Do it" type="object"/>' +
+ '</header>' +
+ '<sheet>' +
+ '<group>' +
+ '<button string="Geolocate" name="geo_localize" icon="fa-check" type="object"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.strictEqual(form.$('button.btn.btn-sm div.fa.fa-check').length, 1,
+ "should contain a button with correct content");
+
+ assert.strictEqual(form.$('.o_form_statusbar button').length, 2,
+ "should have 2 buttons in the statusbar");
+
+ assert.strictEqual(form.$('.o_form_statusbar button:visible').length, 1,
+ "should have only 1 visible button in the statusbar");
+
+ testUtils.intercept(form, 'execute_action', function (event) {
+ assert.strictEqual(event.data.action_data.name, "post",
+ "should trigger execute_action with correct method name");
+ assert.strictEqual(event.data.record_id, 2, "should have correct id in event data");
+ event.data.on_success();
+ });
+ rpcCount = 0;
+ form.$('.o_form_statusbar button.p').click();
+
+ assert.strictEqual(rpcCount, 1, "should have done 1 rpcs to reload");
+
+ testUtils.intercept(form, 'execute_action', function (event) {
+ event.data.on_fail();
+ });
+ form.$('.o_form_statusbar button.s').click();
+
+ assert.strictEqual(rpcCount, 2, "should have done 1 rpcs to reload");
+ form.destroy();
+ });
+
+ QUnit.test('buttons in form view, new record', function (assert) {
+ // this simulates a situation similar to the settings forms.
+ assert.expect(6);
+
+ var resID;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<header>' +
+ '<button name="post" class="p" string="Confirm" type="object"/>' +
+ '<button name="some_method" class="s" string="Do it" type="object"/>' +
+ '</header>' +
+ '<sheet>' +
+ '<group>' +
+ '<button string="Geolocate" name="geo_localize" icon="fa-check" type="object"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'create') {
+ return this._super.apply(this, arguments).then(function (result) {
+ resID = result;
+ return resID;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ testUtils.intercept(form, 'execute_action', function (event) {
+ assert.step('execute_action');
+ assert.strictEqual(event.data.record_id, resID,
+ "execute action should be done on correct record id");
+ event.data.on_success();
+ });
+ form.$('.o_form_statusbar button.p').click();
+
+ assert.verifySteps(['default_get', 'create', 'execute_action', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('change and save char', function (assert) {
+ assert.expect(6);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/></group>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.ok(true, "should call the /write route");
+ }
+ return this._super(route, args);
+ },
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+ assert.strictEqual(form.$('span:contains(blip)').length, 1,
+ "should contain span with field value");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
+ form.$('input').val("tralala").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+ assert.strictEqual(form.$('span:contains(tralala)').length, 1,
+ "should contain span with field value");
+ form.destroy();
+ });
+
+ QUnit.test('properly reload data from server', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/></group>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ args.args[1].foo = "apple";
+ }
+ return this._super(route, args);
+ },
+ res_id: 2,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val("tralala").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('span:contains(apple)').length, 1,
+ "should contain span with field value");
+ form.destroy();
+ });
+
+ QUnit.test('properly apply onchange in simple case', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length + 1000;
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/><field name="int_field"/></group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('input').eq(1).val(), "9",
+ "should contain input with initial value");
+
+ form.$('input').first().val("tralala").trigger('input');
+
+ assert.strictEqual(form.$('input').eq(1).val(), "1007",
+ "should contain input with onchange applied");
+ form.destroy();
+ });
+
+ QUnit.test('onchange send only the present fields to the server', function (assert) {
+ assert.expect(1);
+ this.data.partner.records[0].product_id = false;
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.foo = obj.foo + " alligator";
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="bar"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="timmy"/>' +
+ '</form>',
+ archs: {
+ "partner_type,false,list": '<tree><field name="name"/></tree>'
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === "onchange") {
+ assert.deepEqual(args.args[3],
+ {"foo": "1", "p": "", "p.bar": "", "p.display_name": "", "p.product_id": "", "timmy": "", "timmy.name": ""},
+ "should send only the fields used in the views");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input:first').val("tralala").trigger('input');
+
+ form.destroy();
+ });
+
+ QUnit.test('evaluate in python field options', function (assert) {
+ assert.expect(1);
+
+ var isOk = false;
+ var tmp = py.eval;
+ py.eval = function (expr) {
+ if (expr === "{'horizontal': true}") {
+ isOk = true;
+ }
+ return tmp.apply(tmp, arguments);
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" options="{\'horizontal\': true}"/>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ py.eval = tmp;
+
+ assert.ok(isOk, "should have evaluated the field options");
+ form.destroy();
+ });
+
+ QUnit.test('can create a record with default values', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ var n = this.data.partner.records.length;
+
+ form.$buttons.find('.o_form_button_create').click();
+ assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode');
+
+ assert.strictEqual(form.$('input:first').val(), "My little Foo Value",
+ "should have correct default_get value");
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+ assert.strictEqual(this.data.partner.records.length, n + 1, "should have created a record");
+ form.destroy();
+ });
+
+ QUnit.test('sidebar is hidden when switching to edit mode', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="foo"/>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {sidebar: true},
+ res_id: 1,
+ });
+
+ assert.ok(!form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.ok(form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.ok(!form.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
+ form.destroy();
+ });
+
+ QUnit.test('basic default record', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.default = "default foo value";
+
+ var count = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ count++;
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(form.$('input').val(), "default foo value", "should have correct default");
+ assert.strictEqual(count, 1, "should do only one rpc");
+ form.destroy();
+ });
+
+
+ QUnit.test('make default record with non empty one2many', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.p.default = [
+ [6, 0, []], // replace with zero ids
+ [0, 0, {foo: "new foo1", product_id: 41}], // create a new value
+ [0, 0, {foo: "new foo2", product_id: 37}], // create a new value
+ ];
+
+ var nameGetCount = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ nameGetCount++;
+ }
+ return this._super(route, args);
+ },
+ });
+ assert.ok(form.$('td:contains(new foo1)').length,
+ "should have new foo1 value in one2many");
+ assert.ok(form.$('td:contains(new foo2)').length,
+ "should have new foo2 value in one2many");
+ assert.ok(form.$('td:contains(xphone)').length,
+ "should have a cell with the name field 'product_id', set to xphone");
+ assert.strictEqual(nameGetCount, 1, "should have done only 1 nameget");
+ form.destroy();
+ });
+
+ QUnit.test('make default record with non empty many2one', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ this.data.partner.fields.trululu.default = 4;
+
+ var nameGetCount = 0;
+
+ createAsyncView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="trululu"/></form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ nameGetCount++;
+ var result = this._super.apply(this, arguments);
+ return concurrency.delay(1).then(function () {
+ return result;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ assert.ok(form.$('.o_form_input').val(), 'aaa',
+ 'default value should be correctly displayed');
+ assert.strictEqual(nameGetCount, 1, 'should have done one name_get');
+ form.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('form view properly change its title', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.get('title'), 'first record',
+ "should have the display name of the record as title");
+ form.$buttons.find('.o_form_button_create').click();
+ assert.strictEqual(form.get('title'), _t("New"),
+ "should have the display name of the record as title");
+ form.destroy();
+ });
+
+ QUnit.test('can duplicate a record', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {sidebar: true},
+ });
+
+ assert.strictEqual(form.get('title'), 'first record',
+ "should have the display name of the record as title");
+ form.sidebar.$('a:contains(Duplicate)').click();
+
+ assert.strictEqual(form.get('title'), 'first record (copy)',
+ "should have duplicated the record");
+
+ assert.strictEqual(form.mode, "edit", 'should be in edit mode');
+ form.destroy();
+ });
+
+ QUnit.test('buttons in footer are moved to $buttons if required', function (assert) {
+ // not sure about this test...
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<footer>' +
+ '<button string="Create" type="object" class="infooter"/>' +
+ '</footer>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {footer_to_buttons: true},
+ });
+
+ assert.ok(form.$buttons.find('button.infooter').length, "footer button should be in footer");
+ assert.ok(!form.$('button.infooter').length, "footer button should not be in form");
+ form.destroy();
+ });
+
+ QUnit.test('clicking on stat buttons in edit mode', function (assert) {
+ assert.expect(8);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<div name="button_box">' +
+ '<button class="oe_stat_button">' +
+ '<field name="bar"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].foo, "tralala", "should have saved the changes");
+ }
+ assert.step(args.method);
+ return this._super(route, args);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ var count = 0;
+ testUtils.intercept(form, "execute_action", function (event) {
+ event.stopPropagation();
+ count++;
+ });
+ form.$('.oe_stat_button').first().click();
+ assert.strictEqual(count, 1, "should have triggered a execute action");
+ assert.strictEqual(form.mode, "edit", "form view should be in edit mode");
+
+
+ form.$('input').val("tralala").trigger('input');
+ form.$('.oe_stat_button').first().click();
+
+ assert.strictEqual(form.mode, "edit", "form view should be in edit mode");
+ assert.strictEqual(count, 2, "should have triggered a execute action");
+ assert.verifySteps(['read', 'write']);
+ form.destroy();
+ });
+
+
+ QUnit.test('buttons with attr "special" do not trigger a save', function (assert) {
+ assert.expect(4);
+
+ var executeActionCount = 0;
+ var writeCount = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<button string="Do something" class="btn-primary" name="abc" type="object"/>' +
+ '<button string="Discard" class="btn-default" special="cancel"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ writeCount++;
+ }
+ return this._super(route, args);
+ }
+ });
+ testUtils.intercept(form, "execute_action", function () {
+ executeActionCount++;
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // make the record dirty
+ form.$('input').val("tralala").trigger('input');
+
+
+ form.$('button').eq(0).click();
+ assert.strictEqual(writeCount, 1, "should have triggered a write");
+ assert.strictEqual(executeActionCount, 1, "should have triggered a execute action");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val("abcdef").trigger('input');
+
+ form.$('button').eq(1).click();
+ assert.strictEqual(writeCount, 1, "should not have triggered a write");
+ assert.strictEqual(executeActionCount, 2, "should have triggered a execute action");
+ form.destroy();
+ });
+
+ QUnit.test('missing widgets do not crash', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.type = 'new field type without widget';
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('.o_field_widget').length, 1, "should have rendered an abstract field");
+ form.destroy();
+ });
+
+ QUnit.test('nolabel', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<group class="firstgroup"><field name="foo" nolabel="1"/></group>' +
+ '<group class="secondgroup">'+
+ '<field name="product_id"/>' +
+ '<field name="int_field" nolabel="1"/><field name="qux" nolabel="1"/>' +
+ '</group>' +
+ '<group><field name="bar"/></group>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$("label").length, 2, "should have rendered only two label");
+ assert.strictEqual(form.$("label").first().text(), "Product",
+ "one should be the one for the product field");
+ assert.strictEqual(form.$("label").eq(1).text(), "Bar",
+ "one should be the one for the bar field");
+
+ assert.strictEqual(form.$('.firstgroup td').first().attr('colspan'), "1",
+ "foo td should have a colspan of 1");
+ assert.strictEqual(form.$('.secondgroup tr').length, 2,
+ "int_field and qux should have same tr");
+
+ assert.strictEqual(form.$('.secondgroup tr:first td').length, 2,
+ "product_id field should be on its own tr");
+ form.destroy();
+ });
+
+ QUnit.test('many2one in a one2many', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('td:contains(xphone)').length, 1,
+ "should display the name of the many2one");
+ form.destroy();
+ });
+
+ QUnit.test('discard changes on a non dirty form view', function (assert) {
+ assert.expect(4);
+
+ var nbWrite = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ nbWrite++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'yop', 'input should contain yop');
+
+ // click on discard
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.ok(!$('.modal').length, 'no confirm modal should be displayed');
+ assert.strictEqual(form.$('.o_form_field').text(), 'yop', 'field in readonly should display yop');
+
+ assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
+ form.destroy();
+ });
+
+ QUnit.test('discard changes on a dirty form view', function (assert) {
+ assert.expect(7);
+
+ var nbWrite = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ nbWrite++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // switch to edit mode and edit the foo field
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'yop', 'input should contain yop');
+ form.$('.o_form_input').val('new value').trigger('input');
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should contain new value');
+
+ // click on discard and cancel the confirm request
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-default').click(); // click on cancel
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should still contain new value');
+
+ // click on discard and confirm
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+ assert.strictEqual(form.$('.o_form_field').text(), 'yop', 'field in readonly should display yop');
+
+ assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
+ form.destroy();
+ });
+
+ QUnit.test('discard changes on a new (non dirty, except for defaults) form view', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.default = "ABC";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ });
+
+ // switch to edit mode and edit the foo field
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'ABC', 'input should contain ABC');
+
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ assert.strictEqual($('.modal').length, 0,
+ 'there should not be a confirm modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('discard changes on a duplicated record', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ res_id: 1,
+ viewOptions: {sidebar: true},
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val("tralala").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.sidebar.$('a:contains(Duplicate)').click();
+
+ assert.strictEqual(form.$('.o_form_input').val(), 'tralala', 'input should contain ABC');
+
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ assert.strictEqual($('.modal').length, 0,
+ 'there should not be a confirm modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('switching to another record from a dirty one', function (assert) {
+ assert.expect(11);
+
+ var nbWrite = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ nbWrite++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+ assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2", 'pager limit should be 2');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'yop', 'input should contain yop');
+
+ // edit the foo field
+ form.$('.o_form_input').val('new value').trigger('input');
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should contain new value');
+
+ // click on the pager to switch to the next record and cancel the confirm request
+ form.pager.$('.o_pager_next').click(); // click on next
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-default').click(); // click on cancel
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should still contain new value');
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should still be 1');
+
+ // click on the pager to switch to the next record and confirm
+ form.pager.$('.o_pager_next').click(); // click on next
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+ assert.strictEqual(form.$('.o_form_input').val(), 'blip', 'input should contain blip');
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
+
+ assert.strictEqual(nbWrite, 0, 'no write RPC should have been done');
+ form.destroy();
+ });
+
+ QUnit.test('handling dirty state: switching to another record', function (assert) {
+ assert.expect(12);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"></field>' +
+ '<field name="priority" widget="priority"></field>' +
+ '</form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ res_id: 1,
+ });
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'yop', 'input should contain yop');
+
+ // edit the foo field
+ form.$('.o_form_input').val('new value').trigger('input');
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should contain new value');
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ // click on the pager to switch to the next record and cancel the confirm request
+ form.pager.$('.o_pager_next').click(); // click on next
+ assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
+
+ assert.strictEqual(form.$('.o_priority .fa-star-o').length, 2,
+ 'priority widget should have been rendered with correct value');
+
+ // edit the value in readonly
+ form.$('.o_priority .fa-star-o:first').click(); // click on the first star
+ assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
+ 'priority widget should have been updated');
+
+ form.pager.$('.o_pager_next').click(); // click on next
+ assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'new value', 'input should contain yop');
+
+ // edit the foo field
+ form.$('.o_form_input').val('wrong value').trigger('input');
+
+ form.$buttons.find('.o_form_button_cancel').click();
+ assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+ form.pager.$('.o_pager_next').click(); // click on next
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
+ form.destroy();
+ });
+
+ QUnit.test('handling dirty state: canBeDiscarded should be idempotent', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"></field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_form_input').val(), 'yop', 'input should contain yop');
+
+ // edit the foo field to make it dirty
+ form.$('.o_form_input').val('new value').trigger('input');
+
+ // discard changes once
+ form.canBeDiscarded();
+ assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+
+ // discard changes a second time
+ form.canBeDiscarded();
+ assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed');
+
+ form.destroy();
+ });
+
+
+ QUnit.test('restore local state when switching to another record', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<notebook>' +
+ '<page string="First Page" name="first">' +
+ '<field name="foo"/>' +
+ '</page>' +
+ '<page string="Second page" name="second">' +
+ '<field name="bar"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ res_id: 1,
+ });
+
+ // click on second page tab
+ form.$('.o_notebook li:eq(1) a').click();
+
+ assert.notOk(form.$('.o_notebook li:eq(0)').hasClass('active'),
+ "first tab should not be active");
+ assert.ok(form.$('.o_notebook li:eq(1)').hasClass('active'),
+ "second tab should be active");
+
+ // click on the pager to switch to the next record
+ form.pager.$('.o_pager_next').click();
+
+ assert.notOk(form.$('.o_notebook li:eq(0)').hasClass('active'),
+ "first tab should not be active");
+ assert.ok(form.$('.o_notebook li:eq(1)').hasClass('active'),
+ "second tab should be active");
+ form.destroy();
+ });
+
+ QUnit.test('pager is hidden in create mode', function (assert) {
+ assert.expect(7);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ });
+
+ assert.ok(form.pager.$el.is(':visible'), "pager should be visible");
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1",
+ "current pager value should be 1");
+ assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2",
+ "current pager limit should be 1");
+ form.$buttons.find('.o_form_button_create').click();
+
+ assert.notOk(form.pager.$el.is(':visible'), "pager should not be visible");
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.ok(form.pager.$el.is(':visible'), "pager should be visible");
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "3",
+ "current pager value should be 3");
+ assert.strictEqual(form.pager.$('.o_pager_limit').text(), "3",
+ "current pager limit should be 3");
+ form.destroy();
+ });
+
+ QUnit.test('switching to another record, in readonly mode', function (assert) {
+ assert.expect(5);
+
+ var pushStateCount = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ res_id: 1,
+ intercepts: {
+ push_state: function (event) {
+ pushStateCount++;
+ }
+ }
+ });
+
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+ form.pager.$('.o_pager_next').click(); // click on next
+
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "2", 'pager value should be 2');
+ assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode');
+
+ assert.strictEqual(pushStateCount, 2, "should have triggered 2 push_state");
+ form.destroy();
+ });
+
+ QUnit.test('modifiers are reevaluated when creating new record', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet><group>' +
+ '<field name="foo" class="foo_field" attrs=\'{"invisible": [["bar", "=", True]]}\'/>' +
+ '<field name="bar"/>' +
+ '</group></sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('span.foo_field').length, 1, "should have a span foo field");
+
+ assert.ok(!form.$('span.foo_field').is(':visible'),
+ "foo field should not be visible");
+
+ form.$buttons.find('.o_form_button_create').click();
+
+ assert.strictEqual(form.$('input.foo_field').length, 1,
+ "should have a visible input for foo field");
+
+ assert.ok(form.$('input.foo_field').is(':visible'),
+ "foo field should be visible");
+ form.destroy();
+ });
+
+ QUnit.test('empty readonly fields are visible on new records', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.readonly = true;
+ this.data.partner.fields.foo.default = undefined;
+ this.data.partner.records[0].foo = undefined;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet><group>' +
+ '<field name="foo"/>' +
+ '</group></sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field_empty').length, 1,
+ 'readonly field should be invisible on an existing record');
+
+ form.$buttons.find('.o_form_button_create').click();
+
+ assert.strictEqual(form.$('.o_form_field_empty').length, 0,
+ 'readonly field should be visible on a new record');
+ form.destroy();
+ });
+
+
+ QUnit.test('all group children have correct layout classname', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet><group>' +
+ '<group class="inner_group">' +
+ '<field name="name"/>' +
+ '</group>' +
+ '<div class="inner_div">' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</group></sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('.inner_group').hasClass('o_group_col_6'),
+ "inner groups should have classname 'o_group_col_6'");
+ assert.ok(form.$('.inner_div').hasClass('o_group_col_6'),
+ "divs inside groups should have classname 'o_group_col_6'");
+ form.destroy();
+ });
+
+ QUnit.test('deleting a record', function (assert) {
+ assert.expect(8);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="foo"></field></form>',
+ viewOptions: {
+ ids: [1, 2, 4],
+ index: 0,
+ sidebar: true,
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+ assert.strictEqual(form.pager.$('.o_pager_limit').text(), "3", 'pager limit should be 3');
+ assert.strictEqual(form.$('span:contains(yop)').length, 1,
+ 'should have a field with foo value for record 1');
+ assert.ok(!$('.modal').length, 'no confirm modal should be displayed');
+
+ // open sidebar
+ form.sidebar.$('button.o_dropdown_toggler_btn').click();
+ form.sidebar.$('a:contains(Delete)').click();
+
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+
+ // confirm the delete
+ $('.modal .modal-footer button.btn-primary').click();
+
+ assert.strictEqual(form.pager.$('.o_pager_value').text(), "1", 'pager value should be 1');
+ assert.strictEqual(form.pager.$('.o_pager_limit').text(), "2", 'pager limit should be 2');
+ assert.strictEqual(form.$('span:contains(blip)').length, 1,
+ 'should have a field with foo value for record 2');
+ form.destroy();
+ });
+
+ QUnit.test('empty required fields cannot be saved', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.foo.required = true;
+ delete this.data.partner.fields.foo.default;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/></group>' +
+ '</form>',
+ });
+
+ testUtils.intercept(form, 'warning', function (event) {
+ assert.strictEqual(event.data.title, 'The following fields are invalid:',
+ "should have a warning with correct title");
+ assert.strictEqual(event.data.message, '<ul><li>Foo</li></ul>',
+ "should have a warning with correct message");
+ });
+
+ form.$buttons.find('.o_form_button_save').click();
+ assert.ok(form.$('label').hasClass('o_form_invalid'),
+ "label should be tagged as invalid");
+ assert.ok(form.$('input').hasClass('o_form_invalid'),
+ "input should be tagged as invalid");
+
+ form.$('input').val("tralala").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_form_invalid').length, 0,
+ "nothing should be marked as invalid");
+ form.destroy();
+ });
+
+ QUnit.test('changes in a readonly form view are saved directly', function (assert) {
+ assert.expect(10);
+
+ var nbWrite = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="priority" widget="priority"/>' +
+ '</group>' +
+ '</form>',
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ nbWrite++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_priority .o_priority_star').length, 2,
+ 'priority widget should have been rendered');
+ assert.strictEqual(form.$('.o_priority .fa-star-o').length, 2,
+ 'priority widget should have been rendered with correct value');
+
+ // edit the value in readonly
+ form.$('.o_priority .fa-star-o:first').click(); // click on the first star
+ assert.strictEqual(nbWrite, 1, 'should have saved directly');
+ assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
+ 'priority widget should have been updated');
+
+ // switch to edit mode and edit the value again
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_priority .o_priority_star').length, 2,
+ 'priority widget should have been correctly rendered');
+ assert.strictEqual(form.$('.o_priority .fa-star').length, 1,
+ 'priority widget should have correct value');
+ form.$('.o_priority .fa-star-o:first').click(); // click on the second star
+ assert.strictEqual(nbWrite, 1, 'should not have saved directly');
+ assert.strictEqual(form.$('.o_priority .fa-star').length, 2,
+ 'priority widget should have been updated');
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(nbWrite, 2, 'should not have saved directly');
+ assert.strictEqual(form.$('.o_priority .fa-star').length, 2,
+ 'priority widget should have correct value');
+ form.destroy();
+ });
+
+ QUnit.test('display a dialog if onchange result is a warning', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = { foo: true };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/><field name="int_field"/></group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ value: { int_field: 10 },
+ warning: {
+ title: "Warning",
+ message: "You must first select a partner"
+ }
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ warning: function (event) {
+ assert.strictEqual(event.data.type, 'dialog',
+ "should have triggered an event with the correct data");
+ assert.strictEqual(event.data.title, "Warning",
+ "should have triggered an event with the correct data");
+ assert.strictEqual(event.data.message, "You must first select a partner",
+ "should have triggered an event with the correct data");
+ },
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_form_input[name=int_field]').val(), '9',
+ "'int_field' value should be 9 before the change");
+
+ form.$('input').first().val("tralala").trigger('input');
+
+ assert.strictEqual(form.$('.o_form_input[name=int_field]').val(), '10',
+ "the onchange should have been correctly applied");
+
+ form.destroy();
+ });
+
+ QUnit.test('do nothing if add a line in one2many result in a onchange with a warning', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = { foo: true };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ value: {},
+ warning: {
+ title: "Warning",
+ message: "You must first select a partner"
+ }
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ warning: function () {
+ assert.step("should have triggered a warning");
+ },
+ },
+ });
+
+ // go to edit mode, click to add a record in the o2m
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 0,
+ "should not have added a line");
+ form.destroy();
+ });
+
+ QUnit.test('attrs are properly transmitted to new records', function (assert) {
+ assert.expect(2);
+
+ // this test checks that the fieldsInfo have been transmitted to the
+ // load function when creating a new record
+
+ var terminology = {
+ string_true: "Production Environment",
+ hover_true: "Switch to test environment",
+ string_false: "Test Environment",
+ hover_false: "Switch to production environment"
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="bar" widget="boolean_button" options=\'{"terminology": ' +
+ JSON.stringify(terminology) + '}\'/>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_stat_text.o_not_hover:contains(Production Environment)').length, 1,
+ "button should contain correct string");
+
+ form.$buttons.find('.o_form_button_create').click();
+
+ assert.strictEqual(form.$('.o_stat_text.o_not_hover:contains(Test Environment)').length, 1,
+ "button should contain correct string");
+
+ form.destroy();
+ });
+
+ QUnit.test('button box is not rendered in create mode', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<div name="button_box" class="oe_button_box">' +
+ '<button type="object" class="oe_stat_button" icon="fa-check-square">' +
+ '<field name="bar"/>' +
+ '</button>' +
+ '</div>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // readonly mode
+ assert.strictEqual(form.$('.oe_stat_button').length, 1,
+ "button box should be displayed in readonly");
+
+ // edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.oe_stat_button').length, 1,
+ "button box should be displayed in edit on an existing record");
+
+ // create mode
+ form.$buttons.find('.o_form_button_create').click();
+ assert.strictEqual(form.$('.oe_stat_button').length, 0,
+ "button box should not be displayed when creating a new record");
+
+ form.destroy();
+ });
+
+ QUnit.test('properly apply onchange on many2many fields', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.timmy = [
+ [5],
+ [1, 12, {display_name: "gold"}],
+ ];
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/></group>' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_form_field_many2many .o_data_row').length, 0,
+ "there should be no many2many record linked at first");
+
+ // switch to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_input').val('let us trigger an onchange').trigger('input');
+ assert.strictEqual(form.$('.o_form_field_many2many .o_data_row').length, 1,
+ "there should be one linked record");
+ assert.strictEqual(form.$('.o_form_field_many2many .o_data_row td:first').text(), 'gold',
+ "the 'display_name' of the many2many record should be correctly displayed");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchanges on date(time) fields', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.date = '2021-12-12';
+ obj.datetime = '2021-12-12 10:55:05';
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="date"/>' +
+ '<field name="datetime"/>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_form_field[name=date]').text(),
+ '01/25/2017', "the initial date should be correct");
+ assert.strictEqual(form.$('.o_form_field[name=datetime]').text(),
+ '12/12/2016 10:55:05', "the initial datetime should be correct");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_form_field[name=date] input').val(),
+ '01/25/2017', "the initial date should be correct in edit");
+ assert.strictEqual(form.$('.o_form_field[name=datetime] input').val(),
+ '12/12/2016 10:55:05', "the initial datetime should be correct in edit");
+
+ // trigger the onchange
+ form.$('.o_form_field[name="foo"]').val("coucou").trigger('input');
+
+ assert.strictEqual(form.$('.o_form_field[name=date] input').val(),
+ '12/12/2021', "the initial date should be correct in edit");
+ assert.strictEqual(form.$('.o_form_field[name=datetime] input').val(),
+ '12/12/2021 10:55:05', "the initial datetime should be correct in edit");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchanges are not sent for each keystrokes', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ var onchangeNbr = 0;
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length + 1000;
+ },
+ };
+ var def = $.Deferred();
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group><field name="foo"/><field name="int_field"/></group>' +
+ '</form>',
+ res_id: 2,
+ fieldDebounce: 3,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ onchangeNbr++;
+ return concurrency.delay(3).then(function () {
+ def.resolve();
+ return result;
+ });
+ }
+ return result;
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ form.$('input').first().val("1").trigger('input');
+ assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
+ form.$('input').first().val("12").trigger('input');
+ assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet");
+
+ return waitForFinishedOnChange().then(function () {
+ assert.strictEqual(onchangeNbr, 1, "one onchange has been called");
+
+ // add something in the input, then focus another input
+ form.$('input').first().val("123").trigger('input');
+ form.$('input').first().change();
+ assert.strictEqual(onchangeNbr, 2,
+ "one onchange has been called immediately");
+
+ return waitForFinishedOnChange();
+ }).then(function () {
+ assert.strictEqual(onchangeNbr, 2,
+ "no extra onchange should have been called");
+
+ form.destroy();
+ done();
+ });
+
+ function waitForFinishedOnChange() {
+ return def.then(function () {
+ def = $.Deferred();
+ return concurrency.delay(0);
+ });
+ }
+ });
+
+ QUnit.test('rpc complete after destroying parent', function (assert) {
+ // We just test that there is no crash in this situation
+ assert.expect(0);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<button name="update_module" type="object" class="o_form_button_update"/>' +
+ '</form>',
+ res_id: 2,
+ intercepts: {
+ execute_action: function (event) {
+ form.destroy();
+ event.data.on_success();
+ }
+ }
+ });
+ form.$('.o_form_button_update').click();
+ });
+
+ QUnit.test('onchanges that complete after discarding', function (assert) {
+ assert.expect(4);
+
+ var def1 = $.Deferred();
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length + 1000;
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group><field name="foo"/><field name="int_field"/></group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ assert.step('onchange is done');
+ return def1.then(function () {
+ return result;
+ });
+ }
+ return result;
+ },
+ });
+
+ // go into edit mode
+ assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
+ "field foo should be displayed to initial value");
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // edit a value
+ form.$('input').first().val("1234").trigger('input');
+
+ // discard changes
+ form.$buttons.find('.o_form_button_cancel').click();
+ $('.modal .modal-footer .btn-primary').click();
+ assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
+ "field foo should still be displayed to initial value");
+
+ // complete the onchange
+ def1.resolve();
+ assert.strictEqual(form.$('span[name="foo"]').text(), "blip",
+ "field foo should still be displayed to initial value");
+
+ form.destroy();
+ });
+
+ QUnit.test('unchanged relational data is sent for onchanges', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[1].p = [4];
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length + 1000;
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.deepEqual(args.args[1].p, [[4, 4, false]],
+ "should send a command for field p even if it hasn't changed");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_input:first').val('trigger an onchange').trigger('input');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchanges on unknown fields of o2m are ignored', function (assert) {
+ // many2one fields need to be postprocessed (the onchange returns [id,
+ // display_name]), but if we don't know the field, we can't know it's a
+ // many2one, so it isn't ignored, its value is an array instead of a
+ // dataPoint id, which may cause errors later (e.g. when saving).
+ assert.expect(2);
+
+ this.data.partner.records[1].p = [4];
+ this.data.partner.onchanges = {
+ foo: function () {},
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="foo"/>' +
+ '<field name="product_id"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ value: {
+ p: [
+ [5],
+ [1, 4, {
+ foo: 'foo changed',
+ product_id: [37, "xphone"],
+ }]
+ ],
+ },
+ });
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [[1, 4, {
+ foo: 'foo changed',
+ }]], "should only write value of known fields");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_input:first').val('trigger an onchange').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
+ "onchange should have been correctly applied on field in o2m list");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange value are not discarded on o2m edition', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[1].p = [4];
+ this.data.partner.onchanges = {
+ foo: function () {},
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="foo"/>' +
+ '<field name="product_id"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ value: {
+ p: [[5], [1, 4, {foo: 'foo changed'}]],
+ },
+ });
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [[1, 4, {
+ foo: 'foo changed',
+ }]], "should only write value of known fields");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
+ "the initial value should be the default one");
+
+ form.$('.o_form_input:first').val('trigger an onchange').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
+ "onchange should have been correctly applied on field in o2m list");
+
+ form.$('.o_data_row').click(); // edit the o2m in the dialog
+ assert.strictEqual($('.modal .o_form_field').val(), 'foo changed',
+ "the onchange value hasn't been discarded when opening the o2m");
+
+ form.destroy();
+ });
+
+ QUnit.test('args of onchanges in o2m fields are correct (inline edition)', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[1].p = [4];
+ this.data.partner.fields.p.relation_field = 'rel_field';
+ this.data.partner.fields.int_field.default = 14;
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.foo = '[' + obj.rel_field.foo + '] ' + obj.int_field;
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
+ "the initial value should be the default one");
+
+ form.$('.o_data_row td:nth(1)').click(); // edit the o2m inline
+ form.$('.o_data_row input:nth(1)').val(77).trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row input:first').val(), '[blip] 77',
+ "onchange should have been correctly applied");
+
+ // create a new o2m record
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('.o_data_row input:first').val(), '[blip] 14',
+ "onchange should have been correctly applied after default get");
+
+ form.destroy();
+ });
+
+ QUnit.test('args of onchanges in o2m fields are correct (dialog edition)', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[1].p = [4];
+ this.data.partner.fields.p.relation_field = 'rel_field';
+ this.data.partner.fields.int_field.default = 14;
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.foo = '[' + obj.rel_field.foo + '] ' + obj.int_field;
+ },
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_data_row td:first').text(), 'My little Foo Value',
+ "the initial value should be the default one");
+
+ form.$('.o_data_row td:first').click(); // edit the o2m in a dialog
+ $('.modal .o_form_input:nth(1)').val(77).trigger('input');
+ assert.strictEqual($('.modal .o_form_input:first').val(), '[blip] 77',
+ "onchange should have been correctly applied");
+ $('.modal .modal-footer .btn-primary').click(); // save the dialog
+ assert.strictEqual(form.$('.o_data_row td:first').text(), '[blip] 77',
+ "onchange should have been correctly applied");
+
+ // create a new o2m record
+ form.$('.o_form_field_x2many_list_row_add a').click();
+ assert.strictEqual($('.modal .o_form_input:first').val(), '[blip] 14',
+ "onchange should have been correctly applied after default get");
+ $('.modal .modal-footer .btn-primary').click(); // save the dialog
+ assert.strictEqual(form.$('.o_data_row:nth(1) td:first').text(), '[blip] 14',
+ "onchange should have been correctly applied after default get");
+
+ form.destroy();
+ });
+
+ QUnit.test('context of onchanges contains the context of changed fields', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ foo: function () {},
+ };
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="foo" context="{\'test\': 1}"/>' +
+ '<field name="int_field" context="{\'int_ctx\': 1}"/>' +
+ '</group>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.strictEqual(args.args[4].test, 1,
+ "the context of the field triggering the onchange should be given");
+ assert.strictEqual(args.args[4].int_ctx, undefined,
+ "the context of other fields should not be given");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 2,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_form_input:first').val('coucou').trigger('input');
+
+ form.destroy();
+ });
+
+ QUnit.test('navigation with tab key in form view', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // go to edit mode
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // focus first input, trigger tab
+ form.$('input[name="foo"]').focus();
+ form.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ assert.ok($.contains(form.$('div[name="bar"]')[0], document.activeElement),
+ "bar checkbox should be focused");
+
+ // simulate shift+tab on active element
+ $(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "first input should be focused");
+
+ form.destroy();
+ });
+
+ QUnit.test('clicking on a stat button with a context', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" type="action" name="1" context="{\'test\': active_id}">' +
+ '<field name="qux" widget="statinfo"/>' +
+ '</button>' +
+ '</div>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ intercepts: {
+ execute_action: function (e) {
+ assert.deepEqual(e.data.action_data.context, {test: 2},
+ "button context should have been evaluated and given to the action");
+ },
+ },
+ });
+
+ form.$('.oe_stat_button').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('diplay a stat button outside a buttonbox', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<sheet>' +
+ '<button class="oe_stat_button" type="action" name="1">' +
+ '<field name="int_field" widget="statinfo"/>' +
+ '</button>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('button .o_field_widget').length, 1,
+ "a field widget should be display inside the button");
+ assert.strictEqual(form.$('button .o_field_widget').children().length, 2,
+ "the field widget should have 2 children, the text and the value");
+ assert.strictEqual(parseInt(form.$('button .o_field_widget .o_stat_value').text()), 9,
+ "the value rendered should be the same than the field value");
+ form.destroy();
+ });
+
+ QUnit.test('one2many default value creation', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].product_ids = [37];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_ids" nolabel="1">' +
+ '<tree editable="top" create="0">' +
+ '<field name="name" readonly="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ product_ids: [[0, 0, {
+ name: 'xdroid',
+ partner_type_id: 12,
+ }]]
+ });
+ }
+ if (args.method === 'create') {
+ var command = args.args[0].product_ids[0];
+ assert.strictEqual(command[2].partner_type_id, 12,
+ "the default partner_type_id should be equal to 12");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+});
+});
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
new file mode 100644
index 00000000000..faab8420fbb
--- /dev/null
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -0,0 +1,256 @@
+odoo.define('web.graph_tests', function (require) {
+"use strict";
+
+var concurrency = require('web.concurrency');
+var GraphView = require('web.GraphView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ foo: {
+ fields: {
+ foo: {string: "Foo", type: "integer", store: true},
+ bar: {string: "bar", type: "boolean"},
+ product_id: {string: "Product", type: "many2one", relation: 'product'},
+ color_id: {string: "Color", type: "many2one", relation: 'color'},
+ },
+ records: [
+ {id: 1, foo: 3, bar: true, product_id: 37},
+ {id: 2, foo: 53, bar: true, product_id: 37, color_id: 7},
+ {id: 3, foo: 2, bar: true, product_id: 37},
+ {id: 4, foo: 24, bar: false, product_id: 37},
+ {id: 5, foo: 4, bar: false, product_id: 41},
+ {id: 6, foo: 63, bar: false, product_id: 41},
+ {id: 7, foo: 42, bar: false, product_id: 41},
+ ]
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ color: {
+ fields: {
+ name: {string: "Color", type: "char"}
+ },
+ records: [{
+ id: 7,
+ display_name: "red",
+ }, {
+ id: 14,
+ display_name: "black",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('GraphView');
+
+ QUnit.test('simple graph rendering', function (assert) {
+ assert.expect(4);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+
+ var done = assert.async();
+ return concurrency.delay(0).then(function () {
+ assert.strictEqual(graph.$('div.o_graph_svg_container svg.nvd3-svg').length, 1,
+ "should contain a div with a svg element");
+
+ assert.strictEqual(graph.renderer.state.mode, "bar",
+ "should be in bar chart mode by default");
+
+ // here, we would like to test the svg in the dom. However, it is
+ // not so easy, because there is an animation which means that we
+ // don't really have a nice way to find the proper rect elements.
+ // So, instead we will do some white box testing.
+ assert.strictEqual(graph.model.chart.data[0].value, 3,
+ "should have first datapoint with value 3");
+ assert.strictEqual(graph.model.chart.data[1].value, 4,
+ "should have first datapoint with value 4");
+ graph.destroy();
+ done();
+ });
+ });
+
+
+ QUnit.test('default type attribute', function (assert) {
+ assert.expect(1);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners" type="pie">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+ assert.strictEqual(graph.renderer.state.mode, "pie", "should be in pie chart mode by default");
+ graph.destroy();
+ });
+
+
+ QUnit.test('switching mode', function (assert) {
+ assert.expect(6);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners" type="line">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+ assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode by default");
+ assert.notOk(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
+ 'bar type button should not be active');
+ assert.ok(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
+ 'line type button should be active');
+ graph.$buttons.find('button[data-mode="bar"]').click();
+ assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode by default");
+ assert.notOk(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
+ 'line type button should not be active');
+ assert.ok(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
+ 'bar type button should be active');
+ graph.destroy();
+ });
+
+ QUnit.test('displaying line chart with only 1 data point', function (assert) {
+ assert.expect(2);
+
+ // this test makes sure the line chart does not crash when only one data
+ // point is displayed. This was the case since a line cannot be drawn with
+ // only one point of reference.
+ var done = assert.async();
+ this.data.foo.records = this.data.foo.records.slice(0,1);
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners" type="line">' +
+ '<field name="bar"/>' +
+ '</graph>',
+ });
+ return concurrency.delay(0).then(function () {
+ assert.ok(!graph.$('svg').length, "should not have a svg");
+ assert.ok(graph.$('.oe_view_nocontent').length, "should have an error message");
+ graph.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('switching measures', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var rpcCount = 0;
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Gloups">' +
+ '<field name="product_id"/>' +
+ '</graph>',
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+ return concurrency.delay(0).then(function () {
+ assert.ok(graph.$('text.nv-legend-text:contains(Count)').length,
+ "should have used the correct measure");
+ assert.ok(graph.$buttons.find('li[data-field="foo"]').length,
+ "should have foo in the list of measures");
+ graph.$buttons.find('li[data-field="foo"] a').click();
+
+ return concurrency.delay(0);
+ }).then(function () {
+ assert.ok(graph.$('text.nv-legend-text:contains(Foo)').length,
+ "should now use the Foo measure");
+ assert.strictEqual(rpcCount, 2, "should have done 2 rpcs (2 readgroups)");
+ graph.destroy();
+ done();
+ });
+ });
+
+
+ QUnit.test('no content helper', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Gloups">' +
+ '<field name="product_id"/>' +
+ '</graph>',
+ });
+ return concurrency.delay(0).then(function () {
+ assert.ok(graph.$('div.o_graph_svg_container svg.nvd3-svg').length,
+ "should contain a div with a svg element");
+ assert.notOk(graph.$('div.oe_view_nocontent').length,
+ "should not display the no content helper");
+ graph.update({domain: [['product_id', '=', 4]]});
+
+ assert.notOk(graph.$('div.o_graph_svg_container svg.nvd3-svg').length,
+ "should not contain a div with a svg element");
+ assert.ok(graph.$('div.oe_view_nocontent').length,
+ "should display the no content helper");
+ graph.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('can reload with other group by', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var graph = createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Gloups">' +
+ '<field name="product_id"/>' +
+ '</graph>',
+ });
+ return concurrency.delay(0).then(function () {
+ assert.ok(graph.$('text:contains(xphone)').length,
+ "should contain a text element with product in legend");
+ assert.notOk(graph.$('text:contains(red)').length,
+ "should not contain a text element with color in legend");
+
+ graph.update({groupBy: ['color_id']});
+
+ return concurrency.delay(0);
+ }).then(function () {
+ assert.notOk(graph.$('text:contains(xphone)').length,
+ "should not contain a text element with product in legend");
+ assert.ok(graph.$('text:contains(red)').length,
+ "should contain a text element with color in legend");
+ graph.destroy();
+ done();
+ });
+ });
+
+});
+
+});
diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js
new file mode 100644
index 00000000000..7c64b7dcf6a
--- /dev/null
+++ b/addons/web/static/tests/views/kanban_model_tests.js
@@ -0,0 +1,253 @@
+odoo.define('web.kanban_model_tests', function (require) {
+"use strict";
+
+var KanbanModel = require('web.KanbanModel');
+var testUtils = require('web.test_utils');
+
+var createModel = testUtils.createModel;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ active: {string: "Active", type: "boolean", default: true},
+ display_name: {string: "STRING", type: 'char'},
+ foo: {string: "Foo", type: 'char'},
+ bar: {string: "Bar", type: 'integer'},
+ qux: {string: "Qux", type: 'many2one', relation: 'partner'},
+ product_id: {string: "Favorite product", type: 'many2one', relation: 'product'},
+ product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
+ category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
+ },
+ records: [
+ {id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner"},
+ {id: 2, foo: 'gnap', bar: 2, product_id: 41, display_name: "second partner"},
+ ],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [
+ {id: 37, display_name: "xphone"},
+ {id: 41, display_name: "xpad"}
+ ]
+ },
+ partner_type: {
+ fields: {
+ display_name: {string: "Partner Type", type: "char"}
+ },
+ records: [
+ {id: 12, display_name: "gold"},
+ {id: 14, display_name: "silver"},
+ {id: 15, display_name: "bronze"}
+ ]
+ },
+ };
+
+ // add related fields to category.
+ this.data.partner.fields.category.relatedFields =
+ $.extend(true, {}, this.data.partner_type.fields);
+ this.params = {
+ fields: this.data.partner.fields,
+ limit: 40,
+ modelName: 'partner',
+ openGroupByDefault: true,
+ };
+ }
+}, function () {
+
+ QUnit.module('KanbanModel');
+
+ QUnit.test('load grouped + add a new group', function (assert) {
+ assert.expect(20);
+
+ var calledRoutes = {};
+ var model = createModel({
+ Model: KanbanModel,
+ data: this.data,
+ mockRPC: function (route) {
+ if (!(route in calledRoutes)) {
+ calledRoutes[route] = 1;
+ } else {
+ calledRoutes[route]++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var params = _.extend(this.params, {
+ groupedBy: ['product_id'],
+ fieldNames: ['foo'],
+ });
+
+ model.load(params).then(function (resultID) {
+ // various checks on the load result
+ var state = model.get(resultID);
+ assert.ok(_.isEqual(state.groupedBy, ['product_id']), 'should be grouped by "product_id"');
+ assert.strictEqual(state.count, 2, 'should have found 2 groups');
+ var xphoneGroup = _.findWhere(state.data, {res_id: 37});
+ assert.strictEqual(xphoneGroup.model, 'partner', 'group should have correct model');
+ assert.ok(xphoneGroup, 'should have a group for res_id 37');
+ assert.ok(xphoneGroup.isOpen, '"xphone" group should be open');
+ assert.strictEqual(xphoneGroup.value, 'xphone', 'group 37 should be "xphone"');
+ assert.strictEqual(xphoneGroup.count, 1, '"xphone" group should have one record');
+ assert.strictEqual(xphoneGroup.data.length, 1, 'should have fetched the records in the group');
+ assert.ok(_.isEqual(xphoneGroup.domain[0], ['product_id', '=', 37]),
+ 'domain should be correct');
+ assert.strictEqual(xphoneGroup.limit, 40, 'limit in a group should be 40');
+
+ // add a new group
+ model.createGroup('xpod', resultID);
+ state = model.get(resultID);
+ assert.strictEqual(state.count, 3, 'should now have 3 groups');
+ var xpodGroup = _.findWhere(state.data, {value: 'xpod'});
+ assert.strictEqual(xpodGroup.model, 'partner', 'new group should have correct model');
+ assert.ok(xpodGroup, 'should have an "xpod" group');
+ assert.ok(xpodGroup.isOpen, 'new group should be open');
+ assert.strictEqual(xpodGroup.count, 0, 'new group should contain no record');
+ assert.ok(_.isEqual(xpodGroup.domain[0], ['product_id', '=', xpodGroup.res_id]),
+ 'new group should have correct domain');
+
+ // check the rpcs done
+ assert.strictEqual(Object.keys(calledRoutes).length, 3, 'three different routes have been called');
+ var nbReadGroups = calledRoutes['/web/dataset/call_kw/partner/read_group'];
+ var nbSearchRead = calledRoutes['/web/dataset/search_read'];
+ var nbNameCreate = calledRoutes['/web/dataset/call_kw/product/name_create'];
+ assert.strictEqual(nbReadGroups, 1, 'should have done 1 read_group');
+ assert.strictEqual(nbSearchRead, 2, 'should have done 2 search_read');
+ assert.strictEqual(nbNameCreate, 1, 'should have done 1 name_create');
+ model.destroy();
+ });
+ });
+
+ QUnit.test('archive/restore a column', function (assert) {
+ assert.expect(4);
+
+ var model = createModel({
+ Model: KanbanModel,
+ data: this.data,
+ });
+
+ var params = _.extend(this.params, {
+ groupedBy: ['product_id'],
+ fieldNames: ['foo'],
+ });
+
+ model.load(params).then(function (resultID) {
+ var state = model.get(resultID);
+ var xphoneGroup = _.findWhere(state.data, {res_id: 37});
+ var xpadGroup = _.findWhere(state.data, {res_id: 41});
+ assert.strictEqual(xphoneGroup.count, 1, 'xphone group has one record');
+ assert.strictEqual(xpadGroup.count, 1, 'xpad group has one record');
+
+ // archive the column 'xphone'
+ var recordIDs = _.pluck(xphoneGroup.data, 'id');
+ model.toggleActive(recordIDs, false, xphoneGroup.id);
+ state = model.get(resultID);
+ xphoneGroup = _.findWhere(state.data, {res_id: 37});
+ assert.strictEqual(xphoneGroup.count, 0, 'xphone group has no record anymore');
+ xpadGroup = _.findWhere(state.data, {res_id: 41});
+ assert.strictEqual(xpadGroup.count, 1, 'xpad group still has one record');
+ model.destroy();
+ });
+ });
+
+ QUnit.test('kanban model does not allow nested groups', function (assert) {
+ assert.expect(2);
+
+ var model = createModel({
+ Model: KanbanModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.groupby, ['product_id'],
+ "the second level of groupBy should have been removed");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var params = _.extend(this.params, {
+ groupedBy: ['product_id', 'qux'],
+ fieldNames: ['foo'],
+ });
+
+ model.load(params).then(function (resultID) {
+ var state = model.get(resultID);
+
+ assert.deepEqual(state.groupedBy, ['product_id'],
+ "the second level of groupBy should have been removed");
+
+ model.destroy();
+ });
+ });
+
+ QUnit.test('resequence columns and records', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
+
+ var nbReseq = 0;
+ var model = createModel({
+ Model: KanbanModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ nbReseq++;
+ if (nbReseq === 1) { // resequencing columns
+ assert.deepEqual(args.ids, [41, 37],
+ "ids should be correct");
+ assert.strictEqual(args.model, 'product_id',
+ "model should be correct");
+ } else if (nbReseq === 2) { // resequencing records
+ assert.deepEqual(args.ids, [3, 1],
+ "ids should be correct");
+ assert.strictEqual(args.model, 'partner',
+ "model should be correct");
+ }
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ var params = _.extend(this.params, {
+ groupedBy: ['product_id'],
+ fieldNames: ['foo'],
+ });
+
+ model.load(params)
+ .then(function (stateID) {
+ var state = model.get(stateID);
+ assert.strictEqual(state.data[0].res_id, 37,
+ "first group should be res_id 37");
+
+ // resequence columns
+ return model.resequence('product_id', [41, 37], stateID);
+ })
+ .then(function (stateID) {
+ var state = model.get(stateID);
+ assert.strictEqual(state.data[0].res_id, 41,
+ "first group should be res_id 41 after resequencing");
+ assert.strictEqual(state.data[1].data[0].res_id, 1,
+ "first record should be res_id 1");
+
+ // resequence records
+ return model.resequence('partner', [3, 1], state.data[1].id);
+ })
+ .then(function (groupID) {
+ var group = model.get(groupID);
+ assert.strictEqual(group.data[0].res_id, 3,
+ "first record should be res_id 3 after resequencing");
+
+ model.destroy();
+ done();
+ });
+
+ });
+});
+
+});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
new file mode 100644
index 00000000000..dd9b2eb024a
--- /dev/null
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -0,0 +1,1068 @@
+odoo.define('web.kanban_tests', function (require) {
+"use strict";
+
+var KanbanView = require('web.KanbanView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ foo: {string: "Foo", type: "char"},
+ bar: {string: "Bar", type: "boolean"},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "my float", type: "float"},
+ product_id: {string: "something_id", type: "many2one", relation: "product"},
+ category_ids: { string: "categories", type: "many2many", relation: 'category'},
+ state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]},
+ date: {string: "Date Field", type: 'date'},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ },
+ records: [
+ {id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: []},
+ {id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
+ {id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
+ {id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
+ ]
+ },
+ product: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ name: {string: "Display Name", type: "char"},
+ },
+ records: [
+ {id: 3, name: "hello"},
+ {id: 5, name: "xmo"},
+ ]
+ },
+ category: {
+ fields: {
+ name: {string: "Category Name", type: "char"},
+ color: {string: "Color index", type: "int"},
+ },
+ records: [
+ {id: 6, name: "gold", color: 2},
+ {id: 7, name: "silver", color: 5},
+ ]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('KanbanView');
+
+ QUnit.test('basic ungrouped rendering', function (assert) {
+ assert.expect(5);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<t t-esc="record.foo.value"/>' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ });
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
+ "should have classname 'o_kanban_ungrouped'");
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_test'),
+ "should have classname 'o_kanban_test'");
+
+ assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ "should have 4 records");
+ assert.strictEqual(kanban.$('.o_kanban_ghost').length, 6, "should have 6 ghosts");
+ assert.strictEqual(kanban.$('.o_kanban_record:contains(gnap)').length, 1,
+ "should contain gnap");
+ kanban.destroy();
+ });
+
+ QUnit.test('basic grouped rendering', function (assert) {
+ assert.expect(13);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ // the lazy option is important, so the server can fill in
+ // the empty groups
+ assert.ok(args.kwargs.lazy, "should use lazy read_group");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
+ "should have classname 'o_kanban_grouped'");
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_test'),
+ "should have classname 'o_kanban_test'");
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have " + 2 + " columns");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should contain " + 1 + " record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 3,
+ "column should contain " + 3 + " record(s)");
+ // check available actions in kanban header's config dropdown
+ assert.ok(kanban.$('.o_kanban_header:first .o_kanban_config .o_kanban_toggle_fold').length,
+ "should be able to fold the column");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive').length,
+ "should be not able to archive the records");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive').length,
+ "should be not able to restore the records");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_edit').length,
+ "should not be able to edit the column");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_delete').length,
+ "should not be able to delete the column");
+
+ // the next line makes sure that reload works properly. It looks useless,
+ // but it actually test that a grouped local record can be reloaded without
+ // changing its result.
+ kanban.reload();
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 3,
+ "column should contain " + 3 + " record(s)");
+ kanban.destroy();
+ });
+
+ QUnit.test('basic grouped rendering with active field', function (assert) {
+ assert.expect(4);
+
+ // add active field on partner model and make all records active
+ this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="active"/>' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ // check available actions in kanban header's config dropdown
+ assert.ok(kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive').length,
+ "should be able to archive the records");
+ assert.ok(kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive').length,
+ "should be able to restore the records");
+
+ // archive the records of the first column
+ assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 3,
+ "last column should contain 3 records");
+ kanban.$('.o_kanban_group:last .o_column_archive').click(); // click on 'Archive'
+ assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 0,
+ "last column should contain no record");
+ kanban.destroy();
+ });
+
+ QUnit.test('pager should be hidden in grouped mode', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+ kanban.renderPager();
+
+ assert.ok(kanban.pager.$el.hasClass('o_hidden'),
+ "pager should be hidden in grouped kanban");
+ kanban.destroy();
+ });
+
+ QUnit.test('pager, ungrouped, with default limit', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.limit, 40, "default limit should be 40 in Kanban");
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!kanban.pager.$el.hasClass('o_hidden'),
+ "pager should be visible in ungrouped kanban");
+ assert.strictEqual(kanban.pager.state.size, 4, "pager's size should be 4");
+ kanban.destroy();
+ });
+
+ QUnit.test('pager, ungrouped, with limit given in options', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.limit, 2, "limit should be 2");
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ limit: 2,
+ },
+ });
+
+ assert.strictEqual(kanban.pager.state.limit, 2, "pager's limit should be 2");
+ assert.strictEqual(kanban.pager.state.size, 4, "pager's size should be 4");
+ kanban.destroy();
+ });
+
+ QUnit.test('pager, ungrouped, with limit set on arch and given in options', function (assert) {
+ assert.expect(3);
+
+ // the limit given in the arch should take the priority over the one given in options
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" limit="3">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.limit, 3, "limit should be 3");
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ limit: 2,
+ },
+ });
+
+ assert.strictEqual(kanban.pager.state.limit, 3, "pager's limit should be 3");
+ assert.strictEqual(kanban.pager.state.size, 4, "pager's size should be 4");
+ kanban.destroy();
+ });
+
+ QUnit.test('create in grouped on m2o', function (assert) {
+ assert.expect(5);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ });
+ kanban.renderButtons();
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('ui-sortable'),
+ "columns are sortable when grouped by a m2o field");
+ assert.ok(kanban.$buttons.find('.o-kanban-button-new').hasClass('btn-primary'),
+ "'create' button should be btn-primary for grouped kanban with at least one column");
+ assert.ok(kanban.$('.o_kanban_view > div:last').hasClass('o_column_quick_create'),
+ "column quick create should be enabled when grouped by a many2one field)");
+
+ kanban.$buttons.find('.o-kanban-button-new').click(); // Click on 'Create'
+ assert.ok(kanban.$('.o_kanban_group:first() > div:nth(1)').hasClass('o_kanban_quick_create'),
+ "clicking on create should open the quick_create in the first column");
+
+ assert.ok(kanban.$('span.o_column_title:contains(hello)').length,
+ "should have a column title with a value from the many2one");
+ kanban.destroy();
+ });
+
+ QUnit.test('create in grouped on char', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['foo'],
+ });
+
+ assert.ok(!kanban.$('.o_kanban_view').hasClass('ui-sortable'),
+ "columns aren't sortable when not grouped by a m2o field");
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 3, "should have " + 3 + " columns");
+ assert.strictEqual(kanban.$('.o_kanban_group:first() .o_column_title').text(), "yop",
+ "'yop' column should be the first column");
+ assert.ok(!kanban.$('.o_kanban_view > div:last').hasClass('o_column_quick_create'),
+ "column quick create should be disabled when not grouped by a many2one field)");
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create in grouped mode', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ groupBy: ['bar'],
+ });
+
+ // click to add an element and cancel the quick creation
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+
+ var $quickCreate = kanban.$('.o_kanban_quick_create');
+ assert.strictEqual($quickCreate.length, 1, "should have a quick create element");
+
+ $quickCreate.find('input').trigger($.Event('keydown', {keyCode: $.ui.keyCode.ESCAPE}));
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "should have destroyed the quick create element");
+
+ // click to really add an element
+ kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
+ $quickCreate = kanban.$('.o_kanban_quick_create');
+ $quickCreate.find('input').val('new partner');
+ $quickCreate.find('button.o_kanban_add').click();
+
+ assert.strictEqual(this.data.partner.records.length, 5,
+ "should have created a partner");
+ assert.strictEqual(_.last(this.data.partner.records).name, "new partner",
+ "should have correct name");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create fail in grouped', function (assert) {
+ assert.expect(7);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ archs: {
+ 'partner,false,form': '<form string="Partner">' +
+ '<field name="product_id"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ },
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ throw new Error("Odoo server error");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ kanban.renderButtons();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "there should be 2 records in first column");
+
+ kanban.$buttons.find('.o-kanban-button-new').click(); // Click on 'Create'
+ assert.ok(kanban.$('.o_kanban_group:first() > div:nth(1)').hasClass('o_kanban_quick_create'),
+ "clicking on create should open the quick_create in the first column");
+
+ kanban.$('.o_kanban_quick_create input')
+ .val('test')
+ .trigger($.Event('keypress', {keyCode: $.ui.keyCode.ENTER}));
+
+ assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
+ "a form view dialog should have been opened (in edit)");
+ assert.strictEqual($('.modal .o_form_field_many2one input').val(), 'hello',
+ "the correct product_id should already be set");
+
+ // specify a name and save
+ $('.modal .o_form_input[name=foo]').val('test').trigger('input');
+ $('.modal-footer .btn-primary').click();
+
+ assert.strictEqual($('.modal').length, 0, "the modal should be closed");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
+ "there should be 3 records in first column");
+ var $firstRecord = kanban.$('.o_kanban_group:first .o_kanban_record:first');
+ assert.strictEqual($firstRecord.text(), 'test',
+ "the first record of the first column should be the new one");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('many2manytags are correctly fetched and displayed', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].category_ids = [6, 7];
+ this.data.partner.records[1].category_ids = [7];
+
+ var count = 0;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="category_ids"/>' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ mockRPC: function () {
+ count++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var $first_record = kanban.$('.o_kanban_record:first()');
+ assert.strictEqual($first_record.find('.o_form_field_many2manytags .o_tag').length, 2,
+ 'first record should contain 2 tags');
+ assert.ok($first_record.find('.o_tag:first()').hasClass('o_tag_color_2'),
+ 'first tag should have color 2');
+ assert.strictEqual(count, 2,
+ 'two RPC should have been done(one search read and one read for the m2m');
+ kanban.destroy();
+ });
+
+ QUnit.test('can drag and drop a record from one column to the next', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.sequence = {type: 'number', string: "Sequence"};
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/>' +
+ '<t t-if="widget.editable"><span class="thisiseditable">edit</span></t>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ assert.ok(true, "should call resequence");
+ return $.when(true);
+ }
+ return this._super(route, args);
+ },
+ });
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2,
+ "column should contain 2 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
+ "column should contain 2 record(s)");
+
+ assert.strictEqual(kanban.$('.thisiseditable').length, 4, "all records should be editable");
+ var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
+ var $group = kanban.$('.o_kanban_group:nth-child(2)');
+ testUtils.dragAndDrop($record, $group);
+
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
+ "column should now contain 1 record(s)");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 3,
+ "column should contain 3 record(s)");
+ assert.strictEqual(kanban.$('.thisiseditable').length, 4, "all records should be editable");
+ kanban.destroy();
+ });
+
+ QUnit.test('kanban view with default_group_by', function (assert) {
+ assert.expect(7);
+ this.data.partner.records.product_id = 1;
+ this.data.product.records.push({id: 1, display_name: "third product"});
+
+ var readGroupCount = 0;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" default_group_by="bar">' +
+ '<field name="bar"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/read_group') {
+ readGroupCount++;
+ var correctGroupBy;
+ if (readGroupCount === 2) {
+ correctGroupBy = ['product_id'];
+ } else {
+ correctGroupBy = ['bar'];
+ }
+ // this is done three times
+ assert.ok(_.isEqual(args.kwargs.groupby, correctGroupBy),
+ "groupby args should be correct");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
+ "should have classname 'o_kanban_grouped'");
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have " + 2 + " columns");
+
+ // simulate an update coming from the searchview, with another groupby given
+ kanban.update({groupBy: ['product_id']});
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should now have " + 3 + " columns");
+
+ // simulate an update coming from the searchview, removing the previously set groupby
+ kanban.update({groupBy: []});
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have " + 2 + " columns again");
+ kanban.destroy();
+ });
+
+ QUnit.test('kanban view with create=False', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" create="0">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates></kanban>',
+ });
+
+ assert.ok(!kanban.$buttons || !kanban.$buttons.find('.o-kanban-button-new').length,
+ "Create button shouldn't be there");
+ kanban.destroy();
+ });
+
+ QUnit.test('clicking on a link triggers correct event', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
+ '<div><a type="edit">Edit</a></div>' +
+ '</t></templates></kanban>',
+ });
+
+ testUtils.intercept(kanban, 'switch_view', function (event) {
+ assert.deepEqual(event.data, {
+ view_type: 'form',
+ res_id: 1,
+ mode: 'edit',
+ model: 'partner',
+ });
+ });
+ kanban.$('a').first().click();
+ kanban.destroy();
+ });
+
+ QUnit.test('create a column in grouped on m2o', function (assert) {
+ assert.expect(10);
+
+ var nbRPCs = 0;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ nbRPCs++;
+ if (args.method === 'name_create') {
+ assert.ok(true, "should call name_create");
+ }
+ return this._super(route, args);
+ }
+ });
+ assert.strictEqual(kanban.$('.o_column_quick_create').length, 1, "should have a quick create column");
+ assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'),
+ "the input should not be visible");
+
+ kanban.$('.o_column_quick_create').click();
+
+ assert.ok(kanban.$('.o_column_quick_create input').is(':visible'),
+ "the input should be visible");
+
+ kanban.$('.o_column_quick_create input').val('new value');
+ kanban.$('.o_column_quick_create button.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new value)').length, 1,
+ "the last column should be the newly created one");
+ assert.ok(_.isNumber(kanban.$('.o_kanban_group:last').data('id')),
+ 'the created column should have the correct id');
+ assert.ok(!kanban.$('.o_kanban_group:last').hasClass('o_column_folded'),
+ 'the created column should not be folded');
+
+ // fold and unfold the created column, and check that no RPC is done (as there is no record)
+ nbRPCs = 0;
+ kanban.$('.o_kanban_group:last .o_kanban_toggle_fold').click(); // fold the group
+ assert.ok(kanban.$('.o_kanban_group:last').hasClass('o_column_folded'),
+ 'the created column should now be folded');
+ kanban.$('.o_kanban_group:last').click();
+ assert.ok(!kanban.$('.o_kanban_group:last').hasClass('o_column_folded'),
+ 'the created column should not be folded');
+ assert.strictEqual(nbRPCs, 0, 'no rpc should have been done when folding/unfolding');
+ kanban.destroy();
+ });
+
+ QUnit.test('delete a column in grouped on m2o', function (assert) {
+ assert.expect(21);
+
+ var nbRPCs = 0;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ nbRPCs++;
+ return this._super(route, args);
+ }
+ });
+
+ // check the initial rendering
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have two columns");
+ assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 3,
+ 'first column should be [3, "hello"]');
+ assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 5,
+ 'second column should be [5, "xmo"]');
+ assert.strictEqual(kanban.$('.o_kanban_group:last .o_column_title').text(), 'xmo',
+ 'second column should have correct title');
+ assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 2,
+ "second column should have two records");
+
+ // check available actions in kanban header's config dropdown
+ assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length,
+ "should be able to fold the column");
+ assert.ok(kanban.$('.o_kanban_group:first .o_column_edit').length,
+ "should be able to edit the column");
+ assert.ok(kanban.$('.o_kanban_group:first .o_column_delete').length,
+ "should be able to delete the column");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive').length,
+ "should not be able to archive the records");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive').length,
+ "should not be able to restore the records");
+
+ // delete second column (first cancel the confirm request, then confirm)
+ kanban.$('.o_kanban_group:last .o_column_delete').click(); // click on delete
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-default').click(); // click on cancel
+ assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 5,
+ 'column [5, "xmo"] should still be there');
+ kanban.$('.o_kanban_group:last .o_column_delete').click(); // click on delete
+ assert.ok($('.modal').length, 'a confirm modal should be displayed');
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+ assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 3,
+ 'last column should now be [3, "hello"]');
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should still have two columns");
+ assert.ok(!kanban.$('.o_kanban_group:first').data('id'),
+ 'first column should have no id (Undefined column)');
+ // check available actions on 'Undefined' column
+ assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length,
+ "should be able to fold the column");
+ assert.ok(!kanban.$('.o_kanban_group:first .o_column_delete').length,
+ 'Undefined column could not be deleted');
+ assert.ok(!kanban.$('.o_kanban_group:first .o_column_edit').length,
+ 'Undefined column could not be edited');
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_archive').length,
+ "should not be able to archive the records");
+ assert.ok(!kanban.$('.o_kanban_header:first .o_kanban_config .o_column_unarchive').length,
+ "should not be able to restore the records");
+ kanban.destroy();
+ });
+
+ QUnit.test('edit a column in grouped on m2o', function (assert) {
+ assert.expect(12);
+
+ var nbRPCs = 0;
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ archs: {
+ 'product,false,form': '<form string="Product"><field name="display_name"/></form>',
+ },
+ mockRPC: function (route, args) {
+ nbRPCs++;
+ return this._super(route, args);
+ },
+ });
+ assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo',
+ 'title of the column should be "xmo"');
+
+ // edit the title of column [5, 'xmo'] and close without saving
+ kanban.$('.o_kanban_group[data-id=5] .o_column_edit').click(); // click on 'Edit'
+ assert.ok($('.modal .o_form_editable').length, 'a form view should be open in a modal');
+ assert.strictEqual($('.modal .o_form_editable .o_form_input').val(), 'xmo',
+ 'the name should be "xmo"');
+ $('.modal .o_form_editable .o_form_input').val('ged').trigger('input'); // change the value
+ nbRPCs = 0;
+ $('.modal .modal-header .close').click(); // click on the cross to close the modal
+ assert.ok(!$('.modal').length, 'the modal should be closed');
+ assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo',
+ 'title of the column should still be "xmo"');
+ assert.strictEqual(nbRPCs, 0, 'no RPC should have been done');
+
+ // edit the title of column [5, 'xmo'] and discard
+ kanban.$('.o_kanban_group[data-id=5] .o_column_edit').click(); // click on 'Edit'
+ $('.modal .o_form_editable .o_form_input').val('ged').trigger('input'); // change the value
+ nbRPCs = 0;
+ $('.modal .modal-footer .btn-default').click(); // click on discard
+ assert.ok(!$('.modal').length, 'the modal should be closed');
+ assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'xmo',
+ 'title of the column should still be "xmo"');
+ assert.strictEqual(nbRPCs, 0, 'no RPC should have been done');
+
+ // edit the title of column [5, 'xmo'] and save
+ kanban.$('.o_kanban_group[data-id=5] .o_column_edit').click(); // click on 'Edit'
+ $('.modal .o_form_editable .o_form_input').val('ged').trigger('input'); // change the value
+ nbRPCs = 0;
+ $('.modal .modal-footer .btn-primary').click(); // click on save
+ assert.ok(!$('.modal').length, 'the modal should be closed');
+ assert.strictEqual(kanban.$('.o_kanban_group[data-id=5] .o_column_title').text(), 'ged',
+ 'title of the column should be "ged"');
+ assert.strictEqual(nbRPCs, 4, 'should have done 1 write, 1 read_group and 2 search_read');
+ kanban.destroy();
+ });
+
+ QUnit.test('if view was grouped at start, it stays grouped', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ });
+ kanban.update({groupBy: []});
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'));
+ kanban.destroy();
+ });
+
+ QUnit.test('if view was not grouped at start, it can be grouped and ungrouped', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ });
+ assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'), "should not be grouped");
+ kanban.update({groupBy: ['product_id']});
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'), "should be grouped");
+ kanban.update({groupBy: []});
+ assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'), "should not be grouped");
+ kanban.destroy();
+ });
+
+ QUnit.test('no content helper when no data', function (assert) {
+ assert.expect(5);
+
+ var records = this.data.partner.records;
+
+ this.data.partner.records = [];
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<t t-esc="record.foo.value"/>' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ viewOptions: {
+ action: {
+ help: '<p class="hello">click to add a partner</p>'
+ }
+ },
+ });
+
+ assert.ok(kanban.$el.hasClass('o_kanban_nocontent'),
+ "$el should have correct no content class");
+
+ assert.strictEqual(kanban.$('.oe_view_nocontent').length, 1,
+ "should display the no content helper");
+
+ assert.strictEqual(kanban.$('.oe_view_nocontent p.hello:contains(add a partner)').length, 1,
+ "should have rendered no content helper from action");
+
+ this.data.partner.records = records;
+ kanban.reload();
+
+ assert.notOk(kanban.$el.hasClass('o_kanban_nocontent'),
+ "$el should have removed no content class");
+
+ assert.strictEqual(kanban.$('.oe_view_nocontent').length, 0,
+ "should not display the no content helper");
+ kanban.destroy();
+ });
+
+ QUnit.test('no nocontent helper for grouped kanban with empty groups', function (assert) {
+ assert.expect(2);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ // override read_group to return empty groups, as this is
+ // the case for several models (e.g. project.task grouped
+ // by stage_id)
+ return this._super.apply(this, arguments).then(function (result) {
+ _.each(result, function (group) {
+ group[args.kwargs.groupby[0] + '_count'] = 0;
+ });
+ return result;
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ action: {
+ help: "No content helper",
+ },
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
+ "there should be two columns");
+ assert.strictEqual(kanban.$('.o_kanban_record').length, 0,
+ "there should be no records");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('buttons with modifiers', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[1].bar = false; // so that test is more complete
+
+ var kanban = createView({
+ View: KanbanView,
+ model: "partner",
+ data: this.data,
+ arch:
+ '<kanban>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '<field name="state"/>' +
+ '<templates><div t-name="kanban-box">' +
+ '<button class="o_btn_test_1" type="object" name="a1" ' +
+ 'attrs="{\'invisible\': [[\'foo\', \'!=\', \'yop\']]}"/>' +
+ '<button class="o_btn_test_2" type="object" name="a2" ' +
+ 'attrs="{\'invisible\': [[\'bar\', \'=\', True]]}" ' +
+ 'states="abc,def"/>' +
+ '</div></templates>' +
+ '</kanban>',
+ });
+
+ assert.strictEqual(kanban.$(".o_btn_test_1").length, 1,
+ "kanban should have one buttons of type 1");
+ assert.strictEqual(kanban.$(".o_btn_test_2").length, 3,
+ "kanban should have three buttons of type 2");
+ kanban.destroy();
+ });
+
+ QUnit.test('rendering date and datetime', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].date = "2017-01-25";
+ this.data.partner.records[1].datetime= "2016-12-12 10:55:05";
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="date"/>' +
+ '<field name="datetime"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<t t-esc="record.date.raw_value"/>' +
+ '<t t-esc="record.datetime.raw_value"/>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ });
+
+ // FIXME: this test is locale dependant. we need to do it right.
+ assert.strictEqual(kanban.$('div.o_kanban_record:contains(Wed Jan 25)').length, 1,
+ "should have formatted the date");
+ assert.strictEqual(kanban.$('div.o_kanban_record:contains(Mon Dec 12)').length, 1,
+ "should have formatted the datetime");
+ kanban.destroy();
+ });
+
+ QUnit.test('evaluate conditions on relational fields', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].product_id = false;
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="product_id"/>' +
+ '<field name="category_ids"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<button t-if="!record.product_id.raw_value" class="btn_a">A</button>' +
+ '<button t-if="!record.category_ids.raw_value" class="btn_b">B</button>' +
+ '</div>' +
+ '</t></templates>' +
+ '</kanban>',
+ });
+
+ assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ "there should be 4 records");
+ assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost) .btn_a').length, 1,
+ "only 1 of them should have the 'Action' button");
+ assert.strictEqual($('.o_kanban_record:not(.o_kanban_ghost) .btn_b').length, 2,
+ "only 2 of them should have the 'Action' button");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('resequence columns in grouped by m2o', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="product_id"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ mockRPC: function (route) {
+ if (route === '/web/dataset/resequence') {
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('ui-sortable'),
+ "columns should be sortable");
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
+ "should have two columns");
+ assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 3,
+ "first column should be id 3 before resequencing");
+
+ // there is a 100ms delay on the d&d feature (jquery sortable) for
+ // kanban columns, making it hard to test. So we rather bypass the d&d
+ // for this test, and directly call the event handler
+ kanban._onResequenceColumn({data: {ids: [5, 3]}});
+ kanban.update({}, {reload: false}); // re-render without reloading
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5,
+ "first column should be id 5 before resequencing");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('properly evaluate more complex domains', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '<field name="category_ids"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="foo"/>' +
+ '<button type="object" attrs="{\'invisible\':[\'|\', (\'bar\',\'=\',True), (\'category_ids\', \'!=\', [])]}" class="btn btn-primary pull-right btn-sm" name="channel_join_and_get_info">Join</button>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ });
+
+ assert.strictEqual(kanban.$('button.oe_kanban_action_button').length, 1,
+ "only one button should be visible");
+ kanban.destroy();
+ });
+
+});
+
+});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
new file mode 100644
index 00000000000..0e710ca1653
--- /dev/null
+++ b/addons/web/static/tests/views/list_tests.js
@@ -0,0 +1,1264 @@
+odoo.define('web.list_tests', function (require) {
+"use strict";
+
+var config = require('web.config');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ foo: {
+ fields: {
+ foo: {string: "Foo", type: "char"},
+ bar: {string: "Bar", type: "boolean"},
+ date: {string: "Some Date", type: "date"},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "my float", type: "float"},
+ m2o: {string: "M2O field", type: "many2one", relation: "bar"},
+ m2m: {string: "M2M field", type: "many2many", relation: "bar"},
+ amount: {string: "Monetary field", type: "monetary"},
+ currency_id: {string: "Currency", type: "many2one",
+ relation: "res_currency", default: 1},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ },
+ records: [
+ {
+ id: 1,
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.4,
+ m2o: 1,
+ m2m: [1, 2],
+ amount: 1200,
+ currency_id: 2,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ },
+ {id: 2, bar: true, foo: "blip", int_field: 9, qux: 13,
+ m2o: 2, m2m: [1, 2, 3], amount: 500},
+ {id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3,
+ m2o: 1, m2m: [], amount: 300},
+ {id: 4, bar: false, foo: "blip", int_field: -4, qux: 9,
+ m2o: 1, m2m: [1], amount: 0},
+ ]
+ },
+ bar: {
+ fields: {},
+ records: [
+ {id: 1, display_name: "Value 1"},
+ {id: 2, display_name: "Value 2"},
+ {id: 3, display_name: "Value 3"},
+ ]
+ },
+ res_currency: {
+ fields: {
+ symbol: {string: "Symbol", type: "char"},
+ position: {
+ string: "Position",
+ type: "selection",
+ selection: [['after', 'A'], ['before', 'B']],
+ },
+ },
+ records: [
+ {id: 1, display_name: "USD", symbol: '$', position: 'before'},
+ {id: 2, display_name: "EUR", symbol: '€', position: 'after'},
+ ],
+ }
+ };
+ }
+}, function () {
+
+ QUnit.module('ListView');
+
+ QUnit.test('simple readonly list', function (assert) {
+ assert.expect(9);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
+ });
+
+ // 3 th (1 for checkbox, 2 for columns)
+ assert.strictEqual(list.$('th').length, 3, "should have 3 columns");
+
+ assert.strictEqual(list.$('td:contains(gnap)').length, 1, "should contain gnap");
+ assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
+ assert.strictEqual(list.$('th.o_column_sortable').length, 1, "should have 1 sortable column");
+
+ assert.strictEqual(list.$('thead th:nth(2)').css('text-align'), 'right',
+ "header cells of integer fields should be right aligned");
+ assert.strictEqual(list.$('tbody tr:first td:nth(2)').css('text-align'), 'right',
+ "integer cells should be right aligned");
+
+ assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
+ "should have a visible Create button");
+ assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
+ "should not have a visible save button");
+ assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
+ "should not have a visible save button");
+ list.destroy();
+ });
+
+
+ QUnit.test('simple editable rendering', function (assert) {
+ assert.expect(9);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
+ });
+
+ assert.strictEqual(list.$('th').length, 3, "should have 2 th");
+ assert.strictEqual(list.$('th').length, 3, "should have 3 th");
+ assert.strictEqual(list.$('td:contains(yop)').length, 1, "should contain yop");
+
+ assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
+ "should have a visible Create button");
+ assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
+ "should not have a visible save button");
+ assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
+ "should not have a visible discard button");
+
+ list.$('td:not(.o_list_record_selector)').first().click();
+
+ assert.ok(!list.$buttons.find('.o_list_button_add').is(':visible'),
+ "should not have a visible Create button");
+ assert.ok(list.$buttons.find('.o_list_button_save').is(':visible'),
+ "should have a visible save button");
+ assert.ok(list.$buttons.find('.o_list_button_discard').is(':visible'),
+ "should have a visible discard button");
+ list.destroy();
+ });
+
+ QUnit.test('invisible columns are not displayed', function (assert) {
+ assert.expect(1);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar" invisible="1"/>' +
+ '</tree>',
+ });
+
+ // 1 th for checkbox, 1 for 1 visible column
+ assert.strictEqual(list.$('th').length, 2, "should have 2 th");
+ list.destroy();
+ });
+
+ QUnit.test('at least 4 rows are rendered, even if less data', function (assert) {
+ assert.expect(1);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="bar"/></tree>',
+ domain: [['bar', '=', true]],
+ });
+
+ assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
+ list.destroy();
+ });
+
+ QUnit.test('basic grouped list rendering', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ groupBy: ['bar'],
+ });
+
+ assert.strictEqual(list.$('th:contains(Foo)').length, 1, "should contain Foo");
+ assert.strictEqual(list.$('th:contains(Bar)').length, 1, "should contain Bar");
+ assert.strictEqual(list.$('tr.o_group_header').length, 2, "should have 2 .o_group_header");
+ assert.strictEqual(list.$('th.o_group_name').length, 2, "should have 2 .o_group_name");
+ list.destroy();
+ });
+
+ QUnit.test('many2one field rendering', function (assert) {
+ assert.expect(1);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="m2o"/></tree>',
+ });
+
+ assert.ok(list.$('td:contains(Value 1)').length,
+ "should have the display_name of the many2one");
+ list.destroy();
+ });
+
+ QUnit.test('grouped list view, with 1 open group', function (assert) {
+ assert.expect(6);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
+ groupBy: ['foo'],
+ });
+
+ list.$('th.o_group_name').get(1).click();
+ assert.strictEqual(list.$('tbody:eq(1) tr').length, 2, "open group should contain 2 records");
+ assert.strictEqual(list.$('tbody').length, 3, "should contain 3 tbody");
+ assert.strictEqual(list.$('td:contains(9)').length, 1, "should contain 9");
+ assert.strictEqual(list.$('td:contains(-4)').length, 1, "should contain -4");
+ assert.strictEqual(list.$('td:contains(10)').length, 1, "should contain 10");
+ assert.strictEqual(list.$('tr.o_group_header td:contains(10)').length, 1, "but 10 should be in a header");
+ list.destroy();
+ });
+
+ QUnit.test('opening records when clicking on record', function (assert) {
+ assert.expect(3);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ testUtils.intercept(list, "open_record", function () {
+ assert.ok("list view should trigger 'open_record' event");
+ });
+
+ list.$('tr td:not(.o_list_record_selector)').first().click();
+ list.update({groupBy: ['foo']});
+ assert.strictEqual(list.$('tr.o_group_header').length, 3, "list should be grouped");
+ list.$('th.o_group_name').first().click();
+
+ list.$('tr:not(.o_group_header) td:not(.o_list_record_selector)').first().click();
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: readonly fields cannot be edited', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.fields.foo.readonly = true;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '<field name="int_field" readonly="1"/>' +
+ '</tree>',
+ });
+ var $td = list.$('td:not(.o_list_record_selector)').first();
+ var $second_td = list.$('td:not(.o_list_record_selector)').eq(1);
+ var $third_td = list.$('td:not(.o_list_record_selector)').eq(2);
+ $td.click();
+ assert.ok(!$td.hasClass('o_edit_mode'), "foo cells should not be editable");
+ assert.ok($second_td.hasClass('o_edit_mode'), "bar cells should be editable");
+ assert.ok(!$third_td.hasClass('o_edit_mode'), "int_field cells should not be editable");
+ list.destroy();
+ });
+
+ QUnit.test('basic operations for editable list renderer', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
+ });
+
+ var $td = list.$('td:not(.o_list_record_selector)').first();
+ assert.strictEqual($td.hasClass('o_edit_mode'), false, "td should not be in edit mode");
+ $td.click();
+ assert.strictEqual($td.hasClass('o_edit_mode'), true, "td should be in edit mode");
+ assert.strictEqual($td.hasClass('o_field_dirty'), false, "td should not be dirty");
+ $td.find('input').val('abc').trigger('input');
+ assert.strictEqual($td.hasClass('o_field_dirty'), true, "td should be dirty");
+ list.destroy();
+ });
+
+ QUnit.test('field changes are triggered correctly', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
+ });
+ var $td = list.$('td:not(.o_list_record_selector)').first();
+
+ var n = 0;
+ testUtils.intercept(list, "field_changed", function () {
+ n = 1;
+ });
+ $td.click();
+ $td.find('input').val('abc').trigger('input');
+ assert.strictEqual(n, 1, "field_changed should not have been triggered");
+ list.$('td:not(.o_list_record_selector)').eq(2).click();
+ assert.strictEqual(n, 1, "field_changed should have been triggered");
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: basic char field edition', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
+ });
+ var $td = list.$('td:not(.o_list_record_selector)').first();
+ $td.click();
+ $td.find('input').val('abc').trigger('input');
+ assert.strictEqual($td.find('input').val(), 'abc', "char field has been edited correctly");
+
+ var $next_row_td = list.$('tbody tr:eq(1) td:not(.o_list_record_selector)').first();
+ $next_row_td.click(); // should trigger the save of the previous row
+ assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), 'abc',
+ 'changes should be saved correctly');
+ assert.ok(!list.$('tbody tr').first().hasClass('o_selected_row'),
+ 'saved row should be in readonly mode');
+ assert.strictEqual(this.data.foo.records[0].foo, 'abc',
+ "the edition should have been properly saved");
+ list.destroy();
+ });
+
+ QUnit.test('selection changes are triggered correctly', function (assert) {
+ assert.expect(8);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ });
+ var $tbody_selector = list.$('tbody .o_list_record_selector input').first();
+ var $thead_selector = list.$('thead .o_list_record_selector input');
+
+ var n = 0;
+ testUtils.intercept(list, "selection_changed", function () {
+ n += 1;
+ });
+
+ // tbody checkbox click
+ $tbody_selector.click();
+ assert.strictEqual(n, 1, "selection_changed should have been triggered");
+ assert.ok($tbody_selector.is(':checked'), "selection checkbox should be checked");
+ $tbody_selector.click();
+ assert.strictEqual(n, 2, "selection_changed should have been triggered");
+ assert.ok(!$tbody_selector.is(':checked'), "selection checkbox shouldn't be checked");
+
+ // head checkbox click
+ $thead_selector.click();
+ assert.strictEqual(n, 3, "selection_changed should have been triggered");
+ assert.strictEqual(list.$('tbody .o_list_record_selector input:checked').length,
+ list.$('tbody tr').length, "all selection checkboxes should be checked");
+
+ $thead_selector.click();
+ assert.strictEqual(n, 4, "selection_changed should have been triggered");
+
+ assert.strictEqual(list.$('tbody .o_list_record_selector input:checked').length, 0,
+ "no selection checkbox should be checked");
+ list.destroy();
+ });
+
+ QUnit.test('aggregates are computed correctly', function (assert) {
+ assert.expect(3);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="int_field" sum="Sum"/></tree>',
+ });
+ var $tbody_selectors = list.$('tbody .o_list_record_selector input');
+ var $thead_selector = list.$('thead .o_list_record_selector input');
+
+ assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32");
+
+ $tbody_selectors.first().click();
+ $tbody_selectors.last().click();
+ assert.strictEqual(list.$('tfoot td:nth(2)').text(), "6",
+ "total should be 6 as first and last records are selected");
+
+ $thead_selector.click();
+ assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32",
+ "total should be 32 as all records are selected");
+ list.destroy();
+ });
+
+ QUnit.test('aggregates are computed correctly in grouped lists', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ groupBy: ['m2o'],
+ arch: '<tree editable="bottom"><field name="int_field" sum="Sum"/></tree>',
+ });
+
+ var $group_1_header = list.$('.o_group_header').filter(function (index, el) {
+ return $(el).data('group').res_id === 1;
+ });
+ var $group_2_header = list.$('.o_group_header').filter(function (index, el) {
+ return $(el).data('group').res_id === 2;
+ });
+ assert.strictEqual($group_1_header.find('td:nth(1)').text(), "23", "first group total should be 23");
+ assert.strictEqual($group_2_header.find('td:nth(1)').text(), "9", "second group total should be 9");
+ assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32");
+
+ $group_1_header.click();
+ list.$('tbody .o_list_record_selector input').first().click();
+ assert.strictEqual(list.$('tfoot td:nth(2)').text(), "10",
+ "total should be 10 as first record of first group is selected");
+ list.destroy();
+ });
+
+ QUnit.test('properly apply onchange in simple case', function (assert) {
+ assert.expect(2);
+
+ this.data.foo.onchanges = {
+ foo: function (obj) {
+ obj.int_field = obj.foo.length + 1000;
+ },
+ };
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top"><field name="foo"/><field name="int_field"/></tree>',
+ });
+
+ var $foo_td = list.$('td:not(.o_list_record_selector)').first();
+ var $int_field_td = list.$('td:not(.o_list_record_selector)').eq(1);
+
+ assert.strictEqual($int_field_td.text(), '10', "should contain initial value");
+
+ $foo_td.click();
+ $foo_td.find('input').val('tralala').trigger('input');
+
+ assert.strictEqual($int_field_td.find('input').val(), "1007",
+ "should contain input with onchange applied");
+ list.destroy();
+ });
+
+ QUnit.test('column width should not change when switching mode', function (assert) {
+ assert.expect(10);
+
+ // Warning: this test is css dependant
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '<field name="int_field" readonly="1"/>' +
+ '<field name="m2o"/>' +
+ '<field name="m2m" widget="many2many_tags"/>' +
+ '</tree>',
+ });
+ var startWidths = _.pluck(list.$('thead th'), 'offsetWidth');
+
+ // start edition of first row
+ list.$('td:not(.o_list_record_selector)').first().click();
+
+ var editionWidths = _.pluck(list.$('thead th'), 'offsetWidth');
+
+ // leave edition
+ list.$buttons.find('.o_list_button_save').click();
+ var readonlyWidths = _.pluck(list.$('thead th'), 'offsetWidth');
+
+ for (var i = 0; i < startWidths.length; i++) {
+ assert.strictEqual(startWidths[i], editionWidths[i],
+ 'width of columns should remain unchanged which switching from readonly to edit mode');
+ assert.strictEqual(editionWidths[i], readonlyWidths[i],
+ 'width of columns should remain unchanged which switching from edit to readonly mode');
+ }
+ list.destroy();
+ });
+
+ QUnit.test('deleting one record', function (assert) {
+ assert.expect(5);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ viewOptions: {sidebar: true},
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ assert.ok(list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be invisible');
+ assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 4, "should have 4 records");
+
+ list.$('tbody td.o_list_record_selector:first input').click();
+
+ assert.ok(!list.sidebar.$el.hasClass('o_hidden'), 'sidebar should be visible');
+
+ list.sidebar.$('a:contains(Delete)').click();
+ assert.ok($('body').hasClass('modal-open'), 'body should have modal-open clsss');
+
+ $('body .modal-dialog button span:contains(Ok)').click();
+
+ assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 3, "should have 3 records");
+ list.destroy();
+ });
+
+ QUnit.test('pager (ungrouped and grouped mode), default limit', function (assert) {
+ assert.expect(4);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/search_read') {
+ assert.strictEqual(args.limit, 80, "default limit should be 80 in List");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!list.pager.$el.hasClass('o_hidden'), "pager should be visible");
+ assert.strictEqual(list.pager.state.size, 4, "pager's size should be 4");
+ list.update({ groupBy: ['bar']});
+ assert.ok(list.pager.$el.hasClass('o_hidden'), "pager should be invisible");
+ list.destroy();
+ });
+
+ QUnit.test('can sort records when clicking on header', function (assert) {
+ assert.expect(9);
+
+ this.data.foo.fields.foo.sortable = true;
+
+ var nbSearchRead = 0;
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ mockRPC: function (route) {
+ if (route === '/web/dataset/search_read') {
+ nbSearchRead++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
+ assert.ok(list.$('tbody tr:first td:contains(yop)').length,
+ "record 1 should be first");
+ assert.ok(list.$('tbody tr:eq(3) td:contains(blip)').length,
+ "record 3 should be first");
+
+ nbSearchRead = 0;
+ list.$('thead th:contains(Foo)').click();
+ assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
+ assert.ok(list.$('tbody tr:first td:contains(blip)').length,
+ "record 3 should be first");
+ assert.ok(list.$('tbody tr:eq(3) td:contains(yop)').length,
+ "record 1 should be first");
+
+ nbSearchRead = 0;
+ list.$('thead th:contains(Foo)').click();
+ assert.strictEqual(nbSearchRead, 1, "should have done one search_read");
+ assert.ok(list.$('tbody tr:first td:contains(yop)').length,
+ "record 3 should be first");
+ assert.ok(list.$('tbody tr:eq(3) td:contains(blip)').length,
+ "record 1 should be first");
+
+ list.destroy();
+ });
+
+ QUnit.test('can display button in edit mode', function (assert) {
+ assert.expect(1);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<button name="notafield" type="object" icon="fa-asterisk"/>' +
+ '</tree>',
+ });
+ assert.ok(list.$('tbody button').length, "should have a button");
+ list.destroy();
+ });
+
+ QUnit.test('can display a list with a many2many field', function (assert) {
+ assert.expect(2);
+
+ var rpcCount = 0;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="m2m"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ rpcCount++;
+ return this._super(route, args);
+ },
+ });
+ assert.strictEqual(rpcCount, 2, "should have done 2 rpcs: 1 searchread and 1 read for m2m");
+ assert.ok(list.$('td:contains(3 records)').length,
+ "should have a td with correct formatted value");
+ list.destroy();
+ });
+
+
+ QUnit.test('list without import button', function (assert) {
+ assert.expect(1);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ context: {
+ group_by_no_leaf: true,
+ }
+ });
+
+ assert.ok(!list.$buttons, "should not have any buttons");
+ list.destroy();
+ });
+
+ QUnit.test('display a tooltip on a field', function (assert) {
+ assert.expect(2);
+
+ var initialDebugMode = config.debug;
+ config.debug = false;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ // this is done to force the tooltip to show immediately instead of waiting
+ // 1000 ms. not totally academic, but a short test suite is easier to sell :(
+ list.$('th:not(.o_list_record_selector)').tooltip('show', false);
+
+ list.$('th:not(.o_list_record_selector)').trigger($.Event('mouseenter'));
+ assert.strictEqual($('.tooltip .oe_tooltip_string').length, 0, "should not have rendered a tooltip");
+
+ config.debug = true;
+ // it is necessary to rerender the list so tooltips can be properly created
+ list.reload();
+ list.$('th:not(.o_list_record_selector)').tooltip('show', false);
+
+ list.$('th:not(.o_list_record_selector)').trigger($.Event('mouseenter'));
+ assert.strictEqual($('.tooltip .oe_tooltip_string').length, 1, "should have rendered a tooltip");
+
+ config.debug = initialDebugMode;
+ list.destroy();
+ });
+
+ QUnit.test('support row decoration', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree decoration-info="int_field > 5">' +
+ '<field name="foo"/><field name="int_field"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('tbody tr.text-info').length, 3,
+ "should have 3 columns with text-info class");
+
+ assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
+ list.destroy();
+ });
+
+ QUnit.test('support row decoration with date', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.records[0].datetime = '2017-02-27 12:51:35';
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree decoration-info="datetime == \'2017-02-27 12:51:35\'" decoration-danger="datetime &gt; \'2017-02-27 12:51:35\' AND datetime &lt; \'2017-02-27 10:51:35\'">' +
+ '<field name="datetime"/><field name="int_field"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('tbody tr.text-info').length, 1,
+ "should have 1 columns with text-info class with good datetime");
+
+ assert.strictEqual(list.$('tbody tr.text-danger').length, 0,
+ "should have 0 columns with text-danger class with wrong timezone datetime");
+
+ assert.strictEqual(list.$('tbody tr').length, 4, "should have 4 rows");
+ list.destroy();
+ });
+
+ QUnit.test('no content helper when no data', function (assert) {
+ assert.expect(5);
+
+ var records = this.data.foo.records;
+
+ this.data.foo.records = [];
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ viewOptions: {
+ action: {
+ help: '<p class="hello">click to add a partner</p>'
+ }
+ },
+ });
+
+ assert.strictEqual(list.$('.oe_view_nocontent').length, 1,
+ "should display the no content helper");
+
+ assert.strictEqual(list.$('table').length, 0, "should not have a table in the dom");
+
+ assert.strictEqual(list.$('.oe_view_nocontent p.hello:contains(add a partner)').length, 1,
+ "should have rendered no content helper from action");
+
+ this.data.foo.records = records;
+ list.reload();
+
+ assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
+ "should not display the no content helper");
+ assert.strictEqual(list.$('table').length, 1, "should have a table in the dom");
+ list.destroy();
+ });
+
+ QUnit.test('no nocontent helper when no data and no help', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.records = [];
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
+ "should not display the no content helper");
+
+ assert.strictEqual(list.$('tr.o_data_row').length, 0,
+ "should not have any data row");
+
+ assert.strictEqual(list.$('table').length, 1, "should have a table in the dom");
+ list.destroy();
+ });
+
+ QUnit.test('list view, editable, without data', function (assert) {
+ assert.expect(9);
+
+ this.data.foo.records = [];
+
+ this.data.foo.fields.date.default = "2017-02-10";
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="date"/>' +
+ '<field name="m2o"/>' +
+ '<field name="foo"/>' +
+ '</tree>',
+ viewOptions: {
+ action: {
+ help: '<p class="hello">click to add a partner</p>'
+ }
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.ok(true, "should have created a record");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(list.$('.oe_view_nocontent').length, 1,
+ "should have a no content helper displayed");
+
+ assert.strictEqual(list.$('div.table-responsive').length, 0,
+ "should not have a div.table-responsive");
+ assert.strictEqual(list.$('table').length, 0, "should not have rendered a table");
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.strictEqual(list.$('.oe_view_nocontent').length, 0,
+ "should not have a no content helper displayed");
+ assert.strictEqual(list.$('table').length, 1, "should have rendered a table");
+ assert.strictEqual(list.$el.css('height'), list.$('div.table-responsive').css('height'),
+ "the div for the table should take the full height");
+
+
+ assert.ok(list.$('tbody tr:eq(0) td:eq(1)').hasClass('o_edit_mode'),
+ "the date field td should be in edit mode");
+ assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
+ "the date field td should not have any content");
+
+ list.$buttons.find('.o_list_button_save').click();
+ list.destroy();
+ });
+
+ QUnit.test('list view, editable, with a button', function (assert) {
+ assert.expect(1);
+
+ this.data.foo.records = [];
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="foo"/>' +
+ '<button string="abc" icon="fa-phone" type="object" name="schedule_another_phonecall"/>' +
+ '</tree>',
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.strictEqual(list.$('table button.o_icon_button i.fa-phone').length, 1,
+ "should have rendered a button");
+ list.destroy();
+ });
+
+ QUnit.test('list view, editable, can discard', function (assert) {
+ assert.expect(5);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 0, "no input should be in the table");
+
+ list.$('tbody td:not(.o_list_record_selector):first').click();
+ assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 1, "first cell should be editable");
+
+ assert.ok(list.$buttons.find('.o_list_button_discard').is(':visible'),
+ "discard button should be visible");
+
+ list.$buttons.find('.o_list_button_discard').click();
+
+ assert.strictEqual(list.$('td:not(.o_list_record_selector) input').length, 0, "no input should be in the table");
+
+ assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
+ "discard button should not be visible");
+ list.destroy();
+ });
+
+ QUnit.test('editable list view, click on the list to save', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.fields.date.default = "2017-02-10";
+ this.data.foo.records = [];
+
+ var createCount = 0;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="date"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ createCount++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+ list.$('div.table-responsive').click();
+
+ assert.strictEqual(createCount, 1, "should have created a record");
+
+ list.$buttons.find('.o_list_button_add').click();
+ list.$('tfoot').click();
+
+ assert.strictEqual(createCount, 2, "should have created a record");
+
+ list.$buttons.find('.o_list_button_add').click();
+ list.$('tbody tr').last().click();
+
+ assert.strictEqual(createCount, 3, "should have created a record");
+ list.destroy();
+ });
+
+ QUnit.test('click on a button in a list view', function (assert) {
+ assert.expect(6);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="foo"/>' +
+ '<button string="a button" name="button_action" icon="fa-car" type="object"/>' +
+ '</tree>',
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.record_id, 1,
+ 'should call with correct id');
+ assert.strictEqual(event.data.model, 'foo',
+ 'should call with correct model');
+ assert.strictEqual(event.data.action_data.name, 'button_action',
+ "should call correct method");
+ assert.strictEqual(event.data.action_data.type, 'object',
+ 'should have correct type');
+ },
+ },
+ });
+
+ assert.strictEqual(list.$('.o_list_button').length, 4,
+ "there should be one button per row");
+ assert.strictEqual(list.$('.o_list_button:first .o_icon_button .fa.fa-car').length, 1,
+ 'buttons should have correct icon');
+
+ list.$('.o_list_button:first > button').click(); // click on the button
+ list.destroy();
+ });
+
+ QUnit.test('invisible attrs in readonly and editable list', function (assert) {
+ assert.expect(5);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<button string="a button" name="button_action" icon="fa-car" ' +
+ 'type="object" attrs="{\'invisible\': [(\'id\',\'=\', 1)]}"/>' +
+ '<field name="int_field"/>' +
+ '<field name="qux"/>' +
+ '<field name="foo" attrs="{\'invisible\': [(\'id\',\'=\', 1)]}"/>' +
+ '</tree>',
+ });
+
+ assert.equal(list.$('tbody tr:nth(0) td:nth(4)').html(), "",
+ "td that contains an invisible field should be empty");
+ assert.equal(list.$('tbody tr:nth(0) td:nth(1)').html(), "",
+ "td that contains an invisible button should be empty");
+
+ // edit first row
+ list.$('tbody tr:nth(0) td:nth(2)').click(); // click on first row to edit it
+ assert.equal(list.$('tbody tr:nth(0) td:nth(4)').html(), "",
+ "td that contains an invisible field should be empty");
+ assert.equal(list.$('tbody tr:nth(0) td:nth(1)').html(), "",
+ "td that contains an invisible button should be empty");
+ list.$buttons.find('.o_list_button_discard').click(); // leave edition
+
+ // click on the invisible field's cell to edit first row
+ list.$('tbody tr:nth(0) td:nth(4)').click();
+ assert.ok(list.$('tbody tr:nth(0)').hasClass('o_selected_row'),
+ "first row should be in edition");
+ list.destroy();
+ });
+
+ QUnit.test('monetary fields are properly rendered', function (assert) {
+ assert.expect(3);
+
+ var currencies = {};
+ _.each(this.data.res_currency.records, function (currency) {
+ currencies[currency.id] = currency;
+ });
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="id"/>' +
+ '<field name="amount"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ session: {
+ currencies: currencies,
+ },
+ });
+
+ assert.strictEqual(list.$('tbody tr:first td').length, 3,
+ "currency_id column should not be in the table");
+ assert.strictEqual(list.$('tbody tr:first td:nth(2)').text().replace(/\s/g, ' '),
+ '1200.00 €', "currency_id column should not be in the table");
+ assert.strictEqual(list.$('tbody tr:nth(1) td:nth(2)').text().replace(/\s/g, ' '),
+ '$ 500.00', "currency_id column should not be in the table");
+
+ list.destroy();
+ });
+
+ QUnit.test('simple list with date and datetime', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="date"/><field name="datetime"/></tree>',
+ });
+
+ // FIXME: this test is locale dependant. we need to do it right.
+ assert.strictEqual(list.$('td:contains(01/25/2017)').length, 1,
+ "should have formatted the date");
+ assert.strictEqual(list.$('td:contains(12/12/2016 10:55:05)').length, 1,
+ "should have formatted the datetime");
+ list.destroy();
+ });
+
+ QUnit.test('edit a row by clicking on a readonly field', function (assert) {
+ assert.expect(8);
+
+ this.data.foo.fields.foo.readonly = true;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="int_field"/></tree>',
+ });
+
+ assert.ok(list.$('.o_data_row:first td:nth(1)').hasClass('o_readonly'),
+ "foo field cells should have class 'o_readonly'");
+
+ // edit the first row
+ list.$('.o_data_row:first td:nth(1)').click();
+ assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
+ "first row should be selected");
+ assert.ok(list.$('.o_data_row:first td:nth(1)').hasClass('o_readonly'),
+ "foo field cells should have class 'o_readonly'");
+ assert.strictEqual(list.$('.o_data_row:first td:nth(1)').text(), 'yop',
+ "no widget should be rendered for readonly fields");
+ assert.ok(list.$('.o_data_row:first td:nth(2)').hasClass('o_edit_mode'),
+ "field 'int_field' should be in edition");
+ assert.strictEqual(list.$('.o_data_row:first td:nth(2) input').length, 1,
+ "a widget for field 'int_field should have been rendered'");
+
+ // click again on readonly cell of first line: nothing should have changed
+ list.$('.o_data_row:first td:nth(1)').click();
+ assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
+ "first row should be selected");
+ assert.strictEqual(list.$('.o_data_row:first td:nth(2) input').length, 1,
+ "a widget for field 'int_field' should have been rendered (only once)");
+
+ list.destroy();
+ });
+
+ QUnit.test('list view with nested groups', function (assert) {
+ assert.expect(37);
+
+ this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1});
+ this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 2});
+
+ var nbRPCs = {readGroup: 0, searchRead: 0};
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="id"/><field name="int_field"/></tree>',
+ groupBy: ['m2o', 'foo'],
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ if (args.kwargs.groupby[0] === 'foo') { // nested read_group
+ // called twice (once when opening the group, once when sorting)
+ assert.deepEqual(args.kwargs.domain, [['m2o', '=', 1]],
+ "nested read_group should be called with correct domain");
+ }
+ nbRPCs.readGroup++;
+ } else if (route === '/web/dataset/search_read') {
+ // called twice (once when opening the group, once when sorting)
+ assert.deepEqual(args.domain, [['foo', '=', 'blip'], ['m2o', '=', 1]],
+ "nested search_read should be called with correct domain");
+ nbRPCs.searchRead++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ switch_view: function (event) {
+ assert.strictEqual(event.data.res_id, 4,
+ "'switch_view' event has been triggered");
+ },
+ },
+ });
+
+ assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ // basic rendering tests
+ assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
+ assert.strictEqual(list.$('.o_group_header').length, 2,
+ "should contain 2 groups at first level");
+ assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
+ "group should have correct name and count");
+ assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
+ "the carret of closed groups should be right");
+ assert.strictEqual(list.$('.o_group_name:first span').css('padding-left'),
+ '0px', "groups of level 1 should have a 0px padding-left");
+ assert.strictEqual(list.$('.o_group_header:first td:last').text(), '16',
+ "group aggregates are correctly displayed");
+
+ // open the first group
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('.o_group_header:first').click();
+ assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ var $openGroup = list.$('tbody:nth(1)');
+ assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
+ "group should have correct name and count (of records, not inner subgroups)");
+ assert.strictEqual(list.$('tbody').length, 3, "there should be 3 tbodys");
+ assert.strictEqual(list.$('.o_group_name:first .fa-caret-down').length, 1,
+ "the carret of open groups should be down");
+ assert.strictEqual($openGroup.find('.o_group_header').length, 3,
+ "open group should contain 3 groups");
+ assert.strictEqual($openGroup.find('.o_group_name:nth(2)').text(), 'blip (2)',
+ "group should have correct name and count");
+ assert.strictEqual($openGroup.find('.o_group_name:nth(2) span').css('padding-left'),
+ '20px', "groups of level 2 should have a 20px padding-left");
+ assert.strictEqual($openGroup.find('.o_group_header:nth(2) td:last').text(), '-11',
+ "inner group aggregates are correctly displayed");
+
+ // open subgroup
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ $openGroup.find('.o_group_header:nth(2)').click();
+ assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
+ assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
+
+ var $openSubGroup = list.$('tbody:nth(2)');
+ assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
+ assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
+ "open subgroup should contain 2 data rows");
+ assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-4',
+ "first record in open subgroup should be res_id 4 (with int_field -4)");
+
+ // open a record (should trigger event 'open_record')
+ $openSubGroup.find('.o_data_row:first').click();
+
+ // sort by int_field (ASC) and check that open groups are still open
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('thead th:last').click();
+ assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
+ assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
+
+ $openSubGroup = list.$('tbody:nth(2)');
+ assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
+ assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
+ "open subgroup should contain 2 data rows");
+ assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-7',
+ "first record in open subgroup should be res_id 5 (with int_field -7)");
+
+ // close first level group
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('.o_group_header:first').click();
+ assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
+ assert.strictEqual(list.$('.o_group_header').length, 2,
+ "should contain 2 groups at first level");
+ assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
+ "the carret of closed groups should be right");
+
+ list.destroy();
+ });
+
+ QUnit.test('grouped list with a pager in a group', function (assert) {
+ assert.expect(6);
+ this.data.foo.records[3].bar = true;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="foo"/><field name="bar"/></tree>',
+ groupBy: ['bar'],
+ viewOptions: {
+ limit: 3,
+ },
+ });
+
+ var headerHeight = list.$('.o_group_header').css('height');
+
+ // basic rendering checks
+ list.$('.o_group_header').click();
+ assert.strictEqual(list.$('.o_group_header').css('height'), headerHeight,
+ "height of group header shouldn't have changed");
+ assert.ok(list.$('.o_group_header td:last').hasClass('o_group_pager'),
+ "last cell of open group header should have classname 'o_group_header'");
+ assert.strictEqual(list.$('.o_pager_value').text(), '1-3',
+ "pager's value should be correct");
+ assert.strictEqual(list.$('.o_data_row').length, 3,
+ "open group should display 3 records");
+
+ // go to next page
+ list.$('.o_pager_next').click();
+ assert.strictEqual(list.$('.o_pager_value').text(), '4-4',
+ "pager's value should be correct");
+ assert.strictEqual(list.$('.o_data_row').length, 1,
+ "open group should display 1 record");
+
+ list.destroy();
+ });
+
+ QUnit.test('edition: create new line, then discard', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
+ });
+
+
+ assert.strictEqual(list.$('tr.o_data_row').length, 4,
+ "should have 4 records");
+ list.$buttons.find('.o_list_button_add').click();
+ list.$buttons.find('.o_list_button_discard').click();
+ assert.strictEqual(list.$('tr.o_data_row').length, 4,
+ "should still have 4 records");
+ list.destroy();
+ });
+});
+
+
+});
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
new file mode 100644
index 00000000000..5c835dfb27e
--- /dev/null
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -0,0 +1,536 @@
+odoo.define('web.pivot_tests', function (require) {
+"use strict";
+
+var core = require('web.core');
+var PivotView = require('web.PivotView');
+var testUtils = require('web.test_utils');
+
+var _t = core._t;
+var createView = testUtils.createView;
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ foo: {string: "Foo", type: "integer"},
+ bar: {string: "bar", type: "boolean"},
+ date: {string: "Date", type: "date", store: true},
+ product_id: {string: "Product", type: "many2one", relation: 'product', store: true},
+ non_stored_m2o: {string: "Non Stored M2O", type: "many2one", relation: 'product'},
+ },
+ records: [
+ {
+ id: 1,
+ foo: 12,
+ bar: true,
+ date: '2016-12-14',
+ product_id: 37,
+ }, {
+ id: 2,
+ foo: 1,
+ bar: true,
+ date: '2016-10-26',
+ product_id: 41,
+ }, {
+ id: 3,
+ foo: 17,
+ bar: true,
+ date: '2016-12-15',
+ product_id: 41,
+ }, {id: 4,
+ foo: 2,
+ bar: false,
+ date: '2016-04-11',
+ product_id: 41,
+ },
+ ]
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ };
+ }
+}, function () {
+ QUnit.module('PivotView');
+
+ QUnit.test('simple pivot rendering', function (assert) {
+ assert.expect(2);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners">' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.kwargs.lazy, false,
+ "the read_group should be done with the lazy=false option");
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(32)').length, 1,
+ "should contain a pivot cell with the sum of all records");
+ pivot.destroy();
+ });
+
+ QUnit.test('pivot view without "string" attribute', function (assert) {
+ assert.expect(1);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ });
+
+ // this is important for export functionality.
+ assert.strictEqual(pivot.title, _t("Untitled"), "should have a valid title");
+ pivot.destroy();
+ });
+
+ QUnit.test('pivot view grouped by date field', function (assert) {
+ assert.expect(2);
+
+ var data = this.data;
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="date" interval="month" type="col"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function (route, params) {
+ var wrong_fields = _.filter(params.kwargs.fields, function (field) {
+ return !(field in data.partner.fields);
+ });
+ assert.ok(!wrong_fields.length, 'fields given to read_group should exist on the model');
+ return this._super.apply(this, arguments);
+ },
+ });
+ pivot.destroy();
+ });
+
+ QUnit.test('without measures, pivot view uses __count by default', function (assert) {
+ assert.expect(2);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot></pivot>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.fields, ['__count'],
+ "should make a read_group with no valid fields");
+ }
+ return this._super(route, args);
+ }
+ });
+
+ var $countMeasure = pivot.$buttons.find('li[data-field=__count]');
+ assert.ok($countMeasure.hasClass('selected'), "The count measure should be activated");
+ pivot.destroy();
+ });
+
+ QUnit.test('pivot view can be reloaded', function (assert) {
+ assert.expect(4);
+ var readGroupCount = 0;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot></pivot>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ readGroupCount++;
+ }
+ return this._super(route, args);
+ }
+ });
+
+ assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(4)').length, 1,
+ "should contain a pivot cell with the number of all records");
+ assert.strictEqual(readGroupCount, 1, "should have done 1 rpc");
+
+ pivot.update({domain: [['foo', '>', 10]]});
+ assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(2)').length, 1,
+ "should contain a pivot cell with the number of remaining records");
+ assert.strictEqual(readGroupCount, 2, "should have done 2 rpcs");
+ pivot.destroy();
+ });
+
+ QUnit.test('pivot view grouped by many2one field', function (assert) {
+ assert.expect(3);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ });
+
+ assert.strictEqual(pivot.$('.o_pivot_header_cell_opened').length, 1,
+ "should have one opened header");
+ assert.strictEqual(pivot.$('.o_pivot_header_cell_closed:contains(xphone)').length, 1,
+ "should display one header with 'xphone'");
+ assert.strictEqual(pivot.$('.o_pivot_header_cell_closed:contains(xpad)').length, 1,
+ "should display one header with 'xpad'");
+ pivot.destroy();
+ });
+
+ QUnit.test('basic folding/unfolding', function (assert) {
+ assert.expect(7);
+
+ var rpcCount = 0;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(pivot.$('tbody tr').length, 3,
+ "should have 3 rows: 1 for the opened header, and 2 for data");
+
+ // click on the opened header to close it
+ pivot.$('.o_pivot_header_cell_opened').click();
+
+ assert.strictEqual(pivot.$('tbody tr').length, 1, "should have 1 row");
+
+ // click on closed header to open dropdown
+ pivot.$('tbody .o_pivot_header_cell_closed').click();
+
+ assert.strictEqual(pivot.$('ul.o_pivot_field_menu > li[data-field="date"]').length, 1,
+ "should have the date field as proposition");
+ assert.strictEqual(pivot.$('.o_field_selection li[data-field="product_id"]').length, 1,
+ "should have the product_id field as proposition");
+ assert.strictEqual(pivot.$('.o_field_selection li[data-field="non_stored_m2o"]').length, 0,
+ "should not have the non_stored_m2o field as proposition");
+
+ pivot.$('ul.o_pivot_field_menu > li[data-field="date"] a').click();
+
+ assert.strictEqual(pivot.$('tbody tr').length, 4,
+ "should have 4 rows: one for header, 3 for data");
+ assert.strictEqual(rpcCount, 3,
+ "should have done 3 rpcs (initial load) + open header with different groupbys");
+ pivot.destroy();
+ });
+
+ QUnit.test('more folding/unfolding', function (assert) {
+ assert.expect(1);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ });
+
+ // open dropdown to zoom into first row
+ pivot.$('tbody .o_pivot_header_cell_closed').first().click();
+ // click on date by day
+ pivot.$('ul.o_pivot_field_menu > li[data-field="date"] a[data-interval="day"]').click();
+
+ // open dropdown to zoom into second row
+ pivot.$('tbody td.o_pivot_header_cell_closed:eq(1)').first().click();
+
+ assert.strictEqual(pivot.$('tbody tr').length, 7,
+ "should have 7 rows (1 for total, 1 for xphone, 1 for xpad, 4 for data)");
+ pivot.destroy();
+ });
+
+ QUnit.test('pivot view can be flipped', function (assert) {
+ assert.expect(3);
+
+ var rpcCount = 0;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '</pivot>',
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(pivot.$('tbody tr').length, 3,
+ "should have 3 rows: 1 for the open header, and 2 for data");
+
+ rpcCount = 0;
+ pivot.$buttons.find('.o_pivot_flip_button').click();
+
+ assert.strictEqual(rpcCount, 0, "should not have done any rpc");
+ assert.strictEqual(pivot.$('tbody tr').length, 1,
+ "should have 1 rows: 1 for the main header");
+ pivot.destroy();
+ });
+
+ QUnit.test('can toggle extra measure', function (assert) {
+ assert.expect(8);
+
+ var rpcCount = 0;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 3,
+ "should have 3 cells: 1 for the open header, and 2 for data");
+ assert.ok(!pivot.$buttons.find('li[data-field=__count]').hasClass('selected'),
+ "the __count measure should not be selected");
+
+ rpcCount = 0;
+ pivot.$buttons.find('li[data-field=__count] a').click();
+
+ assert.ok(pivot.$buttons.find('li[data-field=__count]').hasClass('selected'),
+ "the __count measure should be selected");
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 6,
+ "should have 6 cells: 2 for the open header, and 4 for data");
+ assert.strictEqual(rpcCount, 2,
+ "should have done 2 rpcs to reload data");
+
+ pivot.$buttons.find('li[data-field=__count] a').click();
+
+ assert.ok(!pivot.$buttons.find('li[data-field=__count]').hasClass('selected'),
+ "the __count measure should not be selected");
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 3,
+ "should have 3 cells: 1 for the open header, and 2 for data");
+ assert.strictEqual(rpcCount, 2,
+ "should not have done any extra rpcs");
+ pivot.destroy();
+ });
+
+ QUnit.test('no content helper when no active measure', function (assert) {
+ assert.expect(4);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners">' +
+ '</pivot>',
+ });
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 0,
+ "should not have a no_content_helper");
+ assert.strictEqual(pivot.$('table').length, 1,
+ "should have a table in DOM");
+
+ pivot.$buttons.find('li[data-field=__count] a').click();
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 1,
+ "should have a no_content_helper");
+ assert.strictEqual(pivot.$('table').length, 0,
+ "should not have a table in DOM");
+ pivot.destroy();
+ });
+
+ QUnit.test('no content helper when no data', function (assert) {
+ assert.expect(4);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners">' +
+ '</pivot>',
+ });
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 0,
+ "should not have a no_content_helper");
+ assert.strictEqual(pivot.$('table').length, 1,
+ "should have a table in DOM");
+
+ pivot.update({domain: [['foo', '=', 12345]]});
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 1,
+ "should have a no_content_helper");
+ assert.strictEqual(pivot.$('table').length, 0,
+ "should not have a table in DOM");
+ pivot.destroy();
+ });
+
+ QUnit.test('no content helper when no data, part 2', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records = [];
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners"></pivot>',
+ });
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 1,
+ "should have a no_content_helper");
+ pivot.destroy();
+ });
+
+ QUnit.test('no content helper when no data, part 3', function (assert) {
+ assert.expect(4);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot string="Partners"></pivot>',
+ viewOptions: {
+ domain: [['foo', '=', 12345]]
+ },
+ });
+
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 1,
+ "should have a no_content_helper");
+ pivot.update({domain: [['foo', '=', 12345]]});
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 1,
+ "should still have a no_content_helper");
+ pivot.update({domain: []});
+ assert.strictEqual(pivot.$('.oe_view_nocontent').length, 0,
+ "should not have a no_content_helper");
+
+ // tries to open a field selection menu, to make sure it was not
+ // removed from the dom.
+ pivot.$('.o_pivot_header_cell_closed').first().click();
+ assert.strictEqual(pivot.$('ul.o_pivot_field_menu').length, 1,
+ "the field selector menu exists");
+ pivot.destroy();
+ });
+
+ QUnit.test('tries to restore previous state after domain change', function (assert) {
+ assert.expect(5);
+
+ var rpcCount = 0;
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 3,
+ "should have 3 cells: 1 for the open header, and 2 for data");
+ assert.strictEqual(pivot.$('.o_pivot_measure_row:contains(Foo)').length, 1,
+ "should have 1 row for measure Foo");
+
+ pivot.update({domain: [['foo', '=', 12345]]});
+
+ rpcCount = 0;
+ pivot.update({domain: []});
+
+ assert.equal(rpcCount, 2, "should have reloaded data");
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 3,
+ "should still have 3 cells: 1 for the open header, and 2 for data");
+ assert.strictEqual(pivot.$('.o_pivot_measure_row:contains(Foo)').length, 1,
+ "should still have 1 row for measure Foo");
+ pivot.destroy();
+ });
+
+ QUnit.test('can be grouped with the update function', function (assert) {
+ assert.expect(4);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ });
+
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 1,
+ "should have only 1 cell");
+ assert.strictEqual(pivot.$('tbody tr').length, 1,
+ "should have 1 rows");
+
+ pivot.update({groupBy: ['product_id']});
+
+ assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 3,
+ "should have 3 cells");
+ assert.strictEqual(pivot.$('tbody tr').length, 3,
+ "should have 3 rows");
+ pivot.destroy();
+ });
+
+ QUnit.test('can sort data in a column by clicking on header', function (assert) {
+ assert.expect(3);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="foo" type="measure"/>' +
+ '<field name="product_id" type="row"/>' +
+ '</pivot>',
+ });
+
+ assert.strictEqual($('td.o_pivot_cell_value').text(), "321220",
+ "should have proper values in cells (total, result 1, result 2");
+
+ pivot.$('th.o_pivot_measure_row').click();
+
+ assert.strictEqual($('td.o_pivot_cell_value').text(), "322012",
+ "should have proper values in cells (total, result 2, result 1");
+
+ pivot.$('th.o_pivot_measure_row').click();
+
+ assert.strictEqual($('td.o_pivot_cell_value').text(), "321220",
+ "should have proper values in cells (total, result 1, result 2");
+
+ pivot.destroy();
+ });
+
+});});
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
new file mode 100644
index 00000000000..2a86f044af0
--- /dev/null
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -0,0 +1,63 @@
+odoo.define('web.view_dialogs_tests', function (require) {
+"use strict";
+
+var testUtils = require('web.test_utils');
+var dialogs = require('web.view_dialogs');
+var Widget = require('web.Widget');
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ foo: {string: "Foo", type: 'char'},
+ },
+ records: [
+ {id: 1, foo: 'blip',},
+ ],
+ },
+ };
+
+ },
+}, function () {
+
+ QUnit.module('view_dialogs');
+
+ function createParent(params) {
+ var widget = new Widget();
+
+ testUtils.addMockEnvironment(widget, params);
+ return widget;
+ }
+
+ QUnit.test('formviewdialog buttons in footer are positioned properly', function (assert) {
+ assert.expect(2);
+
+ var parent = createParent({
+ data: this.data,
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner">' +
+ '<sheet>' +
+ '<group><field name="foo"/></group>' +
+ '<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
+ '</sheet>' +
+ '</form>',
+ },
+ });
+
+ var dialog = new dialogs.FormViewDialog(parent, {
+ res_model: 'partner',
+ res_id: 1,
+ }).open();
+
+ assert.notOk($('div.modal .modal-body button').length,
+ "should not have any button in body");
+ assert.strictEqual($('div.modal .modal-footer button').length, 1,
+ "should have only one button in footer");
+ dialog.destroy();
+ });
+
+});
+
+}); \ No newline at end of file
diff --git a/addons/web/static/tests/widgets/domain_selector_tests.js b/addons/web/static/tests/widgets/domain_selector_tests.js
new file mode 100644
index 00000000000..19731156a2c
--- /dev/null
+++ b/addons/web/static/tests/widgets/domain_selector_tests.js
@@ -0,0 +1,171 @@
+odoo.define('web.domain_selector_tests', function (require) {
+"use strict";
+
+var DomainSelector = require("web.DomainSelector");
+var testUtils = require("web.test_utils");
+
+QUnit.module('widgets', {}, function () {
+
+QUnit.module('DomainSelector', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ foo: {string: "Foo", type: "char", searchable: true},
+ bar: {string: "Bar", type: "boolean", searchable: true},
+ nice_datetime: {string: "Datetime", type: "datetime", searchable: true},
+ product_id: {string: "Product", type: "many2one", relation: "product", searchable: true},
+ },
+ records: [{
+ id: 1,
+ foo: "yop",
+ bar: true,
+ product_id: 37,
+ }, {
+ id: 2,
+ foo: "blip",
+ bar: true,
+ product_id: false,
+ }, {
+ id: 4,
+ foo: "abc",
+ bar: false,
+ product_id: 41,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char", searchable: true}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ };
+ },
+}, function () {
+
+ QUnit.test("creating a domain from scratch", function (assert) {
+ assert.expect(13);
+
+ var $target = $("#qunit-fixture");
+
+ // Create the domain selector and its mock environment
+ var domainSelector = new DomainSelector(null, "partner", [], {
+ readonly: false,
+ debugMode: true,
+ });
+ testUtils.addMockEnvironment(domainSelector, {data: this.data});
+ domainSelector.appendTo($target);
+
+ // As we gave an empty domain, there should be a visible button to add
+ // the first domain part
+ var $domainAddFirstNodeButton = domainSelector.$(".o_domain_add_first_node_button:visible");
+ assert.strictEqual($domainAddFirstNodeButton.length, 1,
+ "there should be a button to create first domain element");
+
+ // Clicking on the button should add a visible field selector in the
+ // widget so that the user can change the field chain
+ $domainAddFirstNodeButton.click();
+ var $fieldSelector = domainSelector.$(".o_field_selector:visible");
+ assert.strictEqual($fieldSelector.length, 1,
+ "there should be a field selector");
+
+ // Focusing the field selector input should open a field selector popover
+ $fieldSelector.trigger('focusin');
+ var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover:visible");
+ assert.strictEqual($fieldSelectorPopover.length, 1,
+ "field selector popover should be visible");
+
+ // The field selector popover should contain the list of "partner"
+ // fields. "Bar" should be among them.
+ var $lis = $fieldSelectorPopover.find("li");
+ var $barLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Bar") >= 0) {
+ $barLi = $li;
+ }
+ });
+ assert.strictEqual($barLi.length, 1,
+ "field selector popover should contain the 'Bar' field");
+
+ // Clicking the "Bar" field should change the internal domain and this
+ // should be displayed in the debug input
+ $barLi.click();
+ assert.strictEqual(
+ domainSelector.$(".o_domain_debug_input").val(),
+ '[["bar","=",True]]',
+ "the domain input should contain a domain with 'bar'"
+ );
+
+ // There should be a "+" button to add a domain part; clicking on it
+ // should add the default "['id', '=', 1]" domain
+ var $plus = domainSelector.$(".fa-plus-circle");
+ assert.strictEqual($plus.length, 1, "there should be a '+' button");
+ $plus.click();
+ assert.strictEqual(
+ domainSelector.$(".o_domain_debug_input").val(),
+ '["&",["bar","=",True],["id","=",1]]',
+ "the domain input should contain a domain with 'bar' and 'id'");
+
+ // There should be two "..." buttons to add a domain group; clicking on
+ // the first one, should add this group with defaults "['id', '=', 1]"
+ // domains and the "|" operator
+ var $dots = domainSelector.$(".fa-ellipsis-h");
+ assert.strictEqual($dots.length, 2, "there should be two '...' buttons");
+ $dots.first().click();
+ assert.strictEqual(
+ domainSelector.$(".o_domain_debug_input").val(),
+ '["&","&",["bar","=",True],"|",["id","=",1],["id","=",1],["id","=",1]]',
+ "the domain input should contain a domain with 'bar', 'id' and a subgroup"
+ );
+
+ // Changing the domain input to update the subgroup to use the "foo"
+ // field instead of "id" should rerender the widget and adapt the
+ // widget suggestions
+ domainSelector.$(".o_domain_debug_input").val('["&","&",["bar","=",True],"|",["foo","=","hello"],["id","=",1],["id","=",1]]').change();
+ assert.strictEqual(domainSelector.$(".o_field_selector").eq(1).find("input").val(), "foo",
+ "the second field selector should now contain the 'foo' value");
+ assert.ok(domainSelector.$(".o_domain_leaf_operator_select").eq(1).html().indexOf("contains") >= 0,
+ "the second operator selector should now contain the 'contains' operator");
+
+ // There should be five "-" buttons to remove domain part; clicking on
+ // the two last ones, should leave a domain with only the "bar" and
+ // "foo" fields, with the initial "&" operator
+ var $minus = domainSelector.$(".o_domain_delete_node_button");
+ assert.strictEqual($minus.length, 5, "there should be five 'x' buttons");
+ $minus.last().click();
+ domainSelector.$(".o_domain_delete_node_button").last().click();
+ assert.strictEqual(
+ domainSelector.$(".o_domain_debug_input").val(),
+ '["&",["bar","=",True],["foo","=","hello"]]',
+ "the domain input should contain a domain with 'bar' and 'foo'"
+ );
+ domainSelector.destroy();
+ });
+
+ QUnit.test("building a domain with a datetime", function (assert) {
+ assert.expect(1);
+
+ var $target = $("#qunit-fixture");
+
+ // Create the domain selector and its mock environment
+ var domainSelector = new DomainSelector(null, "partner", [["nice_datetime", "=", "2017-03-27 15:42:00"]], {
+ readonly: false,
+ });
+ testUtils.addMockEnvironment(domainSelector, {data: this.data});
+ domainSelector.appendTo($target);
+
+ // Check that there is a datepicker to choose the date
+ assert.strictEqual(domainSelector.$(".o_datepicker:visible").length, 1,
+ "there should be a datepicker");
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/widgets/model_field_selector_tests.js b/addons/web/static/tests/widgets/model_field_selector_tests.js
new file mode 100644
index 00000000000..d92e41760dc
--- /dev/null
+++ b/addons/web/static/tests/widgets/model_field_selector_tests.js
@@ -0,0 +1,137 @@
+odoo.define('web.model_field_selector_tests', function (require) {
+"use strict";
+
+var ModelFieldSelector = require("web.ModelFieldSelector");
+var testUtils = require("web.test_utils");
+
+QUnit.module('widgets', {}, function () {
+
+QUnit.module('ModelFieldSelector', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ foo: {string: "Foo", type: "char", searchable: true},
+ bar: {string: "Bar", type: "boolean", searchable: true},
+ product_id: {string: "Product", type: "many2one", relation: "product", searchable: true},
+ },
+ records: [{
+ id: 1,
+ foo: "yop",
+ bar: true,
+ product_id: 37,
+ }, {
+ id: 2,
+ foo: "blip",
+ bar: true,
+ product_id: false,
+ }, {
+ id: 4,
+ foo: "abc",
+ bar: false,
+ product_id: 41,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char", searchable: true}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ };
+ },
+}, function () {
+
+ QUnit.test("creating a field chain from scratch", function (assert) {
+ assert.expect(10);
+
+ var $target = $("#qunit-fixture");
+
+ // Create the field selector and its mock environment
+ var fieldSelector = new ModelFieldSelector(null, "partner", [], {
+ readonly: false,
+ debugMode: true,
+ });
+ testUtils.addMockEnvironment(fieldSelector, {data: this.data});
+ fieldSelector.appendTo($target);
+ var $value = fieldSelector.$("> .o_field_selector_value");
+
+ // Focusing the field selector input should open a field selector popover
+ fieldSelector.$el.trigger('focusin');
+ var $fieldSelectorPopover = fieldSelector.$(".o_field_selector_popover:visible");
+ assert.strictEqual($fieldSelectorPopover.length, 1,
+ "field selector popover should be visible");
+
+ // The field selector popover should contain the list of "partner"
+ // fields. "Bar" should be among them.
+ var $lis = $fieldSelectorPopover.find("li");
+ var $barLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Bar") >= 0) {
+ $barLi = $li;
+ }
+ });
+ assert.strictEqual($barLi.length, 1,
+ "field selector popover should contain the 'Bar' field");
+
+ // Clicking the "Bar" field should close the popover and set the field
+ // chain to "bar" as it is a basic field
+ $barLi.click();
+ assert.notOk($fieldSelectorPopover.is("visible"),
+ "field selector popover should be closed now");
+ assert.strictEqual(getValueFromDOM($value), "Bar",
+ "field selector value should be displayed with a 'Bar' tag");
+
+ // Focusing the input again should open the same popover
+ fieldSelector.$el.trigger('focusin');
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // The field selector popover should contain the list of "partner"
+ // fields. "Product" should be among them.
+ $lis = $fieldSelectorPopover.find("li");
+ var $productLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Product") >= 0) {
+ $productLi = $li;
+ }
+ });
+ assert.strictEqual($productLi.length, 1,
+ "field selector popover should contain the 'Product' field");
+
+ // Clicking on the "Product" field should update the popover to show
+ // the product fields (so only "Product Name" should be there)
+ $productLi.click();
+ $lis = $fieldSelectorPopover.find("li");
+ assert.strictEqual($lis.length, 1,
+ "there should be only one field proposition for 'product' model");
+ assert.ok($lis.first().html().indexOf("Product Name") >= 0,
+ "the name of the only suggestion should be 'Product Name'");
+
+ // Clicking on "Product Name" should close the popover and set the chain
+ // to "product_id.name"
+ $lis.first().click();
+ assert.notOk($fieldSelectorPopover.is("visible"),
+ "field selector popover should be closed now");
+ assert.strictEqual(getValueFromDOM($value), "Product -> Product Name",
+ "field selector value should be displayed with two tags: 'Product' and 'Product Name'");
+ fieldSelector.destroy();
+
+ function getValueFromDOM($dom) {
+ return _.map($dom.find(".o_field_selector_chain_part"), function (part) {
+ return $(part).text().trim();
+ }).join(" -> ");
+ }
+ });
+});
+});
+});