From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- .../static/tests/chrome/action_manager_tests.js | 4682 ++++++++ .../chrome/keyboard_navigation_mixin_tests.js | 88 + addons/web/static/tests/chrome/menu_tests.js | 47 + addons/web/static/tests/chrome/systray_tests.js | 42 + addons/web/static/tests/chrome/user_menu_tests.js | 32 + .../web/static/tests/component_extension_tests.js | 252 + .../static/tests/components/action_menus_tests.js | 251 + .../tests/components/custom_checkbox_tests.js | 56 + .../tests/components/custom_file_input_tests.js | 90 + .../static/tests/components/datepicker_tests.js | 350 + .../static/tests/components/dropdown_menu_tests.js | 442 + addons/web/static/tests/components/pager_tests.js | 194 + .../tests/control_panel/comparison_menu_tests.js | 145 + .../control_panel_model_extension_tests.js | 420 + .../tests/control_panel/control_panel_tests.js | 256 + .../control_panel/custom_filter_item_tests.js | 496 + .../control_panel/custom_group_by_item_tests.js | 74 + .../tests/control_panel/favorite_menu_tests.js | 625 + .../tests/control_panel/filter_menu_tests.js | 503 + .../tests/control_panel/groupby_menu_tests.js | 478 + .../static/tests/control_panel/search_bar_tests.js | 702 ++ .../tests/control_panel/search_utils_tests.js | 362 + addons/web/static/tests/core/ajax_tests.js | 35 + addons/web/static/tests/core/class_tests.js | 168 + addons/web/static/tests/core/concurrency_tests.js | 576 + .../tests/core/data_comparison_utils_tests.js | 75 + addons/web/static/tests/core/dialog_tests.js | 173 + addons/web/static/tests/core/dom_tests.js | 133 + addons/web/static/tests/core/domain_tests.js | 186 + addons/web/static/tests/core/math_utils_tests.js | 56 + addons/web/static/tests/core/mixins_tests.js | 36 + addons/web/static/tests/core/owl_dialog_tests.js | 332 + addons/web/static/tests/core/patch_mixin_tests.js | 994 ++ addons/web/static/tests/core/popover_tests.js | 280 + addons/web/static/tests/core/py_utils_tests.js | 1376 +++ addons/web/static/tests/core/registry_tests.js | 90 + addons/web/static/tests/core/rpc_tests.js | 316 + addons/web/static/tests/core/time_tests.js | 165 + addons/web/static/tests/core/util_tests.js | 339 + addons/web/static/tests/core/widget_tests.js | 530 + .../tests/fields/basic_fields_mobile_tests.js | 227 + .../web/static/tests/fields/basic_fields_tests.js | 7807 +++++++++++++ .../web/static/tests/fields/field_utils_tests.js | 437 + .../relational_fields/field_many2many_tests.js | 1809 +++ .../relational_fields/field_many2one_tests.js | 3565 ++++++ .../relational_fields/field_one2many_tests.js | 9959 ++++++++++++++++ .../tests/fields/relational_fields_mobile_tests.js | 66 + .../static/tests/fields/relational_fields_tests.js | 3679 ++++++ addons/web/static/tests/fields/signature_tests.js | 217 + .../static/tests/fields/special_fields_tests.js | 365 + .../static/tests/fields/upgrade_fields_tests.js | 66 + addons/web/static/tests/helpers/mock_server.js | 2060 ++++ addons/web/static/tests/helpers/qunit_asserts.js | 244 + addons/web/static/tests/helpers/qunit_config.js | 249 + addons/web/static/tests/helpers/test_env.js | 88 + addons/web/static/tests/helpers/test_utils.js | 282 + .../tests/helpers/test_utils_control_panel.js | 351 + .../web/static/tests/helpers/test_utils_create.js | 512 + addons/web/static/tests/helpers/test_utils_dom.js | 551 + .../web/static/tests/helpers/test_utils_fields.js | 250 + addons/web/static/tests/helpers/test_utils_file.js | 158 + addons/web/static/tests/helpers/test_utils_form.js | 74 + .../web/static/tests/helpers/test_utils_graph.js | 28 + .../web/static/tests/helpers/test_utils_kanban.js | 102 + addons/web/static/tests/helpers/test_utils_mock.js | 781 ++ .../web/static/tests/helpers/test_utils_modal.js | 26 + .../web/static/tests/helpers/test_utils_pivot.js | 57 + .../web/static/tests/helpers/test_utils_tests.js | 36 + addons/web/static/tests/main_tests.js | 25 + addons/web/static/tests/mockserver_tests.js | 201 + addons/web/static/tests/owl_compatibility_tests.js | 1305 +++ addons/web/static/tests/qweb_tests.js | 100 + .../web/static/tests/report/client_action_tests.js | 111 + .../static/tests/services/crash_manager_tests.js | 62 + .../static/tests/services/data_manager_tests.js | 239 + .../tests/services/notification_service_tests.js | 289 + .../web/static/tests/tools/debug_manager_tests.js | 175 + .../tests/views/abstract_controller_tests.js | 168 + .../web/static/tests/views/abstract_model_tests.js | 130 + .../tests/views/abstract_view_banner_tests.js | 108 + .../web/static/tests/views/abstract_view_tests.js | 146 + addons/web/static/tests/views/basic_model_tests.js | 2533 ++++ addons/web/static/tests/views/calendar_tests.js | 3883 ++++++ addons/web/static/tests/views/form_benchmarks.js | 108 + addons/web/static/tests/views/form_tests.js | 9907 ++++++++++++++++ addons/web/static/tests/views/graph_tests.js | 2048 ++++ addons/web/static/tests/views/kanban_benchmarks.js | 92 + .../web/static/tests/views/kanban_model_tests.js | 380 + addons/web/static/tests/views/kanban_tests.js | 7248 ++++++++++++ addons/web/static/tests/views/list_benchmarks.js | 113 + addons/web/static/tests/views/list_tests.js | 11702 +++++++++++++++++++ addons/web/static/tests/views/pivot_tests.js | 3294 ++++++ addons/web/static/tests/views/qweb_tests.js | 72 + .../web/static/tests/views/sample_server_tests.js | 486 + .../web/static/tests/views/search_panel_tests.js | 4173 +++++++ .../web/static/tests/views/view_dialogs_tests.js | 615 + .../static/tests/widgets/company_switcher_tests.js | 169 + .../web/static/tests/widgets/data_export_tests.js | 421 + .../static/tests/widgets/domain_selector_tests.js | 249 + .../tests/widgets/model_field_selector_tests.js | 325 + .../web/static/tests/widgets/rainbow_man_tests.js | 39 + 101 files changed, 101409 insertions(+) create mode 100644 addons/web/static/tests/chrome/action_manager_tests.js create mode 100644 addons/web/static/tests/chrome/keyboard_navigation_mixin_tests.js create mode 100644 addons/web/static/tests/chrome/menu_tests.js create mode 100644 addons/web/static/tests/chrome/systray_tests.js create mode 100644 addons/web/static/tests/chrome/user_menu_tests.js create mode 100644 addons/web/static/tests/component_extension_tests.js create mode 100644 addons/web/static/tests/components/action_menus_tests.js create mode 100644 addons/web/static/tests/components/custom_checkbox_tests.js create mode 100644 addons/web/static/tests/components/custom_file_input_tests.js create mode 100644 addons/web/static/tests/components/datepicker_tests.js create mode 100644 addons/web/static/tests/components/dropdown_menu_tests.js create mode 100644 addons/web/static/tests/components/pager_tests.js create mode 100644 addons/web/static/tests/control_panel/comparison_menu_tests.js create mode 100644 addons/web/static/tests/control_panel/control_panel_model_extension_tests.js create mode 100644 addons/web/static/tests/control_panel/control_panel_tests.js create mode 100644 addons/web/static/tests/control_panel/custom_filter_item_tests.js create mode 100644 addons/web/static/tests/control_panel/custom_group_by_item_tests.js create mode 100644 addons/web/static/tests/control_panel/favorite_menu_tests.js create mode 100644 addons/web/static/tests/control_panel/filter_menu_tests.js create mode 100644 addons/web/static/tests/control_panel/groupby_menu_tests.js create mode 100644 addons/web/static/tests/control_panel/search_bar_tests.js create mode 100644 addons/web/static/tests/control_panel/search_utils_tests.js create mode 100644 addons/web/static/tests/core/ajax_tests.js create mode 100644 addons/web/static/tests/core/class_tests.js create mode 100644 addons/web/static/tests/core/concurrency_tests.js create mode 100644 addons/web/static/tests/core/data_comparison_utils_tests.js create mode 100644 addons/web/static/tests/core/dialog_tests.js create mode 100644 addons/web/static/tests/core/dom_tests.js create mode 100644 addons/web/static/tests/core/domain_tests.js create mode 100644 addons/web/static/tests/core/math_utils_tests.js create mode 100644 addons/web/static/tests/core/mixins_tests.js create mode 100644 addons/web/static/tests/core/owl_dialog_tests.js create mode 100644 addons/web/static/tests/core/patch_mixin_tests.js create mode 100644 addons/web/static/tests/core/popover_tests.js create mode 100644 addons/web/static/tests/core/py_utils_tests.js create mode 100644 addons/web/static/tests/core/registry_tests.js create mode 100644 addons/web/static/tests/core/rpc_tests.js create mode 100644 addons/web/static/tests/core/time_tests.js create mode 100644 addons/web/static/tests/core/util_tests.js create mode 100644 addons/web/static/tests/core/widget_tests.js create mode 100644 addons/web/static/tests/fields/basic_fields_mobile_tests.js create mode 100644 addons/web/static/tests/fields/basic_fields_tests.js create mode 100644 addons/web/static/tests/fields/field_utils_tests.js create mode 100644 addons/web/static/tests/fields/relational_fields/field_many2many_tests.js create mode 100644 addons/web/static/tests/fields/relational_fields/field_many2one_tests.js create mode 100644 addons/web/static/tests/fields/relational_fields/field_one2many_tests.js create mode 100644 addons/web/static/tests/fields/relational_fields_mobile_tests.js create mode 100644 addons/web/static/tests/fields/relational_fields_tests.js create mode 100644 addons/web/static/tests/fields/signature_tests.js create mode 100644 addons/web/static/tests/fields/special_fields_tests.js create mode 100644 addons/web/static/tests/fields/upgrade_fields_tests.js create mode 100644 addons/web/static/tests/helpers/mock_server.js create mode 100644 addons/web/static/tests/helpers/qunit_asserts.js create mode 100644 addons/web/static/tests/helpers/qunit_config.js create mode 100644 addons/web/static/tests/helpers/test_env.js create mode 100644 addons/web/static/tests/helpers/test_utils.js create mode 100644 addons/web/static/tests/helpers/test_utils_control_panel.js create mode 100644 addons/web/static/tests/helpers/test_utils_create.js create mode 100644 addons/web/static/tests/helpers/test_utils_dom.js create mode 100644 addons/web/static/tests/helpers/test_utils_fields.js create mode 100644 addons/web/static/tests/helpers/test_utils_file.js create mode 100644 addons/web/static/tests/helpers/test_utils_form.js create mode 100644 addons/web/static/tests/helpers/test_utils_graph.js create mode 100644 addons/web/static/tests/helpers/test_utils_kanban.js create mode 100644 addons/web/static/tests/helpers/test_utils_mock.js create mode 100644 addons/web/static/tests/helpers/test_utils_modal.js create mode 100644 addons/web/static/tests/helpers/test_utils_pivot.js create mode 100644 addons/web/static/tests/helpers/test_utils_tests.js create mode 100644 addons/web/static/tests/main_tests.js create mode 100644 addons/web/static/tests/mockserver_tests.js create mode 100644 addons/web/static/tests/owl_compatibility_tests.js create mode 100644 addons/web/static/tests/qweb_tests.js create mode 100644 addons/web/static/tests/report/client_action_tests.js create mode 100644 addons/web/static/tests/services/crash_manager_tests.js create mode 100644 addons/web/static/tests/services/data_manager_tests.js create mode 100644 addons/web/static/tests/services/notification_service_tests.js create mode 100644 addons/web/static/tests/tools/debug_manager_tests.js create mode 100644 addons/web/static/tests/views/abstract_controller_tests.js create mode 100644 addons/web/static/tests/views/abstract_model_tests.js create mode 100644 addons/web/static/tests/views/abstract_view_banner_tests.js create mode 100644 addons/web/static/tests/views/abstract_view_tests.js create mode 100644 addons/web/static/tests/views/basic_model_tests.js create mode 100644 addons/web/static/tests/views/calendar_tests.js create mode 100644 addons/web/static/tests/views/form_benchmarks.js create mode 100644 addons/web/static/tests/views/form_tests.js create mode 100644 addons/web/static/tests/views/graph_tests.js create mode 100644 addons/web/static/tests/views/kanban_benchmarks.js create mode 100644 addons/web/static/tests/views/kanban_model_tests.js create mode 100644 addons/web/static/tests/views/kanban_tests.js create mode 100644 addons/web/static/tests/views/list_benchmarks.js create mode 100644 addons/web/static/tests/views/list_tests.js create mode 100644 addons/web/static/tests/views/pivot_tests.js create mode 100644 addons/web/static/tests/views/qweb_tests.js create mode 100644 addons/web/static/tests/views/sample_server_tests.js create mode 100644 addons/web/static/tests/views/search_panel_tests.js create mode 100644 addons/web/static/tests/views/view_dialogs_tests.js create mode 100644 addons/web/static/tests/widgets/company_switcher_tests.js create mode 100644 addons/web/static/tests/widgets/data_export_tests.js create mode 100644 addons/web/static/tests/widgets/domain_selector_tests.js create mode 100644 addons/web/static/tests/widgets/model_field_selector_tests.js create mode 100644 addons/web/static/tests/widgets/rainbow_man_tests.js (limited to 'addons/web/static/tests') diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js new file mode 100644 index 00000000..8511a8bf --- /dev/null +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -0,0 +1,4682 @@ +odoo.define('web.action_manager_tests', function (require) { +"use strict"; + +var ActionManager = require('web.ActionManager'); +var ReportClientAction = require('report.client_action'); +var Notification = require('web.Notification'); +var NotificationService = require('web.NotificationService'); +var AbstractAction = require('web.AbstractAction'); +var AbstractStorageService = require('web.AbstractStorageService'); +var BasicFields = require('web.basic_fields'); +var core = require('web.core'); +var ListController = require('web.ListController'); +var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin'); +var RamStorage = require('web.RamStorage'); +var ReportService = require('web.ReportService'); +var SessionStorageService = require('web.SessionStorageService'); +var testUtils = require('web.test_utils'); +var WarningDialog = require('web.CrashManager').WarningDialog; +var Widget = require('web.Widget'); + +var createActionManager = testUtils.createActionManager; +const cpHelpers = testUtils.controlPanel; +const { xml } = owl.tags; + +QUnit.module('ActionManager', { + beforeEach: function () { + this.data = { + partner: { + fields: { + foo: {string: "Foo", type: "char"}, + bar: {string: "Bar", type: "many2one", relation: 'partner'}, + o2m: {string: "One2Many", type: "one2many", relation: 'partner', relation_field: 'bar'}, + }, + records: [ + {id: 1, display_name: "First record", foo: "yop", bar: 2, o2m: [2, 3]}, + {id: 2, display_name: "Second record", foo: "blip", bar: 1, o2m: [1, 4, 5]}, + {id: 3, display_name: "Third record", foo: "gnap", bar: 1, o2m: []}, + {id: 4, display_name: "Fourth record", foo: "plop", bar: 2, o2m: []}, + {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, o2m: []}, + ], + }, + pony: { + fields: { + name: {string: 'Name', type: 'char'}, + }, + records: [ + {id: 4, name: 'Twilight Sparkle'}, + {id: 6, name: 'Applejack'}, + {id: 9, name: 'Fluttershy'} + ], + }, + }; + + this.actions = [{ + id: 1, + name: 'Partners Action 1', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[1, 'kanban']], + }, { + id: 2, + type: 'ir.actions.server', + }, { + id: 3, + name: 'Partners', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [1, 'kanban'], [false, 'form']], + }, { + id: 4, + name: 'Partners Action 4', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[1, 'kanban'], [2, 'list'], [false, 'form']], + }, { + id: 5, + name: 'Create a Partner', + res_model: 'partner', + target: 'new', + type: 'ir.actions.act_window', + views: [[false, 'form']], + }, { + id: 6, + name: 'Partner', + res_id: 2, + res_model: 'partner', + target: 'inline', + type: 'ir.actions.act_window', + views: [[false, 'form']], + }, { + id: 7, + name: "Some Report", + report_name: 'some_report', + report_type: 'qweb-pdf', + type: 'ir.actions.report', + }, { + id: 8, + name: 'Favorite Ponies', + res_model: 'pony', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }, { + id: 9, + name: 'A Client Action', + tag: 'ClientAction', + type: 'ir.actions.client', + }, { + id: 10, + type: 'ir.actions.act_window_close', + }, { + id: 11, + name: "Another Report", + report_name: 'another_report', + report_type: 'qweb-pdf', + type: 'ir.actions.report', + close_on_report_download: true, + }, { + id: 12, + name: "Some HTML Report", + report_name: 'some_report', + report_type: 'qweb-html', + type: 'ir.actions.report', + }]; + + this.archs = { + // kanban views + 'partner,1,kanban': '' + + '
' + + '
', + + // list views + 'partner,false,list': '', + 'partner,2,list': '', + 'pony,false,list': '', + + // form views + 'partner,false,form': '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
', + 'pony,false,form': '
' + + '' + + '', + + // search views + 'partner,false,search': '', + 'partner,1,search': '' + + '' + + '', + 'pony,false,search': '', + }; + }, +}, function () { + QUnit.module('Misc'); + + QUnit.test('breadcrumbs and actions with target inline', async function (assert) { + assert.expect(3); + + this.actions[3].views = [[false, 'form']]; + this.actions[3].target = 'inline'; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(4); + assert.ok(!$('.o_control_panel').is(':visible'), + "control panel should not be visible"); + + await actionManager.doAction(1, {clear_breadcrumbs: true}); + assert.ok($('.o_control_panel').is(':visible'), + "control panel should now be visible"); + assert.strictEqual($('.o_control_panel .breadcrumb').text(), "Partners Action 1", + "should have only one current action visible in breadcrumbs"); + + actionManager.destroy(); + }); + + QUnit.test('no widget memory leaks when doing some action stuff', async function (assert) { + assert.expect(1); + + var delta = 0; + testUtils.mock.patch(Widget, { + init: function () { + delta++; + this._super.apply(this, arguments); + }, + destroy: function () { + delta--; + this._super.apply(this, arguments); + }, + }); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(8); + + var n = delta; + await actionManager.doAction(4); + + // kanban view is loaded, switch to list view + await cpHelpers.switchView(actionManager, 'list'); + // open a record in form view + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + // go back to action 7 in breadcrumbs + await testUtils.dom.click($('.o_control_panel .breadcrumb a:first')); + + assert.strictEqual(delta, n, + "should have properly destroyed all other widgets"); + actionManager.destroy(); + testUtils.mock.unpatch(Widget); + }); + + QUnit.test('no widget memory leaks when executing actions in dialog', async function (assert) { + assert.expect(1); + + var delta = 0; + testUtils.mock.patch(Widget, { + init: function () { + delta++; + this._super.apply(this, arguments); + }, + destroy: function () { + if (!this.isDestroyed()) { + delta--; + } + this._super.apply(this, arguments); + }, + }); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + var n = delta; + + await actionManager.doAction(5); + await actionManager.doAction({type: 'ir.actions.act_window_close'}); + + assert.strictEqual(delta, n, + "should have properly destroyed all widgets"); + + actionManager.destroy(); + testUtils.mock.unpatch(Widget); + }); + + QUnit.test('no memory leaks when executing an action while switching view', async function (assert) { + assert.expect(1); + + var def; + var delta = 0; + testUtils.mock.patch(Widget, { + init: function () { + delta += 1; + this._super.apply(this, arguments); + }, + destroy: function () { + delta -= 1; + this._super.apply(this, arguments); + }, + }); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'read') { + return Promise.resolve(def).then(_.constant(result)); + } + return result; + }, + }); + + await actionManager.doAction(4); + var n = delta; + + await actionManager.doAction(3, {clear_breadcrumbs: true}); + + // switch to the form view (this request is blocked) + def = testUtils.makeTestPromise(); + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + + // execute another action meanwhile (don't block this request) + await actionManager.doAction(4, {clear_breadcrumbs: true}); + + // unblock the switch to the form view in action 3 + def.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(n, delta, + "all widgets of action 3 should have been destroyed"); + + actionManager.destroy(); + testUtils.mock.unpatch(Widget); + }); + + QUnit.test('no memory leaks when executing an action while loading views', async function (assert) { + assert.expect(1); + + var def; + var delta = 0; + testUtils.mock.patch(Widget, { + init: function () { + delta += 1; + this._super.apply(this, arguments); + }, + destroy: function () { + delta -= 1; + this._super.apply(this, arguments); + }, + }); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'load_views') { + return Promise.resolve(def).then(_.constant(result)); + } + return result; + }, + }); + + // execute action 4 to know the number of widgets it instantiates + await actionManager.doAction(4); + var n = delta; + + // execute a first action (its 'load_views' RPC is blocked) + def = testUtils.makeTestPromise(); + actionManager.doAction(3, {clear_breadcrumbs: true}); + + // execute another action meanwhile (and unlock the RPC) + actionManager.doAction(4, {clear_breadcrumbs: true}); + def.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(n, delta, + "all widgets of action 3 should have been destroyed"); + + actionManager.destroy(); + testUtils.mock.unpatch(Widget); + }); + + QUnit.test('no memory leaks when executing an action while loading data of default view', async function (assert) { + assert.expect(1); + + var def; + var delta = 0; + testUtils.mock.patch(Widget, { + init: function () { + delta += 1; + this._super.apply(this, arguments); + }, + destroy: function () { + delta -= 1; + this._super.apply(this, arguments); + }, + }); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route) { + var result = this._super.apply(this, arguments); + if (route === '/web/dataset/search_read') { + return Promise.resolve(def).then(_.constant(result)); + } + return result; + }, + }); + + // execute action 4 to know the number of widgets it instantiates + await actionManager.doAction(4); + var n = delta; + + // execute a first action (its 'search_read' RPC is blocked) + def = testUtils.makeTestPromise(); + actionManager.doAction(3, {clear_breadcrumbs: true}); + + // execute another action meanwhile (and unlock the RPC) + actionManager.doAction(4, {clear_breadcrumbs: true}); + def.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(n, delta, + "all widgets of action 3 should have been destroyed"); + + actionManager.destroy(); + testUtils.mock.unpatch(Widget); + }); + + QUnit.test('action with "no_breadcrumbs" set to true', async function (assert) { + assert.expect(2); + + _.findWhere(this.actions, {id: 4}).context = {no_breadcrumbs: true}; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(3); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1, + "there should be one controller in the breadcrumbs"); + + // push another action flagged with 'no_breadcrumbs=true' + await actionManager.doAction(4); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 0, + "the breadcrumbs should be empty"); + + actionManager.destroy(); + }); + + QUnit.test('on_reverse_breadcrumb handler is correctly called', async function (assert) { + assert.expect(3); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + // execute action 3 and open a record in form view + await actionManager.doAction(3); + testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + + // execute action 4 without 'on_reverse_breadcrumb' handler, then go back + await actionManager.doAction(4); + await testUtils.dom.click($('.o_control_panel .breadcrumb a:first')); + assert.verifySteps([]); + + // execute action 4 with an 'on_reverse_breadcrumb' handler, then go back + await actionManager.doAction(4, { + on_reverse_breadcrumb: function () { + assert.step('on_reverse_breadcrumb'); + } + }); + await testUtils.dom.click($('.o_control_panel .breadcrumb a:first')); + assert.verifySteps(['on_reverse_breadcrumb']); + + actionManager.destroy(); + }); + + QUnit.test('handles "history_back" event', async function (assert) { + assert.expect(2); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(4); + await actionManager.doAction(3); + actionManager.trigger_up('history_back'); + + await testUtils.nextTick(); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1, + "there should be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners Action 4', + "breadcrumbs should display the display_name of the action"); + + actionManager.destroy(); + }); + + QUnit.test('stores and restores scroll position', async function (assert) { + assert.expect(7); + + var left; + var top; + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + getScrollPosition: function (ev) { + assert.step('getScrollPosition'); + ev.data.callback({left: left, top: top}); + }, + scrollTo: function (ev) { + assert.step('scrollTo left ' + ev.data.left + ', top ' + ev.data.top); + }, + }, + }); + + // execute a first action and simulate a scroll + assert.step('execute action 3'); + await actionManager.doAction(3); + left = 50; + top = 100; + + // execute a second action (in which we don't scroll) + assert.step('execute action 4'); + await actionManager.doAction(4); + + // go back using the breadcrumbs + assert.step('go back to action 3'); + await testUtils.dom.click($('.o_control_panel .breadcrumb a')); + + assert.verifySteps([ + 'execute action 3', + 'execute action 4', + 'getScrollPosition', // of action 3, before leaving it + 'go back to action 3', + 'getScrollPosition', // of action 4, before leaving it + 'scrollTo left 50, top 100', // restore scroll position of action 3 + ]); + + actionManager.destroy(); + }); + + QUnit.test('executing an action with target != "new" closes all dialogs', async function (assert) { + assert.expect(4); + + this.archs['partner,false,form'] = '
' + + '' + + '' + + '' + + '' + + ''; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(3); + assert.containsOnce(actionManager, '.o_list_view'); + + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + assert.containsOnce(actionManager, '.o_form_view'); + + await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first')); + assert.containsOnce(document.body, '.modal .o_form_view'); + + await actionManager.doAction(1); // target != 'new' + assert.containsNone(document.body, '.modal'); + + actionManager.destroy(); + }); + + QUnit.test('executing an action with target "new" does not close dialogs', async function (assert) { + assert.expect(4); + + this.archs['partner,false,form'] = '
' + + '' + + '' + + '' + + '' + + ''; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(3); + assert.containsOnce(actionManager, '.o_list_view'); + + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + assert.containsOnce(actionManager, '.o_form_view'); + + await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first')); + assert.containsOnce(document.body, '.modal .o_form_view'); + + await actionManager.doAction(5); // target 'new' + assert.containsN(document.body, '.modal .o_form_view', 2); + + actionManager.destroy(); + }); + + QUnit.test('executing a window action with onchange warning does not hide it', async function (assert) { + assert.expect(2); + + this.archs['partner,false,form'] = ` +
+ + `; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (args.method === 'onchange') { + return Promise.resolve({ + value: {}, + warning: { + title: "Warning", + message: "Everything is alright", + type: 'dialog', + }, + }); + } + return this._super.apply(this, arguments); + }, + intercepts: { + warning: function (event) { + new WarningDialog(actionManager, { + title: event.data.title, + }, event.data).open(); + }, + }, + }); + + await actionManager.doAction(3); + + await testUtils.dom.click(actionManager.$('.o_list_button_add')); + assert.containsOnce( + $, + '.modal.o_technical_modal.show', + "Warning modal should be opened"); + + await testUtils.dom.click($('.modal.o_technical_modal.show button.close')); + assert.containsNone( + $, + '.modal.o_technical_modal.show', + "Warning modal should be closed"); + + actionManager.destroy(); + }); + + QUnit.module('Push State'); + + QUnit.test('properly push state', async function (assert) { + assert.expect(3); + + var stateDescriptions = [ + {action: 4, model: "partner", title: "Partners Action 4", view_type: "kanban"}, + {action: 8, model: "pony", title: "Favorite Ponies", view_type: "list"}, + {action: 8, id: 4, model: "pony", title: "Twilight Sparkle", view_type: "form"}, + ]; + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function (event) { + var descr = stateDescriptions.shift(); + assert.deepEqual(_.extend({}, event.data.state), descr, + "should notify the environment of new state"); + }, + }, + }); + await actionManager.doAction(4); + await actionManager.doAction(8); + await testUtils.dom.click(actionManager.$('tr.o_data_row:first')); + + actionManager.destroy(); + }); + + QUnit.test('push state after action is loaded, not before', async function (assert) { + assert.expect(5); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function () { + assert.step('push_state'); + }, + }, + mockRPC: function (route) { + assert.step(route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.doAction(4); + assert.verifySteps([ + '/web/action/load', + '/web/dataset/call_kw/partner', + '/web/dataset/search_read', + 'push_state' + ]); + + actionManager.destroy(); + }); + + QUnit.test('do not push state for actions in target=new', async function (assert) { + assert.expect(3); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function () { + assert.step('push_state'); + }, + }, + }); + await actionManager.doAction(4); + assert.verifySteps(['push_state']); + await actionManager.doAction(5); + assert.verifySteps([]); + + actionManager.destroy(); + }); + + QUnit.test('do not push state when action fails', async function (assert) { + assert.expect(4); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function () { + assert.step('push_state'); + }, + }, + mockRPC: function (route, args) { + if (args.method === 'read') { + // this is the rpc to load form view + return Promise.reject(); + } + return this._super.apply(this, arguments); + }, + }); + await actionManager.doAction(8); + assert.verifySteps(['push_state']); + await testUtils.dom.click(actionManager.$('tr.o_data_row:first')); + assert.verifySteps([]); + // we make sure here that the list view is still in the dom + assert.containsOnce(actionManager, '.o_list_view', + "there should still be a list view in dom"); + + actionManager.destroy(); + }); + + QUnit.module('Load State'); + + QUnit.test('should not crash on invalid state', async function (assert) { + assert.expect(2); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + res_model: 'partner', // the valid key for the model is 'model', not 'res_model' + }); + + assert.strictEqual(actionManager.$el.text(), '', "should display nothing"); + assert.verifySteps([]); + + actionManager.destroy(); + }); + + QUnit.test('properly load client actions', async function (assert) { + assert.expect(2); + + var ClientAction = AbstractAction.extend({ + start: function () { + this.$el.text('Hello World'); + this.$el.addClass('o_client_action_test'); + }, + }); + core.action_registry.add('HelloWorldTest', ClientAction); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + action: 'HelloWorldTest', + }); + + assert.strictEqual(actionManager.$('.o_client_action_test').text(), + 'Hello World', "should have correctly rendered the client action"); + + assert.verifySteps([]); + + actionManager.destroy(); + delete core.action_registry.map.HelloWorldTest; + }); + + QUnit.test('properly load act window actions', async function (assert) { + assert.expect(6); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + action: 1, + }); + + assert.strictEqual($('.o_control_panel').length, 1, + "should have rendered a control panel"); + assert.containsOnce(actionManager, '.o_kanban_view', + "should have rendered a kanban view"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', + ]); + + actionManager.destroy(); + }); + + QUnit.test('properly load records', async function (assert) { + assert.expect(5); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + id: 2, + model: 'partner', + }); + + assert.containsOnce(actionManager, '.o_form_view', + "should have rendered a form view"); + assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Second record', + "should have opened the second record"); + + assert.verifySteps([ + 'load_views', + 'read', + ]); + + actionManager.destroy(); + }); + + QUnit.test('properly load default record', async function (assert) { + assert.expect(5); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + action: 3, + id: "", // might happen with bbq and id=& in URL + model: 'partner', + view_type: 'form', + }); + + assert.containsOnce(actionManager, '.o_form_view', + "should have rendered a form view"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + 'onchange', + ]); + + actionManager.destroy(); + }); + + QUnit.test('load requested view for act window actions', async function (assert) { + assert.expect(6); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + action: 3, + view_type: 'kanban', + }); + + assert.containsNone(actionManager, '.o_list_view', + "should not have rendered a list view"); + assert.containsOnce(actionManager, '.o_kanban_view', + "should have rendered a kanban view"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', + ]); + + actionManager.destroy(); + }); + + QUnit.test('lazy load multi record view if mono record one is requested', async function (assert) { + assert.expect(11); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + action: 3, + id: 2, + view_type: 'form', + }); + assert.containsNone(actionManager, '.o_list_view', + "should not have rendered a list view"); + assert.containsOnce(actionManager, '.o_form_view', + "should have rendered a form view"); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2, + "there should be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Second record', + "breadcrumbs should contain the display_name of the opened record"); + + // go back to Lst + await testUtils.dom.click($('.o_control_panel .breadcrumb a')); + assert.containsOnce(actionManager, '.o_list_view', + "should now display the list view"); + assert.containsNone(actionManager, '.o_form_view', + "should not display the form view anymore"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + 'read', // read the opened record + '/web/dataset/search_read', // search read when coming back to List + ]); + + actionManager.destroy(); + }); + + QUnit.test('lazy load multi record view with previous action', async function (assert) { + assert.expect(6); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(4); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4', + "breadcrumbs should contain the display_name of the opened record"); + + await actionManager.doAction(3, { + resID: 2, + viewType: 'form', + }); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3, + "there should be three controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4PartnersSecond record', + "the breadcrumb elements should be correctly ordered"); + + // go back to List + await testUtils.dom.click($('.o_control_panel .breadcrumb a:last')); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4Partners', + "the breadcrumb elements should be correctly ordered"); + + actionManager.destroy(); + }); + + QUnit.test('lazy loaded multi record view with failing mono record one', async function (assert) { + assert.expect(4); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (args.method === 'read') { + return Promise.reject(); + } + return this._super.apply(this, arguments); + }, + }); + + await actionManager.loadState({ + action: 3, + id: 2, + view_type: 'form', + }).then(function () { + assert.ok(false, 'should not resolve the deferred'); + }).catch(function () { + assert.ok(true, 'should reject the deferred'); + }); + + assert.containsNone(actionManager, '.o_form_view'); + assert.containsNone(actionManager, '.o_list_view'); + + await actionManager.doAction(1); + + assert.containsOnce(actionManager, '.o_kanban_view'); + + actionManager.destroy(); + }); + + QUnit.test('change the viewType of the current action', async function (assert) { + assert.expect(13); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + await actionManager.doAction(3); + + assert.containsOnce(actionManager, '.o_list_view', + "should have rendered a list view"); + + // switch to kanban view + await actionManager.loadState({ + action: 3, + view_type: 'kanban', + }); + + assert.containsNone(actionManager, '.o_list_view', + "should not display the list view anymore"); + assert.containsOnce(actionManager, '.o_kanban_view', + "should have switched to the kanban view"); + + // switch to form view, open record 4 + await actionManager.loadState({ + action: 3, + id: 4, + view_type: 'form', + }); + + assert.containsNone(actionManager, '.o_kanban_view', + "should not display the kanban view anymore"); + assert.containsOnce(actionManager, '.o_form_view', + "should have switched to the form view"); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2, + "there should be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Fourth record', + "should have opened the requested record"); + + // verify steps to ensure that the whole action hasn't been re-executed + // (if it would have been, /web/action/load and load_views would appear + // several times) + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list view + '/web/dataset/search_read', // kanban view + 'read', // form view + ]); + + actionManager.destroy(); + }); + + QUnit.test('change the id of the current action', async function (assert) { + assert.expect(11); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + + // execute action 3 and open the first record in a form view + await actionManager.doAction(3); + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + + assert.containsOnce(actionManager, '.o_form_view', + "should have rendered a form view"); + assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'First record', + "should have opened the first record"); + + // switch to record 4 + await actionManager.loadState({ + action: 3, + id: 4, + view_type: 'form', + }); + + assert.containsOnce(actionManager, '.o_form_view', + "should still display the form view"); + assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2, + "there should be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Fourth record', + "should have switched to the requested record"); + + // verify steps to ensure that the whole action hasn't been re-executed + // (if it would have been, /web/action/load and load_views would appear + // twice) + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list view + 'read', // form view, record 1 + 'read', // form view, record 4 + ]); + + actionManager.destroy(); + }); + + QUnit.test('should not push a loaded state', async function (assert) { + assert.expect(3); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function () { + assert.step('push_state'); + }, + }, + }); + await actionManager.loadState({action: 3}); + + assert.verifySteps([], "should not push the loaded state"); + + await testUtils.dom.click(actionManager.$('tr.o_data_row:first')); + + assert.verifySteps(['push_state'], + "should push the state of it changes afterwards"); + + actionManager.destroy(); + }); + + QUnit.test('should not push a loaded state of a client action', async function (assert) { + assert.expect(4); + + var ClientAction = AbstractAction.extend({ + init: function (parent, action, options) { + this._super.apply(this, arguments); + this.controllerID = options.controllerID; + }, + start: function () { + var self = this; + var $button = $(' + + `; + document.body.append(bsDropdown); + + const dropdown = await createComponent(DropdownMenu, { + props: { + items: this.items, + title: "Dropdown", + }, + }); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + + assert.hasClass(dropdown.el.querySelector('.dropdown-menu'), 'show'); + assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show'); + + assert.isVisible(dropdown.el.querySelector('.dropdown-menu'), + "owl dropdown menu should be visible"); + assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'), + "bs dropdown menu should not be visible"); + + await testUtils.dom.click(bsDropdown.querySelector('.btn.dropdown-toggle')); + + assert.doesNotHaveClass(dropdown.el, 'show'); + assert.containsNone(dropdown.el, '.dropdown-menu', + "owl dropdown menu should not be set inside the dom"); + + assert.hasClass(bsDropdown.querySelector('.dropdown-menu'), 'show'); + assert.isVisible(bsDropdown.querySelector('.dropdown-menu'), + "bs dropdown menu should be visible"); + + await testUtils.dom.click(document.body); + + assert.doesNotHaveClass(dropdown.el, 'show'); + assert.containsNone(dropdown.el, '.dropdown-menu', + "owl dropdown menu should not be set inside the dom"); + + assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show'); + assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'), + "bs dropdown menu should not be visible"); + + bsDropdown.remove(); + dropdown.destroy(); + }); + + QUnit.test('click on an item without options should toggle it', async function (assert) { + assert.expect(7); + + delete this.items[0].options; + + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + intercepts: { + 'item-selected': function (ev) { + assert.strictEqual(ev.detail.item.id, 1); + this.state.items[0].isActive = !this.state.items[0].isActive; + }, + } + }); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + + const firstItemEl = dropdown.el.querySelector('.o_menu_item > a'); + assert.doesNotHaveClass(firstItemEl, 'selected'); + await testUtils.dom.click(firstItemEl); + assert.hasClass(firstItemEl, 'selected'); + assert.isVisible(firstItemEl); + await testUtils.dom.click(firstItemEl); + assert.doesNotHaveClass(firstItemEl, 'selected'); + assert.isVisible(firstItemEl); + + dropdown.destroy(); + }); + + QUnit.test('click on an item should not change url', async function (assert) { + assert.expect(1); + + delete this.items[0].options; + + const initialHref = window.location.href; + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + }); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a')); + assert.strictEqual(window.location.href, initialHref, + "the url should not have changed after a click on an item"); + + dropdown.destroy(); + }); + + QUnit.test('options rendering', async function (assert) { + assert.expect(6); + + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + }); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3); + + const firstItemEl = dropdown.el.querySelector('.o_menu_item > a'); + assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right'); + // open options menu + await testUtils.dom.click(firstItemEl); + assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-down'); + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6); + + // close options menu + await testUtils.dom.click(firstItemEl); + assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right'); + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3); + + dropdown.destroy(); + }); + + QUnit.test('close menu closes also submenus', async function (assert) { + assert.expect(2); + + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + }); + + // open dropdown menu + await testUtils.dom.click(dropdown.el.querySelector('button')); + // open options menu of first item + await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item a')); + + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6); + await testUtils.dom.click(dropdown.el.querySelector('button')); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3); + + dropdown.destroy(); + }); + + QUnit.test('click on an option should trigger the event "item_option_clicked" with appropriate data', async function (assert) { + assert.expect(18); + + let eventNumber = 0; + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + intercepts: { + 'item-selected': function (ev) { + eventNumber++; + const { option } = ev.detail; + assert.strictEqual(ev.detail.item.id, 1); + if (eventNumber === 1) { + assert.strictEqual(option.id, 1); + this.state.items[0].isActive = true; + this.state.items[0].options[0].isActive = true; + } + if (eventNumber === 2) { + assert.strictEqual(option.id, 2); + this.state.items[0].options[1].isActive = true; + } + if (eventNumber === 3) { + assert.strictEqual(option.id, 1); + this.state.items[0].options[0].isActive = false; + } + if (eventNumber === 4) { + assert.strictEqual(option.id, 2); + this.state.items[0].isActive = false; + this.state.items[0].options[1].isActive = false; + } + }, + } + }); + + // open dropdown menu + await testUtils.dom.click(dropdown.el.querySelector('button')); + assert.containsN(dropdown, '.dropdown-divider, .o_menu_item', 3); + + // open menu options of first item + await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a')); + let optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a'); + + // click on first option + await testUtils.dom.click(optionELs[0]); + assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected'); + optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a'); + assert.hasClass(optionELs[0], 'selected'); + assert.doesNotHaveClass(optionELs[1], 'selected'); + + // click on second option + await testUtils.dom.click(optionELs[1]); + assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected'); + optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a'); + assert.hasClass(optionELs[0], 'selected'); + assert.hasClass(optionELs[1], 'selected'); + + // click again on first option + await testUtils.dom.click(optionELs[0]); + // click again on second option + await testUtils.dom.click(optionELs[1]); + assert.doesNotHaveClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected'); + optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a'); + assert.doesNotHaveClass(optionELs[0], 'selected'); + assert.doesNotHaveClass(optionELs[1], 'selected'); + + dropdown.destroy(); + }); + + QUnit.test('keyboard navigation', async function (assert) { + assert.expect(12); + + // Shorthand method to trigger a specific keydown. + // Note that BootStrap handles some of the navigation moves (up and down) + // so we need to give the event the proper "which" property. We also give + // it when it's not required to check if it has been correctly prevented. + async function navigate(key, global) { + const which = { + Enter: 13, + Escape: 27, + ArrowLeft: 37, + ArrowUp: 38, + ArrowRight: 39, + ArrowDown: 40, + }[key]; + const target = global ? document.body : document.activeElement; + await testUtils.dom.triggerEvent(target, 'keydown', { key, which }); + if (key === 'Enter') { + // Pressing "Enter" on a focused element triggers a click (HTML5 specs) + await testUtils.dom.click(target); + } + } + + const dropdown = await createComponent(DropdownMenu, { + props: { items: this.items }, + }); + + // Initialize active element (start at toggle button) + dropdown.el.querySelector('button').focus(); + await testUtils.dom.click(dropdown.el.querySelector('button')); + + await navigate('ArrowDown'); // Go to next item + + assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a')); + assert.containsNone(dropdown, '.o_item_option'); + + await navigate('ArrowRight'); // Unfold first item's options (w/ Right) + + assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a')); + assert.containsN(dropdown, '.o_item_option', 2); + + await navigate('ArrowDown'); // Go to next option + + assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_item_option a')); + + await navigate('ArrowLeft'); // Fold first item's options (w/ Left) + + assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a')); + assert.containsNone(dropdown, '.o_item_option'); + + await navigate('Enter'); // Unfold first item's options (w/ Enter) + + assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a')); + assert.containsN(dropdown, '.o_item_option', 2); + + await navigate('ArrowDown'); // Go to next option + await navigate('Escape'); // Fold first item's options (w/ Escape) + await testUtils.nextTick(); + + assert.strictEqual(dropdown.el.querySelector('.o_menu_item a'), document.activeElement); + assert.containsNone(dropdown, '.o_item_option'); + + await navigate('Escape', true); // Close the dropdown + + assert.containsNone(dropdown, 'ul.o_dropdown_menu', "Dropdown should be folded"); + + dropdown.destroy(); + }); + + QUnit.test('interactions between multiple dropdowns', async function (assert) { + assert.expect(7); + + const props = { items: this.items }; + class Parent extends owl.Component { + constructor() { + super(...arguments); + this.state = owl.useState(props); + } + } + Parent.components = { DropdownMenu }; + Parent.template = owl.tags.xml` +
+ + +
`; + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget(), { position: 'first-child' }); + + const [menu1, menu2] = parent.el.querySelectorAll('.o_dropdown'); + + assert.containsNone(parent, '.o_dropdown_menu'); + + await testUtils.dom.click(menu1.querySelector('button')); + + assert.containsOnce(parent, '.o_dropdown_menu'); + assert.containsOnce(parent, '.o_dropdown.first .o_dropdown_menu'); + + await testUtils.dom.click(menu2.querySelector('button')); + + assert.containsOnce(parent, '.o_dropdown_menu'); + assert.containsOnce(parent, '.o_dropdown.second .o_dropdown_menu'); + + await testUtils.dom.click(menu2.querySelector('.o_menu_item a')); + await testUtils.dom.click(menu1.querySelector('button')); + + assert.containsOnce(parent, '.o_dropdown_menu'); + assert.containsOnce(parent, '.o_dropdown.first .o_dropdown_menu'); + + parent.destroy(); + }); + + QUnit.test("dropdown doesn't get close on mousedown inside and mouseup outside dropdown", async function (assert) { + // In this test, we simulate a case where the user clicks inside a dropdown menu item + // (e.g. in the input of the 'Save current search' item in the Favorites menu), keeps + // the click pressed, moves the cursor outside the dropdown and releases the click + // (i.e. mousedown and focus inside the item, mouseup and click outside the dropdown). + // In this case, we want to keep the dropdown menu open. + assert.expect(5); + + const items = this.items; + class Parent extends owl.Component { + constructor() { + super(...arguments); + this.items = items; + } + } + Parent.components = { DropdownMenu }; + Parent.template = owl.tags.xml` +
+ +
`; + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget(), { position: "first-child" }); + + const menu = parent.el.querySelector(".o_dropdown"); + assert.doesNotHaveClass(menu, "show", "dropdown should not be open"); + + await testUtils.dom.click(menu.querySelector("button")); + assert.hasClass(menu, "show", "dropdown should be open"); + + const firstItemEl = menu.querySelector(".o_menu_item > a"); + // open options menu + await testUtils.dom.click(firstItemEl); + assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down"); + + // force the focus inside the dropdown item and click outside + firstItemEl.parentElement.querySelector(".o_menu_item_options .o_item_option a").focus(); + await testUtils.dom.triggerEvents(parent.el, "click"); + assert.hasClass(menu, "show", "dropdown should still be open"); + assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down"); + + parent.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/components/pager_tests.js b/addons/web/static/tests/components/pager_tests.js new file mode 100644 index 00000000..61b7feed --- /dev/null +++ b/addons/web/static/tests/components/pager_tests.js @@ -0,0 +1,194 @@ +odoo.define('web.pager_tests', function (require) { + "use strict"; + + const Pager = require('web.Pager'); + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createComponent } = testUtils; + + QUnit.module('Components', {}, function () { + + QUnit.module('Pager'); + + QUnit.test('basic interactions', async function (assert) { + assert.expect(2); + + const pager = await createComponent(Pager, { + props: { + currentMinimum: 1, + limit: 4, + size: 10, + }, + intercepts: { + 'pager-changed': function (ev) { + Object.assign(this.state, ev.detail); + }, + }, + }); + + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4", + "currentMinimum should be set to 1"); + + await cpHelpers.pagerNext(pager); + + assert.strictEqual(cpHelpers.getPagerValue(pager), "5-8", + "currentMinimum should now be 5"); + + pager.destroy(); + }); + + QUnit.test('edit the pager', async function (assert) { + assert.expect(4); + + const pager = await createComponent(Pager, { + props: { + currentMinimum: 1, + limit: 4, + size: 10, + }, + intercepts: { + 'pager-changed': function (ev) { + Object.assign(this.state, ev.detail); + }, + }, + }); + + await testUtils.dom.click(pager.el.querySelector('.o_pager_value')); + + assert.containsOnce(pager, 'input', + "the pager should contain an input"); + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4", + "the input should have correct value"); + + // change the limit + await cpHelpers.setPagerValue(pager, "1-6"); + + assert.containsNone(pager, 'input', + "the pager should not contain an input anymore"); + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-6", + "the limit should have been updated"); + + pager.destroy(); + }); + + QUnit.test("keydown on pager with same value", async function (assert) { + assert.expect(7); + + const pager = await createComponent(Pager, { + props: { + currentMinimum: 1, + limit: 4, + size: 10, + }, + intercepts: { + "pager-changed": () => assert.step("pager-changed"), + }, + }); + + // Enter edit mode + await testUtils.dom.click(pager.el.querySelector('.o_pager_value')); + + assert.containsOnce(pager, "input"); + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4"); + assert.verifySteps([]); + + // Exit edit mode + await testUtils.dom.triggerEvent(pager.el.querySelector('input'), "keydown", { key: "Enter" }); + + assert.containsNone(pager, "input"); + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4"); + assert.verifySteps(["pager-changed"]); + + pager.destroy(); + }); + + QUnit.test('pager value formatting', async function (assert) { + assert.expect(8); + + const pager = await createComponent(Pager, { + props: { + currentMinimum: 1, + limit: 4, + size: 10, + }, + intercepts: { + 'pager-changed': function (ev) { + Object.assign(this.state, ev.detail); + }, + }, + }); + + assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4", "Initial value should be correct"); + + async function inputAndAssert(input, expected, reason) { + await cpHelpers.setPagerValue(pager, input); + assert.strictEqual(cpHelpers.getPagerValue(pager), expected, + `Pager value should be "${expected}" when given "${input}": ${reason}`); + } + + await inputAndAssert("4-4", "4", "values are squashed when minimum = maximum"); + await inputAndAssert("1-11", "1-10", "maximum is floored to size when out of range"); + await inputAndAssert("20-15", "10", "combination of the 2 assertions above"); + await inputAndAssert("6-5", "10", "fallback to previous value when minimum > maximum"); + await inputAndAssert("definitelyValidNumber", "10", "fallback to previous value if not a number"); + await inputAndAssert(" 1 , 2 ", "1-2", "value is normalized and accepts several separators"); + await inputAndAssert("3 8", "3-8", "value accepts whitespace(s) as a separator"); + + pager.destroy(); + }); + + QUnit.test('pager disabling', async function (assert) { + assert.expect(9); + + const reloadPromise = testUtils.makeTestPromise(); + const pager = await createComponent(Pager, { + props: { + currentMinimum: 1, + limit: 4, + size: 10, + }, + intercepts: { + // The goal here is to test the reactivity of the pager; in a + // typical views, we disable the pager after switching page + // to avoid switching twice with the same action (double click). + 'pager-changed': async function (ev) { + // 1. Simulate a (long) server action + await reloadPromise; + // 2. Update the view with loaded data + Object.assign(this.state, ev.detail); + }, + }, + }); + const pagerButtons = pager.el.querySelectorAll('button'); + + // Click twice + await cpHelpers.pagerNext(pager); + await cpHelpers.pagerNext(pager); + // Try to edit the pager value + await testUtils.dom.click(pager.el.querySelector('.o_pager_value')); + + assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed"); + assert.ok(pagerButtons[0].disabled, "'previous' is disabled"); + assert.ok(pagerButtons[1].disabled, "'next' is disabled"); + assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'SPAN', + "pager edition is prevented"); + + // Server action is done + reloadPromise.resolve(); + await testUtils.nextTick(); + + assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed"); + assert.notOk(pagerButtons[0].disabled, "'previous' is enabled"); + assert.notOk(pagerButtons[1].disabled, "'next' is enabled"); + assert.strictEqual(cpHelpers.getPagerValue(pager), "5-8", "value has been updated"); + + await testUtils.dom.click(pager.el.querySelector('.o_pager_value')); + + assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'INPUT', + "pager edition is re-enabled"); + + pager.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/comparison_menu_tests.js b/addons/web/static/tests/control_panel/comparison_menu_tests.js new file mode 100644 index 00000000..e376904f --- /dev/null +++ b/addons/web/static/tests/control_panel/comparison_menu_tests.js @@ -0,0 +1,145 @@ +odoo.define('web.comparison_menu_tests', function (require) { + "use strict"; + + const { + controlPanel: cpHelpers, + createControlPanel, + mock, + } = require('web.test_utils'); + + const { patchDate } = mock; + const searchMenuTypes = ['filter', 'comparison']; + + QUnit.module('Components', { + beforeEach() { + this.fields = { + birthday: { string: "Birthday", type: "date", store: true, sortable: true }, + date_field: { string: "Date", type: "date", store: true, sortable: true }, + float_field: { string: "Float", type: "float", group_operator: 'sum' }, + foo: { string: "Foo", type: "char", store: true, sortable: true }, + }; + this.cpModelConfig = { + arch: ` + + + + `, + fields: this.fields, + searchMenuTypes, + }; + }, + }, function () { + + QUnit.module('ComparisonMenu'); + + QUnit.test('simple rendering', async function (assert) { + assert.expect(6); + + const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0); + const params = { + cpModelConfig: this.cpModelConfig, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsOnce(controlPanel, ".o_dropdown.o_filter_menu"); + assert.containsNone(controlPanel, ".o_dropdown.o_comparison_menu"); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Birthday"); + await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January"); + + assert.containsOnce(controlPanel, 'div.o_comparison_menu > button i.fa.fa-adjust'); + assert.strictEqual(controlPanel.el.querySelector('div.o_comparison_menu > button span').innerText.trim(), "Comparison"); + + await cpHelpers.toggleComparisonMenu(controlPanel); + + const comparisonOptions = [...controlPanel.el.querySelectorAll( + '.o_comparison_menu li' + )]; + assert.strictEqual(comparisonOptions.length, 2); + assert.deepEqual( + comparisonOptions.map(e => e.innerText), + ["Birthday: Previous Period", "Birthday: Previous Year"] + ); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('activate a comparison works', async function (assert) { + assert.expect(5); + + const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0); + const params = { + cpModelConfig: this.cpModelConfig, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Birthday"); + await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January"); + await cpHelpers.toggleComparisonMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Period"); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [ + "Birthday: January 1997", + "Birthday: Previous Period", + ]); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date"); + await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "December"); + await cpHelpers.toggleComparisonMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date: Previous Year"); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [ + ["Birthday: January 1997", "Date: December 1996"].join("or"), + "Date: Previous Year", + ]); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date"); + await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "1996"); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [ + "Birthday: January 1997", + ]); + + await cpHelpers.toggleComparisonMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Year"); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [ + "Birthday: January 1997", + "Birthday: Previous Year", + ]); + + await cpHelpers.removeFacet(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('no timeRanges key in search query if "comparison" not in searchMenuTypes', async function (assert) { + assert.expect(1); + + this.cpModelConfig.searchMenuTypes = ['filter']; + const params = { + cpModelConfig: this.cpModelConfig, + cpProps: { fields: this.fields, searchMenuTypes: ['filter'] }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Birthday"); + await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", 0); + + assert.notOk("timeRanges" in controlPanel.getQuery()); + + controlPanel.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js new file mode 100644 index 00000000..01ab0be4 --- /dev/null +++ b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js @@ -0,0 +1,420 @@ +odoo.define("web/static/tests/control_panel/control_panel_model_extension_tests.js", function (require) { + "use strict"; + + const ActionModel = require("web/static/src/js/views/action_model.js"); + const makeTestEnvironment = require('web.test_env'); + + function createModel(params = {}) { + const archs = (params.arch && { search: params.arch, }) || {}; + const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs); + const extensions = { + ControlPanel: { + context: params.context, + archNodes: controlPanelInfo.children, + dynamicFilters: params.dynamicFilters, + favoriteFilters: params.favoriteFilters, + env: makeTestEnvironment(), + fields: params.fields, + }, + }; + const model = new ActionModel(extensions); + return model; + } + function sanitizeFilters(model) { + const cpme = model.extensions[0].find( + (ext) => ext.constructor.name === "ControlPanelModelExtension" + ); + const filters = Object.values(cpme.state.filters); + return filters.map(filter => { + const copy = Object.assign({}, filter); + delete copy.groupId; + delete copy.groupNumber; + delete copy.id; + return copy; + }); + } + + QUnit.module('ControlPanelModelExtension', { + beforeEach() { + this.fields = { + display_name: { string: "Displayed name", type: 'char' }, + foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true }, + date_field: { string: "Date", type: "date", store: true, sortable: true }, + float_field: { string: "Float", type: "float" }, + bar: { string: "Bar", type: "many2one", relation: 'partner' }, + }; + } + }, function () { + QUnit.module('Arch parsing'); + + QUnit.test('empty arch', async function (assert) { + assert.expect(1); + const model = createModel(); + assert.deepEqual(sanitizeFilters(model), []); + }); + + QUnit.test('one field tag', async function (assert) { + assert.expect(1); + const arch = ` + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + ]); + }); + + QUnit.test('one separator tag', async function (assert) { + assert.expect(1); + const arch = ` + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), []); + }); + + QUnit.test('one separator tag and one field tag', async function (assert) { + assert.expect(1); + const arch = ` + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + ]); + }); + + QUnit.test('one filter tag', async function (assert) { + assert.expect(1); + const arch = ` + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello", + domain: "[]", + type: "filter", + }, + ]); + }); + + QUnit.test('one filter tag with date attribute', async function (assert) { + assert.expect(1); + const arch = ` + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + const dateFilterId = Object.values(model.get('filters'))[0].id; + assert.deepEqual(sanitizeFilters(model), [ + { + defaultOptionId: "this_month", + description: "Date", + fieldName: "date_field", + fieldType: "date", + isDateFilter: true, + hasOptions: true, + type: "filter" + }, + { + comparisonOptionId: "previous_period", + dateFilterId, + description: "Date: Previous Period", + type: "comparison" + }, + { + comparisonOptionId: "previous_year", + dateFilterId, + description: "Date: Previous Year", + type: "comparison" + } + ]); + }); + + QUnit.test('one groupBy tag', async function (assert) { + assert.expect(1); + const arch = ` + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + defaultOptionId: "day", + description: "Hi", + fieldName: "date_field", + fieldType: "date", + hasOptions: true, + type: "groupBy", + }, + ]); + }); + + QUnit.test('two filter tags', async function (assert) { + assert.expect(1); + const arch = ` + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello One", + domain: "[]", + type: "filter", + }, + { + description: "Hello Two", + domain: "[('bar', '=', 3)]", + type: "filter", + }, + ]); + }); + + QUnit.test('two filter tags separated by a separator', async function (assert) { + assert.expect(1); + const arch = ` + + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello One", + domain: "[]", + type: "filter", + }, + { + description: "Hello Two", + domain: "[('bar', '=', 3)]", + type: "filter", + }, + ]); + }); + + QUnit.test('one filter tag and one field', async function (assert) { + assert.expect(1); + const arch = ` + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello", + domain: "[]", + type: "filter", + }, + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field", + }, + ]); + }); + + QUnit.test('two field tags', async function (assert) { + assert.expect(1); + const arch = ` + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Foo", + fieldName: "foo", + fieldType: "char", + type: "field" + }, + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + ]); + }); + + QUnit.module('Preparing initial state'); + + QUnit.test('process favorite filters', async function (assert) { + assert.expect(1); + const favoriteFilters = [{ + user_id: [2, "Mitchell Admin"], + name: 'Sorted filter', + id: 5, + context: { + group_by: ['foo', 'bar'] + }, + sort: '["foo", "-bar"]', + domain: "[('user_id', '=', uid)]", + }]; + + const model = createModel({ favoriteFilters }); + assert.deepEqual(sanitizeFilters(model), [ + { + context: {}, + description: "Sorted filter", + domain: "[('user_id', '=', uid)]", + groupBys: ['foo', 'bar'], + orderedBy: [ + { + asc: true, + name: "foo" + }, + { + asc: false, + name: "bar" + } + ], + removable: true, + serverSideId: 5, + type: "favorite", + userId: 2 + }, + ]); + + }); + + QUnit.test('process dynamic filters', async function (assert) { + assert.expect(1); + const dynamicFilters = [{ + description: 'Quick search', + domain: [['id', 'in', [1, 3, 4]]] + }]; + + const model = createModel({ dynamicFilters }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: 'Quick search', + domain: "[[\"id\",\"in\",[1,3,4]]]", + isDefault: true, + type: 'filter' + }, + ]); + + }); + + QUnit.test('falsy search defaults are not activated', async function (assert) { + assert.expect(1); + + const context = { + search_default_filter: false, + search_default_bar: 0, + search_default_groupby: 2, + }; + const arch = ` + + + + + `; + const fields = this.fields; + const model = createModel({ arch, fields, context }); + // only the truthy filter 'groupby' has isDefault true + assert.deepEqual(sanitizeFilters(model), [ + { + description: 'Hello', + domain: "[]", + type: 'filter', + }, + { + description: 'Bar', + fieldName: 'bar', + fieldType: 'many2one', + type: 'field', + }, + { + defaultRank: 2, + description: 'Goodbye', + fieldName: 'foo', + fieldType: 'char', + isDefault: true, + type: 'groupBy', + }, + ]); + + }); + + QUnit.test('search defaults on X2M fields', async function (assert) { + assert.expect(1); + + const context = { + search_default_otom: [1, 2], + search_default_mtom: [1, 2] + }; + const fields = this.fields; + fields.otom = { string: "O2M", type: "one2many", relation: 'partner' }; + fields.mtom = { string: "M2M", type: "many2many", relation: 'partner' }; + const arch = ` + + + + `; + const model = createModel({ arch, fields, context }); + assert.deepEqual(sanitizeFilters(model), [ + { + "defaultAutocompleteValue": { + "label": [1, 2], + "operator": "ilike", + "value": [1, 2] + }, + "defaultRank": -10, + "description": "O2M", + "fieldName": "otom", + "fieldType": "one2many", + "isDefault": true, + "type": "field" + }, + { + "defaultAutocompleteValue": { + "label": [1, 2], + "operator": "ilike", + "value": [1, 2] + }, + "defaultRank": -10, + "description": "M2M", + "fieldName": "mtom", + "fieldType": "many2many", + "isDefault": true, + "type": "field" + } + ]); + + }); + + }); +}); diff --git a/addons/web/static/tests/control_panel/control_panel_tests.js b/addons/web/static/tests/control_panel/control_panel_tests.js new file mode 100644 index 00000000..c9e89387 --- /dev/null +++ b/addons/web/static/tests/control_panel/control_panel_tests.js @@ -0,0 +1,256 @@ +odoo.define('web.control_panel_tests', function (require) { + "use strict"; + + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createControlPanel } = testUtils; + + QUnit.module('ControlPanel', { + beforeEach() { + this.fields = { + display_name: { string: "Displayed name", type: 'char', searchable: true }, + foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true, searchable: true }, + date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true }, + float_field: { string: "Float", type: "float", searchable: true }, + bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true }, + }; + } + }, function () { + + QUnit.test('default field operator', async function (assert) { + assert.expect(2); + + const fields = { + foo_op: { string: "Foo Op", type: "char", store: true, sortable: true, searchable: true }, + foo: { string: "Foo", type: "char", store: true, sortable: true, searchable: true }, + bar_op: { string: "Bar Op", type: "many2one", relation: 'partner', searchable: true }, + bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true }, + selec: { string: "Selec", type: "selection", selection: [['red', "Red"], ['black', "Black"]] }, + }; + const arch = ` + + + + + + + `; + const searchMenuTypes = []; + const params = { + cpModelConfig: { + arch, + fields, + context: { + show_filterC: true, + search_default_bar: 10, + search_default_bar_op: 10, + search_default_foo: "foo", + search_default_foo_op: "foo_op", + search_default_selec: 'red', + }, + searchMenuTypes, + }, + cpProps: { fields, searchMenuTypes }, + env: { + session: { + async rpc() { + return [[10, "Deco Addict"]]; + }, + }, + }, + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual( + cpHelpers.getFacetTexts(controlPanel).map(t => t.replace(/\s/g, "")), + [ + "BarDecoAddict", + "BarOpDecoAddict", + "Foofoo", + "FooOpfoo_op", + "SelecRed" + ] + ); + assert.deepEqual( + controlPanel.getQuery().domain, + [ + "&", "&", "&", "&", + ["bar", "=", 10], + ["bar_op", "child_of", 10], + ["foo", "ilike", "foo"], + ["foo_op", "=", "foo_op"], + ["selec", "=", "red"], + ] + ); + + controlPanel.destroy(); + }); + + QUnit.module('Keyboard navigation'); + + QUnit.test('remove a facet with backspace', async function (assert) { + assert.expect(2); + + const params = { + cpModelConfig: { + arch: ` `, + fields: this.fields, + context: { search_default_foo: "a" }, + searchMenuTypes: ['filter'], + }, + cpProps: { fields: this.fields }, + }; + + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo\na']); + + // delete a facet + const searchInput = controlPanel.el.querySelector('input.o_searchview_input'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' }); + + assert.containsNone(controlPanel, 'div.o_searchview div.o_searchview_facet'); + + // delete nothing (should not crash) + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' }); + + controlPanel.destroy(); + }); + + QUnit.test('fields and filters with groups/invisible attribute', async function (assert) { + // navigation and automatic menu closure don't work here (i don't know why yet) --> + // should be tested separatly + assert.expect(16); + + const arch = ` + + + + + + + + + `; + const searchMenuTypes = ['filter', 'groupBy']; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + context: { + show_filterC: true, + search_default_display_name: 'value', + search_default_filterB: true, + search_default_groupByB: true + }, + searchMenuTypes + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + function selectorContainsValue(selector, value, shouldContain) { + const elements = [...controlPanel.el.querySelectorAll(selector)]; + const regExp = new RegExp(value); + const matches = elements.filter(el => regExp.test(el.innerText.replace(/\s/g, ""))); + assert.strictEqual(matches.length, shouldContain ? 1 : 0, + `${selector} in the control panel should${shouldContain ? '' : ' not'} contain "${value}".` + ); + } + + // default filters/fields should be activated even if invisible + assert.containsN(controlPanel, 'div.o_searchview_facet', 3); + selectorContainsValue('.o_searchview_facet', "FooBvalue", true); + selectorContainsValue('.o_searchview_facet .o_facet_values', "FB", true); + selectorContainsValue('.o_searchview_facet .o_facet_values', "GB", true); + + await cpHelpers.toggleFilterMenu(controlPanel); + + selectorContainsValue('.o_menu_item a', "FA", true); + selectorContainsValue('.o_menu_item a', "FB", false); + selectorContainsValue('.o_menu_item a', "FC", true); + + await cpHelpers.toggleGroupByMenu(controlPanel); + + selectorContainsValue('.o_menu_item a', "GA", true); + selectorContainsValue('.o_menu_item a', "GB", false); + + // 'a' to filter nothing on bar + await cpHelpers.editSearch(controlPanel, 'a'); + + // the only item in autocomplete menu should be FooA: a + selectorContainsValue('.o_searchview_autocomplete', "SearchFooAfor:a", true); + await cpHelpers.validateSearch(controlPanel); + selectorContainsValue('.o_searchview_facet', "FooAa", true); + + // The items in the Filters menu and the Group By menu should be the same as before + await cpHelpers.toggleFilterMenu(controlPanel); + + selectorContainsValue('.o_menu_item a', "FA", true); + selectorContainsValue('.o_menu_item a', "FB", false); + selectorContainsValue('.o_menu_item a', "FC", true); + + await cpHelpers.toggleGroupByMenu(controlPanel); + + selectorContainsValue('.o_menu_item a', "GA", true); + selectorContainsValue('.o_menu_item a', "GB", false); + + controlPanel.destroy(); + }); + + QUnit.test('invisible fields and filters with unknown related fields should not be rendered', async function (assert) { + assert.expect(2); + + // This test case considers that the current user is not a member of + // the "base.group_system" group and both "bar" and "date_field" fields + // have field-level access control that limit access to them only from + // that group. + // + // As MockServer currently does not support "groups" access control, we: + // + // - emulate field-level access control of fields_get() by removing + // "bar" and "date_field" from the model fields + // - set filters with groups="base.group_system" as `invisible=1` in + // view to emulate the behavior of fields_view_get() + // [see ir.ui.view `_apply_group()`] + + delete this.fields.bar; + delete this.fields.date_field; + + const searchMenuTypes = []; + const params = { + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsNone(controlPanel.el, 'div.o_search_options div.o_filter_menu', + "there should not be filter dropdown"); + assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu', + "there should not be groupby dropdown"); + + controlPanel.destroy(); + }); + + QUnit.test('groupby menu is not rendered if searchMenuTypes does not have groupBy', async function (assert) { + assert.expect(2); + + const arch = ``; + const searchMenuTypes = ['filter']; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsOnce(controlPanel.el, 'div.o_search_options div.o_filter_menu'); + assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu'); + + controlPanel.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/custom_filter_item_tests.js b/addons/web/static/tests/control_panel/custom_filter_item_tests.js new file mode 100644 index 00000000..dcd30428 --- /dev/null +++ b/addons/web/static/tests/control_panel/custom_filter_item_tests.js @@ -0,0 +1,496 @@ +odoo.define('web.filter_menu_generator_tests', function (require) { + "use strict"; + + const Domain = require('web.Domain'); + const CustomFilterItem = require('web.CustomFilterItem'); + const ActionModel = require('web/static/src/js/views/action_model.js'); + const pyUtils = require('web.py_utils'); + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createComponent } = testUtils; + + QUnit.module('Components', { + beforeEach: function () { + this.fields = { + date_field: { name: 'date_field', string: "A date", type: 'date', searchable: true }, + date_time_field: { name: 'date_time_field', string: "DateTime", type: 'datetime', searchable: true }, + boolean_field: { name: 'boolean_field', string: "Boolean Field", type: 'boolean', default: true, searchable: true }, + char_field: { name: 'char_field', string: "Char Field", type: 'char', default: "foo", trim: true, searchable: true }, + float_field: { name: 'float_field', string: "Floaty McFloatface", type: 'float', searchable: true }, + color: { name: 'color', string: "Color", type: 'selection', selection: [['black', "Black"], ['white', "White"]], searchable: true }, + }; + }, + }, function () { + + QUnit.module('CustomFilterItem'); + + QUnit.test('basic rendering', async function (assert) { + assert.expect(17); + + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { + searchModel: new ActionModel(), + }, + }); + + assert.strictEqual(cfi.el.innerText.trim(), "Add Custom Filter"); + assert.hasClass(cfi.el, 'o_generator_menu'); + assert.strictEqual(cfi.el.children.length, 1); + + await cpHelpers.toggleAddCustomFilter(cfi); + + // Single condition + assert.containsOnce(cfi, 'div.o_filter_condition'); + assert.containsOnce(cfi, 'div.o_filter_condition > select.o_generator_menu_field'); + assert.containsOnce(cfi, 'div.o_filter_condition > select.o_generator_menu_operator'); + assert.containsOnce(cfi, 'div.o_filter_condition > span.o_generator_menu_value'); + assert.containsNone(cfi, 'div.o_filter_condition .o_or_filter'); + assert.containsNone(cfi, 'div.o_filter_condition .o_generator_menu_delete'); + + // no deletion allowed on single condition + assert.containsNone(cfi, 'div.o_filter_condition > i.o_generator_menu_delete'); + + // Buttons + assert.containsOnce(cfi, 'div.o_add_filter_menu'); + assert.containsOnce(cfi, 'div.o_add_filter_menu > button.o_apply_filter'); + assert.containsOnce(cfi, 'div.o_add_filter_menu > button.o_add_condition'); + + assert.containsOnce(cfi, 'div.o_filter_condition'); + + await testUtils.dom.click('button.o_add_condition'); + + assert.containsN(cfi, 'div.o_filter_condition', 2); + assert.containsOnce(cfi, 'div.o_filter_condition .o_or_filter'); + assert.containsN(cfi, 'div.o_filter_condition .o_generator_menu_delete', 2); + + cfi.destroy(); + }); + + QUnit.test('selection field: default and updated value', async function (assert) { + assert.expect(4); + + let expectedFilters; + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + assert.deepEqual(preFilters, expectedFilters); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + // Default value + expectedFilters = [{ + description: 'Color is "black"', + domain: '[["color","=","black"]]', + type: 'filter', + }]; + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color'); + await cpHelpers.applyFilter(cfi); + + // Updated value + expectedFilters = [{ + description: 'Color is "white"', + domain: '[["color","=","white"]]', + type: 'filter', + }]; + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color'); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_value select'), 'white'); + await cpHelpers.applyFilter(cfi); + + cfi.destroy(); + }); + + QUnit.test('adding a simple filter works', async function (assert) { + assert.expect(6); + + delete this.fields.date_field; + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const preFilter = preFilters[0]; + assert.strictEqual(preFilter.type, 'filter'); + assert.strictEqual(preFilter.description, 'Boolean Field is true'); + assert.strictEqual(preFilter.domain, '[["boolean_field","=",True]]'); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await cpHelpers.applyFilter(cfi); + + // The only thing visible should be the button 'Add Custome Filter'; + assert.strictEqual(cfi.el.children.length, 1); + assert.containsOnce(cfi, 'button.o_add_custom_filter'); + + cfi.destroy(); + }); + + QUnit.test('filtering by ID interval works', async function (assert) { + assert.expect(2); + this.fields.id_field = { name: 'id_field', string: "ID", type: "id", searchable: true }; + + const expectedDomains = [ + [['id_field','>', 10]], + [['id_field','<=', 20]], + ]; + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const preFilter = preFilters[0]; + // this step combine a tokenization/parsing followed by a string formatting + let domain = pyUtils.assembleDomains([preFilter.domain]); + domain = Domain.prototype.stringToArray(domain); + assert.deepEqual(domain, expectedDomains.shift()); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + async function testValue(operator, value) { + // open filter menu generator, select ID field, switch operator, type value, then click apply + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('select.o_generator_menu_field'), 'id_field'); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_operator'), operator); + await testUtils.fields.editInput(cfi.el.querySelector( + 'div.o_filter_condition > span.o_generator_menu_value input'), + value + ); + await cpHelpers.applyFilter(cfi); + } + + for (const domain of expectedDomains) { + await testValue(domain[0][1], domain[0][2]); + } + + cfi.destroy(); + }); + + + QUnit.test('commit search with an extended proposition with field char does not cause a crash', async function (assert) { + assert.expect(12); + + this.fields.many2one_field = { name: 'many2one_field', string: "Trululu", type: "many2one", searchable: true }; + const expectedDomains = [ + [['many2one_field', 'ilike', `a`]], + [['many2one_field', 'ilike', `"a"`]], + [['many2one_field', 'ilike', `'a'`]], + [['many2one_field', 'ilike', `'`]], + [['many2one_field', 'ilike', `"`]], + [['many2one_field', 'ilike', `\\`]], + ]; + const testedValues = [`a`, `"a"`, `'a'`, `'`, `"`, `\\`]; + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const preFilter = preFilters[0]; + // this step combine a tokenization/parsing followed by a string formatting + let domain = pyUtils.assembleDomains([preFilter.domain]); + domain = Domain.prototype.stringToArray(domain); + assert.deepEqual(domain, expectedDomains.shift()); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + async function testValue(value) { + // open filter menu generator, select trululu field and enter string `a`, then click apply + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('select.o_generator_menu_field'), 'many2one_field'); + await testUtils.fields.editInput(cfi.el.querySelector( + 'div.o_filter_condition > span.o_generator_menu_value input'), + value + ); + await cpHelpers.applyFilter(cfi); + } + + for (const value of testedValues) { + await testValue(value); + } + + delete ActionModel.registry.map.testExtension; + cfi.destroy(); + }); + + QUnit.test('custom filter datetime with equal operator', async function (assert) { + assert.expect(5); + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const preFilter = preFilters[0]; + assert.strictEqual(preFilter.description, + 'DateTime is equal to "02/22/2017 11:00:00"', + "description should be in localized format"); + assert.deepEqual(preFilter.domain, + '[["date_time_field","=","2017-02-22 15:00:00"]]', + "domain should be in UTC format"); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + session: { + getTZOffset() { + return -240; + }, + }, + env: { searchModel }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'date_time_field'); + + assert.strictEqual(cfi.el.querySelector('.o_generator_menu_field').value, 'date_time_field'); + assert.strictEqual(cfi.el.querySelector('.o_generator_menu_operator').value, 'between'); + + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_operator'), '='); + await testUtils.fields.editSelect(cfi.el.querySelector('div.o_filter_condition > span.o_generator_menu_value input'), '02/22/2017 11:00:00'); // in TZ + await cpHelpers.applyFilter(cfi); + + cfi.destroy(); + }); + + QUnit.test('custom filter datetime between operator', async function (assert) { + assert.expect(5); + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const preFilter = preFilters[0]; + assert.strictEqual(preFilter.description, + 'DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"', + "description should be in localized format"); + assert.deepEqual(preFilter.domain, + '[["date_time_field",">=","2017-02-22 15:00:00"]' + + ',["date_time_field","<=","2017-02-22 21:00:00"]]', + "domain should be in UTC format"); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + session: { + getTZOffset() { + return -240; + }, + }, + env: { searchModel }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'date_time_field'); + + assert.strictEqual(cfi.el.querySelector('.o_generator_menu_field').value, 'date_time_field'); + assert.strictEqual(cfi.el.querySelector('.o_generator_menu_operator').value, 'between'); + + const valueInputs = cfi.el.querySelectorAll('.o_generator_menu_value .o_input'); + await testUtils.fields.editSelect(valueInputs[0], '02/22/2017 11:00:00'); // in TZ + await testUtils.fields.editSelect(valueInputs[1], '02-22-2017 17:00:00'); // in TZ + await cpHelpers.applyFilter(cfi); + + cfi.destroy(); + }); + + QUnit.test('input value parsing', async function (assert) { + assert.expect(7); + + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { + searchModel: new ActionModel(), + }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.dom.click('button.o_add_condition'); + + const [floatSelect, idSelect] = cfi.el.querySelectorAll('.o_generator_menu_field'); + await testUtils.fields.editSelect(floatSelect, 'float_field'); + await testUtils.fields.editSelect(idSelect, 'id'); + + const [floatInput, idInput] = cfi.el.querySelectorAll('.o_generator_menu_value .o_input'); + + // Default values + assert.strictEqual(floatInput.value, "0.0"); + assert.strictEqual(idInput.value, "0"); + + // Float parsing + await testUtils.fields.editInput(floatInput, "4.2"); + assert.strictEqual(floatInput.value, "4.2"); + await testUtils.fields.editInput(floatInput, "DefinitelyValidFloat"); + // String input in a number input gives "", which is parsed as 0 + assert.strictEqual(floatInput.value, "0.0"); + + // Number parsing + await testUtils.fields.editInput(idInput, "4"); + assert.strictEqual(idInput.value, "4"); + await testUtils.fields.editInput(idInput, "4.2"); + assert.strictEqual(idInput.value, "4"); + await testUtils.fields.editInput(idInput, "DefinitelyValidID"); + // String input in a number input gives "", which is parsed as 0 + assert.strictEqual(idInput.value, "0"); + + cfi.destroy(); + }); + + QUnit.test('input value parsing with language', async function (assert) { + assert.expect(5); + + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { + searchModel: new ActionModel(), + _t: Object.assign(s => s, { database: { parameters: { decimal_point: "," } }}), + }, + translateParameters: { + decimal_point: ",", + thousands_sep: "", + grouping: [3, 0], + }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.dom.click('button.o_add_condition'); + + const [floatSelect] = cfi.el.querySelectorAll('.o_generator_menu_field'); + await testUtils.fields.editSelect(floatSelect, 'float_field'); + + const [floatInput] = cfi.el.querySelectorAll('.o_generator_menu_value .o_input'); + + // Default values + assert.strictEqual(floatInput.value, "0,0"); + + // Float parsing + await testUtils.fields.editInput(floatInput, '4,'); + assert.strictEqual(floatInput.value, "4,"); + await testUtils.fields.editInput(floatInput, '4,2'); + assert.strictEqual(floatInput.value, "4,2"); + await testUtils.fields.editInput(floatInput, '4,2,'); + assert.strictEqual(floatInput.value, "4,2"); + await testUtils.fields.editInput(floatInput, "DefinitelyValidFloat"); + // The input here is a string, resulting in a parsing error instead of 0 + assert.strictEqual(floatInput.value, "4,2"); + + cfi.destroy(); + }); + + QUnit.test('add custom filter with multiple values', async function (assert) { + assert.expect(2); + + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewFilters'); + const preFilters = args[0]; + const expected = [ + { + description: 'A date is equal to "01/09/1997"', + domain: '[["date_field","=","1997-01-09"]]', + type: "filter", + }, + { + description: 'Boolean Field is true', + domain: '[["boolean_field","=",True]]', + type: "filter", + }, + { + description: 'Floaty McFloatface is equal to "7.2"', + domain: '[["float_field","=",7.2]]', + type: "filter", + }, + { + description: 'ID is "9"', + domain: '[["id","=",9]]', + type: "filter", + }, + ]; + assert.deepEqual(preFilters, expected, + "Conditions should be in the correct order witht the right values."); + } + } + const searchModel = new MockedSearchModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { searchModel }, + }); + + await cpHelpers.toggleAddCustomFilter(cfi); + await testUtils.dom.click('button.o_add_condition'); + await testUtils.dom.click('button.o_add_condition'); + await testUtils.dom.click('button.o_add_condition'); + await testUtils.dom.click('button.o_add_condition'); + + function getCondition(index, selector) { + const condition = cfi.el.querySelectorAll('.o_filter_condition')[index]; + return condition.querySelector(selector); + } + + await testUtils.fields.editSelect(getCondition(0, '.o_generator_menu_field'), 'date_field'); + await testUtils.fields.editSelect(getCondition(0, '.o_generator_menu_value .o_input'), '01/09/1997'); + + await testUtils.fields.editSelect(getCondition(1, '.o_generator_menu_field'), 'boolean_field'); + await testUtils.fields.editInput(getCondition(1, '.o_generator_menu_operator'), '!='); + + await testUtils.fields.editSelect(getCondition(2, '.o_generator_menu_field'), 'char_field'); + await testUtils.fields.editInput(getCondition(2, '.o_generator_menu_value .o_input'), "I will be deleted anyway"); + + await testUtils.fields.editSelect(getCondition(3, '.o_generator_menu_field'), 'float_field'); + await testUtils.fields.editInput(getCondition(3, '.o_generator_menu_value .o_input'), 7.2); + + await testUtils.fields.editSelect(getCondition(4, '.o_generator_menu_field'), 'id'); + await testUtils.fields.editInput(getCondition(4, '.o_generator_menu_value .o_input'), 9); + + await testUtils.dom.click(getCondition(2, '.o_generator_menu_delete')); + + await cpHelpers.applyFilter(cfi); + + cfi.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/custom_group_by_item_tests.js b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js new file mode 100644 index 00000000..ee8d0331 --- /dev/null +++ b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js @@ -0,0 +1,74 @@ +odoo.define('web.groupby_menu_generator_tests', function (require) { + "use strict"; + + const CustomGroupByItem = require('web.CustomGroupByItem'); + const ActionModel = require('web/static/src/js/views/action_model.js'); + const testUtils = require('web.test_utils'); + + const { createComponent } = testUtils; + + QUnit.module('Components', {}, function () { + + QUnit.module('CustomGroupByItem'); + + QUnit.test('click on add custom group toggle group selector', async function (assert) { + assert.expect(6); + + const cgi = await createComponent(CustomGroupByItem, { + props: { + fields: [ + { sortable: true, name: "date", string: 'Super Date', type: 'date' }, + ], + }, + env: { + searchModel: new ActionModel(), + }, + }); + + assert.strictEqual(cgi.el.innerText.trim(), "Add Custom Group"); + assert.hasClass(cgi.el, 'o_generator_menu'); + assert.strictEqual(cgi.el.children.length, 1); + + await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_add_custom_group_by')); + + // Single select node with a single option + assert.containsOnce(cgi, 'div > select.o_group_by_selector'); + assert.strictEqual(cgi.el.querySelector('div > select.o_group_by_selector option').innerText.trim(), + "Super Date"); + + // Button apply + assert.containsOnce(cgi, 'button.o_apply_group_by'); + + cgi.destroy(); + }); + + QUnit.test('select a field name in Add Custom Group menu properly trigger the corresponding field', async function (assert) { + assert.expect(4); + + const fields = [ + { sortable: true, name: 'candlelight', string: 'Candlelight', type: 'boolean' }, + ]; + class MockedSearchModel extends ActionModel { + dispatch(method, ...args) { + assert.strictEqual(method, 'createNewGroupBy'); + const field = args[0]; + assert.deepEqual(field, fields[0]); + } + } + const searchModel = new MockedSearchModel(); + const cgi = await createComponent(CustomGroupByItem, { + props: { fields }, + env: { searchModel }, + }); + + await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_add_custom_group_by')); + await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_apply_group_by')); + + // The only thing visible should be the button 'Add Custome Group'; + assert.strictEqual(cgi.el.children.length, 1); + assert.containsOnce(cgi, 'button.o_add_custom_group_by'); + + cgi.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/favorite_menu_tests.js b/addons/web/static/tests/control_panel/favorite_menu_tests.js new file mode 100644 index 00000000..4dd4571c --- /dev/null +++ b/addons/web/static/tests/control_panel/favorite_menu_tests.js @@ -0,0 +1,625 @@ +odoo.define('web.favorite_menu_tests', function (require) { + "use strict"; + + const FormView = require('web.FormView'); + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createControlPanel, createView, mock } = testUtils; + const { patchDate } = mock; + + const searchMenuTypes = ['favorite']; + + QUnit.module('Components', { + beforeEach: function () { + this.fields = { + bar: { string: "Bar", type: "many2one", relation: 'partner' }, + birthday: { string: "Birthday", type: "date", store: true, sortable: true }, + date_field: { string: "Date", type: "date", store: true, sortable: true }, + float_field: { string: "Float", type: "float", group_operator: 'sum' }, + foo: { string: "Foo", type: "char", store: true, sortable: true }, + }; + }, + }, function () { + + QUnit.module('FavoriteMenu'); + + QUnit.test('simple rendering with no favorite', async function (assert) { + assert.expect(8); + + const params = { + cpModelConfig: { searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes, action: { name: "Action Name" } }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsOnce(controlPanel, 'div.o_favorite_menu > button i.fa.fa-star'); + assert.strictEqual(controlPanel.el.querySelector('div.o_favorite_menu > button span').innerText.trim(), "Favorites"); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + assert.containsNone(controlPanel, '.dropdown-divider'); + assert.containsOnce(controlPanel, '.o_add_favorite'); + assert.strictEqual(controlPanel.el.querySelector('.o_add_favorite > button').innerText.trim(), + "Save current search"); + + await cpHelpers.toggleSaveFavorite(controlPanel); + assert.strictEqual( + controlPanel.el.querySelector('.o_add_favorite input[type="text"]').value, + 'Action Name' + ); + assert.containsN(controlPanel, '.o_add_favorite .custom-checkbox input[type="checkbox"]', 2); + const labelEls = controlPanel.el.querySelectorAll('.o_add_favorite .custom-checkbox label'); + assert.deepEqual( + [...labelEls].map(e => e.innerText.trim()), + ["Use by default", "Share with all users"] + ); + + controlPanel.destroy(); + }); + + QUnit.test('favorites use by default and share are exclusive', async function (assert) { + assert.expect(11); + + const params = { + cpModelConfig: { + viewInfo: { fields: this.fields }, + searchMenuTypes + }, + cpProps: { + fields: this.fields, + searchMenuTypes, + action: {}, + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + const checkboxes = controlPanel.el.querySelectorAll('input[type="checkbox"]'); + + assert.strictEqual(checkboxes.length, 2, '2 checkboxes are present'); + + assert.notOk(checkboxes[0].checked, 'Start: None of the checkboxes are checked (1)'); + assert.notOk(checkboxes[1].checked, 'Start: None of the checkboxes are checked (2)'); + + await testUtils.dom.click(checkboxes[0]); + assert.ok(checkboxes[0].checked, 'The first checkbox is checked'); + assert.notOk(checkboxes[1].checked, 'The second checkbox is not checked'); + + await testUtils.dom.click(checkboxes[1]); + assert.notOk(checkboxes[0].checked, + 'Clicking on the second checkbox checks it, and unchecks the first (1)'); + assert.ok(checkboxes[1].checked, + 'Clicking on the second checkbox checks it, and unchecks the first (2)'); + + await testUtils.dom.click(checkboxes[0]); + assert.ok(checkboxes[0].checked, + 'Clicking on the first checkbox checks it, and unchecks the second (1)'); + assert.notOk(checkboxes[1].checked, + 'Clicking on the first checkbox checks it, and unchecks the second (2)'); + + await testUtils.dom.click(checkboxes[0]); + assert.notOk(checkboxes[0].checked, 'End: None of the checkboxes are checked (1)'); + assert.notOk(checkboxes[1].checked, 'End: None of the checkboxes are checked (2)'); + + controlPanel.destroy(); + }); + + QUnit.test('save filter', async function (assert) { + assert.expect(1); + + const params = { + cpModelConfig: { + fields: this.fields, + searchMenuTypes + }, + cpProps: { + fields: this.fields, + searchMenuTypes, + action: {}, + }, + 'get-controller-query-params': function (callback) { + callback({ + orderedBy: [ + { asc: true, name: 'foo' }, + { asc: false, name: 'bar' } + ] + }); + }, + env: { + dataManager: { + create_filter: async function (filter) { + assert.strictEqual(filter.sort, '["foo","bar desc"]', + 'The right format for the string "sort" should be sent to the server' + ); + } + } + }, + }; + + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + await cpHelpers.editFavoriteName(controlPanel, "aaa"); + await cpHelpers.saveFavorite(controlPanel); + + controlPanel.destroy(); + }); + + QUnit.test('dynamic filters are saved dynamic', async function (assert) { + assert.expect(3); + + const arch = ` + + + + `; + const params = { + cpModelConfig: { + fields: {}, + arch , + searchMenuTypes, + context: { + search_default_positive: true, + } + }, + cpProps: { + fields: {}, + searchMenuTypes, + action: {}, + }, + 'get-controller-query-params': function (callback) { + callback(); + }, + env: { + dataManager: { + create_filter: async function (filter) { + assert.strictEqual( + filter.domain, + "[(\"date_field\", \">=\", (context_today() + relativedelta()).strftime(\"%Y-%m-%d\"))]" + ); + return 1; // serverSideId + } + } + }, + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Float']); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + await cpHelpers.editFavoriteName(controlPanel, "My favorite"); + await cpHelpers.saveFavorite(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]); + + controlPanel.destroy(); + }); + + QUnit.test('save filters created via autocompletion works', async function (assert) { + assert.expect(4); + + const arch = ``; + const params = { + cpModelConfig: { + fields: this.fields, + arch , + searchMenuTypes, + }, + cpProps: { + fields: this.fields, + searchMenuTypes, + action: {}, + }, + 'get-controller-query-params': function (callback) { + callback(); + }, + env: { + dataManager: { + create_filter: async function (filter) { + assert.strictEqual( + filter.domain, + `[["foo", "ilike", "a"]]` + ); + return 1; // serverSideId + } + } + }, + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + await cpHelpers.editSearch(controlPanel, "a"); + await cpHelpers.validateSearch(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Foo\na"]); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + await cpHelpers.editFavoriteName(controlPanel, "My favorite"); + await cpHelpers.saveFavorite(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]); + + controlPanel.destroy(); + }); + + QUnit.test('delete an active favorite remove it both in list of favorite and in search bar', async function (assert) { + assert.expect(6); + + const favoriteFilters = [{ + context: "{}", + domain: "[['foo', '=', 'qsdf']]", + id: 7, + is_default: true, + name: "My favorite", + sort: "[]", + user_id: [2, "Mitchell Admin"], + }]; + const params = { + cpModelConfig: { favoriteFilters, searchMenuTypes }, + cpProps: { searchMenuTypes, action: {} }, + search: function (searchQuery) { + const { domain } = searchQuery; + assert.deepEqual(domain, []); + }, + env: { + dataManager: { + delete_filter: function () { + return Promise.resolve(); + } + } + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + + const { domain } = controlPanel.getQuery(); + assert.deepEqual(domain, [["foo", "=", "qsdf"]]); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]); + assert.hasClass(controlPanel.el.querySelector('.o_favorite_menu .o_menu_item > a'), 'selected'); + + await cpHelpers.deleteFavorite(controlPanel, 0); + + // confirm deletion + await testUtils.dom.click(document.querySelector('div.o_dialog footer button')); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + const itemEls = controlPanel.el.querySelectorAll('.o_favorite_menu .o_menu_item'); + assert.deepEqual([...itemEls].map(e => e.innerText.trim()), ["Save current search"]); + + controlPanel.destroy(); + }); + + QUnit.test('default favorite is not activated if key search_disable_custom_filters is set to true', async function (assert) { + assert.expect(2); + + const favoriteFilters = [{ + context: "{}", + domain: "", + id: 7, + is_default: true, + name: "My favorite", + sort: "[]", + user_id: [2, "Mitchell Admin"], + }]; + const params = { + cpModelConfig: { + favoriteFilters, + searchMenuTypes, + context: { search_disable_custom_filters: true } + }, + cpProps: { searchMenuTypes, action: {} }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + + const { domain } = controlPanel.getQuery(); + assert.deepEqual(domain, []); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + controlPanel.destroy(); + }); + + QUnit.test('toggle favorite correctly clears filter, groupbys, comparison and field "options"', async function (assert) { + assert.expect(11); + + const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0); + + const favoriteFilters = [{ + context: ` + { + "group_by": ["foo"], + "comparison": { + "favorite comparison content": "bla bla..." + }, + } + `, + domain: "['!', ['foo', '=', 'qsdf']]", + id: 7, + is_default: false, + name: "My favorite", + sort: "[]", + user_id: [2, "Mitchell Admin"], + }]; + let firstSearch = true; + const arch = ` + + + + + + `; + const searchMenuTypes = ['filter', 'groupBy', 'comparison', 'favorite']; + const params = { + cpModelConfig: { + favoriteFilters, + arch, + fields: this.fields, + searchMenuTypes, + context: { + search_default_positive: true, + search_default_coolName: true, + search_default_foo: "a", + } + }, + cpProps: { searchMenuTypes, action: {}, fields: this.fields }, + search: function (searchQuery) { + const { domain, groupBy, timeRanges } = searchQuery; + if (firstSearch) { + assert.deepEqual(domain, [['foo', 'ilike', 'a']]); + assert.deepEqual(groupBy, ['date_field:month']); + assert.deepEqual(timeRanges, { + comparisonId: "previous_period", + comparisonRange: ["&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]], + comparisonRangeDescription: "2018", + fieldDescription: "Date Field Filter", + fieldName: "date_field", + range: ["&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-12-31"]], + rangeDescription: "2019", + }); + firstSearch = false; + } else { + assert.deepEqual(domain, ['!', ['foo', '=', 'qsdf']]); + assert.deepEqual(groupBy, ['foo']); + assert.deepEqual(timeRanges, { + "favorite comparison content": "bla bla...", + range: undefined, + comparisonRange: undefined, + }); + } + }, + }; + const controlPanel = await createControlPanel(params); + + const { domain, groupBy, timeRanges } = controlPanel.getQuery(); + assert.deepEqual(domain, [ + "&", + ["foo", "ilike", "a"], + "&", + ["date_field", ">=", "2019-01-01"], + ["date_field", "<=", "2019-12-31"] + ]); + assert.deepEqual(groupBy, ['date_field:month']); + assert.deepEqual(timeRanges, {}); + + assert.deepEqual( + cpHelpers.getFacetTexts(controlPanel), + [ + 'Foo\na', + 'Date Field Filter: 2019', + 'Date Field Groupby: Month', + ] + ); + + // activate a comparison + await cpHelpers.toggleComparisonMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date Field Filter: Previous period"); + + // activate the unique existing favorite + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + + assert.deepEqual( + cpHelpers.getFacetTexts(controlPanel), + ["My favorite"] + ); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('favorites have unique descriptions (the submenus of the favorite menu are correctly updated)', async function (assert) { + assert.expect(3); + + const favoriteFilters = [{ + context: "{}", + domain: "[]", + id: 1, + is_default: false, + name: "My favorite", + sort: "[]", + user_id: [2, "Mitchell Admin"], + }]; + const params = { + cpModelConfig: { favoriteFilters, searchMenuTypes }, + cpProps: { searchMenuTypes, action: {} }, + 'get-controller-query-params': function (callback) { + callback(); + }, + env: { + session: { uid: 4 }, + services: { + notification: { + notify: function (params) { + assert.deepEqual(params, { + message: "Filter with same name already exists.", + type: "danger" + }); + }, + } + }, + dataManager: { + create_filter: async function (irFilter) { + assert.deepEqual(irFilter, { + "action_id": undefined, + "context": { "group_by": [] }, + "domain": "[]", + "is_default": false, + "model_id": undefined, + "name": "My favorite 2", + "sort": "[]", + "user_id": 4, + }); + return 2; // serverSideId + } + } + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFavoriteMenu(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + + // first try: should fail + await cpHelpers.editFavoriteName(controlPanel, "My favorite"); + await cpHelpers.saveFavorite(controlPanel); + + // second try: should succeed + await cpHelpers.editFavoriteName(controlPanel, "My favorite 2"); + await cpHelpers.saveFavorite(controlPanel); + await cpHelpers.toggleSaveFavorite(controlPanel); + + // third try: should fail + await cpHelpers.editFavoriteName(controlPanel, "My favorite 2"); + await cpHelpers.saveFavorite(controlPanel); + + controlPanel.destroy(); + }); + + QUnit.test('save search filter in modal', async function (assert) { + assert.expect(5); + const data = { + partner: { + fields: { + date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true }, + birthday: { string: "Birthday", type: "date", store: true, sortable: true }, + foo: { string: "Foo", type: "char", store: true, sortable: true }, + bar: { string: "Bar", type: "many2one", relation: 'partner' }, + float_field: { string: "Float", type: "float", group_operator: 'sum' }, + }, + records: [ + { id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", birthday: "1983-07-15", float_field: 1 }, + { id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", birthday: "1982-06-04", float_field: 2 }, + { id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", birthday: "1985-09-13", float_field: 1.618 }, + { id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", birthday: "1983-05-05", float_field: -1 }, + { id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", birthday: "1800-01-01", float_field: 13 }, + { id: 7, display_name: "Partner 6", }, + { id: 8, display_name: "Partner 7", }, + { id: 9, display_name: "Partner 8", }, + { id: 10, display_name: "Partner 9", } + ], + }, + }; + const form = await createView({ + arch: ` +
+ + + + + +
`, + archs: { + 'partner,false,list': '', + 'partner,false,search': '', + }, + data, + model: 'partner', + res_id: 1, + View: FormView, + env: { + dataManager: { + create_filter(filter) { + assert.strictEqual(filter.name, "Awesome Test Customer Filter", + "filter name should be correct"); + }, + } + }, + }); + + await testUtils.form.clickEdit(form); + + await testUtils.fields.many2one.clickOpenDropdown('bar'); + await testUtils.fields.many2one.clickItem('bar', 'Search'); + + assert.containsN(document.body, 'tr.o_data_row', 9, "should display 9 records"); + + await cpHelpers.toggleFilterMenu('.modal'); + await cpHelpers.toggleAddCustomFilter('.modal'); + assert.strictEqual(document.querySelector('.o_filter_condition select.o_generator_menu_field').value, + 'date_field', + "date field should be selected"); + await cpHelpers.applyFilter('.modal'); + + assert.containsNone(document.body, 'tr.o_data_row', "should display 0 records"); + + // Save this search + await cpHelpers.toggleFavoriteMenu('.modal'); + await cpHelpers.toggleSaveFavorite('.modal'); + + const filterNameInput = document.querySelector('.o_add_favorite input[type="text"]'); + assert.isVisible(filterNameInput, "should display an input field for the filter name"); + + await testUtils.fields.editInput(filterNameInput, 'Awesome Test Customer Filter'); + await testUtils.dom.click(document.querySelector('.o_add_favorite button.btn-primary')); + + form.destroy(); + }); + + QUnit.test('modal loads saved search filters', async function (assert) { + assert.expect(1); + const data = { + partner: { + fields: { + bar: { string: "Bar", type: "many2one", relation: 'partner' }, + }, + // 10 records so that the Search button shows + records: Array.apply(null, Array(10)).map(function(_, i) { + return { id: i, display_name: "Record " + i, bar: 1 }; + }) + }, + }; + const form = await createView({ + arch: ` +
+ + + + + +
`, + data, + model: 'partner', + res_id: 1, + View: FormView, + interceptsPropagate: { + load_views: function (ev) { + assert.ok(ev.data.options.load_filters, "opening dialog should load the filters"); + }, + }, + }); + + await testUtils.form.clickEdit(form); + + await testUtils.fields.many2one.clickOpenDropdown('bar'); + await testUtils.fields.many2one.clickItem('bar', 'Search'); + + form.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/filter_menu_tests.js b/addons/web/static/tests/control_panel/filter_menu_tests.js new file mode 100644 index 00000000..baba1d39 --- /dev/null +++ b/addons/web/static/tests/control_panel/filter_menu_tests.js @@ -0,0 +1,503 @@ +odoo.define('web.filter_menu_tests', function (require) { + "use strict"; + + const testUtils = require('web.test_utils'); + + const { controlPanel: cpHelpers, createControlPanel, mock } = testUtils; + const { patchDate } = mock; + + const searchMenuTypes = ['filter']; + + QUnit.module('Components', { + beforeEach: function () { + this.fields = { + date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true }, + foo: { string: "Foo", type: "char", store: true, sortable: true }, + }; + }, + }, function () { + + QUnit.module('FilterMenu'); + + QUnit.test('simple rendering with no filter', async function (assert) { + assert.expect(2); + + const params = { + cpModelConfig: { searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider'); + assert.containsOnce(controlPanel, 'div.o_generator_menu'); + + controlPanel.destroy(); + }); + + QUnit.test('simple rendering with a single filter', async function (assert) { + assert.expect(3); + + const arch = ` + + + `; + const params = { + cpModelConfig: { arch, fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + assert.containsOnce(controlPanel, '.o_menu_item'); + assert.containsOnce(controlPanel, '.dropdown-divider'); + assert.containsOnce(controlPanel, 'div.o_generator_menu'); + + controlPanel.destroy(); + }); + + QUnit.test('should have Date and ID field proposed in that order in "Add custom Filter" submenu', async function (assert) { + assert.expect(2); + + const params = { + cpModelConfig: { fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleAddCustomFilter(controlPanel); + const optionEls = controlPanel.el.querySelectorAll('div.o_filter_condition > select.o_generator_menu_field option'); + assert.strictEqual(optionEls[0].innerText.trim(), 'Date'); + assert.strictEqual(optionEls[1].innerText.trim(), 'ID'); + + controlPanel.destroy(); + }); + + QUnit.test('toggle a "simple" filter in filter menu works', async function (assert) { + assert.expect(9); + + const domains = [ + [['foo', '=', 'qsdf']], + [] + ]; + const arch = ` + + + `; + const params = { + cpModelConfig: { arch, searchMenuTypes }, + cpProps: { fields: {}, searchMenuTypes }, + search: function (searchQuery) { + const { domain } = searchQuery; + assert.deepEqual(domain, domains.shift()); + } + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + assert.notOk(cpHelpers.isItemSelected(controlPanel, 0)); + await cpHelpers.toggleMenuItem(controlPanel, "Foo"); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo']); + assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'), + 'span.fa.fa-filter.o_searchview_facet_label'); + + assert.ok(cpHelpers.isItemSelected(controlPanel, "Foo")); + + await cpHelpers.toggleMenuItem(controlPanel, "Foo"); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + assert.notOk(cpHelpers.isItemSelected(controlPanel, "Foo")); + + controlPanel.destroy(); + }); + + QUnit.test('add a custom filter works', async function (assert) { + assert.expect(1); + + const params = { + cpModelConfig: { fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleAddCustomFilter(controlPanel); + // choose ID field in 'Add Custome filter' menu and value 1 + await testUtils.fields.editSelect( + controlPanel.el.querySelector('div.o_filter_condition > select.o_generator_menu_field'), 'id'); + await testUtils.fields.editInput( + controlPanel.el.querySelector('div.o_filter_condition > span.o_generator_menu_value > input'), 1); + + await cpHelpers.applyFilter(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['ID is "1"']); + + controlPanel.destroy(); + }); + + QUnit.test('deactivate a new custom filter works', async function (assert) { + assert.expect(4); + + const unpatchDate = patchDate(2020, 1, 5, 12, 20, 0); + + const params = { + cpModelConfig: { fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleAddCustomFilter(controlPanel); + await cpHelpers.applyFilter(controlPanel); + + assert.ok(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"')); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date is equal to "02/05/2020"']); + + await cpHelpers.toggleMenuItem(controlPanel, 'Date is equal to "02/05/2020"'); + + assert.notOk(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"')); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('filter by a date field using period works', async function (assert) { + assert.expect(56); + + const unpatchDate = patchDate(2017, 2, 22, 1, 0, 0); + + const basicDomains = [ + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]], + ["&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"]], + ["|", + "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"], + "&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"] + ], + ["&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-03-31"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]], + ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]], + ["|", + "&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"], + "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"] + ], + ["|", + "|", + "&", ["date_field", ">=", "2015-01-01"], ["date_field", "<=", "2015-12-31"], + "&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"], + "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"] + ], + ["|", + "|", + "&", ["date_field", ">=", "2015-03-01"], ["date_field", "<=", "2015-03-31"], + "&", ["date_field", ">=", "2016-03-01"], ["date_field", "<=", "2016-03-31"], + "&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"] + ] + ]; + + const arch = ` + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_date_field: 1 }, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + // we inspect query domain + const { domain } = searchQuery; + if (domain.length) { + assert.deepEqual(domain, basicDomains.shift()); + } + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date"); + + const optionEls = controlPanel.el.querySelectorAll('ul.o_menu_item_options > li.o_item_option > a'); + + // default filter should be activated with the global default period 'this_month' + const { domain } = controlPanel.getQuery(); + assert.deepEqual( + domain, + ["&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"]] + ); + assert.ok(cpHelpers.isItemSelected(controlPanel, "Date")); + assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 0)); + + // check option descriptions + const optionDescriptions = [...optionEls].map(e => e.innerText.trim()); + const expectedDescriptions = [ + 'March', 'February', 'January', + 'Q4', 'Q3', 'Q2', 'Q1', + '2017', '2016', '2015' + ]; + assert.deepEqual(optionDescriptions, expectedDescriptions); + + // check generated domains + const steps = [ + { description: 'March', facetContent: 'Date: 2017', selectedoptions: [7] }, + { description: 'February', facetContent: 'Date: February 2017', selectedoptions: [1, 7] }, + { description: 'February', facetContent: 'Date: 2017', selectedoptions: [7] }, + { description: 'January', facetContent: 'Date: January 2017', selectedoptions: [2, 7] }, + { description: 'Q4', facetContent: 'Date: January 2017/Q4 2017', selectedoptions: [2, 3, 7] }, + { description: 'January', facetContent: 'Date: Q4 2017', selectedoptions: [3, 7] }, + { description: 'Q4', facetContent: 'Date: 2017', selectedoptions: [7] }, + { description: 'Q1', facetContent: 'Date: Q1 2017', selectedoptions: [6, 7] }, + { description: 'Q1', facetContent: 'Date: 2017', selectedoptions: [7] }, + { description: '2017', selectedoptions: [] }, + { description: '2017', facetContent: 'Date: 2017', selectedoptions: [7] }, + { description: '2016', facetContent: 'Date: 2016/2017', selectedoptions: [7, 8] }, + { description: '2015', facetContent: 'Date: 2015/2016/2017', selectedoptions: [7, 8, 9] }, + { description: 'March', facetContent: 'Date: March 2015/March 2016/March 2017', selectedoptions: [0, 7, 8, 9] } + ]; + for (const s of steps) { + const index = expectedDescriptions.indexOf(s.description); + await cpHelpers.toggleMenuItemOption(controlPanel, 0, index); + if (s.facetContent) { + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]); + } else { + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + } + s.selectedoptions.forEach(index => { + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index), + `at step ${steps.indexOf(s) + 1}, option ${expectedDescriptions[index]} should be selected`); + }); + } + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('filter by a date field using period works even in January', async function (assert) { + assert.expect(5); + + const unpatchDate = patchDate(2017, 0, 7, 3, 0, 0); + + const arch = ` + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_some_filter: 1 }, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + const { domain } = controlPanel.getQuery(); + assert.deepEqual(domain, [ + '&', + ["date_field", ">=", "2016-12-01"], + ["date_field", "<=", "2016-12-31"] + ]); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: December 2016"]); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, "Date"); + + assert.ok(cpHelpers.isItemSelected(controlPanel, "Date")); + assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 'December')); + assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", '2016')); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('`context` key in is used', async function (assert) { + assert.expect(1); + + const arch = ` + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + // we inspect query context + const { context } = searchQuery; + assert.deepEqual(context, { coucou_1: 1 }); + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + + controlPanel.destroy(); + }); + + QUnit.test('Filter with JSON-parsable domain works', async function (assert) { + assert.expect(1); + + const originalDomain = [['foo', '=', 'Gently Weeps']]; + const xml_domain = JSON.stringify(originalDomain); + + const arch = + ` + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { domain } = searchQuery; + assert.deepEqual(domain, originalDomain, + 'A JSON parsable xml domain should be handled just like any other' + ); + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + + controlPanel.destroy(); + }); + + QUnit.test('filter with date attribute set as search_default', async function (assert) { + assert.expect(1); + + const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0); + + const arch = + ` + + `, + params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { + search_default_date_field: true + } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: June 2019"]); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('filter domains are correcly combined by OR and AND', async function (assert) { + assert.expect(2); + + const arch = + ` + + + + + `, + params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { + search_default_f_1_g1: true, + search_default_f1_g2: true, + search_default_f2_g2: true, + } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + const { domain } = controlPanel.getQuery(); + assert.deepEqual(domain, [ + '&', + ['foo', '=', 'f1_g1'], + '|', + ['foo', '=', 'f1_g2'], + ['foo', '=', 'f2_g2'] + ]); + + assert.deepEqual( + cpHelpers.getFacetTexts(controlPanel), + ["Filter Group 1", "Filter 1 Group 2orFilter 2 GROUP 2"] + ); + + controlPanel.destroy(); + }); + + QUnit.test('arch order of groups of filters preserved', async function (assert) { + assert.expect(12); + + const arch = + ` + + + + + + + + + + + + + + + + + + + + + + `, + params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleFilterMenu(controlPanel); + assert.containsN(controlPanel, '.o_filter_menu .o_menu_item', 11); + + const menuItemEls = controlPanel.el.querySelectorAll('.o_filter_menu .o_menu_item'); + [...menuItemEls].forEach((e, index) => { + assert.strictEqual(e.innerText.trim(), String(index + 1)); + }); + + controlPanel.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/groupby_menu_tests.js b/addons/web/static/tests/control_panel/groupby_menu_tests.js new file mode 100644 index 00000000..1239f088 --- /dev/null +++ b/addons/web/static/tests/control_panel/groupby_menu_tests.js @@ -0,0 +1,478 @@ +odoo.define('web.groupby_menu_tests', function (require) { + "use strict"; + + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createControlPanel } = testUtils; + + const searchMenuTypes = ['groupBy']; + + QUnit.module('Components', { + beforeEach: function () { + this.fields = { + bar: { string: "Bar", type: "many2one", relation: 'partner' }, + birthday: { string: "Birthday", type: "date", store: true, sortable: true }, + date_field: { string: "Date", type: "date", store: true, sortable: true }, + float_field: { string: "Float", type: "float", group_operator: 'sum' }, + foo: { string: "Foo", type: "char", store: true, sortable: true }, + }; + }, + }, function () { + + QUnit.module('GroupByMenu'); + + QUnit.test('simple rendering with neither groupbys nor groupable fields', async function (assert) { + + assert.expect(1); + const params = { + cpModelConfig: { searchMenuTypes }, + cpProps: { fields: {}, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider, div.o_generator_menu'); + + controlPanel.destroy(); + }); + + QUnit.test('simple rendering with no groupby', async function (assert) { + assert.expect(5); + + const params = { + cpModelConfig: { searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider'); + assert.containsOnce(controlPanel, 'div.o_generator_menu'); + + await cpHelpers.toggleAddCustomGroup(controlPanel); + + const optionEls = controlPanel.el.querySelectorAll('div.o_generator_menu select.o_group_by_selector option'); + assert.strictEqual(optionEls[0].innerText.trim(), 'Birthday'); + assert.strictEqual(optionEls[1].innerText.trim(), 'Date'); + assert.strictEqual(optionEls[2].innerText.trim(), 'Foo'); + + controlPanel.destroy(); + }); + + QUnit.test('simple rendering with a single groupby', async function (assert) { + assert.expect(4); + + const arch = ` + + + `; + const params = { + cpModelConfig: { arch, fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + assert.containsOnce(controlPanel, '.o_menu_item'); + assert.strictEqual(controlPanel.el.querySelector('.o_menu_item').innerText.trim(), "Groupby Foo"); + assert.containsOnce(controlPanel, '.dropdown-divider'); + assert.containsOnce(controlPanel, 'div.o_generator_menu'); + + controlPanel.destroy(); + }); + + QUnit.test('toggle a "simple" groupby in groupby menu works', async function (assert) { + assert.expect(9); + + const groupBys = [['foo'], []]; + const arch = ` + + + `; + const params = { + cpModelConfig: {arch, fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { groupBy } = searchQuery; + assert.deepEqual(groupBy, groupBys.shift()); + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + assert.notOk(cpHelpers.isItemSelected(controlPanel, 0)); + + await cpHelpers.toggleMenuItem(controlPanel, 0); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Groupby Foo']); + assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'), + 'span.fa.fa-bars.o_searchview_facet_label'); + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + + await cpHelpers.toggleMenuItem(controlPanel, 0); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 0)); + + controlPanel.destroy(); + }); + + QUnit.test('toggle a "simple" groupby quickly does not crash', async function (assert) { + assert.expect(1); + + const arch = ` + + + `; + const params = { + cpModelConfig: { arch, fields: this.fields, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + + cpHelpers.toggleMenuItem(controlPanel, 0); + cpHelpers.toggleMenuItem(controlPanel, 0); + + assert.ok(true); + controlPanel.destroy(); + }); + + QUnit.test('remove a "Group By" facet properly unchecks groupbys in groupby menu', async function (assert) { + assert.expect(5); + + const arch = ` + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_gb_foo: 1 } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { groupBy } = searchQuery; + assert.deepEqual(groupBy, []); + }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + const facetEl = controlPanel.el.querySelector('.o_searchview .o_searchview_facet'); + assert.strictEqual(facetEl.innerText.trim(), "Groupby Foo"); + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + + await testUtils.dom.click(facetEl.querySelector('i.o_facet_remove')); + assert.containsNone(controlPanel, '.o_searchview .o_searchview_facet'); + await cpHelpers.toggleGroupByMenu(controlPanel); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 0)); + + controlPanel.destroy(); + }); + + QUnit.test('group by a date field using interval works', async function (assert) { + assert.expect(21); + + const groupBys = [ + ['date_field:year', 'date_field:week' ], + ['date_field:year', 'date_field:month', 'date_field:week'], + ['date_field:year', 'date_field:month'], + ['date_field:year'], + [] + ]; + + const arch = ` + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_date: 1 } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { groupBy } = searchQuery; + assert.deepEqual(groupBy, groupBys.shift()); + }, + }; + + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + + const optionEls = controlPanel.el.querySelectorAll('ul.o_menu_item_options > li.o_item_option > a'); + + // default groupby should be activated with the default inteval 'week' + const { groupBy } = controlPanel.getQuery(); + assert.deepEqual(groupBy, ['date_field:week']); + + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 3)); + + // check option descriptions + const optionDescriptions = [...optionEls].map(e => e.innerText.trim()); + const expectedDescriptions = ['Year', 'Quarter', 'Month', 'Week', 'Day']; + assert.deepEqual(optionDescriptions, expectedDescriptions); + + const steps = [ + { description: 'Year', facetContent: 'Date: Year>Date: Week', selectedoptions: [0, 3] }, + { description: 'Month', facetContent: 'Date: Year>Date: Month>Date: Week', selectedoptions: [0, 2, 3] }, + { description: 'Week', facetContent: 'Date: Year>Date: Month', selectedoptions: [0, 2] }, + { description: 'Month', facetContent: 'Date: Year', selectedoptions: [0] }, + { description: 'Year', selectedoptions: [] }, + ]; + for (const s of steps) { + const index = expectedDescriptions.indexOf(s.description); + await cpHelpers.toggleMenuItemOption(controlPanel, 0, index); + if (s.facetContent) { + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]); + } else { + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + } + s.selectedoptions.forEach(index => { + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index)); + }); + } + controlPanel.destroy(); + }); + + QUnit.test('interval options are correctly grouped and ordered', async function (assert) { + assert.expect(8); + + const arch = ` + + + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_bar: 1 } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar']); + + // open menu 'Group By' + await cpHelpers.toggleGroupByMenu(controlPanel); + + // Open the groupby 'Date' + await cpHelpers.toggleMenuItem(controlPanel, 'Date'); + // select option 'week' + await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week']); + + // select option 'day' + await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Day'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week>Date: Day']); + + // select option 'year' + await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Year'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day']); + + // select 'Foo' + await cpHelpers.toggleMenuItem(controlPanel, 'Foo'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day>Foo']); + + // select option 'quarter' + await cpHelpers.toggleMenuItem(controlPanel, 'Date'); + await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Quarter'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']); + + // unselect 'Bar' + await cpHelpers.toggleMenuItem(controlPanel, 'Bar'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']); + + // unselect option 'week' + await cpHelpers.toggleMenuItem(controlPanel, 'Date'); + await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Day>Foo']); + + controlPanel.destroy(); + }); + + QUnit.test('the ID field should not be proposed in "Add Custom Group" menu', async function (assert) { + assert.expect(2); + + const fields = { + foo: { string: "Foo", type: "char", store: true, sortable: true }, + id: { sortable: true, string: 'ID', type: 'integer' } + }; + const params = { + cpModelConfig: { searchMenuTypes }, + cpProps: { fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + await cpHelpers.toggleAddCustomGroup(controlPanel); + + const optionEls = controlPanel.el.querySelectorAll('div.o_generator_menu select.o_group_by_selector option'); + assert.strictEqual(optionEls.length, 1); + assert.strictEqual(optionEls[0].innerText.trim(), "Foo"); + + controlPanel.destroy(); + }); + + QUnit.test('add a date field in "Add Custome Group" activate a groupby with global default option "month"', async function (assert) { + assert.expect(4); + + const fields = { + date_field: { string: "Date", type: "date", store: true, sortable: true }, + id: { sortable: true, string: 'ID', type: 'integer' } + }; + const params = { + cpModelConfig: { fields, searchMenuTypes }, + cpProps: { fields, searchMenuTypes }, + search: function (searchQuery) { + const { groupBy } = searchQuery; + assert.deepEqual(groupBy, ['date_field:month']); + } + }; + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + await cpHelpers.toggleAddCustomGroup(controlPanel); + await cpHelpers.applyGroup(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Month']); + + assert.ok(cpHelpers.isItemSelected(controlPanel, "Date")); + await cpHelpers.toggleMenuItem(controlPanel, "Date"); + assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", "Month")); + + controlPanel.destroy(); + }); + + QUnit.test('default groupbys can be ordered', async function (assert) { + assert.expect(2); + + const arch = ` + + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_birthday: 2, search_default_date: 1 } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + + const controlPanel = await createControlPanel(params); + + // the defautl groupbys should be activated in the right order + const { groupBy } = controlPanel.getQuery(); + assert.deepEqual(groupBy, ['date_field:week', 'birthday:month']); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Week>Birthday: Month']); + + controlPanel.destroy(); + }); + + QUnit.test('a separator in groupbys does not cause problems', async function (assert) { + assert.expect(23); + + const arch = ` + + + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + + const controlPanel = await createControlPanel(params); + + await cpHelpers.toggleGroupByMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + await cpHelpers.toggleMenuItemOption(controlPanel, 0, 4); + + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 1)); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day']); + + await cpHelpers.toggleMenuItem(controlPanel, 1); + await cpHelpers.toggleMenuItem(controlPanel, 0); + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + assert.ok(cpHelpers.isItemSelected(controlPanel, 1)); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day>Bar']); + + await cpHelpers.toggleMenuItemOption(controlPanel, 0, 1); + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + assert.ok(cpHelpers.isItemSelected(controlPanel, 1)); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected'); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day>Bar']); + + await cpHelpers.toggleMenuItem(controlPanel, 1); + await cpHelpers.toggleMenuItem(controlPanel, 0); + assert.ok(cpHelpers.isItemSelected(controlPanel, 0)); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 1)); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected'); + assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected'); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day']); + + await cpHelpers.removeFacet(controlPanel); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + await cpHelpers.toggleGroupByMenu(controlPanel); + await cpHelpers.toggleMenuItem(controlPanel, 0); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 0)); + assert.notOk(cpHelpers.isItemSelected(controlPanel, 1)); + assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected'); + assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected'); + + controlPanel.destroy(); + }); + + QUnit.test('falsy search default groupbys are not activated', async function (assert) { + assert.expect(2); + + const arch = ` + + + + `; + const params = { + cpModelConfig: { + arch, + fields: this.fields, + searchMenuTypes, + context: { search_default_birthday: false, search_default_foo: 0 } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + + const controlPanel = await createControlPanel(params); + const { groupBy } = controlPanel.getQuery(); + assert.deepEqual(groupBy, []); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + controlPanel.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/search_bar_tests.js b/addons/web/static/tests/control_panel/search_bar_tests.js new file mode 100644 index 00000000..9552173f --- /dev/null +++ b/addons/web/static/tests/control_panel/search_bar_tests.js @@ -0,0 +1,702 @@ +odoo.define('web.search_bar_tests', function (require) { + "use strict"; + + const { Model } = require('web/static/src/js/model.js'); + const Registry = require("web.Registry"); + const SearchBar = require('web.SearchBar'); + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createActionManager, createComponent } = testUtils; + + QUnit.module('Components', { + beforeEach: function () { + this.data = { + partner: { + fields: { + bar: { string: "Bar", type: 'many2one', relation: 'partner' }, + birthday: { string: "Birthday", type: 'date' }, + birth_datetime: { string: "Birth DateTime", type: 'datetime' }, + foo: { string: "Foo", type: 'char' }, + bool: { string: "Bool", type: 'boolean' }, + }, + records: [ + { id: 1, display_name: "First record", foo: "yop", bar: 2, bool: true, birthday: '1983-07-15', birth_datetime: '1983-07-15 01:00:00' }, + { id: 2, display_name: "Second record", foo: "blip", bar: 1, bool: false, birthday: '1982-06-04', birth_datetime: '1982-06-04 02:00:00' }, + { id: 3, display_name: "Third record", foo: "gnap", bar: 1, bool: false, birthday: '1985-09-13', birth_datetime: '1985-09-13 03:00:00' }, + { id: 4, display_name: "Fourth record", foo: "plop", bar: 2, bool: true, birthday: '1983-05-05', birth_datetime: '1983-05-05 04:00:00' }, + { id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, bool: true, birthday: '1800-01-01', birth_datetime: '1800-01-01 05:00:00' }, + ], + }, + }; + + this.actions = [{ + id: 1, + name: "Partners Action", + res_model: 'partner', + search_view_id: [false, 'search'], + type: 'ir.actions.act_window', + views: [[false, 'list']], + }]; + + this.archs = { + 'partner,false,list': ` + + + `, + 'partner,false,search': ` + + + + + + + + `, + }; + }, + }, function () { + + QUnit.module('SearchBar'); + + QUnit.test('basic rendering', async function (assert) { + assert.expect(1); + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(1); + + assert.strictEqual(document.activeElement, + actionManager.el.querySelector('.o_searchview input.o_searchview_input'), + "searchview input should be focused"); + + actionManager.destroy(); + }); + + QUnit.test('navigation with facets', async function (assert) { + assert.expect(4); + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(1); + + // add a facet + await cpHelpers.toggleGroupByMenu(actionManager); + await cpHelpers.toggleMenuItem(actionManager, 0); + await cpHelpers.toggleMenuItemOption(actionManager, 0, 0); + assert.containsOnce(actionManager, '.o_searchview .o_searchview_facet', + "there should be one facet"); + assert.strictEqual(document.activeElement, + actionManager.el.querySelector('.o_searchview input.o_searchview_input')); + + // press left to focus the facet + await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowLeft' }); + assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview .o_searchview_facet')); + + // press right to focus the input + await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowRight' }); + assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input')); + + actionManager.destroy(); + }); + + QUnit.test('search date and datetime fields. Support of timezones', async function (assert) { + assert.expect(4); + + let searchReadCount = 0; + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + session: { + getTZOffset() { + return 360; + } + }, + async mockRPC(route, args) { + if (route === '/web/dataset/search_read') { + switch (searchReadCount) { + case 0: + // Done on loading + break; + case 1: + assert.deepEqual(args.domain, [["birthday", "=", "1983-07-15"]], + "A date should stay what the user has input, but transmitted in server's format"); + break; + case 2: + // Done on closing the first facet + break; + case 3: + assert.deepEqual(args.domain, [["birth_datetime", "=", "1983-07-14 18:00:00"]], + "A datetime should be transformed in UTC and transmitted in server's format"); + break; + } + searchReadCount++; + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + // Date case + let searchInput = actionManager.el.querySelector('.o_searchview_input'); + await testUtils.fields.editInput(searchInput, '07/15/1983'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.strictEqual(actionManager.el.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(), + '07/15/1983', + 'The format of the date in the facet should be in locale'); + + // Close Facet + await testUtils.dom.click($('.o_searchview_facet .o_facet_remove')); + + // DateTime case + searchInput = actionManager.el.querySelector('.o_searchview_input'); + await testUtils.fields.editInput(searchInput, '07/15/1983 00:00:00'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.strictEqual(actionManager.el.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(), + '07/15/1983 00:00:00', + 'The format of the datetime in the facet should be in locale'); + + actionManager.destroy(); + }); + + QUnit.test("autocomplete menu clickout interactions", async function (assert) { + assert.expect(9); + + const fields = this.data.partner.fields; + + class TestModelExtension extends Model.Extension { + get(property) { + switch (property) { + case 'facets': + return []; + case 'filters': + return Object.keys(fields).map((fname, index) => Object.assign({ + description: fields[fname].string, + fieldName: fname, + fieldType: fields[fname].type, + id: index, + }, fields[fname])); + default: + break; + } + } + } + class MockedModel extends Model { } + MockedModel.registry = new Registry({ Test: TestModelExtension, }); + const searchModel = new MockedModel({ Test: {} }); + const searchBar = await createComponent(SearchBar, { + data: this.data, + env: { searchModel }, + props: { fields }, + }); + const input = searchBar.el.querySelector('.o_searchview_input'); + + assert.containsNone(searchBar, '.o_searchview_autocomplete'); + + await testUtils.controlPanel.editSearch(searchBar, "Hello there"); + + assert.strictEqual(input.value, "Hello there", "input value should be updated"); + assert.containsOnce(searchBar, '.o_searchview_autocomplete'); + + await testUtils.dom.triggerEvent(input, 'keydown', { key: 'Escape' }); + + assert.strictEqual(input.value, "", "input value should be empty"); + assert.containsNone(searchBar, '.o_searchview_autocomplete'); + + await testUtils.controlPanel.editSearch(searchBar, "General Kenobi"); + + assert.strictEqual(input.value, "General Kenobi", "input value should be updated"); + assert.containsOnce(searchBar, '.o_searchview_autocomplete'); + + await testUtils.dom.click(document.body); + + assert.strictEqual(input.value, "", "input value should be empty"); + assert.containsNone(searchBar, '.o_searchview_autocomplete'); + + searchBar.destroy(); + }); + + QUnit.test('select an autocomplete field', async function (assert) { + assert.expect(3); + + let searchReadCount = 0; + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + async mockRPC(route, args) { + if (route === '/web/dataset/search_read') { + switch (searchReadCount) { + case 0: + // Done on loading + break; + case 1: + assert.deepEqual(args.domain, [["foo", "ilike", "a"]]); + break; + } + searchReadCount++; + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + await testUtils.fields.editInput(searchInput, 'a'); + assert.containsN(actionManager, '.o_searchview_autocomplete li', 2, + "there should be 2 result for 'a' in search bar autocomplete"); + + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(), + "a", "There should be a field facet with label 'a'"); + + actionManager.destroy(); + }); + + QUnit.test('select an autocomplete field with `context` key', async function (assert) { + assert.expect(9); + + let searchReadCount = 0; + const firstLoading = testUtils.makeTestPromise(); + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + async mockRPC(route, args) { + if (route === '/web/dataset/search_read') { + switch (searchReadCount) { + case 0: + firstLoading.resolve(); + break; + case 1: + assert.deepEqual(args.domain, [["bar", "=", 1]]); + assert.deepEqual(args.context.bar, [1]); + break; + case 2: + assert.deepEqual(args.domain, ["|", ["bar", "=", 1], ["bar", "=", 2]]); + assert.deepEqual(args.context.bar, [1, 2]); + break; + } + searchReadCount++; + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + await firstLoading; + assert.strictEqual(searchReadCount, 1, "there should be 1 search_read"); + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + + // 'r' key to filter on bar "First Record" + await testUtils.fields.editInput(searchInput, 'record'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(), + "First record", + "the autocompletion facet should be correct"); + assert.strictEqual(searchReadCount, 2, "there should be 2 search_read"); + + // 'r' key to filter on bar "Second Record" + await testUtils.fields.editInput(searchInput, 'record'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(), + "First recordorSecond record", + "the autocompletion facet should be correct"); + assert.strictEqual(searchReadCount, 3, "there should be 3 search_read"); + + actionManager.destroy(); + }); + + QUnit.test('no search text triggers a reload', async function (assert) { + assert.expect(2); + + // Switch to pivot to ensure that the event comes from the control panel + // (pivot does not have a handler on "reload" event). + this.actions[0].views = [[false, 'pivot']]; + this.archs['partner,false,pivot'] = ` + + + `; + + let rpcs; + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function () { + rpcs++; + return this._super.apply(this, arguments); + }, + }); + await actionManager.doAction(1); + + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + rpcs = 0; + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.containsNone(actionManager, '.o_searchview_facet_label'); + assert.strictEqual(rpcs, 2, "should have reloaded"); + + actionManager.destroy(); + }); + + QUnit.test('selecting (no result) triggers a re-render', async function (assert) { + assert.expect(3); + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(1); + + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + + // 'a' key to filter nothing on bar + await testUtils.fields.editInput(searchInput, 'hello there'); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' }); + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' }); + + assert.strictEqual(actionManager.el.querySelector('.o_searchview_autocomplete .o_selection_focus').innerText.trim(), "(no result)", + "there should be no result for 'a' in bar"); + + await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' }); + + assert.containsNone(actionManager, '.o_searchview_facet_label'); + assert.strictEqual(actionManager.el.querySelector('.o_searchview_input').value, "", + "the search input should be re-rendered"); + + actionManager.destroy(); + }); + + QUnit.test('update suggested filters in autocomplete menu with Japanese IME', async function (assert) { + assert.expect(4); + + // The goal here is to simulate as many events happening during an IME + // assisted composition session as possible. Some of these events are + // not handled but are triggered to ensure they do not interfere. + const TEST = "TEST"; + const テスト = "テスト"; + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + await actionManager.doAction(1); + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + + // Simulate typing "TEST" on search view. + for (let i = 0; i < TEST.length; i++) { + const key = TEST[i].toUpperCase(); + await testUtils.dom.triggerEvent(searchInput, 'keydown', + { key, isComposing: true }); + if (i === 0) { + // Composition is initiated after the first keydown + await testUtils.dom.triggerEvent(searchInput, 'compositionstart'); + } + await testUtils.dom.triggerEvent(searchInput, 'keypress', + { key, isComposing: true }); + searchInput.value = TEST.slice(0, i + 1); + await testUtils.dom.triggerEvent(searchInput, 'keyup', + { key, isComposing: true }); + await testUtils.dom.triggerEvent(searchInput, 'input', + { inputType: 'insertCompositionText', isComposing: true }); + } + assert.containsOnce(actionManager.el, '.o_searchview_autocomplete', + "should display autocomplete dropdown menu on typing something in search view" + ); + assert.strictEqual( + actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(), + "Search Foo for: TEST", + `1st filter suggestion should be based on typed word "TEST"` + ); + + // Simulate soft-selection of another suggestion from IME through keyboard navigation. + await testUtils.dom.triggerEvent(searchInput, 'keydown', + { key: 'ArrowDown', isComposing: true }); + await testUtils.dom.triggerEvent(searchInput, 'keypress', + { key: 'ArrowDown', isComposing: true }); + searchInput.value = テスト; + await testUtils.dom.triggerEvent(searchInput, 'keyup', + { key: 'ArrowDown', isComposing: true }); + await testUtils.dom.triggerEvent(searchInput, 'input', + { inputType: 'insertCompositionText', isComposing: true }); + + assert.strictEqual( + actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(), + "Search Foo for: テスト", + `1st filter suggestion should be updated with soft-selection typed word "テスト"` + ); + + // Simulate selection on suggestion item "TEST" from IME. + await testUtils.dom.triggerEvent(searchInput, 'keydown', + { key: 'Enter', isComposing: true }); + await testUtils.dom.triggerEvent(searchInput, 'keypress', + { key: 'Enter', isComposing: true }); + searchInput.value = TEST; + await testUtils.dom.triggerEvent(searchInput, 'keyup', + { key: 'Enter', isComposing: true }); + await testUtils.dom.triggerEvent(searchInput, 'input', + { inputType: 'insertCompositionText', isComposing: true }); + + // End of the composition + await testUtils.dom.triggerEvent(searchInput, 'compositionend'); + + assert.strictEqual( + actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(), + "Search Foo for: TEST", + `1st filter suggestion should finally be updated with click selection on word "TEST" from IME` + ); + + actionManager.destroy(); + }); + + QUnit.test('open search view autocomplete on paste value using mouse', async function (assert) { + assert.expect(1); + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(1); + // Simulate paste text through the mouse. + const searchInput = actionManager.el.querySelector('.o_searchview_input'); + searchInput.value = "ABC"; + await testUtils.dom.triggerEvent(searchInput, 'input', + { inputType: 'insertFromPaste' }); + await testUtils.nextTick(); + assert.containsOnce(actionManager, '.o_searchview_autocomplete', + "should display autocomplete dropdown menu on paste in search view"); + + actionManager.destroy(); + }); + + QUnit.test('select autocompleted many2one', async function (assert) { + assert.expect(5); + + const archs = Object.assign({}, this.archs, { + 'partner,false,search': ` + + + + + + `, + }); + const actionManager = await createActionManager({ + actions: this.actions, + archs, + data: this.data, + async mockRPC(route, { domain }) { + if (route === '/web/dataset/search_read') { + assert.step(JSON.stringify(domain)); + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + await cpHelpers.editSearch(actionManager, "rec"); + await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child')); + + await cpHelpers.removeFacet(actionManager, 0); + + await cpHelpers.editSearch(actionManager, "rec"); + await testUtils.dom.click(actionManager.el.querySelector('.o_expand')); + await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_menu_item.o_indent')); + + assert.verifySteps([ + '[]', + '[["bar","child_of","rec"]]', // Incomplete string -> Name search + '[]', + '[["bar","child_of",1]]', // Suggestion select -> Specific ID + ]); + + actionManager.destroy(); + }); + + QUnit.test('"null" as autocomplete value', async function (assert) { + assert.expect(4); + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC(route, args) { + if (route === '/web/dataset/search_read') { + assert.step(JSON.stringify(args.domain)); + } + return this._super(...arguments); + }, + }); + + await actionManager.doAction(1); + + await cpHelpers.editSearch(actionManager, "null"); + + assert.strictEqual(actionManager.$('.o_searchview_autocomplete .o_selection_focus').text(), + "Search Foo for: null"); + + await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_selection_focus a')); + + assert.verifySteps([ + JSON.stringify([]), // initial search + JSON.stringify([["foo", "ilike", "null"]]), + ]); + + actionManager.destroy(); + }); + + QUnit.test('autocompletion with a boolean field', async function (assert) { + assert.expect(9); + + this.archs['partner,false,search'] = ''; + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC(route, args) { + if (route === '/web/dataset/search_read') { + assert.step(JSON.stringify(args.domain)); + } + return this._super(...arguments); + }, + }); + + await actionManager.doAction(1); + + await cpHelpers.editSearch(actionManager, "y"); + + assert.containsN(actionManager, '.o_searchview_autocomplete li', 2); + assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "Yes"); + + // select "Yes" + await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child')); + + await cpHelpers.removeFacet(actionManager, 0); + + await cpHelpers.editSearch(actionManager, "No"); + + assert.containsN(actionManager, '.o_searchview_autocomplete li', 2); + assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "No"); + + // select "No" + await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child')); + + + assert.verifySteps([ + JSON.stringify([]), // initial search + JSON.stringify([["bool", "=", true]]), + JSON.stringify([]), + JSON.stringify([["bool", "=", false]]), + ]); + + actionManager.destroy(); + }); + + QUnit.test("reference fields are supported in search view", async function (assert) { + assert.expect(7); + + this.data.partner.fields.ref = { type: 'reference', string: "Reference" }; + this.data.partner.records.forEach((record, i) => { + record.ref = `ref${String(i).padStart(3, "0")}`; + }); + const archs = Object.assign({}, this.archs, { + 'partner,false,search': ` + + + `, + }); + const actionManager = await createActionManager({ + actions: this.actions, + archs, + data: this.data, + async mockRPC(route, { domain }) { + if (route === '/web/dataset/search_read') { + assert.step(JSON.stringify(domain)); + } + return this._super(...arguments); + + } + }); + await actionManager.doAction(1); + + await cpHelpers.editSearch(actionManager, "ref"); + await cpHelpers.validateSearch(actionManager); + + assert.containsN(actionManager, ".o_data_row", 5); + + await cpHelpers.removeFacet(actionManager, 0); + await cpHelpers.editSearch(actionManager, "ref002"); + await cpHelpers.validateSearch(actionManager); + + assert.containsOnce(actionManager, ".o_data_row"); + + assert.verifySteps([ + '[]', + '[["ref","ilike","ref"]]', + '[]', + '[["ref","ilike","ref002"]]', + ]); + + actionManager.destroy(); + }); + + QUnit.test('focus should be on search bar when switching between views', async function (assert) { + assert.expect(4); + + this.actions[0].views = [[false, 'list'], [false, 'form']]; + this.archs['partner,false,form'] = ` +
+ + + +
`; + + const actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + await actionManager.doAction(1); + + assert.containsOnce(actionManager, '.o_list_view'); + assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'), + "searchview should have focus"); + + await testUtils.dom.click(actionManager.$('.o_list_view .o_data_cell:first')); + assert.containsOnce(actionManager, '.o_form_view'); + await testUtils.dom.click(actionManager.$('.o_back_button')); + assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'), + "searchview should have focus"); + + actionManager.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/search_utils_tests.js b/addons/web/static/tests/control_panel/search_utils_tests.js new file mode 100644 index 00000000..ab7ef429 --- /dev/null +++ b/addons/web/static/tests/control_panel/search_utils_tests.js @@ -0,0 +1,362 @@ +odoo.define('web.search_utils_tests', function (require) { + "use strict"; + + const { constructDateDomain } = require('web.searchUtils'); + const testUtils = require('web.test_utils'); + const { _t } = require('web.core'); + + const patchDate = testUtils.mock.patchDate; + + QUnit.module('SearchUtils', function () { + + QUnit.module('Construct domain'); + + QUnit.test('construct simple domain based on date field (no comparisonOptionId)', function (assert) { + assert.expect(4); + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', []), + { + domain: "[]", + description: "", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "June 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "Q2 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year']), + { + domain: `["&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-12-31"]]`, + description: "2020", + } + ); + unpatchDate(); + }); + + QUnit.test('construct simple domain based on datetime field (no comparisonOptionId)', function (assert) { + assert.expect(3); + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-06-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`, + description: "June 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`, + description: "Q2 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year']), + { + domain: `["&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-12-31 23:59:59"]]`, + description: "2020", + } + ); + unpatchDate(); + }); + + QUnit.test('construct domain based on date field (no comparisonOptionId)', function (assert) { + assert.expect(3); + const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` + + `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-03-31"]` + + "]", + description: "January 2020/Q1 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2019-04-01"], ["date_field", "<=", "2019-06-30"], ` + + `"&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]` + + "]", + description: "Q2 2019/Q2 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'this_month', 'antepenultimate_month']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` + + `"&", ["date_field", ">=", "2020-11-01"], ["date_field", "<=", "2020-11-30"]` + + "]", + description: "January 2020/November 2020", + } + ); + unpatchDate(); + }); + + QUnit.test('construct domain based on datetime field (no comparisonOptionId)', function (assert) { + assert.expect(3); + const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` + + `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-03-31 23:59:59"]` + + "]", + description: "January 2020/Q1 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]` + + "]", + description: "Q2 2019/Q2 2020", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'this_month', 'antepenultimate_month']), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` + + `"&", ["date_field", ">=", "2020-11-01 00:00:00"], ["date_field", "<=", "2020-11-30 23:59:59"]` + + "]", + description: "January 2020/November 2020", + } + ); + unpatchDate(); + }); + + QUnit.test('construct comparison domain based on date field and option "previous_period"', function (assert) { + assert.expect(5); + const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year'], 'previous_period'), + { + domain: "[" + + `"|", "|", ` + + `"&", ["date_field", ">=", "2019-10-01"], ["date_field", "<=", "2019-10-31"], ` + + `"&", ["date_field", ">=", "2019-11-01"], ["date_field", "<=", "2019-11-30"], ` + + `"&", ["date_field", ">=", "2019-12-01"], ["date_field", "<=", "2019-12-31"]` + + "]", + description: "October 2019/November 2019/December 2019", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year'], 'previous_period'), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-03-31"], ` + + `"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` + + "]", + description: "Q1 2018/Q1 2019", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_period'), + { + domain: "[" + + `"|", "|", "|", ` + + `"&", ["date_field", ">=", "2015-02-01"], ["date_field", "<=", "2015-02-28"], ` + + `"&", ["date_field", ">=", "2015-12-01"], ["date_field", "<=", "2015-12-31"], ` + + `"&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"], ` + + `"&", ["date_field", ">=", "2017-12-01"], ["date_field", "<=", "2017-12-31"]` + + "]", + description: "February 2015/December 2015/February 2017/December 2017", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'last_year'], 'previous_period'), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"], ` + + `"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]` + + "]", + description: "2017/2018", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'third_quarter', 'last_year'], 'previous_period'), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2018-10-01"], ["date_field", "<=", "2018-12-31"], ` + + `"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` + + "]", + description: "Q4 2018/Q1 2019", + } + ); + unpatchDate(); + }); + + QUnit.test('construct comparison domain based on datetime field and option "previous_year"', function (assert) { + assert.expect(3); + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().utc(); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year'], 'previous_year'), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2019-01-01 00:00:00"], ["date_field", "<=", "2019-03-31 23:59:59"]` + + "]", + description: "June 2019/Q1 2019", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year'], 'previous_year'), + { + domain: "[" + + `"|", ` + + `"&", ["date_field", ">=", "2018-04-01 00:00:00"], ["date_field", "<=", "2018-06-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` + + "]", + description: "Q2 2018/Q2 2019", + } + ); + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_year'), + { + domain: "[" + + `"|", "|", "|", ` + + `"&", ["date_field", ">=", "2017-04-01 00:00:00"], ["date_field", "<=", "2017-04-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2017-06-01 00:00:00"], ["date_field", "<=", "2017-06-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-04-30 23:59:59"], ` + + `"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` + + "]", + description: "April 2017/June 2017/April 2019/June 2019", + } + ); + unpatchDate(); + }); + + QUnit.module('Options translation'); + + QUnit.test("Quarter option: custom translation", async function (assert) { + assert.expect(1); + + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().locale('en'); + testUtils.mock.patch(_t.database.db, { + "Q2": "Deuxième trimestre de l'an de grâce", + }); + + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "Deuxième trimestre de l'an de grâce 2020", + }, + "Quarter term should be translated" + ); + + unpatchDate(); + testUtils.mock.unpatch(_t.database.db); + }); + + QUnit.test("Quarter option: right to left", async function (assert) { + assert.expect(1); + + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().locale('en'); + testUtils.mock.patch(_t.database.parameters, { + direction: "rtl", + }); + + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "2020 Q2", + }, + "Notation should be right to left" + ); + + unpatchDate(); + testUtils.mock.unpatch(_t.database.parameters); + }); + + QUnit.test("Quarter option: custom translation and right to left", async function (assert) { + assert.expect(1); + + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const referenceMoment = moment().locale('en'); + testUtils.mock.patch(_t.database.db, { + "Q2": "2e Trimestre", + }); + testUtils.mock.patch(_t.database.parameters, { + direction: "rtl", + }); + + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "2020 2e Trimestre", + }, + "Quarter term should be translated and notation should be right to left" + ); + + unpatchDate(); + testUtils.mock.unpatch(_t.database.db); + testUtils.mock.unpatch(_t.database.parameters); + }); + + QUnit.test("Moment.js localization does not affect formatted domain dates", async function (assert) { + assert.expect(1); + + const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0); + const initialLocale = moment.locale(); + moment.defineLocale('addoneForTest', { + postformat: function (string) { + return string.replace(/\d/g, match => (1 + parseInt(match)) % 10); + } + }); + const referenceMoment = moment().locale('addoneForTest'); + + assert.deepEqual( + constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']), + { + domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`, + description: "June 3131", + }, + "Numbers in domain should not use addoneForTest locale" + ); + + moment.locale(initialLocale); + moment.updateLocale("addoneForTest", null); + unpatchDate(); + }); + }); +}); diff --git a/addons/web/static/tests/core/ajax_tests.js b/addons/web/static/tests/core/ajax_tests.js new file mode 100644 index 00000000..f58d7368 --- /dev/null +++ b/addons/web/static/tests/core/ajax_tests.js @@ -0,0 +1,35 @@ +odoo.define('web.ajax_tests', function (require) { +"use strict"; + +var ajax = require('web.ajax'); + +QUnit.module('core', function () { + + var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css'; + var test_link_selector = 'link[href="' + test_css_url + '"]'; + + QUnit.module('ajax', { + beforeEach: function () { + $(test_link_selector).remove(); + }, + afterEach: function () { + $(test_link_selector).remove(); + } + }); + + QUnit.test('loadCSS', function (assert) { + var done = assert.async(); + assert.expect(2); + ajax.loadCSS(test_css_url).then(function () { + var $links = $(test_link_selector); + assert.strictEqual($links.length, 1, "The css should be added to the dom."); + ajax.loadCSS(test_css_url).then(function () { + var $links = $(test_link_selector); + assert.strictEqual($links.length, 1, "The css should have been added only once."); + done(); + }); + }); + }); +}); + +}); diff --git a/addons/web/static/tests/core/class_tests.js b/addons/web/static/tests/core/class_tests.js new file mode 100644 index 00000000..438b137e --- /dev/null +++ b/addons/web/static/tests/core/class_tests.js @@ -0,0 +1,168 @@ +odoo.define('web.class_tests', function (require) { +"use strict"; + +var Class = require('web.Class'); + +QUnit.module('core', {}, function () { + + QUnit.module('Class'); + + + QUnit.test('Basic class creation', function (assert) { + assert.expect(2); + + var C = Class.extend({ + foo: function () { + return this.somevar; + } + }); + var i = new C(); + i.somevar = 3; + + assert.ok(i instanceof C); + assert.strictEqual(i.foo(), 3); + }); + + QUnit.test('Class initialization', function (assert) { + assert.expect(2); + + var C1 = Class.extend({ + init: function () { + this.foo = 3; + } + }); + var C2 = Class.extend({ + init: function (arg) { + this.foo = arg; + } + }); + + var i1 = new C1(), + i2 = new C2(42); + + assert.strictEqual(i1.foo, 3); + assert.strictEqual(i2.foo, 42); + }); + + QUnit.test('Inheritance', function (assert) { + assert.expect(3); + + var C0 = Class.extend({ + foo: function () { + return 1; + } + }); + var C1 = C0.extend({ + foo: function () { + return 1 + this._super(); + } + }); + var C2 = C1.extend({ + foo: function () { + return 1 + this._super(); + } + }); + + assert.strictEqual(new C0().foo(), 1); + assert.strictEqual(new C1().foo(), 2); + assert.strictEqual(new C2().foo(), 3); + }); + + QUnit.test('In-place extension', function (assert) { + assert.expect(4); + + var C0 = Class.extend({ + foo: function () { + return 3; + }, + qux: function () { + return 3; + }, + bar: 3 + }); + + C0.include({ + foo: function () { + return 5; + }, + qux: function () { + return 2 + this._super(); + }, + bar: 5, + baz: 5 + }); + + assert.strictEqual(new C0().bar, 5); + assert.strictEqual(new C0().baz, 5); + assert.strictEqual(new C0().foo(), 5); + assert.strictEqual(new C0().qux(), 5); + }); + + QUnit.test('In-place extension and inheritance', function (assert) { + assert.expect(4); + + var C0 = Class.extend({ + foo: function () { return 1; }, + bar: function () { return 1; } + }); + var C1 = C0.extend({ + foo: function () { return 1 + this._super(); } + }); + assert.strictEqual(new C1().foo(), 2); + assert.strictEqual(new C1().bar(), 1); + + C1.include({ + foo: function () { return 2 + this._super(); }, + bar: function () { return 1 + this._super(); } + }); + assert.strictEqual(new C1().foo(), 4); + assert.strictEqual(new C1().bar(), 2); + }); + + QUnit.test('In-place extensions alter existing instances', function (assert) { + assert.expect(4); + + var C0 = Class.extend({ + foo: function () { return 1; }, + bar: function () { return 1; } + }); + var i = new C0(); + assert.strictEqual(i.foo(), 1); + assert.strictEqual(i.bar(), 1); + + C0.include({ + foo: function () { return 2; }, + bar: function () { return 2 + this._super(); } + }); + assert.strictEqual(i.foo(), 2); + assert.strictEqual(i.bar(), 3); + }); + + QUnit.test('In-place extension of subclassed types', function (assert) { + assert.expect(3); + + var C0 = Class.extend({ + foo: function () { return 1; }, + bar: function () { return 1; } + }); + var C1 = C0.extend({ + foo: function () { return 1 + this._super(); }, + bar: function () { return 1 + this._super(); } + }); + var i = new C1(); + + assert.strictEqual(i.foo(), 2); + + C0.include({ + foo: function () { return 2; }, + bar: function () { return 2 + this._super(); } + }); + + assert.strictEqual(i.foo(), 3); + assert.strictEqual(i.bar(), 4); + }); + + +}); + +}); diff --git a/addons/web/static/tests/core/concurrency_tests.js b/addons/web/static/tests/core/concurrency_tests.js new file mode 100644 index 00000000..1e195fde --- /dev/null +++ b/addons/web/static/tests/core/concurrency_tests.js @@ -0,0 +1,576 @@ +odoo.define('web.concurrency_tests', function (require) { +"use strict"; + +var concurrency = require('web.concurrency'); +var testUtils = require('web.test_utils'); + +var makeTestPromise = testUtils.makeTestPromise; +var makeTestPromiseWithAssert = testUtils.makeTestPromiseWithAssert; + +QUnit.module('core', {}, function () { + + QUnit.module('concurrency'); + + QUnit.test('mutex: simple scheduling', async function (assert) { + assert.expect(5); + var mutex = new concurrency.Mutex(); + + var prom1 = makeTestPromiseWithAssert(assert, 'prom1'); + var prom2 = makeTestPromiseWithAssert(assert, 'prom2'); + + mutex.exec(function () { return prom1; }); + mutex.exec(function () { return prom2; }); + + assert.verifySteps([]); + + await prom1.resolve(); + + assert.verifySteps(['ok prom1']); + + await prom2.resolve(); + + assert.verifySteps(['ok prom2']); + }); + + QUnit.test('mutex: simpleScheduling2', async function (assert) { + assert.expect(5); + var mutex = new concurrency.Mutex(); + + var prom1 = makeTestPromiseWithAssert(assert, 'prom1'); + var prom2 = makeTestPromiseWithAssert(assert, 'prom2'); + + mutex.exec(function () { return prom1; }); + mutex.exec(function () { return prom2; }); + + assert.verifySteps([]); + + await prom2.resolve(); + + assert.verifySteps(['ok prom2']); + + await prom1.resolve(); + + assert.verifySteps(['ok prom1']); + }); + + QUnit.test('mutex: reject', async function (assert) { + assert.expect(7); + var mutex = new concurrency.Mutex(); + + var prom1 = makeTestPromiseWithAssert(assert, 'prom1'); + var prom2 = makeTestPromiseWithAssert(assert, 'prom2'); + var prom3 = makeTestPromiseWithAssert(assert, 'prom3'); + + mutex.exec(function () { return prom1; }).catch(function () {}); + mutex.exec(function () { return prom2; }).catch(function () {}); + mutex.exec(function () { return prom3; }).catch(function () {}); + + assert.verifySteps([]); + + prom1.resolve(); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok prom1']); + + prom2.catch(function () { + assert.verifySteps(['ko prom2']); + }); + prom2.reject({name: "sdkjfmqsjdfmsjkdfkljsdq"}); + await testUtils.nextMicrotaskTick(); + + prom3.resolve(); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok prom3']); + }); + + QUnit.test('mutex: getUnlockedDef checks', async function (assert) { + assert.expect(9); + + var mutex = new concurrency.Mutex(); + + var prom1 = makeTestPromiseWithAssert(assert, 'prom1'); + var prom2 = makeTestPromiseWithAssert(assert, 'prom2'); + + mutex.getUnlockedDef().then(function () { + assert.step('mutex unlocked (1)'); + }); + + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['mutex unlocked (1)']); + + mutex.exec(function () { return prom1; }); + await testUtils.nextMicrotaskTick(); + + mutex.getUnlockedDef().then(function () { + assert.step('mutex unlocked (2)'); + }); + + assert.verifySteps([]); + + mutex.exec(function () { return prom2; }); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps([]); + + await prom1.resolve(); + + assert.verifySteps(['ok prom1']); + + prom2.resolve(); + await testUtils.nextTick(); + + assert.verifySteps(['ok prom2', 'mutex unlocked (2)']); + }); + + QUnit.test('DropPrevious: basic usecase', async function (assert) { + assert.expect(4); + + var dp = new concurrency.DropPrevious(); + + var prom1 = makeTestPromise(assert, 'prom1'); + var prom2 = makeTestPromise(assert, 'prom2'); + + dp.add(prom1).then(() => assert.step('should not go here')) + .catch(()=> assert.step("rejected dp1")); + dp.add(prom2).then(() => assert.step("ok dp2")); + + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['rejected dp1']); + + prom2.resolve(); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok dp2']); + }); + + QUnit.test('DropPrevious: resolve first before last', async function (assert) { + assert.expect(4); + + var dp = new concurrency.DropPrevious(); + + var prom1 = makeTestPromise(assert, 'prom1'); + var prom2 = makeTestPromise(assert, 'prom2'); + + dp.add(prom1).then(() => assert.step('should not go here')) + .catch(()=> assert.step("rejected dp1")); + dp.add(prom2).then(() => assert.step("ok dp2")); + + + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['rejected dp1']); + + prom1.resolve(); + prom2.resolve(); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok dp2']); + }); + + QUnit.test('DropMisordered: resolve all correctly ordered, sync', async function (assert) { + assert.expect(1); + + var dm = new concurrency.DropMisordered(), + flag = false; + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + var r1 = dm.add(d1), + r2 = dm.add(d2); + + Promise.all([r1, r2]).then(function () { + flag = true; + }); + + d1.resolve(); + d2.resolve(); + await testUtils.nextTick(); + + assert.ok(flag); + }); + + QUnit.test("DropMisordered: don't resolve mis-ordered, sync", async function (assert) { + assert.expect(4); + + var dm = new concurrency.DropMisordered(), + done1 = false, + done2 = false, + fail1 = false, + fail2 = false; + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + dm.add(d1).then(function () { done1 = true; }) + .catch(function () { fail1 = true; }); + dm.add(d2).then(function () { done2 = true; }) + .catch(function () { fail2 = true; }); + + d2.resolve(); + d1.resolve(); + await testUtils.nextMicrotaskTick(); + + // d1 is in limbo + assert.ok(!done1); + assert.ok(!fail1); + + // d2 is fulfilled + assert.ok(done2); + assert.ok(!fail2); + }); + + QUnit.test('DropMisordered: fail mis-ordered flag, sync', async function (assert) { + assert.expect(4); + + var dm = new concurrency.DropMisordered(true/* failMisordered */), + done1 = false, + done2 = false, + fail1 = false, + fail2 = false; + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + dm.add(d1).then(function () { done1 = true; }) + .catch(function () { fail1 = true; }); + dm.add(d2).then(function () { done2 = true; }) + .catch(function () { fail2 = true; }); + + d2.resolve(); + d1.resolve(); + await testUtils.nextMicrotaskTick(); + + // d1 is in limbo + assert.ok(!done1); + assert.ok(fail1); + + // d2 is resolved + assert.ok(done2); + assert.ok(!fail2); + }); + + QUnit.test('DropMisordered: resolve all correctly ordered, async', function (assert) { + var done = assert.async(); + assert.expect(1); + + var dm = new concurrency.DropMisordered(); + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + var r1 = dm.add(d1), + r2 = dm.add(d2); + + setTimeout(function () { d1.resolve(); }, 10); + setTimeout(function () { d2.resolve(); }, 20); + + Promise.all([r1, r2]).then(function () { + assert.ok(true); + done(); + }); + }); + + QUnit.test("DropMisordered: don't resolve mis-ordered, async", function (assert) { + var done = assert.async(); + assert.expect(4); + + var dm = new concurrency.DropMisordered(), + done1 = false, done2 = false, + fail1 = false, fail2 = false; + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + dm.add(d1).then(function () { done1 = true; }) + .catch(function () { fail1 = true; }); + dm.add(d2).then(function () { done2 = true; }) + .catch(function () { fail2 = true; }); + + setTimeout(function () { d1.resolve(); }, 20); + setTimeout(function () { d2.resolve(); }, 10); + + setTimeout(function () { + // d1 is in limbo + assert.ok(!done1); + assert.ok(!fail1); + + // d2 is resolved + assert.ok(done2); + assert.ok(!fail2); + done(); + }, 30); + }); + + QUnit.test('DropMisordered: fail mis-ordered flag, async', function (assert) { + var done = assert.async(); + assert.expect(4); + + var dm = new concurrency.DropMisordered(true), + done1 = false, done2 = false, + fail1 = false, fail2 = false; + + var d1 = makeTestPromise(); + var d2 = makeTestPromise(); + + dm.add(d1).then(function () { done1 = true; }) + .catch(function () { fail1 = true; }); + dm.add(d2).then(function () { done2 = true; }) + .catch(function () { fail2 = true; }); + + setTimeout(function () { d1.resolve(); }, 20); + setTimeout(function () { d2.resolve(); }, 10); + + setTimeout(function () { + // d1 is failed + assert.ok(!done1); + assert.ok(fail1); + + // d2 is resolved + assert.ok(done2); + assert.ok(!fail2); + done(); + }, 30); + }); + + QUnit.test('MutexedDropPrevious: simple', async function (assert) { + assert.expect(5); + + var m = new concurrency.MutexedDropPrevious(); + var d1 = makeTestPromise(); + + d1.then(function () { + assert.step("d1 resolved"); + }); + m.exec(function () { return d1; }).then(function (result) { + assert.step("p1 done"); + assert.strictEqual(result, 'd1'); + }); + + assert.verifySteps([]); + d1.resolve('d1'); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(["d1 resolved","p1 done"]); + }); + + QUnit.test('MutexedDropPrevious: d2 arrives after d1 resolution', async function (assert) { + assert.expect(8); + + var m = new concurrency.MutexedDropPrevious(); + var d1 = makeTestPromiseWithAssert(assert, 'd1'); + + m.exec(function () { return d1; }).then(function () { + assert.step("p1 resolved"); + }); + + assert.verifySteps([]); + d1.resolve('d1'); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok d1','p1 resolved']); + + var d2 = makeTestPromiseWithAssert(assert, 'd2'); + m.exec(function () { return d2; }).then(function () { + assert.step("p2 resolved"); + }); + + assert.verifySteps([]); + d2.resolve('d2'); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok d2','p2 resolved']); + }); + + QUnit.test('MutexedDropPrevious: p1 does not return a deferred', async function (assert) { + assert.expect(7); + + var m = new concurrency.MutexedDropPrevious(); + + m.exec(function () { return 42; }).then(function () { + assert.step("p1 resolved"); + }); + + assert.verifySteps([]); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['p1 resolved']); + + var d2 = makeTestPromiseWithAssert(assert, 'd2'); + m.exec(function () { return d2; }).then(function () { + assert.step("p2 resolved"); + }); + + assert.verifySteps([]); + d2.resolve('d2'); + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['ok d2','p2 resolved']); + }); + + QUnit.test('MutexedDropPrevious: p2 arrives before p1 resolution', async function (assert) { + assert.expect(8); + + var m = new concurrency.MutexedDropPrevious(); + var d1 = makeTestPromiseWithAssert(assert, 'd1'); + + m.exec(function () { return d1; }).catch(function () { + assert.step("p1 rejected"); + }); + assert.verifySteps([]); + + var d2 = makeTestPromiseWithAssert(assert, 'd2'); + m.exec(function () { return d2; }).then(function () { + assert.step("p2 resolved"); + }); + + assert.verifySteps([]); + d1.resolve('d1'); + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['p1 rejected', 'ok d1']); + + d2.resolve('d2'); + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['ok d2', 'p2 resolved']); + }); + + QUnit.test('MutexedDropPrevious: 3 arrives before 2 initialization', async function (assert) { + assert.expect(10); + var m = new concurrency.MutexedDropPrevious(); + + var d1 = makeTestPromiseWithAssert(assert, 'd1'); + var d3 = makeTestPromiseWithAssert(assert, 'd3'); + + m.exec(function () { return d1; }).catch(function () { + assert.step('p1 rejected'); + }); + + m.exec(function () { + assert.ok(false, "should not execute this function"); + }).catch(function () { + assert.step('p2 rejected'); + }); + + m.exec(function () { return d3; }).then(function (result) { + assert.strictEqual(result, 'd3'); + assert.step('p3 resolved'); + }); + + assert.verifySteps([]); + + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['p1 rejected', 'p2 rejected']); + + d1.resolve('d1'); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok d1']); + + d3.resolve('d3'); + await testUtils.nextTick(); + + + assert.verifySteps(['ok d3','p3 resolved']); + }); + + QUnit.test('MutexedDropPrevious: 3 arrives after 2 initialization', async function (assert) { + assert.expect(15); + var m = new concurrency.MutexedDropPrevious(); + + var d1 = makeTestPromiseWithAssert(assert, 'd1'); + var d2 = makeTestPromiseWithAssert(assert, 'd2'); + var d3 = makeTestPromiseWithAssert(assert, 'd3'); + + m.exec(function () { + assert.step('execute d1'); + return d1; + }).catch(function () { + assert.step('p1 rejected'); + }); + + m.exec(function () { + assert.step('execute d2'); + return d2; + }).catch(function () { + assert.step('p2 rejected'); + }); + + assert.verifySteps(['execute d1']); + + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['p1 rejected']); + + d1.resolve('d1'); + await testUtils.nextMicrotaskTick(); + + assert.verifySteps(['ok d1', 'execute d2']); + + m.exec(function () { + assert.step('execute d3'); + return d3; + }).then(function () { + assert.step('p3 resolved'); + }); + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['p2 rejected']); + + d2.resolve(); + await testUtils.nextMicrotaskTick(); + assert.verifySteps(['ok d2', 'execute d3']); + + d3.resolve(); + await testUtils.nextTick(); + assert.verifySteps(['ok d3', 'p3 resolved']); + + }); + + QUnit.test('MutexedDropPrevious: 2 in then of 1 with 3', async function (assert) { + assert.expect(9); + + var m = new concurrency.MutexedDropPrevious(); + + var d1 = makeTestPromiseWithAssert(assert, 'd1'); + var d2 = makeTestPromiseWithAssert(assert, 'd2'); + var d3 = makeTestPromiseWithAssert(assert, 'd3'); + var p3; + + m.exec(function () { return d1; }) + .catch(function () { + assert.step('p1 rejected'); + p3 = m.exec(function () { + return d3; + }).then(function () { + assert.step('p3 resolved'); + }); + return p3; + }); + + await testUtils.nextTick(); + assert.verifySteps([]); + + m.exec(function () { + assert.ok(false, 'should not execute this function'); + return d2; + }).catch(function () { + assert.step('p2 rejected'); + }); + + await testUtils.nextTick(); + assert.verifySteps(['p1 rejected', 'p2 rejected']); + + d1.resolve('d1'); + await testUtils.nextTick(); + + assert.verifySteps(['ok d1']); + + d3.resolve('d3'); + await testUtils.nextTick(); + + assert.verifySteps(['ok d3', 'p3 resolved']); + }); + +}); + +}); diff --git a/addons/web/static/tests/core/data_comparison_utils_tests.js b/addons/web/static/tests/core/data_comparison_utils_tests.js new file mode 100644 index 00000000..f5058714 --- /dev/null +++ b/addons/web/static/tests/core/data_comparison_utils_tests.js @@ -0,0 +1,75 @@ +odoo.define('web.data_comparison_utils_tests', function(require) { +"use strict"; + +var dataComparisonUtils = require('web.dataComparisonUtils'); +var DateClasses = dataComparisonUtils.DateClasses; + +QUnit.module('dataComparisonUtils', function () { + + QUnit.module('DateClasses'); + + + QUnit.test('main parameters are correctly computed', function(assert) { + assert.expect(30); + + var dateClasses; + + dateClasses = new DateClasses([['2019']]); + assert.strictEqual(dateClasses.referenceIndex, 0); + assert.strictEqual(dateClasses.dateClass(0, '2019'), 0); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']); + + dateClasses = new DateClasses([['2018', '2019']]); + assert.strictEqual(dateClasses.referenceIndex, 0); + assert.strictEqual(dateClasses.dateClass(0, '2018'), 0); + assert.strictEqual(dateClasses.dateClass(0, '2019'), 1); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2018']); + assert.deepEqual(dateClasses.dateClassMembers(1), ['2019']); + + dateClasses = new DateClasses([['2019'], []]); + assert.strictEqual(dateClasses.referenceIndex, 0); + assert.strictEqual(dateClasses.dateClass(0, '2019'), 0); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']); + + dateClasses = new DateClasses([[], ['2019']]); + assert.strictEqual(dateClasses.referenceIndex, 1); + assert.strictEqual(dateClasses.dateClass(1, '2019'), 0); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2019']); + + dateClasses = new DateClasses([['2019'],['2018', '2019']]); + assert.strictEqual(dateClasses.referenceIndex, 0); + assert.strictEqual(dateClasses.dateClass(0, '2019'), 0); + assert.strictEqual(dateClasses.dateClass(1, '2018'), 0); + assert.strictEqual(dateClasses.dateClass(1, '2019'), 1); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2019', '2018']); + assert.deepEqual(dateClasses.dateClassMembers(1), ['2019']); + + + dateClasses = new DateClasses([['2019'], ['2017', '2018', '2020'], ['2017', '2019']]); + assert.strictEqual(dateClasses.referenceIndex, 0); + assert.strictEqual(dateClasses.dateClass(0, '2019'), 0); + assert.strictEqual(dateClasses.dateClass(1, '2017'), 0); + assert.strictEqual(dateClasses.dateClass(1, '2018'), 1); + assert.strictEqual(dateClasses.dateClass(1, '2020'), 2); + assert.strictEqual(dateClasses.dateClass(2, '2017'), 0); + assert.strictEqual(dateClasses.dateClass(2, '2019'), 1); + assert.deepEqual(dateClasses.dateClassMembers(0), ['2019', '2017']); + assert.deepEqual(dateClasses.dateClassMembers(1), ['2018', '2019']); + assert.deepEqual(dateClasses.dateClassMembers(2), ['2020']); + + + }); + + QUnit.test('two overlapping datesets and classes representatives', function(assert) { + assert.expect(4); + + var dateClasses = new DateClasses([['March 2017'], ['February 2017', 'March 2017']]); + + assert.strictEqual(dateClasses.representative(0, 0), 'March 2017'); + assert.strictEqual(dateClasses.representative(0, 1), 'February 2017'); + + assert.strictEqual(dateClasses.representative(1, 0), undefined); + assert.strictEqual(dateClasses.representative(1, 1), 'March 2017'); + }); +}); +}); diff --git a/addons/web/static/tests/core/dialog_tests.js b/addons/web/static/tests/core/dialog_tests.js new file mode 100644 index 00000000..3c83b2ba --- /dev/null +++ b/addons/web/static/tests/core/dialog_tests.js @@ -0,0 +1,173 @@ +odoo.define('web.dialog_tests', function (require) { +"use strict"; + +var Dialog = require('web.Dialog'); +var testUtils = require('web.test_utils'); +var Widget = require('web.Widget'); + +var ESCAPE_KEY = $.Event("keyup", { which: 27 }); + +async function createEmptyParent(debug) { + var widget = new Widget(); + + await testUtils.mock.addMockEnvironment(widget, { + debug: debug || false, + }); + return widget; +} + +QUnit.module('core', {}, function () { + + QUnit.module('Dialog'); + + QUnit.test("Closing custom dialog using buttons calls standard callback", async function (assert) { + assert.expect(3); + + var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback'); + var parent = await createEmptyParent(); + new Dialog(parent, { + buttons: [ + { + text: "Close", + classes: 'btn-primary', + close: true, + click: testPromise.resolve, + }, + ], + $content: $('
'), + onForceClose: testPromise.reject, + }).open(); + + assert.verifySteps([]); + + await testUtils.nextTick(); + await testUtils.dom.click($('.modal[role="dialog"] .btn-primary')); + + testPromise.then(() => { + assert.verifySteps(['ok custom callback']); + }); + + parent.destroy(); + }); + + QUnit.test("Closing custom dialog without using buttons calls force close callback", async function (assert) { + assert.expect(3); + + var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'custom callback'); + var parent = await createEmptyParent(); + new Dialog(parent, { + buttons: [ + { + text: "Close", + classes: 'btn-primary', + close: true, + click: testPromise.reject, + }, + ], + $content: $('
'), + onForceClose: testPromise.resolve, + }).open(); + + assert.verifySteps([]); + + await testUtils.nextTick(); + await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]); + + testPromise.then(() => { + assert.verifySteps(['ok custom callback']); + }); + + parent.destroy(); + }); + + QUnit.test("Closing confirm dialog without using buttons calls cancel callback", async function (assert) { + assert.expect(3); + + var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'confirm callback'); + var parent = await createEmptyParent(); + var options = { + confirm_callback: testPromise.reject, + cancel_callback: testPromise.resolve, + }; + Dialog.confirm(parent, "", options); + + assert.verifySteps([]); + + await testUtils.nextTick(); + await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]); + + testPromise.then(() => { + assert.verifySteps(['ok confirm callback']); + }); + + parent.destroy(); + }); + + QUnit.test("Closing alert dialog without using buttons calls confirm callback", async function (assert) { + assert.expect(3); + + var testPromise = testUtils.makeTestPromiseWithAssert(assert, 'alert callback'); + var parent = await createEmptyParent(); + var options = { + confirm_callback: testPromise.resolve, + }; + Dialog.alert(parent, "", options); + + assert.verifySteps([]); + + await testUtils.nextTick(); + await testUtils.dom.triggerEvents($('.modal[role="dialog"]'), [ESCAPE_KEY]); + + testPromise.then(() => { + assert.verifySteps(['ok alert callback']); + }); + + parent.destroy(); + }); + + QUnit.test("Ensure on_attach_callback and on_detach_callback are properly called", async function (assert) { + assert.expect(4); + + const TestDialog = Dialog.extend({ + on_attach_callback() { + assert.step('on_attach_callback'); + }, + on_detach_callback() { + assert.step('on_detach_callback'); + }, + }); + + const parent = await createEmptyParent(); + const dialog = new TestDialog(parent, { + buttons: [ + { + text: "Close", + classes: 'btn-primary', + close: true, + }, + ], + $content: $('
'), + }).open(); + + await dialog.opened(); + + assert.verifySteps(['on_attach_callback']); + + await testUtils.dom.click($('.modal[role="dialog"] .btn-primary')); + assert.verifySteps(['on_detach_callback']); + + parent.destroy(); + }); + + QUnit.test("Should not be displayed if parent is destroyed while dialog is being opened", async function (assert) { + assert.expect(1); + const parent = await createEmptyParent(); + const dialog = new Dialog(parent); + dialog.open(); + parent.destroy(); + await testUtils.nextTick(); + assert.containsNone(document.body, ".modal[role='dialog']"); + }); +}); + +}); diff --git a/addons/web/static/tests/core/dom_tests.js b/addons/web/static/tests/core/dom_tests.js new file mode 100644 index 00000000..c57ced52 --- /dev/null +++ b/addons/web/static/tests/core/dom_tests.js @@ -0,0 +1,133 @@ +odoo.define('web.dom_tests', function (require) { +"use strict"; + +var dom = require('web.dom'); +var testUtils = require('web.test_utils'); + +/** + * Create an autoresize text area with 'border-box' as box sizing rule. + * The minimum height of this autoresize text are is 1px. + * + * @param {Object} [options={}] + * @param {integer} [options.borderBottomWidth=0] + * @param {integer} [options.borderTopWidth=0] + * @param {integer} [options.padding=0] + */ +function prepareAutoresizeTextArea(options) { + options = options || {}; + var $textarea = $('