19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,303 @@
import { setSelection } from "@html_editor/../tests/_helpers/selection";
import { insertText } from "@html_editor/../tests/_helpers/user_actions";
import { FileSelector } from "@html_editor/main/media/media_dialog/file_selector";
import { uploadService } from "@html_editor/main/media/media_dialog/upload_progress_toast/upload_service";
import { HtmlComposerMessageField } from "@mail/views/web/fields/html_composer_message_field/html_composer_message_field";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import {
manuallyDispatchProgrammaticEvent,
press,
queryAll,
queryAllTexts,
queryOne,
waitFor,
waitForNone,
} from "@odoo/hoot-dom";
import { Deferred, animationFrame } from "@odoo/hoot-mock";
import {
contains,
makeMockServer,
mockService,
mountView,
mountViewInDialog,
onRpc,
patchWithCleanup,
serverState,
} from "@web/../tests/web_test_helpers";
import {
click,
defineMailModels,
mailModels,
openFormView,
openView,
registerArchs,
start,
startServer,
} from "../mail_test_helpers";
// Need this hack to use the arch in mountView(...)
mailModels.MailComposeMessage._views = {};
defineMailModels([]);
let htmlEditor;
beforeEach(() => {
patchWithCleanup(HtmlComposerMessageField.prototype, {
onEditorLoad(editor) {
htmlEditor = editor;
return super.onEditorLoad(...arguments);
},
});
});
test("media dialog: upload", async function () {
const isUploaded = new Deferred();
patchWithCleanup(FileSelector.prototype, {
async onUploaded() {
await super.onUploaded(...arguments);
isUploaded.resolve();
},
});
mockService("upload", uploadService);
const { env } = await makeMockServer();
const resId = env["mail.compose.message"].create({
display_name: "Some Composer",
body: "Hello",
attachment_ids: [],
});
let newAttachmentId;
onRpc("web_save", ({ args }) => {
expect.step("web_save");
const createVals = args[1];
expect(createVals.attachment_ids[0][0]).toBe(4); // link command
expect(createVals.attachment_ids[0][1]).toBe(newAttachmentId); // on attachment id "5"
});
onRpc("/html_editor/attachment/add_data", () => {
const attachment = {
name: "test.jpg",
description: false,
mimetype: "image/jpeg",
checksum: "7951a43bbfb08fd742224ada280913d1897b89ab",
url: false,
type: "binary",
res_id: 0,
res_model: "mail.compose.message",
public: false,
access_token: false,
image_src: "/web/image/1-a0e63e61/test.jpg",
image_width: 1,
image_height: 1,
original_id: false,
};
newAttachmentId = env["ir.attachment"].create(attachment);
attachment.id = newAttachmentId;
return attachment;
});
onRpc("ir.attachment", "generate_access_token", () => ["129a52e1-6bf2-470a-830e-8e368b022e13"]);
await mountView({
type: "form",
resId,
resModel: "mail.compose.message",
arch: `
<form>
<field name="body" type="html" widget="html_composer_message"/>
<field name="attachment_ids" widget="many2many_binary"/>
</form>`,
});
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
setSelection({ anchorNode, anchorOffset: 0 });
// Open media dialog
await animationFrame();
await insertText(htmlEditor, "/image");
await press("Enter");
await animationFrame();
// upload test
const fileInputs = queryAll(".o_select_media_dialog input.d-none.o_file_input");
const fileB64 =
"/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==";
const fileBytes = new Uint8Array(
atob(fileB64)
.split("")
.map((char) => char.charCodeAt(0))
);
// redefine 'files' so we can put mock data in through js
fileInputs.forEach((input) =>
Object.defineProperty(input, "files", {
value: [new File(fileBytes, "test.jpg", { type: "image/jpeg" })],
})
);
fileInputs.forEach((input) => {
manuallyDispatchProgrammaticEvent(input, "change");
});
expect("[name='attachment_ids'] .o_attachment[title='test.jpg']").toHaveCount(0);
await isUploaded;
await animationFrame();
expect("[name='attachment_ids'] .o_attachment[title='test.jpg']").toHaveCount(1);
await contains(".o_form_button_save").click();
await expect.waitForSteps(["web_save"]);
});
test("mention a partner", async () => {
onRpc("res.partner", "get_mention_suggestions", ({ kwargs }) => {
expect.step(`get_mention_suggestions: ${kwargs.search}`);
});
const pyEnv = await startServer();
registerArchs({
"mail.compose.message,false,form": `
<form>
<field name="body" type="html" widget="html_composer_message"/>
</form>`,
});
const composerId = pyEnv["mail.compose.message"].create({
subject: "Greetings",
body: "<p><br></p>",
model: "res.partner",
});
await start();
await openView({
res_model: "mail.compose.message",
res_id: composerId,
views: [["mail.compose.message,false,form", "form"]],
});
const anchorNode = queryOne(`.odoo-editor-editable p`);
setSelection({ anchorNode, anchorOffset: 0 });
await insertText(htmlEditor, "@");
await animationFrame();
expect(".overlay .search input[placeholder='Search for a user...']").toBeFocused();
expect(".overlay .o-mail-NavigableList .o-mail-NavigableList-item").toHaveCount(0);
await press("a");
await waitFor(".overlay .o-mail-NavigableList .o-mail-NavigableList-item");
expect(queryAllTexts(".overlay .o-mail-NavigableList .o-mail-NavigableList-item")).toEqual([
"Mitchell Admin",
]);
expect.verifySteps(["get_mention_suggestions: a"]);
await press("enter");
expect("[name='body'] .odoo-editor-editable").toHaveInnerHTML(`
<p>
<a target="_blank" data-oe-protected="true" contenteditable="false" href="https://www.hoot.test/odoo/res.partner/17" class="o_mail_redirect" data-oe-id="17" data-oe-model="res.partner">
@Mitchell Admin
</a>
</p>`);
});
test("mention a channel", async () => {
onRpc("discuss.channel", "get_mention_suggestions", ({ kwargs }) => {
expect.step(`get_mention_suggestions: ${kwargs.search}`);
});
await mountViewInDialog({
type: "form",
resModel: "mail.compose.message",
arch: `
<form>
<field name="body" type="html" widget="html_composer_message"/>
</form>`,
});
const anchorNode = queryOne(`[name='body'] .odoo-editor-editable div.o-paragraph`);
setSelection({ anchorNode, anchorOffset: 0 });
await insertText(htmlEditor, "#");
await animationFrame();
expect(".overlay .search input[placeholder='Search for a channel...']").toBeFocused();
expect(".overlay .o-mail-NavigableList .o-mail-NavigableList-item").toHaveCount(0);
await press("a");
await animationFrame();
expect.verifySteps(["get_mention_suggestions: a"]);
});
describe("Remove attachments", () => {
beforeEach(() => {
mailModels.MailComposeMessage._views = {
"form,false": `
<form js_class="mail_composer_form">
<field name="body" type="html" widget="html_composer_message"/>
<field name="attachment_ids" widget="mail_composer_attachment_list"/>
</form>`,
};
});
test("should remove file from html editor if removed from attachment list", async () => {
mockService("uploadLocalFiles", {
async upload() {
expect.step("File Uploaded");
return [{ id: 1, name: "file.txt", public: true, checksum: "123" }];
},
});
await start();
await openFormView("res.partner", serverState.partnerId);
await click("button", { text: "Log note" });
await click("button[title='Open Full Composer']");
await waitFor(".odoo-editor-editable");
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
setSelection({ anchorNode, anchorOffset: 0 });
await insertText(htmlEditor, "/file");
await press("Enter");
await expect.waitForSteps(["File Uploaded"]);
await waitFor("[name='attachment_ids'] a:contains('file.txt')");
await waitFor(".odoo-editor-editable .o_file_box:has(a:contains('file.txt'))");
await click("[name='attachment_ids'] button:has(i.fa-times)");
await waitForNone("[name='attachment_ids'] a:contains('file.txt')");
await waitForNone(".odoo-editor-editable .o_file_box:has(a:contains('file.txt'))");
});
test("should remove image from html editor if removed from attachment list", async () => {
patchWithCleanup(FileSelector.prototype, {
async onUploaded() {
await super.onUploaded(...arguments);
expect.step("Image Uploaded");
},
});
onRpc("/html_editor/attachment/add_data", () => ({
id: 1,
name: "test.jpg",
description: false,
mimetype: "image/jpeg",
checksum: "7951a43bbfb08fd742224ada280913d1897b89ab",
url: false,
type: "binary",
res_id: 0,
res_model: "mail.compose.message",
public: false,
access_token: false,
image_src: "/web/image/1-a0e63e61/test.jpg",
image_width: 1,
image_height: 1,
original_id: false,
}));
onRpc("ir.attachment", "generate_access_token", () => [
"129a52e1-6bf2-470a-830e-8e368b022e13",
]);
await start();
await openFormView("res.partner", serverState.partnerId);
await click("button", { text: "Log note" });
await click("button[title='Open Full Composer']");
await waitFor(".odoo-editor-editable");
const anchorNode = queryOne(".odoo-editor-editable div.o-paragraph");
setSelection({ anchorNode, anchorOffset: 0 });
await insertText(htmlEditor, "/image");
await press("Enter");
await animationFrame();
const fileInput = queryOne(".o_select_media_dialog input.d-none.o_file_input");
Object.defineProperty(fileInput, "files", {
value: [new File([], "test.jpg", { type: "image/jpeg" })],
});
manuallyDispatchProgrammaticEvent(fileInput, "change");
await expect.waitForSteps(["Image Uploaded"]);
await waitFor("[name='attachment_ids'] a:contains('test.jpg')");
await waitFor(".odoo-editor-editable img[data-attachment-id='1']");
await click("[name='attachment_ids'] button:has(i.fa-times)");
await waitForNone("[name='attachment_ids'] a:contains('test.jpg')");
await waitForNone(".odoo-editor-editable img[data-attachment-id='1']");
});
});

