/* @odoo-module */ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { Command } from "@mail/../tests/helpers/command"; import { start } from "@mail/../tests/helpers/test_utils"; import { config as transitionConfig } from "@web/core/transition"; import { makeDeferred, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; import { click, contains, createFile, dragenterFiles, focus, insertText, scroll, triggerEvents, } from "@web/../tests/utils"; QUnit.module("thread"); QUnit.test("dragover files on thread with composer", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_type: "channel", group_public_id: false, name: "General", }); const { openDiscuss } = await start(); openDiscuss(channelId); const files = [ await createFile({ content: "hello, world", contentType: "text/plain", name: "text3.txt", }), ]; await dragenterFiles(".o-mail-Thread", files); await contains(".o-mail-Dropzone"); }); QUnit.test("load more messages from channel (auto-load on scroll)", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_type: "channel", group_public_id: false, name: "General", }); for (let i = 0; i <= 60; i++) { pyEnv["mail.message"].create({ body: i.toString(), model: "discuss.channel", res_id: channelId, }); } const { openDiscuss } = await start(); openDiscuss(channelId); await contains("button", { text: "Load More", before: [".o-mail-Message", { count: 30 }] }); await contains(".o-mail-Thread", { scroll: "bottom" }); await scroll(".o-mail-Thread", 0); await contains(".o-mail-Message", { count: 60 }); await contains(".o-mail-Message", { text: "30", after: [".o-mail-Message", { text: "29" }] }); }); QUnit.test("show message subject when subject is not the same as the thread name", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_type: "channel", group_public_id: false, name: "General", }); pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, subject: "Salutations, voyageur", }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message", { text: "Subject: Salutations, voyageurnot empty" }); }); QUnit.test("do not show message subject when subject is the same as the thread name", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_type: "channel", group_public_id: false, name: "Salutations, voyageur", }); pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, subject: "Salutations, voyageur", }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message", { text: "not empty" }); await contains(".o-mail-Message", { count: 0, text: "Subject: Salutations, voyageurnot empty", }); }); QUnit.test("auto-scroll to bottom of thread on load", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "general" }); for (let i = 1; i <= 25; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message", { count: 25 }); await contains(".o-mail-Thread", { scroll: "bottom" }); }); QUnit.test("display day separator before first message of the day", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "" }); pyEnv["mail.message"].create([ { body: "not empty", model: "discuss.channel", res_id: channelId, }, { body: "not empty", model: "discuss.channel", res_id: channelId, }, ]); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-DateSection"); }); QUnit.test("do not display day separator if all messages of the day are empty", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "" }); pyEnv["mail.message"].create({ body: "", model: "discuss.channel", res_id: channelId, }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Thread", { text: "There are no messages in this conversation." }); await contains(".o-mail-DateSection", { count: 0 }); }); QUnit.test("scroll position is kept when navigating from one channel to another", async () => { const pyEnv = await startServer(); const channelId_1 = pyEnv["discuss.channel"].create({ name: "channel-1" }); const channelId_2 = pyEnv["discuss.channel"].create({ name: "channel-2" }); // Fill both channels with random messages in order for the scrollbar to // appear. pyEnv["mail.message"].create( Array(50) .fill(0) .map((_, index) => ({ body: "Non Empty Body ".repeat(25), message_type: "comment", model: "discuss.channel", res_id: index < 20 ? channelId_1 : channelId_2, })) ); const { openDiscuss } = await start(); openDiscuss(channelId_1); await contains(".o-mail-Message", { count: 20 }); const scrollValue1 = $(".o-mail-Thread")[0].scrollHeight / 2; await contains(".o-mail-Thread", { scroll: "bottom" }); await scroll(".o-mail-Thread", scrollValue1); await click(".o-mail-DiscussSidebarChannel", { text: "channel-2" }); await contains(".o-mail-Message", { count: 30 }); const scrollValue2 = $(".o-mail-Thread")[0].scrollHeight / 3; await contains(".o-mail-Thread", { scroll: "bottom" }); await scroll(".o-mail-Thread", scrollValue2); await click(".o-mail-DiscussSidebarChannel", { text: "channel-1" }); await contains(".o-mail-Message", { count: 20 }); await contains(".o-mail-Thread", { scroll: scrollValue1 }); await click(".o-mail-DiscussSidebarChannel", { text: "channel-2" }); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-Thread", { scroll: scrollValue2 }); }); QUnit.test("thread is still scrolling after scrolling up then to bottom", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "channel-1" }); pyEnv["mail.message"].create( Array(20) .fill(0) .map(() => ({ body: "Non Empty Body ".repeat(25), message_type: "comment", model: "discuss.channel", res_id: channelId, })) ); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message", { count: 20 }); await contains(".o-mail-Thread", { scroll: "bottom" }); await scroll(".o-mail-Thread", $(".o-mail-Thread")[0].scrollHeight / 2); await scroll(".o-mail-Thread", "bottom"); await insertText(".o-mail-Composer-input", "123"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message", { count: 21 }); await contains(".o-mail-Thread", { scroll: "bottom" }); }); QUnit.test("Can mention other channels in a group-restricted channel", async () => { const pyEnv = await startServer(); const groupId = pyEnv["res.groups"].create({ name: "Mario Group", }); const [channelId1] = pyEnv["discuss.channel"].create([ { channel_type: "channel", group_public_id: groupId, name: "Marios", }, { channel_type: "channel", group_public_id: false, name: "Link and Zelda", }, ]); const { openDiscuss } = await start(); openDiscuss(channelId1); await insertText(".o-mail-Composer-input", "#"); await contains(".o-mail-Composer-suggestion", { text: "#Marios" }); await contains(".o-mail-Composer-suggestion", { text: "#Link and Zelda" }); await click(".o-mail-Composer-suggestion", { text: "#Link and Zelda" }); await contains(".o-mail-Composer-input", { value: "#Link and Zelda " }); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message-body .o_channel_redirect", { text: "#Link and Zelda" }); }); QUnit.test("mention a channel with space in the name", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General good boy" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "#"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "#General good boy " }); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message-body .o_channel_redirect", { text: "#General good boy" }); }); QUnit.test('mention a channel with "&" in the name', async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General & good" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "#"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "#General & good " }); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message-body .o_channel_redirect", { text: "#General & good" }); }); QUnit.test( "mark channel as fetched when a new message is loaded and as seen when focusing composer [REQUIRE FOCUS]", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ email: "fred@example.com", name: "Fred", }); const userId = pyEnv["res.users"].create({ partner_id: partnerId }); const channelId = pyEnv["discuss.channel"].create({ name: "test", channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], channel_type: "chat", }); const { env } = await start({ mockRPC(route, args) { if (args.method === "channel_fetched") { assert.strictEqual(args.args[0][0], channelId); assert.strictEqual(args.model, "discuss.channel"); assert.step("rpc:channel_fetch"); } else if (route === "/discuss/channel/set_last_seen_message") { assert.strictEqual(args.channel_id, channelId); assert.step("rpc:set_last_seen_message"); } }, }); await contains(".o_menu_systray i[aria-label='Messages']"); pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "Hello!", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message"); assert.verifySteps(["rpc:channel_fetch"]); await contains(".o-mail-Thread-newMessage hr + span", { text: "New messages" }); await focus(".o-mail-Composer-input"); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); assert.verifySteps(["rpc:set_last_seen_message"]); } ); QUnit.test( "mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({}); const userId = pyEnv["res.users"].create({ partner_id: partnerId }); const channelId = pyEnv["discuss.channel"].create({ name: "test", channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], }); const { env, openDiscuss } = await start({ async mockRPC(route, args) { if (args.method === "channel_fetched" && args.args[0] === channelId) { throw new Error( "'channel_fetched' RPC must not be called for created channel as message is directly seen" ); } else if (route === "/discuss/channel/set_last_seen_message") { assert.strictEqual(args.channel_id, channelId); assert.step("rpc:set_last_seen_message"); } }, }); openDiscuss(channelId); await focus(".o-mail-Composer-input"); // simulate receiving a message await pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "
Some new message
", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message"); assert.verifySteps(["rpc:set_last_seen_message"]); } ); QUnit.test( "should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" }); const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], }); for (let i = 0; i <= 10; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } const { env } = await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 11 }); await contains(".o-mail-Thread", { scroll: "bottom" }); // simulate receiving a message pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "hello", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message", { count: 12 }); await contains(".o-mail-Thread", { scroll: "bottom" }); } ); QUnit.test( "should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" }); const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], }); for (let i = 0; i <= 10; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } const { env } = await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 11 }); await contains(".o-mail-Thread", { scroll: "bottom" }); await scroll(".o-mail-Thread", 0); // simulate receiving a message pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "hello", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message", { count: 12 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 0 }); } ); QUnit.test("show empty placeholder when thread contains no message", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "general" }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Thread", { text: "There are no messages in this conversation." }); await contains(".o-mail-Message", { count: 0 }); }); QUnit.test("show empty placeholder when thread contains only empty messages", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); pyEnv["mail.message"].create({ model: "discuss.channel", res_id: channelId }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Thread", { text: "There are no messages in this conversation." }); await contains(".o-mail-Message", { count: 0 }); }); QUnit.test( "message list with a full page of empty messages should load more messages until there are some non-empty", async () => { // Technical assumptions : // - /discuss/channel/messages fetching exactly 30 messages, // - empty messages not being displayed // - auto-load more being triggered on scroll, not automatically when the 30 first messages are empty const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); for (let i = 0; i < 50; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } for (let i = 0; i < 50; i++) { pyEnv["mail.message"].create({ model: "discuss.channel", res_id: channelId }); } const { openDiscuss } = await start(); openDiscuss(channelId); // initial load: +30 empty ; (auto) load more: +20 empty +10 non-empty await contains(".o-mail-Message", { count: 10 }); await contains("button", { text: "Load More" }); // still 40 non-empty } ); QUnit.test("no new messages separator on posting message (some message history)", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ message_unread_counter: 0, partner_id: pyEnv.currentPartnerId }), ], channel_type: "channel", name: "General", }); const messageId = pyEnv["mail.message"].create({ body: "first message", model: "discuss.channel", res_id: channelId, }); const [memberId] = pyEnv["discuss.channel.member"].search([ ["channel_id", "=", channelId], ["partner_id", "=", pyEnv.currentPartnerId], ]); pyEnv["discuss.channel.member"].write([memberId], { seen_message_id: messageId }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message"); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); await insertText(".o-mail-Composer-input", "hey!"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message", { count: 2 }); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); }); QUnit.test("new messages separator on receiving new message [REQUIRE FOCUS]", async () => { patchWithCleanup(transitionConfig, { disabled: true }); const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" }); const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId, }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ message_unread_counter: 0, partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], channel_type: "channel", name: "General", }); const messageId = pyEnv["mail.message"].create({ body: "blah", model: "discuss.channel", res_id: channelId, }); const [memberId] = pyEnv["discuss.channel.member"].search([ ["channel_id", "=", channelId], ["partner_id", "=", pyEnv.currentPartnerId], ]); pyEnv["discuss.channel.member"].write([memberId], { seen_message_id: messageId }); const { env, openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message"); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); $(".o-mail-Composer-input")[0].blur(); // simulate receiving a message pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "hu", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message", { count: 2 }); await contains(".o-mail-Thread-newMessage hr + span", { text: "New messages" }); await contains(".o-mail-Thread-newMessage ~ .o-mail-Message", { text: "hu" }); await focus(".o-mail-Composer-input"); await nextTick(); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); }); QUnit.test("no new messages separator on posting message (no message history)", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ message_unread_counter: 0, partner_id: pyEnv.currentPartnerId }), ], channel_type: "channel", name: "General", }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Composer-input"); await contains(".o-mail-Message", { count: 0 }); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); await insertText(".o-mail-Composer-input", "hey!"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message"); await contains(".o-mail-Thread-newMessage hr + span", { count: 0, text: "New messages" }); }); QUnit.test("Mention a partner with special character (e.g. apostrophe ')", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ email: "usatyi@example.com", name: "Pynya's spokesman", }); const channelId = pyEnv["discuss.channel"].create({ name: "test", channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "@"); await insertText(".o-mail-Composer-input", "Pyn"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "@Pynya's spokesman " }); await click(".o-mail-Composer-send:enabled"); await contains( `.o-mail-Message-body .o_mail_redirect[data-oe-id="${partnerId}"][data-oe-model="res.partner"]`, { text: "@Pynya's spokesman" } ); }); QUnit.test("mention 2 different partners that have the same name", async () => { const pyEnv = await startServer(); const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([ { email: "partner1@example.com", name: "TestPartner", }, { email: "partner2@example.com", name: "TestPartner", }, ]); const channelId = pyEnv["discuss.channel"].create({ name: "test", channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId_1 }), Command.create({ partner_id: partnerId_2 }), ], }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "@Te"); await click(":nth-child(1 of .o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "@TestPartner " }); await insertText(".o-mail-Composer-input", "@Te"); await click(":nth-child(2 of .o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "@TestPartner @TestPartner " }); await click(".o-mail-Composer-send:enabled"); await contains( `.o-mail-Message-body .o_mail_redirect[data-oe-id="${partnerId_1}"][data-oe-model="res.partner"]`, { text: "@TestPartner" } ); await contains( `.o-mail-Message-body .o_mail_redirect[data-oe-id="${partnerId_2}"][data-oe-model="res.partner"]`, { text: "@TestPartner" } ); }); QUnit.test("mention a channel on a second line when the first line contains #", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General good" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "#blabla\n#"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "#blabla\n#General good " }); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message-body .o_channel_redirect", { text: "#General good" }); }); QUnit.test( "mention a channel when replacing the space after the mention by another char", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General good" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "#"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "#General good " }); const text = $(".o-mail-Composer-input").val(); $(".o-mail-Composer-input").val(text.slice(0, -1)); await insertText(".o-mail-Composer-input", ", test"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message-body .o_channel_redirect", { text: "#General good" }); } ); QUnit.test("mention 2 different channels that have the same name", async () => { const pyEnv = await startServer(); const [channelId_1, channelId_2] = pyEnv["discuss.channel"].create([ { channel_type: "channel", group_public_id: false, name: "my channel", }, { channel_type: "channel", name: "my channel", }, ]); const { openDiscuss } = await start(); openDiscuss(channelId_1); await insertText(".o-mail-Composer-input", "#m"); await click(":nth-child(1 of .o-mail-Composer-suggestion)"); await contains(".o-mail-Composer-input", { value: "#my channel " }); await insertText(".o-mail-Composer-input", "#m"); await click(":nth-child(2 of .o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "#my channel #my channel " }); await click(".o-mail-Composer-send:enabled"); await contains( `.o-mail-Message-body .o_channel_redirect[data-oe-id="${channelId_1}"][data-oe-model="discuss.channel"]`, { text: "#my channel" } ); await contains( `.o-mail-Message-body .o_channel_redirect[data-oe-id="${channelId_2}"][data-oe-model="discuss.channel"]`, { text: "#my channel" } ); }); QUnit.test( "Post a message containing an email address followed by a mention on another line", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ email: "testpartner@odoo.com", name: "TestPartner", }); const channelId = pyEnv["discuss.channel"].create({ name: "test", channel_member_ids: [ Command.create({ partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "email@odoo.com\n@Te"); await click(".o-mail-Composer-suggestion"); await contains(".o-mail-Composer-input", { value: "email@odoo.com\n@TestPartner " }); await click(".o-mail-Composer-send:enabled"); await contains( `.o-mail-Message-body .o_mail_redirect[data-oe-id="${partnerId}"][data-oe-model="res.partner"]`, { text: "@TestPartner" } ); } ); QUnit.test("basic rendering of canceled notification", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "test" }); const partnerId = pyEnv["res.partner"].create({ name: "Someone" }); const messageId = pyEnv["mail.message"].create({ body: "not empty", message_type: "email", model: "discuss.channel", res_id: channelId, }); pyEnv["mail.notification"].create({ failure_type: "SMTP", mail_message_id: messageId, notification_status: "canceled", notification_type: "email", res_partner_id: partnerId, }); const { openDiscuss } = await start(); openDiscuss(channelId); await contains(".o-mail-Message-notification .fa-envelope-o"); await click(".o-mail-Message-notification"); await contains(".o-mail-MessageNotificationPopover"); await contains(".o-mail-MessageNotificationPopover .fa-trash-o"); await contains(".o-mail-MessageNotificationPopover", { text: "Someone" }); }); QUnit.test( "first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]", async () => { // The goal of removing the focus is to ensure the thread is not marked as seen automatically. // Indeed that would trigger set_last_seen_message no matter what, which is already covered by other tests. // The goal of this test is to cover the conditions specific to transient messages, // and the conditions from focus would otherwise shadow them. const pyEnv = await startServer(); // Needed partner & user to allow simulation of message reception const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" }); const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId, }); const channelId = pyEnv["discuss.channel"].create({ channel_type: "channel", name: "General", channel_member_ids: [ Command.create({ partner_id: partnerId }), Command.create({ partner_id: pyEnv.currentPartnerId }), ], }); pyEnv["mail.message"].create([ { body: "not empty", model: "discuss.channel", res_id: channelId, }, ]); const { openDiscuss, env } = await start(); openDiscuss(channelId); // send a command that leads to receiving a transient message await insertText(".o-mail-Composer-input", "/who"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message", { count: 2 }); // composer is focused by default, we remove that focus $(".o-mail-Composer-input")[0].blur(); // simulate receiving a message pyEnv.withUser(userId, () => env.services.rpc("/mail/message/post", { post_data: { body: "test", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-Message", { count: 3 }); await contains(".o-mail-Thread-newMessage hr + span", { text: "New messages" }); await contains(".o-mail-Message[aria-label='Note'] + .o-mail-Thread-newMessage"); } ); QUnit.test( "composer should be focused automatically after clicking on the send button", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "test" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "Dummy Message"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Composer-input:focus"); } ); QUnit.test("chat window header should not have unread counter for non-channel thread", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "test" }); const messageId = pyEnv["mail.message"].create({ author_id: partnerId, body: "not empty", model: "res.partner", needaction: true, needaction_partner_ids: [pyEnv.currentPartnerId], res_id: partnerId, }); pyEnv["mail.notification"].create({ mail_message_id: messageId, notification_status: "sent", notification_type: "inbox", res_partner_id: pyEnv.currentPartnerId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow-counter", { count: 0, text: "1" }); }); QUnit.test( "[technical] opening a non-channel chat window should not call channel_fold", async (assert) => { // channel_fold should not be called when opening non-channels in chat // window, because there is no server sync of fold state for them. const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "test" }); const messageId = pyEnv["mail.message"].create({ author_id: partnerId, body: "not empty", model: "res.partner", needaction: true, needaction_partner_ids: [pyEnv.currentPartnerId], res_id: partnerId, }); pyEnv["mail.notification"].create({ mail_message_id: messageId, notification_status: "sent", notification_type: "inbox", res_partner_id: pyEnv.currentPartnerId, }); await start({ async mockRPC(route, args) { if (route.includes("channel_fold")) { const message = "should not call channel_fold when opening a non-channel chat window"; assert.step(message); console.error(message); throw Error(message); } }, }); await click(".o_menu_systray i[aria-label='Messages']"); await contains(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow", { count: 0 }); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); } ); QUnit.test("Thread messages are only loaded once", async (assert) => { const pyEnv = await startServer(); const channelIds = pyEnv["discuss.channel"].create([{ name: "General" }, { name: "Sales" }]); const { openDiscuss } = await start({ mockRPC(route, args, originalRPC) { if (route === "/discuss/channel/messages") { assert.step(`load messages - ${args["channel_id"]}`); } return originalRPC(route, args); }, }); pyEnv["mail.message"].create([ { model: "discuss.channel", res_id: channelIds[0], body: "Message on channel1", }, { model: "discuss.channel", res_id: channelIds[1], body: "Message on channel2", }, ]); openDiscuss(); await click(":nth-child(1 of .o-mail-DiscussSidebarChannel"); await contains(".o-mail-Message-content", { text: "Message on channel1" }); await click(":nth-child(2 of .o-mail-DiscussSidebarChannel)"); await contains(".o-mail-Message-content", { text: "Message on channel2" }); await click(":nth-child(1 of .o-mail-DiscussSidebarChannel)"); await contains(".o-mail-Message-content", { text: "Message on channel1" }); assert.verifySteps([`load messages - ${channelIds[0]}`, `load messages - ${channelIds[1]}`]); }); QUnit.test( "Opening thread with needaction messages should mark all messages of thread as read", async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); const { env, openDiscuss } = await start({ mockRPC(route, args) { if (args.model === "mail.message" && args.method === "mark_all_as_read") { assert.step("mark-all-messages-as-read"); assert.deepEqual(args.args[0], [ ["model", "=", "discuss.channel"], ["res_id", "=", channelId], ]); } }, }); openDiscuss(channelId); await contains(".o-mail-Composer-input"); await triggerEvents(".o-mail-Composer-input", ["blur", "focusout"]); await click("button", { text: "Inbox" }); await contains("h4", { text: "Congratulations, your inbox is empty" }); const messageId = pyEnv["mail.message"].create({ author_id: partnerId, body: "@Mitchel Admin", needaction: true, model: "discuss.channel", res_id: channelId, needaction_partner_ids: [pyEnv.currentPartnerId], }); pyEnv["mail.notification"].create({ mail_message_id: messageId, notification_status: "sent", notification_type: "inbox", res_partner_id: pyEnv.currentPartnerId, }); // simulate receiving a new needaction message const [formattedMessage] = await env.services.orm.call("mail.message", "message_format", [ [messageId], ]); pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.message/inbox", formattedMessage); await contains("button", { text: "Inbox", contains: [".badge", { text: "1" }] }); await click("button", { text: "General" }); await contains(".o-discuss-badge", { count: 0 }); await contains("button", { text: "Inbox", contains: [".badge", { count: 0 }] }); assert.verifySteps(["mark-all-messages-as-read"]); } ); QUnit.test( "[technical] Opening thread without needaction messages should not mark all messages of thread as read", async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); const { env, openDiscuss } = await start({ mockRPC(route, args) { if (args.model === "mail.message" && args.method === "mark_all_as_read") { assert.step("mark-all-messages-as-read"); } }, }); openDiscuss(channelId); await click("button", { text: "Inbox" }); await env.services.rpc("/mail/message/post", { post_data: { body: "Hello world!", attachment_ids: [], }, thread_id: channelId, thread_model: "discuss.channel", }); await click("button", { text: "General" }); await nextTick(); assert.verifySteps([]); } ); QUnit.test("can be marked as read while loading", async function () { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ message_unread_counter: 1, partner_id: pyEnv.currentPartnerId }), Command.create({ partner_id: partnerId }), ], channel_type: "chat", }); pyEnv["mail.message"].create({ author_id: partnerId, body: "Test
", model: "discuss.channel", res_id: channelId, }); const loadDeferred = makeDeferred(); const { openDiscuss } = await start({ async mockRPC(route) { if (route === "/discuss/channel/messages") { await loadDeferred; } }, }); openDiscuss(undefined); await contains(".o-discuss-badge", { text: "1" }); await click(".o-mail-DiscussSidebarChannel", { text: "Demo" }); loadDeferred.resolve(); await contains(".o-discuss-badge", { count: 0 }); }); QUnit.test("New message separator not appearing after showing composer on thread", async () => { const pyEnv = await startServer(); pyEnv["mail.message"].create([ { model: "res.partner", res_id: pyEnv.currentPartnerId, body: "Message on partner", }, { model: "res.partner", res_id: pyEnv.currentPartnerId, body: "Message on partner", }, ]); const { openFormView } = await start(); openFormView("res.partner", pyEnv.currentPartnerId); await contains("button", { text: "Log note" }); await contains(".o-mail-Thread-newMessage", { count: 0 }); await click("button", { text: "Log note" }); await contains(".o-mail-Thread-newMessage", { count: 0 }); }); QUnit.test("Transient messages are added at the end of the thread", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); const { openDiscuss } = await start(); openDiscuss(channelId); await insertText(".o-mail-Composer-input", "Dummy Message"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message"); await insertText(".o-mail-Composer-input", "/help"); await click(".o-mail-Composer-send:enabled"); await contains(".o-mail-Message", { count: 2 }); await contains(":nth-child(1 of .o-mail-Message)", { text: "Mitchell Admin" }); await contains(":nth-child(2 of .o-mail-Message)", { text: "TalismanBot" }); });