From 3573a7fef497d7a2d3efd27da91adb5a7f9af27f Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Tue, 26 Apr 2022 13:23:43 +0000 Subject: [PATCH] [IMP] mail, *: use wowlEnv instead of the legacy one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *: bus, calendar, hr, hr_holidays, im_livechat, mail, mail_bot, note, project, sms, snailmail, test_mail, web, website_livechat, website_slides. task-2582313 closes odoo/odoo#83774 Related: odoo/enterprise#26493 Signed-off-by: Sébastien Theys (seb) --- addons/bus/__manifest__.py | 1 + .../js/services/legacy/legacy_bus_service.js | 13 + .../static/tests/helpers/mock_server.js | 7 +- .../tests/systray_activity_menu_tests.js | 4 +- .../static/tests/m2x_avatar_employee_tests.js | 183 ++++--- .../static/tests/helpers/mock_server.js | 12 +- .../static/tests/helpers/mock_server.js | 7 +- addons/mail/__manifest__.py | 6 +- .../attachment_viewer/attachment_viewer.xml | 4 +- .../dialog_manager/dialog_manager.js | 6 +- .../dialog_manager_container.js | 5 +- .../discuss_container/discuss_container.js | 10 +- .../discuss_container/discuss_container.xml | 2 +- .../discuss_public_view.xml | 2 +- .../discuss_public_view_container.js | 5 +- .../messaging_menu_container.js | 5 +- .../notification_request.js | 2 +- .../rtc_activity_notice_container.js | 5 +- addons/mail/static/src/js/tours/mail.js | 2 +- addons/mail/static/src/main.js | 23 +- addons/mail/static/src/model/model_manager.js | 5 - addons/mail/static/src/models/activity.js | 14 +- addons/mail/static/src/models/chatter.js | 10 +- .../mail/static/src/models/composer_view.js | 4 +- addons/mail/static/src/models/device.js | 11 +- addons/mail/static/src/models/discuss.js | 8 +- .../src/models/discuss_sidebar_category.js | 14 +- .../models/discuss_sidebar_category_item.js | 14 +- addons/mail/static/src/models/discuss_view.js | 11 +- addons/mail/static/src/models/locale.js | 6 +- .../mail/static/src/models/mail_template.js | 10 +- addons/mail/static/src/models/message.js | 10 +- addons/mail/static/src/models/messaging.js | 52 +- .../models/messaging_notification_handler.js | 16 +- .../static/src/models/notification_group.js | 20 +- .../src/models/rtc_call_participant_card.js | 2 +- addons/mail/static/src/models/thread.js | 12 +- .../static/src/models/tracking_value_item.js | 3 +- .../static/src/public/discuss_public_boot.js | 30 +- .../src/services/messaging/messaging.js | 34 -- .../static/src/services/messaging_service.js | 44 ++ .../{systray_service => }/systray_service.js | 10 +- .../src/utils/make_messaging_to_legacy_env.js | 11 + .../src/webclient/commands/mail_providers.js | 4 +- .../static/src/widgets/discuss/discuss.js | 74 --- .../static/src/widgets/discuss/discuss.scss | 10 - .../static/src/widgets/discuss/discuss.xml | 11 - .../mail/static/tests/helpers/mock_server.js | 162 ++---- .../mail/static/tests/helpers/test_utils.js | 397 +++----------- .../mail/static/tests/helpers/time_control.js | 53 +- .../static/tests/helpers/webclient_setup.js | 174 ++++++ .../tests/qunit_suite_tests/chatter_tests.js | 211 +++++--- .../activity_mark_done_popover_tests.js | 48 +- .../components/activity_tests.js | 65 +-- .../components/attachment_list_tests.js | 3 - .../components/chat_window_manager_tests.js | 36 +- .../chatter_suggested_recipient_tests.js | 1 - .../components/chatter_topbar_tests.js | 4 - .../components/composer_tests.js | 100 ++-- .../components/discuss_inbox_tests.js | 37 +- .../components/discuss_pinned_tests.js | 1 - .../components/discuss_sidebar_tests.js | 4 - .../components/discuss_tests.js | 139 ++--- .../components/follow_button_tests.js | 118 ++-- .../components/follower_list_menu_tests.js | 112 ++-- .../components/follower_subtype_tests.js | 112 ++-- .../components/follower_tests.js | 24 +- .../components/message_tests.js | 22 +- .../components/messaging_menu_tests.js | 34 +- ...ification_list_notification_group_tests.js | 11 +- .../thread_needaction_preview_tests.js | 14 +- .../components/thread_preview_tests.js | 1 - .../components/thread_view_tests.js | 67 +-- .../m2x_avatar_user_tests.js | 130 +++-- .../models/messaging_tests.js | 50 +- .../commands/mail_providers_tests.js | 2 - .../widgets/form_renderer_tests.js | 512 +++++++++--------- .../widgets/notification_alert_tests.js | 66 +-- .../tours/mail_full_composer_test_tour.js | 4 +- addons/mail/views/mail_channel_views.xml | 3 +- .../note/static/tests/helpers/mock_server.js | 9 +- .../tests/systray_activity_menu_tests.js | 2 +- addons/project/__manifest__.py | 2 +- addons/sms/static/src/models/message.js | 8 +- .../static/src/models/notification_group.js | 20 +- .../components/message_tests.js | 9 +- ...ification_list_notification_group_tests.js | 9 +- addons/snailmail/static/src/models/message.js | 18 +- .../static/src/models/notification_group.js | 18 +- .../static/tests/helpers/mock_server.js | 17 +- .../components/message_tests.js | 23 +- ...ification_list_notification_group_tests.js | 10 +- .../test_mail/static/tests/activity_tests.js | 235 ++++---- .../tests/systray_activity_menu_tests.js | 13 +- .../static/tests/tracking_value_tests.js | 69 ++- addons/web/__manifest__.py | 1 + addons/web/static/src/core/browser/browser.js | 2 + .../web/static/tests/helpers/mock_server.js | 120 +++- .../web/static/tests/helpers/mock_services.js | 3 +- .../tests/mock_relational_fields_tests.js | 135 +++++ addons/web/static/tests/webclient/helpers.js | 5 + .../static/tests/helpers/mock_server.js | 14 +- .../messaging_notification_handler_tests.js | 48 +- .../components/activity_tests.js | 6 +- 104 files changed, 2088 insertions(+), 2179 deletions(-) create mode 100644 addons/bus/static/src/js/services/legacy/legacy_bus_service.js delete mode 100644 addons/mail/static/src/services/messaging/messaging.js create mode 100644 addons/mail/static/src/services/messaging_service.js rename addons/mail/static/src/services/{systray_service => }/systray_service.js (77%) create mode 100644 addons/mail/static/src/utils/make_messaging_to_legacy_env.js delete mode 100644 addons/mail/static/src/widgets/discuss/discuss.js delete mode 100644 addons/mail/static/src/widgets/discuss/discuss.scss delete mode 100644 addons/mail/static/src/widgets/discuss/discuss.xml create mode 100644 addons/mail/static/tests/helpers/webclient_setup.js create mode 100644 addons/web/static/tests/mock_relational_fields_tests.js diff --git a/addons/bus/__manifest__.py b/addons/bus/__manifest__.py index 56ae574ebab..96fcf0dbcde 100644 --- a/addons/bus/__manifest__.py +++ b/addons/bus/__manifest__.py @@ -16,6 +16,7 @@ 'bus/static/src/js/longpolling_bus.js', 'bus/static/src/js/crosstab_bus.js', 'bus/static/src/js/services/bus_service.js', + 'bus/static/src/js/services/legacy/*.js', ], 'web.qunit_suite_tests': [ 'bus/static/tests/*.js', diff --git a/addons/bus/static/src/js/services/legacy/legacy_bus_service.js b/addons/bus/static/src/js/services/legacy/legacy_bus_service.js new file mode 100644 index 00000000000..f9d42c3fb2c --- /dev/null +++ b/addons/bus/static/src/js/services/legacy/legacy_bus_service.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { registry } from '@web/core/registry'; + +const { Component } = owl; + +export const legacyBusService = { + start() { + return Component.env.services.bus_service; + }, +}; + +registry.category('services').add('legacy_bus_service', legacyBusService); diff --git a/addons/calendar/static/tests/helpers/mock_server.js b/addons/calendar/static/tests/helpers/mock_server.js index 4d1899b322f..3e6be594327 100644 --- a/addons/calendar/static/tests/helpers/mock_server.js +++ b/addons/calendar/static/tests/helpers/mock_server.js @@ -2,10 +2,13 @@ // ensure mail override is applied first. import '@mail/../tests/helpers/mock_server'; -import MockServer from 'web.MockServer'; + +import { patch } from '@web/core/utils/patch'; +import { MockServer } from '@web/../tests/helpers/mock_server'; + import { datetime_to_str } from 'web.time'; -MockServer.include({ +patch(MockServer.prototype, 'calendar', { //-------------------------------------------------------------------------- // Private Mocked Methods //-------------------------------------------------------------------------- diff --git a/addons/calendar/static/tests/systray_activity_menu_tests.js b/addons/calendar/static/tests/systray_activity_menu_tests.js index 3a0bcdd50f8..c8afcb848b8 100644 --- a/addons/calendar/static/tests/systray_activity_menu_tests.js +++ b/addons/calendar/static/tests/systray_activity_menu_tests.js @@ -33,9 +33,7 @@ QUnit.test('activity menu widget:today meetings', async function (assert) { ]); legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); - const { wowlEnv: env } = await start({ - hasWebClient: true, - }); + const { env } = await start(); assert.containsOnce(document.body, '.o_mail_systray_item', 'should contain an instance of widget'); await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]')); diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index ab8863d7cdf..b3a6c2197ca 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -6,11 +6,10 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import FormView from 'web.FormView'; -import KanbanView from 'web.KanbanView'; -import ListView from 'web.ListView'; +import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; + import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee'; -import { dom, mock } from 'web.test_utils'; +import { dom } from 'web.test_utils'; QUnit.module('hr', {}, function () { QUnit.module('M2XAvatarEmployee', { @@ -40,17 +39,20 @@ QUnit.module('hr', {}, function () { { employee_id: hrEmployeePublicId2 }, { employee_id: hrEmployeePublicId1 }, ]); - await start({ - hasView: true, - View: ListView, - model: 'm2x.avatar.employee', - arch: '', + const views = { + 'm2x.avatar.employee,false,list': '', + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.employee', + views: [[false, 'list']], }); assert.strictEqual(document.querySelector('.o_data_cell span').innerText, 'Mario'); @@ -120,13 +122,9 @@ QUnit.module('hr', {}, function () { const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ user_id: resUsersId1, user_partner_id: resPartnerId1 }); pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1, employee_ids: [hrEmployeePublicId1] }); - await start({ - hasView: true, - View: KanbanView, - model: 'm2x.avatar.employee', - data: this.data, - arch: ` - + const views = { + 'm2x.avatar.employee,false,kanban': + `
@@ -135,6 +133,13 @@ QUnit.module('hr', {}, function () { `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.employee', + views: [[false, 'kanban']], }); assert.strictEqual(document.querySelector('.o_kanban_record').innerText.trim(), ''); @@ -148,41 +153,36 @@ QUnit.module('hr', {}, function () { const pyEnv = await startServer(); const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ name: 'Mario' }); const m2xHrAvatarUserId1 = pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1 }); - const { widget } = await start({ - hasView: true, - View: FormView, - model: 'm2x.avatar.employee', - data: this.data, - arch: '
', + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, - res_id: m2xHrAvatarUserId1, + serverData: { views }, services: { - notification: { - notify(notification) { - assert.ok( - true, - "should display a toast notification after failing to open chat" - ); - assert.strictEqual( - notification.message, - "You can only chat with employees that have a dedicated user.", - "should display the correct information in the notification" - ); - }, - }, + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with employees that have a dedicated user.", + "should display the correct information in the notification" + ); + }), }, }); - mock.intercept(widget, 'call_service', (ev) => { - if (ev.data.service === 'notification') { - assert.step(`display notification "${ev.data.args[0].message}"`); - } - }, true); + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xHrAvatarUserId1, + views: [[false, 'form']], + }); assert.strictEqual(document.querySelector('.o_field_widget[name=employee_id]').innerText.trim(), 'Mario'); await dom.click(document.querySelector('.o_m2o_avatar > img')); @@ -206,21 +206,23 @@ QUnit.module('hr', {}, function () { const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create( { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, ); - await start({ - hasView: true, - View: FormView, - model: 'm2x.avatar.employee', - data: this.data, - arch: '
', + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, - res_id: m2xAvatarEmployeeId1, + serverData: { views }, }); + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xAvatarEmployeeId1, + views: [[false, 'form']], + }); assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records"); assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), @@ -261,19 +263,22 @@ QUnit.module('hr', {}, function () { pyEnv['m2x.avatar.employee'].create( { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, ); - await start({ - hasView: true, - View: ListView, - model: 'm2x.avatar.employee', - arch: '', + const views = { + 'm2x.avatar.employee,false,list': '', + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, + serverData: { views }, }); + await openView({ + res_model: 'm2x.avatar.employee', + views: [[false, 'list']], + }); assert.containsN(document.body, '.o_data_cell:first .o_field_many2manytags > span', 2, "should have two avatar"); @@ -330,12 +335,9 @@ QUnit.module('hr', {}, function () { pyEnv['m2x.avatar.employee'].create( { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, ); - await start({ - hasView: true, - View: KanbanView, - model: 'm2x.avatar.employee', - arch: ` - + const views = { + 'm2x.avatar.employee,false,kanban': + `
@@ -350,14 +352,20 @@ QUnit.module('hr', {}, function () { `, + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, + serverData: { views }, }); + await openView({ + res_model: 'm2x.avatar.employee', + views: [[false, 'kanban']], + }); assert.containsN(document.body, '.o_kanban_record:first .o_field_many2manytags img.o_m2m_avatar', 2, "should have 2 avatar images"); assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2manytags img.o_m2m_avatar').getAttribute('data-src'), @@ -390,40 +398,35 @@ QUnit.module('hr', {}, function () { const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create( { employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] }, ); - const { widget } = await start({ - hasView: true, - View: FormView, - model: 'm2x.avatar.employee', - arch: '
', + const views = { + 'm2x.avatar.employee,false,form': '
', + }; + const { openView } = await start({ mockRPC(route, args) { if (args.method === 'read') { assert.step(`read ${args.model} ${args.args[0]}`); } - return this._super(...arguments); }, - res_id: m2xAvatarEmployeeId1, + serverData: { views }, services: { - notification: { - notify(notification) { - assert.ok( - true, - "should display a toast notification after failing to open chat" - ); - assert.strictEqual( - notification.message, - "You can only chat with employees that have a dedicated user.", - "should display the correct information in the notification" - ); - }, - }, + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with employees that have a dedicated user.", + "should display the correct information in the notification" + ); + }), }, }); - - mock.intercept(widget, 'call_service', (ev) => { - if (ev.data.service === 'notification') { - assert.step(`display notification "${ev.data.args[0].message}"`); - } - }, true); + await openView({ + res_model: 'm2x.avatar.employee', + res_id: m2xAvatarEmployeeId1, + views: [[false, 'form']], + }); assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records"); diff --git a/addons/hr_holidays/static/tests/helpers/mock_server.js b/addons/hr_holidays/static/tests/helpers/mock_server.js index b34afc79d01..ec536f54fe8 100644 --- a/addons/hr_holidays/static/tests/helpers/mock_server.js +++ b/addons/hr_holidays/static/tests/helpers/mock_server.js @@ -1,11 +1,11 @@ -odoo.define('hr_holidays/static/tests/helpers/mock_server.js', function (require) { -'use strict'; +/** @odoo-module **/ -require('@mail/../tests/helpers/mock_server'); // ensure mail overrides are applied first +import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first -const MockServer = require('web.MockServer'); +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; -MockServer.include({ +patch(MockServer.prototype, 'hr_holidays', { /** * Overrides to add out of office to employees. * @@ -25,5 +25,3 @@ MockServer.include({ return partnerFormats; }, }); - -}); diff --git a/addons/im_livechat/static/tests/helpers/mock_server.js b/addons/im_livechat/static/tests/helpers/mock_server.js index ef1bb11e74e..381a44263ef 100644 --- a/addons/im_livechat/static/tests/helpers/mock_server.js +++ b/addons/im_livechat/static/tests/helpers/mock_server.js @@ -2,9 +2,10 @@ import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first -import MockServer from 'web.MockServer'; +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; -MockServer.include({ +patch(MockServer.prototype, 'im_livechat', { //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -12,7 +13,7 @@ MockServer.include({ /** * @override */ - async _performRpc(route, args) { + async _performRPC(route, args) { if (route === '/im_livechat/get_session') { const channel_id = args.channel_id; const anonymous_name = args.anonymous_name; diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index ebe076efca2..ea2236f8764 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -83,11 +83,12 @@ 'mail/static/src/model/*.js', 'mail/static/src/models/*.js', 'mail/static/src/public/*', - 'mail/static/src/services/messaging/messaging.js', + 'mail/static/src/services/*.js', 'mail/static/src/utils/*.js', # Framework JS 'bus/static/src/js/*.js', 'bus/static/src/js/services/bus_service.js', + 'bus/static/src/js/services/legacy/legacy_bus_service.js', 'web/static/lib/luxon/luxon.js', 'web/static/src/core/**/*', # FIXME: debug menu currently depends on webclient, once it doesn't we don't need to remove the contents of the debug folder @@ -122,7 +123,7 @@ 'mail/static/src/components/*/*.scss', 'mail/static/src/model/*.js', 'mail/static/src/models/*.js', - 'mail/static/src/services/*/*.js', + 'mail/static/src/services/*.js', 'mail/static/src/webclient/commands/*.js', 'mail/static/src/widgets/*/*.js', 'mail/static/src/widgets/*/*.scss', @@ -150,7 +151,6 @@ 'web.assets_qweb': [ 'mail/static/src/xml/*.xml', 'mail/static/src/components/*/*.xml', - 'mail/static/src/widgets/*/*.xml', ], }, 'license': 'LGPL-3', diff --git a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml index b6f017fa428..4d92aa4b995 100644 --- a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml +++ b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml @@ -26,8 +26,8 @@
- - + + Download
diff --git a/addons/mail/static/src/components/dialog_manager/dialog_manager.js b/addons/mail/static/src/components/dialog_manager/dialog_manager.js index f821053eae6..0f8d4d36ca2 100644 --- a/addons/mail/static/src/components/dialog_manager/dialog_manager.js +++ b/addons/mail/static/src/components/dialog_manager/dialog_manager.js @@ -39,16 +39,12 @@ export class DialogManager extends Component { * @private */ _checkDialogOpen() { - if (!this.messaging || !this.messaging.dialogManager) { - return; - } - if (this.messaging.dialogManager.dialogs.length > 0) { + if (this.dialogManager.dialogs.length > 0) { document.body.classList.add('modal-open'); } else { document.body.classList.remove('modal-open'); } } - } Object.assign(DialogManager, { diff --git a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js index 64f1a40bafa..95c30e326f8 100644 --- a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js +++ b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js @@ -5,7 +5,7 @@ import { useModels } from '@mail/component_hooks/use_models'; import '@mail/components/dialog_manager/dialog_manager'; import { getMessagingComponent } from "@mail/utils/messaging_component"; -const { Component, useSubEnv } = owl; +const { Component } = owl; export class DialogManagerContainer extends Component { @@ -13,9 +13,6 @@ export class DialogManagerContainer extends Component { * @override */ setup() { - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - useSubEnv(Component.env); useModels(); super.setup(); } diff --git a/addons/mail/static/src/components/discuss_container/discuss_container.js b/addons/mail/static/src/components/discuss_container/discuss_container.js index 936de438be2..f2dd2503384 100644 --- a/addons/mail/static/src/components/discuss_container/discuss_container.js +++ b/addons/mail/static/src/components/discuss_container/discuss_container.js @@ -14,9 +14,6 @@ export class DiscussContainer extends Component { * @override */ setup() { - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - this.env = Component.env; useModels(); super.setup(); onWillDestroy(() => this._willDestroy()); @@ -30,7 +27,6 @@ export class DiscussContainer extends Component { this.discuss.update({ discussView: insertAndReplace({ actionId: action.id, - actionManager: this.props.actionManager, }), initActiveId, }); @@ -48,7 +44,7 @@ export class DiscussContainer extends Component { * When executing the discuss action while it's already opened, the * action manager first mounts the newest DiscussContainer, then * unmounts the oldest one. The issue is that messaging.discussView is - * updated on setup but is cleared during the unmount. This leads to + * updated on setup but is cleared on destroy. This leads to * errors because there is no discussView anymore. In order to handle * this situation, let's keep a reference to the currentInstance so that * we can check we're deleting the discussView only when there is no @@ -68,7 +64,9 @@ export class DiscussContainer extends Component { Object.assign(DiscussContainer, { props: { action: Object, - actionManager: Object, + actionId: { type: Number, optional: 1 }, + className: String, + globalState: { type: Object, optional: 1 }, }, components: { Discuss: getMessagingComponent('Discuss') }, template: 'mail.DiscussContainer', diff --git a/addons/mail/static/src/components/discuss_container/discuss_container.xml b/addons/mail/static/src/components/discuss_container/discuss_container.xml index c5c14450413..967d263ee3c 100644 --- a/addons/mail/static/src/components/discuss_container/discuss_container.xml +++ b/addons/mail/static/src/components/discuss_container/discuss_container.xml @@ -2,7 +2,7 @@ -
+
Please wait... diff --git a/addons/mail/static/src/components/discuss_public_view/discuss_public_view.xml b/addons/mail/static/src/components/discuss_public_view/discuss_public_view.xml index 3d1fb1aeb23..f42bf52dafe 100644 --- a/addons/mail/static/src/components/discuss_public_view/discuss_public_view.xml +++ b/addons/mail/static/src/components/discuss_public_view/discuss_public_view.xml @@ -2,7 +2,7 @@ -
+
diff --git a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js index 9373574c03f..3c232df6b3f 100644 --- a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js +++ b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js @@ -5,7 +5,7 @@ import { useModels } from '@mail/component_hooks/use_models'; import '@mail/components/discuss_public_view/discuss_public_view'; import { getMessagingComponent } from "@mail/utils/messaging_component"; -const { Component, useSubEnv } = owl; +const { Component } = owl; export class DiscussPublicViewContainer extends Component { @@ -13,9 +13,6 @@ export class DiscussPublicViewContainer extends Component { * @override */ setup() { - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - useSubEnv(Component.env); useModels(); super.setup(); } diff --git a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js index 98f80fe47e8..efa9ab2064d 100644 --- a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js +++ b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js @@ -5,7 +5,7 @@ import { useModels } from '@mail/component_hooks/use_models'; import '@mail/components/messaging_menu/messaging_menu'; import { getMessagingComponent } from "@mail/utils/messaging_component"; -const { Component, useSubEnv } = owl; +const { Component } = owl; export class MessagingMenuContainer extends Component { @@ -13,9 +13,6 @@ export class MessagingMenuContainer extends Component { * @override */ setup() { - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - useSubEnv(Component.env); useModels(); super.setup(); } diff --git a/addons/mail/static/src/components/notification_request/notification_request.js b/addons/mail/static/src/components/notification_request/notification_request.js index f4ba5154762..1730860a0e1 100644 --- a/addons/mail/static/src/components/notification_request/notification_request.js +++ b/addons/mail/static/src/components/notification_request/notification_request.js @@ -31,7 +31,7 @@ export class NotificationRequest extends Component { _handleResponseNotificationPermission(value) { this.messaging.refreshIsNotificationPermissionDefault(); if (value !== 'granted') { - this.env.services['bus_service'].sendNotification({ + this.env.services['legacy_bus_service'].sendNotification({ message: this.env._t("Odoo will not have the permission to send native notifications on this device."), title: this.env._t("Permission denied"), }); diff --git a/addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.js b/addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.js index c0ed9eda5da..facc5e6de85 100644 --- a/addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.js +++ b/addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.js @@ -5,7 +5,7 @@ import { useModels } from '@mail/component_hooks/use_models'; import '@mail/components/rtc_activity_notice/rtc_activity_notice'; import { getMessagingComponent } from "@mail/utils/messaging_component"; -const { Component, useSubEnv } = owl; +const { Component } = owl; export class RtcActivityNoticeContainer extends Component { @@ -13,9 +13,6 @@ export class RtcActivityNoticeContainer extends Component { * @override */ setup() { - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - useSubEnv(Component.env); useModels(); super.setup(); } diff --git a/addons/mail/static/src/js/tours/mail.js b/addons/mail/static/src/js/tours/mail.js index ddda18795f7..a70637fef47 100644 --- a/addons/mail/static/src/js/tours/mail.js +++ b/addons/mail/static/src/js/tours/mail.js @@ -5,7 +5,7 @@ import { Markup } from 'web.utils'; import tour from 'web_tour.tour'; tour.register('mail_tour', { - url: "/web#action=mail.widgets.discuss", + url: "/web#action=mail.action_discuss", sequence: 80, }, [{ trigger: '.o_DiscussSidebar_categoryChannel .o_DiscussSidebarCategory_commandAdd', diff --git a/addons/mail/static/src/main.js b/addons/mail/static/src/main.js index 04e5c66b4a2..d6dba4a81d6 100644 --- a/addons/mail/static/src/main.js +++ b/addons/mail/static/src/main.js @@ -2,17 +2,26 @@ import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; -import { MessagingService } from '@mail/services/messaging/messaging'; -import { SystrayService } from '@mail/services/systray_service/systray_service'; -import { DiscussWidget } from '@mail/widgets/discuss/discuss'; +import { DiscussContainer } from '@mail/components/discuss_container/discuss_container'; +import { messagingService } from '@mail/services/messaging_service'; +import { systrayService } from '@mail/services/systray_service'; +import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; -import { serviceRegistry, action_registry } from 'web.core'; import { registry } from '@web/core/registry'; -serviceRegistry.add('messaging', MessagingService); -serviceRegistry.add('systray_service', SystrayService); +const messagingValuesService = { + start() { + return {}; + } +}; -action_registry.add('mail.widgets.discuss', DiscussWidget); +const serviceRegistry = registry.category('services'); +serviceRegistry.add('messaging', messagingService); +serviceRegistry.add('messagingValues', messagingValuesService); +serviceRegistry.add('systray_service', systrayService); +serviceRegistry.add('messaging_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env)); + +registry.category('actions').add('mail.action_discuss', DiscussContainer); registry.category('main_components').add('DialogManagerContainer', { Component: DialogManagerContainer }); registry.category('main_components').add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer }); diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 0145856a232..19d90ea1a64 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -134,11 +134,6 @@ export class ModelManager { * "Promise time". */ await new Promise(resolve => setTimeout(resolve)); - /** - * Some models require session data, like locale text direction (depends on - * fully loaded translation). - */ - await this.env.session.is_bound; this._generateModels(); /** * Create the messaging singleton record. diff --git a/addons/mail/static/src/models/activity.js b/addons/mail/static/src/models/activity.js index 2a3fbc77a73..a28723cea9b 100644 --- a/addons/mail/static/src/models/activity.js +++ b/addons/mail/static/src/models/activity.js @@ -134,10 +134,10 @@ registerModel({ }, res_id: this.id, }; - this.env.bus.trigger('do-action', { + this.env.services.action.doAction( action, - options: { on_close: () => this.fetchAndUpdate() }, - }); + { onClose: () => this.fetchAndUpdate() }, + ); }, async fetchAndUpdate() { const [data] = await this.messaging.rpc({ @@ -210,17 +210,17 @@ registerModel({ if (!action) { return; } - this.env.bus.trigger('do-action', { + this.env.services.action.doAction( action, - options: { - on_close: () => { + { + onClose: () => { if (!thread.exists()) { return; } thread.fetchData(['activities']); }, }, - }); + ); }, /** * @private diff --git a/addons/mail/static/src/models/chatter.js b/addons/mail/static/src/models/chatter.js index 2a7c56d0f1f..84529f8ad2a 100644 --- a/addons/mail/static/src/models/chatter.js +++ b/addons/mail/static/src/models/chatter.js @@ -82,12 +82,12 @@ registerModel({ }, res_id: false, }; - return this.env.bus.trigger('do-action', { + return this.env.services.action.doAction( action, - options: { - on_close: () => this.reloadParentView(), - }, - }); + { + onClose: () => this.reloadParentView(), + } + ); }, /** * Handles click on "send message" button. diff --git a/addons/mail/static/src/models/composer_view.js b/addons/mail/static/src/models/composer_view.js index 3657b221150..6d7897aa422 100644 --- a/addons/mail/static/src/models/composer_view.js +++ b/addons/mail/static/src/models/composer_view.js @@ -533,7 +533,7 @@ registerModel({ }; const composer = this.composer; const options = { - on_close: () => { + onClose: () => { if (!composer.exists()) { return; } @@ -543,7 +543,7 @@ registerModel({ } }, }; - await this.env.bus.trigger('do-action', { action, options }); + await this.env.services.action.doAction(action, options); }, /** * Post a message in provided composer's thread based on current composer fields values. diff --git a/addons/mail/static/src/models/device.js b/addons/mail/static/src/models/device.js index 921a424f2d7..d6b20684098 100644 --- a/addons/mail/static/src/models/device.js +++ b/addons/mail/static/src/models/device.js @@ -3,6 +3,8 @@ import { registerModel } from '@mail/model/model_core'; import { attr } from '@mail/model/model_field'; import { browser } from "@web/core/browser/browser"; +import { isMobileOS } from "@web/core/browser/feature_detection"; +import { SIZES } from '@web/core/ui/ui_service'; registerModel({ name: 'Device', @@ -30,9 +32,9 @@ registerModel({ this.update({ globalWindowInnerHeight: this.messaging.browser.innerHeight, globalWindowInnerWidth: this.messaging.browser.innerWidth, - isMobileDevice: this.env.device.isMobileDevice, - isSmall: this.env.device.isMobile, - sizeClass: this.env.device.size_class, + isMobileDevice: isMobileOS(), + isSmall: this.env.isSmall, + sizeClass: this.env.services.ui.size, }); }, }, @@ -56,5 +58,8 @@ registerModel({ * attribute. */ sizeClass: attr(), + sizeClasses: attr({ + default: SIZES, + }), }, }); diff --git a/addons/mail/static/src/models/discuss.js b/addons/mail/static/src/models/discuss.js index 4bec19bc910..bdb5620861a 100644 --- a/addons/mail/static/src/models/discuss.js +++ b/addons/mail/static/src/models/discuss.js @@ -162,14 +162,14 @@ registerModel({ this.focus(); } if (!this.discussView) { - this.env.bus.trigger('do-action', { - action: 'mail.action_discuss', - options: { + this.env.services.action.doAction( + 'mail.action_discuss', + { active_id: this.threadToActiveId(this), clear_breadcrumbs: false, on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway }, - }); + ); } }, /** diff --git a/addons/mail/static/src/models/discuss_sidebar_category.js b/addons/mail/static/src/models/discuss_sidebar_category.js index 65d6df20a6c..55b1c624795 100644 --- a/addons/mail/static/src/models/discuss_sidebar_category.js +++ b/addons/mail/static/src/models/discuss_sidebar_category.js @@ -176,14 +176,12 @@ registerModel({ */ onClickCommandView(ev) { ev.stopPropagation(); - return this.env.bus.trigger('do-action', { - action: { - name: this.env._t("Public Channels"), - type: 'ir.actions.act_window', - res_model: 'mail.channel', - views: [[false, 'kanban'], [false, 'form']], - domain: [['public', '!=', 'private']], - }, + return this.env.services.action.doAction({ + name: this.env._t("Public Channels"), + type: 'ir.actions.act_window', + res_model: 'mail.channel', + views: [[false, 'kanban'], [false, 'form']], + domain: [['public', '!=', 'private']], }); }, /** diff --git a/addons/mail/static/src/models/discuss_sidebar_category_item.js b/addons/mail/static/src/models/discuss_sidebar_category_item.js index 4ca911b23d5..6bbd0c3c0e2 100644 --- a/addons/mail/static/src/models/discuss_sidebar_category_item.js +++ b/addons/mail/static/src/models/discuss_sidebar_category_item.js @@ -134,14 +134,12 @@ registerModel({ */ onClickCommandSettings(ev) { ev.stopPropagation(); - return this.env.bus.trigger('do-action', { - action: { - type: 'ir.actions.act_window', - res_model: this.channel.model, - res_id: this.channel.id, - views: [[false, 'form']], - target: 'current', - }, + return this.env.services.action.doAction({ + type: 'ir.actions.act_window', + res_model: this.channel.model, + res_id: this.channel.id, + views: [[false, 'form']], + target: 'current', }); }, /** diff --git a/addons/mail/static/src/models/discuss_view.js b/addons/mail/static/src/models/discuss_view.js index 7d91175a0f2..5ff4b97a61e 100644 --- a/addons/mail/static/src/models/discuss_view.js +++ b/addons/mail/static/src/models/discuss_view.js @@ -45,9 +45,9 @@ registerModel({ * Handles OWL update on the discuss component. */ onComponentUpdate() { - if (this.discuss.thread && this.actionManager) { + if (this.discuss.thread) { if (this.lastPushStateActiveThread !== this.discuss.thread) { - this.actionManager.do_push_state({ + this.env.services.router.pushState({ action: this.actionId, active_id: this.discuss.activeId, }); @@ -63,7 +63,7 @@ registerModel({ this.lastThreadCache && this.lastThreadCache.localId === this.discuss.threadView.threadCache.localId && this.lastThreadCounter > 0 && this.discuss.thread.counter === 0 ) { - this.env.bus.trigger('show-effect', { + this.env.services.effect.add({ message: this.env._t("Congratulations, your inbox is empty!"), type: 'rainbow_man', }); @@ -156,11 +156,6 @@ registerModel({ * The id of the action which opened discuss. */ actionId: attr(), - /** - * Used to push state when changing active thread. - * The actionManager passed to the discuss widget. - */ - actionManager: attr(), discuss: one('Discuss', { inverse: 'discussView', readonly: true, diff --git a/addons/mail/static/src/models/locale.js b/addons/mail/static/src/models/locale.js index 7e8f39466af..98cc402a36f 100644 --- a/addons/mail/static/src/models/locale.js +++ b/addons/mail/static/src/models/locale.js @@ -3,6 +3,8 @@ import { registerModel } from '@mail/model/model_core'; import { attr } from '@mail/model/model_field'; +import { localization } from '@web/core/l10n/localization'; + registerModel({ name: 'Locale', identifyingFields: ['messaging'], @@ -12,14 +14,14 @@ registerModel({ * @returns {string} */ _computeLanguage() { - return this.env._t.database.parameters.code; + return this.env.services.user.lang; }, /** * @private * @returns {string} */ _computeTextDirection() { - return this.env._t.database.parameters.direction; + return localization.direction; }, }, fields: { diff --git a/addons/mail/static/src/models/mail_template.js b/addons/mail/static/src/models/mail_template.js index a44d6936126..0c28e1d0d4e 100644 --- a/addons/mail/static/src/models/mail_template.js +++ b/addons/mail/static/src/models/mail_template.js @@ -25,14 +25,10 @@ registerModel({ force_email: true, }, }; - this.env.bus.trigger('do-action', { + this.env.services.action.doAction( action, - options: { - on_close: () => { - activity.thread.fetchData(['attachments', 'messages']); - }, - }, - }); + { onClose: () => activity.thread.fetchData(['attachments', 'messages']) } + ); }, /** * @param {Activity} activity diff --git a/addons/mail/static/src/models/message.js b/addons/mail/static/src/models/message.js index 9fc77200fdb..fab793a800f 100644 --- a/addons/mail/static/src/models/message.js +++ b/addons/mail/static/src/models/message.js @@ -231,14 +231,14 @@ registerModel({ * Opens the view that allows to resend the message in case of failure. */ openResendAction() { - this.env.bus.trigger('do-action', { - action: 'mail.mail_resend_message_action', - options: { + this.env.services.action.doAction( + 'mail.mail_resend_message_action', + { additional_context: { mail_message_to_resend: this.id, }, - }, - }); + } + ); }, /** * Removes the given reaction from this message. diff --git a/addons/mail/static/src/models/messaging.js b/addons/mail/static/src/models/messaging.js index 250409d6b65..7d906ec08b2 100644 --- a/addons/mail/static/src/models/messaging.js +++ b/addons/mail/static/src/models/messaging.js @@ -15,8 +15,8 @@ registerModel({ identifyingFields: [], lifecycleHooks: { _willDelete() { - if (this.env.services['bus_service']) { - this.env.services['bus_service'].off('window_focus', null, this._handleGlobalWindowFocus); + if (this.env.services['legacy_bus_service']) { + this.env.services['legacy_bus_service'].off('window_focus', null, this._handleGlobalWindowFocus); } }, }, @@ -25,7 +25,7 @@ registerModel({ * Starts messaging and related records. */ async start() { - this.env.services['bus_service'].on('window_focus', null, this._handleGlobalWindowFocus); + this.env.services['legacy_bus_service'].on('window_focus', null, this._handleGlobalWindowFocus); await this.initializer.start(); if (!this.exists()) { return; @@ -70,7 +70,8 @@ registerModel({ * @return {number} the id of the notification. */ notify(params) { - return this.env.services.notification.notify(params); + const { message, ...options } = params; + return this.env.services.notification.add(message, options); }, /** * Opens a chat with the provided person and returns it. @@ -100,13 +101,11 @@ registerModel({ * @param {string} param0.model */ async openDocument({ id, model }) { - this.env.bus.trigger('do-action', { - action: { - type: 'ir.actions.act_window', - res_model: model, - views: [[false, 'form']], - res_id: id, - }, + this.env.services.action.doAction({ + type: 'ir.actions.act_window', + res_model: model, + views: [[false, 'form']], + res_id: id, }); if (this.messaging.device.isSmall) { // messaging menu has a higher z-index than views so it must @@ -157,8 +156,35 @@ registerModel({ * @param {Object} params * @return {any} */ - async rpc(params, options) { - return this.env.services.rpc(params, options); + async rpc(params, options = {}) { + if (params.route) { + const { route, params: rpcParameters } = params; + const { shadow: silent, ...rpcSettings } = options; + return this.env.services.rpc(route, rpcParameters, { silent, ...rpcSettings }); + } else { + const { args, method, model, kwargs = {} } = params; + const { context, domain, fields, groupBy, ...ormOptions } = kwargs; + + const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm; + switch (method) { + case 'create': + return ormService.create(model, args[0], context); + case 'read': + return ormService.read(model, args[0], args.length > 1 ? args[1] : undefined, context); + case 'read_group': + return ormService.readGroup(model, domain, fields, groupBy, ormOptions, context); + case 'search': + return ormService.search(model, args[0], ormOptions, context); + case 'search_read': + return ormService.searchRead(model, domain, fields, ormOptions, context); + case 'unlink': + return ormService.unlink(model, args[0], context); + case 'write': + return ormService.write(model, args[0], args[1], context); + default: + return ormService.call(model, method, args, kwargs); + } + } }, /** * Refreshes the value of `isNotificationPermissionDefault`. diff --git a/addons/mail/static/src/models/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler.js index ad8cceaf9f3..c850e8919e7 100644 --- a/addons/mail/static/src/models/messaging_notification_handler.js +++ b/addons/mail/static/src/models/messaging_notification_handler.js @@ -16,9 +16,9 @@ registerModel({ identifyingFields: ['messaging'], lifecycleHooks: { _willDelete() { - if (this.env.services['bus_service']) { - this.env.services['bus_service'].off('notification'); - this.env.services['bus_service'].stopPolling(); + if (this.env.services['legacy_bus_service']) { + this.env.services['legacy_bus_service'].off('notification'); + this.env.services['legacy_bus_service'].stopPolling(); } }, }, @@ -28,8 +28,8 @@ registerModel({ * the current users. This includes pinned channels for instance. */ start() { - this.env.services.bus_service.onNotification(null, notifs => this._handleNotifications(notifs)); - this.env.services.bus_service.startPolling(); + this.env.services.legacy_bus_service.onNotification(null, notifs => this._handleNotifications(notifs)); + this.env.services.legacy_bus_service.startPolling(); }, /** * @private @@ -269,7 +269,7 @@ registerModel({ channel.correspondent === this.messaging.partnerRoot ); if (!isChatWithOdooBot) { - const isOdooFocused = this.env.services['bus_service'].isOdooFocused(); + const isOdooFocused = this.env.services['legacy_bus_service'].isOdooFocused(); // Notify if out of focus if (!isOdooFocused && channel.isChatChannel) { this._notifyNewChannelMessageWhileOutOfFocus({ @@ -657,7 +657,7 @@ registerModel({ // then open a chat for the current user with the new user. const message = sprintf(this.env._t('%s connected'), username); const title = this.env._t("This is their first connection. Wish them luck."); - this.env.services['bus_service'].sendNotification({ message, title, type: 'info' }); + this.env.services['legacy_bus_service'].sendNotification({ message, title, type: 'info' }); const chat = await this.messaging.getChat({ partnerId }); if (!this.exists() || !chat || this.messaging.device.isSmall) { return; @@ -699,7 +699,7 @@ registerModel({ const notificationContent = escape( htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE) ); - this.env.services['bus_service'].sendNotification({ + this.env.services['legacy_bus_service'].sendNotification({ message: notificationContent, title: notificationTitle, type: 'info', diff --git a/addons/mail/static/src/models/notification_group.js b/addons/mail/static/src/models/notification_group.js index 2f710566f14..fd8f2600927 100644 --- a/addons/mail/static/src/models/notification_group.js +++ b/addons/mail/static/src/models/notification_group.js @@ -88,17 +88,15 @@ registerModel({ if (this.notification_type !== 'email') { return; } - this.env.bus.trigger('do-action', { - action: { - name: this.env._t("Mail Failures"), - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', - res_model: this.res_model, - domain: [['message_has_error', '=', true]], - context: { create: false }, - }, + this.env.services.action.doAction({ + name: this.env._t("Mail Failures"), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: this.res_model, + domain: [['message_has_error', '=', true]], + context: { create: false }, }); if (this.messaging.device.isSmall) { // messaging menu has a higher z-index than views so it must diff --git a/addons/mail/static/src/models/rtc_call_participant_card.js b/addons/mail/static/src/models/rtc_call_participant_card.js index 9989c96d8f6..d7069e9e386 100644 --- a/addons/mail/static/src/models/rtc_call_participant_card.js +++ b/addons/mail/static/src/models/rtc_call_participant_card.js @@ -88,7 +88,7 @@ registerModel({ * @returns {boolean} */ _computeHasConnectionInfo() { - return Boolean(this.rtcSession && this.env.isDebug() && this.channel.rtc); + return Boolean(this.rtcSession && this.env.debug && this.channel.rtc); }, /** * @private diff --git a/addons/mail/static/src/models/thread.js b/addons/mail/static/src/models/thread.js index 8b0c6c483af..5d8fcbdb7f2 100644 --- a/addons/mail/static/src/models/thread.js +++ b/addons/mail/static/src/models/thread.js @@ -1716,18 +1716,18 @@ registerModel({ default_res_id: this.id, }, }; - this.env.bus.trigger('do-action', { + this.env.services.action.doAction( action, - options: { - on_close: async () => { + { + onClose: async () => { if (!this.exists()) { return; - } + } await this.fetchData(['followers']); this.env.bus.trigger('Thread:promptAddFollower-closed'); }, - }, - }); + } + ); }, /** * @private diff --git a/addons/mail/static/src/models/tracking_value_item.js b/addons/mail/static/src/models/tracking_value_item.js index a8e164d5173..28fe783ee35 100644 --- a/addons/mail/static/src/models/tracking_value_item.js +++ b/addons/mail/static/src/models/tracking_value_item.js @@ -4,6 +4,7 @@ import { registerModel } from '@mail/model/model_core'; import { attr, one } from '@mail/model/model_field'; import { format } from 'web.field_utils'; +import { session } from '@web/session'; registerModel({ name: 'TrackingValueItem', @@ -53,7 +54,7 @@ registerModel({ case 'monetary': return format.monetary(this.value, undefined, { currency: this.currencyId - ? this.env.session.currencies[this.currencyId] + ? session.currencies[this.currencyId] : undefined, forceString: true, }); diff --git a/addons/mail/static/src/public/discuss_public_boot.js b/addons/mail/static/src/public/discuss_public_boot.js index 88c3fa69614..2bbf404ce74 100644 --- a/addons/mail/static/src/public/discuss_public_boot.js +++ b/addons/mail/static/src/public/discuss_public_boot.js @@ -1,20 +1,16 @@ /** @odoo-module **/ import { data } from 'mail.discuss_public_channel_template'; +import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; import { DiscussPublicViewContainer } from '@mail/components/discuss_public_view_container/discuss_public_view_container'; -import { MessagingService } from '@mail/services/messaging/messaging'; +import { messagingService } from '@mail/services/messaging_service'; import { processTemplates } from '@web/core/assets'; import { MainComponentsContainer } from '@web/core/main_components_container'; import { registry } from '@web/core/registry'; import { makeEnv, startServices } from '@web/env'; -import { session } from '@web/session'; - -import * as AbstractService from 'web.AbstractService'; -import { serviceRegistry as legacyServiceRegistry } from 'web.core'; -import * as legacyEnv from 'web.env'; import { makeLegacyCrashManagerService, makeLegacyDialogMappingService, @@ -23,6 +19,10 @@ import { makeLegacySessionService, mapLegacyEnvToWowlEnv, } from '@web/legacy/utils'; +import { session } from '@web/session'; + +import * as AbstractService from 'web.AbstractService'; +import * as legacyEnv from 'web.env'; import * as legacySession from 'web.session'; const { Component, mount, whenReady } = owl; @@ -31,6 +31,15 @@ Component.env = legacyEnv; (async function boot() { await whenReady(); + + const messagingValuesService = { + start() { + return { + autofetchPartnerImStatus: false, + }; + } + }; + AbstractService.prototype.deployServices(Component.env); const serviceRegistry = registry.category('services'); serviceRegistry.add('legacy_rpc', makeLegacyRpcService(Component.env)); @@ -38,6 +47,10 @@ Component.env = legacyEnv; serviceRegistry.add('legacy_notification', makeLegacyNotificationService(Component.env)); serviceRegistry.add('legacy_crash_manager', makeLegacyCrashManagerService(Component.env)); serviceRegistry.add('legacy_dialog_mapping', makeLegacyDialogMappingService(Component.env)); + serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(Component.env)); + + serviceRegistry.add('messaging', messagingService); + serviceRegistry.add('messagingValues', messagingValuesService); const mainComponentsRegistry = registry.category('main_components'); mainComponentsRegistry.add('DiscussPublicViewContainer', { Component: DiscussPublicViewContainer }); @@ -61,11 +74,6 @@ Component.env = legacyEnv; ]); mapLegacyEnvToWowlEnv(Component.env, env); odoo.isReady = true; - legacyServiceRegistry.add('messaging', MessagingService.extend({ - messagingValues: { - autofetchPartnerImStatus: false, - }, - })); await mount(MainComponentsContainer, document.body, { env, templates, dev: env.debug }); createDiscussPublicView(); })(); diff --git a/addons/mail/static/src/services/messaging/messaging.js b/addons/mail/static/src/services/messaging/messaging.js deleted file mode 100644 index 31d6f8fb0f6..00000000000 --- a/addons/mail/static/src/services/messaging/messaging.js +++ /dev/null @@ -1,34 +0,0 @@ -/** @odoo-module **/ - -import { ModelManager } from '@mail/model/model_manager'; - -import AbstractService from 'web.AbstractService'; - -export const MessagingService = AbstractService.extend({ - dependencies: ['bus_service'], - messagingValues: {}, - /** - * @override - */ - init(env) { - this._super(env); - this.modelManager = new ModelManager(env); - }, - /** - * @override - */ - start() { - this._super(); - this.modelManager.start(this.messagingValues); - }, - /** - * Returns the messaging record once it is initialized. This method should - * be considered the main entry point to the messaging system for outside - * code. - * - * @returns {Messaging} - **/ - async get() { - return this.modelManager.getMessaging(); - } -}); diff --git a/addons/mail/static/src/services/messaging_service.js b/addons/mail/static/src/services/messaging_service.js new file mode 100644 index 00000000000..499d7a3906b --- /dev/null +++ b/addons/mail/static/src/services/messaging_service.js @@ -0,0 +1,44 @@ +/** @odoo-module **/ + +import { ModelManager } from '@mail/model/model_manager'; + +export const messagingService = { + dependencies: [ + 'effect', + 'legacy_bus_service', + 'localization', + 'messagingValues', + 'orm', + 'router', + 'rpc', + 'ui', + 'user', + ], + + start(env, { messagingValues }) { + const modelManager = new ModelManager(env); + this._startModelManager(modelManager, messagingValues); + + return { + /** + * Returns the messaging record once it is initialized. This method + * should be considered the main entry point to the messaging system + * for outside code. + * + * @returns {mail.messaging} + **/ + async get() { + return modelManager.getMessaging(); + }, + modelManager, + }; + }, + /** + * Separate method to control creation delay in tests. + * + * @private + */ + _startModelManager(modelManager, messagingValues) { + modelManager.start(messagingValues); + }, +}; diff --git a/addons/mail/static/src/services/systray_service/systray_service.js b/addons/mail/static/src/services/systray_service.js similarity index 77% rename from addons/mail/static/src/services/systray_service/systray_service.js rename to addons/mail/static/src/services/systray_service.js index 41e25e7cf83..9553c9cba09 100644 --- a/addons/mail/static/src/services/systray_service/systray_service.js +++ b/addons/mail/static/src/services/systray_service.js @@ -3,18 +3,14 @@ import { MessagingMenuContainer } from '@mail/components/messaging_menu_container/messaging_menu_container'; import { RtcActivityNoticeContainer } from '@mail/components/rtc_activity_notice_container/rtc_activity_notice_container'; -import AbstractService from 'web.AbstractService'; import { registry } from '@web/core/registry'; const systrayRegistry = registry.category('systray'); -export const SystrayService = AbstractService.extend({ +export const systrayService = { dependencies: ['messaging'], - /** - * @override {web.AbstractService} - */ - async start() { + start() { systrayRegistry.add('mail.MessagingMenuContainer', { Component: MessagingMenuContainer }); systrayRegistry.add('mail.RtcActivityNoticeContainer', { Component: RtcActivityNoticeContainer }, { sequence: 100 }); }, -}); +}; diff --git a/addons/mail/static/src/utils/make_messaging_to_legacy_env.js b/addons/mail/static/src/utils/make_messaging_to_legacy_env.js new file mode 100644 index 00000000000..a9a32563dd3 --- /dev/null +++ b/addons/mail/static/src/utils/make_messaging_to_legacy_env.js @@ -0,0 +1,11 @@ + +/** @odoo-module **/ + +export function makeMessagingToLegacyEnv(legacyEnv) { + return { + dependencies: ['messaging'], + start(_, { messaging }) { + legacyEnv.services.messaging = messaging; + }, + }; +} diff --git a/addons/mail/static/src/webclient/commands/mail_providers.js b/addons/mail/static/src/webclient/commands/mail_providers.js index 4cd0ab17f26..81664f16e94 100644 --- a/addons/mail/static/src/webclient/commands/mail_providers.js +++ b/addons/mail/static/src/webclient/commands/mail_providers.js @@ -31,7 +31,7 @@ const commandProviderRegistry = registry.category("command_provider"); commandProviderRegistry.add("partner", { namespace: "@", async provide(newEnv, options) { - const messaging = await Component.env.services.messaging.get(); + const messaging = await newEnv.services.messaging.get(); const suggestions = []; await messaging.models['Partner'].imSearch({ callback(partners) { @@ -58,7 +58,7 @@ commandProviderRegistry.add("partner", { commandProviderRegistry.add("channel", { namespace: "#", async provide(newEnv, options) { - const messaging = await Component.env.services.messaging.get(); + const messaging = await newEnv.services.messaging.get(); const channels = await messaging.models['Thread'].searchChannelsToOpen({ limit: 10, searchTerm: options.searchValue, diff --git a/addons/mail/static/src/widgets/discuss/discuss.js b/addons/mail/static/src/widgets/discuss/discuss.js deleted file mode 100644 index 3da180743bb..00000000000 --- a/addons/mail/static/src/widgets/discuss/discuss.js +++ /dev/null @@ -1,74 +0,0 @@ -/** @odoo-module **/ - -import { DiscussContainer } from "@mail/components/discuss_container/discuss_container"; - -import AbstractAction from 'web.AbstractAction'; - -const { App } = owl; - -export const DiscussWidget = AbstractAction.extend({ - template: 'mail.widgets.Discuss', - /** - * @override {web.AbstractAction} - * @param {web.ActionManager} parent - * @param {Object} action - * @param {Object} [action.context] - * @param {string} [action.context.active_id] - * @param {Object} [action.params] - * @param {string} [action.params.default_active_id] - * @param {Object} [options={}] - */ - init(parent, action, options={}) { - this._super(...arguments); - - this.app = undefined; - this.action = action; - this.actionManager = parent; - }, - /** - * @override {web.AbstractAction} - */ - destroy() { - if (this.app) { - this.app.destroy(); - this.app = undefined; - } - if (this.$buttons) { - this.$buttons.off().remove(); - } - this._super(...arguments); - }, - /** - * @override {web.AbstractAction} - */ - async on_attach_callback() { - this._super(...arguments); - if (this.app) { - // prevent twice call to on_attach_callback (FIXME) - return; - } - - this.app = new App(DiscussContainer, { - templates: window.__OWL_TEMPLATES__, - env: owl.Component.env, - dev: owl.Component.env.isDebug(), - translateFn: owl.Component.env._t, - translatableAttributes: ["data-tooltip"], - props: { - action: this.action, - actionManager: this.actionManager, - }, - }); - await this.app.mount(this.el); - }, - /** - * @override {web.AbstractAction} - */ - on_detach_callback() { - this._super(...arguments); - if (this.app) { - this.app.destroy(); - } - this.app = undefined; - }, -}); diff --git a/addons/mail/static/src/widgets/discuss/discuss.scss b/addons/mail/static/src/widgets/discuss/discuss.scss deleted file mode 100644 index e7a19f7ec2c..00000000000 --- a/addons/mail/static/src/widgets/discuss/discuss.scss +++ /dev/null @@ -1,10 +0,0 @@ -// ------------------------------------------------------------------ -// Layout -// ------------------------------------------------------------------ - -.o_widget_Discuss { - display: flex; - flex: 0 0 100%; - flex-flow: column; - height: 100%; -} diff --git a/addons/mail/static/src/widgets/discuss/discuss.xml b/addons/mail/static/src/widgets/discuss/discuss.xml deleted file mode 100644 index 5826f157aff..00000000000 --- a/addons/mail/static/src/widgets/discuss/discuss.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - -
- - - diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 2df186cc7f6..07f98e63d11 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -1,57 +1,25 @@ /** @odoo-module **/ -import { getPyEnv, nextAnimationFrame } from '@mail/../tests/helpers/test_utils'; +import { getPyEnv, TEST_USER_IDS } from '@mail/../tests/helpers/test_utils'; + +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; -import MockServer from 'web.MockServer'; import { date_to_str, datetime_to_str } from 'web.time'; -MockServer.include({ - /** - * Param 'data' may have keys for the different magic partners/users. - * - * Note: we must delete these keys, so that this is not - * handled as a model definition. - * - * @override - * @param {Object} [data.currentPartnerId] - * @param {Object} [data.currentUserId] - * @param {Object} [data.partnerRootId] - * @param {Object} [data.publicPartnerId] - * @param {Object} [data.publicUserId] - * @param {Widget} [options.widget] mocked widget (use to call services) - */ - init(data, options) { - if (data && data.currentPartnerId) { - this.currentPartnerId = data.currentPartnerId; - delete data.currentPartnerId; - } - if (data && data.currentUserId) { - this.currentUserId = data.currentUserId; - delete data.currentUserId; - } - if (data && data.partnerRootId) { - this.partnerRootId = data.partnerRootId; - delete data.partnerRootId; - } - if (data && data.publicPartnerId) { - this.publicPartnerId = data.publicPartnerId; - delete data.publicPartnerId; - } - if (data && data.publicUserId) { - this.publicUserId = data.publicUserId; - delete data.publicUserId; - } - this._widget = options.widget; +patch(MockServer.prototype, 'mail', { + init({ models }) { this._super(...arguments); + Object.assign(this, TEST_USER_IDS); - if (this.currentPartnerId) { + if (this.currentPartnerId && models && 'res.partner' in models) { this.currentPartner = this.getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0]; } MockServer.currentMockServer = this; // creation of the ir.model.fields records, required for tracked fields - for (const modelName in data) { - const fieldNamesToFields = data[modelName].fields; + for (const modelName in models) { + const fieldNamesToFields = models[modelName].fields; for (const fname in fieldNamesToFields) { if (fieldNamesToFields[fname].tracking) { this.mockCreate('ir.model.fields', { model: modelName, name: fname }); @@ -65,6 +33,32 @@ MockServer.include({ async setup() { this.pyEnv = await getPyEnv(); }, + /** + * @override + */ + async performRPC(route, args) { + if (route === '/mail/attachment/upload') { + const ufile = args.body.get('ufile'); + const is_pending = args.body.get('is_pending') === 'true'; + const model = is_pending ? 'mail.compose.message' : args.body.get('thread_model'); + const id = is_pending ? 0 : parseInt(args.body.get('thread_id')); + const attachmentId = this.mockCreate('ir.attachment', { + // datas, + mimetype: ufile.type, + name: ufile.name, + res_id: id, + res_model: model, + }); + const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0]; + return { + 'filename': attachment.name, + 'id': attachment.id, + 'mimetype': attachment.mimetype, + 'size': attachment.file_size + }; + } + return this._super(...arguments); + }, //-------------------------------------------------------------------------- // Private @@ -73,34 +67,11 @@ MockServer.include({ /** * @override */ - async _performFetch(resource, init) { - if (resource === '/mail/attachment/upload') { - const ufile = init.body.get('ufile'); - const is_pending = init.body.get('is_pending') === 'true'; - const model = is_pending ? 'mail.compose.message' : init.body.get('thread_model'); - const id = is_pending ? 0 : parseInt(init.body.get('thread_id')); - const attachmentId = this.pyEnv['ir.attachment'].create({ - // datas, - mimetype: ufile.type, - name: ufile.name, - res_id: id, - res_model: model, - }); - const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0]; - return new window.Response(JSON.stringify({ - 'filename': attachment.name, - 'id': attachment.id, - 'mimetype': attachment.mimetype, - 'size': attachment.file_size - })); - } - return this._super(...arguments); - }, - /** - * @override - */ - async _performRpc(route, args) { + async _performRPC(route, args) { // routes + if (route === '/longpolling/im_status') { + return []; + } if (route === '/mail/message/post') { if (args.thread_model === 'mail.channel') { return this._mockMailChannelMessagePost(args.thread_id, args.post_data, args.context); @@ -327,7 +298,7 @@ MockServer.include({ /** * @override */ - _mockWrite(model) { + mockWrite(model) { const initialTrackedFieldValuesByRecordId = this._mockMailThread_TrackPrepare(model); const mockWriteResult = this._super(...arguments); if (initialTrackedFieldValuesByRecordId) { @@ -652,7 +623,7 @@ MockServer.include({ */ _mockMailActivityActionDone(ids) { const activities = this.getRecords('mail.activity', [['id', 'in', ids]]); - this._mockUnlink('mail.activity', [activities.map(activity => activity.id)]); + this.mockUnlink('mail.activity', [activities.map(activity => activity.id)]); }, /** * Simulates `action_feedback` on `mail.activity`. @@ -1249,7 +1220,7 @@ MockServer.include({ return matchingChannels; }; - const mentionSuggestions = mentionSuggestionsFilter(this.data['mail.channel'].records, search, limit); + const mentionSuggestions = mentionSuggestionsFilter(this.models['mail.channel'].records, search, limit); return mentionSuggestions; }, @@ -1442,9 +1413,6 @@ MockServer.include({ * @returns {Object[]} */ async _mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) { - // TODO FIXME delay RPC until next potential render as a workaround - // to OWL issue (possibly https://github.com/odoo/owl/issues/904) - await nextAnimationFrame(); if (max_id) { domain.push(['id', '<', max_id]); } @@ -1782,7 +1750,7 @@ MockServer.include({ if (record.user_id) { const user = this.getRecords('res.users', [['id', '=', record.user_id]]); if (user.partner_id) { - const reason = this.data[model].fields['user_id'].string; + const reason = this.models[model].fields['user_id'].string; this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason); } } @@ -1967,9 +1935,9 @@ MockServer.include({ * Simulates `_message_track` on `mail.thread` */ _mockMailThread_MessageTrack(modelName, trackedFieldNames, initialTrackedFieldValuesByRecordId) { - const trackFieldNamesToField = this._mockFieldsGet(modelName, [trackedFieldNames]); + const trackFieldNamesToField = this.mockFieldsGet(modelName, [trackedFieldNames]); const tracking = {}; - const records = this.data[modelName].records; + const records = this.models[modelName].records; for (const record of records) { tracking[record.id] = this._mockMailBaseModel_MailTrack(modelName, trackFieldNamesToField, initialTrackedFieldValuesByRecordId[record.id], record); } @@ -2005,7 +1973,7 @@ MockServer.include({ * Simulates `_track_get_fields` on `mail.thread` */ _mockMailThread_TrackGetFields(model) { - return Object.entries(this.data[model].fields).reduce((prev, next) => { + return Object.entries(this.models[model].fields).reduce((prev, next) => { if (next[1].tracking) { prev.push(next[0]); } @@ -2021,7 +1989,7 @@ MockServer.include({ return; } const initialTrackedFieldValuesByRecordId = {}; - for (const record of this.data[model].records) { + for (const record of this.models[model].records) { const values = {}; initialTrackedFieldValuesByRecordId[record.id] = values; for (const fname of trackedFieldNames) { @@ -2041,7 +2009,7 @@ MockServer.include({ */ _mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fieldName, field, modelName) { let isTracked = true; - const irField = this.data['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName); + const irField = this.models['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName); if (!irField) { return; @@ -2504,36 +2472,4 @@ MockServer.include({ ), }); }, - - //-------------------------------------------------------------------------- - // New mock server compatibility - //-------------------------------------------------------------------------- - - getRecords() { - return this._getRecords(...arguments); - }, - mockCreate() { - return this._mockCreate(...arguments); - }, - mockRead() { - return this._mockRead(...arguments); - }, - mockReadGroup() { - return this._mockReadGroup(...arguments); - }, - mockSearch() { - return this._mockSearch(...arguments); - }, - mockSearchCount() { - return this._mockSearchCount(...arguments); - }, - mockSearchRead() { - return this._mockSearchRead(...arguments); - }, - mockUnlink() { - return this._mockUnlink(...arguments); - }, - mockWrite() { - return this._mockWrite(...arguments); - }, }); diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index c354fcf3647..a807e615cb6 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -1,39 +1,22 @@ /** @odoo-module **/ -import BusService from 'bus.BusService'; - -import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; -import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; import { MessagingMenuContainer } from '@mail/components/messaging_menu_container/messaging_menu_container'; import { insertAndReplace, replace } from '@mail/model/model_field_command'; -import { MessagingService } from '@mail/services/messaging/messaging'; -import { makeDeferred } from '@mail/utils/deferred'; import { getMessagingComponent } from '@mail/utils/messaging_component'; import { nextTick } from '@mail/utils/utils'; -import { DiscussWidget } from '@mail/widgets/discuss/discuss'; -import { addTimeControlToEnv } from '@mail/../tests/helpers/time_control'; +import { getAdvanceTime } from '@mail/../tests/helpers/time_control'; +import { getWebClientReady } from '@mail/../tests/helpers/webclient_setup'; -import core from 'web.core'; -import AbstractStorageService from 'web.AbstractStorageService'; -import RamStorage from 'web.RamStorage'; -import { - createView, - mock, -} from 'web.test_utils'; -import Widget from 'web.Widget'; -import { browser } from '@web/core/browser/browser'; import { registry } from '@web/core/registry'; import { registerCleanup } from "@web/../tests/helpers/cleanup"; -import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils"; -import { createWebClient, getActionManagerServerData } from "@web/../tests/webclient/helpers"; +import { MockServer } from "@web/../tests/helpers/mock_server"; +import { getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers"; -import LegacyRegistry from "web.Registry"; -import MockServer from 'web.MockServer'; +import core from 'web.core'; -const { App, Component, EventBus } = owl; +const { App, EventBus } = owl; const { afterNextRender } = App; -const { addMockEnvironment } = mock; - const modelDefinitionsPromise = new Promise(resolve => { QUnit.begin(() => resolve(getModelDefinitions())); }); @@ -59,44 +42,6 @@ function _createFakeDataTransfer(files) { }; } -/** - * @private - * @param {HTMLElement} target - */ -async function mountChatWindowManagerContainer(target) { - const app = new App(ChatWindowManagerContainer, { - templates: window.__OWL_TEMPLATES__, - env: owl.Component.env, - dev: owl.Component.env.isDebug(), - translateFn: owl.Component.env._t, - translatableAttributes: ["data-tooltip"], - }); - await app.mount(target); - - registerCleanup(() => { - app.destroy(); - }); -} - -/** - * @private - * @param {HTMLElement} target - */ -async function mountDialogManagerContainer(target) { - const app = new App(DialogManagerContainer, { - templates: window.__OWL_TEMPLATES__, - env: owl.Component.env, - dev: owl.Component.env.isDebug(), - translateFn: owl.Component.env._t, - translatableAttributes: ["data-tooltip"], - }); - await app.mount(target); - - registerCleanup(() => { - app.destroy(); - }); -} - //------------------------------------------------------------------------------ // Public: rendering timers //------------------------------------------------------------------------------ @@ -198,7 +143,7 @@ const VIEW_TYPE_TO_ARCH = { * server (creation, deletion, update of records...) */ export async function startServer() { - const data = { ...TEST_USER_IDS }; + const models = {}; const views = {}; const modelDefinitions = await modelDefinitionsPromise; const recordsToInsertRegistry = registry.category('mail.model.definitions').category('recordsToInsert'); @@ -208,7 +153,7 @@ const VIEW_TYPE_TO_ARCH = { // prevent tests from mutating the records. records.push(...JSON.parse(JSON.stringify(recordsToInsertRegistry.get(modelName)))); } - data[modelName] = { fields: { ...fields }, records }; + models[modelName] = { fields: { ...fields }, records }; // generate default views for this model. for (const [viewType, viewArch] of Object.entries(VIEW_TYPE_TO_ARCH)) { @@ -217,13 +162,16 @@ const VIEW_TYPE_TO_ARCH = { } pyEnv = new Proxy( { + getData() { + return this.mockServer.models; + }, + getViews() { + return views; + }, ...TEST_USER_IDS, get currentPartner() { return this.mockServer.currentPartner; }, - getViews() { - return views; - } }, { get(target, name) { @@ -305,7 +253,7 @@ const VIEW_TYPE_TO_ARCH = { }, }, ); - pyEnv['mockServer'] = new MockServer(data, {}); + pyEnv['mockServer'] = new MockServer({ models }, {}); await pyEnv['mockServer'].setup(); registerCleanup(() => pyEnv = undefined); return pyEnv; @@ -366,7 +314,7 @@ function getAfterEvent({ messagingBus }) { clearTimeout(timeoutNoEvent); // If the event is triggered before the end of the async function, // ensure the function finishes its job before returning. - await funcRes; + return await funcRes; }; } @@ -499,7 +447,7 @@ function getCreateMessageComponent({ env, modelManager, target }) { function getCreateMessagingMenuComponent({ env, target }) { return async function createMessagingMenuComponent() { - return await createRootComponent({ env }, MessagingMenuContainer, { target }); + return await createRootComponent(env, MessagingMenuContainer, { target }); }; } @@ -527,7 +475,7 @@ function getCreateThreadViewComponent({ afterEvent, env, target }) { 'flex-flow': 'column', height: '300px', }); - target.append(div); + target.appendChild(div); actualTarget = div; } else { actualTarget = target; @@ -553,67 +501,41 @@ function getCreateThreadViewComponent({ afterEvent, env, target }) { }; } -function getOpenDiscuss({ afterNextRender, discuss, selector, widget }) { +function getOpenDiscuss(webClient, { context, params, ...props } = {}) { return async function openDiscuss() { - widget.do_push_state = () => {}; - const discussWidget = new DiscussWidget(widget, discuss); - await discussWidget.appendTo($(selector)); - await afterNextRender(() => discussWidget.on_attach_callback()); - // Some changes in the models are made on mount, but these changes don't - // cause a rerender directly, they cause the model to fetch more data - // but we cannot wait for that data to come back as the model manager - // doesn't expose it. This means that in the following microticks, the - // data will come back from the server and cause a render. The following - // is a way for us to catch the render cascade caused by the data coming - // back and wait for it. - await afterNextRender(() => { - discussWidget.app.root.render(); - }); - return discussWidget; + const actionOpenDiscuss = { + // hardcoded actionId, required for discuss_container props validation. + id: 104, + context, + params, + tag: 'mail.action_discuss', + type: 'ir.actions.client', + }; + await afterNextRender(() => doAction(webClient, actionOpenDiscuss, { props })); }; } +//------------------------------------------------------------------------------ +// Public: start function helpers +//------------------------------------------------------------------------------ + /** * Main function used to make a mocked environment with mocked messaging env. * * @param {Object} [param0={}] - * @param {string} [param0.arch] makes only sense when `param0.hasView` is set: - * the arch to use in createView. - * @param {Object} [param0.archs] - * @param {boolean} [param0.autoOpenDiscuss=false] determine whether mounted discuss should be + * @param {boolean} [param0.autoOpenDiscuss=false] determine if discuss should be * open initially. Deprecated, call openDiscuss() instead. - * @param {boolean} [param0.debug=false] - * @param {Object} [param0.data] makes only sense when `param0.hasView` is set: - * the data to use in createView. - * @param {Object} [param0.discuss={}] provide data that is passed to discuss widget - * (= client action) as 2nd positional argument. - * @param {Object} [param0.env={}] - * @param {function} [param0.mockFetch] + * @param {Object} [param0.serverData] The data to pass to the webClient + * @param {Object} [param0.discuss={}] provide data that is passed to the discuss action. + * @param {Object} [param0.legacyServices] + * @param {Object} [param0.services] * @param {function} [param0.mockRPC] - * @param {boolean} [param0.hasWebClient=false] if set, use - * createWebClient * @param {boolean} [param0.hasTimeControl=false] if set, all flow of time - * with `env.browser.setTimeout` are fully controlled by test itself. - * @see addTimeControlToEnv that adds `advanceTime` function in - * `env.testUtils`. - * @param {boolean} [param0.hasView=false] if set, use createView to create a - * view instead of a generic widget. + * with `messaging.browser.setTimeout` are fully controlled by test itself. * @param {integer} [param0.loadingBaseDelayDuration=0] * @param {Deferred|Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()] * Deferred that let tests block messaging creation and simulate resolution. * Useful for testing working components when messaging is not yet created. - * @param {string} [param0.model] makes only sense when `param0.hasView` is set: - * the model to use in createView. - * @param {integer} [param0.res_id] makes only sense when `param0.hasView` is set: - * the res_id to use in createView. - * @param {Object} [param0.services] - * @param {Object} [param0.session] - * @param {Element} [param0.target] if provided, the component will be mounted inside - * that element (only used if `params0.hasWebClient` is true) - * @param {Object} [param0.View] makes only sense when `param0.hasView` is set: - * the View class to use in createView. - * @param {Object} [param0.viewOptions] makes only sense when `param0.hasView` - * is set: the view options to use in createView. * @param {Object} [param0.waitUntilEvent] * @param {String} [param0.waitUntilEvent.eventName] * @param {String} [param0.waitUntilEvent.message] @@ -631,7 +553,6 @@ function getOpenDiscuss({ afterNextRender, discuss, selector, widget }) { * as param of `messagingBeforeCreationDeferred`. To make sure messaging is * not initialized, test should mock RPC `mail/init_messaging` and block its * resolution. - * @param {...Object} [param0.kwargs] * @throws {Error} in case some provided parameters are wrong, such as * `waitUntilMessagingCondition`. * @returns {Object} @@ -643,189 +564,19 @@ async function start(param0 = {}) { throttle: func => func, }); const { - autoOpenDiscuss = false, + autoOpenDiscuss, discuss = {}, - env: providedEnv = {}, - hasWebClient = false, - hasTimeControl = false, - hasView = false, - loadingBaseDelayDuration = 0, - messagingBeforeCreationDeferred = Promise.resolve(), - target: webclientTarget = getFixture(), + hasTimeControl, waitUntilEvent, waitUntilMessagingCondition = 'initialized', } = param0; + const target = param0['target'] || getFixture(); + param0['target'] = target; if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) { throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`); } - delete param0.autoOpenDiscuss; - delete param0.env; - delete param0.hasWebClient; - delete param0.hasTimeControl; - delete param0.hasView; - delete param0.target; const messagingBus = new EventBus(); - const { debug = false } = param0; const testSetupDoneDeferred = makeDeferred(); - let env = { - ...providedEnv, - browser: { - ...providedEnv.browser, - innerWidth: browser.innerWidth, - innerHeight: browser.innerHeight, - }, - device: { - ...providedEnv.device, - isMobile: owl.Component.env.device.isMobile, - }, - }; - env.session = Object.assign( - { - is_bound: Promise.resolve(), - url: s => s, - }, - env.session - ); - env.isDebug = env.isDebug || (() => true); - if (hasTimeControl) { - env = addTimeControlToEnv(env); - } - - const services = Object.assign({}, { - bus_service: BusService.extend({ - _beep() {}, // Do nothing - _poll() {}, // Do nothing - _registerWindowUnload() {}, // Do nothing - isOdooFocused() { - return true; - }, - updateOption() {}, - }), - local_storage: AbstractStorageService.extend({ storage: new RamStorage() }), - messaging: MessagingService.extend({ - // test specific values - messagingValues: { - autofetchPartnerImStatus: false, - disableAnimation: true, - loadingBaseDelayDuration, - messagingBus, - }, - /** - * Override to ensure tests run in debug mode to catch all potential - * programming errors and provide better message when they happen. - */ - init(...args) { - this._super(...args); - this.modelManager.isDebug = true; - }, - /** - * Override: - * - to ensure the test setup is complete before starting otherwise - * for example the mock server might not be ready yet at init - * messaging, - * - to add control on when messaging is created, useful to test - * spinners and race conditions. - * - * @override - */ - async start() { - const _super = this._super.bind(this); - await testSetupDoneDeferred; - await messagingBeforeCreationDeferred; - _super(); - }, - }), - }, param0.services); - - if (!param0.data && (!param0.serverData || !param0.serverData.models)) { - pyEnv = pyEnv || await startServer(); - const data = pyEnv.mockServer.data; - Object.assign(data, TEST_USER_IDS); - if (hasWebClient) { - param0.serverData = param0.serverData || getActionManagerServerData(); - param0.serverData.models = data; - param0.serverData.views = { ...pyEnv.getViews(), ...param0.serverData.views }; - } else { - param0.data = data; - } - } - - const kwargs = Object.assign({}, param0, { - archs: Object.assign({}, { - 'mail.message,false,search': '' - }, param0.archs), - debug: param0.debug || false, - services: Object.assign({}, services, param0.services), - }, { env }); - let widget; - let mockServer; - let testEnv; - const selector = debug ? 'body' : '#qunit-fixture'; - if (hasView) { - widget = await createView(kwargs); - } else if (hasWebClient) { - let serverData; - if (!kwargs.serverData) { - serverData = getActionManagerServerData(); - } else { - serverData = kwargs.serverData; - delete kwargs.serverData; - } - - if (kwargs.actions) { - const actions = {}; - kwargs.actions.forEach((act) => { - actions[act.xml_id || act.id] = act; - }); - Object.assign(serverData.actions, actions); - delete kwargs.actions; - } - - Object.assign(serverData.views, kwargs.archs); - delete kwargs.archs; - - Object.assign(serverData.models, kwargs.data); - delete kwargs.data; - - const mockRPC = kwargs.mockRPC; - delete kwargs.mockRPC; - - if (kwargs.services) { - const serviceRegistry = kwargs.serviceRegistry = new LegacyRegistry(); - for (const sname in kwargs.services) { - serviceRegistry.add(sname, kwargs.services[sname]); - } - delete kwargs.services; - } - - const legacyParams = kwargs; - legacyParams.withLegacyMockServer = true; - legacyParams.env = env; - - widget = await createWebClient({ target: webclientTarget, serverData, mockRPC, legacyParams }); - } else { - const Parent = Widget.extend({ do_push_state() {} }); - const parent = new Parent(); - mockServer = await addMockEnvironment(parent, kwargs); - widget = new Widget(parent); - await widget.appendTo($(selector)); - Object.assign(widget, { - destroy() { - delete widget.destroy; - parent.destroy(); - }, - }); - } - // get the final test env after execution of createView/createWebClient/addMockEnvironment - testEnv = Component.env; - patchWithCleanup(browser, { - fetch: testEnv.browser.fetch, - }); - // link the pyEnv to the actual mockServer after execution of createView/createWebClient/addMockEnvironment - if (pyEnv) { - pyEnv.mockServer = MockServer.currentMockServer; - mockServer = pyEnv.mockServer; - } const afterEvent = getAfterEvent({ messagingBus }); let waitUntilEventPromise; if (waitUntilEvent) { @@ -834,22 +585,15 @@ async function start(param0 = {}) { testSetupDoneDeferred.resolve(); waitUntilEventPromise = Promise.resolve(); } - const result = { - afterEvent, - env: testEnv, - mockServer, - pyEnv, - widget, - wowlEnv: hasWebClient ? widget.env : undefined, - }; - if (hasTimeControl) { - result['advanceTime'] = env.testUtils.advanceTime; - } - const { modelManager } = testEnv.services.messaging; - registerCleanup(async() => { - if (!hasWebClient) { - widget.destroy(); - } + + pyEnv = await getPyEnv(); + param0.serverData = param0.serverData || getActionManagerServerData(); + param0.serverData.models = { ...pyEnv.getData(), ...param0.serverData.models }; + param0.serverData.views = { ...pyEnv.getViews(), ...param0.serverData.views }; + const webClient = await getWebClientReady({ ...param0, messagingBus, testSetupDoneDeferred }); + + const { modelManager } = webClient.env.services.messaging; + registerCleanup(async () => { await modelManager.messagingInitializedPromise; modelManager.messaging.delete(); }); @@ -860,30 +604,37 @@ async function start(param0 = {}) { await modelManager.messagingCreatedPromise; await modelManager.messagingInitializedPromise; } - const openDiscuss = getOpenDiscuss({ afterNextRender, discuss, selector, widget }); + const openDiscuss = getOpenDiscuss(webClient, discuss); if (autoOpenDiscuss) { - const discussWidget = await openDiscuss(); - result['discussWidget'] = discussWidget; + await openDiscuss(); } - const target = hasWebClient ? webclientTarget : widget.el; - await mountChatWindowManagerContainer(target); - await mountDialogManagerContainer(target); + // link the pyEnv to the actual mockServer after execution of createWebClient. + pyEnv.mockServer = MockServer.currentMockServer; + const openView = async (action, options) => { + action['type'] = action['type'] || 'ir.actions.act_window'; + return doAction(webClient, action, { props: options }); + }; await waitUntilEventPromise; return { - ...result, + advanceTime: hasTimeControl ? getAdvanceTime() : undefined, + afterEvent, afterNextRender, click: getClick({ afterNextRender }), - createChatterContainerComponent: getCreateChatterContainerComponent({ afterEvent, env: testEnv, target }), - createComposerComponent: getCreateComposerComponent({ env: testEnv, modelManager, target }), - createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ env: testEnv, modelManager, target }), - createMessageComponent: getCreateMessageComponent({ env: testEnv, modelManager, target }), - createMessagingMenuComponent: getCreateMessagingMenuComponent({ env: testEnv, target }), - createNotificationListComponent: getCreateNotificationListComponent({ env: testEnv, modelManager, target }), - createRootMessagingComponent: (componentName, props) => createRootMessagingComponent(testEnv, componentName, { props, target }), - createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, env: testEnv, target }), + createChatterContainerComponent: getCreateChatterContainerComponent({ afterEvent, env: webClient.env, target }), + createComposerComponent: getCreateComposerComponent({ env: webClient.env, modelManager, target }), + createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ env: webClient.env, modelManager, target }), + createMessageComponent: getCreateMessageComponent({ env: webClient.env, modelManager, target }), + createMessagingMenuComponent: getCreateMessagingMenuComponent({ env: webClient.env, target }), + createNotificationListComponent: getCreateNotificationListComponent({ env: webClient.env, modelManager, target }), + createRootMessagingComponent: (componentName, props) => createRootMessagingComponent(webClient.env, componentName, { props, target }), + createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, env: webClient.env, target }), + env: webClient.env, insertText, messaging: modelManager.messaging, openDiscuss, + openView, + pyEnv, + webClient, }; } diff --git a/addons/mail/static/tests/helpers/time_control.js b/addons/mail/static/tests/helpers/time_control.js index 78ca6a14a61..600735b5aa8 100644 --- a/addons/mail/static/tests/helpers/time_control.js +++ b/addons/mail/static/tests/helpers/time_control.js @@ -3,17 +3,9 @@ import { nextTick } from '@mail/utils/utils'; import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -/** - * @param {Object} [providedEnv={}] - * @returns {Object} - */ - export function addTimeControlToEnv(providedEnv = {}) { - const env = { ...providedEnv }; - if (!env.browser) { - env.browser = {}; - } +export function getAdvanceTime() { // list of timeout ids that have timed out. let timedOutIds = []; // key: timeoutId, value: func + remaining duration @@ -39,31 +31,24 @@ import { patchWithCleanup } from '@web/../tests/helpers/utils'; return timeoutId; }, }); - if (!env.testUtils) { - env.testUtils = {}; - } - Object.assign(env.testUtils, { - advanceTime: async duration => { - await nextTick(); - for (const id of timeouts.keys()) { - const timeout = timeouts.get(id); - if (timeout.isTimedOut) { - continue; - } - timeout.duration = Math.max(timeout.duration - duration, 0); - if (timeout.duration === 0) { - timedOutIds.push(id); - } + return async function (duration) { + await nextTick(); + for (const id of timeouts.keys()) { + const timeout = timeouts.get(id); + if (timeout.isTimedOut) { + continue; } - while (timedOutIds.length > 0) { - const id = timedOutIds.shift(); - const timeout = timeouts.get(id); - timeouts.delete(id); - timeout.func(); - await nextTick(); + timeout.duration = Math.max(timeout.duration - duration, 0); + if (timeout.duration === 0) { + timedOutIds.push(id); } + } + while (timedOutIds.length > 0) { + const id = timedOutIds.shift(); + const timeout = timeouts.get(id); + timeouts.delete(id); + timeout.func(); await nextTick(); - }, - }); - return env; + } + }; } diff --git a/addons/mail/static/tests/helpers/webclient_setup.js b/addons/mail/static/tests/helpers/webclient_setup.js new file mode 100644 index 00000000000..6c10d2cca2b --- /dev/null +++ b/addons/mail/static/tests/helpers/webclient_setup.js @@ -0,0 +1,174 @@ +/** @odoo-module **/ + +import { legacyBusService } from '@bus/js/services/legacy/legacy_bus_service'; + +import BusService from 'bus.BusService'; + +import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; +import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; +import { DiscussContainer } from '@mail/components/discuss_container/discuss_container'; +import { messagingService } from '@mail/services/messaging_service'; +import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; + +import { registry } from '@web/core/registry'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; +import { createWebClient } from "@web/../tests/webclient/helpers"; + +import AbstractStorageService from 'web.AbstractStorageService'; +import RamStorage from 'web.RamStorage'; +import LegacyRegistry from 'web.Registry'; + +const WEBCLIENT_LOAD_ROUTES = [ + '/web/webclient/load_menus', + '/web/dataset/call_kw/res.users/load_views', +]; +const WEBCLIENT_PARAMETER_NAMES = new Set(['legacyParams', 'mockRPC', 'serverData', 'target', 'webClientClass']); +const SERVICES_PARAMETER_NAMES = new Set([ + 'legacyServices', 'loadingBaseDelayDuration', 'messagingBeforeCreationDeferred', + 'messagingBus', 'services', 'testSetupDoneDeferred', +]); + +/** + * Add required components to the main component registry. + */ + function setupMainComponentRegistry() { + const mainComponentRegistry = registry.category('main_components'); + mainComponentRegistry.add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer }); + mainComponentRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer }); + registry.category('actions').add('mail.action_discuss', DiscussContainer); +} + +/** + * Setup both legacy and new service registries. + * + * @param {Object} param0 + * @param {Object} [param0.services] + * @param {Object} [param0.legacyServices] + * @param {number} [param0.loadingBaseDelayDuration=0] + * @param {Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()] + * Deferred that let tests block messaging creation and simulate resolution. + * Useful for testing components behavior when messaging is not yet created. + * @param {EventBus} [param0.messagingBus] + * @param {Promise} [param0.testSetupDoneDeferred] The Promise to whose revolution has to be + * waited before starting the messaging service. + * @returns {LegacyRegistry} The registry containing all the legacy services that will be passed + * to the webClient as a legacy parameter. + */ +function setupMessagingServiceRegistries({ + legacyServices, + loadingBaseDelayDuration = 0, + messagingBeforeCreationDeferred = Promise.resolve(), + messagingBus, + services, + testSetupDoneDeferred, + }) { + const serviceRegistry = registry.category('services'); + const legacyServiceRegistry = new LegacyRegistry(); + legacyServices = { + bus_service: BusService.extend({ + _beep() {}, // Do nothing + _poll() {}, // Do nothing + _registerWindowUnload() {}, // Do nothing + isOdooFocused() { + return true; + }, + updateOption() {}, + }), + local_storage: AbstractStorageService.extend({ storage: new RamStorage() }), + ...legacyServices, + }; + + patchWithCleanup(messagingService, { + async _startModelManager() { + const _super = this._super.bind(this); + await testSetupDoneDeferred; + await messagingBeforeCreationDeferred; + return _super(...arguments); + }, + }); + + const messagingValues = { + start() { + return { + autofetchPartnerImStatus: false, + disableAnimation: true, + loadingBaseDelayDuration, + messagingBus, + }; + } + }; + + services = { + messagingValues, + ...services, + }; + // during tests, we need to wait for the legacyBusService to be added before adding the messaging + // service to the registry or it will result in an error during the webClient creation. + const addMessagingToRegistryFn = ev => { + const { key, operation } = ev.detail; + if (key === 'legacy_bus_service' && operation === 'add') { + serviceRegistry.add('messaging', messagingService); + serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env)); + serviceRegistry.removeEventListener('UPDATE', addMessagingToRegistryFn); + } + }; + serviceRegistry.addEventListener('UPDATE', addMessagingToRegistryFn); + + Object.entries(services).forEach(([serviceName, service]) => { + serviceRegistry.add(serviceName, service); + }); + Object.entries(legacyServices).forEach(([serviceName, service]) => { + legacyServiceRegistry.add(serviceName, service); + }); + + return legacyServiceRegistry; +} + +/** + * Creates a properly configured instance of WebClient, with the messaging service and all it's + * dependencies initialized. + * + * @param {Object} param0 + * @param {Object} [param0.serverData] + * @param {Object} [param0.services] + * @param {Object} [param0.legacyServices] + * @param {Object} [param0.legacyParams={}] + * @param {Object} [param0.loadingBaseDelayDuration] + * @param {Object} [param0.messagingBeforeCreationDeferred] + * @param {Promise} [param0.testSetupDoneDeferred] The Promise to whose revolution has to be + * waited before starting the messaging service. + * @param {EventBus} [param0.messagingBus] The event bus to be used by messaging. + * @returns {WebClient} + */ + async function getWebClientReady(param0) { + setupMainComponentRegistry(); + + const servicesParameters = {}; + const param0Entries = Object.entries(param0); + for (const [parameterName, value] of param0Entries) { + if (SERVICES_PARAMETER_NAMES.has(parameterName)) { + servicesParameters[parameterName] = value; + } + } + const legacyServiceRegistry = setupMessagingServiceRegistries(servicesParameters); + + const webClientParameters = {}; + for (const [parameterName, value] of param0Entries) { + if (WEBCLIENT_PARAMETER_NAMES.has(parameterName)) { + webClientParameters[parameterName] = value; + } + } + webClientParameters.legacyParams = { + legacyServicesToDeployInWowlEnv: { + legacy_bus_service: legacyBusService, + }, + serviceRegistry: legacyServiceRegistry, + ...webClientParameters.legacyParams, + }; + return createWebClient(webClientParameters); +} + +export { + getWebClientReady, + WEBCLIENT_LOAD_ROUTES, +}; diff --git a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js index 1f96a727c1b..0c66213ebb4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js @@ -1,10 +1,10 @@ /** @odoo-module **/ import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { WEBCLIENT_LOAD_ROUTES } from '@mail/../tests/helpers/webclient_setup'; -import FormView from 'web.FormView'; -import ListView from 'web.ListView'; import testUtils from 'web.test_utils'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('Chatter'); @@ -13,19 +13,25 @@ QUnit.test('list activity widget with no activity', async function (assert) { assert.expect(4); const pyEnv = await startServer(); - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: '', - mockRPC: function (route) { - if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) { + const views = { + 'res.users,false,list': '', + }; + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== 'get_views' && + !['/mail/init_messaging', '/mail/load_message_failures', ...WEBCLIENT_LOAD_ROUTES].includes(route) + ) { assert.step(route); } - return this._super(...arguments); }, + serverData: { views }, session: { uid: pyEnv.currentUserId }, }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], + }); assert.containsOnce(document.body, '.o_mail_activity .o_activity_color_default'); assert.strictEqual(document.querySelector('.o_activity_summary').innerText, ''); @@ -55,18 +61,24 @@ QUnit.test('list activity widget with activities', async function (assert) { activity_summary: false, activity_type_id: mailActivityTypeId2, }); + const views = { + 'res.users,false,list': '', + }; - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: '', - mockRPC: function (route) { - if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) { + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== 'get_views' && + !['/mail/init_messaging', '/mail/load_message_failures', ...WEBCLIENT_LOAD_ROUTES].includes(route) + ) { assert.step(route); } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], }); const firstRow = document.querySelector('.o_data_row'); @@ -95,17 +107,23 @@ QUnit.test('list activity widget with exception', async function (assert) { activity_exception_icon: 'fa-warning', }); - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: '', - mockRPC: function (route) { - if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) { + const views = { + 'res.users,false,list': '', + }; + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== 'get_views' && + !['/mail/init_messaging', '/mail/load_message_failures', ...WEBCLIENT_LOAD_ROUTES].includes(route) + ) { assert.step(route); } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], }); assert.containsOnce(document.body, '.o_activity_color_today.text-warning.fa-warning'); @@ -146,16 +164,15 @@ QUnit.test('list activity widget: open dropdown', async function (assert) { activity_type_id: mailActivityTypeId2, }); - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: ` - - - `, + const views = { + 'res.users,false,list': '', + }; + const { env, openView } = await start({ mockRPC: function (route, args) { - if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) { + if ( + args.method !== 'get_views' && + !['/mail/init_messaging', '/mail/load_message_failures', ...WEBCLIENT_LOAD_ROUTES].includes(route) + ) { assert.step(args.method || route); } if (args.method === 'action_feedback') { @@ -165,14 +182,21 @@ QUnit.test('list activity widget: open dropdown', async function (assert) { activity_summary: 'Meet FP', activity_type_id: mailActivityTypeId1, }); - return Promise.resolve(); + // random value returned in order for the mock server to know that this route is implemented. + return true; } - return this._super(route, args); }, - intercepts: { - switch_view: () => assert.step('switch_view'), + serverData: { views }, + }); + patchWithCleanup(env.services.action, { + switchView() { + assert.step('switch_view'); }, }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], + }); assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Call with Al'); @@ -236,13 +260,18 @@ QUnit.test('list activity exception widget with activity', async function (asser activity_exception_decoration: 'warning', activity_exception_icon: 'fa-warning', }); - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: '' + - ' ' + - '', + const views = { + 'res.users,false,list': + ` + + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], }); assert.containsN(document.body, '.o_data_row', 2, "should have two records"); @@ -274,15 +303,18 @@ QUnit.test('list activity widget: done the activity with "ENTER" keyboard shortc activity_summary: 'Call with Al', activity_type_id: mailActivityTypeId1, }); - - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: ` - + const views = { + 'res.users,false,list': + ` `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], }); await testUtils.dom.click(document.querySelector('.o_activity_btn span')); @@ -322,19 +354,24 @@ QUnit.test('list activity widget: done and schedule the next activity with "ENTE activity_type_id: mailActivityTypeId1, }); - await start({ - hasView: true, - View: ListView, - model: 'res.users', - arch: ` - + const views = { + 'res.users,false,list': + ` `, - intercepts: { - do_action: function (ev) { - assert.strictEqual(ev.data.action.name, "Schedule an Activity", 'should do a do_action with correct parameters'); - }, - }, + }; + const { env, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.users', + views: [[false, 'list']], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.strictEqual(action.name, "Schedule an Activity", 'should do a do_action with correct parameters'); + return this._super(...arguments); + } }); await testUtils.dom.click(document.querySelector('.o_activity_btn span')); @@ -359,32 +396,35 @@ QUnit.test('fieldmany2many tags email (edition)', async function (assert) { const mailMessageId1 = pyEnv['mail.message'].create({ partner_ids: [resPartnerId1], }); - - await start({ - hasView: true, - View: FormView, - model: 'mail.message', - res_id: mailMessageId1, - arch: '
' + + const views = { + 'mail.message,false,form': + '' + '' + '' + '' + '' + '
', - viewOptions: { - mode: 'edit', - }, + 'res.partner,false,form': '
', + }; + var { openView } = await start({ + serverData: { views }, mockRPC: function (route, args) { if (args.method === 'read' && args.model === 'res.partner') { assert.step(JSON.stringify(args.args[0])); assert.ok(args.args[1].includes('email'), "should read the email"); } - return this._super.apply(this, arguments); - }, - archs: { - 'res.partner,false,form': '
', }, }); + await openView( + { + res_id: mailMessageId1, + res_model: 'mail.message', + views: [[false, 'form']], + }, + { + mode: 'edit', + }, + ); assert.verifySteps([`[${resPartnerId1}]`]); assert.containsOnce(document.body, '.o_field_many2manytags[name="partner_ids"] .badge.o_tag_color_0', @@ -428,13 +468,16 @@ QUnit.test('many2many_tags_email widget can load more than 40 records', async fu const mailMessageId1 = pyEnv['mail.message'].create({ partner_ids: messagePartnerIds, }); - - const { click } = await start({ - hasView: true, - View: FormView, - model: 'mail.message', - arch: '
', + const views = { + 'mail.message,false,form': '
', + }; + var { click, openView } = await start({ + serverData: { views }, + }); + await openView({ res_id: mailMessageId1, + res_model: 'mail.message', + views: [[false, 'form']], }); assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 100); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js index 2fb59a60b7b..c462fdd84ed 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js @@ -2,7 +2,7 @@ import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -129,13 +129,13 @@ QUnit.test('activity mark done popover mark done without feedback', async functi assert.strictEqual(args.args[0][0], mailActivityId1); assert.strictEqual(args.kwargs.attachment_ids.length, 0); assert.notOk(args.kwargs.feedback); - return; + // random value returned in order for the mock server to know that this route is implemented. + return true; } if (route === '/web/dataset/call_kw/mail.activity/unlink') { // 'unlink' on non-existing record raises a server crash throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -170,13 +170,13 @@ QUnit.test('activity mark done popover mark done with feedback', async function assert.strictEqual(args.args[0][0], mailActivityId1); assert.strictEqual(args.kwargs.attachment_ids.length, 0); assert.strictEqual(args.kwargs.feedback, 'This task is done'); - return; + // random value returned in order for the mock server to know that this route is implemented. + return true; } if (route === '/web/dataset/call_kw/mail.activity/unlink') { // 'unlink' on non-existing record raises a server crash throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -198,11 +198,6 @@ QUnit.test('activity mark done popover mark done with feedback', async function QUnit.test('activity mark done popover mark done and schedule next', async function (assert) { assert.expect(6); - const bus = new Bus(); - bus.on('do-action', null, () => { - assert.step('activity_action'); - throw new Error("The do-action event should not be triggered when the route doesn't return an action"); - }); const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create(); const mailActivityId1 = pyEnv['mail.activity'].create({ @@ -211,7 +206,7 @@ QUnit.test('activity mark done popover mark done and schedule next', async funct res_id: resPartnerId1, res_model: 'res.partner', }); - const { click, createChatterContainerComponent } = await start({ + const { click, env, createChatterContainerComponent } = await start({ async mockRPC(route, args) { if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') { assert.step('action_feedback_schedule_next'); @@ -225,9 +220,13 @@ QUnit.test('activity mark done popover mark done and schedule next', async funct // 'unlink' on non-existing record raises a server crash throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); } - return this._super(...arguments); }, - env: { bus }, + }); + patchWithCleanup(env.services.action, { + doAction() { + assert.step('activity_action'); + throw new Error("The do-action event should not be triggered when the route doesn't return an action"); + }, }); await createChatterContainerComponent({ threadId: resPartnerId1, @@ -248,15 +247,6 @@ QUnit.test('activity mark done popover mark done and schedule next', async funct QUnit.test('[technical] activity mark done & schedule next with new action', async function (assert) { assert.expect(3); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { - assert.step('activity_action'); - assert.deepEqual( - action, - { type: 'ir.actions.act_window' }, - "The content of the action should be correct" - ); - }); const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create(); pyEnv['mail.activity'].create({ @@ -265,13 +255,21 @@ QUnit.test('[technical] activity mark done & schedule next with new action', asy res_id: resPartnerId1, res_model: 'res.partner', }); - const { click, createChatterContainerComponent } = await start({ - env: { bus }, + const { click, createChatterContainerComponent, env } = await start({ async mockRPC(route, args) { if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') { return { type: 'ir.actions.act_window' }; } - return this._super(...arguments); + }, + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step('activity_action'); + assert.deepEqual( + action, + { type: 'ir.actions.act_window' }, + "The content of the action should be correct" + ); }, }); await createChatterContainerComponent({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js index 03a3164064e..370b95a8fe3 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js @@ -2,8 +2,8 @@ import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; import { date_to_str } from 'web.time'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -602,8 +602,9 @@ QUnit.test('activity with mail template: preview mail', async function (assert) res_id: resPartnerId1, res_model: 'res.partner', }); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { createChatterContainerComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.context.default_res_id, @@ -634,8 +635,8 @@ QUnit.test('activity with mail template: preview mail', async function (assert) "mail.compose.message", 'Action should have "mail.compose.message" as res_model' ); + }, }); - const { createChatterContainerComponent } = await start({ env: { bus } }); await createChatterContainerComponent({ threadId: resPartnerId1, threadModel: 'res.partner', @@ -678,9 +679,8 @@ QUnit.test('activity with mail template: send mail', async function (assert) { assert.strictEqual(args.args[0].length, 1); assert.strictEqual(args.args[0][0], resPartnerId1); assert.strictEqual(args.args[1], mailTemplateId1); - return; - } else { - return this._super(...arguments); + // random value returned in order for the mock server to know that this route is implemented. + return true; } }, }); @@ -835,8 +835,9 @@ QUnit.test('activity click on edit', async function (assert) { res_id: resPartnerId1, res_model: 'res.partner', }); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { click, createChatterContainerComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.context.default_res_id, @@ -863,8 +864,9 @@ QUnit.test('activity click on edit', async function (assert) { mailActivityId1, 'Action should have activity id as res_id' ); + return this._super(...arguments); + }, }); - const { createChatterContainerComponent } = await start({ env: { bus } }); await createChatterContainerComponent({ threadId: resPartnerId1, threadModel: 'res.partner', @@ -880,7 +882,7 @@ QUnit.test('activity click on edit', async function (assert) { "should have activity edit button" ); - document.querySelector('.o_Activity_editButton').click(); + await click('.o_Activity_editButton'); assert.verifySteps( ['do_action'], "should have called 'schedule activity' action correctly" @@ -898,8 +900,9 @@ QUnit.test('activity edition', async function (assert) { res_id: resPartnerId1, res_model: 'res.partner', }); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + const { click, createChatterContainerComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('do_action'); assert.strictEqual( action.context.default_res_id, @@ -927,9 +930,9 @@ QUnit.test('activity edition', async function (assert) { 'Action should have activity id as res_id' ); pyEnv['mail.activity'].write([mailActivityId1], { icon: 'fa-check' }); - options.on_close(); + options.onClose(); + }, }); - const { click, createChatterContainerComponent } = await start({ env: { bus } }); await createChatterContainerComponent({ threadId: resPartnerId1, threadModel: 'res.partner', @@ -995,9 +998,6 @@ QUnit.test('activity click on cancel', async function (assert) { assert.step('unlink'); assert.strictEqual(args.args[0].length, 1); assert.strictEqual(args.args[0][0], mailActivityId1); - return; - } else { - return this._super(...arguments); } }, }); @@ -1109,8 +1109,20 @@ QUnit.test('activity mark done popover click on discard', async function (assert QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) { assert.expect(7); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv['res.partner'].create(); + const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; + pyEnv['mail.activity'].create({ + activity_category: 'default', + activity_type_id: emailActivityTypeId, + can_write: true, + note: `

some.model_250

`, + res_id: resPartnerId1, + res_model: 'res.partner', + }); + const { createChatterContainerComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.strictEqual( action.type, 'ir.actions.act_window', @@ -1127,19 +1139,8 @@ QUnit.test('data-oe-id & data-oe-model link redirection on click', async functio "action should open view on 250" ); assert.step('do-action:openFormView_some.model_250'); + }, }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create(); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - note: `

some.model_250

`, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { createChatterContainerComponent } = await start({ data: this.data, env: { bus } }); await createChatterContainerComponent({ threadId: resPartnerId1, threadModel: 'res.partner', diff --git a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js index 3fa20c72f99..6cf7bbeaea8 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js @@ -81,7 +81,6 @@ QUnit.test('simplest layout + editable', async function (assert) { "should fetch image with 200x200 pixels ratio"); assert.step('fetch_image'); } - return this._super(...arguments); }, }); const attachment = messaging.models['Attachment'].create({ @@ -248,9 +247,7 @@ QUnit.test('clicking on the delete attachment button multiple times should do th async mockRPC(route, args) { if (route === '/mail/attachment/delete') { assert.step('attachment_unlink'); - return; } - return this._super(...arguments); }, }); const attachment = messaging.models['Attachment'].create({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js index fc249484b7b..476e27b6f63 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js @@ -10,6 +10,7 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; +import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services"; import { file, dom } from 'web.test_utils'; const { createFile, inputFiles } = file; const { triggerEvent } = dom; @@ -231,12 +232,10 @@ QUnit.test('open chat from "new message" chat window should open chat in place o const imSearchDef = makeDeferred(); patchUiSize({ width: 1920 }); const { click, createMessagingMenuComponent } = await start({ - async mockRPC(route, args) { - const res = await this._super(...arguments); + mockRPC(route, args) { if (args.method === 'im_search') { imSearchDef.resolve(); } - return res; } }); await createMessagingMenuComponent(); @@ -381,12 +380,10 @@ QUnit.test('new message autocomplete should automatically select first result', pyEnv['res.users'].create({ partner_id: resPartnerId1 }); const imSearchDef = makeDeferred(); const { click, createMessagingMenuComponent } = await start({ - async mockRPC(route, args) { - const res = await this._super(...arguments); + mockRPC(route, args) { if (args.method === 'im_search') { imSearchDef.resolve(); } - return res; }, }); await createMessagingMenuComponent(); @@ -509,7 +506,6 @@ QUnit.test('chat window: fold', async function (assert) { if (args.method === 'channel_fold') { assert.step(`rpc:${args.method}/${args.kwargs.state}`); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); @@ -561,7 +557,6 @@ QUnit.test('chat window: open / close', async function (assert) { if (args.method === 'channel_fold') { assert.step(`rpc:channel_fold/${args.kwargs.state}`); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); @@ -732,7 +727,6 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) { if (args.method === 'channel_fold') { assert.step(`rpc:channel_fold/${args.kwargs.state}`); } - return this._super(...arguments); }, }); assert.containsOnce( @@ -2013,7 +2007,6 @@ QUnit.test('chat window does not fetch messages if hidden', async function (asse const { channel_id } = args; assert.step(`rpc:/mail/channel/messages:${channel_id}`); } - return this._super(...arguments); }, }); @@ -2280,21 +2273,16 @@ QUnit.test('Textual representations of shift previous/next operations are correc }, ]); await start({ - env: { - _t: Object.assign((s => s), { - database: { - parameters: { - code: "en_US", - date_format: '%m/%d/%Y', - decimal_point: ".", - direction: 'rtl', - grouping: [], - thousands_sep: ",", - time_format: '%H:%M:%S', - }, - }, + services: { + localization: makeFakeLocalizationService({ + dateFormat: '%m/%d/%Y', + timeFormat: '%H:%M:%S', + direction: 'rtl', + grouping: [], + thousandsSep: ',', + decimalPoint: '.', }), - } + }, }); assert.strictEqual( diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js index 5840b8cbf50..7dcde918118 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js @@ -235,7 +235,6 @@ QUnit.test("suggested recipients should not be notified when posting an internal "post data should not contain suggested recipients when posting an internal note" ); } - return this._super(...arguments); }, }); await createChatterContainerComponent({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js index ef1cb38dd0c..c3682aa0731 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js @@ -118,7 +118,6 @@ QUnit.test('attachment loading is delayed', async function (assert) { if (route.includes('/mail/thread/data')) { await makeTestPromise(); // simulate long loading } - return this._super(...arguments); } }); await createChatterContainerComponent({ @@ -160,7 +159,6 @@ QUnit.test('attachment counter while loading attachments', async function (asser if (route.includes('/mail/thread/data')) { await makeTestPromise(); // simulate long loading } - return this._super(...arguments); } }); await createChatterContainerComponent({ @@ -198,11 +196,9 @@ QUnit.test('attachment counter transition when attachments become loaded)', asyn const attachmentPromise = makeTestPromise(); const { createChatterContainerComponent } = await start({ async mockRPC(route) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route.includes('/mail/thread/data')) { await attachmentPromise; } - return _super(); }, }); await createChatterContainerComponent({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js index 6f66c5208d2..de68ab2b36e 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js @@ -11,7 +11,6 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import FormView from 'web.FormView'; import { file, makeTestPromise, @@ -21,32 +20,7 @@ const { createFile, inputFiles } = file; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { -QUnit.module('composer_tests.js', { - beforeEach() { - // FIXME archs could be removed once task-2248306 is done - // The mockServer will try to get the list view - // of every relational fields present in the main view. - // In the case of mail fields, we don't really need them, - // but they still need to be defined. - this.createView = async (viewParams, ...args) => { - const startResult = makeTestPromise(); - await afterNextRender(async () => { - const viewArgs = Object.assign( - { - archs: { - 'mail.activity,false,list': '', - 'mail.followers,false,list': '', - 'mail.message,false,list': '', - }, - }, - viewParams, - ); - startResult.resolve(await start(viewArgs, ...args)); - }); - return startResult; - }; - }, -}); +QUnit.module('composer_tests.js'); QUnit.test('composer text input: basic rendering when posting a message', async function (assert) { assert.expect(5); @@ -613,7 +587,6 @@ QUnit.test('do not send typing notification on typing "/" command', async functi if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -637,7 +610,6 @@ QUnit.test('do not send typing notification on typing after selecting suggestion if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -1050,7 +1022,6 @@ QUnit.test('composer text input cleared on message post', async function (assert if (route === '/mail/message/post') { assert.step('message_post'); } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1122,7 +1093,6 @@ QUnit.test('current partner notify is typing to other thread members', async fun if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -1152,7 +1122,6 @@ QUnit.test('current partner is typing should not translate on textual typing sta if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -1190,7 +1159,6 @@ QUnit.test('current partner notify no longer is typing to thread members after 5 if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -1227,7 +1195,6 @@ QUnit.test('current partner notify is typing again to other members every 50s of if (args.method === 'notify_typing') { assert.step(`notify_typing:${args.kwargs.is_typing}`); } - return this._super(...arguments); }, }); @@ -1259,12 +1226,10 @@ QUnit.test('composer: send button is disabled if attachment upload is not finish const attachmentUploadedPromise = makeTestPromise(); const mailChannelId1 = pyEnv['mail.channel'].create(); const { createComposerComponent, messaging } = await start({ - async mockFetch(resource, init) { - const res = this._super(...arguments); - if (resource === '/mail/attachment/upload') { + async mockRPC(route) { + if (route === '/mail/attachment/upload') { await attachmentUploadedPromise; } - return res; } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1373,13 +1338,11 @@ QUnit.test('remove an uploading attachment', async function (assert) { const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create(); const { click, createComposerComponent, messaging } = await start({ - async mockFetch(resource, init) { - const res = this._super(...arguments); - if (resource === '/mail/attachment/upload') { + async mockRPC(route) { + if (route === '/mail/attachment/upload') { // simulates uploading indefinitely await new Promise(() => {}); } - return res; } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1428,13 +1391,11 @@ QUnit.test('remove an uploading attachment aborts upload', async function (asser const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create(); const { afterEvent, createComposerComponent, messaging } = await start({ - async mockFetch(resource, init) { - const res = this._super(...arguments); - if (resource === '/mail/attachment/upload') { + async mockRPC(route) { + if (route === '/mail/attachment/upload') { // simulates uploading indefinitely await new Promise(() => {}); } - return res; } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1477,13 +1438,9 @@ QUnit.test("Show a default status in the recipient status text when the thread d const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create(); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -1491,9 +1448,15 @@ QUnit.test("Show a default status in the recipient status text when the thread d
- - `, + `, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); await click('.o_ChatterTopbar_buttonSendMessage'); assert.strictEqual( @@ -1508,13 +1471,9 @@ QUnit.test("Show a thread name in the recipient status text.", async function (a const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create({ name: "test name" }); - const { click, messaging } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -1522,9 +1481,15 @@ QUnit.test("Show a thread name in the recipient status text.", async function (a
- - `, + `, + }; + const { click, messaging, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); // hack: provide awareness of name (not received in usual chatter flow) messaging.models['Thread'].insert({ id: resPartnerId1, model: 'res.partner', name: "test name" }); @@ -1546,7 +1511,6 @@ QUnit.test('send message only once when button send is clicked twice quickly', a if (route === '/mail/message/post') { assert.step('message_post'); } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1578,12 +1542,10 @@ QUnit.test('[technical] does not crash when an attachment is removed before its const uploadPromise = makeTestPromise(); const mailChannelId1 = pyEnv['mail.channel'].create(); const { createComposerComponent, messaging } = await start({ - async mockFetch(resource) { - const _super = this._super.bind(this, ...arguments); - if (resource === '/mail/attachment/upload') { + async mockRPC(route) { + if (route === '/mail/attachment/upload') { await uploadPromise; } - return _super(); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js index 58798fb52d1..289dabb8390 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js @@ -7,7 +7,7 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -348,7 +348,6 @@ QUnit.test('"reply to" composer should log note if message replied to is a note' "should set subtype_xmlid as 'note'" ); } - return this._super(...arguments); }, waitUntilEvent: { eventName: 'o-thread-view-hint-processed', @@ -415,7 +414,6 @@ QUnit.test('"reply to" composer should send message if message replied to is not "should set subtype_xmlid as 'comment'" ); } - return this._super(...arguments); }, waitUntilEvent: { eventName: 'o-thread-view-hint-processed', @@ -601,8 +599,21 @@ QUnit.test('click on (non-channel/non-partner) origin thread link should redirec notification_type: 'inbox', res_partner_id: pyEnv.currentPartnerId, }); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { env } = await this.start({ + waitUntilEvent: { + eventName: 'o-thread-view-hint-processed', + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.box' && + threadViewer.thread.id === 'inbox' + ); + }, + }, + }); + patchWithCleanup(env.services.action, { + doAction(action) { // Callback of doing an action (action manager). // Expected to be called on click on origin thread link, // which redirects to form view of record related to origin thread @@ -627,21 +638,7 @@ QUnit.test('click on (non-channel/non-partner) origin thread link should redirec resFakeId1, "action should open view with id of resFake1 (id of message origin thread)" ); - }); - await this.start({ - env: { - bus, - }, - waitUntilEvent: { - eventName: 'o-thread-view-hint-processed', - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.box' && - threadViewer.thread.id === 'inbox' - ); - }, + return Promise.resolve(); }, }); assert.containsOnce( diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js index f1d66f75f9e..f807539a895 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js @@ -78,7 +78,6 @@ QUnit.test('sidebar: pinned channel 3: open channel and leave it', async functio "The right id is sent to the server to remove" ); } - return this._super(...arguments); }, }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js index 878daa9cc31..d96ebc02aab 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js @@ -25,11 +25,9 @@ QUnit.test('sidebar find shows channels matching search term', async function (a const { click } = await start({ autoOpenDiscuss: true, async mockRPC(route, args) { - const res = await this._super(...arguments); if (args.method === 'search_read') { searchReadDef.resolve(); } - return res; }, }); await click(`.o_DiscussSidebarCategory_commandAdd`); @@ -78,11 +76,9 @@ QUnit.test('sidebar find shows channels matching search term even when user is m const { click } = await start({ autoOpenDiscuss: true, async mockRPC(route, args) { - const res = await this._super(...arguments); if (args.method === 'search_read') { searchReadDef.resolve(); } - return res; }, }); await click(`.o_DiscussSidebarCategory_commandAdd`); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js index dfdcbd8f0bd..4de8ade642f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js @@ -10,7 +10,9 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; +import { destroy } from '@web/../tests/helpers/utils'; + import { makeTestPromise, file } from 'web.test_utils'; const { createFile, inputFiles } = file; @@ -59,9 +61,9 @@ QUnit.test('discuss should be marked as opened if the component is already rende QUnit.test('discuss should be marked as closed when the component is unmounted', async function (assert) { assert.expect(1); - const { messaging, widget } = await this.start(); + const { messaging, webClient } = await this.start(); - await afterNextRender(() => widget.destroy()); + await afterNextRender(() => destroy(webClient)); assert.notOk( messaging.discuss.discussView, "discuss should be marked as closed when the component is unmounted" @@ -73,11 +75,9 @@ QUnit.test('messaging not initialized', async function (assert) { await this.start({ async mockRPC(route) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await makeTestPromise(); // simulate messaging never initialized } - return _super(); }, waitUntilMessagingCondition: 'created', }); @@ -95,11 +95,9 @@ QUnit.test('messaging becomes initialized', async function (assert) { await this.start({ async mockRPC(route) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await messagingInitializedProm; } - return _super(); }, waitUntilMessagingCondition: 'created', }); @@ -1022,7 +1020,6 @@ QUnit.test('initially load messages from inbox', async function (assert) { "should fetch up to 30 messages" ); } - return this._super(...arguments); }, }); assert.verifySteps(['/mail/channel/messages']); @@ -1093,7 +1090,6 @@ QUnit.test('load single message from channel initially', async function (assert) "should fetch up to 30 messages" ); } - return this._super(...arguments); }, }); assert.strictEqual( @@ -1506,7 +1502,6 @@ QUnit.test('load all messages from channel initially, less than fetch limit (29 if (route === '/mail/channel/messages') { assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); } - return this._super(...arguments); }, }); assert.strictEqual( @@ -2442,7 +2437,6 @@ QUnit.test('toggle_star message', async function (assert) { "should have message Id in args" ); } - return this._super(...arguments); }, }); assert.strictEqual( @@ -2654,7 +2648,6 @@ QUnit.test('post a simple message', async function (assert) { const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create(); - let postedMessageId; const { click, insertText, messaging } = await this.start({ discuss: { params: { @@ -2662,7 +2655,6 @@ QUnit.test('post a simple message', async function (assert) { }, }, async mockRPC(route, args) { - const res = await this._super(...arguments); if (route === '/mail/message/post') { assert.step('message_post'); assert.strictEqual( @@ -2690,9 +2682,7 @@ QUnit.test('post a simple message', async function (assert) { "mail.mt_comment", "should set subtype_xmlid as 'comment'" ); - postedMessageId = res.id; } - return res; }, }); assert.strictEqual( @@ -2731,6 +2721,7 @@ QUnit.test('post a simple message', async function (assert) { 1, "should display a message after posting message" ); + const [postedMessageId] = pyEnv['mail.message'].search([], { order: 'id DESC' }); const message = document.querySelector(`.o_Message`); assert.strictEqual( message.dataset.messageLocalId, @@ -3127,22 +3118,19 @@ QUnit.test('reply to message from inbox (message linked to document)', async fun "should set message type as 'comment'" ); } - return this._super(...arguments); }, services: { - notification: { - notify(notification) { - assert.ok( - true, - "should display a notification after posting reply" - ); - assert.strictEqual( - notification.message, - "Message posted on \"Refactoring\"", - "notification should tell that message has been posted to the record 'Refactoring'" - ); - } - } + notification: makeFakeNotificationService(notification => { + assert.ok( + true, + "should display a notification after posting reply" + ); + assert.strictEqual( + notification, + "Message posted on \"Refactoring\"", + "notification should tell that message has been posted to the record 'Refactoring'" + ); + }), }, }); assert.strictEqual( @@ -3531,15 +3519,8 @@ QUnit.test('receive new chat message: out of odoo focus (notification, channel)' const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' }); - const bus = new Bus(); - bus.on('set_title_part', null, payload => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); - await this.start({ - env: { bus }, - services: { + const { env } = await this.start({ + legacyServices: { bus_service: BusService.extend({ _beep() {}, // Do nothing _poll() {}, // Do nothing @@ -3549,6 +3530,11 @@ QUnit.test('receive new chat message: out of odoo focus (notification, channel)' }), }, }); + env.bus.on('set_title_part', null, payload => { + assert.step('set_title_part'); + assert.strictEqual(payload.part, '_chat'); + assert.strictEqual(payload.title, "1 Message"); + }); const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; // simulate receiving a new message with odoo focused @@ -3570,15 +3556,8 @@ QUnit.test('receive new chat message: out of odoo focus (notification, chat)', a const pyEnv = await startServer(); const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: "chat" }); - const bus = new Bus(); - bus.on('set_title_part', null, payload => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); - await this.start({ - env: { bus }, - services: { + const { env } = await this.start({ + legacyServices: { bus_service: BusService.extend({ _beep() {}, // Do nothing _poll() {}, // Do nothing @@ -3588,6 +3567,11 @@ QUnit.test('receive new chat message: out of odoo focus (notification, chat)', a }), }, }); + env.bus.on('set_title_part', null, payload => { + assert.step('set_title_part'); + assert.strictEqual(payload.part, '_chat'); + assert.strictEqual(payload.title, "1 Message"); + }); const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; // simulate receiving a new message with odoo focused @@ -3613,8 +3597,18 @@ QUnit.test('receive new chat messages: out of odoo focus (tab title)', async fun { channel_type: 'chat', public: 'private' }, { channel_type: 'chat', public: 'private' }, ]); - const bus = new Bus(); - bus.on('set_title_part', null, payload => { + const { env } = await this.start({ + legacyServices: { + bus_service: BusService.extend({ + _beep() {}, // Do nothing + _poll() {}, // Do nothing + _registerWindowUnload() {}, // Do nothing + isOdooFocused: () => false, + updateOption() {}, + }), + }, + }); + env.bus.on('set_title_part', null, payload => { step++; assert.step('set_title_part'); assert.strictEqual(payload.part, '_chat'); @@ -3628,18 +3622,6 @@ QUnit.test('receive new chat messages: out of odoo focus (tab title)', async fun assert.strictEqual(payload.title, "3 Messages"); } }); - await this.start({ - env: { bus }, - services: { - bus_service: BusService.extend({ - _beep() {}, // Do nothing - _poll() {}, // Do nothing - _registerWindowUnload() {}, // Do nothing - isOdooFocused: () => false, - updateOption() {}, - }), - }, - }); const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; // simulate receiving a new message in chat 1 with odoo focused @@ -3885,30 +3867,26 @@ QUnit.test('warning on send with shortcut when attempting to post message with s active_id: `mail.channel_${mailChannelId1}`, }, }, - async mockFetch(resource, init) { - const res = this._super(...arguments); - if (resource === '/mail/attachment/upload') { + async mockRPC(route) { + if (route === '/mail/attachment/upload') { // simulates attachment is never finished uploading await new Promise(() => {}); } - return res; }, services: { - notification: { - notify(params) { - assert.strictEqual( - params.message, - "Please wait while the file is uploading.", - "notification content should be about the uploading file" - ); - assert.strictEqual( - params.type, - 'warning', - "notification should be a warning" - ); - assert.step('notification'); - } - } + notification: makeFakeNotificationService((message, options) => { + assert.strictEqual( + message, + "Please wait while the file is uploading.", + "notification content should be about the uploading file" + ); + assert.strictEqual( + options.type, + 'warning', + "notification should be a warning" + ); + assert.step('notification'); + }), }, }); const file = await createFile({ @@ -3963,7 +3941,6 @@ QUnit.test('send message only once when enter is pressed twice quickly', async f if (route === '/mail/message/post') { assert.step('message_post'); } - return this._super(...arguments); }, }); // Type message diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js index bbd2ddbb6ee..df459d8b544 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js @@ -1,54 +1,21 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; import { afterNextRender, start, startServer, } from '@mail/../tests/helpers/test_utils'; -import FormView from 'web.FormView'; - QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { -QUnit.module('follow_button_tests.js', { - beforeEach() { - // FIXME archs could be removed once task-2248306 is done - // The mockServer will try to get the list view - // of every relational fields present in the main view. - // In the case of mail fields, we don't really need them, - // but they still need to be defined. - this.createView = async (viewParams, ...args) => { - const startResult = makeDeferred(); - await afterNextRender(async () => { - const viewArgs = Object.assign( - { - archs: { - 'mail.activity,false,list': '', - 'mail.followers,false,list': '', - 'mail.message,false,list': '', - }, - }, - viewParams, - ); - startResult.resolve(await start(viewArgs, ...args)); - }); - return startResult; - }; - }, -}); +QUnit.module('follow_button_tests.js'); QUnit.test('base rendering not editable', async function (assert) { assert.expect(2); - const pyEnv = await startServer(); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -56,9 +23,15 @@ QUnit.test('base rendering not editable', async function (assert) {
- - `, + `, + }; + const { openView, pyEnv } = await start({ + serverData: { views }, + }); + await openView({ res_id: pyEnv.currentPartnerId, + res_model: 'res.partner', + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -86,13 +59,9 @@ QUnit.test('hover following button', async function (assert) { pyEnv['res.partner'].write([pyEnv.currentPartnerId], { message_follower_ids: [followerId], }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -100,9 +69,15 @@ QUnit.test('hover following button', async function (assert) {
- - `, + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ res_id: pyEnv.currentPartnerId, + res_model: 'res.partner', + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -156,14 +131,9 @@ QUnit.test('hover following button', async function (assert) { QUnit.test('click on "follow" button', async function (assert) { assert.expect(6); - const pyEnv = await startServer(); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -171,15 +141,20 @@ QUnit.test('click on "follow" button', async function (assert) {
- - `, - res_id: pyEnv.currentPartnerId, + `, + }; + const { click, openView, pyEnv } = await start({ async mockRPC(route, args) { if (route.includes('message_subscribe')) { assert.step('rpc:message_subscribe'); } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: 'res.partner', + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -222,13 +197,9 @@ QUnit.test('click on "unfollow" button', async function (assert) { pyEnv['res.partner'].write([pyEnv.currentPartnerId], { message_follower_ids: [followerId], }); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -236,15 +207,20 @@ QUnit.test('click on "unfollow" button', async function (assert) {
- - `, - res_id: pyEnv.currentPartnerId, + `, + }; + const { click, openView } = await start({ async mockRPC(route, args) { if (route.includes('message_unsubscribe')) { assert.step('rpc:message_unsubscribe'); } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: 'res.partner', + views: [[false, 'form']], }); assert.containsOnce( document.body, diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js index ce22535382d..76c428414f4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js @@ -1,55 +1,23 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; import { - afterNextRender, start, startServer, } from '@mail/../tests/helpers/test_utils'; import { nextTick } from 'web.test_utils'; -import FormView from 'web.FormView'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { -QUnit.module('follower_list_menu_tests.js', { - async beforeEach() { - // FIXME archs could be removed once task-2248306 is done - // The mockServer will try to get the list view - // of every relational fields present in the main view. - // In the case of mail fields, we don't really need them, - // but they still need to be defined. - this.createView = async (viewParams, ...args) => { - const startResult = makeDeferred(); - await afterNextRender(async () => { - const viewArgs = Object.assign( - { - archs: { - 'mail.activity,false,list': '', - 'mail.followers,false,list': '', - 'mail.message,false,list': '', - }, - }, - viewParams, - ); - startResult.resolve(await start(viewArgs, ...args)); - }); - return startResult; - }; - }, -}); +QUnit.module('follower_list_menu_tests.js'); QUnit.test('base rendering not editable', async function (assert) { assert.expect(5); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -57,12 +25,18 @@ QUnit.test('base rendering not editable', async function (assert) {
- - `, - viewOptions: { - mode: 'edit', - }, + `, + }; + const { openView } = await start({ + serverData: { views }, }); + await openView( + { + res_model: 'res.partner', + views: [[false, 'form']], + }, + { mode: 'edit' }, + ); assert.containsOnce( document.body, '.o_FollowerListMenu', @@ -99,14 +73,13 @@ QUnit.test('base rendering editable', async function (assert) { const resPartnerId1 = pyEnv['res.partner'].create(); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(route, args); res['hasWriteAccess'] = true; return res; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -159,8 +132,19 @@ QUnit.test('click on "add followers" button', async function (assert) { res_id: resPartnerId1, res_model: 'res.partner', }); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + + const { click, createChatterContainerComponent, env } = await start({ + async mockRPC(route, args, performRPC) { + if (route === '/mail/thread/data') { + // mimic user with write access + const res = await performRPC(route, args); + res['hasWriteAccess'] = true; + return res; + } + }, + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('action:open_view'); assert.strictEqual( action.context.default_res_model, @@ -190,19 +174,7 @@ QUnit.test('click on "add followers" button', async function (assert) { res_id: resPartnerId1, res_model: 'res.partner', }); - options.on_close(); - }); - - const { click, createChatterContainerComponent } = await start({ - env: { bus }, - async mockRPC(route, args) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await this._super(...arguments); - res['hasWriteAccess'] = true; - return res; - } - return this._super(...arguments); + options.onClose(); }, }); await createChatterContainerComponent({ @@ -284,10 +256,10 @@ QUnit.test('click on remove follower', async function (assert) { res_model: 'res.partner', }); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(route, args); res['hasWriteAccess'] = true; return res; } @@ -299,7 +271,6 @@ QUnit.test('click on remove follower', async function (assert) { "message_unsubscribe should be called with right argument" ); } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -353,14 +324,13 @@ QUnit.test('Hide "Add follower" and subtypes edition/removal buttons except own }, ]); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with no write access - const res = await this._super(...arguments); + const res = await performRPC(route, args); res['hasWriteAccess'] = false; return res; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -419,14 +389,13 @@ QUnit.test('Show "Add follower" and subtypes edition/removal buttons on all foll }, ]); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(...arguments); res['hasWriteAccess'] = true; return res; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -470,14 +439,13 @@ QUnit.test('Show "No Followers" dropdown-item if there are no followers and user const resPartnerId1 = pyEnv['res.partner'].create(); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user without write access - const res = await this._super(...arguments); + const res = await performRPC(route, args); res['hasWriteAccess'] = false; return res; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js index e067d671505..3c996a17962 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js @@ -1,42 +1,13 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; import { - afterNextRender, start, startServer, } from '@mail/../tests/helpers/test_utils'; -import FormView from 'web.FormView'; - QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { -QUnit.module('follower_subtype_tests.js', { - beforeEach() { - // FIXME archs could be removed once task-2248306 is done - // The mockServer will try to get the list view - // of every relational fields present in the main view. - // In the case of mail fields, we don't really need them, - // but they still need to be defined. - this.createView = async (viewParams, ...args) => { - const startResult = makeDeferred(); - await afterNextRender(async () => { - const viewArgs = Object.assign( - { - archs: { - 'mail.activity,false,list': '', - 'mail.followers,false,list': '', - 'mail.message,false,list': '', - }, - }, - viewParams, - ); - startResult.resolve(await start(viewArgs, ...args)); - }); - return startResult; - }; - }, -}); +QUnit.module('follower_subtype_tests.js'); QUnit.test('simplest layout of a followed subtype', async function (assert) { assert.expect(5); @@ -56,13 +27,9 @@ QUnit.test('simplest layout of a followed subtype', async function (assert) { pyEnv['res.partner'].write([pyEnv.currentPartnerId], { message_follower_ids: [followerId], }); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -70,19 +37,24 @@ QUnit.test('simplest layout of a followed subtype', async function (assert) {
- - `, - res_id: pyEnv.currentPartnerId, + `, + }; + const { click, openView } = await start({ // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(...arguments); res['hasWriteAccess'] = true; return res; } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + res_id: pyEnv.currentPartnerId, + views: [[false, 'form']], }); await click('.o_FollowerListMenu_buttonFollowers'); await click('.o_Follower_editButton'); @@ -129,13 +101,9 @@ QUnit.test('simplest layout of a not followed subtype', async function (assert) pyEnv['res.partner'].write([pyEnv.currentPartnerId], { message_follower_ids: [followerId], }); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -143,19 +111,24 @@ QUnit.test('simplest layout of a not followed subtype', async function (assert) - - `, - res_id: pyEnv.currentPartnerId, + `, + }; + const { click, openView } = await start({ // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(...arguments); res['hasWriteAccess'] = true; return res; } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + res_id: pyEnv.currentPartnerId, + views: [[false, 'form']], }); await click('.o_FollowerListMenu_buttonFollowers'); await click('.o_Follower_editButton'); @@ -182,13 +155,9 @@ QUnit.test('toggle follower subtype checkbox', async function (assert) { pyEnv['res.partner'].write([pyEnv.currentPartnerId], { message_follower_ids: [followerId], }); - const { click, messaging } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -196,19 +165,24 @@ QUnit.test('toggle follower subtype checkbox', async function (assert) { - - `, - res_id: pyEnv.currentPartnerId, + `, + }; + const { click, messaging, openView } = await start({ // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args) { + async mockRPC(route, args, performRPC) { if (route === '/mail/thread/data') { // mimic user with write access - const res = await this._super(...arguments); + const res = await performRPC(...arguments); res['hasWriteAccess'] = true; return res; } - return this._super(...arguments); }, + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + res_id: pyEnv.currentPartnerId, + views: [[false, 'form']], }); await click('.o_FollowerListMenu_buttonFollowers'); await click('.o_Follower_editButton'); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js index a5828178ee7..b2d0bdb828b 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js @@ -3,7 +3,7 @@ import { makeDeferred } from '@mail/utils/deferred'; import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -21,14 +21,13 @@ QUnit.test('base rendering not editable', async function (assert) { res_model: 'res.partner', }); const { click, createChatterContainerComponent } = await start({ - async mockRPC(route, args) { + async mockRPC(route, args, performRpc) { if (route === '/mail/thread/data') { // mimic user without write access - const res = await this._super(...arguments); + const res = await performRpc(...arguments); res['hasWriteAccess'] = false; return res; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -124,8 +123,13 @@ QUnit.test('click on partner follower details', async function (assert) { res_model: 'res.partner', }); const openFormDef = makeDeferred(); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { click, createChatterContainerComponent, env } = await start(); + await createChatterContainerComponent({ + threadId, + threadModel: 'res.partner', + }); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.res_id, @@ -143,11 +147,7 @@ QUnit.test('click on partner follower details', async function (assert) { "The redirect action should be of type 'ir.actions.act_window'" ); openFormDef.resolve(); - }); - const { click, createChatterContainerComponent } = await start({ env: { bus } }); - await createChatterContainerComponent({ - threadId, - threadModel: 'res.partner', + }, }); await click('.o_FollowerListMenu_buttonFollowers'); assert.containsOnce( @@ -185,7 +185,6 @@ QUnit.test('click on edit follower', async function (assert) { if (route.includes('/mail/read_subscription_data')) { assert.step('fetch_subtypes'); } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].create({ @@ -245,7 +244,6 @@ QUnit.test('edit follower and close subtype dialog', async function (assert) { res_model: 'res.partner' }]; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js index f3c7c1333c7..86439f030ff 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js @@ -9,7 +9,7 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -188,8 +188,9 @@ QUnit.test('Notification Error', async function (assert) { res_partner_id: resPartnerId1, }); const openResendActionDef = makeDeferred(); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + const { createThreadViewComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('do_action'); assert.strictEqual( action, @@ -202,8 +203,8 @@ QUnit.test('Notification Error', async function (assert) { "action should have correct message id" ); openResendActionDef.resolve(); + }, }); - const { createThreadViewComponent, messaging } = await start({ env: { bus } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ id: mailChannelId1, model: 'mail.channel', @@ -744,8 +745,9 @@ QUnit.test('subtype description should not be displayed if it is similar to body QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) { assert.expect(7); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { createMessageComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.strictEqual( action.type, 'ir.actions.act_window', @@ -762,8 +764,8 @@ QUnit.test('data-oe-id & data-oe-model link redirection on click', async functio "action should open view on 250" ); assert.step('do-action:openFormView_some.model_250'); + }, }); - const { createMessageComponent, messaging } = await start({ env: { bus } }); const message = messaging.models['Message'].create({ body: `

some.model_250

`, id: 100, @@ -961,7 +963,11 @@ QUnit.test('message should not be considered as "clicked" after clicking on noti notification_status: 'exception', notification_type: 'email', }); - const { createThreadViewComponent, messaging } = await start(); + const { createThreadViewComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + // intercept the action: this action is not relevant in the context of this test. + doAction() {}, + }); const threadViewer = messaging.models['ThreadViewer'].create({ hasThreadView: true, qunitTest: insertAndReplace(), diff --git a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js index 788a72075f2..5cf90e078ea 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js @@ -7,7 +7,8 @@ import { } from '@mail/../tests/helpers/test_utils'; import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; import { makeTestPromise } from 'web.test_utils'; @@ -56,7 +57,6 @@ QUnit.test('messaging not initialized', async function (assert) { // simulate messaging never initialized return new Promise(resolve => {}); } - return this._super(...arguments); }, waitUntilMessagingCondition: 'created', }); @@ -82,11 +82,9 @@ QUnit.test('messaging becomes initialized', async function (assert) { const { click, createMessagingMenuComponent } = await start({ async mockRPC(route) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js if (route === '/mail/init_messaging') { await messagingInitializedProm; } - return _super(); }, waitUntilMessagingCondition: 'created', }); @@ -403,7 +401,7 @@ QUnit.test('new message', async function (assert) { QUnit.test('no new message when discuss is open', async function (assert) { assert.expect(3); - const { click, discussWidget, createMessagingMenuComponent } = await start({ + const { click, createMessagingMenuComponent, openDiscuss, openView } = await start({ autoOpenDiscuss: true, }); await createMessagingMenuComponent(); @@ -415,16 +413,18 @@ QUnit.test('no new message when discuss is open', async function (assert) { "should not have 'new message' when discuss is open" ); - // simulate closing discuss app - await afterNextRender(() => discussWidget.on_detach_callback()); + await openView({ + res_model: 'res.partner', + views: [[false, 'form']], + }); + assert.strictEqual( document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, 1, "should have 'new message' when discuss is closed" ); - // simulate opening discuss app - await afterNextRender(() => discussWidget.on_attach_callback()); + await openDiscuss(); assert.strictEqual( document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, 0, @@ -916,16 +916,12 @@ QUnit.test('respond to notification prompt (denied)', async function (assert) { }, }); const { click, createMessagingMenuComponent } = await start({ - env: { - services: { - notification: { - notify() { - assert.step( - "should display a toast notification with the deny confirmation" - ); - } - } - } + services: { + notification: makeFakeNotificationService(() => { + assert.step( + "should display a toast notification with the deny confirmation" + ); + }), }, }); await createMessagingMenuComponent(); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js index 286cd7eae7b..6d29043052d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js @@ -2,7 +2,7 @@ import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -225,8 +225,10 @@ QUnit.test('grouped notifications by document model', async function (assert) { notification_type: 'email', // expected failure type for email message }, ]); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + + const { createNotificationListComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.name, @@ -263,9 +265,8 @@ QUnit.test('grouped notifications by document model', async function (assert) { JSON.stringify([['message_has_error', '=', true]]), "action should have 'message_has_error' as domain" ); + }, }); - - const { createNotificationListComponent } = await start({ env: { bus } }); await createNotificationListComponent(); assert.containsOnce( diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js index 7bd70f73b86..bd659c7c8d3 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js @@ -2,7 +2,7 @@ import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('mail', {}, function () { QUnit.module('components', {}, function () { @@ -38,7 +38,6 @@ QUnit.test('mark as read', async function (assert) { "should mark all as read the correct thread" ); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); @@ -98,7 +97,6 @@ QUnit.test('click on preview should mark as read and open the thread', async fun "should mark all as read the correct thread" ); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); @@ -150,8 +148,9 @@ QUnit.test('click on expand from chat window should close the chat window and op notification_type: 'inbox', res_partner_id: pyEnv.currentPartnerId, }); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { afterEvent, click, createMessagingMenuComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.res_id, @@ -163,9 +162,7 @@ QUnit.test('click on expand from chat window should close the chat window and op 'res.partner', "should redirect to the model of the thread" ); - }); - const { afterEvent, click, createMessagingMenuComponent } = await start({ - env: { bus }, + }, }); await createMessagingMenuComponent(); await afterNextRender(() => afterEvent({ @@ -233,7 +230,6 @@ QUnit.test('[technical] opening a non-channel chat window should not call channe console.error(message); throw Error(message); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_preview_tests.js index 9c070589ff8..c5c4614eb40 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_preview_tests.js @@ -34,7 +34,6 @@ QUnit.test('mark as read', async function (assert) { if (route.includes('set_last_seen_message')) { assert.step('set_last_seen_message'); } - return this._super(...arguments); }, }); await createMessagingMenuComponent(); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js index 091683fb183..e9e6b29037a 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js @@ -252,7 +252,6 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe ); assert.step('rpc:set_last_seen_message'); } - return this._super(...arguments); } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -307,7 +306,6 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com }); const mailChannelId1 = pyEnv['mail.channel'].create({}); const { afterEvent, createThreadViewComponent, messaging } = await start({ - data: this.data, mockRPC(route, args) { if (args.method === 'channel_fetched' && args.args[0] === mailChannelId1) { throw new Error("'channel_fetched' RPC must not be called for created channel as message is directly seen"); @@ -319,7 +317,6 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com ); assert.step('rpc:set_last_seen_message'); } - return this._super(...arguments); } }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -774,25 +771,20 @@ QUnit.test('should scroll to bottom on receiving new message if the list is init res_id: mailChannelId1, }); } - const { afterEvent, createThreadViewComponent, messaging } = await start(); + const { afterEvent, click, createMessagingMenuComponent, messaging } = await start(); const thread = messaging.models['Thread'].findFromIdentifyingData({ id: mailChannelId1, model: 'mail.channel' }); - const threadViewer = messaging.models['ThreadViewer'].create({ - hasThreadView: true, - qunitTest: insertAndReplace(), - thread: replace(thread), - }); + await createMessagingMenuComponent(); + await click(`.o_MessagingMenu_toggler`); await afterEvent({ eventName: 'o-component-message-list-scrolled', - func: () => createThreadViewComponent( - threadViewer.threadView, - undefined, - { isFixedSize: true }, - ), - message: "should wait until channel 20 scrolled initially", - predicate: data => threadViewer === data.threadViewer, + async func() { + await click(`.o_NotificationList_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: data => thread === data.threadViewer.thread, }); const initialMessageList = document.querySelector('.o_ThreadView_messageList'); assert.ok( @@ -814,8 +806,8 @@ QUnit.test('should scroll to bottom on receiving new message if the list is init uuid: thread.uuid, }, }), - message: "should wait until channel 20 scrolled after receiving a message", - predicate: data => threadViewer === data.threadViewer, + message: "should wait until channel scrolled after receiving a message", + predicate: data => thread === data.threadViewer.thread, }); const messageList = document.querySelector('.o_ThreadView_messageList'); assert.ok( @@ -844,25 +836,21 @@ QUnit.test('should not scroll on receiving new message if the list is initially res_id: mailChannelId1, }); } - const { afterEvent, createThreadViewComponent, messaging } = await start(); + const { afterEvent, click, createMessagingMenuComponent, messaging } = await start(); const thread = messaging.models['Thread'].findFromIdentifyingData({ id: mailChannelId1, model: 'mail.channel' }); - const threadViewer = messaging.models['ThreadViewer'].create({ - hasThreadView: true, - qunitTest: insertAndReplace(), - thread: replace(thread), - }); + + await createMessagingMenuComponent(); + await click(`.o_MessagingMenu_toggler`); await afterEvent({ eventName: 'o-component-message-list-scrolled', - func: () => createThreadViewComponent( - threadViewer.threadView, - undefined, - { isFixedSize: true }, - ), - message: "should wait until channel 1 scrolled initially", - predicate: data => threadViewer === data.threadViewer, + async func() { + await click(`.o_NotificationList_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: data => thread === data.threadViewer.thread, }); const initialMessageList = document.querySelector('.o_ThreadView_messageList'); assert.ok( @@ -874,7 +862,7 @@ QUnit.test('should not scroll on receiving new message if the list is initially eventName: 'o-component-message-list-scrolled', func: () => initialMessageList.scrollTop = 0, message: "should wait until channel 1 processed manual scroll", - predicate: data => threadViewer === data.threadViewer, + predicate: data => thread === data.threadViewer.thread, }); assert.strictEqual( initialMessageList.scrollTop, @@ -896,8 +884,8 @@ QUnit.test('should not scroll on receiving new message if the list is initially uuid: thread.uuid, }, }), - message: "should wait until channel 20 processed new message hint", - predicate: data => threadViewer === data.threadViewer && data.hint.type === 'message-received', + message: "should wait until channel processed new message hint", + predicate: data => thread === data.threadViewer.thread && data.hint.type === 'message-received', }); assert.strictEqual( document.querySelector('.o_ThreadView_messageList').scrollTop, @@ -1596,9 +1584,8 @@ QUnit.test('failure on loading messages should display error', async function (a const { createThreadViewComponent, messaging } = await start({ async mockRPC(route, args) { if (route === '/mail/channel/messages') { - throw new Error(); + return Promise.reject(); } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1630,9 +1617,8 @@ QUnit.test('failure on loading messages should prompt retry button', async funct const { createThreadViewComponent, messaging } = await start({ async mockRPC(route, args) { if (route === '/mail/channel/messages') { - throw new Error(); + return Promise.reject(); } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1679,7 +1665,6 @@ QUnit.test('failure on loading more messages should not alter message list displ throw new Error(); } } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1729,7 +1714,6 @@ QUnit.test('failure on loading more messages should display error and prompt ret throw new Error(); } } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ @@ -1785,10 +1769,9 @@ QUnit.test('Retry loading more messages on failed load more messages should load async mockRPC(route, args) { if (route === '/mail/channel/messages') { if (messageFetchShouldFail) { - throw new Error(); + return Promise.reject(); } } - return this._super(...arguments); }, }); const thread = messaging.models['Thread'].findFromIdentifyingData({ diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js index 57404962f14..2e698c2a938 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js @@ -3,13 +3,9 @@ import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user'; import { start, startServer } from '@mail/../tests/helpers/test_utils'; import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; -import { doAction } from '@web/../tests/webclient/helpers'; import { registry } from "@web/core/registry"; import { makeLegacyCommandService } from "@web/legacy/utils"; import core from 'web.core'; -import FormView from 'web.FormView'; -import KanbanView from 'web.KanbanView'; -import ListView from 'web.ListView'; import session from 'web.session'; import makeTestEnvironment from "web.test_env"; import { dom, nextTick } from 'web.test_utils'; @@ -32,14 +28,18 @@ QUnit.module('mail', {}, function () { const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const { widget: list } = await start({ - hasView: true, - View: ListView, - model: 'm2x.avatar.user', - arch: '', + const views = { + 'm2x.avatar.user,false,list': '', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', + views: [[false, "list"]], }); - await dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar > img')); + await dom.click(document.querySelector('.o_data_cell .o_m2o_avatar > img')); assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); assert.strictEqual( document.querySelector('.o_ChatWindowHeader_name').textContent, @@ -55,12 +55,16 @@ QUnit.module('mail', {}, function () { const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); - await start({ - hasView: true, - View: FormView, - model: 'm2x.avatar.user', - arch: '
', + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', res_id: m2xAvatarUserId1, + views: [[false, 'form']], }); await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); @@ -80,13 +84,9 @@ QUnit.module('mail', {}, function () { [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }, { name: "Tapu" }], ); pyEnv['m2x.avatar.user'].create({ user_ids: resUsersIds }); - - await start({ - hasView: true, - View: KanbanView, - model: 'm2x.avatar.user', - arch: ` - + const views = { + 'm2x.avatar.user,false,kanban': + `
@@ -102,6 +102,13 @@ QUnit.module('mail', {}, function () { `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', + views: [[false, 'kanban']], }); assert.containsOnce(document.body, '.o_kanban_record .o_field_many2manytags .o_m2m_avatar_empty', @@ -131,10 +138,9 @@ QUnit.module('mail', {}, function () { const views = { 'm2x.avatar.user,false,form': '
', - 'm2x.avatar.user,false,search': '', }; - const { widget: webClient } = await start({ hasWebClient: true, serverData: { views } }); - await doAction(webClient, { + const { openView } = await start({ serverData: { views } }); + await openView({ res_id: m2xAvatarUserId1, type: 'ir.actions.act_window', target: 'current', @@ -176,10 +182,9 @@ QUnit.module('mail', {}, function () { const views = { 'm2x.avatar.user,false,form': '
', - 'm2x.avatar.user,false,search': '', }; - const { widget: webClient } = await start({ hasWebClient: true, serverData: { views } }); - await doAction(webClient, { + const { openView } = await start({ serverData: { views } }); + await openView({ res_id: m2xAvatarUserId1, type: 'ir.actions.act_window', target: 'current', @@ -220,10 +225,9 @@ QUnit.module('mail', {}, function () { const views = { 'm2x.avatar.user,false,form': '
', - 'm2x.avatar.user,false,search': '', }; - const { widget: webClient } = await start({ hasWebClient: true, serverData: { views } }); - await doAction(webClient, { + const { openView } = await start({ serverData: { views } }); + await openView({ res_id: m2xAvatarUserId1, type: 'ir.actions.act_window', target: 'current', @@ -268,10 +272,9 @@ QUnit.module('mail', {}, function () { const views = { 'm2x.avatar.user,false,form': '
', - 'm2x.avatar.user,false,search': '', }; - const { widget: webClient } = await start({ hasWebClient: true, serverData: { views } }); - await doAction(webClient, { + const { openView } = await start({ serverData: { views } }); + await openView({ res_id: m2xAvatarUserId1, type: 'ir.actions.act_window', target: 'current', @@ -308,12 +311,16 @@ QUnit.module('mail', {}, function () { const pyEnv = await startServer(); const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - await start({ - hasView: true, - View: ListView, - model: 'm2x.avatar.user', - arch: '', + const views = { + 'm2x.avatar.user,false,list': '', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', res_id: m2xAvatarUserId1, + views: [[false, 'list']], }); assert.strictEqual( document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'), @@ -328,20 +335,25 @@ QUnit.module('mail', {}, function () { const pyEnv = await startServer(); const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - await start({ - hasView: true, - View: KanbanView, - model: 'm2x.avatar.user', - arch: ` - - -
- -
-
-
-
`, + const views = { + 'm2x.avatar.user,false,kanban': + ` + + +
+ +
+
+
+
`, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', res_id: m2xAvatarUserId1, + views: [[false, 'kanban']], }); assert.strictEqual( document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'), @@ -356,12 +368,16 @@ QUnit.module('mail', {}, function () { const pyEnv = await startServer(); const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); - await start({ - hasView: true, - View: FormView, - model: 'm2x.avatar.user', - arch: '
', + const views = { + 'm2x.avatar.user,false,form': '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'm2x.avatar.user', res_id: m2xAvatarUserId1, + views: [[false, 'form']], }); assert.strictEqual( document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), diff --git a/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js index 6fdb47a2f4f..37b15b676e6 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js @@ -2,6 +2,8 @@ import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; + QUnit.module('mail', {}, function () { QUnit.module('models', {}, function () { QUnit.module('messaging_tests.js', {}, function () { @@ -13,19 +15,17 @@ QUnit.test('openChat: display notification for partner without user', async func const resPartnerId1 = pyEnv['res.partner'].create(); const { messaging } = await start({ services: { - notification: { - notify(notification) { - assert.ok( - true, - "should display a toast notification after failing to open chat" - ); - assert.strictEqual( - notification.message, - "You can only chat with partners that have a dedicated user.", - "should display the correct information in the notification" - ); - }, - }, + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with partners that have a dedicated user.", + "should display the correct information in the notification" + ); + }), }, }); @@ -39,19 +39,17 @@ QUnit.test('openChat: display notification for wrong user', async function (asse pyEnv['res.users'].create(); const { messaging } = await start({ services: { - notification: { - notify(notification) { - assert.ok( - true, - "should display a toast notification after failing to open chat" - ); - assert.strictEqual( - notification.message, - "You can only chat with existing users.", - "should display the correct information in the notification" - ); - }, - }, + notification: makeFakeNotificationService(message => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with existing users.", + "should display the correct information in the notification" + ); + }), }, }); diff --git a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js index 5d4832284d8..3c2e86da808 100644 --- a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js @@ -28,7 +28,6 @@ QUnit.module('mail', {}, function () { const target = getFixture(); await start({ - hasWebClient: true, target, }); triggerHotkey("control+k"); @@ -53,7 +52,6 @@ QUnit.module('mail', {}, function () { }); const target = getFixture(); await start({ - hasWebClient: true, target, }); triggerHotkey("control+k"); diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js index 2f9a9a56989..848a07a0e8a 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js @@ -11,7 +11,6 @@ import { } from '@mail/../tests/helpers/test_utils'; import fieldRegistry from 'web.field_registry'; -import FormView from 'web.FormView'; import { dom, nextTick } from 'web.test_utils'; import { registerCleanup } from "@web/../tests/helpers/cleanup"; @@ -19,32 +18,7 @@ const { triggerEvent } = dom; QUnit.module('mail', {}, function () { QUnit.module('widgets', {}, function () { -QUnit.module('form_renderer_tests.js', { - async beforeEach() { - // FIXME archs could be removed once task-2248306 is done - // The mockServer will try to get the list view - // of every relational fields present in the main view. - // In the case of mail fields, we don't really need them, - // but they still need to be defined. - this.createView = async (viewParams, ...args) => { - const startResult = makeDeferred(); - await afterNextRender(async () => { - const viewArgs = Object.assign( - { - archs: { - 'mail.activity,false,list': '', - 'mail.followers,false,list': '', - 'mail.message,false,list': '', - }, - }, - viewParams, - ); - startResult.resolve(await start(viewArgs, ...args)); - }); - return startResult; - }; - }, -}); +QUnit.module('form_renderer_tests.js'); QUnit.test('[technical] spinner when messaging is not created', async function (assert) { /** @@ -61,22 +35,24 @@ QUnit.test('[technical] spinner when messaging is not created', async function ( const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner", }); - await this.createView({ - hasView: true, - messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation - waitUntilMessagingCondition: 'none', - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { openView } = await start({ + messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation + serverData: { views }, + waitUntilMessagingCondition: 'none', + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -111,29 +87,29 @@ QUnit.test('[technical] keep spinner on transition from messaging non-created to const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner", }); - await this.createView({ - hasView: true, - messagingBeforeCreationDeferred, - async mockRPC(route, args) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js - if (route === '/mail/init_messaging') { - await new Promise(() => {}); // simulate messaging never initialized - } - return _super(); - }, - waitUntilMessagingCondition: 'none', - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { openView } = await start({ + messagingBeforeCreationDeferred, + async mockRPC(route, args) { + if (route === '/mail/init_messaging') { + await new Promise(() => {}); // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: 'none', + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, @@ -168,28 +144,28 @@ QUnit.test('spinner when messaging is created but not initialized', async functi const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner", }); - await this.createView({ - hasView: true, - async mockRPC(route, args) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js - if (route === '/mail/init_messaging') { - await new Promise(() => {}); // simulate messaging never initialized - } - return _super(); - }, - waitUntilMessagingCondition: 'created', - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === '/mail/init_messaging') { + await new Promise(() => {}); // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: 'created', + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -216,28 +192,28 @@ QUnit.test('transition non-initialized messaging to initialized messaging: displ const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner", }); - await this.createView({ - hasView: true, - async mockRPC(route, args) { - const _super = this._super.bind(this, ...arguments); // limitation of class.js - if (route === '/mail/init_messaging') { - await messagingBeforeInitializationDeferred; - } - return _super(); - }, - waitUntilMessagingCondition: 'created', - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === '/mail/init_messaging') { + await messagingBeforeInitializationDeferred; + } + }, + serverData: { views }, + waitUntilMessagingCondition: 'created', + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.strictEqual( document.querySelector('.o_ChatterContainer').textContent, @@ -264,20 +240,22 @@ QUnit.test('basic chatter rendering', async function (assert) { const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.strictEqual( document.querySelectorAll(`.o_Chatter`).length, @@ -291,13 +269,9 @@ QUnit.test('basic chatter rendering without followers', async function (assert) const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -305,9 +279,15 @@ QUnit.test('basic chatter rendering without followers', async function (assert)
- - `, + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -346,13 +326,9 @@ QUnit.test('basic chatter rendering without activities', async function (assert) const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -360,9 +336,15 @@ QUnit.test('basic chatter rendering without activities', async function (assert) - - `, + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -401,13 +383,9 @@ QUnit.test('basic chatter rendering without messages', async function (assert) { const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -415,9 +393,15 @@ QUnit.test('basic chatter rendering without messages', async function (assert) { - - `, + `, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', res_id: resPartnerId1, + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -460,37 +444,36 @@ QUnit.test('chatter updating', async function (assert) { { display_name: "second partner" }, ]); pyEnv['mail.message'].create({ body: "not empty", model: 'res.partner', res_id: resPartnerId2 }); - const { afterEvent } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - viewOptions: { - ids: [resPartnerId1, resPartnerId2], - index: 0, - }, - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, - waitUntilEvent: { - eventName: 'o-thread-view-hint-processed', - message: "should wait until partner 11 thread loaded messages initially", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'res.partner' && - threadViewer.thread.id === resPartnerId1 - ); - }, - } + `, + }; + const { afterEvent, openView } = await start({ + serverData: { views }, + }); + const openViewAction = { + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }; + await afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => openView(openViewAction, { resIds: [resPartnerId1, resPartnerId2] }), + message: "should wait until partner 11 thread loaded messages initially", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'res.partner' && + threadViewer.thread.id === resPartnerId1 + ); + }, }); await afterNextRender(() => afterEvent({ eventName: 'o-thread-view-hint-processed', @@ -514,24 +497,23 @@ QUnit.test('chatter updating', async function (assert) { QUnit.test('chatter should become enabled when creation done', async function (assert) { assert.expect(10); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, - viewOptions: { - mode: 'edit', - }, + `, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + views: [[false, 'form']], }); assert.containsOnce( document.body, @@ -606,27 +588,30 @@ QUnit.test('read more/less links are not duplicated when switching from read to model: 'res.partner', res_id: resPartnerId1, }); - const { afterEvent } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, - waitUntilEvent: { - eventName: 'o-component-message-read-more-less-inserted', - message: "should wait until read more/less is inserted initially", - predicate: ({ message }) => message.id === mailMessageId1, - }, + `, + }; + const { afterEvent, openView } = await start({ + serverData: { views }, + }); + const openViewAction = { + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }; + await afterEvent({ + func: () => openView(openViewAction), + eventName: 'o-component-message-read-more-less-inserted', + message: "should wait until read more/less is inserted initially", + predicate: ({ message }) => message.id === mailMessageId1, }); assert.containsOnce( document.body, @@ -691,27 +676,30 @@ QUnit.test('read more links becomes read less after being clicked', async functi model: 'res.partner', res_id: resPartnerId1, }]); - const { afterEvent } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, - waitUntilEvent: { - eventName: 'o-component-message-read-more-less-inserted', - message: "should wait until read more/less is inserted initially", - predicate: ({ message }) => message.id === mailMessageId1, - }, + `, + }; + const { afterEvent, openView } = await start({ + serverData: { views }, + }); + const openViewAction = { + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }; + await afterEvent({ + func: () => openView(openViewAction), + eventName: 'o-component-message-read-more-less-inserted', + message: "should wait until read more/less is inserted initially", + predicate: ({ message }) => message.id === mailMessageId1, }); assert.containsOnce( document.body, @@ -775,15 +763,9 @@ QUnit.test('Form view not scrolled when switching record', async function (asser }; }); pyEnv['mail.message'].create(messages); - - patchUiSize({ size: SIZES.LG }); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` @@ -791,13 +773,22 @@ QUnit.test('Form view not scrolled when switching record', async function (asser
- - `, - viewOptions: { - currentId: resPartnerId1, - ids: [resPartnerId1, resPartnerId2], - }, + `, + }; + patchUiSize({ size: SIZES.LG }); + const { click, openView } = await start({ + serverData: { views }, }); + await openView( + { + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }, + { + resIds: [resPartnerId1, resPartnerId2], + } + ); const controllerContentEl = document.querySelector('.o_content'); @@ -861,27 +852,31 @@ QUnit.test('Attachments that have been unlinked from server should be visually u res_model: 'res.partner', }, ]); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - viewOptions: { - ids: [resPartnerId1, resPartnerId2], - index: 0, - }, - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { click, openView } = await start({ + serverData: { views }, }); + await openView( + { + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }, + { + currentId: resPartnerId1, + resIds: [resPartnerId1, resPartnerId2], + }, + ); assert.strictEqual( document.querySelector('.o_ChatterTopbar_buttonCount').textContent, '2', @@ -906,18 +901,24 @@ QUnit.test('chatter just contains "creating a new record" message during the cre const pyEnv = await startServer(); const resPartnerId1 = pyEnv['res.partner'].create(); - const { click } = await this.createView({ - hasView: true, - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - arch: ` -
+ const views = { + 'res.partner,false,form': + ` + + +
-
- `, + `, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], }); await click('.o_form_button_create'); @@ -964,22 +965,24 @@ QUnit.test('[TECHNICAL] unfolded read more/less links should not fold on message model: 'res.partner', res_id: resPartnerId1, }); - const { click } = await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - res_id: resPartnerId1, - arch: ` -
+ const views = { + 'res.partner,false,form': + `
-
- `, + `, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], }); assert.strictEqual( document.querySelector('.o_Message_readMoreLess').textContent, @@ -1022,24 +1025,23 @@ QUnit.test('chatter does not flicker when the form view is re-rendered', async f delete fieldRegistry.map.async_widget; }); - await this.createView({ - hasView: true, - // View params - View: FormView, - model: 'res.partner', - arch: ` -
+ const views = { + 'res.partner,false,form': + ` - +
-
- `, - viewOptions: { - currentId: resPartnerId1, - ids: [resPartnerId1, resPartnerId2], - }, + `, + }; + const { openView } = await start({ + serverData: { views }, }); + await openView({ + res_model: 'res.partner', + res_id: resPartnerId1, + views: [[false, 'form']], + }, { resIds: [resPartnerId1, resPartnerId2]}); assert.strictEqual( document.querySelectorAll(`.o_Chatter`).length, 1, diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js index d6109aa3a0a..ebd3a67fa3d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js @@ -3,36 +3,30 @@ import { start } from '@mail/../tests/helpers/test_utils'; import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; - -import FormView from 'web.FormView'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; QUnit.module('mail', {}, function () { QUnit.module('widgets', {}, function () { QUnit.module('notification_alert_tests.js'); -QUnit.skip('notification_alert widget: display blocked notification alert', async function (assert) { - // FIXME: Test should work, but for some reasons OWL always flags the - // component as not mounted, even though it is in the DOM and it's state - // is good for rendering... task-227947 +QUnit.test('notification_alert widget: display blocked notification alert', async function (assert) { assert.expect(1); + const views = { + 'mail.message,false,form': `
`, + }; patchWithCleanup(browser, { Notification: { ...browser.Notification, permission: 'denied', }, }); - await start({ - arch: ` -
- - - `, - hasView: true, - model: 'mail.message', - // View params - View: FormView, + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'mail.message', + views: [[false, 'form']], }); assert.containsOnce( @@ -45,21 +39,20 @@ QUnit.skip('notification_alert widget: display blocked notification alert', asyn QUnit.test('notification_alert widget: no notification alert when granted', async function (assert) { assert.expect(1); + const views = { + 'mail.message,false,form': `
`, + }; patchWithCleanup(browser, { Notification: { permission: 'granted', }, }); - await start({ - arch: ` -
- - - `, - hasView: true, - model: 'mail.message', - // View params - View: FormView, + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'mail.message', + views: [[false, 'form']], }); assert.containsNone( @@ -72,21 +65,20 @@ QUnit.test('notification_alert widget: no notification alert when granted', asyn QUnit.test('notification_alert widget: no notification alert when default', async function (assert) { assert.expect(1); + const views = { + 'mail.message,false,form': `
`, + }; patchWithCleanup(browser, { Notification: { permission: 'default', }, }); - await start({ - arch: ` -
- - - `, - hasView: true, - model: 'mail.message', - // View params - View: FormView, + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: 'mail.message', + views: [[false, 'form']], }); assert.containsNone( diff --git a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js index 2d81a67cf33..7af58fa351c 100644 --- a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js +++ b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js @@ -7,8 +7,6 @@ import { import tour from 'web_tour.tour'; -const { Component } = owl; - /** * This tour depends on data created by python test in charge of launching it. * It is not intended to work when launched from interface. It is needed to test @@ -33,7 +31,7 @@ tour.register('mail/static/tests/tours/mail_full_composer_test_tour.js', { contentType: 'text/plain', name: 'text.txt', }); - const messaging = await Component.env.services.messaging.get(); + const messaging = await odoo.__DEBUG__.messaging; const uploaders = messaging.models['FileUploader'].all(); inputFiles( uploaders[0].fileInput, diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml index a9794d0bda7..5bc0894c7fd 100644 --- a/addons/mail/views/mail_channel_views.xml +++ b/addons/mail/views/mail_channel_views.xml @@ -128,8 +128,7 @@ Discuss - mail.widgets.discuss - mail.channel + mail.action_discuss diff --git a/addons/note/static/tests/helpers/mock_server.js b/addons/note/static/tests/helpers/mock_server.js index b03178d4629..9b41cf2ffa1 100644 --- a/addons/note/static/tests/helpers/mock_server.js +++ b/addons/note/static/tests/helpers/mock_server.js @@ -2,10 +2,13 @@ // ensure mail override is applied first. import '@mail/../tests/helpers/mock_server'; -import MockServer from 'web.MockServer'; + +import { patch } from '@web/core/utils/patch'; +import { MockServer } from '@web/../tests/helpers/mock_server'; + import { date_to_str } from 'web.time'; -MockServer.include({ +patch(MockServer.prototype, 'note', { //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -13,7 +16,7 @@ MockServer.include({ /** * @override */ - async _performRpc(route, args) { + async _performRPC(route, args) { if (route === '/note/new') { return this._mockRouteNoteNew(args); } diff --git a/addons/note/static/tests/systray_activity_menu_tests.js b/addons/note/static/tests/systray_activity_menu_tests.js index 26c9567ef38..84d838e0daf 100644 --- a/addons/note/static/tests/systray_activity_menu_tests.js +++ b/addons/note/static/tests/systray_activity_menu_tests.js @@ -16,7 +16,7 @@ QUnit.test('note activity menu widget: create note from activity menu', async fu legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); - await start({ hasWebClient: true }); + await start(); assert.containsOnce(document.body, '.o_mail_systray_item', 'should contain an instance of widget'); await testUtils.nextTick(); diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 38f3689918a..85e4b1b4c31 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -113,7 +113,7 @@ # Remove Longpolling bus and packages needed this bus ('remove', 'bus/static/src/js/services/assets_watchdog_service.js'), - ('remove', 'mail/static/src/services/messaging/messaging.js'), + ('remove', 'mail/static/src/services/messaging_service.js'), ('remove', 'mail/static/src/components/dialog_manager/dialog_manager.js'), ('remove', 'mail/static/src/components/chat_window_manager/chat_window_manager.js'), diff --git a/addons/sms/static/src/models/message.js b/addons/sms/static/src/models/message.js index b26a1b92e86..1ff1604bac8 100644 --- a/addons/sms/static/src/models/message.js +++ b/addons/sms/static/src/models/message.js @@ -10,14 +10,14 @@ patchRecordMethods('Message', { */ openResendAction() { if (this.message_type === 'sms') { - this.env.bus.trigger('do-action', { - action: 'sms.sms_resend_action', - options: { + this.env.services.action.doAction( + 'sms.sms_resend_action', + { additional_context: { default_mail_message_id: this.id, }, }, - }); + ); } else { this._super(...arguments); } diff --git a/addons/sms/static/src/models/notification_group.js b/addons/sms/static/src/models/notification_group.js index cbc3fca0a85..47dba868aa9 100644 --- a/addons/sms/static/src/models/notification_group.js +++ b/addons/sms/static/src/models/notification_group.js @@ -12,17 +12,15 @@ patchRecordMethods('NotificationGroup', { if (this.notification_type !== 'sms') { return this._super(...arguments); } - this.env.bus.trigger('do-action', { - action: { - name: this.env._t("SMS Failures"), - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', - res_model: this.res_model, - domain: [['message_has_sms_error', '=', true]], - context: { create: false }, - }, + this.env.services.action.doAction({ + name: this.env._t("SMS Failures"), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: this.res_model, + domain: [['message_has_sms_error', '=', true]], + context: { create: false }, }); if (this.messaging.device.isSmall) { // messaging menu has a higher z-index than views so it must diff --git a/addons/sms/static/tests/qunit_suite_tests/components/message_tests.js b/addons/sms/static/tests/qunit_suite_tests/components/message_tests.js index 140424510de..1316023bd85 100644 --- a/addons/sms/static/tests/qunit_suite_tests/components/message_tests.js +++ b/addons/sms/static/tests/qunit_suite_tests/components/message_tests.js @@ -8,7 +8,7 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('sms', {}, function () { QUnit.module('components', {}, function () { @@ -113,8 +113,9 @@ QUnit.test('Notification Error', async function (assert) { notification_type: 'sms', res_partner_id: resPartnerId1, }); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + const { createThreadViewComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('do_action'); assert.strictEqual( action, @@ -127,8 +128,8 @@ QUnit.test('Notification Error', async function (assert) { "action should have correct message id" ); openResendActionDef.resolve(); + }, }); - const { createThreadViewComponent, messaging } = await start({ env: { bus } }); const threadViewer = messaging.models['ThreadViewer'].create({ hasThreadView: true, qunitTest: insertAndReplace(), diff --git a/addons/sms/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js b/addons/sms/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js index caec4220c22..3446c13a73a 100644 --- a/addons/sms/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js +++ b/addons/sms/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js @@ -2,7 +2,7 @@ import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('sms', {}, function () { QUnit.module('components', {}, function () { @@ -189,8 +189,9 @@ QUnit.test('grouped notifications by document model', async function (assert) { notification_type: 'sms', // expected failure type for sms message }, ]); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { createNotificationListComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.name, @@ -227,9 +228,9 @@ QUnit.test('grouped notifications by document model', async function (assert) { JSON.stringify([['message_has_sms_error', '=', true]]), "action should have 'message_has_sms_error' as domain" ); + }, }); - const { createNotificationListComponent } = await start({ env: { bus } }); await createNotificationListComponent(); assert.containsOnce( diff --git a/addons/snailmail/static/src/models/message.js b/addons/snailmail/static/src/models/message.js index 0c9d599e69d..2fa26471909 100644 --- a/addons/snailmail/static/src/models/message.js +++ b/addons/snailmail/static/src/models/message.js @@ -22,14 +22,14 @@ addRecordMethods('Message', { * Opens the action about 'snailmail.letter' format error. */ openFormatLetterAction() { - this.env.bus.trigger('do-action', { - action: 'snailmail.snailmail_letter_format_error_action', - options: { + this.env.services.action.doAction( + 'snailmail.snailmail_letter_format_error_action', + { additional_context: { message_id: this.id, }, }, - }); + ); }, /** * Opens the action about 'snailmail.letter' missing fields. @@ -40,14 +40,14 @@ addRecordMethods('Message', { method: 'search', args: [[['message_id', '=', this.id]]], }); - this.env.bus.trigger('do-action', { - action: 'snailmail.snailmail_letter_missing_required_fields_action', - options: { + this.env.services.action.doAction( + 'snailmail.snailmail_letter_missing_required_fields_action', + { additional_context: { default_letter_id: letterIds[0], }, - }, - }); + } + ); }, /** * Retries to send the 'snailmail.letter' corresponding to this message. diff --git a/addons/snailmail/static/src/models/notification_group.js b/addons/snailmail/static/src/models/notification_group.js index c5b135a617e..db8254cfaed 100644 --- a/addons/snailmail/static/src/models/notification_group.js +++ b/addons/snailmail/static/src/models/notification_group.js @@ -12,16 +12,14 @@ patchRecordMethods('NotificationGroup', { if (this.notification_type !== 'snail') { return this._super(...arguments); } - this.env.bus.trigger('do-action', { - action: { - name: this.env._t("Snailmail Failures"), - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', - res_model: this.res_model, - domain: [['message_ids.snailmail_error', '=', true]], - }, + this.env.services.action.doAction({ + name: this.env._t("Snailmail Failures"), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: this.res_model, + domain: [['message_ids.snailmail_error', '=', true]], }); if (this.messaging.device.isSmall) { // messaging menu has a higher z-index than views so it must diff --git a/addons/snailmail/static/tests/helpers/mock_server.js b/addons/snailmail/static/tests/helpers/mock_server.js index aa804f6a67b..e42cfe1746b 100644 --- a/addons/snailmail/static/tests/helpers/mock_server.js +++ b/addons/snailmail/static/tests/helpers/mock_server.js @@ -1,8 +1,11 @@ /** @odoo-module **/ -import MockServer from 'web.MockServer'; +import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first -MockServer.include({ +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; + +patch(MockServer.prototype, 'snailmail', { //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -10,7 +13,7 @@ MockServer.include({ /** * @override */ - async _performRpc(route, args) { + async performRPC(route, args) { if (args.model === 'mail.message' && args.method === 'cancel_letter') { const ids = args.args[0]; return this._mockMailMessageCancelLetter(ids); @@ -19,6 +22,10 @@ MockServer.include({ const ids = args.args[0]; return this._mockMailMessageSendLetter(ids); } + if (args.method === 'get_credits_url') { + // random value returned in order for the mock server to know that this route is implemented. + return true; + } return this._super(...arguments); }, @@ -34,6 +41,8 @@ MockServer.include({ */ _mockMailMessageCancelLetter(ids) { // TODO implement this mock and improve related tests (task-2300496) + // random value returned in order for the mock server to know that this route is implemented. + return true; }, /** * Simulates `send_letter` on `mail.message`. @@ -43,5 +52,7 @@ MockServer.include({ */ _mockMailMessageSendLetter(ids) { // TODO implement this mock and improve related tests (task-2300496) + // random value returned in order for the mock server to know that this route is implemented. + return true; }, }); diff --git a/addons/snailmail/static/tests/qunit_suite_tests/components/message_tests.js b/addons/snailmail/static/tests/qunit_suite_tests/components/message_tests.js index 735d5a32d6c..1de8671bdfc 100644 --- a/addons/snailmail/static/tests/qunit_suite_tests/components/message_tests.js +++ b/addons/snailmail/static/tests/qunit_suite_tests/components/message_tests.js @@ -7,7 +7,7 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('snailmail', {}, function () { QUnit.module('components', {}, async function() { @@ -274,7 +274,6 @@ QUnit.test('No Price Available', async function (assert) { if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === mailMessageId1) { assert.step(args.method); } - return this._super(...arguments); }, }); const threadViewer = messaging.models['ThreadViewer'].create({ @@ -368,7 +367,6 @@ QUnit.test('Credit Error', async function (assert) { if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === mailMessageId1) { assert.step(args.method); } - return this._super(...arguments); }, }); const threadViewer = messaging.models['ThreadViewer'].create({ @@ -467,7 +465,6 @@ QUnit.test('Trial Error', async function (assert) { if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === mailMessageId1) { assert.step(args.method); } - return this._super(...arguments); }, }); const threadViewer = messaging.models['ThreadViewer'].create({ @@ -561,8 +558,9 @@ QUnit.test('Format Error', async function (assert) { notification_type: 'snail', res_partner_id: resPartnerId1, }); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + const { createThreadViewComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('do_action'); assert.strictEqual( action, @@ -574,8 +572,8 @@ QUnit.test('Format Error', async function (assert) { mailMessageId1, "action should have correct message id" ); + }, }); - const { createThreadViewComponent, messaging } = await start({ env: { bus } }); const threadViewer = messaging.models['ThreadViewer'].create({ hasThreadView: true, qunitTest: insertAndReplace(), @@ -636,8 +634,10 @@ QUnit.test('Missing Required Fields', async function (assert) { const snailMailLetterId1 = pyEnv['snailmail.letter'].create({ message_id: mailMessageId1, }); - const bus = new Bus(); - bus.on('do-action', null, ({ action, options }) => { + + const { createThreadViewComponent, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('do_action'); assert.strictEqual( action, @@ -649,10 +649,7 @@ QUnit.test('Missing Required Fields', async function (assert) { snailMailLetterId1, "action should have correct letter id" ); - }); - - const { createThreadViewComponent, messaging } = await start({ - env: { bus }, + }, }); const threadViewer = messaging.models['ThreadViewer'].create({ hasThreadView: true, diff --git a/addons/snailmail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js b/addons/snailmail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js index 25b07430c26..2c9cc8c9ba9 100644 --- a/addons/snailmail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js +++ b/addons/snailmail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js @@ -2,7 +2,7 @@ import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import Bus from 'web.Bus'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; QUnit.module('snailmail', {}, function () { QUnit.module('components', {}, function () { @@ -189,8 +189,9 @@ QUnit.test('grouped notifications by document model', async function (assert) { notification_type: 'snail', // expected failure type for snailmail message }, ]); - const bus = new Bus(); - bus.on('do-action', null, ({ action }) => { + const { createNotificationListComponent, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { assert.step('do_action'); assert.strictEqual( action.name, @@ -227,9 +228,8 @@ QUnit.test('grouped notifications by document model', async function (assert) { JSON.stringify([['message_ids.snailmail_error', '=', true]]), "action should have 'message_has_sms_error' as domain" ); + }, }); - - const { createNotificationListComponent } = await start({ env: { bus } }); await createNotificationListComponent(); assert.containsOnce( diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js index a698a8e0fb3..d163fe8671d 100644 --- a/addons/test_mail/static/tests/activity_tests.js +++ b/addons/test_mail/static/tests/activity_tests.js @@ -1,11 +1,9 @@ /** @odoo-module **/ import ActivityRenderer from '@mail/js/views/activity/activity_renderer'; -import ActivityView from '@mail/js/views/activity/activity_view'; import { start, startServer } from '@mail/../tests/helpers/test_utils'; import testUtils from 'web.test_utils'; -import domUtils from 'web.dom'; import { legacyExtraNextTick, patchWithCleanup, click } from "@web/../tests/helpers/utils"; import { doAction } from "@web/../tests/webclient/helpers"; @@ -62,7 +60,7 @@ QUnit.module('activity view', { { name: 'Office planning', activity_ids: [mailActivityIds[1], mailActivityIds[2]] }, ]); serverData = { - views : { + views: { 'mail.test.activity,false,activity': '' + '' + @@ -71,9 +69,6 @@ QUnit.module('activity view', { '' + '' + '', - 'mail.activity,false,form': '
', - 'mail.test.activity,false,form': '', - 'mail.test.activity,false,search': '', }, }; } @@ -87,8 +82,17 @@ QUnit.test('activity view: simple activity rendering', async function (assert) { assert.expect(14); const mailTestActivityIds = pyEnv['mail.test.activity'].search([]); const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]); - const actionServiceInterceptor = event => { - assert.deepEqual(event.data.action, { + + const { env, openView } = await start({ + serverData, + }); + await openView({ + res_model: "mail.test.activity", + views: [[false, "activity"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.deepEqual(action, { context: { default_res_id: mailTestActivityIds[1], default_res_model: "mail.test.activity", @@ -103,25 +107,12 @@ QUnit.test('activity view: simple activity rendering', async function (assert) { views: [[false, "form"]] }, "should do a do_action with correct parameters"); - event.data.options.on_close(); - }; - await start({ - View: ActivityView, - hasView: true, - model: 'mail.test.activity', - arch: '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', - intercepts: { - do_action: actionServiceInterceptor, + options.onClose(); + return Promise.resolve(); }, }); - const $activity = $(document); + const $activity = $(document.querySelector('.o_activity_view')); assert.containsOnce($activity, 'table', 'should have a table'); var $th1 = $activity.find('table thead tr:first th:nth-child(2)'); @@ -158,20 +149,14 @@ QUnit.test('activity view: simple activity rendering', async function (assert) { QUnit.test('activity view: no content rendering', async function (assert) { assert.expect(2); + const { openView, pyEnv } = await start({ + serverData, + }); // reset incompatible setup pyEnv['mail.activity.type'].unlink(pyEnv['mail.activity.type'].search([])); - - await start({ - hasView: true, - View: ActivityView, - model: 'mail.test.activity', - arch: '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', + await openView({ + res_model: "mail.test.activity", + views: [[false, "activity"]], }); const $activity = $(document); @@ -187,25 +172,19 @@ QUnit.test('activity view: batch send mail on activity', async function (assert) const mailTestActivityIds = pyEnv['mail.test.activity'].search([]); const mailTemplateIds = pyEnv['mail.template'].search([]); - await start({ - hasView: true, - View: ActivityView, - model: 'mail.test.activity', - arch: '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', + const { openView } = await start({ + serverData, mockRPC: function(route, args) { - if (args.method === 'activity_send_mail'){ + if (args.method === 'activity_send_mail') { assert.step(JSON.stringify(args.args)); - return Promise.resolve(); + return Promise.resolve(true); } - return this._super.apply(this, arguments); }, }); + await openView({ + res_model: "mail.test.activity", + views: [[false, "activity"]], + }); const $activity = $(document); assert.notOk($activity.find('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show').length, 'dropdown shouldn\'t be displayed'); @@ -232,8 +211,33 @@ QUnit.test('activity view: activity widget', async function (assert) { const mailActivityTypeIds = pyEnv['mail.activity.type'].search([]); const [mailTestActivityId2] = pyEnv['mail.test.activity'].search([['name', '=', 'Office planning']]); const [mailTemplateId1] = pyEnv['mail.template'].search([['name', '=', 'Template1']]); - const actionServiceInterceptor = ev => { - var action = ev.data.action; + const { env, openView } = await start({ + mockRPC: function (route, args) { + if (args.method === 'activity_send_mail') { + assert.deepEqual([[mailTestActivityId2], mailTemplateId1], args.args, "Should send template related to mailTestActivity2"); + assert.step('activity_send_mail'); + // random value returned in order for the mock server to know that this route is implemented. + return true; + } + if (args.method === 'action_feedback_schedule_next') { + assert.deepEqual( + [pyEnv['mail.activity'].search([['state', '=', 'overdue']])], + args.args, + "Should execute action_feedback_schedule_next only on the overude activity" + ); + assert.equal(args.kwargs.feedback, "feedback2"); + assert.step('action_feedback_schedule_next'); + return Promise.resolve({ serverGeneratedAction: true }); + } + }, + serverData, + }); + await openView({ + res_model: 'mail.test.activity', + views: [[false, 'activity']], + }); + patchWithCleanup(env.services.action, { + doAction(action) { if (action.serverGeneratedAction) { assert.step('serverGeneratedAction'); } else if (action.res_model === 'mail.compose.message') { @@ -255,38 +259,10 @@ QUnit.test('activity view: activity widget', async function (assert) { } else { assert.step("Unexpected action"); } - }; - const params = { - hasView: true, - View: ActivityView, - model: 'mail.test.activity', - arch: '' + - '' + - '
' + - '' + - '
' + - '
'+ - '
', - mockRPC: function(route, args) { - if (args.method === 'activity_send_mail'){ - assert.deepEqual([[mailTestActivityId2],mailTemplateId1],args.args, "Should send template 8 on record 30"); - assert.step('activity_send_mail'); - return Promise.resolve(); - } - if (args.method === 'action_feedback_schedule_next'){ - assert.deepEqual([[3]],args.args, "Should execute action_feedback_schedule_next on activity 3 only "); - assert.equal(args.kwargs.feedback, "feedback2"); - assert.step('action_feedback_schedule_next'); - return Promise.resolve({serverGeneratedAction: true}); - } - return this._super.apply(this, arguments); + return Promise.resolve(); }, - intercepts: { - do_action: actionServiceInterceptor, - }, - }; + }); - await start(params); const activity = $(document); var today = activity.find('table tbody tr:first td:nth-child(2).today'); var dropdown = today.find('.dropdown-menu.o_activity'); @@ -319,7 +295,8 @@ QUnit.test('activity view: activity widget', async function (assert) { "do_action_activity", "action_feedback_schedule_next", "serverGeneratedAction" - ]); + ]); + }); QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) { @@ -347,7 +324,7 @@ QUnit.test("activity view: no group_by_menu and no comparison_menu", async funct patchWithCleanup(session.user_context, { lang: "zz_ZZ" }); - const { widget: webClient } = await start({ hasWebClient: true, serverData, mockRPC, legacyParams: {withLegacyMockServer: true}}); + const { webClient } = await start({ serverData, mockRPC }); await doAction(webClient, 1); @@ -373,7 +350,20 @@ QUnit.test('activity view: search more to schedule an activity for a record of a Object.assign(serverData.views, { 'mail.test.activity,false,list': '', }); - const actionServiceInterceptor = ev => { + const { env, openView } = await start({ + mockRPC(route, args) { + if (args.method === 'name_search') { + args.kwargs.name = "MailTestActivity"; + } + }, + serverData, + }); + await openView({ + res_model: 'mail.test.activity', + views: [[false, 'activity']], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { assert.step('doAction'); var expectedAction = { context: { @@ -388,43 +378,20 @@ QUnit.test('activity view: search more to schedule an activity for a record of a view_mode: "form", views: [[false, "form"]], }; - assert.deepEqual(ev.data.action, expectedAction, + assert.deepEqual(action, expectedAction, "should execute an action with correct params"); - ev.data.options.on_close(); - }; - await start({ - hasView: true, - View: ActivityView, - model: 'mail.test.activity', - arch: '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', - archs: { - "mail.test.activity,false,list": '', - "mail.test.activity,false,search": '', - }, - mockRPC: function(route, args) { - if (args.method === 'name_search') { - args.kwargs.name = "Task"; - } - return this._super.apply(this, arguments); - }, - intercepts: { - do_action: actionServiceInterceptor, + options.onClose(); + return Promise.resolve(); }, }); - const $activity = $(document); - assert.containsOnce($activity, 'table tfoot tr .o_record_selector', + const activity = $(document); + assert.containsOnce(activity, 'table tfoot tr .o_record_selector', 'should contain search more selector to choose the record to schedule an activity for it'); - await testUtils.dom.click($activity.find('table tfoot tr .o_record_selector')); + await testUtils.dom.click(activity.find('table tfoot tr .o_record_selector')); // search create dialog var $modal = $('.modal-lg'); - assert.strictEqual($modal.find('.o_data_row').length, 3, "all tasks should be available to select"); + assert.strictEqual($modal.find('.o_data_row').length, 3, "all mail.test.activity should be available to select"); // select a record to schedule an activity for it (this triggers a do_action) testUtils.dom.click($modal.find('.o_data_row:last')); assert.verifySteps(['doAction']); @@ -459,7 +426,7 @@ QUnit.test("Activity view: discard an activity creation dialog", async function } }; - const { widget: webClient } = await start({ hasWebClient: true, serverData, mockRPC, legacyParams: {withLegacyMockServer: true} }); + const { webClient } = await start({ serverData, mockRPC }); await doAction(webClient, 1); await testUtils.dom.click( @@ -503,7 +470,7 @@ QUnit.test('Activity view: many2one_avatar_user widget in activity view', async } }; - const { widget: webClient } = await start({ hasWebClient: true, serverData, legacyParams: { withLegacyMockServer: true } }); + const { webClient } = await start({ serverData }); await doAction(webClient, 1); await legacyExtraNextTick(); @@ -517,20 +484,6 @@ QUnit.test('Activity view: many2one_avatar_user widget in activity view', async QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (assert) { assert.expect(3); - const params = { - hasView: true, - View: ActivityView, - model: 'mail.test.activity', - data: this.data, - arch: ` - -
- -
-
-
`, - }; - patchWithCleanup(ActivityRenderer.prototype, { setup() { this._super(); @@ -543,8 +496,18 @@ QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (a } }); - const { widget: activity } = await start(params); - domUtils.detach([{ widget: activity }]); + const { openView } = await start({ + serverData, + }); + await openView({ + res_model: 'mail.test.activity', + views: [[false, 'activity']], + }); + // force the unmounting of the activity view by opening another one + await openView({ + res_model: 'mail.test.activity', + views: [[false, 'form']], + }); assert.verifySteps([ 'mounted', @@ -567,7 +530,7 @@ QUnit.test("Schedule activity dialog uses the same search view as activity view" } } - const { widget: webClient } = await start({ hasWebClient: true, serverData, mockRPC, legacyParams: {withLegacyMockServer: true} }); + const { webClient } = await start({ serverData, mockRPC }); // open an activity view (with default search arch) await doAction(webClient, { @@ -606,7 +569,7 @@ QUnit.test("Schedule activity dialog uses the same search view as activity view" assert.verifySteps([ '[[false,"list"],[1,"search"]]', - ]) + ]); }); QUnit.test('Activity view: apply progressbar filter', async function (assert) { @@ -622,7 +585,7 @@ QUnit.test('Activity view: apply progressbar filter', async function (assert) { } }; - const { widget: webClient } = await start({ hasWebClient:true, serverData, legacyParams: { withLegacyMockServer: true } }); + const { webClient } = await start({ serverData }); await doAction(webClient, 1); diff --git a/addons/test_mail/static/tests/systray_activity_menu_tests.js b/addons/test_mail/static/tests/systray_activity_menu_tests.js index 00690df9cd6..102ead63702 100644 --- a/addons/test_mail/static/tests/systray_activity_menu_tests.js +++ b/addons/test_mail/static/tests/systray_activity_menu_tests.js @@ -40,7 +40,6 @@ QUnit.test('activity menu widget: menu with no records', async function (assert) legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); await start({ - hasWebClient: true, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { return Promise.resolve([]); @@ -56,9 +55,7 @@ QUnit.test('activity menu widget: activity menu with 2 models', async function ( legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); - const { wowlEnv: env } = await start({ - hasWebClient: true, - }); + const { env } = await start(); await testUtils.nextTick(); assert.containsOnce(document.body, '.o_mail_systray_item', 'should contain an instance of widget'); @@ -113,9 +110,7 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) { patchWithCleanup(session, { uid: 10 }); legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); - const { wowlEnv: env } = await start({ - hasWebClient: true, - }); + const { env } = await start(); await testUtils.nextTick(); assert.containsN(document.body, '.o_mail_activity_action', 2, "widget should have 2 activity view icons"); @@ -167,9 +162,7 @@ QUnit.test('activity menu widget: close on messaging menu click', async function legacySystrayItems.push(ActivityMenu); registerCleanup(() => legacySystrayItems.pop()); - const { click, createMessagingMenuComponent } = await start({ - hasWebClient: true, - }); + const { click, createMessagingMenuComponent } = await start(); await createMessagingMenuComponent(); await testUtils.nextTick(); diff --git a/addons/test_mail/static/tests/tracking_value_tests.js b/addons/test_mail/static/tests/tracking_value_tests.js index 1fbc4bef69e..68b9ab5ac35 100644 --- a/addons/test_mail/static/tests/tracking_value_tests.js +++ b/addons/test_mail/static/tests/tracking_value_tests.js @@ -5,39 +5,54 @@ import { startServer, } from '@mail/../tests/helpers/test_utils'; -import FormView from 'web.FormView'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; + +import session from 'web.session'; import testUtils from 'web.test_utils'; QUnit.module('test_mail', {}, function () { QUnit.module('tracking_value_tests.js', { beforeEach() { - this.start = async params => { - return start({ - arch: ` - - - - - - - - - - - - -
- -
- `, - archs: { 'mail.message,false,list': '' }, - hasView: true, - model: 'mail.test.track.all', - View: FormView, - viewOptions: { mode: 'edit' }, - ...params, - }); + const views = { + 'mail.test.track.all,false,form': + `
+ + + + + + + + + + + + +
+ +
+
`, }; + this.start = async ({ res_id }) => { + const { openView, ...remainder } = await start({ + serverData: { views }, + }); + await openView( + { + res_model: 'mail.test.track.all', + res_id, + views: [[false, 'form']], + }, + { mode: 'edit' } + ); + return remainder; + }; + + patchWithCleanup(session, { + getTZOffset() { + return 0; + }, + }); }, }); diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 009622c79e0..7441e83839a 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -506,6 +506,7 @@ This module provides the core of the Odoo Web Client. 'web.qunit_suite_tests': [ 'base/static/tests/base_settings_tests.js', 'web/static/tests/env_tests.js', + 'web/static/tests/mock_relational_fields_tests.js', 'web/static/tests/core/**/*.js', 'web/static/tests/fields/**/*.js', 'web/static/tests/search/**/*.js', diff --git a/addons/web/static/src/core/browser/browser.js b/addons/web/static/src/core/browser/browser.js index d587af041fa..0223cc526d6 100644 --- a/addons/web/static/src/core/browser/browser.js +++ b/addons/web/static/src/core/browser/browser.js @@ -38,6 +38,8 @@ export const browser = { localStorage, sessionStorage, fetch: window.fetch.bind(window), + innerHeight: window.innerHeight, + innerWidth: window.innerWidth, }; Object.defineProperty(browser, "location", { diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index b88018538e0..2876df50ad8 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -32,6 +32,10 @@ function traverseElementTree(tree, cb) { export class MockServer { constructor(data, options = {}) { + this.init(data, options); + } + + init(data, options) { this.models = data.models || {}; this.actions = data.actions || {}; this.menus = data.menus || null; @@ -62,6 +66,15 @@ export class MockServer { model.onchanges = model.onchanges || {}; model.methods = model.methods || {}; }); + + // fill relational fields' inverse. + for (const modelName in this.models) { + const records = this.models[modelName].records + if (!Array.isArray(this.models[modelName].records)) { + continue; + } + records.forEach(record => this.updateComodelRelationalFields(modelName, record)); + } } /** @@ -478,6 +491,8 @@ export class MockServer { return this.mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs); case "search_read": return this.mockSearchRead(args.model, args.args, args.kwargs); + case "unlink": + return this.mockUnlink(args.model, args.args); case "web_search_read": return this.mockWebSearchRead(args.model, args.args, args.kwargs); case "read_group": @@ -505,6 +520,7 @@ export class MockServer { model.records.push(record); this.applyDefaults(model, values); this.writeRecord(modelName, values, id); + this.updateComodelRelationalFields(modelName, record); return id; } @@ -998,6 +1014,32 @@ export class MockServer { return readGroupResult; } + mockUnlink(modelName, args) { + var ids = args[0]; + if (!Array.isArray(ids)) { + ids = [ids]; + } + this.models[modelName].records = this.models[modelName].records.filter(record => !ids.includes(record.id)); + + // update value of relationnal fields pointing to the deleted records + _.each(this.models, function (d) { + var relatedFields = _.pick(d.fields, function (field) { + return field.relation === modelName; + }); + _.each(Object.keys(relatedFields), function (relatedField) { + _.each(d.records, function (record) { + if (Array.isArray(record[relatedField])) { + record[relatedField] = _.difference(record[relatedField], ids); + } else if (ids.includes(record[relatedField])) { + record[relatedField] = false; + } + }); + }); + }); + + return { result: true }; + } + mockWebReadGroup(modelName, kwargs) { const groups = this.mockReadGroup(modelName, kwargs); if (kwargs.expand && kwargs.groupby.length === 1) { @@ -1637,13 +1679,82 @@ export class MockServer { } mockWrite(modelName, args) { - args[0].forEach((id) => this.writeRecord(modelName, args[1], id)); + args[0].forEach((id) => { + const [originalRecord] = this.mockSearchRead(modelName, [[['id', '=', id]]], {}); + this.writeRecord(modelName, args[1], id); + const updatedRecord = this.models[modelName].records.find(record => record.id === id); + this.updateComodelRelationalFields(modelName, updatedRecord, originalRecord); + }); return true; } ////////////////////////////////////////////////////////////////////////////// // Private ////////////////////////////////////////////////////////////////////////////// + + /** + * Fill all inverse fields of the relational fields present in the record + * to be created/updated. + * + * @param {string} modelName + * @param {Object} record record that have been created/updated. + * @param {Object|undefined} originalRecord record before update. + */ + updateComodelRelationalFields(modelName, record, originalRecord) { + for (const fname in record) { + const field = this.models[modelName].fields[fname]; + const comodelName = field.relation || record[field['model_name_ref_fname']]; + const inverseFieldName = field['inverse_fname_by_model_name'] && field['inverse_fname_by_model_name'][comodelName]; + if (!inverseFieldName) { + // field has no inverse, skip it. + continue; + } + const relatedRecordIds = Array.isArray(record[fname]) ? record[fname] : [record[fname]]; + // we only want to set a value for comodel inverse field if the model field has a value. + if (record[fname]) { + for (const relatedRecordId of relatedRecordIds) { + let inverseFieldNewValue = record.id; + const relatedRecord = this.models[comodelName].records.find(record => record.id === relatedRecordId); + const relatedFieldValue = relatedRecord && relatedRecord[inverseFieldName]; + if ( + relatedFieldValue === undefined || + relatedFieldValue === record.id || + field.type !== 'one2many' && relatedFieldValue.includes(record.id) + ) { + // related record does not exist or the related value is already up to date. + continue; + } + if (Array.isArray(relatedFieldValue)) { + inverseFieldNewValue = [...relatedFieldValue, record.id]; + } + this.writeRecord(comodelName, { [inverseFieldName]: inverseFieldNewValue }, relatedRecordId); + } + } else if (field.type === 'many2one_reference') { + // we need to clean the many2one_field as well. + const comodel_inverse_field = this.models[comodelName].fields[inverseFieldName]; + const model_many2one_field = comodel_inverse_field['inverse_fname_by_model_name'][modelName]; + this.writeRecord(modelName, { [model_many2one_field]: false }, record.id); + } + // it's an update, get the records that were originally referenced but are not + // anymore and update their relational fields. + if (originalRecord) { + const originalRecordIds = Array.isArray(originalRecord[fname]) ? originalRecord[fname] : [originalRecord[fname]]; + // search read returns [id, name], let's ensure the removedRecordIds are integers. + const removedRecordIds = originalRecordIds.filter(recordId => Number.isInteger(recordId) && !relatedRecordIds.includes(recordId)); + for (const removedRecordId of removedRecordIds) { + const removedRecord = this.models[comodelName].records.find(record => record.id === removedRecordId); + if (!removedRecord) { + continue; + } + let inverseFieldNewValue = false; + if (Array.isArray(removedRecord[inverseFieldName])) { + inverseFieldNewValue = removedRecord[inverseFieldName].filter(id => id !== record.id); + } + this.writeRecord(comodelName, { [inverseFieldName]: inverseFieldNewValue }, removedRecordId); + } + } + } + } evaluateDomain(domain, record) { return new Domain(domain).contains(record); } @@ -1841,7 +1952,12 @@ export class MockServer { for (const command of value || []) { if (command[0] === 0) { // CREATE - const newId = this.mockCreate(field.relation, command[2]); + const inverseData = command[2]; // write in place instead of copy, because some tests rely on the object given being updated + const inverseFieldName = field.inverse_fname_by_model_name && field.inverse_fname_by_model_name[field.relation]; + if (inverseFieldName) { + inverseData[inverseFieldName] = id; + } + const newId = this.mockCreate(field.relation, inverseData); ids.push(newId); } else if (command[0] === 1) { // UPDATE diff --git a/addons/web/static/tests/helpers/mock_services.js b/addons/web/static/tests/helpers/mock_services.js index b697ff3e429..06913a4c43f 100644 --- a/addons/web/static/tests/helpers/mock_services.js +++ b/addons/web/static/tests/helpers/mock_services.js @@ -120,9 +120,8 @@ export function makeMockXHR(response, sendCb, def) { export function makeMockFetch(mockRPC) { const _rpc = buildMockRPC(mockRPC); - return async (input) => { + return async (input, params) => { let route = typeof input === "string" ? input : input.url; - let params; if (route.includes("load_menus")) { const routeArray = route.split("/"); params = { diff --git a/addons/web/static/tests/mock_relational_fields_tests.js b/addons/web/static/tests/mock_relational_fields_tests.js new file mode 100644 index 00000000000..2b4b8313099 --- /dev/null +++ b/addons/web/static/tests/mock_relational_fields_tests.js @@ -0,0 +1,135 @@ +/** @odoo-module **/ + +import { MockServer } from "@web/../tests/helpers/mock_server"; + +QUnit.module('web', {}, function () { +QUnit.module('mock_relational_fields_tests.js', { + beforeEach() { + this.models = { + foo: { + fields: { + one2many_field: { type: 'one2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2one_field' } }, + many2one_field: { type: 'many2one', relation: 'bar', inverse_fname_by_model_name: { bar: 'one2many_field' } }, + many2many_field: { type: 'many2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2many_field' } }, + many2one_reference: { type: 'many2one_reference', model_name_ref_fname: 'res_model', inverse_fname_by_model_name: { bar: 'one2many_field' } }, + res_model: { type: 'char' }, + + }, + records: [], + }, + bar: { + fields: { + many2one_field: { type: 'many2one', relation: 'foo' }, + one2many_field: { type: 'one2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2one_field' } }, + many2many_field: { type: 'many2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2many_field' } }, + }, + records: [], + }, + }; + } +}); + +QUnit.test('many2one_ref should auto fill inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + res_model: 'bar', + many2one_reference: 1, + }); + const mockServer = new MockServer({ models: this.models }); + assert.deepEqual([2], mockServer.models['bar'].records[0].one2many_field); + + mockServer.mockUnlink('foo', [2]); + assert.deepEqual([], mockServer.models['bar'].records[0].one2many_field); +}); + +QUnit.test('many2one should auto fill inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + many2one_field: 1, + }); + const mockServer = new MockServer({ models: this.models }); + assert.deepEqual([2], mockServer.models['bar'].records[0].one2many_field); + + mockServer.mockUnlink('foo', [2]); + assert.deepEqual([], mockServer.models['bar'].records[0].one2many_field); +}); + +QUnit.test('one2many should auto fill inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['bar'].records.push({ id: 2 }); + this.models['foo'].records.push({ + id: 3, + one2many_field: [1, 2], + }); + const mockServer = new MockServer({ models: this.models }); + assert.strictEqual(3, mockServer.models['bar'].records[0].many2one_field); + assert.strictEqual(3, mockServer.models['bar'].records[1].many2one_field); + + mockServer.mockUnlink('foo', [3]); + assert.strictEqual(false, mockServer.models['bar'].records[0].many2one_field); + assert.strictEqual(false, mockServer.models['bar'].records[1].many2one_field); +}); + +QUnit.test('many2many should auto fill inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + many2many_field: [1], + }); + const mockServer = new MockServer({ models: this.models }); + assert.deepEqual([2], mockServer.models['bar'].records[0].many2many_field); + + mockServer.mockUnlink('foo', [2]); + assert.deepEqual([], mockServer.models['bar'].records[0].many2many_field); +}); + +QUnit.test('one2many update should update inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['bar'].records.push({ id: 2 }); + this.models['foo'].records.push({ + id: 3, + one2many_field: [1, 2], + }); + const mockServer = new MockServer({ models: this.models }); + mockServer.mockWrite('foo', [[3], { one2many_field: [1] }]); + assert.strictEqual(3, mockServer.models['bar'].records[0].many2one_field); + assert.strictEqual(false, mockServer.models['bar'].records[1].many2one_field); +}); + +QUnit.test('many2many update should update inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + many2many_field: [1], + }); + const mockServer = new MockServer({ models: this.models }); + mockServer.mockWrite('foo', [[2], { many2many_field: [] }]); + assert.deepEqual([], mockServer.models['bar'].records[0].many2many_field); +}); + +QUnit.test('many2one update should update inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + many2one_field: 1, + }); + const mockServer = new MockServer({ models: this.models }); + mockServer.mockWrite('foo', [[2], { many2one_field: false }]); + assert.deepEqual([], mockServer.models['bar'].records[0].one2many_field); +}); + +QUnit.test('many2one_ref update should update inverse field', async function (assert) { + this.models['bar'].records.push({ id: 1 }); + this.models['foo'].records.push({ + id: 2, + res_model: 'bar', + many2one_reference: 1, + }); + const mockServer = new MockServer({ models: this.models }); + mockServer.mockWrite('foo', [[2], { many2one_reference: false }]); + assert.deepEqual([], mockServer.models['bar'].records[0].one2many_field); +}); + +}); diff --git a/addons/web/static/tests/webclient/helpers.js b/addons/web/static/tests/webclient/helpers.js index 1c572300361..266b93726f8 100644 --- a/addons/web/static/tests/webclient/helpers.js +++ b/addons/web/static/tests/webclient/helpers.js @@ -194,6 +194,11 @@ export async function addLegacyMockEnvironment(env, legacyParams = {}) { const legacyActionManagerService = makeLegacyActionManagerService(legacyEnv); serviceRegistry.add("legacy_action_manager", legacyActionManagerService); serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv)); + // deploy the legacyServices given in parameter in WowlEnv + const { legacyServicesToDeployInWowlEnv } = legacyParams; + for (const legacyServiceName in legacyServicesToDeployInWowlEnv) { + serviceRegistry.add(legacyServiceName, legacyServicesToDeployInWowlEnv[legacyServiceName]); + } // patch DebouncedField delay const debouncedField = basicFields.DebouncedField; const initialDebouncedVal = debouncedField.prototype.DEBOUNCE; diff --git a/addons/website_livechat/static/tests/helpers/mock_server.js b/addons/website_livechat/static/tests/helpers/mock_server.js index a184b131693..e00c4a36546 100644 --- a/addons/website_livechat/static/tests/helpers/mock_server.js +++ b/addons/website_livechat/static/tests/helpers/mock_server.js @@ -2,9 +2,19 @@ import '@im_livechat/../tests/helpers/mock_server'; // ensure mail overrides are applied first -import MockServer from 'web.MockServer'; +import { patch } from "@web/core/utils/patch"; +import { MockServer } from "@web/../tests/helpers/mock_server"; -MockServer.include({ +patch(MockServer.prototype, 'website_livechat', { + /** + * @override + */ + async _performRPC(route, args) { + if (route === '/web/dataset/call_button') { + return this._mockCallButton(args); + } + return this._super(route, args); + }, /** * Simulate a 'call_button' operation from a view. * diff --git a/addons/website_livechat/static/tests/qunit_suite_tests/messaging_notification_handler_tests.js b/addons/website_livechat/static/tests/qunit_suite_tests/messaging_notification_handler_tests.js index 155e84df8b7..8f37622d46c 100644 --- a/addons/website_livechat/static/tests/qunit_suite_tests/messaging_notification_handler_tests.js +++ b/addons/website_livechat/static/tests/qunit_suite_tests/messaging_notification_handler_tests.js @@ -5,9 +5,10 @@ import { start, startServer, } from '@mail/../tests/helpers/test_utils'; +import { ViewAdapter } from '@web/legacy/action_adapters'; +import { patchWithCleanup } from '@web/../tests/helpers/utils'; import FormView from 'web.FormView'; -import { mock } from 'web.test_utils'; QUnit.module('website_livechat', {}, function () { QUnit.module('messaging_notification_handler_tests.js'); @@ -19,31 +20,38 @@ QUnit.test('should open chat window on send chat request to website visitor', as const websiteVisitorId1 = pyEnv['website.visitor'].create({ display_name: "Visitor #11", }); - const { env, widget } = await start({ - hasView: true, - // View params - View: FormView, - model: 'website.visitor', - arch: ` -
+ const views = { + 'website.visitor,false,form': + `
- - `, - res_id: websiteVisitorId1, + `, + }; + const { openView } = await start({ + serverData: { views }, + View: FormView, }); - mock.intercept(widget, 'execute_action', payload => { - env.services.rpc({ - route: '/web/dataset/call_button', - params: { - args: [payload.data.env.resIDs], - kwargs: { context: payload.data.env.context }, - method: payload.data.action_data.name, - model: payload.data.env.model, + await openView({ + res_model: 'website.visitor', + res_id: websiteVisitorId1, + views: [[false, 'form']], + }); + patchWithCleanup(ViewAdapter.prototype, { + _trigger_up: async function({ name, data }) { + if (name === 'execute_action') { + this.env.services.rpc({ + route: '/web/dataset/call_button', + params: { + args: [data.env.resIDs], + kwargs: { context: data.env.context }, + method: data.action_data.name, + model: data.env.model, + } + }); } - }); + }, }); await afterNextRender(() => diff --git a/addons/website_slides/static/tests/qunit_suite_tests/components/activity_tests.js b/addons/website_slides/static/tests/qunit_suite_tests/components/activity_tests.js index 9f168f9114b..7ab46259d18 100644 --- a/addons/website_slides/static/tests/qunit_suite_tests/components/activity_tests.js +++ b/addons/website_slides/static/tests/qunit_suite_tests/components/activity_tests.js @@ -26,8 +26,9 @@ QUnit.test('grant course access', async function (assert) { assert.strictEqual(args.args[0][0], slideChannelId1); assert.strictEqual(args.kwargs.partner_id, resPartnerId1); assert.step('access_grant'); + // random value returned in order for the mock server to know that this route is implemented. + return true; } - return this._super(...arguments); }, }); await createChatterContainerComponent({ @@ -62,8 +63,9 @@ QUnit.test('refuse course access', async function (assert) { assert.strictEqual(args.args[0][0], slideChannelId1); assert.strictEqual(args.kwargs.partner_id, resPartnerId1); assert.step('access_refuse'); + // random value returned in order for the mock server to know that this route is implemented. + return true; } - return this._super(...arguments); }, }); await createChatterContainerComponent({