diff options
| author | Julien Mougenot <jum@odoo.com> | 2020-03-11 09:38:29 +0000 |
|---|---|---|
| committer | Julien Mougenot <jum@odoo.com> | 2020-03-13 14:08:33 +0000 |
| commit | bb8eb766a84b9fd06007d3fb4ab9a29b70097738 (patch) | |
| tree | f944ba5c9d0efcb2a9a881d8eeaef7ecc96e6245 /addons | |
| parent | e68d3bb09d1f844387ceb20aa76022ace48507d9 (diff) | |
[REF] web: Control panel tests
Use of the new helpers in control panel (and subcomponents) tests and addition
of new tests.
Part of task 2196029
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Diffstat (limited to 'addons')
15 files changed, 4081 insertions, 590 deletions
diff --git a/addons/web/static/tests/components/action_menus_tests.js b/addons/web/static/tests/components/action_menus_tests.js new file mode 100644 index 00000000000..e17ae95ba78 --- /dev/null +++ b/addons/web/static/tests/components/action_menus_tests.js @@ -0,0 +1,231 @@ +odoo.define('web.action_menus_tests', function (require) { + "use strict"; + + const ActionMenus = require('web.ActionMenus'); + const Registry = require('web.Registry'); + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createComponent } = testUtils; + + QUnit.module('Components', { + beforeEach() { + this.action = { + res_model: 'hobbit', + }; + this.view = { + // needed by google_drive module, makes sense to give a view anyway. + type: 'form', + }; + this.props = { + activeIds: [23], + context: {}, + items: { + action: [ + { action: { id: 1 }, name: "What's taters, precious ?", id: 1 }, + ], + print: [ + { action: { id: 2 }, name: "Po-ta-toes", id: 2 }, + ], + other: [ + { description: "Boil'em", callback() { } }, + { description: "Mash'em", callback() { } }, + { description: "Stick'em in a stew", url: '#stew' }, + ], + }, + }; + // Patch the registry of the action menus + this.actionMenusRegistry = ActionMenus.registry; + ActionMenus.registry = new Registry(); + }, + afterEach() { + ActionMenus.registry = this.actionMenusRegistry; + }, + }, function () { + + QUnit.module('ActionMenus'); + + QUnit.test('basic interactions', async function (assert) { + assert.expect(10); + + const actionMenus = await createComponent(ActionMenus, { + env: { + action: this.action, + view: this.view, + }, + props: this.props, + }); + + const dropdowns = actionMenus.el.getElementsByClassName('o_dropdown'); + assert.strictEqual(dropdowns.length, 2, "ActionMenus should contain 2 menus"); + assert.strictEqual(dropdowns[0].querySelector('.o_dropdown_title').innerText.trim(), "Print"); + assert.strictEqual(dropdowns[1].querySelector('.o_dropdown_title').innerText.trim(), "Action"); + assert.containsNone(actionMenus, '.o_dropdown_menu'); + + await cpHelpers.toggleActionMenu(actionMenus, "Action"); + + assert.containsOnce(actionMenus, '.o_dropdown_menu'); + assert.containsN(actionMenus, '.o_dropdown_menu .o_menu_item', 4); + const actionsTexts = [...dropdowns[1].querySelectorAll('.o_menu_item')].map(el => el.innerText.trim()); + assert.deepEqual(actionsTexts, [ + "Boil'em", + "Mash'em", + "Stick'em in a stew", + "What's taters, precious ?", + ], "callbacks should appear before actions"); + + await cpHelpers.toggleActionMenu(actionMenus, "Print"); + + assert.containsOnce(actionMenus, '.o_dropdown_menu'); + assert.containsN(actionMenus, '.o_dropdown_menu .o_menu_item', 1); + + await cpHelpers.toggleActionMenu(actionMenus, "Print"); + + assert.containsNone(actionMenus, '.o_dropdown_menu'); + + actionMenus.destroy(); + }); + + QUnit.test('execute action', async function (assert) { + assert.expect(4); + + const actionMenus = await createComponent(ActionMenus, { + env: { + action: this.action, + view: this.view, + }, + props: this.props, + intercepts: { + 'do-action': ev => assert.step('do-action'), + }, + async mockRPC(route, args) { + switch (route) { + case '/web/action/load': + const expectedContext = { + active_id: 23, + active_ids: [23], + active_model: 'hobbit', + }; + assert.deepEqual(args.context, expectedContext); + assert.step('load-action'); + return { context: {}, flags: {} }; + default: + return this._super(...arguments); + + } + }, + }); + + await cpHelpers.toggleActionMenu(actionMenus, "Action"); + await cpHelpers.toggleMenuItem(actionMenus, "What's taters, precious ?"); + + assert.verifySteps(['load-action', 'do-action']); + + actionMenus.destroy(); + }); + + QUnit.test('execute callback action', async function (assert) { + assert.expect(2); + + const callbackPromise = testUtils.makeTestPromise(); + this.props.items.other[0].callback = function (items) { + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0].description, "Boil'em"); + callbackPromise.resolve(); + }; + + const actionMenus = await createComponent(ActionMenus, { + env: { + action: this.action, + view: this.view, + }, + props: this.props, + async mockRPC(route, args) { + switch (route) { + case '/web/action/load': + throw new Error("No action should be loaded."); + default: + return this._super(...arguments); + } + }, + }); + + await cpHelpers.toggleActionMenu(actionMenus, "Action"); + await cpHelpers.toggleMenuItem(actionMenus, "Boil'em"); + + await callbackPromise; + + actionMenus.destroy(); + }); + + QUnit.test('execute print action', async function (assert) { + assert.expect(4); + + const actionMenus = await createComponent(ActionMenus, { + env: { + action: this.action, + view: this.view, + }, + intercepts: { + 'do-action': ev => assert.step('do-action'), + }, + props: this.props, + async mockRPC(route, args) { + switch (route) { + case '/web/action/load': + const expectedContext = { + active_id: 23, + active_ids: [23], + active_model: 'hobbit', + }; + assert.deepEqual(args.context, expectedContext); + assert.step('load-action'); + return { context: {}, flags: {} }; + default: + return this._super(...arguments); + + } + }, + }); + + await cpHelpers.toggleActionMenu(actionMenus, "Print"); + await cpHelpers.toggleMenuItem(actionMenus, "Po-ta-toes"); + + assert.verifySteps(['load-action', 'do-action']); + + actionMenus.destroy(); + }); + + QUnit.test('execute url action', async function (assert) { + assert.expect(2); + + const actionMenus = await createComponent(ActionMenus, { + env: { + action: this.action, + services: { + navigate(url) { + assert.step(url); + }, + }, + view: this.view, + }, + props: this.props, + async mockRPC(route, args) { + switch (route) { + case '/web/action/load': + throw new Error("No action should be loaded."); + default: + return this._super(...arguments); + } + }, + }); + + await cpHelpers.toggleActionMenu(actionMenus, "Action"); + await cpHelpers.toggleMenuItem(actionMenus, "Stick'em in a stew"); + + assert.verifySteps(['#stew']); + + actionMenus.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/components/datepicker_tests.js b/addons/web/static/tests/components/datepicker_tests.js new file mode 100644 index 00000000000..995521259a1 --- /dev/null +++ b/addons/web/static/tests/components/datepicker_tests.js @@ -0,0 +1,215 @@ +odoo.define('web.datepicker_tests', function (require) { + "use strict"; + + const { DatePicker, DateTimePicker } = require('web.DatePickerOwl'); + const testUtils = require('web.test_utils'); + + const { createComponent } = testUtils; + + QUnit.module('Components', {}, function () { + + QUnit.module('DatePicker'); + + QUnit.test("basic rendering", async function (assert) { + assert.expect(8); + + const picker = await createComponent(DatePicker, { + props: { date: moment('01/09/1997') }, + }); + + assert.containsOnce(picker, 'input.o_input.o_datepicker_input'); + assert.containsOnce(picker, 'span.o_datepicker_button'); + assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget'); + + const input = picker.el.querySelector('input.o_input.o_datepicker_input'); + assert.strictEqual(input.value, '01/09/1997', + "Value should be the one given") + ; + assert.strictEqual(input.dataset.target, `#${picker.el.id}`, + "DatePicker id should match its input target"); + + await testUtils.dom.click(input); + + assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker'); + assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget .timepicker'); + assert.strictEqual( + document.querySelector('.datepicker .day.active').dataset.day, + '01/09/1997', + "Datepicker should have set the correct day" + ); + + picker.destroy(); + }); + + QUnit.test("pick a date", async function (assert) { + assert.expect(5); + + const picker = await createComponent(DatePicker, { + props: { date: moment('01/09/1997') }, + intercepts: { + 'datetime-changed': ev => { + assert.step('datetime-changed'); + assert.strictEqual(ev.detail.date.format('MM/DD/YYYY'), '02/08/1997', + "Event should transmit the correct date"); + }, + } + }); + const input = picker.el.querySelector('.o_datepicker_input'); + + await testUtils.dom.click(input); + await testUtils.dom.click(document.querySelector('.datepicker th.next')); // next month + + assert.verifySteps([]); + + await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // previous day + + assert.strictEqual(input.value, '02/08/1997'); + assert.verifySteps(['datetime-changed']); + + picker.destroy(); + }); + + QUnit.test("enter a date value", async function (assert) { + assert.expect(5); + + const picker = await createComponent(DatePicker, { + props: { date: moment('01/09/1997') }, + intercepts: { + 'datetime-changed': ev => { + assert.step('datetime-changed'); + assert.strictEqual(ev.detail.date.format('MM/DD/YYYY'), '02/08/1997', + "Event should transmit the correct date"); + }, + } + }); + const input = picker.el.querySelector('.o_datepicker_input'); + + assert.verifySteps([]); + + await testUtils.fields.editAndTrigger(input, '02/08/1997', ['change']); + + assert.verifySteps(['datetime-changed']); + + await testUtils.dom.click(input); + + assert.strictEqual( + document.querySelector('.datepicker .day.active').dataset.day, + '02/08/1997', + "Datepicker should have set the correct day" + ); + + picker.destroy(); + }); + + QUnit.module('DateTimePicker'); + + QUnit.test("basic rendering", async function (assert) { + assert.expect(11); + + const picker = await createComponent(DateTimePicker, { + props: { date: moment('01/09/1997 12:30:01') }, + }); + + assert.containsOnce(picker, 'input.o_input.o_datepicker_input'); + assert.containsOnce(picker, 'span.o_datepicker_button'); + assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget'); + + const input = picker.el.querySelector('input.o_input.o_datepicker_input'); + assert.strictEqual(input.value, '01/09/1997 12:30:01', "Value should be the one given"); + assert.strictEqual(input.dataset.target, `#${picker.el.id}`, + "DateTimePicker id should match its input target"); + + await testUtils.dom.click(input); + + assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker'); + assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .timepicker'); + assert.strictEqual( + document.querySelector('.datepicker .day.active').dataset.day, + '01/09/1997', + "Datepicker should have set the correct day"); + + assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '12', + "Datepicker should have set the correct hour"); + assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '30', + "Datepicker should have set the correct minute"); + assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '01', + "Datepicker should have set the correct second"); + + picker.destroy(); + }); + + QUnit.test("pick a date and time", async function (assert) { + assert.expect(5); + + const picker = await createComponent(DateTimePicker, { + props: { date: moment('01/09/1997 12:30:01') }, + intercepts: { + 'datetime-changed': ev => { + assert.step('datetime-changed'); + assert.strictEqual(ev.detail.date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05', + "Event should transmit the correct date"); + }, + } + }); + const input = picker.el.querySelector('input.o_input.o_datepicker_input'); + + await testUtils.dom.click(input); + await testUtils.dom.click(document.querySelector('.datepicker th.next')); // February + await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // 08 + await testUtils.dom.click(document.querySelector('a[title="Select Time"]')); + await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour')); + await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h + await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute')); + await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m + await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second')); + + assert.verifySteps([]); + + await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s + + assert.strictEqual(input.value, '02/08/1997 15:45:05'); + assert.verifySteps(['datetime-changed']); + + picker.destroy(); + }); + + QUnit.test("enter a datetime value", async function (assert) { + assert.expect(9); + + const picker = await createComponent(DateTimePicker, { + props: { date: moment('01/09/1997 12:30:01') }, + intercepts: { + 'datetime-changed': ev => { + assert.step('datetime-changed'); + assert.strictEqual(ev.detail.date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05', + "Event should transmit the correct date"); + }, + } + }); + const input = picker.el.querySelector('.o_datepicker_input'); + + assert.verifySteps([]); + + await testUtils.fields.editAndTrigger(input, '02/08/1997 15:45:05', ['change']); + + assert.verifySteps(['datetime-changed']); + + await testUtils.dom.click(input); + + assert.strictEqual(input.value, '02/08/1997 15:45:05'); + assert.strictEqual( + document.querySelector('.datepicker .day.active').dataset.day, + '02/08/1997', + "Datepicker should have set the correct day" + ); + assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '15', + "Datepicker should have set the correct hour"); + assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '45', + "Datepicker should have set the correct minute"); + assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '05', + "Datepicker should have set the correct second"); + + picker.destroy(); + }); + }); +}); diff --git a/addons/web/static/tests/components/dropdown_menu_tests.js b/addons/web/static/tests/components/dropdown_menu_tests.js new file mode 100644 index 00000000000..bf91fa9c04d --- /dev/null +++ b/addons/web/static/tests/components/dropdown_menu_tests.js @@ -0,0 +1,345 @@ +odoo.define('web.dropdown_menu_tests', function (require) { + "use strict"; + + const DropdownMenu = require('web.DropdownMenu'); + const testUtils = require('web.test_utils'); + + const { createComponent } = testUtils; + + QUnit.module('Components', { + beforeEach: function () { + this.items = [ + { + isActive: false, + description: 'Some Item', + id: 1, + groupId: 1, + groupNumber: 1, + options: [ + { description: "First Option", groupNumber: 1, id: 1 }, + { description: "Second Option", groupNumber: 2, id: 2 }, + ], + }, { + isActive: true, + description: 'Some other Item', + id: 2, + groupId: 2, + groupNumber: 2, + }, + ]; + }, + }, function () { + QUnit.module('DropdownMenu'); + + QUnit.test('simple rendering and basic interactions', async function (assert) { + assert.expect(10); + + const dropdown = await createComponent(DropdownMenu, { + props: { + items: this.items, + title: "Dropdown", + }, + }); + + assert.strictEqual(dropdown.el.querySelector('button').innerText.trim(), "Dropdown"); + assert.containsOnce(dropdown, 'button i.fa.fa-caret-right'); + assert.containsNone(dropdown, 'ul.o_dropdown_menu'); + + await testUtils.dom.click(dropdown.el.querySelector('button')); + + assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3, + 'should have 3 elements counting the divider'); + assert.containsOnce(dropdown, 'button i.fa.fa-caret-down'); + const itemEls = dropdown.el.querySelectorAll('.o_menu_item > .dropdown-item'); + assert.strictEqual(itemEls[0].innerText.trim(), 'Some Item'); + assert.doesNotHaveClass(itemEls[0], 'selected'); + assert.hasClass(itemEls[1], 'selected'); + + const dropdownElements = dropdown.el.querySelectorAll('.o_menu_item *'); + for (const dropdownEl of dropdownElements) { + await testUtils.dom.click(dropdownEl); + } + assert.containsOnce(dropdown, 'ul.o_dropdown_menu', + "Clicking on any item of the dropdown should not close it"); + + await testUtils.dom.click(document.body); + + assert.containsNone(dropdown, 'ul.o_dropdown_menu', + "Clicking outside of the dropdown should close it"); + + 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` + <div> + <DropdownMenu class="first" title="'First'" items="state.items"/> + <DropdownMenu class="second" title="'Second'" items="state.items"/> + </div>`; + 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(); + }); + }); +}); 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 00000000000..424bdde1eaa --- /dev/null +++ b/addons/web/static/tests/components/pager_tests.js @@ -0,0 +1,163 @@ +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('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/control_panel_model_tests.js b/addons/web/static/tests/control_panel/control_panel_model_tests.js new file mode 100644 index 00000000000..bfde57ee082 --- /dev/null +++ b/addons/web/static/tests/control_panel/control_panel_model_tests.js @@ -0,0 +1,294 @@ +odoo.define('web.control_panel_model_tests', function (require) { + "use strict"; + + const ControlPanelModel = require('web.ControlPanelModel'); + const makeTestEnvironment = require('web.test_env'); + + function createControlPanelModel(config = {}) { + return new ControlPanelModel(Object.assign( + { env: makeTestEnvironment() }, + config + )); + } + function sanitizeFilters(model) { + return Object.values(model.state.filters).map(filter => { + const copy = Object.assign({}, filter); + delete copy.groupId; + delete copy.groupNumber; + delete copy.id; + return copy; + }); + } + + QUnit.module('ControlPanelModel', { + 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', function (assert) { + assert.expect(1); + + const model = createControlPanelModel(); + assert.deepEqual(sanitizeFilters(model), [{ type: 'timeRange' }]); + }); + + QUnit.test('one field tag', function (assert) { + assert.expect(1); + const arch = ` + <search> + <field name="bar"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('one separator tag', function (assert) { + assert.expect(1); + const arch = ` + <search> + <separator/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [{ type: 'timeRange' }]); + }); + + QUnit.test('one separator tag and one field tag', function (assert) { + assert.expect(1); + const arch = ` + <search> + <separator/> + <field name="bar"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('one filter tag', function (assert) { + assert.expect(1); + const arch = ` + <search> + <filter name="filter" string="Hello" domain="[]"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello", + domain: "[]", + type: "filter", + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('one groupBy tag', function (assert) { + assert.expect(1); + const arch = ` + <search> + <filter name="groupby" string="Hi" context="{ 'group_by': 'date_field:day'}"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + defaultOptionId: "day", + description: "Hi", + fieldName: "date_field", + fieldType: "date", + hasOptions: true, + type: "groupBy", + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('two filter tags', function (assert) { + assert.expect(1); + const arch = ` + <search> + <filter name="filter_1" string="Hello One" domain="[]"/> + <filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello One", + domain: "[]", + type: "filter", + }, + { + description: "Hello Two", + domain: "[('bar', '=', 3)]", + type: "filter", + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('two filter tags separated by a separator', function (assert) { + assert.expect(1); + const arch = ` + <search> + <filter name="filter_1" string="Hello One" domain="[]"/> + <separator/> + <filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello One", + domain: "[]", + type: "filter", + }, + { + description: "Hello Two", + domain: "[('bar', '=', 3)]", + type: "filter", + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('one filter tag and one field', function (assert) { + assert.expect(1); + const arch = ` + <search> + <filter name="filter" string="Hello" domain="[]"/> + <field name="bar"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Hello", + domain: "[]", + type: "filter", + }, + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field", + }, + { type: 'timeRange' }, + ]); + }); + + QUnit.test('two field tags', function (assert) { + assert.expect(1); + const arch = ` + <search> + <field name="foo"/> + <field name="bar"/> + </search>`; + const fields = this.fields; + const model = createControlPanelModel({ viewInfo: { arch, fields } }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: "Foo", + fieldName: "foo", + fieldType: "char", + type: "field" + }, + { + description: "Bar", + fieldName: "bar", + fieldType: "many2one", + type: "field" + }, + { type: 'timeRange' }, + ]); + }); + + 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 = createControlPanelModel({ viewInfo: { 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 + }, + { type: 'timeRange' } + ]); + + }); + + QUnit.test('process dynamic filters', async function (assert) { + assert.expect(1); + const dynamicFilters = [{ + description: 'Quick search', + domain: [['id', 'in', [1, 3, 4]]] + }]; + + const model = createControlPanelModel({ dynamicFilters }); + assert.deepEqual(sanitizeFilters(model), [ + { + description: 'Quick search', + domain: "[[\"id\",\"in\",[1,3,4]]]", + isDefault: true, + type: 'filter' + }, + { type: 'timeRange' }, + ]); + + }); + }); +}); 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 00000000000..560063cefb2 --- /dev/null +++ b/addons/web/static/tests/control_panel/control_panel_tests.js @@ -0,0 +1,129 @@ +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.module('Keyboard navigation'); + + QUnit.test('remove a facet with backspace', async function (assert) { + assert.expect(2); + + const params = { + cpStoreConfig: { + viewInfo: { + arch: `<search> <field name="foo"/></search>`, + fields: this.fields, + }, + actionContext: { search_default_foo: "a" }, + }, + 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(14); + + const arch = ` + <search> + <field name="display_name" string="Foo B" invisible="1"/> + <field name="foo" string="Foo A"/> + <filter name="filterA" string="FA" domain="[]"/> + <filter name="filterB" string="FB" invisible="1" domain="[]"/> + <filter name="groupByA" string="GA" context="{ 'group_by': 'date_field:day' }"/> + <filter name="groupByB" string="GB" context="{ 'group_by': 'date_field:day' }" invisible="1"/> + </search>`; + const searchMenuTypes = ['filter', 'groupBy']; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + actionContext: { + 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); + + 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); + + await cpHelpers.toggleGroupByMenu(controlPanel); + + selectorContainsValue('.o_menu_item a', "GA", true); + selectorContainsValue('.o_menu_item a', "GB", false); + + 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 00000000000..0d9cfac7bf2 --- /dev/null +++ b/addons/web/static/tests/control_panel/custom_filter_item_tests.js @@ -0,0 +1,355 @@ +odoo.define('web.filter_menu_generator_tests', function (require) { + "use strict"; + + const Domain = require('web.Domain'); + const CustomFilterItem = require('web.CustomFilterItem'); + const { Model } = require('web.model'); + const pyUtils = require('web.py_utils'); + const testUtils = require('web.test_utils'); + const session = require('web.session'); + + const cpHelpers = testUtils.controlPanel; + const { createComponent } = testUtils; + + function patchSession(newSession) { + // We have to patch the "legacy" session because field_utils is using it. + // TODO: remove it when the field_utils are re-written. + const initialSession = session; + Object.assign(session, newSession); + return function () { + Object.assign(session, initialSession); + }; + } + + 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 }, + }; + }, + }, function () { + + QUnit.module('CustomFilterItem'); + + QUnit.test('basic rendering', async function (assert) { + assert.expect(17); + + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { + controlPanelModel: new Model(), + }, + }); + + 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('adding a simple filter works', async function (assert) { + assert.expect(5); + + delete this.fields.date_field; + class MockedControlPanelModel extends Model { + createNewFilters(preFilters) { + 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 controlPanelModel = new MockedControlPanelModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { controlPanelModel }, + }); + + 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('commit search with an extended proposition with field char does not cause a crash', async function (assert) { + assert.expect(6); + + 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 MockedControlPanelModel extends Model { + createNewFilters(preFilters) { + 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 controlPanelModel = new MockedControlPanelModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { controlPanelModel }, + }); + + 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); + } + + cfi.destroy(); + }); + + QUnit.test('custom filter datetime with equal operator', async function (assert) { + assert.expect(4); + + patchSession({ + getTZOffset: function () { + return -240; + }, + }); + class MockedControlPanelModel extends Model { + createNewFilters(preFilters) { + 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 controlPanelModel = new MockedControlPanelModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { controlPanelModel }, + }); + + 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(4); + + patchSession({ + getTZOffset: function () { + return -240; + }, + }); + class MockedControlPanelModel extends Model { + createNewFilters(preFilters) { + 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 controlPanelModel = new MockedControlPanelModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { controlPanelModel }, + }); + + 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(6); + + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { + controlPanelModel: new Model(), + }, + }); + + 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"); + assert.strictEqual(floatInput.value, "4.2"); + + // Number parsing + await testUtils.fields.editInput(idInput, 4); + assert.strictEqual(idInput.value, "4"); + await testUtils.fields.editInput(idInput, "DefinitelyValidID"); + assert.strictEqual(idInput.value, "4"); + + cfi.destroy(); + }); + + QUnit.test('add custom filter with multiple values', async function (assert) { + assert.expect(1); + + class MockedControlPanelModel extends Model { + createNewFilters(preFilters) { + 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 controlPanelModel = new MockedControlPanelModel(); + const cfi = await createComponent(CustomFilterItem, { + props: { + fields: this.fields, + }, + env: { controlPanelModel }, + }); + + 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 00000000000..957668a2dae --- /dev/null +++ b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js @@ -0,0 +1,72 @@ +odoo.define('web.groupby_menu_generator_tests', function (require) { + "use strict"; + + const CustomGroupByItem = require('web.CustomGroupByItem'); + const { Model } = require('web.model'); + 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: { + controlPanelModel: new Model(), + }, + }); + + 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(3); + + const fields = [ + { sortable: true, name: 'candlelight', string: 'Candlelight', type: 'boolean' }, + ]; + class MockedControlPanelModel extends Model { + createNewGroupBy(field) { + assert.deepEqual(field, fields[0]); + } + } + const controlPanelModel = new MockedControlPanelModel(); + const cgi = await createComponent(CustomGroupByItem, { + props: { fields }, + env: { controlPanelModel }, + }); + + 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 00000000000..5aadc99f619 --- /dev/null +++ b/addons/web/static/tests/control_panel/favorite_menu_tests.js @@ -0,0 +1,572 @@ +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 = { + cpStoreConfig: { 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 = { + cpStoreConfig: { + 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 = { + cpStoreConfig: { + viewInfo: { 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 = ` + <search> + <filter string="Float" name="positive" domain="[('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d'))]"/> + </search> + `; + const params = { + cpStoreConfig: { + viewInfo: { fields: {}, arch }, + searchMenuTypes, + actionContext: { + 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 = `<search><field name="foo"/></search>`; + const params = { + cpStoreConfig: { + viewInfo: { 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 = { + cpStoreConfig: { viewInfo: { 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 = { + cpStoreConfig: { + viewInfo: { favoriteFilters }, + searchMenuTypes, + actionContext: { 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, time ranges and field "options"', async function (assert) { + assert.expect(8); + + const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0); + + const favoriteFilters = [{ + context: "{'group_by': ['foo'], 'time_ranges': {'field': 'birthday', 'range': 'this_year'}}", + domain: "['!', ['foo', '=', 'qsdf']]", + id: 7, + is_default: false, + name: "My favorite", + sort: "[]", + user_id: [2, "Mitchell Admin"], + }]; + const arch = ` + <search> + <field string="Foo" name="foo"/> + <filter string="Date Field Filter" name="positive" date="date_field"/> + <filter string="Date Field Groupby" name="coolName" context="{'group_by': 'date_field'}"/> + </search> + `; + const searchMenuTypes = ['filter', 'groupBy', 'timeRange', 'favorite']; + const params = { + cpStoreConfig: { + viewInfo: { favoriteFilters, arch, fields: this.fields }, + searchMenuTypes, + actionContext: { + search_default_positive: true, + search_default_coolName: true, + search_default_foo: "a", + time_ranges: { field: 'date_field', range: 'today', comparisonRange: 'previous_period' } + } + }, + cpProps: { searchMenuTypes, action: {}, fields: this.fields }, + search: function (searchQuery) { + const { domain, groupBy, timeRanges } = searchQuery; + assert.deepEqual(domain, ['!', ['foo', '=', 'qsdf']]); + assert.deepEqual(groupBy, ['foo']); + assert.deepEqual(timeRanges, { + fieldDescription: "Birthday", + fieldName: "birthday", + range: [ + "&", ["birthday", ">=", "2019-01-01"], ["birthday", "<", "2020-01-01"] + ], + rangeDescription: "This Year", + rangeId: "this_year" + }); + } + }; + const controlPanel = await createControlPanel(params); + + const { domain, groupBy, timeRanges } = controlPanel.getQuery(); + assert.deepEqual(domain, [ + "&", + ["foo", "=", "a"], + "&", + ["date_field", ">=", "2019-07-01"], + ["date_field", "<=", "2019-07-31"] + ]); + assert.deepEqual(groupBy, ['date_field:month']); + assert.deepEqual(timeRanges, { + comparisonRange: [ + "&", ["date_field", ">=", "2019-07-30"], ["date_field", "<", "2019-07-31"] + ], + comparisonRangeDescription: "Previous Period", + comparisonRangeId: "previous_period", + fieldDescription: "Date", + fieldName: "date_field", + range: [ + "&", ["date_field", ">=", "2019-07-31"], ["date_field", "<", "2019-08-01"] + ], + rangeDescription: "Today", + rangeId: "today" + }); + + assert.deepEqual( + cpHelpers.getFacetTexts(controlPanel), + [ + 'Foo\na', + 'Date Field Filter: July 2019', + 'Date Field Groupby: Month', + 'Date: Today / Previous Period' + ] + ); + + 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 = { + cpStoreConfig: { viewInfo: { favoriteFilters }, searchMenuTypes }, + cpProps: { searchMenuTypes, action: {} }, + 'get-controller-query-params': function (callback) { + callback(); + }, + env: { + session: { uid: 4 }, + services: { + notification: { + notify: function (params) { + assert.deepEqual(params, { + title: "Error", + 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: ` + <form string="Partners"> + <sheet> + <group> + <field name="bar"/> + </group> + </sheet> + </form>`, + archs: { + 'partner,false,list': '<tree><field name="display_name"/></tree>', + 'partner,false,search': '<search><field name="date_field"/></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(); + }); + }); +});
\ No newline at end of file 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 00000000000..4728c356728 --- /dev/null +++ b/addons/web/static/tests/control_panel/filter_menu_tests.js @@ -0,0 +1,496 @@ +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 = { + cpStoreConfig: { 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 = ` + <search> + <filter string="Foo" name="foo" domain="[]"/> + </search>`; + const params = { + cpStoreConfig: { viewInfo: { 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 = { + cpStoreConfig: { viewInfo: { 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 = ` + <search> + <filter string="Foo" name="foo" domain="[['foo', '=', 'qsdf']]"/> + </search>`; + const params = { + cpStoreConfig: { viewInfo: { 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 = { + cpStoreConfig: { viewInfo: { 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 = { + cpStoreConfig: { viewInfo: { 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", ">=", "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", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"], + "&", ["date_field", ">=", "2015-01-01"], ["date_field", "<=", "2015-12-31"] + ], + ["|", + "|", + "&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"], + "&", ["date_field", ">=", "2016-03-01"], ["date_field", "<=", "2016-03-31"], + "&", ["date_field", ">=", "2015-03-01"], ["date_field", "<=", "2015-03-31"] + ] + ]; + + const arch = ` + <search> + <filter string="Date" name="date_field" date="date_field"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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: 2017 / 2016', selectedoptions: [7, 8] }, + { description: '2015', facetContent: 'Date: 2017 / 2016 / 2015', selectedoptions: [7, 8, 9] }, + { description: 'March', facetContent: 'Date: March 2017 / March 2016 / March 2015', 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 = ` + <search> + <filter string="Date" name="some_filter" date="date_field" default_period="last_month"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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 <filter> is used', async function (assert) { + assert.expect(1); + + const arch = ` + <search> + <filter string="Filter" name="some_filter" domain="[]" context="{'coucou_1': 1}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { 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 = + `<search> + <filter string="Foo" name="gently_weeps" domain="${_.escape(xml_domain)}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { 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 = + `<search> + <filter string="Date" name="date_field" date="date_field" default_period="last_month"/> + </search>`, + params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { + 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 = + `<search> + <filter string="Filter Group 1" name="f_1_g1" domain="[['foo', '=', 'f1_g1']]"/> + <separator/> + <filter string="Filter 1 Group 2" name="f1_g2" domain="[['foo', '=', 'f1_g2']]"/> + <filter string="Filter 2 GROUP 2" name="f2_g2" domain="[['foo', '=', 'f2_g2']]"/> + </search>`, + params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { + 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 = + `<search> + <filter string="1" name="coolName1" date="date_field"/> + <separator/> + <filter string="2" name="coolName2" date="date_field"/> + <separator/> + <filter string="3" name="coolName3" domain="[]"/> + <separator/> + <filter string="4" name="coolName4" domain="[]"/> + <separator/> + <filter string="5" name="coolName5" domain="[]"/> + <separator/> + <filter string="6" name="coolName6" domain="[]"/> + <separator/> + <filter string="7" name="coolName7" domain="[]"/> + <separator/> + <filter string="8" name="coolName8" domain="[]"/> + <separator/> + <filter string="9" name="coolName9" domain="[]"/> + <separator/> + <filter string="10" name="coolName10" domain="[]"/> + <separator/> + <filter string="11" name="coolName11" domain="[]"/> + </search>`, + params = { + cpStoreConfig: { + viewInfo: { 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(); + }); + }); +});
\ No newline at end of file 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 00000000000..abaaeceeaf3 --- /dev/null +++ b/addons/web/static/tests/control_panel/groupby_menu_tests.js @@ -0,0 +1,447 @@ +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 = { + cpStoreConfig: { 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 = { + cpStoreConfig: { 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 = ` + <search> + <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/> + </search>`; + const params = { + cpStoreConfig: { viewInfo: { 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 = ` + <search> + <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/> + </search>`; + const params = { + cpStoreConfig: { viewInfo: { 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 = ` + <search> + <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/> + </search>`; + const params = { + cpStoreConfig: { viewInfo: { 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 = ` + <search> + <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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 = ` + <search> + <filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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 = ` + <search> + <filter string="Bar" name="bar" context="{'group_by': 'bar'}"/> + <filter string="Date" name="date" context="{'group_by': 'date_field'}"/> + <filter string="Foo" name="foo" context="{'group_by': 'foo'}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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 = { + cpStoreConfig: { 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 = { + cpStoreConfig: { viewInfo: { 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 = ` + <search> + <filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/> + <filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { arch, fields: this.fields }, + searchMenuTypes, + actionContext: { 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 = ` + <search> + <filter string="Date" name="coolName" context="{'group_by': 'date_field'}"/> + <separator/> + <filter string="Bar" name="superName" context="{'group_by': 'bar'}"/> + </search>`; + const params = { + cpStoreConfig: { + viewInfo: { 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(); + }); + }); +});
\ No newline at end of file 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 00000000000..0678e77e9c6 --- /dev/null +++ b/addons/web/static/tests/control_panel/search_bar_tests.js @@ -0,0 +1,415 @@ +odoo.define('web.search_bar_tests', function (require) { + "use strict"; + + const testUtils = require('web.test_utils'); + + const cpHelpers = testUtils.controlPanel; + const { createActionManager } = 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' }, + }, + records: [ + { id: 1, display_name: "First record", foo: "yop", bar: 2, birthday: '1983-07-15', birth_datetime: '1983-07-15 01:00:00' }, + { id: 2, display_name: "Second record", foo: "blip", bar: 1, birthday: '1982-06-04', birth_datetime: '1982-06-04 02:00:00' }, + { id: 3, display_name: "Third record", foo: "gnap", bar: 1, birthday: '1985-09-13', birth_datetime: '1985-09-13 03:00:00' }, + { id: 4, display_name: "Fourth record", foo: "plop", bar: 2, birthday: '1983-05-05', birth_datetime: '1983-05-05 04:00:00' }, + { id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, 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': ` + <tree> + <field name="foo"/> + </tree>`, + 'partner,false,search': ` + <search> + <field name="foo"/> + <field name="birthday"/> + <field name="birth_datetime"/> + <field name="bar" context="{'bar': self}"/> + <filter string="Date Field Filter" name="positive" date="birthday"/> + <filter string="Date Field Groupby" name="coolName" context="{'group_by': 'birthday:day'}"/> + </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('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'] = ` + <pivot> + <field name="foo" type="row"/> + </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(); + }); + }); +}); diff --git a/addons/web/static/tests/control_panel/time_range_menu_tests.js b/addons/web/static/tests/control_panel/time_range_menu_tests.js new file mode 100644 index 00000000000..17b6936c9ca --- /dev/null +++ b/addons/web/static/tests/control_panel/time_range_menu_tests.js @@ -0,0 +1,333 @@ +odoo.define('web.time_range_menu_tests', function (require) { + "use strict"; + + const testUtils = require('web.test_utils'); + const { TIME_RANGE_OPTIONS, COMPARISON_TIME_RANGE_OPTIONS } = require('web.searchUtils'); + + const { controlPanel: cpHelpers, createControlPanel, mock } = testUtils; + const { patchDate } = mock; + + const searchMenuTypes = ['timeRange']; + + QUnit.module('Components', { + beforeEach: function () { + 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 }, + }; + }, + }, function () { + + QUnit.module('TimeRangeMenu'); + + QUnit.test('simple rendering', async function (assert) { + assert.expect(12); + + const params = { + cpStoreConfig: { viewInfo: { fields: this.fields }, searchMenuTypes }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + assert.containsOnce(controlPanel, 'div.o_time_range_menu > button i.fa.fa-calendar'); + assert.strictEqual(controlPanel.el.querySelector('div.o_time_range_menu > button span').innerText.trim(), 'Time Ranges'); + + await cpHelpers.toggleTimeRangeMenu(controlPanel); + + assert.containsN(controlPanel, 'div.o_time_range_section', 3); + + let selectEls = [...controlPanel.el.querySelectorAll('div.o_time_range_section select')]; + assert.strictEqual(selectEls.length, 2); + assert.deepEqual(selectEls.map(e => e.value), ['birthday', 'this_month']); + assert.containsN(controlPanel, 'div.o_time_range_section label', 3); + + const labelEls = [...controlPanel.el.querySelectorAll('div.o_time_range_section label')]; + assert.deepEqual(labelEls.map(e => e.innerText.trim()), ['Based On', 'Range', 'Compare To']); + assert.containsOnce(controlPanel, 'div.o_time_range_section input[type="checkbox"]'); + + assert.containsOnce(controlPanel, 'div.o_time_range_menu ul button'); + assert.strictEqual( + // text is upper cased in enterprise + controlPanel.el.querySelector('div.o_time_range_menu ul button').innerText.trim().toUpperCase(), + 'APPLY' + ); + + await cpHelpers.toggleTimeRangeMenuBox(controlPanel); + + selectEls = controlPanel.el.querySelectorAll('div.o_time_range_section select'); + assert.strictEqual(selectEls.length, 3); + assert.strictEqual(selectEls[2].value, 'previous_period'); + + controlPanel.destroy(); + }); + + QUnit.test('initialization with default time range without comparisonRange', async function (assert) { + assert.expect(5); + + const unpatchDate = patchDate(2020, 0, 29, 15, 0, 0); + + const params = { + cpStoreConfig: { + viewInfo: { fields: this.fields }, + searchMenuTypes, + actionContext: { + time_ranges: { field: 'date_field', range: 'this_week' } + } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + const { timeRanges } = controlPanel.getQuery(); + assert.deepEqual(timeRanges, { + fieldDescription: "Date", + fieldName: "date_field", + range: [ + "&", + ["date_field", ">=", "2020-01-27"], + ["date_field", "<", "2020-02-03"] + ], + rangeDescription: "This Week", + rangeId: "this_week" + }); + + await cpHelpers.toggleTimeRangeMenu(controlPanel); + + const selectEls = controlPanel.el.querySelectorAll('div.o_time_range_section select'); + assert.strictEqual(selectEls.length, 2); + assert.deepEqual( + [...selectEls].map(e => e.value), + ['date_field', 'this_week'] + ); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: This Week']); + assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'), + 'span.fa.fa-calendar.o_searchview_facet_label'); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('initialization with default time range with comparisonRange', async function (assert) { + assert.expect(4); + + const unpatchDate = patchDate(2020, 0, 29, 15, 0, 0); + + const params = { + cpStoreConfig: { + viewInfo: { fields: this.fields }, + searchMenuTypes, + actionContext: { + time_ranges: { field: 'date_field', range: 'today', comparisonRange: 'previous_year' } + } + }, + cpProps: { fields: this.fields, searchMenuTypes }, + }; + const controlPanel = await createControlPanel(params); + + const { timeRanges } = controlPanel.getQuery(); + assert.deepEqual(timeRanges, { + comparisonRange: [ + "&", + ["date_field", ">=", "2019-01-29"], + ["date_field", "<", "2019-01-30"] + ], + comparisonRangeDescription: "Previous Year", + comparisonRangeId: "previous_year", + fieldDescription: "Date", + fieldName: "date_field", + range: [ + "&", + ["date_field", ">=", "2020-01-29"], + ["date_field", "<", "2020-01-30"] + ], + rangeDescription: "Today", + rangeId: "today" + }); + + await cpHelpers.toggleTimeRangeMenu(controlPanel); + + const selectEls = controlPanel.el.querySelectorAll('div.o_time_range_section select'); + assert.strictEqual(selectEls.length, 3); + assert.deepEqual( + [...selectEls].map(e => e.value), + ['date_field', 'today', 'previous_year'] + ); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Today / Previous Year']); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('activate a time range works', async function (assert) { + assert.expect(3); + + const unpatchDate = patchDate(2020, 0, 29, 1, 0, 0); + + const params = { + cpStoreConfig: { + viewInfo: { fields: this.fields }, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { timeRanges } = searchQuery; + assert.deepEqual(timeRanges, { + comparisonRange: [ + "&", + ["birthday", ">=", "2019-01-29"], + ["birthday", "<", "2019-01-30"] + ], + comparisonRangeDescription: "Previous Year", + comparisonRangeId: "previous_year", + fieldDescription: "Birthday", + fieldName: "birthday", + range: [ + "&", + ["birthday", ">=", "2020-01-29"], + ["birthday", "<", "2020-01-30"] + ], + rangeDescription: "Today", + rangeId: "today" + }); + } + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + await cpHelpers.toggleTimeRangeMenu(controlPanel); + await cpHelpers.toggleTimeRangeMenuBox(controlPanel); + + await cpHelpers.selectField(controlPanel, 'birthday'); + await cpHelpers.selectRange(controlPanel, 'today'); + await cpHelpers.selectComparisonRange(controlPanel, 'previous_year'); + + await cpHelpers.applyTimeRange(controlPanel); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Birthday: Today / Previous Year']); + + controlPanel.destroy(); + unpatchDate(); + }); + + QUnit.test('no timeRanges key in search query if "timeRange" not in searchMenuTypes', async function (assert) { + assert.expect(1); + + const params = { + cpStoreConfig: { + viewInfo: { fields: this.fields }, + actionContext: { + time_ranges: { field: 'date_field', range: 'this_week' } + } + }, + cpProps: { fields: this.fields }, + }; + const controlPanel = await createControlPanel(params); + + const { timeRanges } = controlPanel.getQuery(); + assert.strictEqual(timeRanges, undefined); + + controlPanel.destroy(); + }); + + QUnit.test('time ranges generated are correct', async function (assert) { + assert.expect(85); + + const unpatchDate = patchDate(2017, 2, 22, 1, 0, 0); + + const ranges = { + last_7_days: ['&', ["birthday", ">=", "2017-03-15"], ["birthday", "<", "2017-03-22"]], + last_30_days: ['&', ["birthday", ">=", "2017-02-20"], ["birthday", "<", "2017-03-22"]], + last_365_days: ['&', ["birthday", ">=", "2016-03-22"], ["birthday", "<", "2017-03-22"]], + last_5_years: ['&', ["birthday", ">=", "2012-03-22"], ["birthday", "<", "2017-03-22"]], + today: ['&', ["birthday", ">=", "2017-03-22"], ["birthday", "<", "2017-03-23"]], + this_week: ['&', ["birthday", ">=", "2017-03-20"], ["birthday", "<", "2017-03-27"]], + this_month: ['&', ["birthday", ">=", "2017-03-01"], ["birthday", "<", "2017-04-01"]], + this_quarter: ['&', ["birthday", ">=", "2017-01-01"], ["birthday", "<", "2017-04-01"]], + this_year: ['&', ["birthday", ">=", "2017-01-01"], ["birthday", "<", "2018-01-01"]], + yesterday: ['&', ["birthday", ">=", "2017-03-21"], ["birthday", "<", "2017-03-22"]], + last_week: ['&', ["birthday", ">=", "2017-03-13"], ["birthday", "<", "2017-03-20"]], + last_month: ['&', ["birthday", ">=", "2017-02-01"], ["birthday", "<", "2017-03-01"]], + last_quarter: ['&', ["birthday", ">=", "2016-10-01"], ["birthday", "<", "2017-01-01"]], + last_year: ['&', ["birthday", ">=", "2016-01-01"], ["birthday", "<", "2017-01-01"]], + }; + const comparisonRanges = { + previous_period: { + last_7_days: ['&', ["birthday", ">=", "2017-03-08"], ["birthday", "<", "2017-03-15"]], + last_30_days: ['&', ["birthday", ">=", "2017-01-21"], ["birthday", "<", "2017-02-20"]], + last_365_days: ['&', ["birthday", ">=", "2015-03-23"], ["birthday", "<", "2016-03-22"]], + last_5_years: ['&', ["birthday", ">=", "2007-03-22"], ["birthday", "<", "2012-03-22"]], + today: ['&', ["birthday", ">=", "2017-03-21"], ["birthday", "<", "2017-03-22"]], + this_week: ['&', ["birthday", ">=", "2017-03-13"], ["birthday", "<", "2017-03-20"]], + this_month: ['&', ["birthday", ">=", "2017-02-01"], ["birthday", "<", "2017-03-01"]], + this_quarter: ['&', ["birthday", ">=", "2016-10-01"], ["birthday", "<", "2017-01-01"]], + this_year: ['&', ["birthday", ">=", "2016-01-01"], ["birthday", "<", "2017-01-01"]], + yesterday: ['&', ["birthday", ">=", "2017-03-20"], ["birthday", "<", "2017-03-21"]], + last_week: ['&', ["birthday", ">=", "2017-03-06"], ["birthday", "<", "2017-03-13"]], + last_month: ['&', ["birthday", ">=", "2017-01-01"], ["birthday", "<", "2017-02-01"]], + last_quarter: ['&', ["birthday", ">=", "2016-07-01"], ["birthday", "<", "2016-10-01"]], + last_year: ['&', ["birthday", ">=", "2015-01-01"], ["birthday", "<", "2016-01-01"]], + }, + previous_year: { + last_7_days: ["&", ["birthday", ">=", "2016-03-15"], ["birthday", "<", "2016-03-22"]], + last_30_days: ["&", ["birthday", ">=", "2016-02-21"], ["birthday", "<", "2016-03-22"]], + last_365_days: ["&", ["birthday", ">=", "2015-03-23"], ["birthday", "<", "2016-03-22"]], + last_5_years: ["&", ["birthday", ">=", "2011-03-22"], ["birthday", "<", "2016-03-22"]], + today: ["&", ["birthday", ">=", "2016-03-22"], ["birthday", "<", "2016-03-23"]], + this_week: ["&", ["birthday", ">=", "2016-03-21"], ["birthday", "<", "2016-03-28"]], + this_month: ["&", ["birthday", ">=", "2016-03-01"], ["birthday", "<", "2016-04-01"]], + this_quarter: ["&", ["birthday", ">=", "2016-01-01"], ["birthday", "<", "2016-04-01"]], + this_year: ["&", ["birthday", ">=", "2016-01-01"], ["birthday", "<", "2017-01-01"]], + yesterday: ["&", ["birthday", ">=", "2016-03-21"], ["birthday", "<", "2016-03-22"]], + last_week: ["&", ["birthday", ">=", "2016-03-14"], ["birthday", "<", "2016-03-21"]], + last_month: ["&", ["birthday", ">=", "2016-02-01"], ["birthday", "<", "2016-03-01"]], + last_quarter: ["&", ["birthday", ">=", "2015-10-01"], ["birthday", "<", "2016-01-01"]], + last_year: ["&", ["birthday", ">=", "2015-01-01"], ["birthday", "<", "2016-01-01"]], + } + }; + + const params = { + cpStoreConfig: { + viewInfo: { fields: this.fields }, + searchMenuTypes, + }, + cpProps: { fields: this.fields, searchMenuTypes }, + search: function (searchQuery) { + const { timeRanges } = searchQuery; + const { range, comparisonRange, rangeId, comparisonRangeId } = timeRanges; + if (!comparisonRangeId) { + assert.deepEqual(range, ranges[rangeId]); + } else { + assert.deepEqual(comparisonRange, comparisonRanges[comparisonRangeId][rangeId]); + } + } + }; + const controlPanel = await createControlPanel(params); + + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []); + + await cpHelpers.toggleTimeRangeMenu(controlPanel); + + for (const o of Object.values(TIME_RANGE_OPTIONS)) { + await cpHelpers.selectRange(controlPanel, o.id); + await cpHelpers.applyTimeRange(controlPanel); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [`Birthday: ${o.description}`]); + + await cpHelpers.toggleTimeRangeMenuBox(controlPanel); + for (const co of Object.values(COMPARISON_TIME_RANGE_OPTIONS)) { + await cpHelpers.selectComparisonRange(controlPanel, co.id); + await cpHelpers.applyTimeRange(controlPanel); + assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [`Birthday: ${o.description} / ${co.description}`]); + } + await cpHelpers.toggleTimeRangeMenuBox(controlPanel); + } + + controlPanel.destroy(); + unpatchDate(); + }); + }); +}); diff --git a/addons/web/static/tests/views/control_panel_tests.js b/addons/web/static/tests/views/control_panel_tests.js deleted file mode 100644 index 557811b39c3..00000000000 --- a/addons/web/static/tests/views/control_panel_tests.js +++ /dev/null @@ -1,584 +0,0 @@ -odoo.define('web.control_panel_tests', function (require) { -"use strict"; - -const AbstractAction = require('web.AbstractAction'); -const ControlPanelView = require('web.ControlPanelView'); -const core = require('web.core'); -const testUtils = require('web.test_utils'); - -const createActionManager = testUtils.createActionManager; -const createControlPanel = testUtils.createControlPanel; - -function createControlPanelFactory(arch, fields) { - arch = arch || "<search></search>"; - fields = fields || {}; - var viewInfo = {arch: arch, fields: fields}; - var controlPanelFactory = new ControlPanelView({viewInfo: viewInfo, context: {}}); - return controlPanelFactory; -} - -QUnit.module('Views', { - beforeEach: function () { - this.data = { - partner: { - fields: { - display_name: { string: "Displayed name", type: 'char' }, - foo: {string: "Foo", type: "char", default: "My little Foo Value", 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'}, - }, - records: [], - onchanges: {}, - }, - }; - } -}, function () { - QUnit.module('ControlPanelView'); - - QUnit.module('Control Panel Arch Parsing'); - - QUnit.test('empty arch', function (assert) { - assert.expect(1); - - var controlPanelFactory = createControlPanelFactory(); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [], - "there should be no group at all" - ); - }); - - QUnit.test('parse one field tag', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<field name=\"bar\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [[{ - attrs: { - name: "bar", - string: "Bar" - }, - autoCompleteValues: [], - description: "bar", - isDefault: false, - type: "field" - }]], - "there should be one group with one field" - ); - }); - - QUnit.test('parse one separator tag', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<separator/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [], - "there should be no group at all"); - }); - - QUnit.test('parse one separator tag and one field tag', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<separator/>" + - "<field name=\"bar\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [[ - { - attrs: { - name: "bar", - string: "Bar" - }, - autoCompleteValues: [], - description: "bar", - isDefault: false, - type: "field" - } - ]], - "there should be one group with one field" - ); - }); - - QUnit.test('parse one filter tag', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<filter name=\"filter\" string=\"Hello\" " + - "domain=\"[]\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [[{ - context: {}, - description: "Hello", - groupNumber: 2, - domain: "[]", - isDefault: false, - type: "filter" - }]], - "there should be one group with one filter" - ); - }); - - QUnit.test('parse one groupBy tag', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<groupBy name=\"groupby\" string=\"Hi\" " + - "context=\"{\'group_by\': \'date_field:day\'}\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [[ - { - currentOptionIds: new Set(), - defaultOptionId: "day", - description: "Hi", - fieldName: "date_field", - fieldType: "date", - groupNumber: 2, - hasOptions: true, - isDefault: false, - options: [ - { - description: "Year", - groupId: 1, - optionId: "year" - }, - { - description: "Quarter", - groupId: 1, - optionId: "quarter" - }, - { - description: "Month", - groupId: 1, - optionId: "month" - }, - { - description: "Week", - groupId: 1, - optionId: "week" - }, - { - description: "Day", - groupId: 1, - optionId: "day" - } - ], - type: "groupBy" - } - ]], - "there should be one group with one groupBy with options" - ); - }); - - QUnit.test('parse two filter tags', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<filter name=\"filter_1\" string=\"Hello One\" " + - "domain=\"[]\"/>" + - "<filter name=\"filter_2\" string=\"Hello Two\" " + - "domain=\"[(\'bar\', \'=\', 3)]\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [[ - { - "context": {}, - "description": "Hello One", - "domain": "[]", - "groupNumber": 2, - "isDefault": false, - "type": "filter" - }, - { - "context": {}, - "description": "Hello Two", - "domain": "[('bar', '=', 3)]", - "groupNumber": 2, - "isDefault": false, - "type": "filter" - } - ]], - 'there should be one group of two filters' - ); - }); - - QUnit.test('parse two filter tags separated by a separator', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<filter name=\"filter_1\" string=\"Hello One\" " + - "domain=\"[]\"/>" + - "<separator/>" + - "<filter name=\"filter_2\" string=\"Hello Two\" " + - "domain=\"[(\'bar\', \'=\', 3)]\"/>" + - "</search>"; - - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [ - [ - { - context: {}, - description: "Hello One", - domain: "[]", - groupNumber: 2, - isDefault: false, - type: "filter" - } - ], - [ - { - context: {}, - description: "Hello Two", - domain: "[('bar', '=', 3)]", - groupNumber: 4, - isDefault: false, - type: "filter" - } - ] - ], - "there should be two groups of one filter" - ); - }); - - QUnit.test('parse one filter tag and one field', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<filter name=\"filter\" string=\"Hello\" domain=\"[]\"/>" + - "<field name=\"bar\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [ - [ - { - context: {}, - description: "Hello", - domain: "[]", - groupNumber: 2, - isDefault: false, - type: "filter" - } - ], - [ - { - attrs: { - name: "bar", - string: "Bar" - }, - autoCompleteValues: [], - description: "bar", - isDefault: false, - type: "field" - } - ] - ], - "there should be one group with a filter and one group with a field" - ); - }); - - QUnit.test('parse two field tags', function (assert) { - assert.expect(1); - var arch = "<search>" + - "<field name=\"foo\"/>" + - "<field name=\"bar\"/>" + - "</search>"; - var fields = this.data.partner.fields; - var controlPanelFactory = createControlPanelFactory(arch, fields); - assert.deepEqual( - controlPanelFactory.loadParams.groups, - [ - [ - { - attrs: { - name: "foo", - string: "Foo" - }, - autoCompleteValues: [], - description: "foo", - isDefault: false, - type: "field" - } - ], - [ - { - attrs: { - name: "bar", - string: "Bar" - }, - autoCompleteValues: [], - description: "bar", - isDefault: false, - type: "field" - } - ] - ], - "there should be two groups of a single field" - ); - }); - - QUnit.module('Control Panel behaviour'); - - QUnit.test('remove a facet with backspace', async function (assert) { - assert.expect(2); - - var controlPanel = await createControlPanel({ - model: 'partner', - arch: "<search><filter name=\"filterA\" string=\"A\" domain=\"[]\"/></search>", - data: this.data, - searchMenuTypes: ['filter'], - }); - await testUtils.dom.click(controlPanel.$('.o_filters_menu_button')); - await testUtils.dom.click($('.o_menu_item a')); - assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').text().trim(), 'A', - 'should have a facet with A'); - - // delete a facet - controlPanel.$('input.o_searchview_input').trigger($.Event('keydown', { - which: $.ui.keyCode.BACKSPACE, - keyCode: $.ui.keyCode.BACKSPACE, - })); - await testUtils.nextTick(); - assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0, - 'there should be no facet'); - - // delete nothing (should not crash) - controlPanel.$('input.o_searchview_input').trigger($.Event('keydown', { - which: $.ui.keyCode.BACKSPACE, - keyCode: $.ui.keyCode.BACKSPACE, - })); - await testUtils.nextTick(); - - controlPanel.destroy(); - }); - - QUnit.module('Control Panel Rendering'); - - QUnit.test('default breadcrumb in abstract action', async function (assert) { - assert.expect(1); - - const ConcreteAction = AbstractAction.extend({ - hasControlPanel: true, - }); - core.action_registry.add('ConcreteAction', ConcreteAction); - - const actionManager = await createActionManager(); - await actionManager.doAction({id: 1, - name: 'A Concrete Action', - tag: 'ConcreteAction', - type: 'ir.actions.client', - }); - - assert.strictEqual(actionManager.$('.breadcrumb').text(), 'A Concrete Action'); - - actionManager.destroy(); - }); - - QUnit.test('fiels and filters with groups/invisible attribute are not always rendered but activable as search default', async function (assert) { - assert.expect(13); - var controlPanel = await createControlPanel({ - model: 'partner', - arch: "<search>" + - "<field name=\"display_name\" string=\"Foo B\" invisible=\"1\"/>" + - "<field name=\"foo\" string=\"Foo A\"/>" + - "<filter name=\"filterA\" string=\"FA\" domain=\"[]\"/>" + - "<filter name=\"filterB\" string=\"FB\" invisible=\"1\" domain=\"[]\"/>" + - "<filter name=\"groupByA\" string=\"GA\" context=\"{'group_by': 'date_field:day'}\"/>" + - "<filter name=\"groupByB\" string=\"GB\" context=\"{'group_by': 'date_field:day'}\" invisible=\"1\"/>" + - "</search>", - data: this.data, - searchMenuTypes: ['filter', 'groupBy'], - context: { - search_default_display_name: 'value', - search_default_filterB: true, - search_default_groupByB: true, - }, - }); - - // default filters/fields should be activated even if invisible - assert.containsN(controlPanel, '.o_searchview_facet', 3); - - - await testUtils.dom.click(controlPanel.$('.o_filters_menu_button')); - assert.containsOnce(controlPanel, '.o_menu_item a:contains("FA")'); - assert.containsNone(controlPanel, '.o_menu_item a:contains("FB")'); - // default filter should be activated even if invisible - assert.containsOnce(controlPanel, '.o_searchview_facet .o_facet_values:contains(FB)'); - - await testUtils.dom.click(controlPanel.$('button span.fa-bars')); - assert.containsOnce(controlPanel, '.o_menu_item a:contains("GA")'); - assert.containsNone(controlPanel, '.o_menu_item a:contains("GB")'); - // default filter should be activated even if invisible - assert.containsOnce(controlPanel, '.o_searchview_facet .o_facet_values:contains(GB)'); - - assert.strictEqual(controlPanel.$('.o_searchview_facet').eq(0).text().replace(/[\s\t]+/g, ""), "FooBvalue"); - - - // 'a' key to filter nothing on bar - controlPanel.$('.o_searchview_input').val('a'); - controlPanel.$('.o_searchview_input').trigger($.Event('keypress', { which: 65, keyCode: 65 })); - await testUtils.nextTick(); - // the only item in autocomplete menu should be FooA: a - assert.strictEqual(controlPanel.$('div.o_searchview_autocomplete').text().replace(/[\s\t]+/g, ""), "SearchFooAfor:A"); - controlPanel.$('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER })); - await testUtils.nextTick(); - - - // The items in the Filters menu and the Group By menu should be the same as before - await testUtils.dom.click(controlPanel.$('.o_filters_menu_button')); - assert.containsOnce(controlPanel, '.o_menu_item a:contains("FA")'); - assert.containsNone(controlPanel, '.o_menu_item a:contains("FB")'); - - await testUtils.dom.click(controlPanel.$('button span.fa-bars')); - assert.containsOnce(controlPanel, '.o_menu_item a:contains("GA")'); - assert.containsNone(controlPanel, '.o_menu_item a:contains("GB")'); - - - controlPanel.destroy(); - }); - - QUnit.test('Favorites Use by Default and Share are exclusive', async function (assert) { - assert.expect(11); - var controlPanel = await createControlPanel({ - model: 'partner', - arch: "<search></search>", - data: this.data, - searchMenuTypes: ['favorite'], - }); - testUtils.dom.click(controlPanel.$('.o_favorites_menu_button')); - testUtils.dom.click(controlPanel.$('button.o_add_favorite')); - var $checkboxes = controlPanel.$('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)'); - - testUtils.dom.click($checkboxes.eq(0)); - assert.ok($checkboxes[0].checked, 'The first checkbox is checked'); - assert.notOk($checkboxes[1].checked, 'The second checkbox is not checked'); - - testUtils.dom.click($checkboxes.eq(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)'); - - testUtils.dom.click($checkboxes.eq(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)'); - - testUtils.dom.click($checkboxes.eq(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('load filter', async function (assert) { - assert.expect(1); - - var controlPanel = await createControlPanel({ - model: 'partner', - arch: "<search></search>", - data: this.data, - searchMenuTypes: ['filter'], - intercepts: { - load_filters: function (ev) { - ev.data.on_success([ - { - user_id: [2,"Mitchell Admin"], - name: 'sorted filter', - id: 5, - context: {}, - sort: "[\"foo\", \"-bar\"]", - domain: "[('user_id', '=', uid)]", - } - ]); - } - } - }); - - _.each(controlPanel.exportState().filters, function (filter) { - if (filter.type === 'favorite') { - assert.deepEqual(filter.orderedBy, - [{ - name: 'foo', - asc: true, - }, { - name: 'bar', - asc: false, - }], - 'the filter should have the right orderedBy values'); - } - }); - - controlPanel.destroy(); - }); - - QUnit.test('save filter', async function (assert) { - assert.expect(1); - - var controlPanel = await createControlPanel({ - model: 'partner', - arch: "<search></search>", - data: this.data, - searchMenuTypes: ['filter'], - intercepts: { - create_filter: function (ev) { - assert.strictEqual(ev.data.filter.sort, "[\"foo\",\"bar desc\"]", - 'The right format for the string "sort" should be sent to the server'); - }, - get_controller_query_params: function (ev) { - ev.data.callback({ - orderedBy: [ - { - name: 'foo', - asc: true, - }, { - name: 'bar', - asc: false, - } - ] - }); - } - } - }); - - controlPanel._onNewFavorite({ - data: { - description: 'Morbier', - type: 'favorite', - }, - stopPropagation: function () {return;} - }); - - controlPanel.destroy(); - }); -}); -}); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 8928ab3b210..b251bf20138 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -679,7 +679,6 @@ <script type="text/javascript" src="/web/static/tests/views/abstract_controller_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/abstract_view_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/views/control_panel_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/form_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/graph_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/list_tests.js"></script> @@ -691,11 +690,7 @@ <script type="text/javascript" src="/web/static/tests/views/abstract_view_banner_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/kanban_model_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/view_dialogs_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/views/search_filter_menu_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/views/search_groupby_menu_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/widgets/dropdown_menu_tests.js"></script> <script type="text/javascript" src="/web/static/tests/views/search_panel_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/views/search_view_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/ajax_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/registry_tests.js"></script> <script type="text/javascript" src="/web/static/tests/core/py_utils_tests.js"></script> @@ -714,13 +709,26 @@ <script type="text/javascript" src="/web/static/tests/core/dom_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/action_manager_tests.js"></script> - <script type="text/javascript" src="/web/static/tests/chrome/pager_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/keyboard_navigation_mixin_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/menu_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/user_menu_tests.js"></script> <script type="text/javascript" src="/web/static/tests/chrome/systray_tests.js"></script> <script type="text/javascript" src="/web/static/tests/components/custom_file_input_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/components/datepicker_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/components/pager_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/components/action_menus_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/components/dropdown_menu_tests.js"></script> + + <script type="text/javascript" src="/web/static/tests/control_panel/control_panel_model_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/control_panel_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/favorite_menu_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/custom_filter_item_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/filter_menu_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/custom_group_by_item_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/groupby_menu_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/search_bar_tests.js"></script> + <script type="text/javascript" src="/web/static/tests/control_panel/time_range_menu_tests.js"></script> <script type="text/javascript" src="/web/static/tests/widgets/company_switcher_tests.js"/> <script type="text/javascript" src="/web/static/tests/widgets/data_export_tests.js"/> |