View file

@ -0,0 +1,249 @@
import {
click,
contains,
defineMailModels,
insertText,
onRpcBefore,
openFormView,
registerArchs,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { Deferred, tick } from "@odoo/hoot-mock";
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
const archs = {
"res.fake,false,form": `
<form string="Fake">
<sheet></sheet>
<chatter/>
</form>`,
"res.partner,false,form": `
<form string="Partner">
<sheet>
<field name="name"/>
<field name="email"/>
<field name="phone"/>
</sheet>
</form>`,
};
test("Show 'Followers only' placeholder for recipients input when no recipient", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "test name 1", email: "test1@odoo.com" });
await start();
await openFormView("res.partner", partnerId);
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o-autocomplete--input[placeholder='Followers only']");
});
test("Opening full composer in 'send message' mode should copy selected suggested recipients", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "John Jane",
email: "john@jane.be",
});
const fakeId = pyEnv["res.fake"].create({
email_cc: "john@test.be",
phone: "123456789",
partner_ids: [partnerId],
});
const def = new Deferred();
mockService("action", {
async doAction(action) {
if (action?.res_model === "res.fake") {
return super.doAction(...arguments);
}
asyncStep("do-action");
expect(action.name).toBe("Compose Email");
expect(action.context.default_subtype_xmlid).toBe("mail.mt_comment");
expect(action.context.default_partner_ids).toHaveLength(2);
const [johnTestPartnerId] = pyEnv["res.partner"].search([
["email", "=", "john@test.be"],
]);
expect(action.context.default_partner_ids).toEqual([johnTestPartnerId, partnerId]);
def.resolve();
},
});
await start();
await openFormView("res.fake", fakeId);
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
await click("button[title='Open Full Composer']");
await def;
await waitForSteps(["do-action"]);
});
test("Opening full composer in 'log note' mode should not copy selected suggested recipients", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "John Jane",
email: "john@jane.be",
});
const fakeId = pyEnv["res.fake"].create({
email_cc: "john@test.be",
partner_ids: [partnerId],
});
const def = new Deferred();
mockService("action", {
async doAction(action) {
if (action?.res_model === "res.fake") {
return super.doAction(...arguments);
}
asyncStep("do-action");
expect(action.name).toBe("Log note");
expect(action.context.default_subtype_xmlid).toBe("mail.mt_note");
expect(action.context.default_partner_ids).toBeEmpty();
def.resolve();
},
});
await start();
await openFormView("res.fake", fakeId);
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
await click("button", { text: "Log note" });
await click("button[title='Open Full Composer']");
await def;
await waitForSteps(["do-action"]);
});
test("Check that a partner is created for new followers when sending a message", async () => {
const pyEnv = await startServer();
const [partnerId, partnerId_2] = pyEnv["res.partner"].create([
{ name: "John Jane", email: "john@jane.be" },
{ name: "Peter Johnson", email: "peter@johnson.be" },
]);
const fakeId = pyEnv["res.fake"].create({
email_cc: "john@test.be",
partner_ids: [partnerId],
});
pyEnv["mail.followers"].create({
partner_id: partnerId_2,
email: "peter@johnson.be",
is_active: true,
res_id: fakeId,
res_model: "res.fake",
});
registerArchs(archs);
await start();
await openFormView("res.fake", fakeId);
await contains(".o-mail-Followers-counter", { text: "1" });
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(John Jane)");
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
// Ensure that partner `john@test.be` is created while sending the message (not before)
const partners = pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]]);
expect(partners).toHaveLength(0);
await insertText(".o-mail-Composer-input", "Dummy Message");
await click(".o-mail-Composer-send:enabled");
await contains(".o-mail-Followers-counter", { text: "1" });
});
test("suggest recipient on 'Send message' composer", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "Peter Johnson",
email: "peter@johnson.be",
});
const fakeId = pyEnv["res.fake"].create({ email_cc: "john@test.be" });
pyEnv["mail.followers"].create({
partner_id: partnerId,
email: "peter@johnson.be",
is_active: true,
res_id: fakeId,
res_model: "res.fake",
});
registerArchs(archs);
await start();
await openFormView("res.fake", fakeId);
await contains(".o-mail-Followers-counter", { text: "1" });
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o_tag_badge_text:contains(john@test.be)");
// Ensure that partner `john@test.be` is created before sending the message
expect(pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]])).toHaveLength(0);
await insertText(".o-mail-Composer-input", "Dummy Message");
await click(".o-mail-Composer-send:enabled");
await tick();
expect(pyEnv["res.partner"].search_read([["email", "=", "john@test.be"]])).toHaveLength(1);
await contains(".o-mail-Followers-counter", { text: "1" });
});
test("suggested recipients should not be notified when posting an internal note", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "John Jane",
email: "john@jane.be",
});
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
onRpcBefore("/mail/message/post", (args) => {
asyncStep("message_post");
expect(args.post_data.partner_ids).toBeEmpty();
});
await start();
await openFormView("res.fake", fakeId);
await click("button", { text: "Log note" });
await insertText(".o-mail-Composer-input", "Dummy Message");
await click(".o-mail-Composer-send:enabled");
await contains(".o-mail-Message");
await waitForSteps(["message_post"]);
});
test("suggested recipients without name should show display_name instead", async () => {
const pyEnv = await startServer();
const [partner1, partner2] = pyEnv["res.partner"].create([
{ name: "Test Partner" },
// Partner without name
{ type: "invoice" },
]);
pyEnv["res.partner"].write([partner2], { parent_id: partner1 });
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partner2] });
registerArchs(archs);
await start();
await openFormView("res.fake", fakeId);
await click("button", { text: "Send message" });
await contains(".o-mail-RecipientsInput .o_tag_badge_text", { text: "Test Partner, Invoice" });
});
test("update email for the partner on the fly", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "John Jane",
});
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
registerArchs(archs);
await start();
await openFormView("res.fake", fakeId);
await click("button", { text: "Send message" });
await insertText(".o-mail-RecipientsInputTagsListPopover input", "john@jane.be");
await click(".o-mail-RecipientsInputTagsListPopover .btn-primary");
await insertText(".o-mail-Composer-input", "Dummy Message");
await click(".o-mail-Composer-send:enabled");
await contains(".o-mail-Message");
await contains(".o-mail-Followers-counter", { text: "0" });
});
test("suggested recipients should not be added as follower when posting a message", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
name: "John Jane",
email: "john@jane.be",
});
const fakeId = pyEnv["res.fake"].create({ partner_ids: [partnerId] });
registerArchs(archs);
await start();
await openFormView("res.fake", fakeId);
await contains(".o-mail-Followers-counter", { text: "0" });
await click("button", { text: "Send message" });
await insertText(".o-mail-Composer-input", "Dummy Message");
await click(".o-mail-Composer-send:enabled");
await contains(".o-mail-Message");
await contains(".o-mail-Followers-counter", { text: "0" });
});