[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:
@@ -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;
|
||||
|
||||
@@ -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, {
|
||||
|
||||
+1
-4
@@ -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"
|
||||
/>
|
||||
|
||||
+1
-4
@@ -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();
|
||||
}
|
||||
|
||||
+1
-4
@@ -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"),
|
||||
});
|
||||
|
||||
+1
-4
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-7
@@ -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>
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
+23
-25
@@ -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({
|
||||
|
||||
+12
-24
@@ -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(
|
||||
|
||||
-1
@@ -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();
|
||||
|
||||
+6
-5
@@ -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(
|
||||
|
||||
+5
-9
@@ -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,
|
||||
|
||||
@@ -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=""{ 'default_active_id': 'mail.box_inbox' }""/>
|
||||
</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();
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+5
-4
@@ -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,
|
||||
|
||||
+5
-5
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user