odoo.define('web.search_bar_tests', function (require) {
"use strict";
const { Model } = require('web/static/src/js/model.js');
const Registry = require("web.Registry");
const SearchBar = require('web.SearchBar');
const testUtils = require('web.test_utils');
const cpHelpers = testUtils.controlPanel;
const { createActionManager, createComponent } = testUtils;
QUnit.module('Components', {
beforeEach: function () {
this.data = {
partner: {
fields: {
bar: { string: "Bar", type: 'many2one', relation: 'partner' },
birthday: { string: "Birthday", type: 'date' },
birth_datetime: { string: "Birth DateTime", type: 'datetime' },
foo: { string: "Foo", type: 'char' },
bool: { string: "Bool", type: 'boolean' },
},
records: [
{ id: 1, display_name: "First record", foo: "yop", bar: 2, bool: true, birthday: '1983-07-15', birth_datetime: '1983-07-15 01:00:00' },
{ id: 2, display_name: "Second record", foo: "blip", bar: 1, bool: false, birthday: '1982-06-04', birth_datetime: '1982-06-04 02:00:00' },
{ id: 3, display_name: "Third record", foo: "gnap", bar: 1, bool: false, birthday: '1985-09-13', birth_datetime: '1985-09-13 03:00:00' },
{ id: 4, display_name: "Fourth record", foo: "plop", bar: 2, bool: true, birthday: '1983-05-05', birth_datetime: '1983-05-05 04:00:00' },
{ id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, bool: true, birthday: '1800-01-01', birth_datetime: '1800-01-01 05:00:00' },
],
},
};
this.actions = [{
id: 1,
name: "Partners Action",
res_model: 'partner',
search_view_id: [false, 'search'],
type: 'ir.actions.act_window',
views: [[false, 'list']],
}];
this.archs = {
'partner,false,list': `
`,
'partner,false,search': `
`,
};
},
}, function () {
QUnit.module('SearchBar');
QUnit.test('basic rendering', async function (assert) {
assert.expect(1);
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
assert.strictEqual(document.activeElement,
actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
"searchview input should be focused");
actionManager.destroy();
});
QUnit.test('navigation with facets', async function (assert) {
assert.expect(4);
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
// add a facet
await cpHelpers.toggleGroupByMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, 0);
await cpHelpers.toggleMenuItemOption(actionManager, 0, 0);
assert.containsOnce(actionManager, '.o_searchview .o_searchview_facet',
"there should be one facet");
assert.strictEqual(document.activeElement,
actionManager.el.querySelector('.o_searchview input.o_searchview_input'));
// press left to focus the facet
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowLeft' });
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview .o_searchview_facet'));
// press right to focus the input
await testUtils.dom.triggerEvent(document.activeElement, 'keydown', { key: 'ArrowRight' });
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'));
actionManager.destroy();
});
QUnit.test('search date and datetime fields. Support of timezones', async function (assert) {
assert.expect(4);
let searchReadCount = 0;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
session: {
getTZOffset() {
return 360;
}
},
async mockRPC(route, args) {
if (route === '/web/dataset/search_read') {
switch (searchReadCount) {
case 0:
// Done on loading
break;
case 1:
assert.deepEqual(args.domain, [["birthday", "=", "1983-07-15"]],
"A date should stay what the user has input, but transmitted in server's format");
break;
case 2:
// Done on closing the first facet
break;
case 3:
assert.deepEqual(args.domain, [["birth_datetime", "=", "1983-07-14 18:00:00"]],
"A datetime should be transformed in UTC and transmitted in server's format");
break;
}
searchReadCount++;
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
// Date case
let searchInput = actionManager.el.querySelector('.o_searchview_input');
await testUtils.fields.editInput(searchInput, '07/15/1983');
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(),
'07/15/1983',
'The format of the date in the facet should be in locale');
// Close Facet
await testUtils.dom.click($('.o_searchview_facet .o_facet_remove'));
// DateTime case
searchInput = actionManager.el.querySelector('.o_searchview_input');
await testUtils.fields.editInput(searchInput, '07/15/1983 00:00:00');
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_facet .o_facet_values').innerText.trim(),
'07/15/1983 00:00:00',
'The format of the datetime in the facet should be in locale');
actionManager.destroy();
});
QUnit.test("autocomplete menu clickout interactions", async function (assert) {
assert.expect(9);
const fields = this.data.partner.fields;
class TestModelExtension extends Model.Extension {
get(property) {
switch (property) {
case 'facets':
return [];
case 'filters':
return Object.keys(fields).map((fname, index) => Object.assign({
description: fields[fname].string,
fieldName: fname,
fieldType: fields[fname].type,
id: index,
}, fields[fname]));
default:
break;
}
}
}
class MockedModel extends Model { }
MockedModel.registry = new Registry({ Test: TestModelExtension, });
const searchModel = new MockedModel({ Test: {} });
const searchBar = await createComponent(SearchBar, {
data: this.data,
env: { searchModel },
props: { fields },
});
const input = searchBar.el.querySelector('.o_searchview_input');
assert.containsNone(searchBar, '.o_searchview_autocomplete');
await testUtils.controlPanel.editSearch(searchBar, "Hello there");
assert.strictEqual(input.value, "Hello there", "input value should be updated");
assert.containsOnce(searchBar, '.o_searchview_autocomplete');
await testUtils.dom.triggerEvent(input, 'keydown', { key: 'Escape' });
assert.strictEqual(input.value, "", "input value should be empty");
assert.containsNone(searchBar, '.o_searchview_autocomplete');
await testUtils.controlPanel.editSearch(searchBar, "General Kenobi");
assert.strictEqual(input.value, "General Kenobi", "input value should be updated");
assert.containsOnce(searchBar, '.o_searchview_autocomplete');
await testUtils.dom.click(document.body);
assert.strictEqual(input.value, "", "input value should be empty");
assert.containsNone(searchBar, '.o_searchview_autocomplete');
searchBar.destroy();
});
QUnit.test('select an autocomplete field', async function (assert) {
assert.expect(3);
let searchReadCount = 0;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
async mockRPC(route, args) {
if (route === '/web/dataset/search_read') {
switch (searchReadCount) {
case 0:
// Done on loading
break;
case 1:
assert.deepEqual(args.domain, [["foo", "ilike", "a"]]);
break;
}
searchReadCount++;
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
const searchInput = actionManager.el.querySelector('.o_searchview_input');
await testUtils.fields.editInput(searchInput, 'a');
assert.containsN(actionManager, '.o_searchview_autocomplete li', 2,
"there should be 2 result for 'a' in search bar autocomplete");
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
"a", "There should be a field facet with label 'a'");
actionManager.destroy();
});
QUnit.test('select an autocomplete field with `context` key', async function (assert) {
assert.expect(9);
let searchReadCount = 0;
const firstLoading = testUtils.makeTestPromise();
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
async mockRPC(route, args) {
if (route === '/web/dataset/search_read') {
switch (searchReadCount) {
case 0:
firstLoading.resolve();
break;
case 1:
assert.deepEqual(args.domain, [["bar", "=", 1]]);
assert.deepEqual(args.context.bar, [1]);
break;
case 2:
assert.deepEqual(args.domain, ["|", ["bar", "=", 1], ["bar", "=", 2]]);
assert.deepEqual(args.context.bar, [1, 2]);
break;
}
searchReadCount++;
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
await firstLoading;
assert.strictEqual(searchReadCount, 1, "there should be 1 search_read");
const searchInput = actionManager.el.querySelector('.o_searchview_input');
// 'r' key to filter on bar "First Record"
await testUtils.fields.editInput(searchInput, 'record');
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
"First record",
"the autocompletion facet should be correct");
assert.strictEqual(searchReadCount, 2, "there should be 2 search_read");
// 'r' key to filter on bar "Second Record"
await testUtils.fields.editInput(searchInput, 'record');
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_input_container .o_facet_values').innerText.trim(),
"First recordorSecond record",
"the autocompletion facet should be correct");
assert.strictEqual(searchReadCount, 3, "there should be 3 search_read");
actionManager.destroy();
});
QUnit.test('no search text triggers a reload', async function (assert) {
assert.expect(2);
// Switch to pivot to ensure that the event comes from the control panel
// (pivot does not have a handler on "reload" event).
this.actions[0].views = [[false, 'pivot']];
this.archs['partner,false,pivot'] = `
`;
let rpcs;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function () {
rpcs++;
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(1);
const searchInput = actionManager.el.querySelector('.o_searchview_input');
rpcs = 0;
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.containsNone(actionManager, '.o_searchview_facet_label');
assert.strictEqual(rpcs, 2, "should have reloaded");
actionManager.destroy();
});
QUnit.test('selecting (no result) triggers a re-render', async function (assert) {
assert.expect(3);
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
const searchInput = actionManager.el.querySelector('.o_searchview_input');
// 'a' key to filter nothing on bar
await testUtils.fields.editInput(searchInput, 'hello there');
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowRight' });
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'ArrowDown' });
assert.strictEqual(actionManager.el.querySelector('.o_searchview_autocomplete .o_selection_focus').innerText.trim(), "(no result)",
"there should be no result for 'a' in bar");
await testUtils.dom.triggerEvent(searchInput, 'keydown', { key: 'Enter' });
assert.containsNone(actionManager, '.o_searchview_facet_label');
assert.strictEqual(actionManager.el.querySelector('.o_searchview_input').value, "",
"the search input should be re-rendered");
actionManager.destroy();
});
QUnit.test('update suggested filters in autocomplete menu with Japanese IME', async function (assert) {
assert.expect(4);
// The goal here is to simulate as many events happening during an IME
// assisted composition session as possible. Some of these events are
// not handled but are triggered to ensure they do not interfere.
const TEST = "TEST";
const テスト = "テスト";
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
const searchInput = actionManager.el.querySelector('.o_searchview_input');
// Simulate typing "TEST" on search view.
for (let i = 0; i < TEST.length; i++) {
const key = TEST[i].toUpperCase();
await testUtils.dom.triggerEvent(searchInput, 'keydown',
{ key, isComposing: true });
if (i === 0) {
// Composition is initiated after the first keydown
await testUtils.dom.triggerEvent(searchInput, 'compositionstart');
}
await testUtils.dom.triggerEvent(searchInput, 'keypress',
{ key, isComposing: true });
searchInput.value = TEST.slice(0, i + 1);
await testUtils.dom.triggerEvent(searchInput, 'keyup',
{ key, isComposing: true });
await testUtils.dom.triggerEvent(searchInput, 'input',
{ inputType: 'insertCompositionText', isComposing: true });
}
assert.containsOnce(actionManager.el, '.o_searchview_autocomplete',
"should display autocomplete dropdown menu on typing something in search view"
);
assert.strictEqual(
actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(),
"Search Foo for: TEST",
`1st filter suggestion should be based on typed word "TEST"`
);
// Simulate soft-selection of another suggestion from IME through keyboard navigation.
await testUtils.dom.triggerEvent(searchInput, 'keydown',
{ key: 'ArrowDown', isComposing: true });
await testUtils.dom.triggerEvent(searchInput, 'keypress',
{ key: 'ArrowDown', isComposing: true });
searchInput.value = テスト;
await testUtils.dom.triggerEvent(searchInput, 'keyup',
{ key: 'ArrowDown', isComposing: true });
await testUtils.dom.triggerEvent(searchInput, 'input',
{ inputType: 'insertCompositionText', isComposing: true });
assert.strictEqual(
actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(),
"Search Foo for: テスト",
`1st filter suggestion should be updated with soft-selection typed word "テスト"`
);
// Simulate selection on suggestion item "TEST" from IME.
await testUtils.dom.triggerEvent(searchInput, 'keydown',
{ key: 'Enter', isComposing: true });
await testUtils.dom.triggerEvent(searchInput, 'keypress',
{ key: 'Enter', isComposing: true });
searchInput.value = TEST;
await testUtils.dom.triggerEvent(searchInput, 'keyup',
{ key: 'Enter', isComposing: true });
await testUtils.dom.triggerEvent(searchInput, 'input',
{ inputType: 'insertCompositionText', isComposing: true });
// End of the composition
await testUtils.dom.triggerEvent(searchInput, 'compositionend');
assert.strictEqual(
actionManager.el.querySelector('.o_searchview_autocomplete li').innerText.trim(),
"Search Foo for: TEST",
`1st filter suggestion should finally be updated with click selection on word "TEST" from IME`
);
actionManager.destroy();
});
QUnit.test('open search view autocomplete on paste value using mouse', async function (assert) {
assert.expect(1);
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
// Simulate paste text through the mouse.
const searchInput = actionManager.el.querySelector('.o_searchview_input');
searchInput.value = "ABC";
await testUtils.dom.triggerEvent(searchInput, 'input',
{ inputType: 'insertFromPaste' });
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_searchview_autocomplete',
"should display autocomplete dropdown menu on paste in search view");
actionManager.destroy();
});
QUnit.test('select autocompleted many2one', async function (assert) {
assert.expect(5);
const archs = Object.assign({}, this.archs, {
'partner,false,search': `
`,
});
const actionManager = await createActionManager({
actions: this.actions,
archs,
data: this.data,
async mockRPC(route, { domain }) {
if (route === '/web/dataset/search_read') {
assert.step(JSON.stringify(domain));
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
await cpHelpers.editSearch(actionManager, "rec");
await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
await cpHelpers.removeFacet(actionManager, 0);
await cpHelpers.editSearch(actionManager, "rec");
await testUtils.dom.click(actionManager.el.querySelector('.o_expand'));
await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_menu_item.o_indent'));
assert.verifySteps([
'[]',
'[["bar","child_of","rec"]]', // Incomplete string -> Name search
'[]',
'[["bar","child_of",1]]', // Suggestion select -> Specific ID
]);
actionManager.destroy();
});
QUnit.test('"null" as autocomplete value', async function (assert) {
assert.expect(4);
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC(route, args) {
if (route === '/web/dataset/search_read') {
assert.step(JSON.stringify(args.domain));
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
await cpHelpers.editSearch(actionManager, "null");
assert.strictEqual(actionManager.$('.o_searchview_autocomplete .o_selection_focus').text(),
"Search Foo for: null");
await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li.o_selection_focus a'));
assert.verifySteps([
JSON.stringify([]), // initial search
JSON.stringify([["foo", "ilike", "null"]]),
]);
actionManager.destroy();
});
QUnit.test('autocompletion with a boolean field', async function (assert) {
assert.expect(9);
this.archs['partner,false,search'] = '';
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC(route, args) {
if (route === '/web/dataset/search_read') {
assert.step(JSON.stringify(args.domain));
}
return this._super(...arguments);
},
});
await actionManager.doAction(1);
await cpHelpers.editSearch(actionManager, "y");
assert.containsN(actionManager, '.o_searchview_autocomplete li', 2);
assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "Yes");
// select "Yes"
await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
await cpHelpers.removeFacet(actionManager, 0);
await cpHelpers.editSearch(actionManager, "No");
assert.containsN(actionManager, '.o_searchview_autocomplete li', 2);
assert.strictEqual(actionManager.$('.o_searchview_autocomplete li:last-child').text(), "No");
// select "No"
await testUtils.dom.click(actionManager.el.querySelector('.o_searchview_autocomplete li:last-child'));
assert.verifySteps([
JSON.stringify([]), // initial search
JSON.stringify([["bool", "=", true]]),
JSON.stringify([]),
JSON.stringify([["bool", "=", false]]),
]);
actionManager.destroy();
});
QUnit.test("reference fields are supported in search view", async function (assert) {
assert.expect(7);
this.data.partner.fields.ref = { type: 'reference', string: "Reference" };
this.data.partner.records.forEach((record, i) => {
record.ref = `ref${String(i).padStart(3, "0")}`;
});
const archs = Object.assign({}, this.archs, {
'partner,false,search': `
`,
});
const actionManager = await createActionManager({
actions: this.actions,
archs,
data: this.data,
async mockRPC(route, { domain }) {
if (route === '/web/dataset/search_read') {
assert.step(JSON.stringify(domain));
}
return this._super(...arguments);
}
});
await actionManager.doAction(1);
await cpHelpers.editSearch(actionManager, "ref");
await cpHelpers.validateSearch(actionManager);
assert.containsN(actionManager, ".o_data_row", 5);
await cpHelpers.removeFacet(actionManager, 0);
await cpHelpers.editSearch(actionManager, "ref002");
await cpHelpers.validateSearch(actionManager);
assert.containsOnce(actionManager, ".o_data_row");
assert.verifySteps([
'[]',
'[["ref","ilike","ref"]]',
'[]',
'[["ref","ilike","ref002"]]',
]);
actionManager.destroy();
});
QUnit.test('focus should be on search bar when switching between views', async function (assert) {
assert.expect(4);
this.actions[0].views = [[false, 'list'], [false, 'form']];
this.archs['partner,false,form'] = `
`;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
assert.containsOnce(actionManager, '.o_list_view');
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
"searchview should have focus");
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_cell:first'));
assert.containsOnce(actionManager, '.o_form_view');
await testUtils.dom.click(actionManager.$('.o_back_button'));
assert.strictEqual(document.activeElement, actionManager.el.querySelector('.o_searchview input.o_searchview_input'),
"searchview should have focus");
actionManager.destroy();
});
});
});