[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>
This commit is contained in:
co-authored by
Mathieu Duckerts-Antoine
parent
e68d3bb09d
commit
bb8eb766a8
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' },
|
||||
]);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user