Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,264 @@
odoo.define('web.action_menus_tests', function (require) {
"use strict";
const ActionMenus = require('web.ActionMenus');
const Registry = require('web.Registry');
const testUtils = require('web.test_utils');
const { Component } = owl;
const { createComponent } = testUtils;
QUnit.module('Components', {
beforeEach() {
this.action = {
res_model: 'hobbit',
};
this.view = {
// needed by google_drive module, makes sense to give a view anyway.
type: 'form',
};
this.props = {
activeIds: [23],
context: {},
items: {
action: [
{ action: { id: 1 }, name: "What's taters, precious ?", id: 1 },
],
print: [
{ action: { id: 2 }, name: "Po-ta-toes", id: 2 },
],
other: [
{ description: "Boil'em", callback() { } },
{ description: "Mash'em", callback() { } },
{ description: "Stick'em in a stew", url: '#stew' },
],
},
};
// Patch the registry of the action menus
this.actionMenusRegistry = ActionMenus.registry;
ActionMenus.registry = new Registry();
},
afterEach() {
ActionMenus.registry = this.actionMenusRegistry;
},
}, function () {
QUnit.module('ActionMenus');
QUnit.test('basic interactions', async function (assert) {
assert.expect(10);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
props: this.props,
});
const dropdowns = actionMenus.el.getElementsByClassName('dropdown');
assert.strictEqual(dropdowns.length, 2, "ActionMenus should contain 2 menus");
assert.strictEqual(dropdowns[0].querySelector('.o_dropdown_title').innerText.trim(), "Print");
assert.strictEqual(dropdowns[1].querySelector('.o_dropdown_title').innerText.trim(), "Action");
assert.containsNone(actionMenus, '.o-dropdown-menu');
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
assert.containsOnce(actionMenus, '.o-dropdown-menu');
assert.containsN(actionMenus, '.o-dropdown-menu .o_menu_item', 4);
const actionsTexts = [...dropdowns[1].querySelectorAll('.o_menu_item')].map(el => el.innerText.trim());
assert.deepEqual(actionsTexts, [
"Boil'em",
"Mash'em",
"Stick'em in a stew",
"What's taters, precious ?",
], "callbacks should appear before actions");
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
assert.containsOnce(actionMenus, '.o-dropdown-menu');
assert.containsN(actionMenus, '.o-dropdown-menu .o_menu_item', 1);
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
assert.containsNone(actionMenus, '.o-dropdown-menu');
});
QUnit.test("empty action menus", async function (assert) {
assert.expect(1);
ActionMenus.registry.add("test", { Component, getProps: () => false });
this.props.items = {};
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
props: this.props,
});
assert.containsNone(actionMenus, ".o_cp_action_menus > *");
});
QUnit.test('execute action', async function (assert) {
assert.expect(4);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
props: this.props,
intercepts: {
'do-action': ev => assert.step('do-action'),
},
async mockRPC(route, args) {
switch (route) {
case '/web/action/load':
const expectedContext = {
active_id: 23,
active_ids: [23],
active_model: 'hobbit',
};
assert.deepEqual(args.context, expectedContext);
assert.step('load-action');
return { context: {}, flags: {} };
default:
return this._super(...arguments);
}
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "What's taters, precious ?");
assert.verifySteps(['load-action', 'do-action']);
});
QUnit.test('execute callback action', async function (assert) {
assert.expect(2);
const callbackPromise = testUtils.makeTestPromise();
this.props.items.other[0].callback = function (items) {
assert.strictEqual(items.length, 1);
assert.strictEqual(items[0].description, "Boil'em");
callbackPromise.resolve();
};
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
props: this.props,
async mockRPC(route, args) {
switch (route) {
case '/web/action/load':
throw new Error("No action should be loaded.");
default:
return this._super(...arguments);
}
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Boil'em");
await callbackPromise;
});
QUnit.test('execute print action', async function (assert) {
assert.expect(4);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
intercepts: {
'do-action': ev => assert.step('do-action'),
},
props: this.props,
async mockRPC(route, args) {
switch (route) {
case '/web/action/load':
const expectedContext = {
active_id: 23,
active_ids: [23],
active_model: 'hobbit',
};
assert.deepEqual(args.context, expectedContext);
assert.step('load-action');
return { context: {}, flags: {} };
default:
return this._super(...arguments);
}
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Po-ta-toes");
assert.verifySteps(['load-action', 'do-action']);
});
QUnit.test('execute url action', async function (assert) {
assert.expect(2);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
services: {
navigate(url) {
assert.step(url);
},
},
view: this.view,
},
props: this.props,
async mockRPC(route, args) {
switch (route) {
case '/web/action/load':
throw new Error("No action should be loaded.");
default:
return this._super(...arguments);
}
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Stick'em in a stew");
assert.verifySteps(['#stew']);
});
QUnit.test('execute action with context', async function (assert) {
assert.expect(1);
const actionMenus = await createComponent(ActionMenus, {
env: {
action: this.action,
view: this.view,
},
props: {
...this.props,
isDomainSelected: true,
context: {
allowed_company_ids: [112],
},
},
async mockRPC(route, args) {
if (route === "/web/dataset/call_kw/hobbit/search"){
assert.deepEqual(args.kwargs.context, { allowed_company_ids: [112] }, "The kwargs should contains the right context");
}
return this._super(...arguments);
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Action");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "What's taters, precious ?");
});
});
});

View file

@ -0,0 +1,52 @@
odoo.define('web.custom_checkbox_tests', function (require) {
"use strict";
const CustomCheckbox = require('web.CustomCheckbox');
const testUtils = require('web.test_utils');
const { createComponent, dom: testUtilsDom } = testUtils;
QUnit.module('Components', {}, function () {
QUnit.module('CustomCheckbox');
QUnit.test('test checkbox: default values', async function(assert) {
assert.expect(6);
const checkbox = await createComponent(CustomCheckbox, {});
assert.containsOnce(checkbox.el, 'input');
assert.containsNone(checkbox.el, 'input:disabled');
assert.containsOnce(checkbox.el, 'label');
const input = checkbox.el.querySelector('input');
assert.notOk(input.checked, 'checkbox should be unchecked');
assert.ok(input.id.startsWith('checkbox-comp-'));
await testUtilsDom.click(checkbox.el.querySelector('label'));
assert.ok(input.checked, 'checkbox should be checked');
});
QUnit.test('test checkbox: custom values', async function(assert) {
assert.expect(6);
const checkbox = await createComponent(CustomCheckbox, {
props: {
id: 'my-form-check',
disabled: true,
value: true,
text: 'checkbox',
}
});
assert.containsOnce(checkbox.el, 'input');
assert.containsOnce(checkbox.el, 'input:disabled');
assert.containsOnce(checkbox.el, 'label');
const input = checkbox.el.querySelector('input');
assert.ok(input.checked, 'checkbox should be checked');
assert.strictEqual(input.id, 'my-form-check');
assert.ok(input.checked, 'checkbox should be checked');
});
});
});

View file

@ -0,0 +1,86 @@
odoo.define('web.custom_file_input_tests', function (require) {
"use strict";
const CustomFileInput = require('web.CustomFileInput');
const testUtils = require('web.test_utils');
const { createComponent } = testUtils;
QUnit.module('Components', {}, function () {
// This module cannot be tested as thoroughly as we want it to be:
// browsers do not let scripts programmatically assign values to inputs
// of type file
QUnit.module('CustomFileInput');
QUnit.test("Upload a file: default props", async function (assert) {
assert.expect(6);
const customFileInput = await createComponent(CustomFileInput, {
env: {
services: {
async httpRequest(route, params) {
assert.deepEqual(params, {
csrf_token: odoo.csrf_token,
ufile: [],
});
assert.step(route);
return '[]';
},
},
},
});
const input = customFileInput.el.querySelector('input');
assert.strictEqual(customFileInput.el.innerText.trim().toUpperCase(), "CHOOSE FILE",
"File input total text should match its given inner element's text");
assert.strictEqual(input.accept, '*',
"Input should accept all files by default");
await testUtils.dom.triggerEvent(input, 'change');
assert.notOk(input.multiple, "'multiple' attribute should not be set");
assert.verifySteps(['/web/binary/upload']);
});
QUnit.test("Upload a file: custom attachment", async function (assert) {
assert.expect(6);
const customFileInput = await createComponent(CustomFileInput, {
env: {
services: {
async httpRequest(route, params) {
assert.deepEqual(params, {
id: 5,
model: 'res.model',
csrf_token: odoo.csrf_token,
ufile: [],
});
assert.step(route);
return '[]';
},
},
},
props: {
accepted_file_extensions: '.png',
action: '/web/binary/upload_attachment',
id: 5,
model: 'res.model',
multi_upload: true,
},
intercepts: {
'uploaded': ev => assert.strictEqual(ev.detail.files.length, 0,
"'files' property should be an empty array"),
},
});
const input = customFileInput.el.querySelector('input');
assert.strictEqual(input.accept, '.png', "Input should now only accept pngs");
await testUtils.dom.triggerEvent(input, 'change');
assert.ok(input.multiple, "'multiple' attribute should be set");
assert.verifySteps(['/web/binary/upload_attachment']);
});
});
});

View file

@ -0,0 +1,333 @@
odoo.define('web.datepicker_tests', function (require) {
"use strict";
const { DatePicker, DateTimePicker } = require('web.DatePickerOwl');
const testUtils = require('web.test_utils');
const time = require('web.time');
const { createComponent } = testUtils;
QUnit.module('Components', {}, function () {
QUnit.module('DatePicker (legacy)');
QUnit.test("basic rendering", async function (assert) {
assert.expect(8);
const picker = await createComponent(DatePicker, {
props: { date: moment('1997-01-09'), onDateTimeChanged: () => {} },
});
assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
assert.containsOnce(picker, 'span.o_datepicker_button');
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
assert.strictEqual(input.value, '01/09/1997',
"Value should be the one given")
;
assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
"DatePicker id should match its input target");
await testUtils.dom.click(input);
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
assert.strictEqual(
document.querySelector('.datepicker .day.active').dataset.day,
'01/09/1997',
"Datepicker should have set the correct day"
);
});
QUnit.test("pick a date", async function (assert) {
assert.expect(5);
const picker = await createComponent(DatePicker, {
props: {
date: moment('1997-01-09'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY'), '02/08/1997',
"Event should transmit the correct date");
},
}
});
const input = picker.el.querySelector('.o_datepicker_input');
await testUtils.dom.click(input);
await testUtils.dom.click(document.querySelector('.datepicker th.next')); // next month
assert.verifySteps([]);
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // previous day
assert.strictEqual(input.value, '02/08/1997');
assert.verifySteps(['datetime-changed']);
});
QUnit.test("pick a date with locale", async function (assert) {
assert.expect(4);
// weird shit of moment https://github.com/moment/moment/issues/5600
// When month regex returns undefined, january is taken (first month of the default "nameless" locale)
const originalLocale = moment.locale();
// Those parameters will make Moment's internal compute stuff that are relevant to the bug
const months = 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_');
const monthsShort = 'janv._févr._mars_avr._mai_juin_juil._août_custSept._oct._nov._déc.'.split('_');
moment.defineLocale('frenchForTests', { months, monthsShort, code: 'frTest' , monthsParseExact: true});
const hasChanged = testUtils.makeTestPromise();
const picker = await createComponent(DatePicker, {
translateParameters: {
date_format: "%d %b, %Y", // Those are important too
time_format: "%H:%M:%S",
},
props: {
date: moment('09/01/1997', 'MM/DD/YYYY'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY'), '09/02/1997',
"Event should transmit the correct date");
hasChanged.resolve();
},
}
});
const input = picker.el.querySelector('.o_datepicker_input');
await testUtils.dom.click(input);
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[3]); // next day
assert.strictEqual(input.value, '02 custSept., 1997');
assert.verifySteps(['datetime-changed']);
moment.locale(originalLocale);
moment.updateLocale('frenchForTests', null);
});
QUnit.test("enter a date value", async function (assert) {
assert.expect(5);
const picker = await createComponent(DatePicker, {
props: {
date: moment('1997-01-09'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY'), '02/08/1997',
"Event should transmit the correct date");
},
}
});
const input = picker.el.querySelector('.o_datepicker_input');
assert.verifySteps([]);
await testUtils.fields.editAndTrigger(input, '02/08/1997', ['change']);
assert.verifySteps(['datetime-changed']);
await testUtils.dom.click(input);
assert.strictEqual(
document.querySelector('.datepicker .day.active').dataset.day,
'02/08/1997',
"Datepicker should have set the correct day"
);
});
QUnit.test("Date format is correctly set", async function (assert) {
assert.expect(2);
testUtils.mock.patch(time, { getLangDateFormat: () => "YYYY/MM/DD" });
const picker = await createComponent(DatePicker, {
props: { date: moment('1997-01-09'), onDateTimeChanged: () => {} },
});
const input = picker.el.querySelector('.o_datepicker_input');
assert.strictEqual(input.value, '1997/01/09');
// Forces an update to assert that the registered format is the correct one
await testUtils.dom.click(input);
assert.strictEqual(input.value, '1997/01/09');
testUtils.mock.unpatch(time);
});
QUnit.module('DateTimePicker (legacy)');
QUnit.test("basic rendering", async function (assert) {
assert.expect(11);
const picker = await createComponent(DateTimePicker, {
props: { date: moment('1997-01-09 12:30:01'), onDateTimeChanged: () => {} },
});
assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
assert.containsOnce(picker, 'span.o_datepicker_button');
assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
assert.strictEqual(input.value, '01/09/1997 12:30:01', "Value should be the one given");
assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
"DateTimePicker id should match its input target");
await testUtils.dom.click(input);
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
assert.strictEqual(
document.querySelector('.datepicker .day.active').dataset.day,
'01/09/1997',
"Datepicker should have set the correct day");
assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '12',
"Datepicker should have set the correct hour");
assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '30',
"Datepicker should have set the correct minute");
assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '01',
"Datepicker should have set the correct second");
});
QUnit.test("pick a date and time", async function (assert) {
assert.expect(5);
const picker = await createComponent(DateTimePicker, {
props: {
date: moment('1997-01-09 12:30:01'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
"Event should transmit the correct date");
},
}
});
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
await testUtils.dom.click(input);
await testUtils.dom.click(document.querySelector('.datepicker th.next')); // February
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // 08
await testUtils.dom.click(document.querySelector('a[title="Select Time"]'));
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour'));
await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute'));
await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second'));
assert.verifySteps([]);
await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s
assert.strictEqual(input.value, '02/08/1997 15:45:05');
assert.verifySteps(['datetime-changed']);
});
QUnit.test("pick a date and time with locale", async function (assert) {
assert.expect(5);
// weird shit of moment https://github.com/moment/moment/issues/5600
// When month regex returns undefined, january is taken (first month of the default "nameless" locale)
const originalLocale = moment.locale();
// Those parameters will make Moment's internal compute stuff that are relevant to the bug
const months = 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_');
const monthsShort = 'janv._févr._mars_avr._mai_juin_juil._août_custSept._oct._nov._déc.'.split('_');
moment.defineLocale('frenchForTests', { months, monthsShort, code: 'frTest' , monthsParseExact: true});
const hasChanged = testUtils.makeTestPromise();
const picker = await createComponent(DateTimePicker, {
translateParameters: {
date_format: "%d %b, %Y", // Those are important too
time_format: "%H:%M:%S",
},
props: {
date: moment('09/01/1997 12:30:01', 'MM/DD/YYYY HH:mm:ss'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '09/02/1997 15:45:05',
"Event should transmit the correct date");
hasChanged.resolve();
},
}
});
const input = picker.el.querySelector('input.o_input.o_datepicker_input');
await testUtils.dom.click(input);
await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[3]); // next day
await testUtils.dom.click(document.querySelector('a[title="Select Time"]'));
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour'));
await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute'));
await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m
await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second'));
assert.verifySteps([]);
await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s
assert.strictEqual(input.value, '02 custSept., 1997 15:45:05');
assert.verifySteps(['datetime-changed']);
await hasChanged;
moment.locale(originalLocale);
moment.updateLocale('frenchForTests', null);
});
QUnit.test("enter a datetime value", async function (assert) {
assert.expect(9);
const picker = await createComponent(DateTimePicker, {
props: {
date: moment('1997-01-09 12:30:01'),
onDateTimeChanged: date => {
assert.step('datetime-changed');
assert.strictEqual(date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
"Event should transmit the correct date");
},
}
});
const input = picker.el.querySelector('.o_datepicker_input');
assert.verifySteps([]);
await testUtils.fields.editAndTrigger(input, '02/08/1997 15:45:05', ['change']);
assert.verifySteps(['datetime-changed']);
await testUtils.dom.click(input);
assert.strictEqual(input.value, '02/08/1997 15:45:05');
assert.strictEqual(
document.querySelector('.datepicker .day.active').dataset.day,
'02/08/1997',
"Datepicker should have set the correct day"
);
assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '15',
"Datepicker should have set the correct hour");
assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '45',
"Datepicker should have set the correct minute");
assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '05',
"Datepicker should have set the correct second");
});
QUnit.test("Date time format is correctly set", async function (assert) {
assert.expect(2);
testUtils.mock.patch(time, { getLangDatetimeFormat: () => "hh:mm:ss YYYY/MM/DD" });
const picker = await createComponent(DateTimePicker, {
props: { date: moment('1997-01-09 12:30:01'), onDateTimeChanged: () => {} },
});
const input = picker.el.querySelector('.o_datepicker_input');
assert.strictEqual(input.value, '12:30:01 1997/01/09');
// Forces an update to assert that the registered format is the correct one
await testUtils.dom.click(input);
assert.strictEqual(input.value, '12:30:01 1997/01/09');
testUtils.mock.unpatch(time);
});
});
});

