Initial commit: Mail packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 4e53507711
1948 changed files with 751201 additions and 0 deletions

View file

@ -0,0 +1,159 @@
/** @odoo-module **/
import { makeDeferred } from '@mail/utils/deferred';
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('sms', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('message_tests.js');
QUnit.test('Notification Sent', async function (assert) {
assert.expect(9);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone", partner_share: true });
const mailMessageId1 = pyEnv['mail.message'].create({
body: 'not empty',
message_type: 'sms',
model: 'res.partner',
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'sent',
notification_type: 'sms',
res_partner_id: resPartnerId1,
});
const { openFormView } = await start();
await openFormView({
res_id: resPartnerId1,
res_model: 'res.partner',
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a message component"
);
assert.containsOnce(
document.body,
'.o_Message_notificationIconClickable',
"should display the notification icon container"
);
assert.containsOnce(
document.body,
'.o_Message_notificationIcon',
"should display the notification icon"
);
assert.hasClass(
document.querySelector('.o_Message_notificationIcon'),
'fa-mobile',
"icon should represent sms"
);
await afterNextRender(() => {
document.querySelector('.o_Message_notificationIconClickable').click();
});
assert.containsOnce(
document.body,
'.o_MessageNotificationPopoverContent',
"notification popover should be open"
);
assert.containsOnce(
document.body,
'.o_MessageNotificationPopoverContent_notificationIcon',
"popover should have one icon"
);
assert.hasClass(
document.querySelector('.o_MessageNotificationPopoverContent_notificationIcon'),
'fa-check',
"popover should have the sent icon"
);
assert.containsOnce(
document.body,
'.o_MessageNotificationPopoverContent_notificationPartnerName',
"popover should have the partner name"
);
assert.strictEqual(
document.querySelector('.o_MessageNotificationPopoverContent_notificationPartnerName').textContent.trim(),
"Someone",
"partner name should be correct"
);
});
QUnit.test('Notification Error', async function (assert) {
assert.expect(8);
const openResendActionDef = makeDeferred();
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone", partner_share: true });
const mailMessageId1 = pyEnv['mail.message'].create({
body: 'not empty',
message_type: 'sms',
model: 'res.partner',
res_id: resPartnerId1,
});
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'sms',
res_partner_id: resPartnerId1,
});
const { env, openFormView } = await start();
await openFormView({
res_id: resPartnerId1,
res_model: 'res.partner',
});
patchWithCleanup(env.services.action, {
doAction(action, options) {
assert.step('do_action');
assert.strictEqual(
action,
'sms.sms_resend_action',
"action should be the one to resend sms"
);
assert.strictEqual(
options.additionalContext.default_mail_message_id,
mailMessageId1,
"action should have correct message id"
);
openResendActionDef.resolve();
},
});
assert.containsOnce(
document.body,
'.o_Message',
"should display a message component"
);
assert.containsOnce(
document.body,
'.o_Message_notificationIconClickable',
"should display the notification icon container"
);
assert.containsOnce(
document.body,
'.o_Message_notificationIcon',
"should display the notification icon"
);
assert.hasClass(
document.querySelector('.o_Message_notificationIcon'),
'fa-mobile',
"icon should represent sms"
);
document.querySelector('.o_Message_notificationIconClickable').click();
await openResendActionDef;
assert.verifySteps(
['do_action'],
"should do an action to display the resend sms dialog"
);
});
});
});

View file

