summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/control_panel
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/control_panel
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/control_panel')
-rw-r--r--addons/web/static/tests/control_panel/comparison_menu_tests.js145
-rw-r--r--addons/web/static/tests/control_panel/control_panel_model_extension_tests.js420
-rw-r--r--addons/web/static/tests/control_panel/control_panel_tests.js256
-rw-r--r--addons/web/static/tests/control_panel/custom_filter_item_tests.js496
-rw-r--r--addons/web/static/tests/control_panel/custom_group_by_item_tests.js74
-rw-r--r--addons/web/static/tests/control_panel/favorite_menu_tests.js625
-rw-r--r--addons/web/static/tests/control_panel/filter_menu_tests.js503
-rw-r--r--addons/web/static/tests/control_panel/groupby_menu_tests.js478
-rw-r--r--addons/web/static/tests/control_panel/search_bar_tests.js702
-rw-r--r--addons/web/static/tests/control_panel/search_utils_tests.js362
10 files changed, 4061 insertions, 0 deletions
diff --git a/addons/web/static/tests/control_panel/comparison_menu_tests.js b/addons/web/static/tests/control_panel/comparison_menu_tests.js
new file mode 100644
index 00000000..e376904f
--- /dev/null
+++ b/addons/web/static/tests/control_panel/comparison_menu_tests.js
@@ -0,0 +1,145 @@
+odoo.define('web.comparison_menu_tests', function (require) {
+ "use strict";
+
+ const {
+ controlPanel: cpHelpers,
+ createControlPanel,
+ mock,
+ } = require('web.test_utils');
+
+ const { patchDate } = mock;
+ const searchMenuTypes = ['filter', 'comparison'];
+
+ QUnit.module('Components', {
+ beforeEach() {
+ this.fields = {
+ birthday: { string: "Birthday", type: "date", store: true, sortable: true },
+ date_field: { string: "Date", type: "date", store: true, sortable: true },
+ float_field: { string: "Float", type: "float", group_operator: 'sum' },
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ };
+ this.cpModelConfig = {
+ arch: `
+ <search>
+ <filter name="birthday" date="birthday"/>
+ <filter name="date_field" date="date_field"/>
+ </search>`,
+ fields: this.fields,
+ searchMenuTypes,
+ };
+ },
+ }, function () {
+
+ QUnit.module('ComparisonMenu');
+
+ QUnit.test('simple rendering', async function (assert) {
+ assert.expect(6);
+
+ const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
+ const params = {
+ cpModelConfig: this.cpModelConfig,
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.containsOnce(controlPanel, ".o_dropdown.o_filter_menu");
+ assert.containsNone(controlPanel, ".o_dropdown.o_comparison_menu");
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
+ await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January");
+
+ assert.containsOnce(controlPanel, 'div.o_comparison_menu > button i.fa.fa-adjust');
+ assert.strictEqual(controlPanel.el.querySelector('div.o_comparison_menu > button span').innerText.trim(), "Comparison");
+
+ await cpHelpers.toggleComparisonMenu(controlPanel);
+
+ const comparisonOptions = [...controlPanel.el.querySelectorAll(
+ '.o_comparison_menu li'
+ )];
+ assert.strictEqual(comparisonOptions.length, 2);
+ assert.deepEqual(
+ comparisonOptions.map(e => e.innerText),
+ ["Birthday: Previous Period", "Birthday: Previous Year"]
+ );
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('activate a comparison works', async function (assert) {
+ assert.expect(5);
+
+ const unpatchDate = patchDate(1997, 0, 9, 12, 0, 0);
+ const params = {
+ cpModelConfig: this.cpModelConfig,
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
+ await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", "January");
+ await cpHelpers.toggleComparisonMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Period");
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
+ "Birthday: January 1997",
+ "Birthday: Previous Period",
+ ]);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date");
+ await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "December");
+ await cpHelpers.toggleComparisonMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date: Previous Year");
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
+ ["Birthday: January 1997", "Date: December 1996"].join("or"),
+ "Date: Previous Year",
+ ]);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date");
+ await cpHelpers.toggleMenuItemOption(controlPanel, "Date", "1996");
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
+ "Birthday: January 1997",
+ ]);
+
+ await cpHelpers.toggleComparisonMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Birthday: Previous Year");
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [
+ "Birthday: January 1997",
+ "Birthday: Previous Year",
+ ]);
+
+ await cpHelpers.removeFacet(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('no timeRanges key in search query if "comparison" not in searchMenuTypes', async function (assert) {
+ assert.expect(1);
+
+ this.cpModelConfig.searchMenuTypes = ['filter'];
+ const params = {
+ cpModelConfig: this.cpModelConfig,
+ cpProps: { fields: this.fields, searchMenuTypes: ['filter'] },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Birthday");
+ await cpHelpers.toggleMenuItemOption(controlPanel, "Birthday", 0);
+
+ assert.notOk("timeRanges" in controlPanel.getQuery());
+
+ controlPanel.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js
new file mode 100644
index 00000000..01ab0be4
--- /dev/null
+++ b/addons/web/static/tests/control_panel/control_panel_model_extension_tests.js
@@ -0,0 +1,420 @@
+odoo.define("web/static/tests/control_panel/control_panel_model_extension_tests.js", function (require) {
+ "use strict";
+
+ const ActionModel = require("web/static/src/js/views/action_model.js");
+ const makeTestEnvironment = require('web.test_env');
+
+ function createModel(params = {}) {
+ const archs = (params.arch && { search: params.arch, }) || {};
+ const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
+ const extensions = {
+ ControlPanel: {
+ context: params.context,
+ archNodes: controlPanelInfo.children,
+ dynamicFilters: params.dynamicFilters,
+ favoriteFilters: params.favoriteFilters,
+ env: makeTestEnvironment(),
+ fields: params.fields,
+ },
+ };
+ const model = new ActionModel(extensions);
+ return model;
+ }
+ function sanitizeFilters(model) {
+ const cpme = model.extensions[0].find(
+ (ext) => ext.constructor.name === "ControlPanelModelExtension"
+ );
+ const filters = Object.values(cpme.state.filters);
+ return filters.map(filter => {
+ const copy = Object.assign({}, filter);
+ delete copy.groupId;
+ delete copy.groupNumber;
+ delete copy.id;
+ return copy;
+ });
+ }
+
+ QUnit.module('ControlPanelModelExtension', {
+ beforeEach() {
+ this.fields = {
+ display_name: { string: "Displayed name", type: 'char' },
+ foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true },
+ date_field: { string: "Date", type: "date", store: true, sortable: true },
+ float_field: { string: "Float", type: "float" },
+ bar: { string: "Bar", type: "many2one", relation: 'partner' },
+ };
+ }
+ }, function () {
+ QUnit.module('Arch parsing');
+
+ QUnit.test('empty arch', async function (assert) {
+ assert.expect(1);
+ const model = createModel();
+ assert.deepEqual(sanitizeFilters(model), []);
+ });
+
+ QUnit.test('one field tag', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <field name="bar"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Bar",
+ fieldName: "bar",
+ fieldType: "many2one",
+ type: "field"
+ },
+ ]);
+ });
+
+ QUnit.test('one separator tag', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <separator/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), []);
+ });
+
+ QUnit.test('one separator tag and one field tag', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <separator/>
+ <field name="bar"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Bar",
+ fieldName: "bar",
+ fieldType: "many2one",
+ type: "field"
+ },
+ ]);
+ });
+
+ QUnit.test('one filter tag', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <filter name="filter" string="Hello" domain="[]"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Hello",
+ domain: "[]",
+ type: "filter",
+ },
+ ]);
+ });
+
+ QUnit.test('one filter tag with date attribute', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <filter name="date_filter" string="Date" date="date_field"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ const dateFilterId = Object.values(model.get('filters'))[0].id;
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ defaultOptionId: "this_month",
+ description: "Date",
+ fieldName: "date_field",
+ fieldType: "date",
+ isDateFilter: true,
+ hasOptions: true,
+ type: "filter"
+ },
+ {
+ comparisonOptionId: "previous_period",
+ dateFilterId,
+ description: "Date: Previous Period",
+ type: "comparison"
+ },
+ {
+ comparisonOptionId: "previous_year",
+ dateFilterId,
+ description: "Date: Previous Year",
+ type: "comparison"
+ }
+ ]);
+ });
+
+ QUnit.test('one groupBy tag', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <filter name="groupby" string="Hi" context="{ 'group_by': 'date_field:day'}"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ defaultOptionId: "day",
+ description: "Hi",
+ fieldName: "date_field",
+ fieldType: "date",
+ hasOptions: true,
+ type: "groupBy",
+ },
+ ]);
+ });
+
+ QUnit.test('two filter tags', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <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 = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Hello One",
+ domain: "[]",
+ type: "filter",
+ },
+ {
+ description: "Hello Two",
+ domain: "[('bar', '=', 3)]",
+ type: "filter",
+ },
+ ]);
+ });
+
+ QUnit.test('two filter tags separated by a separator', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <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 = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Hello One",
+ domain: "[]",
+ type: "filter",
+ },
+ {
+ description: "Hello Two",
+ domain: "[('bar', '=', 3)]",
+ type: "filter",
+ },
+ ]);
+ });
+
+ QUnit.test('one filter tag and one field', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <filter name="filter" string="Hello" domain="[]"/>
+ <field name="bar"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Hello",
+ domain: "[]",
+ type: "filter",
+ },
+ {
+ description: "Bar",
+ fieldName: "bar",
+ fieldType: "many2one",
+ type: "field",
+ },
+ ]);
+ });
+
+ QUnit.test('two field tags', async function (assert) {
+ assert.expect(1);
+ const arch = `
+ <search>
+ <field name="foo"/>
+ <field name="bar"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: "Foo",
+ fieldName: "foo",
+ fieldType: "char",
+ type: "field"
+ },
+ {
+ description: "Bar",
+ fieldName: "bar",
+ fieldType: "many2one",
+ type: "field"
+ },
+ ]);
+ });
+
+ QUnit.module('Preparing initial state');
+
+ QUnit.test('process favorite filters', async function (assert) {
+ assert.expect(1);
+ const favoriteFilters = [{
+ user_id: [2, "Mitchell Admin"],
+ name: 'Sorted filter',
+ id: 5,
+ context: {
+ group_by: ['foo', 'bar']
+ },
+ sort: '["foo", "-bar"]',
+ domain: "[('user_id', '=', uid)]",
+ }];
+
+ const model = createModel({ favoriteFilters });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ context: {},
+ description: "Sorted filter",
+ domain: "[('user_id', '=', uid)]",
+ groupBys: ['foo', 'bar'],
+ orderedBy: [
+ {
+ asc: true,
+ name: "foo"
+ },
+ {
+ asc: false,
+ name: "bar"
+ }
+ ],
+ removable: true,
+ serverSideId: 5,
+ type: "favorite",
+ userId: 2
+ },
+ ]);
+
+ });
+
+ QUnit.test('process dynamic filters', async function (assert) {
+ assert.expect(1);
+ const dynamicFilters = [{
+ description: 'Quick search',
+ domain: [['id', 'in', [1, 3, 4]]]
+ }];
+
+ const model = createModel({ dynamicFilters });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: 'Quick search',
+ domain: "[[\"id\",\"in\",[1,3,4]]]",
+ isDefault: true,
+ type: 'filter'
+ },
+ ]);
+
+ });
+
+ QUnit.test('falsy search defaults are not activated', async function (assert) {
+ assert.expect(1);
+
+ const context = {
+ search_default_filter: false,
+ search_default_bar: 0,
+ search_default_groupby: 2,
+ };
+ const arch = `
+ <search>
+ <filter name="filter" string="Hello" domain="[]"/>
+ <filter name="groupby" string="Goodbye" context="{'group_by': 'foo'}"/>
+ <field name="bar"/>
+ </search>`;
+ const fields = this.fields;
+ const model = createModel({ arch, fields, context });
+ // only the truthy filter 'groupby' has isDefault true
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ description: 'Hello',
+ domain: "[]",
+ type: 'filter',
+ },
+ {
+ description: 'Bar',
+ fieldName: 'bar',
+ fieldType: 'many2one',
+ type: 'field',
+ },
+ {
+ defaultRank: 2,
+ description: 'Goodbye',
+ fieldName: 'foo',
+ fieldType: 'char',
+ isDefault: true,
+ type: 'groupBy',
+ },
+ ]);
+
+ });
+
+ QUnit.test('search defaults on X2M fields', async function (assert) {
+ assert.expect(1);
+
+ const context = {
+ search_default_otom: [1, 2],
+ search_default_mtom: [1, 2]
+ };
+ const fields = this.fields;
+ fields.otom = { string: "O2M", type: "one2many", relation: 'partner' };
+ fields.mtom = { string: "M2M", type: "many2many", relation: 'partner' };
+ const arch = `
+ <search>
+ <field name="otom"/>
+ <field name="mtom"/>
+ </search>`;
+ const model = createModel({ arch, fields, context });
+ assert.deepEqual(sanitizeFilters(model), [
+ {
+ "defaultAutocompleteValue": {
+ "label": [1, 2],
+ "operator": "ilike",
+ "value": [1, 2]
+ },
+ "defaultRank": -10,
+ "description": "O2M",
+ "fieldName": "otom",
+ "fieldType": "one2many",
+ "isDefault": true,
+ "type": "field"
+ },
+ {
+ "defaultAutocompleteValue": {
+ "label": [1, 2],
+ "operator": "ilike",
+ "value": [1, 2]
+ },
+ "defaultRank": -10,
+ "description": "M2M",
+ "fieldName": "mtom",
+ "fieldType": "many2many",
+ "isDefault": true,
+ "type": "field"
+ }
+ ]);
+
+ });
+
+ });
+});
diff --git a/addons/web/static/tests/control_panel/control_panel_tests.js b/addons/web/static/tests/control_panel/control_panel_tests.js
new file mode 100644
index 00000000..c9e89387
--- /dev/null
+++ b/addons/web/static/tests/control_panel/control_panel_tests.js
@@ -0,0 +1,256 @@
+odoo.define('web.control_panel_tests', function (require) {
+ "use strict";
+
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createControlPanel } = testUtils;
+
+ QUnit.module('ControlPanel', {
+ beforeEach() {
+ this.fields = {
+ display_name: { string: "Displayed name", type: 'char', searchable: true },
+ foo: { string: "Foo", type: "char", default: "My little Foo Value", store: true, sortable: true, searchable: true },
+ date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
+ float_field: { string: "Float", type: "float", searchable: true },
+ bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true },
+ };
+ }
+ }, function () {
+
+ QUnit.test('default field operator', async function (assert) {
+ assert.expect(2);
+
+ const fields = {
+ foo_op: { string: "Foo Op", type: "char", store: true, sortable: true, searchable: true },
+ foo: { string: "Foo", type: "char", store: true, sortable: true, searchable: true },
+ bar_op: { string: "Bar Op", type: "many2one", relation: 'partner', searchable: true },
+ bar: { string: "Bar", type: "many2one", relation: 'partner', searchable: true },
+ selec: { string: "Selec", type: "selection", selection: [['red', "Red"], ['black', "Black"]] },
+ };
+ const arch = `
+ <search>
+ <field name="bar"/>
+ <field name="bar_op" operator="child_of"/>
+ <field name="foo"/>
+ <field name="foo_op" operator="="/>
+ <field name="selec"/>
+ </search>`;
+ const searchMenuTypes = [];
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields,
+ context: {
+ show_filterC: true,
+ search_default_bar: 10,
+ search_default_bar_op: 10,
+ search_default_foo: "foo",
+ search_default_foo_op: "foo_op",
+ search_default_selec: 'red',
+ },
+ searchMenuTypes,
+ },
+ cpProps: { fields, searchMenuTypes },
+ env: {
+ session: {
+ async rpc() {
+ return [[10, "Deco Addict"]];
+ },
+ },
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(
+ cpHelpers.getFacetTexts(controlPanel).map(t => t.replace(/\s/g, "")),
+ [
+ "BarDecoAddict",
+ "BarOpDecoAddict",
+ "Foofoo",
+ "FooOpfoo_op",
+ "SelecRed"
+ ]
+ );
+ assert.deepEqual(
+ controlPanel.getQuery().domain,
+ [
+ "&", "&", "&", "&",
+ ["bar", "=", 10],
+ ["bar_op", "child_of", 10],
+ ["foo", "ilike", "foo"],
+ ["foo_op", "=", "foo_op"],
+ ["selec", "=", "red"],
+ ]
+ );
+
+ controlPanel.destroy();
+ });
+
+ QUnit.module('Keyboard navigation');
+
+ QUnit.test('remove a facet with backspace', async function (assert) {
+ assert.expect(2);
+
+ const params = {
+ cpModelConfig: {
+ arch: `<search> <field name="foo"/></search>`,
+ fields: this.fields,
+ context: { search_default_foo: "a" },
+ searchMenuTypes: ['filter'],
+ },
+ cpProps: { fields: this.fields },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo\na']);
+
+ // delete a facet
+ const searchInput = controlPanel.el.querySelector('input.o_searchview_input');
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' });
+
+ assert.containsNone(controlPanel, 'div.o_searchview div.o_searchview_facet');
+
+ // delete nothing (should not crash)
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Backspace' });
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('fields and filters with groups/invisible attribute', async function (assert) {
+ // navigation and automatic menu closure don't work here (i don't know why yet) -->
+ // should be tested separatly
+ assert.expect(16);
+
+ const arch = `
+ <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="filterC" string="FC" invisible="not context.get('show_filterC')" 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 = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ context: {
+ show_filterC: true,
+ search_default_display_name: 'value',
+ search_default_filterB: true,
+ search_default_groupByB: true
+ },
+ searchMenuTypes
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ function selectorContainsValue(selector, value, shouldContain) {
+ const elements = [...controlPanel.el.querySelectorAll(selector)];
+ const regExp = new RegExp(value);
+ const matches = elements.filter(el => regExp.test(el.innerText.replace(/\s/g, "")));
+ assert.strictEqual(matches.length, shouldContain ? 1 : 0,
+ `${selector} in the control panel should${shouldContain ? '' : ' not'} contain "${value}".`
+ );
+ }
+
+ // default filters/fields should be activated even if invisible
+ assert.containsN(controlPanel, 'div.o_searchview_facet', 3);
+ selectorContainsValue('.o_searchview_facet', "FooBvalue", true);
+ selectorContainsValue('.o_searchview_facet .o_facet_values', "FB", true);
+ selectorContainsValue('.o_searchview_facet .o_facet_values', "GB", true);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+
+ selectorContainsValue('.o_menu_item a', "FA", true);
+ selectorContainsValue('.o_menu_item a', "FB", false);
+ selectorContainsValue('.o_menu_item a', "FC", true);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+
+ selectorContainsValue('.o_menu_item a', "GA", true);
+ selectorContainsValue('.o_menu_item a', "GB", false);
+
+ // 'a' to filter nothing on bar
+ await cpHelpers.editSearch(controlPanel, 'a');
+
+ // the only item in autocomplete menu should be FooA: a
+ selectorContainsValue('.o_searchview_autocomplete', "SearchFooAfor:a", true);
+ await cpHelpers.validateSearch(controlPanel);
+ selectorContainsValue('.o_searchview_facet', "FooAa", true);
+
+ // The items in the Filters menu and the Group By menu should be the same as before
+ await cpHelpers.toggleFilterMenu(controlPanel);
+
+ selectorContainsValue('.o_menu_item a', "FA", true);
+ selectorContainsValue('.o_menu_item a', "FB", false);
+ selectorContainsValue('.o_menu_item a', "FC", true);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+
+ selectorContainsValue('.o_menu_item a', "GA", true);
+ selectorContainsValue('.o_menu_item a', "GB", false);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('invisible fields and filters with unknown related fields should not be rendered', async function (assert) {
+ assert.expect(2);
+
+ // This test case considers that the current user is not a member of
+ // the "base.group_system" group and both "bar" and "date_field" fields
+ // have field-level access control that limit access to them only from
+ // that group.
+ //
+ // As MockServer currently does not support "groups" access control, we:
+ //
+ // - emulate field-level access control of fields_get() by removing
+ // "bar" and "date_field" from the model fields
+ // - set filters with groups="base.group_system" as `invisible=1` in
+ // view to emulate the behavior of fields_view_get()
+ // [see ir.ui.view `_apply_group()`]
+
+ delete this.fields.bar;
+ delete this.fields.date_field;
+
+ const searchMenuTypes = [];
+ const params = {
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.containsNone(controlPanel.el, 'div.o_search_options div.o_filter_menu',
+ "there should not be filter dropdown");
+ assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu',
+ "there should not be groupby dropdown");
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('groupby menu is not rendered if searchMenuTypes does not have groupBy', async function (assert) {
+ assert.expect(2);
+
+ const arch = `<search/>`;
+ const searchMenuTypes = ['filter'];
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.containsOnce(controlPanel.el, 'div.o_search_options div.o_filter_menu');
+ assert.containsNone(controlPanel.el, 'div.o_search_options div.o_group_by_menu');
+
+ controlPanel.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/custom_filter_item_tests.js b/addons/web/static/tests/control_panel/custom_filter_item_tests.js
new file mode 100644
index 00000000..dcd30428
--- /dev/null
+++ b/addons/web/static/tests/control_panel/custom_filter_item_tests.js
@@ -0,0 +1,496 @@
+odoo.define('web.filter_menu_generator_tests', function (require) {
+ "use strict";
+
+ const Domain = require('web.Domain');
+ const CustomFilterItem = require('web.CustomFilterItem');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
+ const pyUtils = require('web.py_utils');
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createComponent } = testUtils;
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.fields = {
+ date_field: { name: 'date_field', string: "A date", type: 'date', searchable: true },
+ date_time_field: { name: 'date_time_field', string: "DateTime", type: 'datetime', searchable: true },
+ boolean_field: { name: 'boolean_field', string: "Boolean Field", type: 'boolean', default: true, searchable: true },
+ char_field: { name: 'char_field', string: "Char Field", type: 'char', default: "foo", trim: true, searchable: true },
+ float_field: { name: 'float_field', string: "Floaty McFloatface", type: 'float', searchable: true },
+ color: { name: 'color', string: "Color", type: 'selection', selection: [['black', "Black"], ['white', "White"]], searchable: true },
+ };
+ },
+ }, function () {
+
+ QUnit.module('CustomFilterItem');
+
+ QUnit.test('basic rendering', async function (assert) {
+ assert.expect(17);
+
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: {
+ searchModel: new ActionModel(),
+ },
+ });
+
+ assert.strictEqual(cfi.el.innerText.trim(), "Add Custom Filter");
+ assert.hasClass(cfi.el, 'o_generator_menu');
+ assert.strictEqual(cfi.el.children.length, 1);
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+
+ // Single condition
+ assert.containsOnce(cfi, 'div.o_filter_condition');
+ assert.containsOnce(cfi, 'div.o_filter_condition > select.o_generator_menu_field');
+ assert.containsOnce(cfi, 'div.o_filter_condition > select.o_generator_menu_operator');
+ assert.containsOnce(cfi, 'div.o_filter_condition > span.o_generator_menu_value');
+ assert.containsNone(cfi, 'div.o_filter_condition .o_or_filter');
+ assert.containsNone(cfi, 'div.o_filter_condition .o_generator_menu_delete');
+
+ // no deletion allowed on single condition
+ assert.containsNone(cfi, 'div.o_filter_condition > i.o_generator_menu_delete');
+
+ // Buttons
+ assert.containsOnce(cfi, 'div.o_add_filter_menu');
+ assert.containsOnce(cfi, 'div.o_add_filter_menu > button.o_apply_filter');
+ assert.containsOnce(cfi, 'div.o_add_filter_menu > button.o_add_condition');
+
+ assert.containsOnce(cfi, 'div.o_filter_condition');
+
+ await testUtils.dom.click('button.o_add_condition');
+
+ assert.containsN(cfi, 'div.o_filter_condition', 2);
+ assert.containsOnce(cfi, 'div.o_filter_condition .o_or_filter');
+ assert.containsN(cfi, 'div.o_filter_condition .o_generator_menu_delete', 2);
+
+ cfi.destroy();
+ });
+
+ QUnit.test('selection field: default and updated value', async function (assert) {
+ assert.expect(4);
+
+ let expectedFilters;
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ assert.deepEqual(preFilters, expectedFilters);
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: { searchModel },
+ });
+
+ // Default value
+ expectedFilters = [{
+ description: 'Color is "black"',
+ domain: '[["color","=","black"]]',
+ type: 'filter',
+ }];
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color');
+ await cpHelpers.applyFilter(cfi);
+
+ // Updated value
+ expectedFilters = [{
+ description: 'Color is "white"',
+ domain: '[["color","=","white"]]',
+ type: 'filter',
+ }];
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'color');
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_value select'), 'white');
+ await cpHelpers.applyFilter(cfi);
+
+ cfi.destroy();
+ });
+
+ QUnit.test('adding a simple filter works', async function (assert) {
+ assert.expect(6);
+
+ delete this.fields.date_field;
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const preFilter = preFilters[0];
+ assert.strictEqual(preFilter.type, 'filter');
+ assert.strictEqual(preFilter.description, 'Boolean Field is true');
+ assert.strictEqual(preFilter.domain, '[["boolean_field","=",True]]');
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: { searchModel },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await cpHelpers.applyFilter(cfi);
+
+ // The only thing visible should be the button 'Add Custome Filter';
+ assert.strictEqual(cfi.el.children.length, 1);
+ assert.containsOnce(cfi, 'button.o_add_custom_filter');
+
+ cfi.destroy();
+ });
+
+ QUnit.test('filtering by ID interval works', async function (assert) {
+ assert.expect(2);
+ this.fields.id_field = { name: 'id_field', string: "ID", type: "id", searchable: true };
+
+ const expectedDomains = [
+ [['id_field','>', 10]],
+ [['id_field','<=', 20]],
+ ];
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const preFilter = preFilters[0];
+ // this step combine a tokenization/parsing followed by a string formatting
+ let domain = pyUtils.assembleDomains([preFilter.domain]);
+ domain = Domain.prototype.stringToArray(domain);
+ assert.deepEqual(domain, expectedDomains.shift());
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: { searchModel },
+ });
+
+ async function testValue(operator, value) {
+ // open filter menu generator, select ID field, switch operator, type value, then click apply
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('select.o_generator_menu_field'), 'id_field');
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_operator'), operator);
+ await testUtils.fields.editInput(cfi.el.querySelector(
+ 'div.o_filter_condition > span.o_generator_menu_value input'),
+ value
+ );
+ await cpHelpers.applyFilter(cfi);
+ }
+
+ for (const domain of expectedDomains) {
+ await testValue(domain[0][1], domain[0][2]);
+ }
+
+ cfi.destroy();
+ });
+
+
+ QUnit.test('commit search with an extended proposition with field char does not cause a crash', async function (assert) {
+ assert.expect(12);
+
+ this.fields.many2one_field = { name: 'many2one_field', string: "Trululu", type: "many2one", searchable: true };
+ const expectedDomains = [
+ [['many2one_field', 'ilike', `a`]],
+ [['many2one_field', 'ilike', `"a"`]],
+ [['many2one_field', 'ilike', `'a'`]],
+ [['many2one_field', 'ilike', `'`]],
+ [['many2one_field', 'ilike', `"`]],
+ [['many2one_field', 'ilike', `\\`]],
+ ];
+ const testedValues = [`a`, `"a"`, `'a'`, `'`, `"`, `\\`];
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const preFilter = preFilters[0];
+ // this step combine a tokenization/parsing followed by a string formatting
+ let domain = pyUtils.assembleDomains([preFilter.domain]);
+ domain = Domain.prototype.stringToArray(domain);
+ assert.deepEqual(domain, expectedDomains.shift());
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: { searchModel },
+ });
+
+ async function testValue(value) {
+ // open filter menu generator, select trululu field and enter string `a`, then click apply
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('select.o_generator_menu_field'), 'many2one_field');
+ await testUtils.fields.editInput(cfi.el.querySelector(
+ 'div.o_filter_condition > span.o_generator_menu_value input'),
+ value
+ );
+ await cpHelpers.applyFilter(cfi);
+ }
+
+ for (const value of testedValues) {
+ await testValue(value);
+ }
+
+ delete ActionModel.registry.map.testExtension;
+ cfi.destroy();
+ });
+
+ QUnit.test('custom filter datetime with equal operator', async function (assert) {
+ assert.expect(5);
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const preFilter = preFilters[0];
+ assert.strictEqual(preFilter.description,
+ 'DateTime is equal to "02/22/2017 11:00:00"',
+ "description should be in localized format");
+ assert.deepEqual(preFilter.domain,
+ '[["date_time_field","=","2017-02-22 15:00:00"]]',
+ "domain should be in UTC format");
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ session: {
+ getTZOffset() {
+ return -240;
+ },
+ },
+ env: { searchModel },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'date_time_field');
+
+ assert.strictEqual(cfi.el.querySelector('.o_generator_menu_field').value, 'date_time_field');
+ assert.strictEqual(cfi.el.querySelector('.o_generator_menu_operator').value, 'between');
+
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_operator'), '=');
+ await testUtils.fields.editSelect(cfi.el.querySelector('div.o_filter_condition > span.o_generator_menu_value input'), '02/22/2017 11:00:00'); // in TZ
+ await cpHelpers.applyFilter(cfi);
+
+ cfi.destroy();
+ });
+
+ QUnit.test('custom filter datetime between operator', async function (assert) {
+ assert.expect(5);
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const preFilter = preFilters[0];
+ assert.strictEqual(preFilter.description,
+ 'DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"',
+ "description should be in localized format");
+ assert.deepEqual(preFilter.domain,
+ '[["date_time_field",">=","2017-02-22 15:00:00"]' +
+ ',["date_time_field","<=","2017-02-22 21:00:00"]]',
+ "domain should be in UTC format");
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ session: {
+ getTZOffset() {
+ return -240;
+ },
+ },
+ env: { searchModel },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.fields.editSelect(cfi.el.querySelector('.o_generator_menu_field'), 'date_time_field');
+
+ assert.strictEqual(cfi.el.querySelector('.o_generator_menu_field').value, 'date_time_field');
+ assert.strictEqual(cfi.el.querySelector('.o_generator_menu_operator').value, 'between');
+
+ const valueInputs = cfi.el.querySelectorAll('.o_generator_menu_value .o_input');
+ await testUtils.fields.editSelect(valueInputs[0], '02/22/2017 11:00:00'); // in TZ
+ await testUtils.fields.editSelect(valueInputs[1], '02-22-2017 17:00:00'); // in TZ
+ await cpHelpers.applyFilter(cfi);
+
+ cfi.destroy();
+ });
+
+ QUnit.test('input value parsing', async function (assert) {
+ assert.expect(7);
+
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: {
+ searchModel: new ActionModel(),
+ },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.dom.click('button.o_add_condition');
+
+ const [floatSelect, idSelect] = cfi.el.querySelectorAll('.o_generator_menu_field');
+ await testUtils.fields.editSelect(floatSelect, 'float_field');
+ await testUtils.fields.editSelect(idSelect, 'id');
+
+ const [floatInput, idInput] = cfi.el.querySelectorAll('.o_generator_menu_value .o_input');
+
+ // Default values
+ assert.strictEqual(floatInput.value, "0.0");
+ assert.strictEqual(idInput.value, "0");
+
+ // Float parsing
+ await testUtils.fields.editInput(floatInput, "4.2");
+ assert.strictEqual(floatInput.value, "4.2");
+ await testUtils.fields.editInput(floatInput, "DefinitelyValidFloat");
+ // String input in a number input gives "", which is parsed as 0
+ assert.strictEqual(floatInput.value, "0.0");
+
+ // Number parsing
+ await testUtils.fields.editInput(idInput, "4");
+ assert.strictEqual(idInput.value, "4");
+ await testUtils.fields.editInput(idInput, "4.2");
+ assert.strictEqual(idInput.value, "4");
+ await testUtils.fields.editInput(idInput, "DefinitelyValidID");
+ // String input in a number input gives "", which is parsed as 0
+ assert.strictEqual(idInput.value, "0");
+
+ cfi.destroy();
+ });
+
+ QUnit.test('input value parsing with language', async function (assert) {
+ assert.expect(5);
+
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: {
+ searchModel: new ActionModel(),
+ _t: Object.assign(s => s, { database: { parameters: { decimal_point: "," } }}),
+ },
+ translateParameters: {
+ decimal_point: ",",
+ thousands_sep: "",
+ grouping: [3, 0],
+ },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.dom.click('button.o_add_condition');
+
+ const [floatSelect] = cfi.el.querySelectorAll('.o_generator_menu_field');
+ await testUtils.fields.editSelect(floatSelect, 'float_field');
+
+ const [floatInput] = cfi.el.querySelectorAll('.o_generator_menu_value .o_input');
+
+ // Default values
+ assert.strictEqual(floatInput.value, "0,0");
+
+ // Float parsing
+ await testUtils.fields.editInput(floatInput, '4,');
+ assert.strictEqual(floatInput.value, "4,");
+ await testUtils.fields.editInput(floatInput, '4,2');
+ assert.strictEqual(floatInput.value, "4,2");
+ await testUtils.fields.editInput(floatInput, '4,2,');
+ assert.strictEqual(floatInput.value, "4,2");
+ await testUtils.fields.editInput(floatInput, "DefinitelyValidFloat");
+ // The input here is a string, resulting in a parsing error instead of 0
+ assert.strictEqual(floatInput.value, "4,2");
+
+ cfi.destroy();
+ });
+
+ QUnit.test('add custom filter with multiple values', async function (assert) {
+ assert.expect(2);
+
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewFilters');
+ const preFilters = args[0];
+ const expected = [
+ {
+ description: 'A date is equal to "01/09/1997"',
+ domain: '[["date_field","=","1997-01-09"]]',
+ type: "filter",
+ },
+ {
+ description: 'Boolean Field is true',
+ domain: '[["boolean_field","=",True]]',
+ type: "filter",
+ },
+ {
+ description: 'Floaty McFloatface is equal to "7.2"',
+ domain: '[["float_field","=",7.2]]',
+ type: "filter",
+ },
+ {
+ description: 'ID is "9"',
+ domain: '[["id","=",9]]',
+ type: "filter",
+ },
+ ];
+ assert.deepEqual(preFilters, expected,
+ "Conditions should be in the correct order witht the right values.");
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cfi = await createComponent(CustomFilterItem, {
+ props: {
+ fields: this.fields,
+ },
+ env: { searchModel },
+ });
+
+ await cpHelpers.toggleAddCustomFilter(cfi);
+ await testUtils.dom.click('button.o_add_condition');
+ await testUtils.dom.click('button.o_add_condition');
+ await testUtils.dom.click('button.o_add_condition');
+ await testUtils.dom.click('button.o_add_condition');
+
+ function getCondition(index, selector) {
+ const condition = cfi.el.querySelectorAll('.o_filter_condition')[index];
+ return condition.querySelector(selector);
+ }
+
+ await testUtils.fields.editSelect(getCondition(0, '.o_generator_menu_field'), 'date_field');
+ await testUtils.fields.editSelect(getCondition(0, '.o_generator_menu_value .o_input'), '01/09/1997');
+
+ await testUtils.fields.editSelect(getCondition(1, '.o_generator_menu_field'), 'boolean_field');
+ await testUtils.fields.editInput(getCondition(1, '.o_generator_menu_operator'), '!=');
+
+ await testUtils.fields.editSelect(getCondition(2, '.o_generator_menu_field'), 'char_field');
+ await testUtils.fields.editInput(getCondition(2, '.o_generator_menu_value .o_input'), "I will be deleted anyway");
+
+ await testUtils.fields.editSelect(getCondition(3, '.o_generator_menu_field'), 'float_field');
+ await testUtils.fields.editInput(getCondition(3, '.o_generator_menu_value .o_input'), 7.2);
+
+ await testUtils.fields.editSelect(getCondition(4, '.o_generator_menu_field'), 'id');
+ await testUtils.fields.editInput(getCondition(4, '.o_generator_menu_value .o_input'), 9);
+
+ await testUtils.dom.click(getCondition(2, '.o_generator_menu_delete'));
+
+ await cpHelpers.applyFilter(cfi);
+
+ cfi.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/custom_group_by_item_tests.js b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js
new file mode 100644
index 00000000..ee8d0331
--- /dev/null
+++ b/addons/web/static/tests/control_panel/custom_group_by_item_tests.js
@@ -0,0 +1,74 @@
+odoo.define('web.groupby_menu_generator_tests', function (require) {
+ "use strict";
+
+ const CustomGroupByItem = require('web.CustomGroupByItem');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
+ const testUtils = require('web.test_utils');
+
+ const { createComponent } = testUtils;
+
+ QUnit.module('Components', {}, function () {
+
+ QUnit.module('CustomGroupByItem');
+
+ QUnit.test('click on add custom group toggle group selector', async function (assert) {
+ assert.expect(6);
+
+ const cgi = await createComponent(CustomGroupByItem, {
+ props: {
+ fields: [
+ { sortable: true, name: "date", string: 'Super Date', type: 'date' },
+ ],
+ },
+ env: {
+ searchModel: new ActionModel(),
+ },
+ });
+
+ assert.strictEqual(cgi.el.innerText.trim(), "Add Custom Group");
+ assert.hasClass(cgi.el, 'o_generator_menu');
+ assert.strictEqual(cgi.el.children.length, 1);
+
+ await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_add_custom_group_by'));
+
+ // Single select node with a single option
+ assert.containsOnce(cgi, 'div > select.o_group_by_selector');
+ assert.strictEqual(cgi.el.querySelector('div > select.o_group_by_selector option').innerText.trim(),
+ "Super Date");
+
+ // Button apply
+ assert.containsOnce(cgi, 'button.o_apply_group_by');
+
+ cgi.destroy();
+ });
+
+ QUnit.test('select a field name in Add Custom Group menu properly trigger the corresponding field', async function (assert) {
+ assert.expect(4);
+
+ const fields = [
+ { sortable: true, name: 'candlelight', string: 'Candlelight', type: 'boolean' },
+ ];
+ class MockedSearchModel extends ActionModel {
+ dispatch(method, ...args) {
+ assert.strictEqual(method, 'createNewGroupBy');
+ const field = args[0];
+ assert.deepEqual(field, fields[0]);
+ }
+ }
+ const searchModel = new MockedSearchModel();
+ const cgi = await createComponent(CustomGroupByItem, {
+ props: { fields },
+ env: { searchModel },
+ });
+
+ await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_add_custom_group_by'));
+ await testUtils.dom.click(cgi.el.querySelector('.o_generator_menu button.o_apply_group_by'));
+
+ // The only thing visible should be the button 'Add Custome Group';
+ assert.strictEqual(cgi.el.children.length, 1);
+ assert.containsOnce(cgi, 'button.o_add_custom_group_by');
+
+ cgi.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/favorite_menu_tests.js b/addons/web/static/tests/control_panel/favorite_menu_tests.js
new file mode 100644
index 00000000..4dd4571c
--- /dev/null
+++ b/addons/web/static/tests/control_panel/favorite_menu_tests.js
@@ -0,0 +1,625 @@
+odoo.define('web.favorite_menu_tests', function (require) {
+ "use strict";
+
+ const FormView = require('web.FormView');
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createControlPanel, createView, mock } = testUtils;
+ const { patchDate } = mock;
+
+ const searchMenuTypes = ['favorite'];
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.fields = {
+ bar: { string: "Bar", type: "many2one", relation: 'partner' },
+ birthday: { string: "Birthday", type: "date", store: true, sortable: true },
+ date_field: { string: "Date", type: "date", store: true, sortable: true },
+ float_field: { string: "Float", type: "float", group_operator: 'sum' },
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ };
+ },
+ }, function () {
+
+ QUnit.module('FavoriteMenu');
+
+ QUnit.test('simple rendering with no favorite', async function (assert) {
+ assert.expect(8);
+
+ const params = {
+ cpModelConfig: { searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes, action: { name: "Action Name" } },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.containsOnce(controlPanel, 'div.o_favorite_menu > button i.fa.fa-star');
+ assert.strictEqual(controlPanel.el.querySelector('div.o_favorite_menu > button span').innerText.trim(), "Favorites");
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ assert.containsNone(controlPanel, '.dropdown-divider');
+ assert.containsOnce(controlPanel, '.o_add_favorite');
+ assert.strictEqual(controlPanel.el.querySelector('.o_add_favorite > button').innerText.trim(),
+ "Save current search");
+
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+ assert.strictEqual(
+ controlPanel.el.querySelector('.o_add_favorite input[type="text"]').value,
+ 'Action Name'
+ );
+ assert.containsN(controlPanel, '.o_add_favorite .custom-checkbox input[type="checkbox"]', 2);
+ const labelEls = controlPanel.el.querySelectorAll('.o_add_favorite .custom-checkbox label');
+ assert.deepEqual(
+ [...labelEls].map(e => e.innerText.trim()),
+ ["Use by default", "Share with all users"]
+ );
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('favorites use by default and share are exclusive', async function (assert) {
+ assert.expect(11);
+
+ const params = {
+ cpModelConfig: {
+ viewInfo: { fields: this.fields },
+ searchMenuTypes
+ },
+ cpProps: {
+ fields: this.fields,
+ searchMenuTypes,
+ action: {},
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+ const checkboxes = controlPanel.el.querySelectorAll('input[type="checkbox"]');
+
+ assert.strictEqual(checkboxes.length, 2, '2 checkboxes are present');
+
+ assert.notOk(checkboxes[0].checked, 'Start: None of the checkboxes are checked (1)');
+ assert.notOk(checkboxes[1].checked, 'Start: None of the checkboxes are checked (2)');
+
+ await testUtils.dom.click(checkboxes[0]);
+ assert.ok(checkboxes[0].checked, 'The first checkbox is checked');
+ assert.notOk(checkboxes[1].checked, 'The second checkbox is not checked');
+
+ await testUtils.dom.click(checkboxes[1]);
+ assert.notOk(checkboxes[0].checked,
+ 'Clicking on the second checkbox checks it, and unchecks the first (1)');
+ assert.ok(checkboxes[1].checked,
+ 'Clicking on the second checkbox checks it, and unchecks the first (2)');
+
+ await testUtils.dom.click(checkboxes[0]);
+ assert.ok(checkboxes[0].checked,
+ 'Clicking on the first checkbox checks it, and unchecks the second (1)');
+ assert.notOk(checkboxes[1].checked,
+ 'Clicking on the first checkbox checks it, and unchecks the second (2)');
+
+ await testUtils.dom.click(checkboxes[0]);
+ assert.notOk(checkboxes[0].checked, 'End: None of the checkboxes are checked (1)');
+ assert.notOk(checkboxes[1].checked, 'End: None of the checkboxes are checked (2)');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('save filter', async function (assert) {
+ assert.expect(1);
+
+ const params = {
+ cpModelConfig: {
+ fields: this.fields,
+ searchMenuTypes
+ },
+ cpProps: {
+ fields: this.fields,
+ searchMenuTypes,
+ action: {},
+ },
+ 'get-controller-query-params': function (callback) {
+ callback({
+ orderedBy: [
+ { asc: true, name: 'foo' },
+ { asc: false, name: 'bar' }
+ ]
+ });
+ },
+ env: {
+ dataManager: {
+ create_filter: async function (filter) {
+ assert.strictEqual(filter.sort, '["foo","bar desc"]',
+ 'The right format for the string "sort" should be sent to the server'
+ );
+ }
+ }
+ },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+ await cpHelpers.editFavoriteName(controlPanel, "aaa");
+ await cpHelpers.saveFavorite(controlPanel);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('dynamic filters are saved dynamic', async function (assert) {
+ assert.expect(3);
+
+ const arch = `
+ <search>
+ <filter string="Float" name="positive" domain="[('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d'))]"/>
+ </search>
+ `;
+ const params = {
+ cpModelConfig: {
+ fields: {},
+ arch ,
+ searchMenuTypes,
+ context: {
+ search_default_positive: true,
+ }
+ },
+ cpProps: {
+ fields: {},
+ searchMenuTypes,
+ action: {},
+ },
+ 'get-controller-query-params': function (callback) {
+ callback();
+ },
+ env: {
+ dataManager: {
+ create_filter: async function (filter) {
+ assert.strictEqual(
+ filter.domain,
+ "[(\"date_field\", \">=\", (context_today() + relativedelta()).strftime(\"%Y-%m-%d\"))]"
+ );
+ return 1; // serverSideId
+ }
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Float']);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+ await cpHelpers.editFavoriteName(controlPanel, "My favorite");
+ await cpHelpers.saveFavorite(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('save filters created via autocompletion works', async function (assert) {
+ assert.expect(4);
+
+ const arch = `<search><field name="foo"/></search>`;
+ const params = {
+ cpModelConfig: {
+ fields: this.fields,
+ arch ,
+ searchMenuTypes,
+ },
+ cpProps: {
+ fields: this.fields,
+ searchMenuTypes,
+ action: {},
+ },
+ 'get-controller-query-params': function (callback) {
+ callback();
+ },
+ env: {
+ dataManager: {
+ create_filter: async function (filter) {
+ assert.strictEqual(
+ filter.domain,
+ `[["foo", "ilike", "a"]]`
+ );
+ return 1; // serverSideId
+ }
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ await cpHelpers.editSearch(controlPanel, "a");
+ await cpHelpers.validateSearch(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Foo\na"]);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+ await cpHelpers.editFavoriteName(controlPanel, "My favorite");
+ await cpHelpers.saveFavorite(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('delete an active favorite remove it both in list of favorite and in search bar', async function (assert) {
+ assert.expect(6);
+
+ const favoriteFilters = [{
+ context: "{}",
+ domain: "[['foo', '=', 'qsdf']]",
+ id: 7,
+ is_default: true,
+ name: "My favorite",
+ sort: "[]",
+ user_id: [2, "Mitchell Admin"],
+ }];
+ const params = {
+ cpModelConfig: { favoriteFilters, searchMenuTypes },
+ cpProps: { searchMenuTypes, action: {} },
+ search: function (searchQuery) {
+ const { domain } = searchQuery;
+ assert.deepEqual(domain, []);
+ },
+ env: {
+ dataManager: {
+ delete_filter: function () {
+ return Promise.resolve();
+ }
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+
+ const { domain } = controlPanel.getQuery();
+ assert.deepEqual(domain, [["foo", "=", "qsdf"]]);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["My favorite"]);
+ assert.hasClass(controlPanel.el.querySelector('.o_favorite_menu .o_menu_item > a'), 'selected');
+
+ await cpHelpers.deleteFavorite(controlPanel, 0);
+
+ // confirm deletion
+ await testUtils.dom.click(document.querySelector('div.o_dialog footer button'));
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+ const itemEls = controlPanel.el.querySelectorAll('.o_favorite_menu .o_menu_item');
+ assert.deepEqual([...itemEls].map(e => e.innerText.trim()), ["Save current search"]);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('default favorite is not activated if key search_disable_custom_filters is set to true', async function (assert) {
+ assert.expect(2);
+
+ const favoriteFilters = [{
+ context: "{}",
+ domain: "",
+ id: 7,
+ is_default: true,
+ name: "My favorite",
+ sort: "[]",
+ user_id: [2, "Mitchell Admin"],
+ }];
+ const params = {
+ cpModelConfig: {
+ favoriteFilters,
+ searchMenuTypes,
+ context: { search_disable_custom_filters: true }
+ },
+ cpProps: { searchMenuTypes, action: {} },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+
+ const { domain } = controlPanel.getQuery();
+ assert.deepEqual(domain, []);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('toggle favorite correctly clears filter, groupbys, comparison and field "options"', async function (assert) {
+ assert.expect(11);
+
+ const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0);
+
+ const favoriteFilters = [{
+ context: `
+ {
+ "group_by": ["foo"],
+ "comparison": {
+ "favorite comparison content": "bla bla..."
+ },
+ }
+ `,
+ domain: "['!', ['foo', '=', 'qsdf']]",
+ id: 7,
+ is_default: false,
+ name: "My favorite",
+ sort: "[]",
+ user_id: [2, "Mitchell Admin"],
+ }];
+ let firstSearch = true;
+ const arch = `
+ <search>
+ <field string="Foo" name="foo"/>
+ <filter string="Date Field Filter" name="positive" date="date_field" default_period="this_year"/>
+ <filter string="Date Field Groupby" name="coolName" context="{'group_by': 'date_field'}"/>
+ </search>
+ `;
+ const searchMenuTypes = ['filter', 'groupBy', 'comparison', 'favorite'];
+ const params = {
+ cpModelConfig: {
+ favoriteFilters,
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: {
+ search_default_positive: true,
+ search_default_coolName: true,
+ search_default_foo: "a",
+ }
+ },
+ cpProps: { searchMenuTypes, action: {}, fields: this.fields },
+ search: function (searchQuery) {
+ const { domain, groupBy, timeRanges } = searchQuery;
+ if (firstSearch) {
+ assert.deepEqual(domain, [['foo', 'ilike', 'a']]);
+ assert.deepEqual(groupBy, ['date_field:month']);
+ assert.deepEqual(timeRanges, {
+ comparisonId: "previous_period",
+ comparisonRange: ["&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]],
+ comparisonRangeDescription: "2018",
+ fieldDescription: "Date Field Filter",
+ fieldName: "date_field",
+ range: ["&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-12-31"]],
+ rangeDescription: "2019",
+ });
+ firstSearch = false;
+ } else {
+ assert.deepEqual(domain, ['!', ['foo', '=', 'qsdf']]);
+ assert.deepEqual(groupBy, ['foo']);
+ assert.deepEqual(timeRanges, {
+ "favorite comparison content": "bla bla...",
+ range: undefined,
+ comparisonRange: undefined,
+ });
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ const { domain, groupBy, timeRanges } = controlPanel.getQuery();
+ assert.deepEqual(domain, [
+ "&",
+ ["foo", "ilike", "a"],
+ "&",
+ ["date_field", ">=", "2019-01-01"],
+ ["date_field", "<=", "2019-12-31"]
+ ]);
+ assert.deepEqual(groupBy, ['date_field:month']);
+ assert.deepEqual(timeRanges, {});
+
+ assert.deepEqual(
+ cpHelpers.getFacetTexts(controlPanel),
+ [
+ 'Foo\na',
+ 'Date Field Filter: 2019',
+ 'Date Field Groupby: Month',
+ ]
+ );
+
+ // activate a comparison
+ await cpHelpers.toggleComparisonMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date Field Filter: Previous period");
+
+ // activate the unique existing favorite
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+
+ assert.deepEqual(
+ cpHelpers.getFacetTexts(controlPanel),
+ ["My favorite"]
+ );
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('favorites have unique descriptions (the submenus of the favorite menu are correctly updated)', async function (assert) {
+ assert.expect(3);
+
+ const favoriteFilters = [{
+ context: "{}",
+ domain: "[]",
+ id: 1,
+ is_default: false,
+ name: "My favorite",
+ sort: "[]",
+ user_id: [2, "Mitchell Admin"],
+ }];
+ const params = {
+ cpModelConfig: { favoriteFilters, searchMenuTypes },
+ cpProps: { searchMenuTypes, action: {} },
+ 'get-controller-query-params': function (callback) {
+ callback();
+ },
+ env: {
+ session: { uid: 4 },
+ services: {
+ notification: {
+ notify: function (params) {
+ assert.deepEqual(params, {
+ message: "Filter with same name already exists.",
+ type: "danger"
+ });
+ },
+ }
+ },
+ dataManager: {
+ create_filter: async function (irFilter) {
+ assert.deepEqual(irFilter, {
+ "action_id": undefined,
+ "context": { "group_by": [] },
+ "domain": "[]",
+ "is_default": false,
+ "model_id": undefined,
+ "name": "My favorite 2",
+ "sort": "[]",
+ "user_id": 4,
+ });
+ return 2; // serverSideId
+ }
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFavoriteMenu(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+
+ // first try: should fail
+ await cpHelpers.editFavoriteName(controlPanel, "My favorite");
+ await cpHelpers.saveFavorite(controlPanel);
+
+ // second try: should succeed
+ await cpHelpers.editFavoriteName(controlPanel, "My favorite 2");
+ await cpHelpers.saveFavorite(controlPanel);
+ await cpHelpers.toggleSaveFavorite(controlPanel);
+
+ // third try: should fail
+ await cpHelpers.editFavoriteName(controlPanel, "My favorite 2");
+ await cpHelpers.saveFavorite(controlPanel);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('save search filter in modal', async function (assert) {
+ assert.expect(5);
+ const data = {
+ partner: {
+ fields: {
+ date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
+ birthday: { string: "Birthday", type: "date", store: true, sortable: true },
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ bar: { string: "Bar", type: "many2one", relation: 'partner' },
+ float_field: { string: "Float", type: "float", group_operator: 'sum' },
+ },
+ records: [
+ { id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", birthday: "1983-07-15", float_field: 1 },
+ { id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", birthday: "1982-06-04", float_field: 2 },
+ { id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", birthday: "1985-09-13", float_field: 1.618 },
+ { id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", birthday: "1983-05-05", float_field: -1 },
+ { id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", birthday: "1800-01-01", float_field: 13 },
+ { id: 7, display_name: "Partner 6", },
+ { id: 8, display_name: "Partner 7", },
+ { id: 9, display_name: "Partner 8", },
+ { id: 10, display_name: "Partner 9", }
+ ],
+ },
+ };
+ const form = await createView({
+ arch: `
+ <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();
+ });
+
+ QUnit.test('modal loads saved search filters', async function (assert) {
+ assert.expect(1);
+ const data = {
+ partner: {
+ fields: {
+ bar: { string: "Bar", type: "many2one", relation: 'partner' },
+ },
+ // 10 records so that the Search button shows
+ records: Array.apply(null, Array(10)).map(function(_, i) {
+ return { id: i, display_name: "Record " + i, bar: 1 };
+ })
+ },
+ };
+ const form = await createView({
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <group>
+ <field name="bar"/>
+ </group>
+ </sheet>
+ </form>`,
+ data,
+ model: 'partner',
+ res_id: 1,
+ View: FormView,
+ interceptsPropagate: {
+ load_views: function (ev) {
+ assert.ok(ev.data.options.load_filters, "opening dialog should load the filters");
+ },
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.clickOpenDropdown('bar');
+ await testUtils.fields.many2one.clickItem('bar', 'Search');
+
+ form.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/filter_menu_tests.js b/addons/web/static/tests/control_panel/filter_menu_tests.js
new file mode 100644
index 00000000..baba1d39
--- /dev/null
+++ b/addons/web/static/tests/control_panel/filter_menu_tests.js
@@ -0,0 +1,503 @@
+odoo.define('web.filter_menu_tests', function (require) {
+ "use strict";
+
+ const testUtils = require('web.test_utils');
+
+ const { controlPanel: cpHelpers, createControlPanel, mock } = testUtils;
+ const { patchDate } = mock;
+
+ const searchMenuTypes = ['filter'];
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.fields = {
+ date_field: { string: "Date", type: "date", store: true, sortable: true, searchable: true },
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ };
+ },
+ }, function () {
+
+ QUnit.module('FilterMenu');
+
+ QUnit.test('simple rendering with no filter', async function (assert) {
+ assert.expect(2);
+
+ const params = {
+ cpModelConfig: { searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider');
+ assert.containsOnce(controlPanel, 'div.o_generator_menu');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('simple rendering with a single filter', async function (assert) {
+ assert.expect(3);
+
+ const arch = `
+ <search>
+ <filter string="Foo" name="foo" domain="[]"/>
+ </search>`;
+ const params = {
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ assert.containsOnce(controlPanel, '.o_menu_item');
+ assert.containsOnce(controlPanel, '.dropdown-divider');
+ assert.containsOnce(controlPanel, 'div.o_generator_menu');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('should have Date and ID field proposed in that order in "Add custom Filter" submenu', async function (assert) {
+ assert.expect(2);
+
+ const params = {
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleAddCustomFilter(controlPanel);
+ const optionEls = controlPanel.el.querySelectorAll('div.o_filter_condition > select.o_generator_menu_field option');
+ assert.strictEqual(optionEls[0].innerText.trim(), 'Date');
+ assert.strictEqual(optionEls[1].innerText.trim(), 'ID');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('toggle a "simple" filter in filter menu works', async function (assert) {
+ assert.expect(9);
+
+ const domains = [
+ [['foo', '=', 'qsdf']],
+ []
+ ];
+ const arch = `
+ <search>
+ <filter string="Foo" name="foo" domain="[['foo', '=', 'qsdf']]"/>
+ </search>`;
+ const params = {
+ cpModelConfig: { arch, searchMenuTypes },
+ cpProps: { fields: {}, searchMenuTypes },
+ search: function (searchQuery) {
+ const { domain } = searchQuery;
+ assert.deepEqual(domain, domains.shift());
+ }
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
+ await cpHelpers.toggleMenuItem(controlPanel, "Foo");
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Foo']);
+ assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'),
+ 'span.fa.fa-filter.o_searchview_facet_label');
+
+ assert.ok(cpHelpers.isItemSelected(controlPanel, "Foo"));
+
+ await cpHelpers.toggleMenuItem(controlPanel, "Foo");
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, "Foo"));
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('add a custom filter works', async function (assert) {
+ assert.expect(1);
+
+ const params = {
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleAddCustomFilter(controlPanel);
+ // choose ID field in 'Add Custome filter' menu and value 1
+ await testUtils.fields.editSelect(
+ controlPanel.el.querySelector('div.o_filter_condition > select.o_generator_menu_field'), 'id');
+ await testUtils.fields.editInput(
+ controlPanel.el.querySelector('div.o_filter_condition > span.o_generator_menu_value > input'), 1);
+
+ await cpHelpers.applyFilter(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['ID is "1"']);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('deactivate a new custom filter works', async function (assert) {
+ assert.expect(4);
+
+ const unpatchDate = patchDate(2020, 1, 5, 12, 20, 0);
+
+ const params = {
+ cpModelConfig: { fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleAddCustomFilter(controlPanel);
+ await cpHelpers.applyFilter(controlPanel);
+
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"'));
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date is equal to "02/05/2020"']);
+
+ await cpHelpers.toggleMenuItem(controlPanel, 'Date is equal to "02/05/2020"');
+
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 'Date is equal to "02/05/2020"'));
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('filter by a date field using period works', async function (assert) {
+ assert.expect(56);
+
+ const unpatchDate = patchDate(2017, 2, 22, 1, 0, 0);
+
+ const basicDomains = [
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
+ ["&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"]],
+ ["|",
+ "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-01-31"],
+ "&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"]
+ ],
+ ["&", ["date_field", ">=", "2017-10-01"], ["date_field", "<=", "2017-12-31"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-03-31"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
+ ["&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]],
+ ["|",
+ "&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"],
+ "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]
+ ],
+ ["|",
+ "|",
+ "&", ["date_field", ">=", "2015-01-01"], ["date_field", "<=", "2015-12-31"],
+ "&", ["date_field", ">=", "2016-01-01"], ["date_field", "<=", "2016-12-31"],
+ "&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"]
+ ],
+ ["|",
+ "|",
+ "&", ["date_field", ">=", "2015-03-01"], ["date_field", "<=", "2015-03-31"],
+ "&", ["date_field", ">=", "2016-03-01"], ["date_field", "<=", "2016-03-31"],
+ "&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"]
+ ]
+ ];
+
+ const arch = `
+ <search>
+ <filter string="Date" name="date_field" date="date_field"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_date_field: 1 },
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ // we inspect query domain
+ const { domain } = searchQuery;
+ if (domain.length) {
+ assert.deepEqual(domain, basicDomains.shift());
+ }
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date");
+
+ const optionEls = controlPanel.el.querySelectorAll('ul.o_menu_item_options > li.o_item_option > a');
+
+ // default filter should be activated with the global default period 'this_month'
+ const { domain } = controlPanel.getQuery();
+ assert.deepEqual(
+ domain,
+ ["&", ["date_field", ">=", "2017-03-01"], ["date_field", "<=", "2017-03-31"]]
+ );
+ assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 0));
+
+ // check option descriptions
+ const optionDescriptions = [...optionEls].map(e => e.innerText.trim());
+ const expectedDescriptions = [
+ 'March', 'February', 'January',
+ 'Q4', 'Q3', 'Q2', 'Q1',
+ '2017', '2016', '2015'
+ ];
+ assert.deepEqual(optionDescriptions, expectedDescriptions);
+
+ // check generated domains
+ const steps = [
+ { description: 'March', facetContent: 'Date: 2017', selectedoptions: [7] },
+ { description: 'February', facetContent: 'Date: February 2017', selectedoptions: [1, 7] },
+ { description: 'February', facetContent: 'Date: 2017', selectedoptions: [7] },
+ { description: 'January', facetContent: 'Date: January 2017', selectedoptions: [2, 7] },
+ { description: 'Q4', facetContent: 'Date: January 2017/Q4 2017', selectedoptions: [2, 3, 7] },
+ { description: 'January', facetContent: 'Date: Q4 2017', selectedoptions: [3, 7] },
+ { description: 'Q4', facetContent: 'Date: 2017', selectedoptions: [7] },
+ { description: 'Q1', facetContent: 'Date: Q1 2017', selectedoptions: [6, 7] },
+ { description: 'Q1', facetContent: 'Date: 2017', selectedoptions: [7] },
+ { description: '2017', selectedoptions: [] },
+ { description: '2017', facetContent: 'Date: 2017', selectedoptions: [7] },
+ { description: '2016', facetContent: 'Date: 2016/2017', selectedoptions: [7, 8] },
+ { description: '2015', facetContent: 'Date: 2015/2016/2017', selectedoptions: [7, 8, 9] },
+ { description: 'March', facetContent: 'Date: March 2015/March 2016/March 2017', selectedoptions: [0, 7, 8, 9] }
+ ];
+ for (const s of steps) {
+ const index = expectedDescriptions.indexOf(s.description);
+ await cpHelpers.toggleMenuItemOption(controlPanel, 0, index);
+ if (s.facetContent) {
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]);
+ } else {
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+ }
+ s.selectedoptions.forEach(index => {
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index),
+ `at step ${steps.indexOf(s) + 1}, option ${expectedDescriptions[index]} should be selected`);
+ });
+ }
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('filter by a date field using period works even in January', async function (assert) {
+ assert.expect(5);
+
+ const unpatchDate = patchDate(2017, 0, 7, 3, 0, 0);
+
+ const arch = `
+ <search>
+ <filter string="Date" name="some_filter" date="date_field" default_period="last_month"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_some_filter: 1 },
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ const { domain } = controlPanel.getQuery();
+ assert.deepEqual(domain, [
+ '&',
+ ["date_field", ">=", "2016-12-01"],
+ ["date_field", "<=", "2016-12-31"]
+ ]);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: December 2016"]);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, "Date");
+
+ assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", 'December'));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", '2016'));
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('`context` key in <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 = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ // we inspect query context
+ const { context } = searchQuery;
+ assert.deepEqual(context, { coucou_1: 1 });
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('Filter with JSON-parsable domain works', async function (assert) {
+ assert.expect(1);
+
+ const originalDomain = [['foo', '=', 'Gently Weeps']];
+ const xml_domain = JSON.stringify(originalDomain);
+
+ const arch =
+ `<search>
+ <filter string="Foo" name="gently_weeps" domain="${_.escape(xml_domain)}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ const { domain } = searchQuery;
+ assert.deepEqual(domain, originalDomain,
+ 'A JSON parsable xml domain should be handled just like any other'
+ );
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('filter with date attribute set as search_default', async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2019, 6, 31, 13, 43, 0);
+
+ const arch =
+ `<search>
+ <filter string="Date" name="date_field" date="date_field" default_period="last_month"/>
+ </search>`,
+ params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: {
+ search_default_date_field: true
+ }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ["Date: June 2019"]);
+
+ controlPanel.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('filter domains are correcly combined by OR and AND', async function (assert) {
+ assert.expect(2);
+
+ const arch =
+ `<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 = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: {
+ search_default_f_1_g1: true,
+ search_default_f1_g2: true,
+ search_default_f2_g2: true,
+ }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ const { domain } = controlPanel.getQuery();
+ assert.deepEqual(domain, [
+ '&',
+ ['foo', '=', 'f1_g1'],
+ '|',
+ ['foo', '=', 'f1_g2'],
+ ['foo', '=', 'f2_g2']
+ ]);
+
+ assert.deepEqual(
+ cpHelpers.getFacetTexts(controlPanel),
+ ["Filter Group 1", "Filter 1 Group 2orFilter 2 GROUP 2"]
+ );
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('arch order of groups of filters preserved', async function (assert) {
+ assert.expect(12);
+
+ const arch =
+ `<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 = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleFilterMenu(controlPanel);
+ assert.containsN(controlPanel, '.o_filter_menu .o_menu_item', 11);
+
+ const menuItemEls = controlPanel.el.querySelectorAll('.o_filter_menu .o_menu_item');
+ [...menuItemEls].forEach((e, index) => {
+ assert.strictEqual(e.innerText.trim(), String(index + 1));
+ });
+
+ controlPanel.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/groupby_menu_tests.js b/addons/web/static/tests/control_panel/groupby_menu_tests.js
new file mode 100644
index 00000000..1239f088
--- /dev/null
+++ b/addons/web/static/tests/control_panel/groupby_menu_tests.js
@@ -0,0 +1,478 @@
+odoo.define('web.groupby_menu_tests', function (require) {
+ "use strict";
+
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createControlPanel } = testUtils;
+
+ const searchMenuTypes = ['groupBy'];
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.fields = {
+ bar: { string: "Bar", type: "many2one", relation: 'partner' },
+ birthday: { string: "Birthday", type: "date", store: true, sortable: true },
+ date_field: { string: "Date", type: "date", store: true, sortable: true },
+ float_field: { string: "Float", type: "float", group_operator: 'sum' },
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ };
+ },
+ }, function () {
+
+ QUnit.module('GroupByMenu');
+
+ QUnit.test('simple rendering with neither groupbys nor groupable fields', async function (assert) {
+
+ assert.expect(1);
+ const params = {
+ cpModelConfig: { searchMenuTypes },
+ cpProps: { fields: {}, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider, div.o_generator_menu');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('simple rendering with no groupby', async function (assert) {
+ assert.expect(5);
+
+ const params = {
+ cpModelConfig: { searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ assert.containsNone(controlPanel, '.o_menu_item, .dropdown-divider');
+ assert.containsOnce(controlPanel, 'div.o_generator_menu');
+
+ await cpHelpers.toggleAddCustomGroup(controlPanel);
+
+ const optionEls = controlPanel.el.querySelectorAll('div.o_generator_menu select.o_group_by_selector option');
+ assert.strictEqual(optionEls[0].innerText.trim(), 'Birthday');
+ assert.strictEqual(optionEls[1].innerText.trim(), 'Date');
+ assert.strictEqual(optionEls[2].innerText.trim(), 'Foo');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('simple rendering with a single groupby', async function (assert) {
+ assert.expect(4);
+
+ const arch = `
+ <search>
+ <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ assert.containsOnce(controlPanel, '.o_menu_item');
+ assert.strictEqual(controlPanel.el.querySelector('.o_menu_item').innerText.trim(), "Groupby Foo");
+ assert.containsOnce(controlPanel, '.dropdown-divider');
+ assert.containsOnce(controlPanel, 'div.o_generator_menu');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('toggle a "simple" groupby in groupby menu works', async function (assert) {
+ assert.expect(9);
+
+ const groupBys = [['foo'], []];
+ const arch = `
+ <search>
+ <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {arch, fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ const { groupBy } = searchQuery;
+ assert.deepEqual(groupBy, groupBys.shift());
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
+
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Groupby Foo']);
+ assert.containsOnce(controlPanel.el.querySelector('.o_searchview .o_searchview_facet'),
+ 'span.fa.fa-bars.o_searchview_facet_label');
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('toggle a "simple" groupby quickly does not crash', async function (assert) {
+ assert.expect(1);
+
+ const arch = `
+ <search>
+ <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: { arch, fields: this.fields, searchMenuTypes },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+
+ cpHelpers.toggleMenuItem(controlPanel, 0);
+ cpHelpers.toggleMenuItem(controlPanel, 0);
+
+ assert.ok(true);
+ controlPanel.destroy();
+ });
+
+ QUnit.test('remove a "Group By" facet properly unchecks groupbys in groupby menu', async function (assert) {
+ assert.expect(5);
+
+ const arch = `
+ <search>
+ <filter string="Groupby Foo" name="gb_foo" context="{'group_by': 'foo'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_gb_foo: 1 }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ const { groupBy } = searchQuery;
+ assert.deepEqual(groupBy, []);
+ },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ const facetEl = controlPanel.el.querySelector('.o_searchview .o_searchview_facet');
+ assert.strictEqual(facetEl.innerText.trim(), "Groupby Foo");
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+
+ await testUtils.dom.click(facetEl.querySelector('i.o_facet_remove'));
+ assert.containsNone(controlPanel, '.o_searchview .o_searchview_facet');
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('group by a date field using interval works', async function (assert) {
+ assert.expect(21);
+
+ const groupBys = [
+ ['date_field:year', 'date_field:week' ],
+ ['date_field:year', 'date_field:month', 'date_field:week'],
+ ['date_field:year', 'date_field:month'],
+ ['date_field:year'],
+ []
+ ];
+
+ const arch = `
+ <search>
+ <filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_date: 1 }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ search: function (searchQuery) {
+ const { groupBy } = searchQuery;
+ assert.deepEqual(groupBy, groupBys.shift());
+ },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+
+ const optionEls = controlPanel.el.querySelectorAll('ul.o_menu_item_options > li.o_item_option > a');
+
+ // default groupby should be activated with the default inteval 'week'
+ const { groupBy } = controlPanel.getQuery();
+ assert.deepEqual(groupBy, ['date_field:week']);
+
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 3));
+
+ // check option descriptions
+ const optionDescriptions = [...optionEls].map(e => e.innerText.trim());
+ const expectedDescriptions = ['Year', 'Quarter', 'Month', 'Week', 'Day'];
+ assert.deepEqual(optionDescriptions, expectedDescriptions);
+
+ const steps = [
+ { description: 'Year', facetContent: 'Date: Year>Date: Week', selectedoptions: [0, 3] },
+ { description: 'Month', facetContent: 'Date: Year>Date: Month>Date: Week', selectedoptions: [0, 2, 3] },
+ { description: 'Week', facetContent: 'Date: Year>Date: Month', selectedoptions: [0, 2] },
+ { description: 'Month', facetContent: 'Date: Year', selectedoptions: [0] },
+ { description: 'Year', selectedoptions: [] },
+ ];
+ for (const s of steps) {
+ const index = expectedDescriptions.indexOf(s.description);
+ await cpHelpers.toggleMenuItemOption(controlPanel, 0, index);
+ if (s.facetContent) {
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), [s.facetContent]);
+ } else {
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+ }
+ s.selectedoptions.forEach(index => {
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, index));
+ });
+ }
+ controlPanel.destroy();
+ });
+
+ QUnit.test('interval options are correctly grouped and ordered', async function (assert) {
+ assert.expect(8);
+
+ const arch = `
+ <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 = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_bar: 1 }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar']);
+
+ // open menu 'Group By'
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+
+ // Open the groupby 'Date'
+ await cpHelpers.toggleMenuItem(controlPanel, 'Date');
+ // select option 'week'
+ await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week']);
+
+ // select option 'day'
+ await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Day');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Week>Date: Day']);
+
+ // select option 'year'
+ await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Year');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day']);
+
+ // select 'Foo'
+ await cpHelpers.toggleMenuItem(controlPanel, 'Foo');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Week>Date: Day>Foo']);
+
+ // select option 'quarter'
+ await cpHelpers.toggleMenuItem(controlPanel, 'Date');
+ await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Quarter');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Bar>Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']);
+
+ // unselect 'Bar'
+ await cpHelpers.toggleMenuItem(controlPanel, 'Bar');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Week>Date: Day>Foo']);
+
+ // unselect option 'week'
+ await cpHelpers.toggleMenuItem(controlPanel, 'Date');
+ await cpHelpers.toggleMenuItemOption(controlPanel, 'Date', 'Week');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Year>Date: Quarter>Date: Day>Foo']);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('the ID field should not be proposed in "Add Custom Group" menu', async function (assert) {
+ assert.expect(2);
+
+ const fields = {
+ foo: { string: "Foo", type: "char", store: true, sortable: true },
+ id: { sortable: true, string: 'ID', type: 'integer' }
+ };
+ const params = {
+ cpModelConfig: { searchMenuTypes },
+ cpProps: { fields, searchMenuTypes },
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ await cpHelpers.toggleAddCustomGroup(controlPanel);
+
+ const optionEls = controlPanel.el.querySelectorAll('div.o_generator_menu select.o_group_by_selector option');
+ assert.strictEqual(optionEls.length, 1);
+ assert.strictEqual(optionEls[0].innerText.trim(), "Foo");
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('add a date field in "Add Custome Group" activate a groupby with global default option "month"', async function (assert) {
+ assert.expect(4);
+
+ const fields = {
+ date_field: { string: "Date", type: "date", store: true, sortable: true },
+ id: { sortable: true, string: 'ID', type: 'integer' }
+ };
+ const params = {
+ cpModelConfig: { fields, searchMenuTypes },
+ cpProps: { fields, searchMenuTypes },
+ search: function (searchQuery) {
+ const { groupBy } = searchQuery;
+ assert.deepEqual(groupBy, ['date_field:month']);
+ }
+ };
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ await cpHelpers.toggleAddCustomGroup(controlPanel);
+ await cpHelpers.applyGroup(controlPanel);
+
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Month']);
+
+ assert.ok(cpHelpers.isItemSelected(controlPanel, "Date"));
+ await cpHelpers.toggleMenuItem(controlPanel, "Date");
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, "Date", "Month"));
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('default groupbys can be ordered', async function (assert) {
+ assert.expect(2);
+
+ const arch = `
+ <search>
+ <filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
+ <filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_birthday: 2, search_default_date: 1 }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ // the defautl groupbys should be activated in the right order
+ const { groupBy } = controlPanel.getQuery();
+ assert.deepEqual(groupBy, ['date_field:week', 'birthday:month']);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Week>Birthday: Month']);
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('a separator in groupbys does not cause problems', async function (assert) {
+ assert.expect(23);
+
+ const arch = `
+ <search>
+ <filter string="Date" name="coolName" context="{'group_by': 'date_field'}"/>
+ <separator/>
+ <filter string="Bar" name="superName" context="{'group_by': 'bar'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+
+ const controlPanel = await createControlPanel(params);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ await cpHelpers.toggleMenuItemOption(controlPanel, 0, 4);
+
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day']);
+
+ await cpHelpers.toggleMenuItem(controlPanel, 1);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 1));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Day>Bar']);
+
+ await cpHelpers.toggleMenuItemOption(controlPanel, 0, 1);
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 1));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day>Bar']);
+
+ await cpHelpers.toggleMenuItem(controlPanel, 1);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ assert.ok(cpHelpers.isItemSelected(controlPanel, 0));
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
+ assert.ok(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), ['Date: Quarter>Date: Day']);
+
+ await cpHelpers.removeFacet(controlPanel);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ await cpHelpers.toggleGroupByMenu(controlPanel);
+ await cpHelpers.toggleMenuItem(controlPanel, 0);
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 0));
+ assert.notOk(cpHelpers.isItemSelected(controlPanel, 1));
+ assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 1), 'selected');
+ assert.notOk(cpHelpers.isOptionSelected(controlPanel, 0, 4), 'selected');
+
+ controlPanel.destroy();
+ });
+
+ QUnit.test('falsy search default groupbys are not activated', async function (assert) {
+ assert.expect(2);
+
+ const arch = `
+ <search>
+ <filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
+ <filter string="Date" name="date" context="{'group_by': 'foo'}"/>
+ </search>`;
+ const params = {
+ cpModelConfig: {
+ arch,
+ fields: this.fields,
+ searchMenuTypes,
+ context: { search_default_birthday: false, search_default_foo: 0 }
+ },
+ cpProps: { fields: this.fields, searchMenuTypes },
+ };
+
+ const controlPanel = await createControlPanel(params);
+ const { groupBy } = controlPanel.getQuery();
+ assert.deepEqual(groupBy, []);
+ assert.deepEqual(cpHelpers.getFacetTexts(controlPanel), []);
+
+ controlPanel.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/search_bar_tests.js b/addons/web/static/tests/control_panel/search_bar_tests.js
new file mode 100644
index 00000000..9552173f
--- /dev/null
+++ b/addons/web/static/tests/control_panel/search_bar_tests.js
@@ -0,0 +1,702 @@
+odoo.define('web.search_bar_tests', function (require) {
+ "use strict";
+
+ const { Model } = require('web/static/src/js/model.js');
+ const Registry = require("web.Registry");
+ const SearchBar = require('web.SearchBar');
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createActionManager, createComponent } = testUtils;
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ bar: { string: "Bar", type: 'many2one', relation: 'partner' },
+ birthday: { string: "Birthday", type: 'date' },
+ birth_datetime: { string: "Birth DateTime", type: 'datetime' },
+ foo: { string: "Foo", type: 'char' },
+ bool: { string: "Bool", type: 'boolean' },
+ },
+ records: [
+ { id: 1, display_name: "First record", foo: "yop", bar: 2, bool: true, birthday: '1983-07-15', birth_datetime: '1983-07-15 01:00:00' },
+ { id: 2, display_name: "Second record", foo: "blip", bar: 1, bool: false, birthday: '1982-06-04', birth_datetime: '1982-06-04 02:00:00' },
+ { id: 3, display_name: "Third record", foo: "gnap", bar: 1, bool: false, birthday: '1985-09-13', birth_datetime: '1985-09-13 03:00:00' },
+ { id: 4, display_name: "Fourth record", foo: "plop", bar: 2, bool: true, birthday: '1983-05-05', birth_datetime: '1983-05-05 04:00:00' },
+ { id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, bool: true, birthday: '1800-01-01', birth_datetime: '1800-01-01 05:00:00' },
+ ],
+ },
+ };
+
+ this.actions = [{
+ id: 1,
+ name: "Partners Action",
+ res_model: 'partner',
+ search_view_id: [false, 'search'],
+ type: 'ir.actions.act_window',
+ views: [[false, 'list']],
+ }];
+
+ this.archs = {
+ 'partner,false,list': `
+ <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("autocomplete menu clickout interactions", async function (assert) {
+ assert.expect(9);
+
+ const fields = this.data.partner.fields;
+
+ class TestModelExtension extends Model.Extension {
+ get(property) {
+ switch (property) {
+ case 'facets':
+ return [];
+ case 'filters':
+ return Object.keys(fields).map((fname, index) => Object.assign({
+ description: fields[fname].string,
+ fieldName: fname,
+ fieldType: fields[fname].type,
+ id: index,
+ }, fields[fname]));
+ default:
+ break;
+ }
+ }
+ }
+ class MockedModel extends Model { }
+ MockedModel.registry = new Registry({ Test: TestModelExtension, });
+ const searchModel = new MockedModel({ Test: {} });
+ const searchBar = await createComponent(SearchBar, {
+ data: this.data,
+ env: { searchModel },
+ props: { fields },
+ });
+ const input = searchBar.el.querySelector('.o_searchview_input');
+
+ assert.containsNone(searchBar, '.o_searchview_autocomplete');
+
+ await testUtils.controlPanel.editSearch(searchBar, "Hello there");
+
+ assert.strictEqual(input.value, "Hello there", "input value should be updated");
+ assert.containsOnce(searchBar, '.o_searchview_autocomplete');
+
+ await testUtils.dom.triggerEvent(input, 'keydown', { key: 'Escape' });
+
+ assert.strictEqual(input.value, "", "input value should be empty");
+ assert.containsNone(searchBar, '.o_searchview_autocomplete');
+
+ await testUtils.controlPanel.editSearch(searchBar, "General Kenobi");
+
+ assert.strictEqual(input.value, "General Kenobi", "input value should be updated");
+ assert.containsOnce(searchBar, '.o_searchview_autocomplete');
+
+ await testUtils.dom.click(document.body);
+
+ assert.strictEqual(input.value, "", "input value should be empty");
+ assert.containsNone(searchBar, '.o_searchview_autocomplete');
+
+ searchBar.destroy();
+ });
+
+ QUnit.test('select an autocomplete field', async function (assert) {
+ assert.expect(3);
+
+ let searchReadCount = 0;
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ async mockRPC(route, args) {
+ if (route === '/web/dataset/search_read') {
+ switch (searchReadCount) {
+ case 0:
+ // Done on loading
+ break;
+ case 1:
+ assert.deepEqual(args.domain, [["foo", "ilike", "a"]]);
+ break;
+ }
+ searchReadCount++;
+ }
+ return this._super(...arguments);
+ },
+ });
+ await actionManager.doAction(1);
+
+ const searchInput = actionManager.el.querySelector('.o_searchview_input');
+ await testUtils.fields.editInput(searchInput, 'a');
+ assert.containsN(actionManager, '.o_searchview_autocomplete li', 2,
+ "there should be 2 result for 'a' in search bar autocomplete");
+
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
+ assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
+ "a", "There should be a field facet with label 'a'");
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('select an autocomplete field with `context` key', async function (assert) {
+ assert.expect(9);
+
+ let searchReadCount = 0;
+ const firstLoading = testUtils.makeTestPromise();
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ async mockRPC(route, args) {
+ if (route === '/web/dataset/search_read') {
+ switch (searchReadCount) {
+ case 0:
+ firstLoading.resolve();
+ break;
+ case 1:
+ assert.deepEqual(args.domain, [["bar", "=", 1]]);
+ assert.deepEqual(args.context.bar, [1]);
+ break;
+ case 2:
+ assert.deepEqual(args.domain, ["|", ["bar", "=", 1], ["bar", "=", 2]]);
+ assert.deepEqual(args.context.bar, [1, 2]);
+ break;
+ }
+ searchReadCount++;
+ }
+ return this._super(...arguments);
+ },
+ });
+ await actionManager.doAction(1);
+ await firstLoading;
+ assert.strictEqual(searchReadCount, 1, "there should be 1 search_read");
+ const searchInput = actionManager.el.querySelector('.o_searchview_input');
+
+ // 'r' key to filter on bar "First Record"
+ await testUtils.fields.editInput(searchInput, 'record');
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
+
+ assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
+ "First record",
+ "the autocompletion facet should be correct");
+ assert.strictEqual(searchReadCount, 2, "there should be 2 search_read");
+
+ // 'r' key to filter on bar "Second Record"
+ await testUtils.fields.editInput(searchInput, 'record');
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
+ await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
+
+ assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
+ "First recordorSecond record",
+ "the autocompletion facet should be correct");
+ assert.strictEqual(searchReadCount, 3, "there should be 3 search_read");
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('no search text triggers a reload', async function (assert) {
+ assert.expect(2);
+
+ // Switch to pivot to ensure that the event comes from the control panel
+ // (pivot does not have a handler on "reload" event).
+ this.actions[0].views = [[false, 'pivot']];
+ this.archs['partner,false,pivot'] = `
+ <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();
+ });
+
+ QUnit.test('open search view autocomplete on paste value using mouse', async function (assert) {
+ assert.expect(1);
+
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+
+ await actionManager.doAction(1);
+ // Simulate paste text through the mouse.
+ const searchInput = actionManager.el.querySelector('.o_searchview_input');
+ searchInput.value = "ABC";
+ await testUtils.dom.triggerEvent(searchInput, 'input',
+ { inputType: 'insertFromPaste' });
+ await testUtils.nextTick();
+ assert.containsOnce(actionManager, '.o_searchview_autocomplete',
+ "should display autocomplete dropdown menu on paste in search view");
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('select autocompleted many2one', async function (assert) {
+ assert.expect(5);
+
+ const archs = Object.assign({}, this.archs, {
+ 'partner,false,search': `
+ <search>
+ <field name="foo"/>
+ <field name="birthday"/>
+ <field name="birth_datetime"/>
+ <field name="bar" operator="child_of"/>
+ </search>`,
+ });
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs,
+ data: this.data,
+ async mockRPC(route, { domain }) {
+ if (route === '/web/dataset/search_read') {
+ assert.step(JSON.stringify(domain));
+ }
+ return this._super(...arguments);
+ },
+ });
+ await actionManager.doAction(1);
+
+ await cpHelpers.editSearch(actionManager, "rec");
+ await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
+
+ await cpHelpers.removeFacet(actionManager, 0);
+
+ await cpHelpers.editSearch(actionManager, "rec");
+ await testUtils.dom.click(actionManager.el.querySelector('.o_expand'));
+ await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_menu_item.o_indent'));
+
+ assert.verifySteps([
+ '[]',
+ '[["bar","child_of","rec"]]', // Incomplete string -> Name search
+ '[]',
+ '[["bar","child_of",1]]', // Suggestion select -> Specific ID
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('"null" as autocomplete value', async function (assert) {
+ assert.expect(4);
+
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC(route, args) {
+ if (route === '/web/dataset/search_read') {
+ assert.step(JSON.stringify(args.domain));
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ await actionManager.doAction(1);
+
+ await cpHelpers.editSearch(actionManager, "null");
+
+ assert.strictEqual(actionManager.$('.o_searchview_autocomplete .o_selection_focus').text(),
+ "Search Foo for: null");
+
+ await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_selection_focus a'));
+
+ assert.verifySteps([
+ JSON.stringify([]), // initial search
+ JSON.stringify([["foo", "ilike", "null"]]),
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('autocompletion with a boolean field', async function (assert) {
+ assert.expect(9);
+
+ this.archs['partner,false,search'] = '<search><field name="bool"/></search>';
+
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC(route, args) {
+ if (route === '/web/dataset/search_read') {
+ assert.step(JSON.stringify(args.domain));
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ await actionManager.doAction(1);
+
+ await cpHelpers.editSearch(actionManager, "y");
+
+ assert.containsN(actionManager, '.o_searchview_autocomplete li', 2);
+ assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "Yes");
+
+ // select "Yes"
+ await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
+
+ await cpHelpers.removeFacet(actionManager, 0);
+
+ await cpHelpers.editSearch(actionManager, "No");
+
+ assert.containsN(actionManager, '.o_searchview_autocomplete li', 2);
+ assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "No");
+
+ // select "No"
+ await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
+
+
+ assert.verifySteps([
+ JSON.stringify([]), // initial search
+ JSON.stringify([["bool", "=", true]]),
+ JSON.stringify([]),
+ JSON.stringify([["bool", "=", false]]),
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test("reference fields are supported in search view", async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.ref = { type: 'reference', string: "Reference" };
+ this.data.partner.records.forEach((record, i) => {
+ record.ref = `ref${String(i).padStart(3, "0")}`;
+ });
+ const archs = Object.assign({}, this.archs, {
+ 'partner,false,search': `
+ <search>
+ <field name="ref"/>
+ </search>`,
+ });
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs,
+ data: this.data,
+ async mockRPC(route, { domain }) {
+ if (route === '/web/dataset/search_read') {
+ assert.step(JSON.stringify(domain));
+ }
+ return this._super(...arguments);
+
+ }
+ });
+ await actionManager.doAction(1);
+
+ await cpHelpers.editSearch(actionManager, "ref");
+ await cpHelpers.validateSearch(actionManager);
+
+ assert.containsN(actionManager, ".o_data_row", 5);
+
+ await cpHelpers.removeFacet(actionManager, 0);
+ await cpHelpers.editSearch(actionManager, "ref002");
+ await cpHelpers.validateSearch(actionManager);
+
+ assert.containsOnce(actionManager, ".o_data_row");
+
+ assert.verifySteps([
+ '[]',
+ '[["ref","ilike","ref"]]',
+ '[]',
+ '[["ref","ilike","ref002"]]',
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('focus should be on search bar when switching between views', async function (assert) {
+ assert.expect(4);
+
+ this.actions[0].views = [[false, 'list'], [false, 'form']];
+ this.archs['partner,false,form'] = `
+ <form>
+ <group>
+ <field name="display_name"/>
+ </group>
+ </form>`;
+
+ const actionManager = await createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+
+ await actionManager.doAction(1);
+
+ assert.containsOnce(actionManager, '.o_list_view');
+ assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
+ "searchview should have focus");
+
+ await testUtils.dom.click(actionManager.$('.o_list_view .o_data_cell:first'));
+ assert.containsOnce(actionManager, '.o_form_view');
+ await testUtils.dom.click(actionManager.$('.o_back_button'));
+ assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
+ "searchview should have focus");
+
+ actionManager.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/control_panel/search_utils_tests.js b/addons/web/static/tests/control_panel/search_utils_tests.js
new file mode 100644
index 00000000..ab7ef429
--- /dev/null
+++ b/addons/web/static/tests/control_panel/search_utils_tests.js
@@ -0,0 +1,362 @@
+odoo.define('web.search_utils_tests', function (require) {
+ "use strict";
+
+ const { constructDateDomain } = require('web.searchUtils');
+ const testUtils = require('web.test_utils');
+ const { _t } = require('web.core');
+
+ const patchDate = testUtils.mock.patchDate;
+
+ QUnit.module('SearchUtils', function () {
+
+ QUnit.module('Construct domain');
+
+ QUnit.test('construct simple domain based on date field (no comparisonOptionId)', function (assert) {
+ assert.expect(4);
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', []),
+ {
+ domain: "[]",
+ description: "",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "June 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "Q2 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-12-31"]]`,
+ description: "2020",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.test('construct simple domain based on datetime field (no comparisonOptionId)', function (assert) {
+ assert.expect(3);
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-06-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`,
+ description: "June 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]]`,
+ description: "Q2 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-12-31 23:59:59"]]`,
+ description: "2020",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.test('construct domain based on date field (no comparisonOptionId)', function (assert) {
+ assert.expect(3);
+ const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` +
+ `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-03-31"]` +
+ "]",
+ description: "January 2020/Q1 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2019-04-01"], ["date_field", "<=", "2019-06-30"], ` +
+ `"&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]` +
+ "]",
+ description: "Q2 2019/Q2 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'this_month', 'antepenultimate_month']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2020-01-01"], ["date_field", "<=", "2020-01-31"], ` +
+ `"&", ["date_field", ">=", "2020-11-01"], ["date_field", "<=", "2020-11-30"]` +
+ "]",
+ description: "January 2020/November 2020",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.test('construct domain based on datetime field (no comparisonOptionId)', function (assert) {
+ assert.expect(3);
+ const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-03-31 23:59:59"]` +
+ "]",
+ description: "January 2020/Q1 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2020-04-01 00:00:00"], ["date_field", "<=", "2020-06-30 23:59:59"]` +
+ "]",
+ description: "Q2 2019/Q2 2020",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'this_month', 'antepenultimate_month']),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2020-01-01 00:00:00"], ["date_field", "<=", "2020-01-31 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2020-11-01 00:00:00"], ["date_field", "<=", "2020-11-30 23:59:59"]` +
+ "]",
+ description: "January 2020/November 2020",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.test('construct comparison domain based on date field and option "previous_period"', function (assert) {
+ assert.expect(5);
+ const unpatchDate = patchDate(2020, 0, 1, 12, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'first_quarter', 'this_year'], 'previous_period'),
+ {
+ domain: "[" +
+ `"|", "|", ` +
+ `"&", ["date_field", ">=", "2019-10-01"], ["date_field", "<=", "2019-10-31"], ` +
+ `"&", ["date_field", ">=", "2019-11-01"], ["date_field", "<=", "2019-11-30"], ` +
+ `"&", ["date_field", ">=", "2019-12-01"], ["date_field", "<=", "2019-12-31"]` +
+ "]",
+ description: "October 2019/November 2019/December 2019",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year', 'last_year'], 'previous_period'),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-03-31"], ` +
+ `"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` +
+ "]",
+ description: "Q1 2018/Q1 2019",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_period'),
+ {
+ domain: "[" +
+ `"|", "|", "|", ` +
+ `"&", ["date_field", ">=", "2015-02-01"], ["date_field", "<=", "2015-02-28"], ` +
+ `"&", ["date_field", ">=", "2015-12-01"], ["date_field", "<=", "2015-12-31"], ` +
+ `"&", ["date_field", ">=", "2017-02-01"], ["date_field", "<=", "2017-02-28"], ` +
+ `"&", ["date_field", ">=", "2017-12-01"], ["date_field", "<=", "2017-12-31"]` +
+ "]",
+ description: "February 2015/December 2015/February 2017/December 2017",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_year', 'last_year'], 'previous_period'),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2017-01-01"], ["date_field", "<=", "2017-12-31"], ` +
+ `"&", ["date_field", ">=", "2018-01-01"], ["date_field", "<=", "2018-12-31"]` +
+ "]",
+ description: "2017/2018",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'third_quarter', 'last_year'], 'previous_period'),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2018-10-01"], ["date_field", "<=", "2018-12-31"], ` +
+ `"&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-03-31"]` +
+ "]",
+ description: "Q4 2018/Q1 2019",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.test('construct comparison domain based on datetime field and option "previous_year"', function (assert) {
+ assert.expect(3);
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().utc();
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_month', 'first_quarter', 'this_year'], 'previous_year'),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2019-01-01 00:00:00"], ["date_field", "<=", "2019-03-31 23:59:59"]` +
+ "]",
+ description: "June 2019/Q1 2019",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['second_quarter', 'this_year', 'last_year'], 'previous_year'),
+ {
+ domain: "[" +
+ `"|", ` +
+ `"&", ["date_field", ">=", "2018-04-01 00:00:00"], ["date_field", "<=", "2018-06-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` +
+ "]",
+ description: "Q2 2018/Q2 2019",
+ }
+ );
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'datetime', ['this_year', 'antepenultimate_year', 'this_month', 'antepenultimate_month'], 'previous_year'),
+ {
+ domain: "[" +
+ `"|", "|", "|", ` +
+ `"&", ["date_field", ">=", "2017-04-01 00:00:00"], ["date_field", "<=", "2017-04-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2017-06-01 00:00:00"], ["date_field", "<=", "2017-06-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2019-04-01 00:00:00"], ["date_field", "<=", "2019-04-30 23:59:59"], ` +
+ `"&", ["date_field", ">=", "2019-06-01 00:00:00"], ["date_field", "<=", "2019-06-30 23:59:59"]` +
+ "]",
+ description: "April 2017/June 2017/April 2019/June 2019",
+ }
+ );
+ unpatchDate();
+ });
+
+ QUnit.module('Options translation');
+
+ QUnit.test("Quarter option: custom translation", async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().locale('en');
+ testUtils.mock.patch(_t.database.db, {
+ "Q2": "Deuxième trimestre de l'an de grâce",
+ });
+
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "Deuxième trimestre de l'an de grâce 2020",
+ },
+ "Quarter term should be translated"
+ );
+
+ unpatchDate();
+ testUtils.mock.unpatch(_t.database.db);
+ });
+
+ QUnit.test("Quarter option: right to left", async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().locale('en');
+ testUtils.mock.patch(_t.database.parameters, {
+ direction: "rtl",
+ });
+
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "2020 Q2",
+ },
+ "Notation should be right to left"
+ );
+
+ unpatchDate();
+ testUtils.mock.unpatch(_t.database.parameters);
+ });
+
+ QUnit.test("Quarter option: custom translation and right to left", async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const referenceMoment = moment().locale('en');
+ testUtils.mock.patch(_t.database.db, {
+ "Q2": "2e Trimestre",
+ });
+ testUtils.mock.patch(_t.database.parameters, {
+ direction: "rtl",
+ });
+
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['second_quarter', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-04-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "2020 2e Trimestre",
+ },
+ "Quarter term should be translated and notation should be right to left"
+ );
+
+ unpatchDate();
+ testUtils.mock.unpatch(_t.database.db);
+ testUtils.mock.unpatch(_t.database.parameters);
+ });
+
+ QUnit.test("Moment.js localization does not affect formatted domain dates", async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2020, 5, 1, 13, 0, 0);
+ const initialLocale = moment.locale();
+ moment.defineLocale('addoneForTest', {
+ postformat: function (string) {
+ return string.replace(/\d/g, match => (1 + parseInt(match)) % 10);
+ }
+ });
+ const referenceMoment = moment().locale('addoneForTest');
+
+ assert.deepEqual(
+ constructDateDomain(referenceMoment, 'date_field', 'date', ['this_month', 'this_year']),
+ {
+ domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`,
+ description: "June 3131",
+ },
+ "Numbers in domain should not use addoneForTest locale"
+ );
+
+ moment.locale(initialLocale);
+ moment.updateLocale("addoneForTest", null);
+ unpatchDate();
+ });
+ });
+});