View file

@ -0,0 +1,68 @@
odoo.define('web.dropdown_menu_mobile_tests', function (require) {
"use strict";
const DropdownMenu = require('web.DropdownMenu');
const testUtils = require('web.test_utils');
const { createComponent } = testUtils;
QUnit.module('Components', {
before: function () {
this.items = [
{
isActive: false,
description: 'Some Item',
id: 1,
groupId: 1,
groupNumber: 1,
options: [
{ description: "First Option", groupNumber: 1, id: 1 },
{ description: "Second Option", groupNumber: 2, id: 2 },
],
}, {
isActive: true,
description: 'Some other Item',
id: 2,
groupId: 2,
groupNumber: 2,
},
];
},
}, function () {
QUnit.module('DropdownMenu');
QUnit.test('display dropdown at the right position', async function (assert) {
assert.expect(2);
const viewPort = testUtils.prepareTarget();
viewPort.style.position = 'initial';
const dropdown = await createComponent(DropdownMenu, {
env: {
device: {
isMobile: true
},
},
props: {
items: this.items,
title: "Dropdown",
},
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsOnce(dropdown.el, '.dropdown-menu-start',
"should display the dropdown menu at the right screen");
await testUtils.dom.click(dropdown.el.querySelector('button'));
// position the dropdown to the right
dropdown.el.parentNode.classList.add('clearfix');
dropdown.el.classList.add('float-end');
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsOnce(dropdown.el, '.dropdown-menu-end',
"should display the dropdown menu at the left screen");
dropdown.el.parentNode.classList.remove('clearfix');
viewPort.style.position = '';
});
});
});

View file

@ -0,0 +1,430 @@
odoo.define('web.dropdown_menu_tests', function (require) {
"use strict";
const DropdownMenu = require('web.DropdownMenu');
const testUtils = require('web.test_utils');
const makeTestEnvironment = require("web.test_env");
const { mount } = require("@web/../tests/helpers/utils");
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { useState, xml } = owl;
const { createComponent } = testUtils;
QUnit.module('Components', {
beforeEach: function () {
this.items = [
{
isActive: false,
description: 'Some Item',
id: 1,
groupId: 1,
groupNumber: 1,
options: [
{ description: "First Option", groupNumber: 1, id: 1 },
{ description: "Second Option", groupNumber: 2, id: 2 },
],
}, {
isActive: true,
description: 'Some other Item',
id: 2,
groupId: 2,
groupNumber: 2,
},
];
},
}, function () {
QUnit.module('DropdownMenu');
QUnit.test('simple rendering and basic interactions', async function (assert) {
assert.expect(8);
const dropdown = await createComponent(DropdownMenu, {
props: {
items: this.items,
title: "Dropdown",
},
});
assert.strictEqual(dropdown.el.querySelector('button').innerText.trim(), "Dropdown");
assert.containsNone(dropdown, '.o-dropdown-menu');
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3,
'should have 3 elements counting the divider');
const itemEls = dropdown.el.querySelectorAll('.o_menu_item > .dropdown-item');
assert.strictEqual(itemEls[0].innerText.trim(), 'Some Item');
assert.doesNotHaveClass(itemEls[0], 'selected');
assert.hasClass(itemEls[1], 'selected');
const dropdownElements = dropdown.el.querySelectorAll('.o_menu_item *');
for (const dropdownEl of dropdownElements) {
await testUtils.dom.click(dropdownEl);
}
assert.containsOnce(dropdown, '.o-dropdown-menu',
"Clicking on any item of the dropdown should not close it");
await testUtils.dom.click(document.body);
assert.containsNone(dropdown, '.o-dropdown-menu',
"Clicking outside of the dropdown should close it");
});
QUnit.test('only one dropdown rendering at same time (owl vs bootstrap dropdown)', async function (assert) {
assert.expect(12);
const bsDropdown = document.createElement('div');
bsDropdown.innerHTML = `<div class="dropdown">
<button class="btn dropdown-toggle" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
BS Dropdown button
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">BS Action</a>
</div>
</div>`;
document.body.append(bsDropdown);
const dropdown = await createComponent(DropdownMenu, {
props: {
items: this.items,
title: "Dropdown",
},
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.hasClass(dropdown.el.querySelector('.o-dropdown-menu'), 'show');
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isVisible(dropdown.el.querySelector('.o-dropdown-menu'),
"owl dropdown menu should be visible");
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should not be visible");
await testUtils.dom.click(bsDropdown.querySelector('.btn.dropdown-toggle'));
assert.doesNotHaveClass(dropdown.el, 'show');
assert.containsNone(dropdown.el, '.o-dropdown-menu',
"owl dropdown menu should not be set inside the dom");
assert.hasClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should be visible");
await testUtils.dom.click(document.body);
assert.doesNotHaveClass(dropdown.el, 'show');
assert.containsNone(dropdown.el, '.o-dropdown-menu',
"owl dropdown menu should not be set inside the dom");
assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
assert.isNotVisible(bsDropdown.querySelector('.dropdown-menu'),
"bs dropdown menu should not be visible");
bsDropdown.remove();
});
QUnit.test('click on an item without options should toggle it', async function (assert) {
assert.expect(7);
delete this.items[0].options;
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
intercepts: {
'item-selected': function (ev) {
assert.strictEqual(ev.detail.item.id, 1);
this.state.items[0].isActive = !this.state.items[0].isActive;
},
}
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
assert.doesNotHaveClass(firstItemEl, 'selected');
await testUtils.dom.click(firstItemEl);
assert.hasClass(firstItemEl, 'selected');
assert.isVisible(firstItemEl);
await testUtils.dom.click(firstItemEl);
assert.doesNotHaveClass(firstItemEl, 'selected');
assert.isVisible(firstItemEl);
});
QUnit.test('click on an item should not change url', async function (assert) {
assert.expect(1);
delete this.items[0].options;
const initialHref = window.location.href;
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
assert.strictEqual(window.location.href, initialHref,
"the url should not have changed after a click on an item");
});
QUnit.test('options rendering', async function (assert) {
assert.expect(6);
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
});
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
// open options menu
await testUtils.dom.click(firstItemEl);
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-down');
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
// close options menu
await testUtils.dom.click(firstItemEl);
assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
});
QUnit.test('close menu closes also submenus', async function (assert) {
assert.expect(2);
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
});
// open dropdown menu
await testUtils.dom.click(dropdown.el.querySelector('button'));
// open options menu of first item
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item a'));
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
await testUtils.dom.click(dropdown.el.querySelector('button'));
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
});
QUnit.test('click on an option should trigger the event "item_option_clicked" with appropriate data', async function (assert) {
assert.expect(18);
let eventNumber = 0;
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
intercepts: {
'item-selected': function (ev) {
eventNumber++;
const { option } = ev.detail;
assert.strictEqual(ev.detail.item.id, 1);
if (eventNumber === 1) {
assert.strictEqual(option.id, 1);
this.state.items[0].isActive = true;
this.state.items[0].options[0].isActive = true;
}
if (eventNumber === 2) {
assert.strictEqual(option.id, 2);
this.state.items[0].options[1].isActive = true;
}
if (eventNumber === 3) {
assert.strictEqual(option.id, 1);
this.state.items[0].options[0].isActive = false;
}
if (eventNumber === 4) {
assert.strictEqual(option.id, 2);
this.state.items[0].isActive = false;
this.state.items[0].options[1].isActive = false;
}
},
}
});
// open dropdown menu
await testUtils.dom.click(dropdown.el.querySelector('button'));
assert.containsN(dropdown, '.dropdown-divider, .o_menu_item', 3);
// open menu options of first item
await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
let optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
// click on first option
await testUtils.dom.click(optionELs[0]);
assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
assert.hasClass(optionELs[0], 'selected');
assert.doesNotHaveClass(optionELs[1], 'selected');
// click on second option
await testUtils.dom.click(optionELs[1]);
assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
assert.hasClass(optionELs[0], 'selected');
assert.hasClass(optionELs[1], 'selected');
// click again on first option
await testUtils.dom.click(optionELs[0]);
// click again on second option
await testUtils.dom.click(optionELs[1]);
assert.doesNotHaveClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
assert.doesNotHaveClass(optionELs[0], 'selected');
assert.doesNotHaveClass(optionELs[1], 'selected');
});
QUnit.test('keyboard navigation', async function (assert) {
assert.expect(12);
// Shorthand method to trigger a specific keydown.
// Note that BootStrap handles some of the navigation moves (up and down)
// so we need to give the event the proper "which" property. We also give
// it when it's not required to check if it has been correctly prevented.
async function navigate(key, global) {
const which = {
Enter: 13,
Escape: 27,
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40,
}[key];
const target = global ? document.body : document.activeElement;
await testUtils.dom.triggerEvent(target, 'keydown', { key, which });
if (key === 'Enter') {
// Pressing "Enter" on a focused element triggers a click (HTML5 specs)
await testUtils.dom.click(target);
}
}
const dropdown = await createComponent(DropdownMenu, {
props: { items: this.items },
});
// Initialize active element (start at toggle button)
dropdown.el.querySelector('button').focus();
await testUtils.dom.click(dropdown.el.querySelector('button'));
await navigate('ArrowDown'); // Go to next item
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
assert.containsNone(dropdown, '.o_item_option');
await navigate('ArrowRight'); // Unfold first item's options (w/ Right)
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
assert.containsN(dropdown, '.o_item_option', 2);
await navigate('ArrowDown'); // Go to next option
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_item_option a'));
await navigate('ArrowLeft'); // Fold first item's options (w/ Left)
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
assert.containsNone(dropdown, '.o_item_option');
await navigate('Enter'); // Unfold first item's options (w/ Enter)
assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
assert.containsN(dropdown, '.o_item_option', 2);
await navigate('ArrowDown'); // Go to next option
await navigate('Escape'); // Fold first item's options (w/ Escape)
await testUtils.nextTick();
assert.strictEqual(dropdown.el.querySelector('.o_menu_item a'), document.activeElement);
assert.containsNone(dropdown, '.o_item_option');
await navigate('Escape', true); // Close the dropdown
assert.containsNone(dropdown, '.o-dropdown-menu', "Dropdown should be folded");
});
QUnit.test('interactions between multiple dropdowns', async function (assert) {
assert.expect(7);
const items = this.items;
class Parent extends LegacyComponent {
setup() {
this.state = useState({ items });
}
}
Parent.components = { DropdownMenu };
Parent.template = xml`
<div>
<DropdownMenu title="'First'" items="state.items"/>
<DropdownMenu title="'Second'" items="state.items"/>
</div>`;
const env = makeTestEnvironment();
const target = testUtils.prepareTarget();
const parent = await mount(Parent, target, { env });
const [menu1, menu2] = parent.el.querySelectorAll('.dropdown');
assert.containsNone(parent, '.o-dropdown-menu');
await testUtils.dom.click(menu1.querySelector('button'));
assert.containsOnce(parent, '.o-dropdown-menu');
const [first, second] = parent.el.querySelectorAll(".dropdown");
assert.containsOnce(first, '.o-dropdown-menu');
await testUtils.dom.click(menu2.querySelector('button'));
assert.containsOnce(parent, '.o-dropdown-menu');
assert.containsOnce(second, '.o-dropdown-menu');
await testUtils.dom.click(menu2.querySelector('.o_menu_item a'));
await testUtils.dom.click(menu1.querySelector('button'));
assert.containsOnce(parent, '.o-dropdown-menu');
assert.containsOnce(first, '.o-dropdown-menu');
});
QUnit.test("dropdown doesn't get close on mousedown inside and mouseup outside dropdown", async function (assert) {
// In this test, we simulate a case where the user clicks inside a dropdown menu item
// (e.g. in the input of the 'Save current search' item in the Favorites menu), keeps
// the click pressed, moves the cursor outside the dropdown and releases the click
// (i.e. mousedown and focus inside the item, mouseup and click outside the dropdown).
// In this case, we want to keep the dropdown menu open.
assert.expect(5);
const items = this.items;
class Parent extends LegacyComponent {
setup() {
this.items = items;
}
}
Parent.components = { DropdownMenu };
Parent.template = xml`
<div>
<DropdownMenu title="'First'" items="items"/>
</div>`;
const env = makeTestEnvironment();
const target = testUtils.prepareTarget();
const parent = await mount(Parent, target, { env });
const menu = parent.el.querySelector(".dropdown");
assert.doesNotHaveClass(menu, "show", "dropdown should not be open");
await testUtils.dom.click(menu.querySelector("button"));
assert.hasClass(menu, "show", "dropdown should be open");
const firstItemEl = menu.querySelector(".o_menu_item > a");
// open options menu
await testUtils.dom.click(firstItemEl);
assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down");
// force the focus inside the dropdown item and click outside
firstItemEl.parentElement.querySelector(".o_menu_item_options .o_item_option a").focus();
await testUtils.dom.triggerEvents(parent.el, "click");
assert.hasClass(menu, "show", "dropdown should still be open");
assert.hasClass(firstItemEl.querySelector("i"), "o_icon_right fa fa-caret-down");
});
});
});