@ -0,0 +1,261 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
QUnit.module('sms', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('notification_list', {}, function () {
QUnit.module('notification_list_notification_group_tests.js');
QUnit.test('mark as read', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({});
const mailMessageId1 = pyEnv['mail.message'].create(
// message that is expected to have a failure
{
author_id: pyEnv.currentPartnerId,
message_type: 'sms',
model: 'mail.channel',
res_id: mailChannelId1,
}
);
pyEnv['mail.notification'].create(
// failure that is expected to be used in the test
{
mail_message_id: mailMessageId1, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'sms',
}
);
const { afterNextRender, click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationGroup_markAsRead',
"should have 1 mark as read button"
);
await afterNextRender(() => {
document.querySelector('.o_NotificationGroup_markAsRead').click();
});
assert.containsNone(
document.body,
'.o_NotificationGroup',
"should have no notification group"
);
});
QUnit.test('notifications grouped by notification_type', async function (assert) {
assert.expect(11);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({});
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
{
message_type: 'sms', // different type from second message
model: 'res.partner', // same model as second message (and not `mail.channel`)
res_id: resPartnerId1, // same res_id as second message
res_model_name: "Partner", // random related model name
},
{
message_type: 'email', // different type from first message
model: 'res.partner', // same model as first message (and not `mail.channel`)
res_id: resPartnerId1, // same res_id as first message
res_model_name: "Partner", // same related model name for consistency
},
]);
pyEnv['mail.notification'].create([
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'sms', // different type from second failure
},
{
mail_message_id: mailMessageId1,
notification_status: 'exception',
notification_type: 'sms',
},
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'email', // different type from first failure
},
{
mail_message_id: mailMessageId2,
notification_status: 'exception',
notification_type: 'email',
},
]);
const { click } = await start();
await click('.o_MessagingMenu_toggler');
assert.containsN(
document.body,
'.o_NotificationGroup',
2,
"should have 2 notifications group"
);
const groups = document.querySelectorAll('.o_NotificationGroup');
assert.containsOnce(
groups[0],
'.o_NotificationGroup_name',
"should have 1 group name in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_name').textContent,
"Partner",
"should have model name as group name"
);
assert.containsOnce(
groups[0],
'.o_NotificationGroup_counter',
"should have 1 group counter in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in first group"
);
assert.strictEqual(
groups[0].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
"An error occurred when sending an email.",
"should have the group text corresponding to email"
);
assert.containsOnce(
groups[1],
'.o_NotificationGroup_name',
"should have 1 group name in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_name').textContent,
"Partner",
"should have second model name as group name"
);
assert.containsOnce(
groups[1],
'.o_NotificationGroup_counter',
"should have 1 group counter in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in second group"
);
assert.strictEqual(
groups[1].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
"An error occurred when sending an SMS.",
"should have the group text corresponding to sms"
);
});
QUnit.test('grouped notifications by document model', async function (assert) {
// If all failures linked to a document model refers to different documents,
// a single notification should group all failures that are linked to this
// document model.
assert.expect(12);
const pyEnv = await startServer();
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
// first message that is expected to have a failure
{
message_type: 'sms', // message must be sms (goal of the test)
model: 'res.partner', // same model as second message (and not `mail.channel`)
res_id: 31, // different res_id from second message
res_model_name: "Partner", // random related model name
},
// second message that is expected to have a failure
{
message_type: 'sms', // message must be sms (goal of the test)
model: 'res.partner', // same model as first message (and not `mail.channel`)
res_id: 32, // different res_id from first message
res_model_name: "Partner", // same related model name for consistency
},
]);
pyEnv['mail.notification'].create([
// first failure that is expected to be used in the test
{
mail_message_id: mailMessageId1, // id of the related first message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'sms', // expected failure type for sms message
},
// second failure that is expected to be used in the test
{
mail_message_id: mailMessageId2, // id of the related second message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'sms', // expected failure type for sms message
},
]);
const { click, env } = await start();
patchWithCleanup(env.services.action, {
doAction(action) {
assert.step('do_action');
assert.strictEqual(
action.name,
"SMS Failures",
"action should have 'SMS Failures' as name",
);
assert.strictEqual(
action.type,
'ir.actions.act_window',
"action should have the type act_window"
);
assert.strictEqual(
action.view_mode,
'kanban,list,form',
"action should have 'kanban,list,form' as view_mode"
);
assert.strictEqual(
JSON.stringify(action.views),
JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]),
"action should have correct views"
);
assert.strictEqual(
action.target,
'current',
"action should have 'current' as target"
);
assert.strictEqual(
action.res_model,
'res.partner',
"action should have the group model as res_model"
);
assert.strictEqual(
JSON.stringify(action.domain),
JSON.stringify([['message_has_sms_error', '=', true]]),
"action should have 'message_has_sms_error' as domain"
);
},
});
await click('.o_MessagingMenu_toggler');
assert.containsOnce(
document.body,
'.o_NotificationGroup',
"should have 1 notification group"
);
assert.containsOnce(
document.body,
'.o_NotificationGroup_counter',
"should have 1 group counter"
);
assert.strictEqual(
document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
"(2)",
"should have 2 notifications in the group"
);
document.querySelector('.o_NotificationGroup').click();
assert.verifySteps(
['do_action'],
"should do an action to display the related records"
);
});
});
});
});

