import { SIZES, assertSteps, click, contains, defineMailModels, focus, inputFiles, insertText, onRpcBefore, openDiscuss, openFormView, openListView, patchUiSize, scroll, start, startServer, step, triggerHotkey, } from "@mail/../tests/mail_test_helpers"; import { describe, expect, test } from "@odoo/hoot"; import { mockDate, tick } from "@odoo/hoot-mock"; import { EventBus } from "@odoo/owl"; import { Command, getService, patchWithCleanup, preloadBundle, serverState, withUser, } from "@web/../tests/web_test_helpers"; import { browser } from "@web/core/browser/browser"; import { rpc } from "@web/core/network/rpc"; describe.current.tags("desktop"); defineMailModels(); preloadBundle("web.assets_emoji"); test("Mobile: chat window shouldn't open automatically after receiving a new message", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); const userId = pyEnv["res.users"].create({ partner_id: partnerId }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ partner_id: serverState.partnerId }), Command.create({ partner_id: partnerId }), ], channel_type: "chat", }); patchUiSize({ size: SIZES.SM }); await start(); await contains(".o_menu_systray i[aria-label='Messages']"); await contains(".o-mail-MessagingMenu-counter", { count: 0 }); // simulate receiving a message withUser(userId, () => rpc("/mail/message/post", { post_data: { body: "hu", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-MessagingMenu-counter", { text: "1" }); await contains(".o-mail-ChatWindow", { count: 0 }); }); test('chat window: post message on channel with "CTRL-Enter" keyboard shortcut for small screen size', async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId }), ], }); patchUiSize({ size: SIZES.SM }); await start(); await openDiscuss(channelId); await insertText(".o-mail-ChatWindow .o-mail-Composer-input", "Test"); triggerHotkey("control+Enter"); await contains(".o-mail-Message"); }); test("Message post in chat window of chatter should log a note", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "TestPartner" }); const messageId = pyEnv["mail.message"].create({ model: "res.partner", body: "A needaction message to have it in messaging menu", author_id: serverState.odoobotId, needaction: true, res_id: partnerId, }); pyEnv["mail.notification"].create({ mail_message_id: messageId, notification_status: "sent", notification_type: "inbox", res_partner_id: serverState.partnerId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); await contains(".o-mail-Message", { text: "A needaction message to have it in messaging menu", contains: [".o-mail-Message-bubble"], // bubble = "Send message" mode }); await contains(".o-mail-Composer [placeholder='Log an internal noteā€¦']"); await insertText(".o-mail-ChatWindow .o-mail-Composer-input", "Test"); triggerHotkey("control+Enter"); await contains(".o-mail-Message", { text: "Test", contains: [".o-mail-Message-bubble", { count: 0 }], // no bubble = "Log note" mode }); }); test("Chatter in chat window should scroll to most recent message", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "TestPartner" }); // Fill both channels with random messages in order for the scrollbar to // appear. pyEnv["mail.message"].create( Array(50) .fill(0) .map((_, index) => ({ model: "res.partner", body: "Non Empty Body ".repeat(25), author_id: serverState.odoobotId, needaction: true, res_id: partnerId, })) ); const lastMessageId = pyEnv["mail.message"].create({ model: "res.partner", body: "A needaction message to have it in messaging menu", author_id: serverState.odoobotId, needaction: true, res_id: partnerId, }); pyEnv["mail.notification"].create({ mail_message_id: lastMessageId, notification_status: "sent", notification_type: "inbox", res_partner_id: serverState.partnerId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-Thread", { scroll: "bottom" }); }); test("load messages from opening chat window from messaging menu", 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 <= 20; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 21 }); }); test("chat window: basic rendering", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "General" }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); await contains(".o-mail-ChatWindow-header", { text: "General" }); await contains(".o-mail-ChatWindow-header .o-mail-ChatWindow-threadAvatar"); await contains(".o-mail-ChatWindow-command", { count: 5 }); await contains("[title='Start a Call']"); await contains("[title='Start a Video Call']"); await contains("[title='Open Actions Menu']"); await contains("[title='Fold']"); await contains("[title*='Close Chat Window']"); await contains(".o-mail-ChatWindow .o-mail-Thread", { text: "The conversation is empty." }); await click("[title='Open Actions Menu']"); await contains(".o-mail-ChatWindow-command", { count: 15 }); await contains(".o-dropdown-item", { text: "Attachments" }); await contains(".o-dropdown-item", { text: "Pinned Messages" }); await contains(".o-dropdown-item", { text: "Members" }); await contains(".o-dropdown-item", { text: "Threads" }); await contains(".o-dropdown-item", { text: "Invite People" }); await contains(".o-dropdown-item", { text: "Search Messages" }); await contains(".o-dropdown-item", { text: "Rename Thread" }); await contains(".o-dropdown-item", { text: "Open in Discuss" }); await contains(".o-dropdown-item", { text: "Notification Settings" }); await contains(".o-dropdown-item", { text: "Call Settings" }); }); test.skip("Fold state of chat window is sync among browser tabs", async () => { // AKU TODO: fix crosstab const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "General" }); const env1 = await start({ asTab: true }); const env2 = await start({ asTab: true }); await click(".o_menu_systray i[aria-label='Messages']", { target: env1 }); await click(".o-mail-NotificationItem", { target: env1 }); await contains(".o-mail-ChatWindow-header", { target: env2 }); await click(".o-mail-ChatWindow-header", { target: env1 }); // Fold await contains(".o-mail-Thread", { count: 0, target: env1 }); await contains(".o-mail-Thread", { count: 0, target: env2 }); await click(".o-mail-ChatBubble", { target: env2 }); // Unfold await contains(".o-mail-ChatWindow .o-mail-Thread", { target: env1 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { target: env2 }); await click("[title*='Close Chat Window']", { target: env1 }); await contains(".o-mail-ChatWindow", { count: 0, target: env1 }); await contains(".o-mail-ChatWindow", { count: 0, target: env2 }); }); test("Mobile: opening a chat window should not update channel state on the server", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "closed", partner_id: serverState.partnerId }), ], }); patchUiSize({ size: SIZES.SM }); await start(); await openDiscuss(channelId); await click(".o-mail-ChatWindow"); const [member] = pyEnv["discuss.channel.member"].search_read([ ["channel_id", "=", channelId], ["partner_id", "=", serverState.partnerId], ]); expect(member.fold_state).toBe("closed"); }); test("chat window: fold", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({}); onRpcBefore("/discuss/channel/fold", (args) => step(`channel_fold/${args.state}`)); await start(); // Open Thread await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow .o-mail-Thread"); await assertSteps(["channel_fold/open"]); // Fold chat window await click(".o-mail-ChatWindow-command[title='Fold']"); await contains(".o-mail-ChatWindow .o-mail-Thread", { count: 0 }); await assertSteps(["channel_fold/folded"]); // Unfold chat window await click(".o-mail-ChatBubble"); await contains(".o-mail-ChatWindow .o-mail-Thread"); await assertSteps(["channel_fold/open"]); }); test("chat window: open / close", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({}); onRpcBefore("/discuss/channel/fold", (args) => step(`channel_fold/${args.state}`)); await start(); await click("button i[aria-label='Messages']"); await contains(".o-mail-ChatWindow", { count: 0 }); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); await assertSteps(["channel_fold/open"]); await click(".o-mail-ChatWindow-command[title*='Close Chat Window']"); await contains(".o-mail-ChatWindow", { count: 0 }); await assertSteps(["channel_fold/closed"]); // Reopen chat window await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); await assertSteps(["channel_fold/open"]); }); test("Open chatwindow as a non member", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [], }); const messageId = pyEnv["mail.message"].create({ model: "discuss.channel", body: "A needaction message to have it in messaging menu", author_id: serverState.odoobotId, needaction: true, res_id: channelId, }); pyEnv["mail.notification"].create({ mail_message_id: messageId, notification_status: "sent", notification_type: "inbox", res_partner_id: serverState.partnerId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); }); test("open chat on very narrow device should work", async () => { const pyEnv = await startServer(); patchUiSize({ width: 200 }); pyEnv["discuss.channel"].create({}); await start(); const store = getService("mail.store"); expect(store.chatHub.WINDOW).toBeGreaterThan(200, { message: "Device is narrower than usual chat window width", }); // scenario where this might fail await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow"); }); test("Mobile: closing a chat window should not update channel state on the server", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId }), ], }); patchUiSize({ size: SIZES.SM }); await start(); await openDiscuss(channelId); await click("[title*='Close Chat Window']"); await contains(".o-mail-ChatWindow", { count: 0 }); const [member] = pyEnv["discuss.channel.member"].search_read([ ["channel_id", "=", channelId], ["partner_id", "=", serverState.partnerId], ]); expect(member.fold_state).toBe("open"); }); test("chat window: close on ESCAPE", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId }), ], }); onRpcBefore("/discuss/channel/fold", (args) => step(`channel_fold/${args.state}`)); await start(); await contains(".o-mail-ChatWindow"); await focus(".o-mail-Composer-input"); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow", { count: 0 }); await assertSteps(["channel_fold/closed"]); }); test("chat window: close on ESCAPE (multi)", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create( Array(4) .keys() .map((i) => ({ name: `channel_${i}`, channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId }), ], })) ); patchUiSize({ width: 1920 }); await start(); await contains(".o-mail-ChatWindow", { count: 4 }); // expected order: 3, 2, 1, 0 await contains(".o-mail-ChatWindow:eq(0)", { text: "channel_3" }); await contains(".o-mail-ChatWindow:eq(1)", { text: "channel_2" }); await contains(".o-mail-ChatWindow:eq(2)", { text: "channel_1" }); await contains(".o-mail-ChatWindow:eq(3)", { text: "channel_0" }); await focus(".o-mail-Composer-input:eq(3)"); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow", { count: 3 }); await contains(".o-mail-ChatWindow:eq(0)", { text: "channel_3" }); await contains(".o-mail-ChatWindow:eq(1)", { text: "channel_2" }); await contains(".o-mail-ChatWindow:eq(2)", { text: "channel_1" }); await contains(".o-mail-ChatWindow:eq(2) .o-mail-Composer.o-focused"); await focus(".o-mail-Composer-input:eq(0)"); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow", { count: 2 }); await contains(".o-mail-ChatWindow:eq(0)", { text: "channel_2" }); await contains(".o-mail-ChatWindow:eq(1)", { text: "channel_1" }); await contains(".o-mail-ChatWindow:eq(0) .o-mail-Composer.o-focused"); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow", { count: 1 }); await contains(".o-mail-ChatWindow", { text: "channel_1" }); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow", { count: 0 }); }); test("Close composer suggestions in chat window with ESCAPE does not also close the chat window", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ email: "testpartner@odoo.com", name: "TestPartner", }); pyEnv["res.users"].create({ partner_id: partnerId }); pyEnv["discuss.channel"].create({ name: "general", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId, fold_state: "open" }), Command.create({ partner_id: partnerId }), ], }); await start(); await insertText(".o-mail-Composer-input", "@"); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow"); }); test("Close emoji picker in chat window with ESCAPE does not also close the chat window", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "general", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId, fold_state: "open" }), ], }); await start(); await click("button[aria-label='Emojis']"); triggerHotkey("Escape"); await contains(".o-EmojiPicker", { count: 0 }); await contains(".o-mail-ChatWindow"); }); test("Close active thread action in chatwindow on ESCAPE", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "General", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId, fold_state: "open" }), ], }); await start(); await contains(".o-mail-ChatWindow"); await click(".o-mail-ChatWindow-command", { text: "General" }); await click(".o-dropdown-item", { text: "Invite People" }); await contains(".o-discuss-ChannelInvitation"); triggerHotkey("Escape"); await contains(".o-discuss-ChannelInvitation", { count: 0 }); await contains(".o-mail-ChatWindow"); }); test("ESC cancels thread rename", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "General", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId, fold_state: "open" }), ], }); await start(); await click(".o-mail-ChatWindow-command", { text: "General" }); await click(".o-dropdown-item", { text: "Rename Thread" }); await contains(".o-mail-AutoresizeInput.o-focused[title='General']"); await insertText(".o-mail-AutoresizeInput", "New", { replace: true }); triggerHotkey("Escape"); await contains(".o-mail-AutoresizeInput.o-focused", { count: 0 }); await contains(".o-mail-ChatWindow-command", { text: "General" }); }); test.tags("focus required"); test("open 2 different chat windows: enough screen width", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create([{ name: "Channel_1" }, { name: "Channel_2" }]); patchUiSize({ width: 1920 }); await start(); const store = getService("mail.store"); expect( store.chatHub.WINDOW_GAP * 2 + store.chatHub.WINDOW * 2 + store.chatHub.WINDOW_INBETWEEN ).toBeLessThan(1920, { message: "should have enough space to open 2 chat windows simultaneously", }); await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "Channel_1" }); await contains(".o-mail-ChatWindow", { text: "Channel_1", contains: [".o-mail-Composer-input:focus"], }); await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "Channel_2" }); await contains(".o-mail-ChatWindow", { count: 2 }); await contains(".o-mail-ChatWindow", { text: "Channel_1" }); await contains(".o-mail-ChatWindow", { text: "Channel_2", contains: [".o-mail-Composer-input:focus"], }); }); test.tags("focus required"); test("focus next visible chat window when closing current chat window with ESCAPE", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create([ { name: "General", channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }, { name: "MyTeam", channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }, ]); patchUiSize({ width: 1920 }); await start(); const store = getService("mail.store"); expect( store.chatHub.WINDOW_GAP * 2 + store.chatHub.WINDOW * 2 + store.chatHub.WINDOW_INBETWEEN ).toBeLessThan(1920, { message: "should have enough space to open 2 chat windows simultaneously", }); await contains(".o-mail-ChatWindow .o-mail-Composer-input", { count: 2 }); await focus(".o-mail-Composer-input", { parent: [".o-mail-ChatWindow", { text: "MyTeam" }], }); triggerHotkey("Escape"); await contains(".o-mail-ChatWindow"); await contains(".o-mail-ChatWindow", { text: "General", contains: [".o-mail-Composer-input:focus"], }); }); test.tags("focus required"); test("chat window: switch on TAB", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create([{ name: "channel1" }, { name: "channel2" }]); patchUiSize({ width: 1920 }); await start(); const store = getService("mail.store"); expect( store.chatHub.WINDOW_GAP * 2 + store.chatHub.WINDOW * 2 + store.chatHub.WINDOW_INBETWEEN ).toBeLessThan(1920, { message: "should have enough space to open 2 chat windows simultaneously", }); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "channel1" }); await contains(".o-mail-ChatWindow", { count: 1 }); await contains(".o-mail-ChatWindow", { text: "channel1", contains: [".o-mail-Composer-input:focus"], }); triggerHotkey("Tab"); await contains(".o-mail-ChatWindow", { text: "channel1", contains: [".o-mail-Composer-input:focus"], }); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "channel2" }); await contains(".o-mail-ChatWindow", { count: 2 }); await contains(".o-mail-ChatWindow", { text: "channel2", contains: [".o-mail-Composer-input:focus"], }); triggerHotkey("Tab"); await contains(".o-mail-ChatWindow", { text: "channel1", contains: [".o-mail-Composer-input:focus"], }); }); test.tags("focus required"); test("chat window: TAB cycle with 3 open chat windows", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create([ { name: "General", channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }, { name: "MyTeam", channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }, { name: "MyProject", channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }, ]); patchUiSize({ width: 1920 }); await start(); const store = getService("mail.store"); expect( store.chatHub.WINDOW_GAP * 3 + store.chatHub.WINDOW * 3 + store.chatHub.WINDOW_INBETWEEN * 2 ).toBeLessThan(1920, { message: "should have enough space to open 3 chat windows simultaneously", }); // FIXME: assumes ordering: MyProject, MyTeam, General await contains(".o-mail-ChatWindow .o-mail-Composer-input", { count: 3 }); await focus(".o-mail-Composer-input", { parent: [".o-mail-ChatWindow", { text: "MyProject" }], }); triggerHotkey("Tab"); await contains(".o-mail-ChatWindow", { text: "MyTeam", contains: [".o-mail-Composer-input:focus"], }); triggerHotkey("Tab"); await contains(".o-mail-ChatWindow", { text: "General", contains: [".o-mail-Composer-input:focus"], }); triggerHotkey("Tab"); await contains(".o-mail-ChatWindow", { text: "MyProject", contains: [".o-mail-Composer-input:focus"], }); }); test("chat window should open when receiving a new DM", async () => { mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!) 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({ channel_member_ids: [ Command.create({ unpin_dt: "2021-01-01 12:00:00", last_interest_dt: "2021-01-01 10:00:00", partner_id: serverState.partnerId, }), Command.create({ partner_id: partnerId }), ], channel_type: "chat", }); onRpcBefore("/mail/data", async (args) => { if (args.init_messaging) { step("init_messaging"); } }); await start(); await assertSteps(["init_messaging"]); await contains(".o-mail-ChatHub"); withUser(userId, () => rpc("/mail/message/post", { post_data: { body: "Hi, are you here?", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-ChatBubble"); await contains(".o-mail-ChatBubble-counter", { text: "1" }); }); test("chat window should not open when receiving a new DM from odoobot", async () => { mockDate("2023-01-03 12:00:00"); // so that it's after last interest (mock server is in 2019 by default!) const pyEnv = await startServer(); const userId = pyEnv["res.users"].create({ partner_id: serverState.odoobotId }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ unpin_dt: "2021-01-01 12:00:00", last_interest_dt: "2021-01-01 10:00:00", partner_id: serverState.partnerId, }), Command.create({ partner_id: serverState.odoobotId }), ], channel_type: "chat", }); await start(); await contains(".o-mail-ChatHub"); withUser(userId, () => rpc("/mail/message/post", { post_data: { body: "Hello, I'm new", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-ChatWindow", { count: 0 }); }); test("chat window should scroll to the newly posted message just after posting it", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }); for (let i = 0; i < 10; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } await start(); await contains(".o-mail-Message", { count: 10 }); await insertText(".o-mail-Composer-input", "WOLOLO"); triggerHotkey("Enter"); await contains(".o-mail-Message", { count: 11 }); await contains(".o-mail-Thread", { scroll: "bottom" }); }); test("chat window should remain folded when new message is received", async () => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); const userId = pyEnv["res.users"].create({ name: "Foreigner user", partner_id: partnerId, }); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "folded", partner_id: serverState.partnerId, }), Command.create({ partner_id: partnerId }), ], channel_type: "chat", }); await start(); await contains(".o-mail-ChatBubble"); await contains(".o-mail-ChatBubble-counter", { count: 0 }); withUser(userId, () => rpc("/mail/message/post", { post_data: { body: "New Message", message_type: "comment" }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-ChatBubble-counter", { text: "1" }); await contains(".o-mail-ChatBubble"); }); test("chat window: composer state conservation on toggle discuss", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({}); const textFile1 = new File( ["hello, world"], "text state conversation on toggle home menu.txt", { type: "text/plain" } ); const textFile2 = new File( ["hello, xdu is da best man"], "text2 state conversation on toggle home menu.txt", { type: "text/plain" } ); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); // Set content of the composer of the chat window await insertText(".o-mail-Composer-input", "XDU for the win !"); await contains(".o-mail-Composer-footer .o-mail-AttachmentList .o-mail-AttachmentCard", { count: 0, }); // Set attachments of the composer await inputFiles(".o-mail-Composer-coreMain .o_input_file", [textFile1, textFile2]); await contains(".o-mail-AttachmentCard .fa-check", { count: 2 }); await openDiscuss(); await contains(".o-mail-ChatWindow", { count: 0 }); await openFormView("discuss.channel", channelId); await contains(".o-mail-Composer-footer .o-mail-AttachmentList .o-mail-AttachmentCard", { count: 2, }); await contains(".o-mail-Composer-input", { value: "XDU for the win !" }); }); test("chat window: scroll conservation on toggle discuss", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({}); for (let i = 0; i < 100; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } await start(); await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 0 }); await tick(); // wait for the scroll to first unread to complete await scroll(".o-mail-ChatWindow .o-mail-Thread", 142); await openDiscuss(); await contains(".o-mail-ChatWindow", { count: 0 }); await openListView("discuss.channel", { res_id: channelId }); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 142 }); }); test("chat window with a thread: keep scroll position in message list on folded", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({}); for (let i = 0; i < 100; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } await start(); await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 0 }); await tick(); // wait for the scroll to first unread to complete await scroll(".o-mail-ChatWindow .o-mail-Thread", 142); // fold chat window await click(".o-mail-ChatWindow-command[title='Fold']"); await contains(".o-mail-Message", { count: 0 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { count: 0 }); // unfold chat window await click(".o-mail-ChatBubble"); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 142 }); }); test("chat window with a thread: keep scroll position in message list on toggle discuss when folded", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({}); for (let i = 0; i < 100; i++) { pyEnv["mail.message"].create({ body: "not empty", model: "discuss.channel", res_id: channelId, }); } await start(); await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])"); await click(".o-mail-NotificationItem"); await contains(".o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 0 }); await tick(); // wait for the scroll to first unread to complete await scroll(".o-mail-ChatWindow .o-mail-Thread", 142); // fold chat window await click(".o-mail-ChatWindow-command[title='Fold']"); await openDiscuss(); await contains(".o-mail-ChatWindow", { count: 0 }); await openListView("discuss.channel", { res_id: channelId }); // unfold chat window await click(".o-mail-ChatBubble"); await contains(".o-mail-ChatWindow .o-mail-Message", { count: 30 }); await contains(".o-mail-ChatWindow .o-mail-Thread", { scroll: 142 }); }); test("folded chat window should hide member-list and settings buttons", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({}); await start(); // Open Thread await click("button i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await click("[title='Open Actions Menu']"); await contains(".o-dropdown-item", { text: "Members" }); await contains(".o-dropdown-item", { text: "Call Settings" }); await click(".o-mail-ChatWindow-header"); // click away to close the more menu await contains(".o-dropdown-item", { text: "Members", count: 0 }); // Fold chat window await click(".o-mail-ChatWindow-command[title='Fold']"); await contains("[title='Open Actions Menu']", { count: 0 }); await contains(".o-dropdown-item", { text: "Members", count: 0 }); await contains(".o-dropdown-item", { text: "Call Settings", count: 0 }); // Unfold chat window await click(".o-mail-ChatBubble"); await click("[title='Open Actions Menu']"); await contains(".o-dropdown-item", { text: "Members" }); await contains(".o-dropdown-item", { text: "Call Settings" }); }); test("Chat window in mobile are not foldable", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId }), ], }); patchUiSize({ size: SIZES.SM }); await start(); await openDiscuss(channelId); await contains(".o-mail-ChatWindow"); await contains(".o-mail-ChatWindow-header.cursor-pointer", { count: 0 }); await click(".o-mail-ChatWindow-header"); await contains(".o-mail-Thread"); // content => non-folded }); test("Server-synced chat windows should not open at page load on mobile", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ channel_member_ids: [ Command.create({ fold_state: "open", partner_id: serverState.partnerId, }), ], }); patchUiSize({ size: SIZES.SM }); await start(); await contains(".o-mail-ChatHub"); await contains(".o-mail-ChatWindow", { count: 0 }); }); test("chat window of channels should not have 'Open in Discuss' (mobile)", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); patchUiSize({ size: SIZES.SM }); await start(); await openDiscuss(channelId); await contains("[title='Open Actions Menu']"); await click("[title='Open Actions Menu']"); await contains(".o-dropdown-item", { text: "Open in Discuss", count: 0 }); }); test("Open chat window of new inviter", async () => { const pyEnv = await startServer(); await start(); const partnerId = pyEnv["res.partner"].create({ name: "Newbie" }); pyEnv["res.users"].create({ partner_id: partnerId }); // simulate receiving notification of new connection of inviting user const [partner] = pyEnv["res.partner"].read(serverState.partnerId); pyEnv["bus.bus"]._sendone(partner, "res.users/connection", { username: "Newbie", partnerId, }); await contains(".o-mail-ChatWindow", { text: "Newbie" }); await contains(".o_notification", { text: "Newbie connected. This is their first connection. Wish them luck.", }); }); test.tags("focus required"); test("keyboard navigation ArrowUp/ArrowDown on message action dropdown in chat window", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); pyEnv["mail.message"].create({ author_id: serverState.partnerId, body: "not empty", model: "discuss.channel", res_id: channelId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await contains(".o-mail-ChatWindow .o-mail-Composer-input:focus"); await click(".o-mail-Message [title='Expand']"); await contains(".o-mail-Message-moreMenu.dropdown-menu"); await focus(".o-mail-Message [title='Expand']"); // necessary otherwise focus is in composer input triggerHotkey("ArrowDown"); await contains(".o-mail-Message-moreMenu :nth-child(1 of .dropdown-item).focus"); triggerHotkey("ArrowDown"); await contains(".o-mail-Message-moreMenu :nth-child(2 of .dropdown-item).focus"); }); test("Close dropdown in chat window with ESCAPE does not also close the chat window", async () => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); pyEnv["mail.message"].create({ author_id: serverState.partnerId, body: "not empty", model: "discuss.channel", res_id: channelId, }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem"); await click(".o-mail-Message [title='Expand']"); await contains(".o-mail-Message-moreMenu.dropdown-menu"); await focus(".o-mail-Message [title='Expand']"); // necessary otherwise focus is in composer input triggerHotkey("Escape"); await contains(".o-mail-Message-moreMenu.dropdown-menu", { count: 0 }); await contains(".o-mail-ChatWindow"); }); test("mark as read when opening chat window", async () => { const pyEnv = await startServer(); const bobPartnerId = pyEnv["res.partner"].create({ name: "bob" }); const bobUserId = pyEnv["res.users"].create({ name: "bob", partner_id: bobPartnerId }); const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId }), Command.create({ partner_id: bobPartnerId }), ], }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "bob" }); await contains(".o-mail-ChatWindow .o-mail-ChatWindow-header", { text: "bob" }); // composer is focused by default, we remove that focus await contains(".o-mail-Composer-input:focus"); document.querySelector(".o-mail-Composer-input").blur(); await contains(".o-mail-Composer-input:not(:focus"); await withUser(bobUserId, () => rpc("/mail/message/post", { post_data: { body: "Hello, how are you?", message_type: "comment", subtype_xmlid: "mail.mt_comment", }, thread_id: channelId, thread_model: "discuss.channel", }) ); await contains(".o-mail-ChatWindow-counter", { text: "1" }); await click(".o-mail-ChatWindow-command[title*='Close Chat Window']"); await contains(".o-mail-ChatWindow", { count: 0 }); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "bob" }); await contains(".o-mail-ChatWindow .o-mail-ChatWindow-header", { text: "bob" }); await contains(".o-mail-ChatWindow-counter", { count: 0 }); }); test("Notification settings rendering in chatwindow", async () => { const pyEnv = await startServer(); pyEnv["discuss.channel"].create({ name: "general", channel_type: "channel", channel_member_ids: [Command.create({ partner_id: serverState.partnerId })], }); await start(); await click(".o_menu_systray i[aria-label='Messages']"); await click(".o-mail-NotificationItem", { text: "general" }); await contains(".o-mail-ChatWindow", { count: 1 }); await click("[title='Open Actions Menu']"); await click(".o-dropdown-item", { text: "Notification Settings" }); await contains("button", { text: "All Messages" }); await contains("button", { text: "Mentions Only", count: 2 }); // the extra is in the Use Default as subtitle await contains("button", { text: "Nothing" }); await click("button", { text: "Mute Conversation" }); await contains("button", { text: "For 15 minutes" }); await contains("button", { text: "For 1 hour" }); await contains("button", { text: "For 3 hours" }); await contains("button", { text: "For 8 hours" }); await contains("button", { text: "For 24 hours" }); await contains("button", { text: "Until I turn it back on" }); }); test("open channel in chat window from push notification", async () => { patchWithCleanup(window.navigator, { serviceWorker: Object.assign(new EventBus(), { register: () => Promise.resolve() }), }); const pyEnv = await startServer(); const [channelId] = pyEnv["discuss.channel"].create([ { name: "General" }, { name: "Sales", channel_member_ids: [ Command.create({ partner_id: serverState.partnerId, fold_state: "open" }), ], }, ]); await start(); await contains(".o-mail-ChatWindow", { text: "Sales" }); await contains(".o-mail-ChatWindow", { text: "General", count: 0 }); browser.navigator.serviceWorker.dispatchEvent( new MessageEvent("message", { data: { action: "OPEN_CHANNEL", data: { id: channelId } }, }) ); await contains(".o-mail-ChatWindow", { text: "General" }); });