summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/components
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/components
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/components')
-rw-r--r--addons/web/static/tests/components/action_menus_tests.js251
-rw-r--r--addons/web/static/tests/components/custom_checkbox_tests.js56
-rw-r--r--addons/web/static/tests/components/custom_file_input_tests.js90
-rw-r--r--addons/web/static/tests/components/datepicker_tests.js350
-rw-r--r--addons/web/static/tests/components/dropdown_menu_tests.js442
-rw-r--r--addons/web/static/tests/components/pager_tests.js194
6 files changed, 1383 insertions, 0 deletions
diff --git a/addons/web/static/tests/components/action_menus_tests.js b/addons/web/static/tests/components/action_menus_tests.js
new file mode 100644
index 00000000..d1df0d59
--- /dev/null
+++ b/addons/web/static/tests/components/action_menus_tests.js
@@ -0,0 +1,251 @@
+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 cpHelpers = testUtils.controlPanel;
+ const { createComponent } = testUtils;
+
+ QUnit.module('Components', {
+ beforeEach() {
+ this.action = {
+ res_model: 'hobbit',
+ };
+ this.view = {
+ // needed by google_drive module, makes sense to give a view anyway.
+ type: 'form',
+ };
+ this.props = {
+ activeIds: [23],
+ context: {},
+ items: {
+ action: [
+ { action: { id: 1 }, name: "What's taters, precious ?", id: 1 },
+ ],
+ print: [
+ { action: { id: 2 }, name: "Po-ta-toes", id: 2 },
+ ],
+ other: [
+ { description: "Boil'em", callback() { } },
+ { description: "Mash'em", callback() { } },
+ { description: "Stick'em in a stew", url: '#stew' },
+ ],
+ },
+ };
+ // Patch the registry of the action menus
+ this.actionMenusRegistry = ActionMenus.registry;
+ ActionMenus.registry = new Registry();
+ },
+ afterEach() {
+ ActionMenus.registry = this.actionMenusRegistry;
+ },
+ }, function () {
+
+ QUnit.module('ActionMenus');
+
+ QUnit.test('basic interactions', async function (assert) {
+ assert.expect(10);
+
+ const actionMenus = await createComponent(ActionMenus, {
+ env: {
+ action: this.action,
+ view: this.view,
+ },
+ props: this.props,
+ });
+
+ const dropdowns = actionMenus.el.getElementsByClassName('o_dropdown');
+ assert.strictEqual(dropdowns.length, 2, "ActionMenus should contain 2 menus");
+ assert.strictEqual(dropdowns[0].querySelector('.o_dropdown_title').innerText.trim(), "Print");
+ assert.strictEqual(dropdowns[1].querySelector('.o_dropdown_title').innerText.trim(), "Action");
+ assert.containsNone(actionMenus, '.o_dropdown_menu');
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Action");
+
+ assert.containsOnce(actionMenus, '.o_dropdown_menu');
+ assert.containsN(actionMenus, '.o_dropdown_menu .o_menu_item', 4);
+ const actionsTexts = [...dropdowns[1].querySelectorAll('.o_menu_item')].map(el => el.innerText.trim());
+ assert.deepEqual(actionsTexts, [
+ "Boil'em",
+ "Mash'em",
+ "Stick'em in a stew",
+ "What's taters, precious ?",
+ ], "callbacks should appear before actions");
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Print");
+
+ assert.containsOnce(actionMenus, '.o_dropdown_menu');
+ assert.containsN(actionMenus, '.o_dropdown_menu .o_menu_item', 1);
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Print");
+
+ assert.containsNone(actionMenus, '.o_dropdown_menu');
+
+ actionMenus.destroy();
+ });
+
+ QUnit.test("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 > *");
+
+ actionMenus.destroy();
+ });
+
+ QUnit.test('execute action', async function (assert) {
+ assert.expect(4);
+
+ const actionMenus = await createComponent(ActionMenus, {
+ env: {
+ action: this.action,
+ view: this.view,
+ },
+ props: this.props,
+ intercepts: {
+ 'do-action': ev => assert.step('do-action'),
+ },
+ async mockRPC(route, args) {
+ switch (route) {
+ case '/web/action/load':
+ const expectedContext = {
+ active_id: 23,
+ active_ids: [23],
+ active_model: 'hobbit',
+ };
+ assert.deepEqual(args.context, expectedContext);
+ assert.step('load-action');
+ return { context: {}, flags: {} };
+ default:
+ return this._super(...arguments);
+
+ }
+ },
+ });
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Action");
+ await cpHelpers.toggleMenuItem(actionMenus, "What's taters, precious ?");
+
+ assert.verifySteps(['load-action', 'do-action']);
+
+ actionMenus.destroy();
+ });
+
+ QUnit.test('execute callback action', async function (assert) {
+ assert.expect(2);
+
+ const callbackPromise = testUtils.makeTestPromise();
+ this.props.items.other[0].callback = function (items) {
+ assert.strictEqual(items.length, 1);
+ assert.strictEqual(items[0].description, "Boil'em");
+ callbackPromise.resolve();
+ };
+
+ const actionMenus = await createComponent(ActionMenus, {
+ env: {
+ action: this.action,
+ view: this.view,
+ },
+ props: this.props,
+ async mockRPC(route, args) {
+ switch (route) {
+ case '/web/action/load':
+ throw new Error("No action should be loaded.");
+ default:
+ return this._super(...arguments);
+ }
+ },
+ });
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Action");
+ await cpHelpers.toggleMenuItem(actionMenus, "Boil'em");
+
+ await callbackPromise;
+
+ actionMenus.destroy();
+ });
+
+ QUnit.test('execute print action', async function (assert) {
+ assert.expect(4);
+
+ const actionMenus = await createComponent(ActionMenus, {
+ env: {
+ action: this.action,
+ view: this.view,
+ },
+ intercepts: {
+ 'do-action': ev => assert.step('do-action'),
+ },
+ props: this.props,
+ async mockRPC(route, args) {
+ switch (route) {
+ case '/web/action/load':
+ const expectedContext = {
+ active_id: 23,
+ active_ids: [23],
+ active_model: 'hobbit',
+ };
+ assert.deepEqual(args.context, expectedContext);
+ assert.step('load-action');
+ return { context: {}, flags: {} };
+ default:
+ return this._super(...arguments);
+
+ }
+ },
+ });
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Print");
+ await cpHelpers.toggleMenuItem(actionMenus, "Po-ta-toes");
+
+ assert.verifySteps(['load-action', 'do-action']);
+
+ actionMenus.destroy();
+ });
+
+ QUnit.test('execute url action', async function (assert) {
+ assert.expect(2);
+
+ const actionMenus = await createComponent(ActionMenus, {
+ env: {
+ action: this.action,
+ services: {
+ navigate(url) {
+ assert.step(url);
+ },
+ },
+ view: this.view,
+ },
+ props: this.props,
+ async mockRPC(route, args) {
+ switch (route) {
+ case '/web/action/load':
+ throw new Error("No action should be loaded.");
+ default:
+ return this._super(...arguments);
+ }
+ },
+ });
+
+ await cpHelpers.toggleActionMenu(actionMenus, "Action");
+ await cpHelpers.toggleMenuItem(actionMenus, "Stick'em in a stew");
+
+ assert.verifySteps(['#stew']);
+
+ actionMenus.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/components/custom_checkbox_tests.js b/addons/web/static/tests/components/custom_checkbox_tests.js
new file mode 100644
index 00000000..21d7ae53
--- /dev/null
+++ b/addons/web/static/tests/components/custom_checkbox_tests.js
@@ -0,0 +1,56 @@
+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');
+
+ checkbox.destroy();
+ });
+
+ QUnit.test('test checkbox: custom values', async function(assert) {
+ assert.expect(6);
+
+ const checkbox = await createComponent(CustomCheckbox, {
+ props: {
+ id: 'my-custom-checkbox',
+ 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-custom-checkbox');
+ assert.ok(input.checked, 'checkbox should be checked');
+
+ checkbox.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/components/custom_file_input_tests.js b/addons/web/static/tests/components/custom_file_input_tests.js
new file mode 100644
index 00000000..279bf67f
--- /dev/null
+++ b/addons/web/static/tests/components/custom_file_input_tests.js
@@ -0,0 +1,90 @@
+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']);
+
+ customFileInput.destroy();
+ });
+
+ 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']);
+
+ customFileInput.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/components/datepicker_tests.js b/addons/web/static/tests/components/datepicker_tests.js
new file mode 100644
index 00000000..643bcdb5
--- /dev/null
+++ b/addons/web/static/tests/components/datepicker_tests.js
@@ -0,0 +1,350 @@
+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');
+
+ QUnit.test("basic rendering", async function (assert) {
+ assert.expect(8);
+
+ const picker = await createComponent(DatePicker, {
+ props: { date: moment('01/09/1997') },
+ });
+
+ assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
+ assert.containsOnce(picker, 'span.o_datepicker_button');
+ assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
+
+ const input = picker.el.querySelector('input.o_input.o_datepicker_input');
+ assert.strictEqual(input.value, '01/09/1997',
+ "Value should be the one given")
+ ;
+ assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
+ "DatePicker id should match its input target");
+
+ await testUtils.dom.click(input);
+
+ assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
+ assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
+ assert.strictEqual(
+ document.querySelector('.datepicker .day.active').dataset.day,
+ '01/09/1997',
+ "Datepicker should have set the correct day"
+ );
+
+ picker.destroy();
+ });
+
+ QUnit.test("pick a date", async function (assert) {
+ assert.expect(5);
+
+ const picker = await createComponent(DatePicker, {
+ props: { date: moment('01/09/1997') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.date.format('MM/DD/YYYY'), '02/08/1997',
+ "Event should transmit the correct date");
+ },
+ }
+ });
+ const input = picker.el.querySelector('.o_datepicker_input');
+
+ await testUtils.dom.click(input);
+ await testUtils.dom.click(document.querySelector('.datepicker th.next')); // next month
+
+ assert.verifySteps([]);
+
+ await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // previous day
+
+ assert.strictEqual(input.value, '02/08/1997');
+ assert.verifySteps(['datetime-changed']);
+
+ picker.destroy();
+ });
+
+ QUnit.test("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') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.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('englishForTest', null);
+
+ picker.destroy();
+ });
+
+ QUnit.test("enter a date value", async function (assert) {
+ assert.expect(5);
+
+ const picker = await createComponent(DatePicker, {
+ props: { date: moment('01/09/1997') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.date.format('MM/DD/YYYY'), '02/08/1997',
+ "Event should transmit the correct date");
+ },
+ }
+ });
+ const input = picker.el.querySelector('.o_datepicker_input');
+
+ assert.verifySteps([]);
+
+ await testUtils.fields.editAndTrigger(input, '02/08/1997', ['change']);
+
+ assert.verifySteps(['datetime-changed']);
+
+ await testUtils.dom.click(input);
+
+ assert.strictEqual(
+ document.querySelector('.datepicker .day.active').dataset.day,
+ '02/08/1997',
+ "Datepicker should have set the correct day"
+ );
+
+ picker.destroy();
+ });
+
+ QUnit.test("Date format is correctly set", async function (assert) {
+ assert.expect(2);
+
+ testUtils.patch(time, { getLangDateFormat: () => "YYYY/MM/DD" });
+ const picker = await createComponent(DatePicker, {
+ props: { date: moment('01/09/1997') },
+ });
+ 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');
+
+ picker.destroy();
+ testUtils.unpatch(time);
+ });
+
+ QUnit.module('DateTimePicker');
+
+ QUnit.test("basic rendering", async function (assert) {
+ assert.expect(11);
+
+ const picker = await createComponent(DateTimePicker, {
+ props: { date: moment('01/09/1997 12:30:01') },
+ });
+
+ assert.containsOnce(picker, 'input.o_input.o_datepicker_input');
+ assert.containsOnce(picker, 'span.o_datepicker_button');
+ assert.containsNone(document.body, 'div.bootstrap-datetimepicker-widget');
+
+ const input = picker.el.querySelector('input.o_input.o_datepicker_input');
+ assert.strictEqual(input.value, '01/09/1997 12:30:01', "Value should be the one given");
+ assert.strictEqual(input.dataset.target, `#${picker.el.id}`,
+ "DateTimePicker id should match its input target");
+
+ await testUtils.dom.click(input);
+
+ assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .datepicker');
+ assert.containsOnce(document.body, 'div.bootstrap-datetimepicker-widget .timepicker');
+ assert.strictEqual(
+ document.querySelector('.datepicker .day.active').dataset.day,
+ '01/09/1997',
+ "Datepicker should have set the correct day");
+
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '12',
+ "Datepicker should have set the correct hour");
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '30',
+ "Datepicker should have set the correct minute");
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '01',
+ "Datepicker should have set the correct second");
+
+ picker.destroy();
+ });
+
+ QUnit.test("pick a date and time", async function (assert) {
+ assert.expect(5);
+
+ const picker = await createComponent(DateTimePicker, {
+ props: { date: moment('01/09/1997 12:30:01') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
+ "Event should transmit the correct date");
+ },
+ }
+ });
+ const input = picker.el.querySelector('input.o_input.o_datepicker_input');
+
+ await testUtils.dom.click(input);
+ await testUtils.dom.click(document.querySelector('.datepicker th.next')); // February
+ await testUtils.dom.click(document.querySelectorAll('.datepicker table td')[15]); // 08
+ await testUtils.dom.click(document.querySelector('a[title="Select Time"]'));
+ await testUtils.dom.click(document.querySelector('.timepicker .timepicker-hour'));
+ await testUtils.dom.click(document.querySelectorAll('.timepicker .hour')[15]); // 15h
+ await testUtils.dom.click(document.querySelector('.timepicker .timepicker-minute'));
+ await testUtils.dom.click(document.querySelectorAll('.timepicker .minute')[9]); // 45m
+ await testUtils.dom.click(document.querySelector('.timepicker .timepicker-second'));
+
+ assert.verifySteps([]);
+
+ await testUtils.dom.click(document.querySelectorAll('.timepicker .second')[1]); // 05s
+
+ assert.strictEqual(input.value, '02/08/1997 15:45:05');
+ assert.verifySteps(['datetime-changed']);
+
+ picker.destroy();
+ });
+
+ QUnit.test("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') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.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);
+
+ picker.destroy();
+ });
+
+ QUnit.test("enter a datetime value", async function (assert) {
+ assert.expect(9);
+
+ const picker = await createComponent(DateTimePicker, {
+ props: { date: moment('01/09/1997 12:30:01') },
+ intercepts: {
+ 'datetime-changed': ev => {
+ assert.step('datetime-changed');
+ assert.strictEqual(ev.detail.date.format('MM/DD/YYYY HH:mm:ss'), '02/08/1997 15:45:05',
+ "Event should transmit the correct date");
+ },
+ }
+ });
+ const input = picker.el.querySelector('.o_datepicker_input');
+
+ assert.verifySteps([]);
+
+ await testUtils.fields.editAndTrigger(input, '02/08/1997 15:45:05', ['change']);
+
+ assert.verifySteps(['datetime-changed']);
+
+ await testUtils.dom.click(input);
+
+ assert.strictEqual(input.value, '02/08/1997 15:45:05');
+ assert.strictEqual(
+ document.querySelector('.datepicker .day.active').dataset.day,
+ '02/08/1997',
+ "Datepicker should have set the correct day"
+ );
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-hour').innerText.trim(), '15',
+ "Datepicker should have set the correct hour");
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-minute').innerText.trim(), '45',
+ "Datepicker should have set the correct minute");
+ assert.strictEqual(document.querySelector('.timepicker .timepicker-second').innerText.trim(), '05',
+ "Datepicker should have set the correct second");
+
+ picker.destroy();
+ });
+
+ QUnit.test("Date time format is correctly set", async function (assert) {
+ assert.expect(2);
+
+ testUtils.patch(time, { getLangDatetimeFormat: () => "hh:mm:ss YYYY/MM/DD" });
+ const picker = await createComponent(DateTimePicker, {
+ props: { date: moment('01/09/1997 12:30:01') },
+ });
+ 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');
+
+ picker.destroy();
+ testUtils.unpatch(time);
+ });
+ });
+});
diff --git a/addons/web/static/tests/components/dropdown_menu_tests.js b/addons/web/static/tests/components/dropdown_menu_tests.js
new file mode 100644
index 00000000..3aff0ae1
--- /dev/null
+++ b/addons/web/static/tests/components/dropdown_menu_tests.js
@@ -0,0 +1,442 @@
+odoo.define('web.dropdown_menu_tests', function (require) {
+ "use strict";
+
+ const DropdownMenu = require('web.DropdownMenu');
+ const testUtils = require('web.test_utils');
+
+ const { createComponent } = testUtils;
+
+ QUnit.module('Components', {
+ beforeEach: function () {
+ this.items = [
+ {
+ isActive: false,
+ description: 'Some Item',
+ id: 1,
+ groupId: 1,
+ groupNumber: 1,
+ options: [
+ { description: "First Option", groupNumber: 1, id: 1 },
+ { description: "Second Option", groupNumber: 2, id: 2 },
+ ],
+ }, {
+ isActive: true,
+ description: 'Some other Item',
+ id: 2,
+ groupId: 2,
+ groupNumber: 2,
+ },
+ ];
+ },
+ }, function () {
+ QUnit.module('DropdownMenu');
+
+ QUnit.test('simple rendering and basic interactions', async function (assert) {
+ assert.expect(8);
+
+ const dropdown = await createComponent(DropdownMenu, {
+ props: {
+ items: this.items,
+ title: "Dropdown",
+ },
+ });
+
+ assert.strictEqual(dropdown.el.querySelector('button').innerText.trim(), "Dropdown");
+ assert.containsNone(dropdown, 'ul.o_dropdown_menu');
+
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3,
+ 'should have 3 elements counting the divider');
+ const itemEls = dropdown.el.querySelectorAll('.o_menu_item > .dropdown-item');
+ assert.strictEqual(itemEls[0].innerText.trim(), 'Some Item');
+ assert.doesNotHaveClass(itemEls[0], 'selected');
+ assert.hasClass(itemEls[1], 'selected');
+
+ const dropdownElements = dropdown.el.querySelectorAll('.o_menu_item *');
+ for (const dropdownEl of dropdownElements) {
+ await testUtils.dom.click(dropdownEl);
+ }
+ assert.containsOnce(dropdown, 'ul.o_dropdown_menu',
+ "Clicking on any item of the dropdown should not close it");
+
+ await testUtils.dom.click(document.body);
+
+ assert.containsNone(dropdown, 'ul.o_dropdown_menu',
+ "Clicking outside of the dropdown should close it");
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('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-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('.dropdown-menu'), 'show');
+ assert.doesNotHaveClass(bsDropdown.querySelector('.dropdown-menu'), 'show');
+
+ assert.isVisible(dropdown.el.querySelector('.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, '.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, '.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();
+ dropdown.destroy();
+ });
+
+ QUnit.test('click on an item without options should toggle it', async function (assert) {
+ assert.expect(7);
+
+ delete this.items[0].options;
+
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ intercepts: {
+ 'item-selected': function (ev) {
+ assert.strictEqual(ev.detail.item.id, 1);
+ this.state.items[0].isActive = !this.state.items[0].isActive;
+ },
+ }
+ });
+
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+
+ const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
+ assert.doesNotHaveClass(firstItemEl, 'selected');
+ await testUtils.dom.click(firstItemEl);
+ assert.hasClass(firstItemEl, 'selected');
+ assert.isVisible(firstItemEl);
+ await testUtils.dom.click(firstItemEl);
+ assert.doesNotHaveClass(firstItemEl, 'selected');
+ assert.isVisible(firstItemEl);
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('click on an item should not change url', async function (assert) {
+ assert.expect(1);
+
+ delete this.items[0].options;
+
+ const initialHref = window.location.href;
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ });
+
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+ await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
+ assert.strictEqual(window.location.href, initialHref,
+ "the url should not have changed after a click on an item");
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('options rendering', async function (assert) {
+ assert.expect(6);
+
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ });
+
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
+
+ const firstItemEl = dropdown.el.querySelector('.o_menu_item > a');
+ assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
+ // open options menu
+ await testUtils.dom.click(firstItemEl);
+ assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-down');
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
+
+ // close options menu
+ await testUtils.dom.click(firstItemEl);
+ assert.hasClass(firstItemEl.querySelector('i'), 'o_icon_right fa fa-caret-right');
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('close menu closes also submenus', async function (assert) {
+ assert.expect(2);
+
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ });
+
+ // open dropdown menu
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+ // open options menu of first item
+ await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item a'));
+
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 6);
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+ assert.containsN(dropdown, '.dropdown-divider, .dropdown-item', 3);
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('click on an option should trigger the event "item_option_clicked" with appropriate data', async function (assert) {
+ assert.expect(18);
+
+ let eventNumber = 0;
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ intercepts: {
+ 'item-selected': function (ev) {
+ eventNumber++;
+ const { option } = ev.detail;
+ assert.strictEqual(ev.detail.item.id, 1);
+ if (eventNumber === 1) {
+ assert.strictEqual(option.id, 1);
+ this.state.items[0].isActive = true;
+ this.state.items[0].options[0].isActive = true;
+ }
+ if (eventNumber === 2) {
+ assert.strictEqual(option.id, 2);
+ this.state.items[0].options[1].isActive = true;
+ }
+ if (eventNumber === 3) {
+ assert.strictEqual(option.id, 1);
+ this.state.items[0].options[0].isActive = false;
+ }
+ if (eventNumber === 4) {
+ assert.strictEqual(option.id, 2);
+ this.state.items[0].isActive = false;
+ this.state.items[0].options[1].isActive = false;
+ }
+ },
+ }
+ });
+
+ // open dropdown menu
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+ assert.containsN(dropdown, '.dropdown-divider, .o_menu_item', 3);
+
+ // open menu options of first item
+ await testUtils.dom.click(dropdown.el.querySelector('.o_menu_item > a'));
+ let optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
+
+ // click on first option
+ await testUtils.dom.click(optionELs[0]);
+ assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
+ optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
+ assert.hasClass(optionELs[0], 'selected');
+ assert.doesNotHaveClass(optionELs[1], 'selected');
+
+ // click on second option
+ await testUtils.dom.click(optionELs[1]);
+ assert.hasClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
+ optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
+ assert.hasClass(optionELs[0], 'selected');
+ assert.hasClass(optionELs[1], 'selected');
+
+ // click again on first option
+ await testUtils.dom.click(optionELs[0]);
+ // click again on second option
+ await testUtils.dom.click(optionELs[1]);
+ assert.doesNotHaveClass(dropdown.el.querySelector('.o_menu_item > a'), 'selected');
+ optionELs = dropdown.el.querySelectorAll('.o_menu_item .o_item_option > a');
+ assert.doesNotHaveClass(optionELs[0], 'selected');
+ assert.doesNotHaveClass(optionELs[1], 'selected');
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('keyboard navigation', async function (assert) {
+ assert.expect(12);
+
+ // Shorthand method to trigger a specific keydown.
+ // Note that BootStrap handles some of the navigation moves (up and down)
+ // so we need to give the event the proper "which" property. We also give
+ // it when it's not required to check if it has been correctly prevented.
+ async function navigate(key, global) {
+ const which = {
+ Enter: 13,
+ Escape: 27,
+ ArrowLeft: 37,
+ ArrowUp: 38,
+ ArrowRight: 39,
+ ArrowDown: 40,
+ }[key];
+ const target = global ? document.body : document.activeElement;
+ await testUtils.dom.triggerEvent(target, 'keydown', { key, which });
+ if (key === 'Enter') {
+ // Pressing "Enter" on a focused element triggers a click (HTML5 specs)
+ await testUtils.dom.click(target);
+ }
+ }
+
+ const dropdown = await createComponent(DropdownMenu, {
+ props: { items: this.items },
+ });
+
+ // Initialize active element (start at toggle button)
+ dropdown.el.querySelector('button').focus();
+ await testUtils.dom.click(dropdown.el.querySelector('button'));
+
+ await navigate('ArrowDown'); // Go to next item
+
+ assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
+ assert.containsNone(dropdown, '.o_item_option');
+
+ await navigate('ArrowRight'); // Unfold first item's options (w/ Right)
+
+ assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
+ assert.containsN(dropdown, '.o_item_option', 2);
+
+ await navigate('ArrowDown'); // Go to next option
+
+ assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_item_option a'));
+
+ await navigate('ArrowLeft'); // Fold first item's options (w/ Left)
+
+ assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
+ assert.containsNone(dropdown, '.o_item_option');
+
+ await navigate('Enter'); // Unfold first item's options (w/ Enter)
+
+ assert.strictEqual(document.activeElement, dropdown.el.querySelector('.o_menu_item a'));
+ assert.containsN(dropdown, '.o_item_option', 2);
+
+ await navigate('ArrowDown'); // Go to next option
+ await navigate('Escape'); // Fold first item's options (w/ Escape)
+ await testUtils.nextTick();
+
+ assert.strictEqual(dropdown.el.querySelector('.o_menu_item a'), document.activeElement);
+ assert.containsNone(dropdown, '.o_item_option');
+
+ await navigate('Escape', true); // Close the dropdown
+
+ assert.containsNone(dropdown, 'ul.o_dropdown_menu', "Dropdown should be folded");
+
+ dropdown.destroy();
+ });
+
+ QUnit.test('interactions between multiple dropdowns', async function (assert) {
+ assert.expect(7);
+
+ const props = { items: this.items };
+ class Parent extends owl.Component {
+ constructor() {
+ super(...arguments);
+ this.state = owl.useState(props);
+ }
+ }
+ Parent.components = { DropdownMenu };
+ Parent.template = owl.tags.xml`
+ <div>
+ <DropdownMenu class="first" title="'First'" items="state.items"/>
+ <DropdownMenu class="second" title="'Second'" items="state.items"/>
+ </div>`;
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget(), { position: 'first-child' });
+
+ const [menu1, menu2] = parent.el.querySelectorAll('.o_dropdown');
+
+ assert.containsNone(parent, '.o_dropdown_menu');
+
+ await testUtils.dom.click(menu1.querySelector('button'));
+
+ assert.containsOnce(parent, '.o_dropdown_menu');
+ assert.containsOnce(parent, '.o_dropdown.first .o_dropdown_menu');
+
+ await testUtils.dom.click(menu2.querySelector('button'));
+
+ assert.containsOnce(parent, '.o_dropdown_menu');
+ assert.containsOnce(parent, '.o_dropdown.second .o_dropdown_menu');
+
+ await testUtils.dom.click(menu2.querySelector('.o_menu_item a'));
+ await testUtils.dom.click(menu1.querySelector('button'));
+
+ assert.containsOnce(parent, '.o_dropdown_menu');
+ assert.containsOnce(parent, '.o_dropdown.first .o_dropdown_menu');
+
+ parent.destroy();
+ });
+
+ 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 owl.Component {
+ constructor() {
+ super(...arguments);
+ this.items = items;
+ }
+ }
+ Parent.components = { DropdownMenu };
+ Parent.template = owl.tags.xml`
+ <div>
+ <DropdownMenu class="first" title="'First'" items="items"/>
+ </div>`;
+ const parent = new Parent();
+ await parent.mount(testUtils.prepareTarget(), { position: "first-child" });
+
+ const menu = parent.el.querySelector(".o_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");
+
+ parent.destroy();
+ });
+ });
+});
diff --git a/addons/web/static/tests/components/pager_tests.js b/addons/web/static/tests/components/pager_tests.js
new file mode 100644
index 00000000..61b7feed
--- /dev/null
+++ b/addons/web/static/tests/components/pager_tests.js
@@ -0,0 +1,194 @@
+odoo.define('web.pager_tests', function (require) {
+ "use strict";
+
+ const Pager = require('web.Pager');
+ const testUtils = require('web.test_utils');
+
+ const cpHelpers = testUtils.controlPanel;
+ const { createComponent } = testUtils;
+
+ QUnit.module('Components', {}, function () {
+
+ QUnit.module('Pager');
+
+ QUnit.test('basic interactions', async function (assert) {
+ assert.expect(2);
+
+ const pager = await createComponent(Pager, {
+ props: {
+ currentMinimum: 1,
+ limit: 4,
+ size: 10,
+ },
+ intercepts: {
+ 'pager-changed': function (ev) {
+ Object.assign(this.state, ev.detail);
+ },
+ },
+ });
+
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4",
+ "currentMinimum should be set to 1");
+
+ await cpHelpers.pagerNext(pager);
+
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "5-8",
+ "currentMinimum should now be 5");
+
+ pager.destroy();
+ });
+
+ QUnit.test('edit the pager', async function (assert) {
+ assert.expect(4);
+
+ const pager = await createComponent(Pager, {
+ props: {
+ currentMinimum: 1,
+ limit: 4,
+ size: 10,
+ },
+ intercepts: {
+ 'pager-changed': function (ev) {
+ Object.assign(this.state, ev.detail);
+ },
+ },
+ });
+
+ await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
+
+ assert.containsOnce(pager, 'input',
+ "the pager should contain an input");
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4",
+ "the input should have correct value");
+
+ // change the limit
+ await cpHelpers.setPagerValue(pager, "1-6");
+
+ assert.containsNone(pager, 'input',
+ "the pager should not contain an input anymore");
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "1-6",
+ "the limit should have been updated");
+
+ pager.destroy();
+ });
+
+ QUnit.test("keydown on pager with same value", async function (assert) {
+ assert.expect(7);
+
+ const pager = await createComponent(Pager, {
+ props: {
+ currentMinimum: 1,
+ limit: 4,
+ size: 10,
+ },
+ intercepts: {
+ "pager-changed": () => assert.step("pager-changed"),
+ },
+ });
+
+ // Enter edit mode
+ await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
+
+ assert.containsOnce(pager, "input");
+ assert.strictEqual(cpHelpers.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(cpHelpers.getPagerValue(pager), "1-4");
+ assert.verifySteps(["pager-changed"]);
+
+ pager.destroy();
+ });
+
+ QUnit.test('pager value formatting', async function (assert) {
+ assert.expect(8);
+
+ const pager = await createComponent(Pager, {
+ props: {
+ currentMinimum: 1,
+ limit: 4,
+ size: 10,
+ },
+ intercepts: {
+ 'pager-changed': function (ev) {
+ Object.assign(this.state, ev.detail);
+ },
+ },
+ });
+
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "1-4", "Initial value should be correct");
+
+ async function inputAndAssert(input, expected, reason) {
+ await cpHelpers.setPagerValue(pager, input);
+ assert.strictEqual(cpHelpers.getPagerValue(pager), expected,
+ `Pager value should be "${expected}" when given "${input}": ${reason}`);
+ }
+
+ await inputAndAssert("4-4", "4", "values are squashed when minimum = maximum");
+ await inputAndAssert("1-11", "1-10", "maximum is floored to size when out of range");
+ await inputAndAssert("20-15", "10", "combination of the 2 assertions above");
+ await inputAndAssert("6-5", "10", "fallback to previous value when minimum > maximum");
+ await inputAndAssert("definitelyValidNumber", "10", "fallback to previous value if not a number");
+ await inputAndAssert(" 1 , 2 ", "1-2", "value is normalized and accepts several separators");
+ await inputAndAssert("3 8", "3-8", "value accepts whitespace(s) as a separator");
+
+ pager.destroy();
+ });
+
+ QUnit.test('pager disabling', async function (assert) {
+ assert.expect(9);
+
+ const reloadPromise = testUtils.makeTestPromise();
+ const pager = await createComponent(Pager, {
+ props: {
+ currentMinimum: 1,
+ limit: 4,
+ size: 10,
+ },
+ intercepts: {
+ // The goal here is to test the reactivity of the pager; in a
+ // typical views, we disable the pager after switching page
+ // to avoid switching twice with the same action (double click).
+ 'pager-changed': async function (ev) {
+ // 1. Simulate a (long) server action
+ await reloadPromise;
+ // 2. Update the view with loaded data
+ Object.assign(this.state, ev.detail);
+ },
+ },
+ });
+ const pagerButtons = pager.el.querySelectorAll('button');
+
+ // Click twice
+ await cpHelpers.pagerNext(pager);
+ await cpHelpers.pagerNext(pager);
+ // Try to edit the pager value
+ await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
+
+ assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
+ assert.ok(pagerButtons[0].disabled, "'previous' is disabled");
+ assert.ok(pagerButtons[1].disabled, "'next' is disabled");
+ assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'SPAN',
+ "pager edition is prevented");
+
+ // Server action is done
+ reloadPromise.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
+ assert.notOk(pagerButtons[0].disabled, "'previous' is enabled");
+ assert.notOk(pagerButtons[1].disabled, "'next' is enabled");
+ assert.strictEqual(cpHelpers.getPagerValue(pager), "5-8", "value has been updated");
+
+ await testUtils.dom.click(pager.el.querySelector('.o_pager_value'));
+
+ assert.strictEqual(pager.el.querySelector('.o_pager_value').tagName, 'INPUT',
+ "pager edition is re-enabled");
+
+ pager.destroy();
+ });
+ });
+});