diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/components | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/tests/components')
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(); + }); + }); +}); |