View file

@ -0,0 +1,188 @@
odoo.define('web.pager_tests', function (require) {
"use strict";
const Pager = require('web.Pager');
const testUtils = require('web.test_utils');
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { createComponent } = testUtils;
const { xml, useState } = owl;
class PagerController extends LegacyComponent {
setup() {
this.state = useState({ ...this.props });
}
async updateProps(nextProps) {
Object.assign(this.state, nextProps);
await testUtils.nextTick();
}
}
PagerController.template = xml`<Pager t-props="state" />`;
PagerController.components = { Pager };
QUnit.module('Components', {}, function () {
QUnit.module('Legacy Pager');
QUnit.test('basic interactions', async function (assert) {
assert.expect(2);
const pager = await createComponent(PagerController, {
props: {
currentMinimum: 1,
limit: 4,
size: 10,
onPagerChanged: function (detail) {
pager.updateProps(detail);
},
},
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4",
"currentMinimum should be set to 1");
await testUtils.controlPanel.pagerNext(pager);
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "5-8",
"currentMinimum should now be 5");
});
QUnit.test('edit the pager', async function (assert) {
assert.expect(4);
const pager = await createComponent(PagerController, {
props: {
currentMinimum: 1,
limit: 4,
size: 10,
onPagerChanged: function (detail) {
pager.updateProps(detail);
},
},
});
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
assert.containsOnce(pager, 'input',
"the pager should contain an input");
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4",
"the input should have correct value");
// change the limit
await testUtils.controlPanel.setPagerValue(pager, "1-6");
assert.containsNone(pager, 'input',
"the pager should not contain an input anymore");
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-6",
"the limit should have been updated");
});
QUnit.test("keydown on pager with same value", async function (assert) {
assert.expect(7);
const pager = await createComponent(PagerController, {
props: {
currentMinimum: 1,
limit: 4,
size: 10,
onPagerChanged: () => assert.step("pager-changed"),
},
});
// Enter edit mode
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
assert.containsOnce(pager, "input");
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4");
assert.verifySteps([]);
// Exit edit mode
await testUtils.dom.triggerEvent(pager.el.querySelector('input'), "keydown", { key: "Enter" });
assert.containsNone(pager, "input");
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4");
assert.verifySteps(["pager-changed"]);
});
QUnit.test('pager value formatting', async function (assert) {
assert.expect(8);
const pager = await createComponent(PagerController, {
props: {
currentMinimum: 1,
limit: 4,
size: 10,
onPagerChanged: (detail) => {
pager.updateProps(detail);
},
},
});
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "1-4", "Initial value should be correct");
async function inputAndAssert(input, expected, reason) {
await testUtils.controlPanel.setPagerValue(pager, input);
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), expected,
`Pager value should be "${expected}" when given "${input}": ${reason}`);
}
await inputAndAssert("4-4", "4", "values are squashed when minimum = maximum");
await inputAndAssert("1-11", "1-10", "maximum is floored to size when out of range");
await inputAndAssert("20-15", "10", "combination of the 2 assertions above");
await inputAndAssert("6-5", "10", "fallback to previous value when minimum > maximum");
await inputAndAssert("definitelyValidNumber", "10", "fallback to previous value if not a number");
await inputAndAssert(" 1 , 2 ", "1-2", "value is normalized and accepts several separators");
await inputAndAssert("3 8", "3-8", "value accepts whitespace(s) as a separator");
});
QUnit.test('pager disabling', async function (assert) {
assert.expect(10);
const reloadPromise = testUtils.makeTestPromise();
const pager = await createComponent(PagerController, {
props: {
currentMinimum: 1,
limit: 4,
size: 10,
// The goal here is to test the reactivity of the pager; in a
// typical views, we disable the pager after switching page
// to avoid switching twice with the same action (double click).
onPagerChanged: async function (detail) {
// 1. Simulate a (long) server action
await reloadPromise;
// 2. Update the view with loaded data
pager.updateProps(detail);
},
},
});
const pagerButtons = pager.el.querySelectorAll('button');
// Click and check button is disabled
await testUtils.controlPanel.pagerNext(pager);
assert.ok(pager.el.querySelector('button.o_pager_next').disabled);
// Try to edit the pager value
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
assert.ok(pagerButtons[0].disabled, "'previous' is disabled");
assert.ok(pagerButtons[1].disabled, "'next' is disabled");
assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'SPAN',
"pager edition is prevented");
// Server action is done
reloadPromise.resolve();
await testUtils.nextTick();
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
assert.notOk(pagerButtons[0].disabled, "'previous' is enabled");
assert.notOk(pagerButtons[1].disabled, "'next' is enabled");
assert.strictEqual(testUtils.controlPanel.getPagerValue(pager), "5-8", "value has been updated");
await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'INPUT',
"pager edition is re-enabled");
});
});
});