[IMP] mail, *: use wowlEnv instead of the legacy one

*: 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) <seb@odoo.com>
This commit is contained in:
tsm-odoo
2022-05-30 19:40:53 +02:00
parent 2f20ec501a
commit 3573a7fef4
104 changed files with 2088 additions and 2179 deletions
+1
View File
@@ -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',
@@ -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);
@@ -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
//--------------------------------------------------------------------------
@@ -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"]'));
@@ -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: '<tree><field name="employee_id" widget="many2one_avatar_employee"/></tree>',
const views = {
'm2x.avatar.employee,false,list': '<tree><field name="employee_id" widget="many2one_avatar_employee"/></tree>',
};
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: `
<kanban>
const views = {
'm2x.avatar.employee,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
@@ -135,6 +133,13 @@ QUnit.module('hr', {}, function () {
</t>
</templates>
</kanban>`,
};
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: '<form><field name="employee_id" widget="many2one_avatar_employee"/></form>',
const views = {
'm2x.avatar.employee,false,form': '<form><field name="employee_id" widget="many2one_avatar_employee"/></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: '<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
const views = {
'm2x.avatar.employee,false,form': '<form><field name="employee_ids" widget="many2many_avatar_employee"/></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: '<tree><field name="employee_ids" widget="many2many_avatar_employee"/></tree>',
const views = {
'm2x.avatar.employee,false,list': '<tree><field name="employee_ids" widget="many2many_avatar_employee"/></tree>',
};
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: `
<kanban>
const views = {
'm2x.avatar.employee,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
@@ -350,14 +352,20 @@ QUnit.module('hr', {}, function () {
</t>
</templates>
</kanban>`,
};
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: '<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
const views = {
'm2x.avatar.employee,false,form': '<form><field name="employee_ids" widget="many2many_avatar_employee"/></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");
@@ -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;
},
});
});
@@ -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;
+3 -3
View File
@@ -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',
@@ -26,8 +26,8 @@
</div>
<div class="o-autogrow"/>
<div class="o_AttachmentViewer_buttonDownload o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton d-flex align-items-center px-3 o_cursor_pointer" t-on-click="attachmentViewer.onClickDownload" role="button" title="Download">
<i class="o_AttachmentViewer_headerItemButtonIcon fa fa-download fa-fw" t-att-class="{ 'o-hasLabel me-2': messaging.device.sizeClass > env.device.SIZES.MD }" role="img"/>
<t t-if="messaging.device.sizeClass > env.device.SIZES.MD">
<i class="o_AttachmentViewer_headerItemButtonIcon fa fa-download fa-fw" t-att-class="{ 'o-hasLabel me-2': messaging.device.sizeClass > messaging.device.sizeClasses.MD }" role="img"/>
<t t-if="messaging.device.sizeClass > messaging.device.sizeClasses.MD">
<span>Download</span>
</t>
</div>
@@ -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, {
@@ -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();
}
@@ -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',
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.DiscussContainer" owl="1">
<div class="o_DiscussContainer d-flex flex-grow-1 flex-column">
<div class="o_DiscussContainer h-100 d-flex flex-grow-1 flex-column">
<Discuss t-if="messaging and messaging.discuss and messaging.discuss.discussView and messaging.isInitialized" className="'flex-grow-1'" record="messaging.discuss.discussView"/>
<div t-else="" class="o_DiscussContainer_spinner d-flex flex-grow-1 align-items-center justify-content-center">
<i class="o_DiscussContainer_spinnerIcon fa fa-circle-o-notch fa-spin me-2"/>Please wait...
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.DiscussPublicView" owl="1">
<t t-if="discussPublicView">
<div class="o_DiscussPublicView d-flex flex-column h-100" t-attf-class="{{ className }}" t-ref="root">
<div class="o_DiscussPublicView d-flex flex-column h-100 w-100 position-absolute" t-attf-class="{{ className }}" t-ref="root">
<ThreadView t-if="discussPublicView.threadView"
record="discussPublicView.threadView"
/>
@@ -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();
}
@@ -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();
}
@@ -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"),
});
@@ -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();
}
+1 -1
View File
@@ -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',
+16 -7
View File
@@ -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 });
@@ -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.
+7 -7
View File
@@ -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
+5 -5
View File
@@ -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.
@@ -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.
+8 -3
View File
@@ -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,
}),
},
});
+4 -4
View File
@@ -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
},
});
);
}
},
/**
@@ -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']],
});
},
/**
@@ -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',
});
},
/**
@@ -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,
+4 -2
View File
@@ -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: {
@@ -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
+5 -5
View File
@@ -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.
+39 -13
View File
@@ -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`.
@@ -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',
@@ -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
@@ -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
+6 -6
View File
@@ -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
@@ -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,
});
@@ -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();
})();
@@ -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();
}
});
@@ -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);
},
};
@@ -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 });
},
});
};
@@ -0,0 +1,11 @@
/** @odoo-module **/
export function makeMessagingToLegacyEnv(legacyEnv) {
return {
dependencies: ['messaging'],
start(_, { messaging }) {
legacyEnv.services.messaging = messaging;
},
};
}
@@ -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,
@@ -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;
},
});
@@ -1,10 +0,0 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_widget_Discuss {
display: flex;
flex: 0 0 100%;
flex-flow: column;
height: 100%;
}
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
@param {mail/static/src/widgets/discuss/discuss.js} widget
-->
<t t-name="mail.widgets.Discuss">
<div class="o_widget_Discuss"/>
</t>
</templates>
+49 -113
View File
@@ -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);
},
});
+74 -323
View File
@@ -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': '<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,
};
}
@@ -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;
}
};
}
@@ -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,
};
@@ -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: '<list><field name="activity_ids" widget="list_activity"/></list>',
mockRPC: function (route) {
if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) {
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></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': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
await start({
hasView: true,
View: ListView,
model: 'res.users',
arch: '<list><field name="activity_ids" widget="list_activity"/></list>',
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: '<list><field name="activity_ids" widget="list_activity"/></list>',
mockRPC: function (route) {
if (!['/mail/init_messaging', '/mail/load_message_failures'].includes(route)) {
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></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: `
<list>
<field name="activity_ids" widget="list_activity"/>
</list>`,
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></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: '<tree>' +
'<field name="activity_exception_decoration" widget="activity_exception"/> ' +
'</tree>',
const views = {
'res.users,false,list':
`<tree>
<field name="activity_exception_decoration" widget="activity_exception"/>
</tree>`,
};
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: `
<list>
const views = {
'res.users,false,list':
`<list>
<field name="activity_ids" widget="list_activity"/>
</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: `
<list>
const views = {
'res.users,false,list':
`<list>
<field name="activity_ids" widget="list_activity"/>
</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: '<form string="Partners">' +
const views = {
'mail.message,false,form':
'<form string="Partners">' +
'<sheet>' +
'<field name="body"/>' +
'<field name="partner_ids" widget="many2many_tags_email"/>' +
'</sheet>' +
'</form>',
viewOptions: {
mode: 'edit',
},
'res.partner,false,form': '<form string="Types"><field name="name"/><field name="email"/></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': '<form string="Types"><field name="name"/><field name="email"/></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: '<form><field name="partner_ids" widget="many2many_tags"/></form>',
const views = {
'mail.message,false,form': '<form><field name="partner_ids" widget="many2many_tags"/></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);
@@ -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({
@@ -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: `<p><a href="#" data-oe-id="250" data-oe-model="some.model">some.model_250</a></p>`,
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: `<p><a href="#" data-oe-id="250" data-oe-model="some.model">some.model_250</a></p>`,
res_id: resPartnerId1,
res_model: 'res.partner',
});
const { createChatterContainerComponent } = await start({ data: this.data, env: { bus } });
await createChatterContainerComponent({
threadId: resPartnerId1,
threadModel: 'res.partner',
@@ -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({
@@ -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(
@@ -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({
@@ -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({
@@ -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': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -1491,9 +1448,15 @@ QUnit.test("Show a default status in the recipient status text when the thread d
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -1522,9 +1481,15 @@ QUnit.test("Show a thread name in the recipient status text.", async function (a
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
`,
</form>`,
};
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({
@@ -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(
@@ -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);
},
});
@@ -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`);
@@ -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
@@ -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': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -56,9 +23,15 @@ QUnit.test('base rendering not editable', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -100,9 +69,15 @@ QUnit.test('hover following button', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -171,15 +141,20 @@ QUnit.test('click on "follow" button', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -236,15 +207,20 @@ QUnit.test('click on "unfollow" button', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
</form>`,
};
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,
@@ -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': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -57,12 +25,18 @@ QUnit.test('base rendering not editable', async function (assert) {
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
`,
viewOptions: {
mode: 'edit',
},
</form>`,
};
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({
@@ -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': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -70,19 +37,24 @@ QUnit.test('simplest layout of a followed subtype', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -143,19 +111,24 @@ QUnit.test('simplest layout of a not followed subtype', async function (assert)
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -196,19 +165,24 @@ QUnit.test('toggle follower subtype checkbox', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
res_id: pyEnv.currentPartnerId,
</form>`,
};
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');
@@ -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({
@@ -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: `<p><a href="#" data-oe-id="250" data-oe-model="some.model">some.model_250</a></p>`,
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(),
@@ -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();
@@ -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(
@@ -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();
@@ -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();
@@ -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({
@@ -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: '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
const views = {
'm2x.avatar.user,false,list': '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
};
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: '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></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: `
<kanban>
const views = {
'm2x.avatar.user,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
@@ -102,6 +102,13 @@ QUnit.module('mail', {}, function () {
</t>
</templates>
</kanban>`,
};
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': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
'm2x.avatar.user,false,search': '<search></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': '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
'm2x.avatar.user,false,search': '<search></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': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
'm2x.avatar.user,false,search': '<search></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': '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
'm2x.avatar.user,false,search': '<search></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: '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
const views = {
'm2x.avatar.user,false,list': '<tree><field name="user_id" widget="many2one_avatar_user"/></tree>',
};
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: `<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="user_id" widget="many2one_avatar_user"/>
</div>
</t>
</templates>
</kanban>`,
const views = {
'm2x.avatar.user,false,kanban':
`<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="user_id" widget="many2one_avatar_user"/>
</div>
</t>
</templates>
</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: '<form><field name="user_ids" widget="many2many_avatar_user"/></form>',
const views = {
'm2x.avatar.user,false,form': '<form><field name="user_ids" widget="many2many_avatar_user"/></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'),
@@ -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"
);
}),
},
});
@@ -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");
@@ -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': '<tree/>',
'mail.followers,false,list': '<tree/>',
'mail.message,false,list': '<tree/>',
},
},
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -305,9 +279,15 @@ QUnit.test('basic chatter rendering without followers', async function (assert)
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -360,9 +336,15 @@ QUnit.test('basic chatter rendering without activities', async function (assert)
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
@@ -415,9 +393,15 @@ QUnit.test('basic chatter rendering without messages', async function (assert) {
<field name="message_follower_ids"/>
<field name="activity_ids"/>
</div>
</form>
`,
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</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
);
},
}
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
`,
viewOptions: {
mode: 'edit',
},
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</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,
},
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</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,
},
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
<field name="description"/>
@@ -791,13 +773,22 @@ QUnit.test('Form view not scrolled when switching record', async function (asser
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
`,
viewOptions: {
currentId: resPartnerId1,
ids: [resPartnerId1, resPartnerId2],
},
</form>`,
};
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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
`,
</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: `
<form>
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
`,
</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: `
<form string="Partners">
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>
`,
</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: `
<form>
const views = {
'res.partner,false,form':
`<form string="Partners">
<sheet>
<field name="name" widget="async_widget"/>
<field name="name"/>
</sheet>
<div class="oe_chatter"></div>
</form>
`,
viewOptions: {
currentId: resPartnerId1,
ids: [resPartnerId1, resPartnerId2],
},
</form>`,
};
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,
@@ -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': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
...browser.Notification,
permission: 'denied',
},
});
await start({
arch: `
<form>
<widget name="notification_alert"/>
</form>
`,
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': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
permission: 'granted',
},
});
await start({
arch: `
<form>
<widget name="notification_alert"/>
</form>
`,
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': `<form><widget name="notification_alert"/></form>`,
};
patchWithCleanup(browser, {
Notification: {
permission: 'default',
},
});
await start({
arch: `
<form>
<widget name="notification_alert"/>
</form>
`,
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(
@@ -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,
+1 -2
View File
@@ -128,8 +128,7 @@
<record id="action_discuss" model="ir.actions.client">
<field name="name">Discuss</field>
<field name="tag">mail.widgets.discuss</field>
<field name="res_model">mail.channel</field>
<field name="tag">mail.action_discuss</field>
<field name="params" eval="&quot;{ 'default_active_id': 'mail.box_inbox' }&quot;"/>
</record>
@@ -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);
}
@@ -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();
+1 -1
View File
@@ -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'),
+4 -4
View File
@@ -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);
}
@@ -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
@@ -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(),
@@ -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(
@@ -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.
@@ -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
@@ -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;
},
});
@@ -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,
@@ -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(
+99 -136
View File
@@ -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':
'<activity string="MailTestActivity">' +
'<templates>' +
@@ -71,9 +69,6 @@ QUnit.module('activity view', {
'</div>' +
'</templates>' +
'</activity>',
'mail.activity,false,form': '<form/>',
'mail.test.activity,false,form': '<form/>',
'mail.test.activity,false,search': '<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: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>' +
'</activity>',
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: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>' +
'</activity>',
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: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>' +
'</activity>',
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: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>'+
'</activity>',
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': '<tree string="MailTestActivity"><field name="name"/></tree>',
});
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: '<activity string="Task">' +
'<templates>' +
'<div t-name="activity-box">' +
'<field name="name"/>' +
'</div>' +
'</templates>' +
'</activity>',
archs: {
"mail.test.activity,false,list": '<tree string="Task"><field name="name"/></tree>',
"mail.test.activity,false,search": '<search></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: `<activity string="Task">
<templates>
<div t-name="activity-box">
<field name="name"/>
</div>
</templates>
</activity>`,
};
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);
@@ -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();
@@ -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: `<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</form>`,
archs: { 'mail.message,false,list': '<tree/>' },
hasView: true,
model: 'mail.test.track.all',
View: FormView,
viewOptions: { mode: 'edit' },
...params,
});
const views = {
'mail.test.track.all,false,form':
`<form>
<sheet>
<field name="boolean_field"/>
<field name="char_field"/>
<field name="date_field"/>
<field name="datetime_field"/>
<field name="float_field"/>
<field name="integer_field"/>
<field name="monetary_field"/>
<field name="many2one_field_id"/>
<field name="selection_field"/>
<field name="text_field"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids"/>
</div>
</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;
},
});
},
});
+1
View File
@@ -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',
@@ -38,6 +38,8 @@ export const browser = {
localStorage,
sessionStorage,
fetch: window.fetch.bind(window),
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
};
Object.defineProperty(browser, "location", {
+118 -2
View File
@@ -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
@@ -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 = {
@@ -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);
});
});

Some files were not shown because too many files have changed in this diff Show More