View file

@ -0,0 +1,196 @@
/** @odoo-module **/
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { click, editInput, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
let serverData;
let target;
QUnit.module(
"fields",
{
beforeEach: function () {
serverData = {
models: {
partner: {
fields: {
message: { string: "message", type: "text" },
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
mobile: { string: "mobile", type: "text" },
},
records: [
{
id: 1,
message: "",
foo: "yop",
mobile: "+32494444444",
},
{
id: 2,
message: "",
foo: "bayou",
},
],
},
visitor: {
fields: {
mobile: { string: "mobile", type: "text" },
},
records: [
{
id: 1,
mobile: "+32494444444",
},
],
},
},
};
setupViewRegistries();
target = getFixture();
},
},
function () {
QUnit.module("SmsButton");
QUnit.test("Sms button in form view", async function (assert) {
await makeView({
type: "form",
resModel: "visitor",
resId: 1,
serverData,
arch: /* xml */ `
<form>
<sheet>
<field name="mobile" widget="phone"/>
</sheet>
</form>`,
});
assert.containsOnce(
target.querySelector(".o_field_phone"),
".o_field_phone_sms",
"the button is present"
);
});
QUnit.test("Sms button with option enable_sms set as False", async function (assert) {
await makeView({
type: "form",
resModel: "visitor",
resId: 1,
serverData,
mode: "readonly",
arch: /* xml */ `
<form>
<sheet>
<field name="mobile" widget="phone" options="{'enable_sms': false}"/>
</sheet>
</form>`,
});
assert.containsNone(
target.querySelector(".o_field_phone"),
".o_field_phone_sms",
"the button is not present"
);
});
QUnit.test(
"click on the sms button while creating a new record in a FormView",
async function (assert) {
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<sheet>
<field name="foo"/>
<field name="mobile" widget="phone"/>
</sheet>
</form>`,
});
patchWithCleanup(form.env.services.action, {
doAction: (action, options) => {
assert.strictEqual(action.type, "ir.actions.act_window");
assert.strictEqual(action.res_model, "sms.composer");
options.onClose();
},
});
await editInput(target, "[name='foo'] input", "John");
await editInput(target, "[name='mobile'] input", "+32494444411");
await click(target, ".o_field_phone_sms", true);
assert.strictEqual(target.querySelector("[name='foo'] input").value, "John");
assert.strictEqual(
target.querySelector("[name='mobile'] input").value,
"+32494444411"
);
}
);
QUnit.test(
"click on the sms button in a FormViewDialog has no effect on the main form view",
async function (assert) {
serverData.models.partner.fields.partner_ids = {
string: "one2many partners field",
type: "one2many",
relation: "partner",
};
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: /* xml */ `
<form>
<sheet>
<field name="foo"/>
<field name="mobile" widget="phone"/>
<field name="partner_ids">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div><t t-esc="record.display_name"/></div>
</t>
</templates>
</kanban>
</field>
</sheet>
</form>`,
});
patchWithCleanup(form.env.services.action, {
doAction: (action, options) => {
assert.strictEqual(action.type, "ir.actions.act_window");
assert.strictEqual(action.res_model, "sms.composer");
options.onClose();
},
});
await editInput(target, "[name='foo'] input", "John");
await editInput(target, "[name='mobile'] input", "+32494444411");
await click(target, "[name='partner_ids'] .o-kanban-button-new");
assert.containsOnce(target, ".modal");
const modal = target.querySelector(".modal");
await editInput(modal, "[name='foo'] input", "Max");
await editInput(modal, "[name='mobile'] input", "+324955555");
await click(modal, ".o_field_phone_sms", true);
assert.strictEqual(modal.querySelector("[name='foo'] input").value, "Max");
assert.strictEqual(
modal.querySelector("[name='mobile'] input").value,
"+324955555"
);
await click(modal, ".o_form_button_cancel");
assert.strictEqual(target.querySelector("[name='foo'] input").value, "John");
assert.strictEqual(
target.querySelector("[name='mobile'] input").value,
"+32494444411"
);
}
);
}
);