19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -0,0 +1,41 @@
import {
click,
contains,
mockGetMedia,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import { test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { Command, serverState } from "@web/../tests/web_test_helpers";
defineLivechatModels();
test.tags("desktop");
test("should display started a call message with operator livechat username", async () => {
mockDate("2025-01-01 12:00:00", +1);
mockGetMedia();
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, {
user_livechat_username: "mitchell boss",
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
setupChatHub({ opened: [channelId] });
await start();
await contains(".o-mail-ChatWindow", { text: "Visitor" });
await click("[title='Start Call']");
await contains(".o-mail-NotificationMessage", { text: "mitchell boss started a call.1:00 PM" });
});

View file

@ -0,0 +1,237 @@
import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
import { mockDate } from "@odoo/hoot-mock";
describe.current.tags("desktop");
defineLivechatModels();
test("Can invite a partner to a livechat channel", async () => {
mockDate("2023-01-03 12:00:00", +1);
const pyEnv = await startServer();
const langIds = pyEnv["res.lang"].create([
{ code: "en", name: "English" },
{ code: "fr", name: "French" },
{ code: "de", name: "German" },
]);
const expertiseIds = pyEnv["im_livechat.expertise"].create([
{ name: "pricing" },
{ name: "events" },
]);
pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Mitch (FR)" });
const userId = pyEnv["res.users"].create({
name: "James",
livechat_lang_ids: langIds,
livechat_expertise_ids: expertiseIds,
});
pyEnv["res.partner"].create({
lang: "en",
name: "James",
user_ids: [userId],
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" });
const channelId = pyEnv["discuss.channel"].create({
name: "Visitor 20",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel
await click("button[title='Invite People']");
await click("input", {
parent: [".o-discuss-ChannelInvitation-selectable", { text: "James" }],
});
await contains(
".o-discuss-ChannelInvitation-selectable:contains('James English French German pricing events')"
);
await click("button:enabled", { text: "Invite" });
await contains(".o-mail-NotificationMessage", {
text: "Mitch (FR) invited James to the channel1:00 PM",
});
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await click("button[title='Members']");
await contains(".o-discuss-ChannelMember", { text: "James" });
});
test("Available operators come first", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].create({
name: "Harry",
im_status: "offline",
user_ids: [pyEnv["res.users"].create({ name: "Harry" })],
});
const ronId = pyEnv["res.partner"].create({
name: "Ron",
im_status: "online",
user_ids: [pyEnv["res.users"].create({ name: "Available operator" })],
});
pyEnv["im_livechat.channel"].create({
available_operator_ids: [Command.create({ partner_id: ronId })],
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #1" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel
await click("button[title='Invite People']");
await contains(".o-discuss-ChannelInvitation-selectable", { count: 2 });
await contains(":nth-child(1 of .o-discuss-ChannelInvitation-selectable)", { text: "Ron" });
await contains(":nth-child(2 of .o-discuss-ChannelInvitation-selectable)", { text: "Harry" });
});
test("Partners invited most frequently by the current user come first", async () => {
mockDate("2023-01-03 12:00:00");
const pyEnv = await startServer();
pyEnv["res.partner"].create({
name: "John",
im_status: "offline",
user_ids: [pyEnv["res.users"].create({ name: "John" })],
});
pyEnv["res.partner"].create({
name: "Albert",
im_status: "offline",
user_ids: [pyEnv["res.users"].create({ name: "Albert" })],
});
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor #1" });
pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId_1,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "visitor",
}),
],
livechat_operator_id: serverState.partnerId,
});
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor #2" });
pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-03 11:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId_2,
last_interest_dt: "2021-01-03 11:00:00",
livechat_member_type: "visitor",
}),
],
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #1" });
await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel
await click("button[title='Invite People']");
await click("input", { parent: [".o-discuss-ChannelInvitation-selectable", { text: "John" }] });
await click("button:enabled", { text: "Invite" });
await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #2" });
await click("button[title='Invite People']");
await contains(".o-discuss-ChannelInvitation-selectable", { count: 2 });
await contains(":nth-child(1 of .o-discuss-ChannelInvitation-selectable)", { text: "John" });
await contains(":nth-child(2 of .o-discuss-ChannelInvitation-selectable)", { text: "Albert" });
});
test("shows operators are in call", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #1" });
const [bobPartnerId] = pyEnv["res.partner"].create([
{ name: "bob", user_ids: [Command.create({ name: "bob" })] },
{ name: "john", user_ids: [Command.create({ name: "john" })] },
]);
const bobChannelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: bobPartnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
});
const [bobMemberId] = pyEnv["discuss.channel.member"].search([
["partner_id", "=", bobPartnerId],
["channel_id", "=", bobChannelId],
]);
pyEnv["discuss.channel.rtc.session"].create({
channel_id: bobChannelId,
channel_member_id: bobMemberId,
});
pyEnv["res.partner"]._compute_is_in_call();
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel
await click("[title='Invite People']");
await contains(".o-discuss-ChannelInvitation-selectable:contains('bob in a call')");
await contains(".o-discuss-ChannelInvitation-selectable:contains('john')");
await contains(".o-discuss-ChannelInvitation-selectable:contains('john in a call')", {
count: 0,
});
});
test("Operator invite shows livechat_username", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].create({
name: "John",
im_status: "offline",
user_ids: [pyEnv["res.users"].create({ name: "John" })],
user_livechat_username: "Johnny",
});
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor #1" });
pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId_1,
last_interest_dt: "2021-01-03 12:00:00",
livechat_member_type: "visitor",
}),
],
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel", { text: "Visitor #1" });
await contains(".o-livechat-ChannelInfoList"); // wait for auto-open of this panel
await click("button[title='Invite People']");
await contains("input", {
parent: [".o-discuss-ChannelInvitation-selectable", { text: "Johnny" }],
});
});

View file

@ -0,0 +1,151 @@
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import {
click,
contains,
openDiscuss,
setupChatHub,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { rpc } from "@web/core/network/rpc";
import { serializeDate, today } from "@web/core/l10n/dates";
import { livechatLastAgentLeaveFromChatWindow } from "./im_livechat_shared_tests";
describe.current.tags("desktop");
defineLivechatModels();
test("from the discuss app", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const [guestId_1, guestId_2] = pyEnv["mail.guest"].create([
{ name: "guest_1" },
{ name: "guest_2" },
]);
const livechatChannelId = pyEnv["im_livechat.channel"].create({
name: "HR",
user_ids: [serverState.userId],
});
pyEnv["discuss.channel"].create([
{
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
livechat_end_dt: false,
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
},
{
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
livechat_end_dt: serializeDate(today()),
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
},
]);
await start();
await openDiscuss();
await contains(
".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']"
);
await click("[title='Leave HR']", {
parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }],
});
await contains(
".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']",
{ count: 0 }
);
await click("[title='Join HR']", {
parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }],
});
await contains(
".o-mail-DiscussSidebarCategory-livechat:has(:text('HR')) .fa-circle[title='You have joined this live chat channel']"
);
await click("[title='Chat Actions']", {
parent: [".o-mail-DiscussSidebarChannel", { text: "guest_1" }],
});
await click(".o-dropdown-item:contains('Leave Channel')");
await click("button:contains(Leave Conversation)");
await contains(".o-mail-DiscussSidebarChannel", { text: "guest_1", count: 0 });
await click("[title='Chat Actions']", {
parent: [".o-mail-DiscussSidebarChannel", { text: "guest_2" }],
});
await click(".o-dropdown-item:contains('Leave Channel')");
await contains(".o-mail-DiscussSidebarChannel", { text: "guest_2", count: 0 });
await click("[title='Leave HR']", {
parent: [".o-mail-DiscussSidebarCategory-livechat", { text: "HR" }],
});
await contains(".o-mail-DiscussSidebarCategory-livechat", { text: "HR", count: 0 });
});
test("from the command palette", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
pyEnv["im_livechat.channel"].create({ name: "HR", user_ids: [serverState.userId] });
await start();
await triggerHotkey("control+k");
await click(".o_command", { text: "Leave HR" });
await contains(".o_notification", { text: "You left HR." });
await contains(".o_command", { text: "HR", count: 0 });
await triggerHotkey("control+k");
await click(".o_command", { text: "Join HR" });
await contains(".o_notification", { text: "You joined HR." });
});
test("from chat window", livechatLastAgentLeaveFromChatWindow);
test("visitor leaving ends the livechat conversation", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
const livechatChannelId = pyEnv["im_livechat.channel"].create({
name: "HR",
user_ids: [serverState.userId],
});
const channel_id = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
setupChatHub({ opened: [channel_id] });
await start();
await contains(".o-mail-ChatWindow");
// simulate visitor leaving
await withGuest(guestId, () => rpc("/im_livechat/visitor_leave_session", { channel_id }));
await contains("span", { text: "This livechat conversation has ended" });
await click("button[title*='Close Chat Window']");
await contains(".o-mail-ChatWindow", { count: 0 });
});

View file

@ -0,0 +1,34 @@
import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("display country in channel member list", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({
name: "Visitor #20",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-ActionPanel:contains(Information)");
await click(".o-mail-DiscussContent-header button[name='member-list']");
await contains(".o-discuss-ChannelMember span", { text: "Belgium", count: 2 });
});

View file

@ -0,0 +1,46 @@
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { click, contains } from "@mail/../tests/mail_test_helpers_contains";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import { setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineLivechatModels();
test("Do not open chat windows automatically when chat hub is compact", async () => {
const pyEnv = await startServer();
setupChatHub({ folded: [pyEnv["discuss.channel"].create({ name: "General" })] });
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
await start();
await click("button[title='Chat Options']");
await click(".o-dropdown-item", { text: "Hide all conversations" });
await contains(".o-mail-ChatHub-bubbleBtn .fa-comments");
await withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "I need help!",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-ChatHub-bubbleBtn .badge", { text: "1" });
await click("button.o-mail-ChatHub-bubbleBtn");
await contains(".o-mail-ChatBubble[name=Visitor] .badge", { text: "1" });
await contains(".o-mail-ChatWindow", { count: 0, text: "Visitor" });
await click(".o-mail-ChatBubble[name=Visitor] .o-mail-ChatHub-bubbleBtn");
await contains(".o-mail-ChatWindow", { text: "Visitor" });
});

View file

@ -0,0 +1,214 @@
import {
click,
contains,
openDiscuss,
openFormView,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { defineLivechatModels } from "./livechat_test_helpers";
import { serializeDate, today } from "@web/core/l10n/dates";
defineLivechatModels();
test.tags("mobile");
test("can fold livechat chat windows in mobile", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Visitor" });
pyEnv["res.users"].create([{ partner_id: partnerId }]);
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
unpin_dt: false,
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ partner_id: partnerId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem", { text: "Visitor" });
await click(".o-mail-ChatWindow-header [title*='Fold']", {
parent: [".o-mail-ChatWindow", { text: "Visitor" }],
});
await contains(".o-mail-ChatBubble");
});
test.tags("desktop");
test("closing a chat window with no message from admin side unpins it", async () => {
const pyEnv = await startServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ name: "Partner 1" },
{ name: "Partner 2" },
]);
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
unpin_dt: false,
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ partner_id: partnerId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
unpin_dt: false,
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ partner_id: partnerId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_end_dt: serializeDate(today()),
livechat_operator_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem", { text: "Partner 2" });
await click(".o-mail-ChatWindow-header [title*='Close Chat Window']", {
parent: [".o-mail-ChatWindow", { text: "Partner 2" }],
});
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel", { text: "Partner 1" });
await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Partner 2" });
});
test.tags("desktop", "focus required");
test("Focus should not be stolen when a new livechat open", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 12" });
const channelIds = pyEnv["discuss.channel"].create([
{ name: "general" },
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
},
]);
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem", { text: "general" });
await contains(".o-mail-ChatWindow", { text: "general" });
await contains(".o-mail-Composer-input[placeholder='Message #general…']:focus");
withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "hu",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelIds[1],
thread_model: "discuss.channel",
})
);
await contains(".o-mail-ChatWindow", { text: "Visitor 12" });
await animationFrame();
await contains(".o-mail-Composer-input[placeholder='Message #general…']:focus");
});
test("do not ask confirmation if other operators are present", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #12" });
const otherOperatorId = pyEnv["res.partner"].create({ name: "John" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
Command.create({ partner_id: otherOperatorId, livechat_member_type: "agent" }),
],
livechat_operator_id: serverState.partnerId,
channel_type: "livechat",
});
setupChatHub({ opened: [channelId] });
await start();
await contains(".o-mail-ChatWindow");
await click("[title*='Close Chat Window']");
await contains(".o-mail-ChatWindow", { count: 0 });
});
test.tags("desktop");
test("Show livechats with new message in chat hub even when in discuss app)", async () => {
// Chat hub show conversations with new message only when outside of discuss app by default.
// Live chats are special in that agents are expected to see their ongoing conversations at all
// time. Closing chat window ends the conversation. Hence the livechat always are shown on chat hub.
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const [livechatId, channelId] = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
{
channel_member_ids: [Command.create({ partner_id: serverState.partnerId })],
channel_type: "channel",
name: "general",
},
]);
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message:contains('Test')");
// simulate livechat visitor sending a message
await withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "Hello, I need help!",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: livechatId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-DiscussSidebar-item:contains('Visitor 11') .badge", { text: "1" });
await openFormView("res.partner", serverState.partnerId);
await contains(".o-mail-ChatWindow-header:contains('Visitor 11')");
});
test("livechat: non-member can close immediately", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor ABC" });
const PartnerId = pyEnv["res.partner"].create({ name: "Agent" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: PartnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
livechat_operator_id: PartnerId,
channel_type: "livechat",
});
await start();
setupChatHub({ opened: [channelId] });
await contains(".o-mail-ChatWindow");
await click("[title*='Close Chat Window']");
await contains(".o-mail-ChatWindow", { count: 0 });
});

View file

@ -0,0 +1,70 @@
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import {
asyncStep,
Command,
onRpc,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineLivechatModels();
test("Can execute help command on livechat channels", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
onRpc("discuss.channel", "execute_command_help", () => {
asyncStep("execute_command_help");
return true;
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/help");
await click(".o-mail-Composer button[title='Send']:enabled");
await waitForSteps(["execute_command_help"]);
});
test('Receives visitor typing status "is typing"', async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-Typing", { text: "" });
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
// simulate receive typing notification from livechat visitor "is typing"
withGuest(guestId, () =>
rpc("/discuss/channel/notify_typing", {
is_typing: true,
channel_id: channel.id,
})
);
await contains(".o-discuss-Typing", { text: "Visitor 20 is typing..." });
});

View file

@ -0,0 +1,165 @@
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { defineLivechatModels } from "./livechat_test_helpers";
import { press } from "@odoo/hoot-dom";
describe.current.tags("desktop");
defineLivechatModels();
test("add livechat in the sidebar on visitor sending first message", 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();
pyEnv["res.users"].write([serverState.userId], { im_status: "online" });
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const livechatChannelId = pyEnv["im_livechat.channel"].create({
user_ids: [serverState.userId],
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
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",
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
country_id: countryId,
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebar");
// simulate livechat visitor sending a message
withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "Hello, I need help!",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container",
{
text: "Visitor (Belgium)",
}
);
});
test("invite button should be present on livechat", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Discuss button[title='Invite People']");
});
test("livechats are sorted by last activity time in the sidebar: most recent at the top", 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 guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" });
pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
{
channel_member_ids: [
Command.create({
last_interest_dt: "2021-02-01 10:00:00",
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
]);
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel", { count: 2 });
await contains(":nth-child(1 of .o-mail-DiscussSidebarChannel-container)", {
text: "Visitor 12",
});
await click(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" });
await insertText(".o-mail-Composer-input", "Blabla");
await press("Enter");
await contains(":nth-child(1 of .o-mail-DiscussSidebarChannel-container)", {
text: "Visitor 11",
});
await contains(":nth-child(2 of .o-mail-DiscussSidebarChannel-container)", {
text: "Visitor 12",
});
});
test("sidebar search finds livechats", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await click("a", { text: "Visitor 11" });
await contains(".o-mail-DiscussContent-threadName[title='Visitor 11']");
});
test("open visitor's partner profile if visitor has one", async () => {
const pyEnv = await startServer();
const livechatPartner = pyEnv["res.partner"].create({ name: "Joel Willis" });
const channel = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ partner_id: livechatPartner, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channel);
await click("a[title='View Contact']");
await contains("div.o_field_widget > input:value(Joel Willis)");
});

View file

@ -0,0 +1,52 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { contains, setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
describe.current.tags("desktop");
defineLivechatModels();
test("persisted session", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
setupChatHub({ opened: [channelId] });
await start({
authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" },
});
await contains(".o-mail-ChatWindow");
});
test("rule received in init", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const autopopupRuleId = pyEnv["im_livechat.channel.rule"].create({
auto_popup_timer: 0,
action: "auto_popup",
});
patchWithCleanup(mailDataHelpers, {
_process_request_for_all(store) {
super._process_request_for_all(...arguments);
store.add(pyEnv["im_livechat.channel.rule"].browse(autopopupRuleId), {
action: "auto_popup",
auto_popup_timer: 0,
});
store.add({ livechat_rule: autopopupRuleId });
},
});
await start({ authenticateAs: false });
await contains(".o-mail-ChatWindow");
});

View file

@ -0,0 +1,38 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { contains, setupChatHub, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, makeMockEnv, serverState } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("Do not show bot IM status", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
await makeMockEnv({ embedLivechat: true });
const partnerId1 = pyEnv["res.partner"].create({ name: "Mitchell", im_status: "online" });
pyEnv["res.users"].create({ partner_id: partnerId1 });
const channelId1 = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: partnerId1, livechat_member_type: "visitor" }),
],
channel_type: "chat",
});
const partnerId2 = pyEnv["res.partner"].create({ name: "Dummy" });
const channelId2 = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "visitor" }),
Command.create({ partner_id: partnerId2, livechat_member_type: "bot" }),
],
channel_type: "livechat",
livechat_operator_id: partnerId2,
});
setupChatHub({ folded: [channelId1, channelId2] });
await start({ authenticateAs: false });
await contains(".o-mail-ChatBubble[name='Mitchell'] .o-mail-ImStatus");
await contains(".o-mail-ChatBubble[name='Dummy']");
await contains(".o-mail-ChatBubble[name='Dummy'] .o-mail-ImStatus", { count: 0 });
});

View file

@ -0,0 +1,183 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import {
assertChatBubbleAndWindowImStatus,
click,
contains,
inputFiles,
insertText,
mockGetMedia,
onRpcBefore,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { asyncStep, serverState, waitForSteps, withUser } from "@web/../tests/web_test_helpers";
import { deserializeDateTime } from "@web/core/l10n/dates";
import { rpc } from "@web/core/network/rpc";
import { getOrigin } from "@web/core/utils/urls";
describe.current.tags("desktop");
defineLivechatModels();
test("internal users can upload file to temporary thread", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const [partnerUser] = pyEnv["res.users"].search_read([["id", "=", serverState.partnerId]]);
await start({ authenticateAs: partnerUser });
await click(".o-livechat-LivechatButton");
const file = new File(["hello, world"], "text.txt", { type: "text/plain" });
await contains(".o-mail-Composer");
await click(".o-mail-Composer button[title='More Actions']");
await contains(".dropdown-item:contains('Attach files')");
await inputFiles(".o-mail-Composer .o_input_file", [file]);
await contains(".o-mail-AttachmentContainer:not(.o-isUploading):contains(text.txt) .fa-check");
await triggerHotkey("Enter");
await contains(".o-mail-Message .o-mail-AttachmentContainer:contains(text.txt)");
});
test("Conversation name is operator livechat user name", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" });
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow-header", { text: "MitchellOp" });
});
test("Portal users should not be able to start a call", async () => {
mockGetMedia();
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const joelUid = pyEnv["res.users"].create({
name: "Joel",
share: true,
login: "joel",
password: "joel",
});
const joelPid = pyEnv["res.partner"].create({
name: "Joel",
user_ids: [joelUid],
});
pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" });
await start({ authenticateAs: { login: "joel", password: "joel" } });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow-header:text('MitchellOp')");
await insertText(".o-mail-Composer-input", "Hello MitchellOp!");
await triggerHotkey("Enter");
await contains(".o-mail-Message[data-persistent]:contains('Hello MitchellOp!')");
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button", { count: 2 });
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title='Fold']");
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title*='Close']");
await contains(".o-discuss-Call", { count: 0 });
// simulate operator starts call
const [channelId] = pyEnv["discuss.channel"].search([
["channel_type", "=", "livechat"],
[
"channel_member_ids",
"in",
pyEnv["discuss.channel.member"].search([["partner_id", "=", joelPid]]),
],
]);
await withUser(serverState.userId, () =>
rpc("/mail/rtc/channel/join_call", { channel_id: channelId }, { silent: true })
);
await contains(".o-discuss-Call button", { count: 2 });
await contains(".o-discuss-Call button[title='Join Video Call']");
await contains(".o-discuss-Call button[title='Join Call']");
// still same actions in header
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button", { count: 2 });
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title='Fold']");
await contains(".o-mail-ChatWindow-header .o-mail-ActionList-button[title*='Close']");
});
test("avatar url contains access token for non-internal users", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
pyEnv["res.partner"].write(serverState.partnerId, { user_livechat_username: "MitchellOp" });
const [partner] = pyEnv["res.partner"].search_read([["id", "=", serverState.partnerId]]);
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(
`.o-mail-ChatWindow-threadAvatar img[data-src="${getOrigin()}/web/image/res.partner/${
partner.id
}/avatar_128?access_token=${partner.id}&unique=${
deserializeDateTime(partner.write_date).ts
}"]`
);
await contains(
`.o-mail-Message-avatar[data-src="${getOrigin()}/web/image/res.partner/${
partner.id
}/avatar_128?access_token=${partner.id}&unique=${
deserializeDateTime(partner.write_date).ts
}"]`
);
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
const guestId = pyEnv.cookie.get("dgid");
const [guest] = pyEnv["mail.guest"].read(guestId);
await contains(
`.o-mail-Message-avatar[data-src="${getOrigin()}/web/image/mail.guest/${
guest.id
}/avatar_128?access_token=${guest.id}&unique=${deserializeDateTime(guest.write_date).ts}"]`
);
});
test("can close confirm livechat with keyboard", async () => {
await startServer();
await loadDefaultEmbedConfig();
onRpcBefore((route) => {
if (route === "/im_livechat/visitor_leave_session") {
asyncStep(route);
}
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await insertText(".o-mail-Composer-input", "Hello");
await triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await triggerHotkey("Escape");
await contains(".o-livechat-CloseConfirmation", {
text: "Leaving will end the live chat. Do you want to proceed?",
});
await triggerHotkey("Escape");
await contains(".o-livechat-CloseConfirmation", { count: 0 });
await triggerHotkey("Escape");
await contains(".o-livechat-CloseConfirmation", {
text: "Leaving will end the live chat. Do you want to proceed?",
});
await triggerHotkey("Enter");
await waitForSteps(["/im_livechat/visitor_leave_session"]);
await contains(".o-mail-ChatWindow", { text: "Did we correctly answer your question?" });
});
test("Should not show IM status of agents", async () => {
mockGetMedia();
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const joelUid = pyEnv["res.users"].create({
name: "Joel",
share: true,
login: "joel",
password: "joel",
});
pyEnv["res.partner"].create({ name: "Joel", user_ids: [joelUid] });
pyEnv["res.partner"].write(serverState.partnerId, {
im_status: "online",
user_livechat_username: "MitchellOp",
});
await start({ authenticateAs: { login: "joel", password: "joel" } });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow-header:text('MitchellOp')");
await insertText(".o-mail-Composer-input", "Hello MitchellOp!");
await triggerHotkey("Enter");
await contains(".o-mail-Message[data-persistent]:contains('Hello MitchellOp!')");
await click(".o-mail-ChatWindow-header");
await contains(".o-mail-ChatBubble");
await assertChatBubbleAndWindowImStatus("MitchellOp", 0);
});

View file

@ -0,0 +1,35 @@
import { expirableStorage } from "@im_livechat/core/common/expirable_storage";
import { describe, expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { asyncStep, waitForSteps } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
test("value is removed from expirable storage after expiration", () => {
mockDate("2023-01-01 00:00:00");
const ONE_DAY = 60 * 60 * 24;
expirableStorage.setItem("foo", "bar", ONE_DAY);
expect(expirableStorage.getItem("foo")).toBe("bar");
mockDate("2023-01-01 23:00:00");
expect(expirableStorage.getItem("foo")).toBe("bar");
mockDate("2023-01-02 00:00:01");
expect(expirableStorage.getItem("foo")).toBe(null);
});
test("subscribe/unsubscribe to storage changes", async () => {
const fooCallback = (value) => asyncStep(`foo - ${value}`);
const barCallback = (value) => asyncStep(`bar - ${value}`);
expirableStorage.onChange("foo", fooCallback);
expirableStorage.onChange("bar", barCallback);
expirableStorage.setItem("foo", 1);
await waitForSteps(["foo - 1"]);
expirableStorage.setItem("bar", 2);
await waitForSteps(["bar - 2"]);
expirableStorage.removeItem("foo");
await waitForSteps(["foo - null"]);
expirableStorage.offChange("foo", fooCallback);
expirableStorage.setItem("foo", 3);
expirableStorage.removeItem("bar");
await waitForSteps(["bar - null"]);
});

View file

@ -0,0 +1,173 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { RATING } from "@im_livechat/embed/common/livechat_service";
import {
click,
contains,
insertText,
onRpcBefore,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import {
asyncStep,
Command,
getService,
patchWithCleanup,
serverState,
waitForSteps,
withUser,
} from "@web/../tests/web_test_helpers";
defineLivechatModels();
test("Do not ask feedback if empty", async () => {
await startServer();
await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await click("[title*='Close Chat Window']");
});
test("Close without feedback", async () => {
await startServer();
await loadDefaultEmbedConfig();
onRpcBefore((route) => {
if (route === "/im_livechat/visitor_leave_session" || route === "/im_livechat/feedback") {
asyncStep(route);
}
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
await click("button", { text: "Close" });
await contains(".o-livechat-LivechatButton");
await waitForSteps(["/im_livechat/visitor_leave_session"]);
});
test("Last operator leaving ends the livechat", async () => {
await startServer();
await loadDefaultEmbedConfig();
const operatorUserId = serverState.userId;
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Message-content", { text: "Hello World!" });
// simulate operator leaving
await withUser(operatorUserId, () =>
getService("orm").call("discuss.channel", "action_unfollow", [
[Object.values(getService("mail.store").Thread.records).at(-1).id],
])
);
await contains("span", { text: "This livechat conversation has ended" });
await contains(".o-mail-Composer-input", { count: 0 });
await click("[title*='Close Chat Window']");
await contains("p", { text: "Did we correctly answer your question?" }); // shows immediately feedback
});
test("Feedback with rating and comment", async () => {
await startServer();
await loadDefaultEmbedConfig();
onRpcBefore((route, args) => {
if (route === "/im_livechat/visitor_leave_session") {
asyncStep(route);
}
if (route === "/im_livechat/feedback") {
asyncStep(route);
expect(args.reason).toInclude("Good job!");
expect(args.rate).toBe(RATING.OK);
}
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
await waitForSteps(["/im_livechat/visitor_leave_session"]);
await click(`img[alt="${RATING.OK}"]`);
await insertText("textarea[placeholder='Explain your note']", "Good job!");
await click("button:contains(Send):enabled");
await contains("p", { text: "Thank you for your feedback" });
await waitForSteps(["/im_livechat/feedback"]);
});
test("Closing folded chat window should open it with feedback", async () => {
await startServer();
await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click("[title='Fold']");
await click(".o-mail-ChatBubble");
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
await click(".o-mail-ChatHub-bubbleBtn");
await contains(".o-mail-ChatWindow p", { text: "Did we correctly answer your question?" });
});
test("Start new session from feedback panel", async () => {
const pyEnv = await startServer();
const channelId = await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow", { text: "Mitchell Admin" });
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
pyEnv["im_livechat.channel"].write([channelId], {
user_ids: [Command.clear(serverState.userId)],
});
pyEnv["im_livechat.channel"].write([channelId], {
user_ids: [
pyEnv["res.users"].create({
partner_id: pyEnv["res.partner"].create({ name: "Bob Operator" }),
}),
],
});
await click("button", { text: "New Session" });
await contains(".o-mail-ChatWindow", { count: 1 });
await contains(".o-mail-ChatWindow", { text: "Bob Operator" });
});
test("open review link on good rating", async () => {
patchWithCleanup(window, {
open: (...args) => {
expect.step("window.open");
expect(args[0]).toBe("https://www.odoo.com");
expect(args[1]).toBe("_blank");
},
});
await startServer();
await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Message-content", { text: "Hello World!" });
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
await click(`img[alt="${RATING.GOOD}"]`);
await insertText("textarea[placeholder='Explain your note']", "Good job!");
await click("button:contains(Send):enabled");
await expect.waitForSteps(["window.open"]);
});

View file

@ -0,0 +1,40 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { click, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { press, waitFor } from "@odoo/hoot-dom";
import {
asyncStep,
contains,
getService,
onRpc,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("Handle livechat history command", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
onRpc("/im_livechat/history", ({ url }) => {
asyncStep(new URL(url).pathname);
return true;
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-Composer-input").edit("Hello World!", { confirm: false });
await press("Enter");
await waitFor(".o-mail-Message:contains(Hello World!)");
const thread = Object.values(getService("mail.store").Thread.records).at(-1);
const guestId = pyEnv.cookie.get("dgid");
const [guest] = pyEnv["mail.guest"].read(guestId);
pyEnv["bus.bus"]._sendone(guest, "im_livechat.history_command", {
id: thread.id,
partner_id: serverState.partnerId,
});
await waitForSteps(["/im_livechat/history"]);
});

View file

@ -0,0 +1,87 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import {
click,
contains,
insertText,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { asyncStep, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("open/close temporary channel", async () => {
await startServer();
await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
await contains(".o-livechat-LivechatButton", { count: 0 });
await click("[title*='Close Chat Window']");
await contains(".o-mail-ChatWindow", { count: 0 });
await contains(".o-livechat-LivechatButton", { count: 1 });
});
test("open/close persisted channel", async () => {
await startServer();
await loadDefaultEmbedConfig();
const env = await start({ authenticateAs: false });
env.services.bus_service.subscribe("discuss.channel/new_message", () =>
asyncStep("discuss.channel/new_message")
);
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "How can I help?");
await triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await contains(".o-mail-Message-content", { text: "How can I help?" });
await waitForSteps(["discuss.channel/new_message"]);
await click("[title*='Close Chat Window']");
await click(".o-livechat-CloseConfirmation-leave");
await contains(".o-mail-ChatWindow", { text: "Did we correctly answer your question?" });
await click("[title*='Close Chat Window']");
await contains(".o-mail-ChatWindow", { count: 0 });
await contains(".o-livechat-LivechatButton", { count: 1 });
});
test("livechat not available", async () => {
await startServer();
await loadDefaultEmbedConfig();
patchWithCleanup(mailDataHelpers, {
_process_request_for_all(store) {
super._process_request_for_all(...arguments);
store.add({ livechat_available: false });
},
});
await start({ authenticateAs: false });
await contains(".o-mail-ChatHub");
await contains(".o-livechat-LivechatButton", { count: 0 });
});
test("clicking on notification opens the chat", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const btnAndTextRuleId = pyEnv["im_livechat.channel.rule"].create({
action: "display_button_and_text",
});
patchWithCleanup(mailDataHelpers, {
_process_request_for_all(store) {
super._process_request_for_all(...arguments);
store.add(pyEnv["im_livechat.channel.rule"].browse(btnAndTextRuleId), {
action: "display_button_and_text",
});
store.add({ livechat_rule: btnAndTextRuleId });
},
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton-notification", {
text: "Need help? Chat with us.",
});
await contains(".o-mail-ChatWindow");
});

View file

@ -0,0 +1,167 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { expirableStorage } from "@im_livechat/core/common/expirable_storage";
import {
click,
contains,
insertText,
listenStoreFetch,
onRpcBefore,
setupChatHub,
start,
startServer,
STORE_FETCH_ROUTES,
triggerHotkey,
userContext,
waitStoreFetch,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import {
asyncStep,
Command,
onRpc,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("persisted session history", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
expirableStorage.setItem(
"im_livechat.saved_state",
JSON.stringify({
store: { "discuss.channel": [{ id: channelId }] },
persisted: true,
livechatUserId: serverState.publicUserId,
})
);
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "Old message in history",
res_id: channelId,
model: "discuss.channel",
message_type: "comment",
});
setupChatHub({ opened: [channelId] });
await start({
authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" },
});
await contains(".o-mail-Message-content", { text: "Old message in history" });
});
test("previous operator prioritized", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const userId = pyEnv["res.users"].create({ name: "John Doe", im_status: "online" });
const previousOperatorId = pyEnv["res.partner"].create({
name: "John Doe",
user_ids: [userId],
});
pyEnv["im_livechat.channel"].write([livechatChannelId], { user_ids: [Command.link(userId)] });
expirableStorage.setItem("im_livechat_previous_operator", JSON.stringify(previousOperatorId));
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-Message-author", { text: "John Doe" });
});
test("Only necessary requests are made when creating a new chat", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const operatorPartnerId = serverState.partnerId;
onRpcBefore((route, args) => {
if (!route.includes("assets") && !STORE_FETCH_ROUTES.includes(route)) {
asyncStep(`${route} - ${JSON.stringify(args)}`);
}
});
listenStoreFetch(undefined, { logParams: ["init_livechat"] });
await start({ authenticateAs: false });
await contains(".o-livechat-LivechatButton");
await waitStoreFetch([
"failures", // called because mail/core/web is loaded in test bundle
"systray_get_activities", // called because mail/core/web is loaded in test bundle
"init_messaging",
["init_livechat", livechatChannelId],
]);
await click(".o-livechat-LivechatButton");
await contains(".o-mail-Message", { text: "Hello, how may I help you?" });
await waitForSteps([
`/im_livechat/get_session - ${JSON.stringify({
channel_id: livechatChannelId,
previous_operator_id: null,
persisted: false,
})}`,
]);
await insertText(".o-mail-Composer-input", "Hello!");
await waitForSteps([]);
await triggerHotkey("Enter");
await contains(".o-mail-Message", { text: "Hello!" });
const [threadId] = pyEnv["discuss.channel"].search([], { order: "id DESC" });
await waitStoreFetch(
[
"failures", // called because mail/core/web is loaded in test bundle
"systray_get_activities", // called because mail/core/web is loaded in test bundle
"init_messaging",
],
{
stepsBefore: [
`/im_livechat/get_session - ${JSON.stringify({
channel_id: livechatChannelId,
previous_operator_id: operatorPartnerId,
persisted: true,
})}`,
`/mail/message/post - ${JSON.stringify({
post_data: {
body: "Hello!",
email_add_signature: true,
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: threadId,
thread_model: "discuss.channel",
context: { ...userContext(), temporary_id: 0.8200000000000001 },
})}`,
],
}
);
});
test("do not create new thread when operator answers to visitor", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
onRpc("/im_livechat/get_session", async () => asyncStep("/im_livechat/get_session"));
onRpc("/mail/message/post", async () => asyncStep("/mail/message/post"));
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
setupChatHub({ opened: [channelId] });
await start({
authenticateAs: pyEnv["res.users"].search_read([["id", "=", serverState.userId]])[0],
});
await insertText(".o-mail-Composer-input", "Hello!");
await triggerHotkey("Enter");
await contains(".o-mail-Message", { text: "Hello!" });
await waitForSteps(["/mail/message/post"]);
});

View file

@ -0,0 +1,106 @@
import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers";
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import {
assertChatHub,
click,
contains,
focus,
insertText,
onRpcBefore,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { advanceTime } from "@odoo/hoot-mock";
import { getService, serverState, withUser } from "@web/../tests/web_test_helpers";
import { LivechatButton } from "@im_livechat/embed/common/livechat_button";
import { queryFirst } from "@odoo/hoot-dom";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineLivechatModels();
test("Session is reset after failing to persist the channel", async () => {
await startServer();
await loadDefaultEmbedConfig();
onRpcBefore("/im_livechat/get_session", (args) => {
if (args.persisted) {
return false;
}
});
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o_notification", {
text: "No available collaborator, please try again later.",
});
await contains(".o-livechat-LivechatButton");
await advanceTime(LivechatButton.DEBOUNCE_DELAY + 10);
await click(".o-livechat-LivechatButton");
await contains(".o-mail-ChatWindow");
});
test("Fold state is saved", async () => {
await startServer();
await loadDefaultEmbedConfig();
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-Thread");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
assertChatHub({ opened: [1] });
await click(".o-mail-ChatWindow-header");
await contains(".o-mail-Thread", { count: 0 });
assertChatHub({ folded: [1] });
await click(".o-mail-ChatBubble");
assertChatHub({ opened: [1] });
});
test.tags("focus required");
test("Seen message is saved on the server", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const userId = serverState.userId;
await start({ authenticateAs: false });
await click(".o-livechat-LivechatButton");
await contains(".o-mail-Thread");
await insertText(".o-mail-Composer-input", "Hello, I need help!");
triggerHotkey("Enter");
await contains(".o-mail-Message", { text: "Hello, I need help!" });
await waitUntilSubscribe();
const initialSeenMessageId = Object.values(getService("mail.store").Thread.records).at(-1)
.self_member_id.seen_message_id?.id;
queryFirst(".o-mail-Composer-input").blur();
await withUser(userId, () =>
rpc("/mail/message/post", {
post_data: {
body: "Hello World!",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: Object.values(getService("mail.store").Thread.records).at(-1).id,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-Thread-newMessage");
await contains(".o-mail-ChatWindow-counter", { text: "1" });
await focus(".o-mail-Composer-input");
await contains(".o-mail-ChatWindow-counter", { count: 0 });
const guestId = pyEnv.cookie.get("dgid");
const [member] = pyEnv["discuss.channel.member"].search_read([
["guest_id", "=", guestId],
["channel_id", "=", Object.values(getService("mail.store").Thread.records).at(-1).id],
]);
expect(initialSeenMessageId).not.toBe(member.seen_message_id[0]);
expect(
Object.values(getService("mail.store").Thread.records).at(-1).self_member_id.seen_message_id
.id
).toBe(member.seen_message_id[0]);
});

View file

@ -0,0 +1,66 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import { expirableStorage } from "@im_livechat/core/common/expirable_storage";
import {
click,
contains,
hover,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
defineLivechatModels();
describe.current.tags("desktop");
test("user custom live chat user name for message reactions", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Michou" });
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
pyEnv["mail.message"].create({
body: "Hello world",
res_id: channelId,
message_type: "comment",
model: "discuss.channel",
reaction_ids: [
pyEnv["mail.message.reaction"].create({
content: "👍",
partner_id: serverState.partnerId,
}),
],
});
expirableStorage.setItem(
"im_livechat.saved_state",
JSON.stringify({
store: { "discuss.channel": [{ id: channelId }] },
persisted: true,
livechatUserId: serverState.publicUserId,
})
);
setupChatHub({ opened: [channelId] });
await start({
authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" },
});
await hover(".o-mail-MessageReaction");
await click(".o-mail-MessageReactionList-preview", {
text: "👍:+1: reacted by Michou",
});
await contains(".o-mail-MessageReactionMenu-persona", { text: "Michou" });
});

View file

@ -0,0 +1,58 @@
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import {
click,
contains,
insertText,
onRpcBefore,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { onRpc } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("send", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
onRpcBefore("/im_livechat/email_livechat_transcript", () => expect.step(`send_transcript`));
const partnerId = pyEnv["res.partner"].create({ email: "paul@example.com", name: "Paul" });
pyEnv["res.users"].create({ partner_id: partnerId, login: "paul", password: "paul" });
await start({ authenticateAs: { login: "paul", password: "paul" } });
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click(".o-mail-ChatWindow-header [title*='Close']");
await click(".o-livechat-CloseConfirmation-leave");
await contains("label", { text: "Receive a copy of this conversation" });
await contains("input:enabled", { value: "paul@example.com" });
await click("button[data-action='sendTranscript']:enabled");
await contains(".form-text", { text: "The conversation was sent." });
await expect.waitForSteps(["send_transcript"]);
});
test("send failed", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
onRpc("/im_livechat/email_livechat_transcript", () => {
throw new Error();
});
const partnerId = pyEnv["res.partner"].create({ email: "paul@example.com", name: "Paul" });
pyEnv["res.users"].create({ partner_id: partnerId, login: "paul", password: "paul" });
await start({ authenticateAs: { login: "paul", password: "paul" } });
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
triggerHotkey("Enter");
await contains(".o-mail-Thread:not([data-transient])");
await click(".o-mail-ChatWindow-header [title*='Close']");
await click(".o-livechat-CloseConfirmation-leave");
await contains("input", { value: "paul@example.com" });
await click("button[data-action='sendTranscript']:enabled");
await contains(".form-text", { text: "An error occurred. Please try again." });
});

View file

@ -0,0 +1,107 @@
import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers";
import { expirableStorage } from "@im_livechat/core/common/expirable_storage";
import {
defineLivechatModels,
loadDefaultEmbedConfig,
} from "@im_livechat/../tests/livechat_test_helpers";
import {
click,
contains,
focus,
insertText,
listenStoreFetch,
setupChatHub,
start,
startServer,
triggerHotkey,
waitStoreFetch,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { asyncStep, Command, onRpc, serverState, withUser } from "@web/../tests/web_test_helpers";
import { queryFirst } from "@odoo/hoot-dom";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineLivechatModels();
test("new message from operator displays unread counter", async () => {
const pyEnv = await startServer();
const livechatChannelId = await loadDefaultEmbedConfig();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ guest_id: guestId }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
expirableStorage.setItem(
"im_livechat.saved_state",
JSON.stringify({
store: { "discuss.channel": [{ id: channelId }] },
persisted: true,
livechatUserId: serverState.publicUserId,
})
);
setupChatHub({ opened: [channelId] });
onRpc("/discuss/channel/messages", () => asyncStep("/discuss/channel/message"));
const userId = serverState.userId;
listenStoreFetch(["init_messaging", "init_livechat", "discuss.channel"]);
await start({
authenticateAs: { ...pyEnv["mail.guest"].read(guestId)[0], _name: "mail.guest" },
});
await waitStoreFetch(["init_messaging", "init_livechat", "discuss.channel"], {
stepsAfter: ["/discuss/channel/message"],
});
// send after init_messaging because bus subscription is done after init_messaging
await withUser(userId, () =>
rpc("/mail/message/post", {
post_data: { body: "Are you there?", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-ChatWindow-counter", { text: "1" });
});
test.tags("focus required");
test("focus on unread livechat marks it as read", async () => {
const pyEnv = await startServer();
await loadDefaultEmbedConfig();
const userId = serverState.userId;
listenStoreFetch(["init_messaging", "init_livechat"]);
await start({ authenticateAs: false });
await waitStoreFetch(["init_messaging", "init_livechat"]);
await click(".o-livechat-LivechatButton");
await insertText(".o-mail-Composer-input", "Hello World!");
await triggerHotkey("Enter");
// Wait for bus subscription to be done after persisting the thread:
// presence of the message is not enough (temporary message).
await waitUntilSubscribe();
await contains(".o-mail-Message-content", { text: "Hello World!" });
const [channelId] = pyEnv["discuss.channel"].search([
["channel_type", "=", "livechat"],
[
"channel_member_ids",
"in",
pyEnv["discuss.channel.member"].search([["guest_id", "=", pyEnv.cookie.get("dgid")]]),
],
]);
await waitStoreFetch("init_messaging");
queryFirst(".o-mail-Composer-input").blur();
// send after init_messaging because bus subscription is done after init_messaging
await withUser(userId, () =>
rpc("/mail/message/post", {
post_data: { body: "Are you there?", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-ChatWindow-counter", { text: "1" });
await contains(".o-mail-Message", { text: "Are you there?" });
await focus(".o-mail-Composer-input");
await contains(".o-mail-ChatWindow-counter", { count: 0 });
});

View file

@ -0,0 +1,422 @@
import {
click,
contains,
focus,
insertText,
openDiscuss,
patchUiSize,
setupChatHub,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { press, waitFor } from "@odoo/hoot-dom";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { defineLivechatModels } from "./livechat_test_helpers";
import { advanceTime, mockDate } from "@odoo/hoot-mock";
describe.current.tags("desktop");
defineLivechatModels();
test("tab on discuss composer goes to oldest unread livechat", async () => {
const pyEnv = await startServer();
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" });
const guestId_3 = pyEnv["mail.guest"].create({ name: "Visitor 13" });
const channelIds = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 1",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
message_unread_counter: 1,
last_interest_dt: "2021-01-02 10:00:00",
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 2",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
message_unread_counter: 1,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_3, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 3",
},
]);
pyEnv["mail.message"].create([
{
author_guest_id: guestId_2,
body: "Hello",
model: "discuss.channel",
res_id: channelIds[1],
},
{
author_guest_id: guestId_3,
body: "Hello",
model: "discuss.channel",
res_id: channelIds[2],
},
]);
await start();
await openDiscuss(channelIds[0]);
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" });
await focus(".o-mail-Composer-input");
await contains(".o-mail-Composer-input[placeholder='Tab to next livechat']");
await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 });
triggerHotkey("Tab");
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 13" });
await focus(".o-mail-Composer-input");
await contains(".o-active .o-mail-DiscussSidebar-badge", { count: 0 });
triggerHotkey("Tab");
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 12" });
});
test.tags("focus required");
test("Tab livechat picks ended livechats last", async () => {
mockDate("2021-01-02T10:05:00");
const pyEnv = await startServer();
const guestIds = pyEnv["mail.guest"].create([
{ name: "Visitor 0" },
{ name: "Visitor 1" },
{ name: "Visitor 2" },
{ name: "Visitor 3" },
{ name: "Visitor 4" },
]);
const livechatChannelId = pyEnv["im_livechat.channel"].create({
name: "Test",
user_ids: [serverState.userId],
});
const channelIds = pyEnv["discuss.channel"].create(
guestIds.map((guestId, idx) => ({
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: `2021-01-02 10:00:0${idx}`,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
}))
);
pyEnv["mail.message"].create(
guestIds.map((guestId, idx) => ({
author_guest_id: guestId,
body: "Hello",
model: "discuss.channel",
res_id: channelIds[idx],
}))
);
/**
* channel id | last_interest_dt | livechat_end_dt | unread
* -----------+---------------------+-----------------+--------
* 0 | 2021-01-02 10:00:00 | false | true
* 1 | 2021-01-02 10:00:01 | false | true
* 2 | 2021-01-02 10:00:02 | false | true
* 3 | 2021-01-02 10:00:03 | false | true
* 4 | 2021-01-02 10:00:04 | false | true
*/
patchUiSize({ width: 1920 });
setupChatHub({ folded: [channelIds[0], channelIds[1], channelIds[2], channelIds[3]] });
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem", { text: "Visitor 4" });
await contains(".o-mail-ChatWindow:contains('Visitor 4') .o-mail-Message:contains('Hello')");
await contains(".o-mail-ChatWindow:contains('Visitor 4') .o-mail-Composer.o-focused");
await contains(".o-mail-ChatWindow:contains('Visitor 4') .badge", { count: 0 });
await advanceTime(5_000);
await withGuest(guestIds[1], () =>
rpc("/mail/message/post", {
post_data: {
body: "livechat 1",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelIds[1],
thread_model: "discuss.channel",
})
);
await advanceTime(5_000);
await withGuest(guestIds[1], () =>
rpc("/im_livechat/visitor_leave_session", { channel_id: channelIds[1] })
);
await advanceTime(5_000);
await withGuest(guestIds[3], () =>
rpc("/mail/message/post", {
post_data: {
body: "livechat 3",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelIds[3],
thread_model: "discuss.channel",
})
);
await waitFor(".o-mail-ChatBubble[name='Visitor 3'] .badge:contains('2')", { timeout: 3000 });
/**
* channel id | last_interest_dt | livechat_end_dt | unread
* -----------+---------------------+-----------------+--------
* 0 | 2021-01-02 10:00:00 | false | true
* 1 | 2021-01-02 10:05:10 | now() | true
* 2 | 2021-01-02 10:00:02 | false | true
* 3 | 2021-01-02 10:05:15 | false | true
* 4 | 2021-01-02 10:00:04 | false | false
*/
await press("Tab");
await contains(".o-mail-ChatWindow", { count: 2 });
await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Message:contains('Hello')");
await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Composer.o-focused");
await contains(".o-mail-ChatWindow:contains('Visitor 0') .badge", { count: 0 });
await press("Tab");
await contains(".o-mail-ChatWindow", { count: 3 });
await contains(".o-mail-ChatWindow:contains('Visitor 2') .o-mail-Message:contains('Hello')");
await contains(".o-mail-ChatWindow:contains('Visitor 2') .o-mail-Composer.o-focused");
await contains(".o-mail-ChatWindow:contains('Visitor 2') .badge", { count: 0 });
await advanceTime(5_000);
await withGuest(guestIds[0], () =>
rpc("/mail/message/post", {
post_data: {
body: "livechat 0",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelIds[0],
thread_model: "discuss.channel",
})
);
await waitFor(".o-mail-ChatWindow:contains('Visitor 0') .badge:contains('1')", {
timeout: 3000,
});
/**
* channel id | last_interest_dt | livechat_end_dt | unread
* -----------+---------------------+-----------------+--------
* 0 | 2021-01-02 10:05:20 | false | true
* 1 | 2021-01-02 10:05:10 | now() | true
* 2 | 2021-01-02 10:00:02 | false | false
* 3 | 2021-01-02 10:05:15 | false | true
* 4 | 2021-01-02 10:00:04 | false | false
*/
await press("Tab");
await contains(".o-mail-ChatWindow:contains('Visitor 3') .o-mail-Message:contains('Hello')");
await contains(".o-mail-ChatWindow:contains('Visitor 3') .o-mail-Composer.o-focused");
await contains(".o-mail-ChatWindow:contains('Visitor 3') .badge", { count: 0 });
await press("Tab");
await contains(".o-mail-ChatWindow:contains('Visitor 0') .o-mail-Composer.o-focused");
await contains(".o-mail-ChatWindow:contains('Visitor 0') .badge", { count: 0 });
await press("Tab");
await contains(".o-mail-ChatWindow:contains('Visitor 1') .o-mail-Message:contains('Hello')");
await contains("span", { text: "This livechat conversation has ended" });
});
test.tags("focus required");
test("switching to folded chat window unfolds it", async () => {
const pyEnv = await startServer();
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" });
const channelIds = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 1",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-02 10:00:00",
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 2",
},
]);
pyEnv["mail.message"].create({
author_guest_id: guestId_2,
body: "Hello",
model: "discuss.channel",
res_id: channelIds[1],
});
setupChatHub({ opened: [channelIds[0]], folded: [channelIds[1]] });
await start();
await contains(".o-mail-ChatBubble[name='Visitor 12']");
await focus(".o-mail-Composer-input", {
parent: [".o-mail-ChatWindow", { text: "Visitor 11" }],
});
triggerHotkey("Tab");
await contains(".o-mail-ChatWindow", {
text: "Visitor 12",
contains: [".o-mail-Composer-input:focus"],
});
});
test.tags("focus required");
test("switching to hidden chat window unhides it", async () => {
const pyEnv = await startServer();
const [guestId_1, guestId_2] = pyEnv["mail.guest"].create([
{ name: "Visitor 11" },
{ name: "Visitor 12" },
]);
const channelIds = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 1",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-02 10:00:00",
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 2",
},
{ name: "general" },
]);
const [livechat_1] = channelIds;
pyEnv["mail.message"].create({
author_guest_id: guestId_2,
body: "Hello",
model: "discuss.channel",
res_id: livechat_1,
});
setupChatHub({ opened: channelIds.reverse() });
patchUiSize({ width: 900 }); // enough for 2 chat windows max
await start();
// FIXME: expected order: general, 12, 11
await contains(".o-mail-ChatWindow", { count: 2 });
await contains(".o-mail-ChatWindow", { count: 0, text: "Visitor 11" });
await focus(".o-mail-Composer-input", {
parent: [".o-mail-ChatWindow", { text: "Visitor 12" }],
});
triggerHotkey("Tab");
await contains(".o-mail-ChatWindow", {
text: "Visitor 11",
contains: [".o-mail-Composer-input:focus"],
});
});
test("tab on composer doesn't switch thread if user is typing", async () => {
const pyEnv = await startServer();
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" });
const channelIds = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 1",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
message_unread_counter: 1,
last_interest_dt: "2021-01-02 10:00:00",
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 2",
},
]);
await start();
await openDiscuss(channelIds[0]);
await insertText(".o-mail-Composer-input", "Hello, ");
triggerHotkey("Tab");
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" });
});
test("tab on composer doesn't switch thread if no unread thread", async () => {
const pyEnv = await startServer();
const guestId_1 = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const guestId_2 = pyEnv["mail.guest"].create({ name: "Visitor 12" });
const channelIds = pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 1",
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId_2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
name: "Livechat 2",
},
]);
await start();
await openDiscuss(channelIds[0]);
await focus(".o-mail-Composer-input");
triggerHotkey("Tab");
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" });
});

View file

@ -1,198 +0,0 @@
/** @odoo-module **/
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'im_livechat', {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
async _performRPC(route, args) {
if (route === '/im_livechat/get_session') {
const channel_id = args.channel_id;
const anonymous_name = args.anonymous_name;
const previous_operator_id = args.previous_operator_id;
const context = args.context;
return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context);
}
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private Mocked Routes
//--------------------------------------------------------------------------
/**
* Simulates the `/im_livechat/get_session` route.
*
* @private
* @param {integer} channel_id
* @param {string} anonymous_name
* @param {integer} [previous_operator_id]
* @param {Object} [context={}]
* @returns {Object}
*/
_mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) {
let user_id;
let country_id;
if ('mockedUserId' in context) {
// can be falsy to simulate not being logged in
user_id = context.mockedUserId;
} else {
user_id = this.currentUserId;
}
// don't use the anonymous name if the user is logged in
if (user_id) {
const user = this.getRecords('res.users', [['id', '=', user_id]])[0];
country_id = user.country_id;
} else {
// simulate geoip
const countryCode = context.mockedCountryCode;
const country = this.getRecords('res.country', [['code', '=', countryCode]])[0];
if (country) {
country_id = country.id;
anonymous_name = anonymous_name + ' (' + country.name + ')';
}
}
return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id);
},
//--------------------------------------------------------------------------
// Private Mocked Methods
//--------------------------------------------------------------------------
/**
* @override
*/
_mockMailChannelChannelInfo(ids) {
const channelInfos = this._super(...arguments);
for (const channelInfo of channelInfos) {
const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
channelInfo['channel']['anonymous_name'] = channel.anonymous_name;
// add the last message date
if (channel.channel_type === 'livechat') {
// add the operator id
if (channel.livechat_operator_id) {
const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0];
// livechat_username ignored for simplicity
channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')];
}
}
}
return channelInfos;
},
/**
* Simulates `_get_available_users` on `im_livechat.channel`.
*
* @private
* @param {integer} id
* @returns {Object}
*/
_mockImLivechatChannel_getAvailableUsers(id) {
const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0];
const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]);
return users.filter(user => user.im_status === 'online');
},
/**
* Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`.
*
* @private
* @param {integer} id
* @returns {Object}
*/
_mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) {
// partner to add to the mail.channel
const operator_partner_id = operator.partner_id;
const membersToAdd = [[0, 0, {
is_pinned: false,
partner_id: operator_partner_id,
}]];
let visitor_user;
if (user_id) {
const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0];
if (visitor_user && visitor_user.active && visitor_user !== operator) {
// valid session user (not public)
membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]);
}
} else {
membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
}
const membersName = [
visitor_user ? visitor_user.display_name : anonymous_name,
operator.livechat_username ? operator.livechat_username : operator.name,
];
return {
'channel_member_ids': membersToAdd,
'livechat_active': true,
'livechat_operator_id': operator_partner_id,
'livechat_channel_id': id,
'anonymous_name': user_id ? false : anonymous_name,
'country_id': country_id,
'channel_type': 'livechat',
'name': membersName.join(' '),
};
},
/**
* Simulates `_get_random_operator` on `im_livechat.channel`.
* Simplified mock implementation: returns the first available operator.
*
* @private
* @param {integer} id
* @returns {Object}
*/
_mockImLivechatChannel_getRandomOperator(id) {
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
return availableUsers[0];
},
/**
* Simulates `_open_livechat_mail_channel` on `im_livechat.channel`.
*
* @private
* @param {integer} id
* @param {string} anonymous_name
* @param {integer} [previous_operator_id]
* @param {integer} [user_id]
* @param {integer} [country_id]
* @returns {Object}
*/
_mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) {
let operator;
if (previous_operator_id) {
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
operator = availableUsers.find(user => user.partner_id === previous_operator_id);
}
if (!operator) {
operator = this._mockImLivechatChannel_getRandomOperator(id);
}
if (!operator) {
// no one available
return false;
}
// create the session, and add the link with the given channel
const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id);
const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals);
this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
return this._mockMailChannelChannelInfo([mailChannelId])[0];
},
/**
* @override
*/
_mockResPartner_GetChannelsAsMember(ids) {
const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
const livechats = this.getRecords('mail.channel', [
['channel_type', '=', 'livechat'],
['channel_member_ids', 'in', members.map(member => member.id)],
]);
return [
...this._super(ids),
...livechats,
];
},
});

View file

@ -1,35 +0,0 @@
/** @odoo-module **/
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'im_livechat/controllers/main', {
/**
* @override
*/
async _performRPC(route, args) {
if (route === '/im_livechat/notify_typing') {
const uuid = args.uuid;
const is_typing = args.is_typing;
const context = args.context;
return this._mockRouteImLivechatNotifyTyping(uuid, is_typing, context);
}
return this._super(...arguments);
},
/**
* Simulates the `/im_livechat/notify_typing` route.
*
* @private
* @param {string} uuid
* @param {boolean} is_typing
* @param {Object} [context={}]
*/
_mockRouteImLivechatNotifyTyping(uuid, is_typing, context = {}) {
const [mailChannel] = this.getRecords('mail.channel', [['uuid', '=', uuid]]);
const partnerId = context.mockedPartnerId || this.currentPartnerId;
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', mailChannel.id], ['partner_id', '=', partnerId]]);
this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
},
});

View file

@ -1,40 +0,0 @@
/** @odoo-module **/
import '@mail/../tests/helpers/mock_server/models/mail_channel_member'; // ensure mail overrides are applied first
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, 'im_livechat/models/mail_channel_member', {
/**
* @override
*/
_mockMailChannelMember_GetPartnerData(ids) {
const [member] = this.getRecords('mail.channel.member', [['id', 'in', ids]]);
const [channel] = this.getRecords('mail.channel', [['id', '=', member.channel_id]]);
const [partner] = this.getRecords('res.partner', [['id', '=', member.partner_id]], { active_test: false });
if (channel.channel_type === 'livechat') {
const data = {
'id': partner.id,
'is_public': partner.is_public,
};
if (partner.user_livechat_username) {
data['user_livechat_username'] = partner.user_livechat_username;
} else {
data['name'] = partner.name;
}
if (!partner.is_public) {
const [country] = this.getRecords('res.country', [['id', '=', partner.country_id]]);
data['country'] = country
? {
'code': country.code,
'id': country.id,
'name': country.name,
}
: [['clear']];
}
return data;
}
return this._super(ids);
},
});

View file

@ -1,8 +0,0 @@
/** @odoo-module **/
import { addModelNamesToFetch, insertModelFields } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch(['im_livechat.channel']);
insertModelFields('res.users.settings', {
is_discuss_sidebar_category_livechat_open: { default: true },
});

View file

@ -0,0 +1,43 @@
import { expect } from "@odoo/hoot";
import {
click,
contains,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { Command, onRpc, serverState } from "@web/../tests/web_test_helpers";
export async function livechatLastAgentLeaveFromChatWindow() {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
const livechatChannelId = pyEnv["im_livechat.channel"].create({
name: "HR",
user_ids: [serverState.userId],
});
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
setupChatHub({ opened: [channelId] });
onRpc("discuss.channel", "action_unfollow", () => {
expect.step("action_unfollow");
});
await start();
await contains(".o-mail-ChatWindow");
await click("button[title*='Close Chat Window']");
await click("button:contains('Yes, leave conversation')");
await expect.waitForSteps(["action_unfollow"]);
await contains(".o-mail-ChatWindow", { count: 0 });
}

View file

@ -0,0 +1,352 @@
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, press, test, waitFor } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { serializeDate, today } from "@web/core/l10n/dates";
import { getOrigin } from "@web/core/utils/urls";
describe.current.tags("desktop");
defineLivechatModels();
test("livechat note is loaded when opening the channel info list", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({
name: "Visitor #20",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
livechat_note: "<p>Initial note<br/>Second line</p>",
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList textarea", { value: "Initial note\nSecond line" });
});
test("shows country and language in channel info list", async () => {
const pyEnv = await startServer();
const countryId = pyEnv["res.country"].create({ code: "BE", name: "Belgium" });
const langId = pyEnv["res.lang"].create({ name: "English" });
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_lang_id: langId,
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains("h6", { text: "Country & Language" });
await contains("span[title='Language']", { text: "English" });
const [country] = pyEnv["res.country"].search_read([["id", "=", countryId]]);
await contains(`.o_country_flag[data-src*='/country_flags/${country.code.toLowerCase()}.png']`);
});
test("editing livechat note is synced between tabs", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: [serverState.groupLivechatId],
});
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({
name: "Visitor #20",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
livechat_note: "<p>Initial note</p>",
});
const tab1 = await start({ asTab: true });
const tab2 = await start({ asTab: true });
await openDiscuss(channelId, { target: tab1 });
await openDiscuss(channelId, { target: tab2 });
await contains(`${tab1.selector} .o-livechat-ChannelInfoList textarea`, {
value: "Initial note",
});
await contains(`${tab2.selector} .o-livechat-ChannelInfoList textarea`, {
value: "Initial note",
});
await insertText(`${tab1.selector} .o-livechat-ChannelInfoList textarea`, "Updated note", {
replace: true,
});
document.querySelector(`${tab1.selector} .o-livechat-ChannelInfoList textarea`).blur(); // Trigger the blur event to save the note
await contains(`${tab2.selector} .o-livechat-ChannelInfoList textarea`, {
value: "Updated note",
}); // Note should be synced with bus
});
test("shows live chat status in discuss sidebar", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({
name: "Visitor #20",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
livechat_status: "waiting",
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList button.active", { text: "Waiting for customer" });
await contains(".o-mail-DiscussSidebar-item span[title='Waiting for customer']");
await click(".o-livechat-ChannelInfoList button", { text: "Looking for help" });
await contains(".o-livechat-ChannelInfoList button.active", { text: "Looking for help" });
await contains(".o-mail-DiscussSidebar-item span[title='Looking for help']");
// live chat status icon also in messaging menu item
await click(".o_menu_systray i[aria-label='Messages']");
await contains(
".o-mail-MessagingMenu .o-mail-NotificationItem:contains('Visitor #20') [title='Looking for help']"
);
});
test("editing livechat status is synced between tabs", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({
name: "Visitor #20",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
livechat_status: "in_progress",
});
const tab1 = await start({ asTab: true });
const tab2 = await start({ asTab: true });
await openDiscuss(channelId, { target: tab1 });
await openDiscuss(channelId, { target: tab2 });
await contains(`${tab1.selector} .o-livechat-ChannelInfoList button.active`, {
text: "In progress",
});
await contains(`${tab2.selector} .o-livechat-ChannelInfoList button.active`, {
text: "In progress",
});
await click(`${tab1.selector} .o-livechat-ChannelInfoList button`, {
text: "Waiting for customer",
});
await contains(`${tab1.selector} .o-livechat-ChannelInfoList button.active`, {
text: "Waiting for customer",
});
await contains(`${tab2.selector} .o-livechat-ChannelInfoList button.active`, {
text: "Waiting for customer",
}); // Status should be synced with bus
});
test("Manage expertises from channel info list", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: [serverState.groupLivechatManagerId, serverState.groupLivechatId],
});
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({ name: "James", user_ids: [userId] });
const countryId = pyEnv["res.country"].create({ code: "be", name: "Belgium" });
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const expertiseIds = pyEnv["im_livechat.expertise"].create([{ name: "pricing" }]);
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
country_id: countryId,
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
livechat_expertise_ids: expertiseIds,
});
await start();
await openDiscuss(channelId);
await contains(".o-livechat-ChannelInfoList .o_tag", { text: "pricing" });
await insertText(".o-livechat-ExpertiseTagsAutocomplete input", "events");
await click("a", { text: 'Create "events"' });
await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events" });
await click(".o-livechat-ExpertiseTagsAutocomplete input");
await press("Backspace");
await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events", count: 0 });
await press("Backspace");
await contains(".o-livechat-ChannelInfoList .o_tag", { text: "pricing", count: 0 });
await contains(".o-livechat-ExpertiseTagsAutocomplete input[placeholder='Add expertise']");
await click("a", { text: "events" });
await contains(".o-livechat-ChannelInfoList .o_tag", { text: "events" });
});
test("Can download transcript from channel info panel", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_end_dt: serializeDate(today().plus({ days: -1 })),
});
await start();
await openDiscuss(channelId);
await contains(
`a[href='${getOrigin()}/im_livechat/download_transcript/${channelId}']:text(Download)`
);
});
test("Disable actions for non-livechat users", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_status: "in_progress",
});
await start();
await openDiscuss(channelId);
await waitFor(".o-livechat-LivechatStatusSelection button:text(In progress):disabled");
await waitFor(".o-livechat-LivechatStatusSelection button:text(Waiting for customer):disabled");
await waitFor(".o-livechat-LivechatStatusSelection button:text(Looking for help):disabled");
await waitFor("textarea[placeholder='Add your notes here...']:disabled");
await waitFor(".o-livechat-ExpertiseTagsAutocomplete.o-disabled");
});
test("info panel toggle state persists across chats", async () => {
const pyEnv = await startServer();
const [guestId1, guestId2] = pyEnv["mail.guest"].create([
{ name: "Visitor 1" },
{ name: "Visitor 2" },
]);
pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId1, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId2, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
]);
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel:text('Visitor 1')");
await contains(".o-livechat-ChannelInfoList");
await click("button[name='livechat-info']");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await click(".o-mail-DiscussSidebarChannel:text('Visitor 2')");
await contains(".o-mail-DiscussContent-threadName[title='Visitor 2']");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await click("button[name='livechat-info']");
await contains(".o-livechat-ChannelInfoList");
await click(".o-mail-DiscussSidebarChannel:text('Visitor 1')");
await contains(".o-mail-DiscussContent-threadName[title='Visitor 1']");
await contains(".o-livechat-ChannelInfoList");
});
test("auto-open of livechat info & members panels should combine", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
pyEnv["discuss.channel"].create([
{
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
},
{
channel_type: "channel",
name: "General",
},
]);
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel:text('General')");
await contains(".o-discuss-ChannelMemberList");
await click(".o-mail-DiscussSidebarChannel:text('Visitor')");
await contains(".o-discuss-ChannelMemberList", { count: 0 });
await contains(".o-livechat-ChannelInfoList");
await click("button[name='livechat-info']");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await contains(".o-discuss-ChannelMemberList", { count: 0 });
await click(".o-mail-DiscussSidebarChannel:text('General')");
await contains(".o-discuss-ChannelMemberList");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await click("button[name='member-list']");
await contains(".o-discuss-ChannelMemberList", { count: 0 });
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await click(".o-mail-DiscussSidebarChannel:text('Visitor')");
await click("button[name='livechat-info']");
await contains(".o-livechat-ChannelInfoList");
await contains(".o-discuss-ChannelMemberList", { count: 0 });
await click("button[name='member-list']");
await contains(".o-discuss-ChannelMemberList");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
await click(".o-mail-DiscussSidebarChannel:text('General')");
await contains(".o-discuss-ChannelMemberList");
await contains(".o-livechat-ChannelInfoList", { count: 0 });
});

View file

@ -0,0 +1,73 @@
import { IrWebSocket } from "@im_livechat/../tests/mock_server/mock_models/ir_websocket";
import { mailModels, startServer } from "@mail/../tests/mail_test_helpers";
import { RatingRating } from "@rating/../tests/mock_server/models/rating_rating";
import {
defineModels,
serverState,
patchWithCleanup,
MockServer,
} from "@web/../tests/web_test_helpers";
import { DiscussChannel } from "./mock_server/mock_models/discuss_channel";
import { DiscussChannelMember } from "./mock_server/mock_models/discuss_channel_member";
import { LivechatChannel } from "./mock_server/mock_models/im_livechat_channel";
import { LivechatChannelRule } from "./mock_server/mock_models/livechat_channel_rule";
import { Im_LivechatExpertise } from "./mock_server/mock_models/im_livechat_expertise";
import { ResGroupsPrivilege } from "./mock_server/mock_models/res_groups_privilege";
import { ResGroups } from "./mock_server/mock_models/res_groups";
import { ResPartner } from "./mock_server/mock_models/res_partner";
import { ResUsers } from "./mock_server/mock_models/res_users";
import { session } from "@web/session";
export function defineLivechatModels() {
return defineModels(livechatModels);
}
export const livechatModels = {
...mailModels,
DiscussChannel,
DiscussChannelMember,
LivechatChannel,
LivechatChannelRule,
Im_LivechatExpertise,
IrWebSocket,
RatingRating,
ResPartner,
ResUsers,
ResGroupsPrivilege,
ResGroups,
};
serverState.groupLivechatId = 42;
serverState.groupLivechatManagerId = 43;
/**
* Setup the server side of the livechat app.
*
* @returns {Promise<number>} the id of the livechat channel.
*/
export async function loadDefaultEmbedConfig() {
const pyEnv = MockServer.env ?? (await startServer());
const livechatChannelId = pyEnv["im_livechat.channel"].create({
user_ids: [serverState.userId],
});
patchWithCleanup(session, {
livechatData: {
can_load_livechat: true,
serverUrl: window.origin,
options: {
header_background_color: "#875A7B",
button_background_color: "#875A7B",
title_color: "#FFFFFF",
button_text_color: "#FFFFFF",
button_text: "Need help? Chat with us.",
default_message: "Hello, how may I help you?",
channel_name: "YourWebsite.com",
channel_id: livechatChannelId,
default_username: "Visitor",
review_link: "https://www.odoo.com",
},
},
});
return livechatChannelId;
}

View file

@ -0,0 +1,314 @@
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import { LFH_UNSUBSCRIBE_DELAY } from "@im_livechat/core/public_web/discuss_app_model_patch";
import {
click,
contains,
openDiscuss,
openFormView,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { advanceTime, describe, expect, test } from "@odoo/hoot";
import { tick, waitFor } from "@odoo/hoot-dom";
import {
Command,
getService,
onRpc,
patchWithCleanup,
serverState,
withUser,
} from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { Deferred } from "@web/core/utils/concurrency";
defineLivechatModels();
describe.current.tags("desktop");
test("Show looking for help in the sidebar while active or still seeking help", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
const bobChannelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down");
await contains(".o-mail-DiscussSidebarChannel", { text: "bob" });
await waitForChannels(["im_livechat.looking_for_help"]);
await rpc("/im_livechat/session/update_status", {
channel_id: bobChannelId,
livechat_status: "in_progress",
});
await contains(".o-mail-DiscussSidebarChannel", { text: "bob", count: 0 });
await rpc("/im_livechat/session/update_status", {
channel_id: bobChannelId,
livechat_status: "need_help",
});
await click(".o-mail-DiscussSidebarChannel", { text: "bob" });
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "bob" });
await waitForChannels([`discuss.channel_${bobChannelId}`]);
await rpc("/im_livechat/session/update_status", {
channel_id: bobChannelId,
livechat_status: "in_progress",
});
await contains(".o-livechat-LivechatStatusSelection .o-inProgress.active");
await waitForChannels([`discuss.channel_${bobChannelId}`]);
await contains(".o-mail-DiscussSidebarChannel", { text: "bob" });
await click(".o-mail-Mailbox[data-mailbox-id=starred");
await contains(".o-mail-DiscussSidebarChannel", { text: "bob", count: 0 });
await waitForChannels([`discuss.channel_${bobChannelId}`], { operation: "delete" });
});
test("Do not auto-open chat window on new message when locally pinned", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
setupChatHub({
folded: [
pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
}),
],
opened: [
pyEnv["discuss.channel"].create({
name: "Support",
channel_type: "channel",
}),
],
});
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
const bobChannelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
});
await start();
getService("bus_service").subscribe("discuss.channel/new_message", () =>
expect.step("discuss.channel/new_message")
);
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down");
await click(".o-mail-DiscussSidebarChannel", { text: "bob" });
await waitForChannels([`discuss.channel_${bobChannelId}`]);
await withUser(serverState.userId, async () => {
await rpc("/mail/message/post", {
post_data: {
body: "Hello, how can I help?",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: bobChannelId,
thread_model: "discuss.channel",
});
});
await contains(".o-mail-Message", { text: "Hello, how can I help?" });
await expect.waitForSteps(["discuss.channel/new_message"]);
await openFormView("res.partner", bobPartnerId);
await contains(".o-mail-ChatBubble");
await contains(".o-mail-ChatBubble[name=General]");
await contains(".o-mail-ChatBubble", { count: 0, text: "bob" });
await contains(".o-mail-ChatWindow", { text: "Support" });
await contains(".o-mail-ChatWindow", { count: 0, text: "bob" });
});
test("Enable/disable looking for help when category is opened/folded", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
localStorage.setItem("discuss_sidebar_category_im_livechat.category_need_help_open", false);
await start();
patchWithCleanup(getService("bus_service"), {
addChannel: (channelName) => {
if (channelName === "im_livechat.looking_for_help") {
expect.step(`addChannel - ${channelName}`);
}
},
deleteChannel: (channelName) => {
if (channelName === "im_livechat.looking_for_help") {
expect.step(`deleteChannel - ${channelName}`);
}
},
});
onRpc("/mail/data", async (req) => {
const { params } = await req.json();
if (params.fetch_params.includes("/im_livechat/looking_for_help")) {
expect.step("fetch looking_for_help");
}
});
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-right");
await expect.waitForSteps([]);
await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button");
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down");
await expect.waitForSteps([
"addChannel - im_livechat.looking_for_help",
"fetch looking_for_help",
]);
await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button");
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-right");
await expect.waitForSteps([]);
await advanceTime(LFH_UNSUBSCRIBE_DELAY + 1000);
await expect.waitForSteps(["deleteChannel - im_livechat.looking_for_help"]);
await click(".o-mail-DiscussSidebarCategory-livechatNeedHelp button");
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down");
await expect.waitForSteps([
"addChannel - im_livechat.looking_for_help",
"fetch looking_for_help",
]);
});
test("Show join button when help is required and self is not a member", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
const channel = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
});
await start();
await openDiscuss(channel);
await contains(".o-mail-DiscussSidebarCategory-livechatNeedHelp .oi-chevron-down");
await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" });
await click("button[name='join-livechat-needing-help']");
await contains(".o-livechat-LivechatStatusSelection .active", { text: "In progress" });
await contains("button[name='join-livechat-needing-help']", { count: 0 });
await click(".o-livechat-LivechatStatusSelection button", { text: "Looking for help" });
await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" });
// Now that we are members, the button is not shown, even if help is required.
await contains("button[name='join-livechat-needing-help']", { count: 0 });
});
test("Show notification when joining a channel that already received help", async () => {
const pyEnv = await startServer();
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
// Simulate another agent attempting to join the channel to provide help at the same time,
// but succeeding just before the current agent (server returns false when it happens).
onRpc("discuss.channel", "livechat_join_channel_needing_help", () => false);
const channel = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
});
const env = await start();
patchWithCleanup(env.services.notification, {
add: (message, options) => expect.step(`${options.type} - ${message}`),
});
await openDiscuss(channel);
await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" });
await click("button[name='join-livechat-needing-help']");
expect.waitForSteps(["warning - Someone has already joined this conversation"]);
});
test("Hide 'help already received' notification when channel is not visible", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
// Simulate another agent attempting to join the channel to provide help at the same time,
// but succeeding just before the current agent (server returns false when it happens).
let canRespondDeferred;
onRpc("discuss.channel", "livechat_join_channel_needing_help", async () => {
await canRespondDeferred;
return false;
});
const channel = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
});
const env = await start();
patchWithCleanup(env.services.notification, {
add: (message, options) => expect.step(`${options.type} - ${message}`),
});
await openDiscuss(channel);
await contains(".o-livechat-LivechatStatusSelection .active", { text: "Looking for help" });
await click("button[name='join-livechat-needing-help']");
expect.waitForSteps(["warning - Someone has already joined this conversation"]);
canRespondDeferred = new Deferred();
await click("button[name='join-livechat-needing-help']");
await click(".o-mail-DiscussSidebar-item", { text: "Inbox" });
await contains(".o-mail-DiscussContent-threadName[title='Inbox']");
canRespondDeferred.resolve();
await tick();
await expect.waitForSteps([]);
});
test("Expertise matching hint is shown in the sidebar when chat is looking for help", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const bobPartnerId = pyEnv["res.partner"].create({
name: "bob",
user_ids: [Command.create({ name: "bob" })],
});
const janePartnerId = pyEnv["res.partner"].create({
name: "jane",
user_ids: [Command.create({ name: "jane" })],
});
const expertiseIds = pyEnv["im_livechat.expertise"].create([{ name: "pricing" }]);
pyEnv["res.users"].write([serverState.userId], { livechat_expertise_ids: expertiseIds });
pyEnv["discuss.channel"].create([
{
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: bobPartnerId })],
livechat_status: "need_help",
livechat_expertise_ids: expertiseIds,
},
{
channel_type: "livechat",
channel_member_ids: [Command.create({ partner_id: janePartnerId })],
livechat_status: "need_help",
},
]);
await start();
await openDiscuss();
await waitFor(
".o-mail-DiscussSidebarChannel:text(bob):has([title='Relevant to your expertise'])"
);
await waitFor(".o-mail-DiscussSidebarChannel:text(jane)");
await waitFor(
".o-mail-DiscussSidebarChannel:text(jane):not(:has([title='Relevant to your expertise']))"
);
});

View file

@ -0,0 +1,47 @@
import { click, contains, patchUiSize, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test('livechats should be in "chat" filter', async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-MessagingMenu button.fw-bold", { text: "Notifications" });
await contains(".o-mail-NotificationItem", { text: "Visitor 11" });
await click(".o-mail-MessagingMenu button", { text: "Chats" });
await contains(".o-mail-MessagingMenu button.fw-bold", { text: "Chats" });
await contains(".o-mail-NotificationItem", { text: "Visitor 11" });
});
test('livechats should be in "livechat" tab in mobile', async () => {
patchUiSize({ height: 360, width: 640 });
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click("button", { text: "Live Chats" });
await contains(".o-mail-NotificationItem", { text: "Visitor 11" });
await click("button", { text: "Chats" });
await contains(".o-mail-NotificationItem", { count: 0, text: "Visitor 11" });
});

View file

@ -0,0 +1,60 @@
import {
contains,
listenStoreFetch,
start,
startServer,
waitStoreFetch,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("push notifications are Odoo toaster on Android", async () => {
// Notifications without ServiceWorker in Chrome Android no longer work.
// This simulates Android Notification behavior by throwing a
// ServiceWorkerRegistration error as a fallback.
patchWithCleanup(window, {
Notification: class Notification {
static get permission() {
return "granted";
}
constructor() {
throw new Error("ServiceWorkerRegistration error");
}
},
});
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor" });
const channelId = pyEnv["discuss.channel"].create({
name: "Livechat 1",
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
});
listenStoreFetch("init_messaging");
await start();
await waitStoreFetch("init_messaging");
// send after init_messaging because bus subscription is done after init_messaging
await withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "Hello world!",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_model: "discuss.channel",
thread_id: channelId,
})
);
await contains(".o_notification:has(.o_notification_bar.bg-info)", {
text: "Visitor. Hello world!",
});
});

View file

@ -0,0 +1,42 @@
import { describe, test } from "@odoo/hoot";
import {
SIZES,
click,
contains,
patchUiSize,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("Livechat button is not present when there is no livechat thread", async () => {
patchUiSize({ size: SIZES.SM });
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-MessagingMenu");
await contains(".o-mail-MessagingMenu-navbar span", { count: 0, text: "Livechat" });
});
test("Livechat button is present when there is at least one livechat thread", async () => {
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
partner_id: serverState.publicPartnerId,
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await contains(".o-mail-MessagingMenu");
await contains(".o-mail-MessagingMenu-navbar", { text: "Live Chats" });
});

View file

@ -0,0 +1,265 @@
import {
mailDataHelpers,
parseRequestParams,
registerRoute,
} from "@mail/../tests/mock_server/mail_mock_server";
import { Command, makeKwArgs, serverState } from "@web/../tests/web_test_helpers";
import { loadBundle } from "@web/core/assets";
import { patch } from "@web/core/utils/patch";
/**
* @template [T={}]
* @typedef {import("@web/../tests/web_test_helpers").RouteCallback<T>} RouteCallback
*/
registerRoute("/im_livechat/get_session", get_session);
/** @type {RouteCallback} */
async function get_session(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
/** @type {import("mock_models").LivechatChannel} */
const LivechatChannel = this.env["im_livechat.channel"];
/** @type {import("mock_models").ResCountry} */
const ResCountry = this.env["res.country"];
/** @type {import("mock_models").ResPartner} */
const ResPartner = this.env["res.partner"];
/** @type {import("mock_models").ResUsers} */
const ResUsers = this.env["res.users"];
let {
channel_id,
previous_operator_id,
persisted,
context = {},
} = await parseRequestParams(request);
previous_operator_id = parseInt(previous_operator_id);
const agent = LivechatChannel._get_operator(channel_id, previous_operator_id);
if (!agent) {
return false;
}
let country_id;
if (this.env.user && !ResUsers._is_public(this.env.uid)) {
country_id = this.env.user.country_id;
} else if (context.mockedCountryCode) {
// simulate geoip
const country = ResCountry._filter([["code", "=", context.mockedCountryCode]])[0];
if (country) {
country_id = country.id;
}
}
if (!persisted) {
const store = new mailDataHelpers.Store();
ResUsers._init_store_data(store);
store.add("discuss.channel", {
channel_type: "livechat",
fetchChannelInfoState: "fetched",
id: -1,
isLoaded: true,
livechat_operator_id: mailDataHelpers.Store.one(
ResPartner.browse(agent.partner_id),
makeKwArgs({ fields: ["avatar_128", "user_livechat_username"] })
),
scrollUnread: false,
});
return { store_data: store.get_result(), channel_id: -1 };
}
const channelVals = LivechatChannel._get_livechat_discuss_channel_vals(channel_id, {
agent: agent,
});
channelVals.country_id = country_id;
const channelId = DiscussChannel.create(channelVals);
const store = new mailDataHelpers.Store();
ResUsers._init_store_data(store);
store.add(DiscussChannel.browse(channelId));
store.add(DiscussChannel.browse(channelId), {
isLoaded: true,
scrollUnread: false,
});
return { store_data: store.get_result(), channel_id: channelId };
}
registerRoute("/im_livechat/visitor_leave_session", visitor_leave_session);
/** @type {RouteCallback} */
async function visitor_leave_session(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
const { channel_id } = await parseRequestParams(request);
const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return;
}
DiscussChannel._close_livechat_session(channel_id);
}
registerRoute("/im_livechat/feedback", feedback);
/** @type {RouteCallback} */
async function feedback(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
/** @type {import("mock_models").RatingRating} */
const RatingRating = this.env["rating.rating"];
const { channel_id, rate, reason } = await parseRequestParams(request);
let [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return false;
}
const values = {
rating: rate,
consumed: true,
feedback: reason,
is_internal: false,
res_id: channel.id,
res_model: "discuss.channel",
rated_partner_id: channel.channel_partner_ids[0],
};
if (channel.rating_ids.length === 0) {
RatingRating.create(values);
} else {
RatingRating.write([channel.rating_ids[0]], values);
}
[channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
return channel.rating_ids[0];
}
registerRoute("/im_livechat/init", livechat_init);
/** @type {RouteCallback} */
async function livechat_init(request) {
return {
available_for_me: true,
rule: {},
};
}
registerRoute("/im_livechat/email_livechat_transcript", email_livechat_transcript);
/** @type {RouteCallback} */
async function email_livechat_transcript(request) {
const DiscussChannel = this.env["discuss.channel"];
const { channel_id, email } = await parseRequestParams(request);
const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return;
}
DiscussChannel._email_livechat_transcript(channel_id, email);
}
registerRoute("/im_livechat/emoji_bundle", get_emoji_bundle);
/** @type {RouteCallback} */
async function get_emoji_bundle(request) {
await loadBundle("web.assets_emoji");
return new Response();
}
registerRoute("/im_livechat/session/update_status", session_update_status);
/** @type {RouteCallback} */
async function session_update_status(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
const { channel_id, livechat_status } = await parseRequestParams(request);
if (this.env.user.share) {
return false;
}
const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return false;
}
DiscussChannel.write([channel_id], {
livechat_status: livechat_status,
});
return true;
}
registerRoute("/im_livechat/session/update_note", session_update_note);
/** @type {RouteCallback} */
async function session_update_note(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
const { channel_id, note } = await parseRequestParams(request);
if (this.env.user.share) {
return false;
}
const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return false;
}
DiscussChannel.write([channel_id], {
livechat_note: note,
});
return true;
}
registerRoute("/im_livechat/conversation/write_expertises", livechat_conversation_write_expertises);
/** @type {RouteCallback} */
async function livechat_conversation_write_expertises(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
const { channel_id, orm_commands } = await parseRequestParams(request);
const [channel] = DiscussChannel.search_read([["id", "=", channel_id]]);
if (!channel) {
return false;
}
DiscussChannel.write(channel_id, { livechat_expertise_ids: orm_commands });
}
registerRoute(
"/im_livechat/conversation/create_and_link_expertise",
livechat_conversation_create_and_link_expertise
);
/** @type {RouteCallback} */
async function livechat_conversation_create_and_link_expertise(request) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
/** @type {import("mock_models").ImLivechatExpertise} */
const ImLivechatExpertise = this.env["im_livechat.expertise"];
const { channel_id, expertise_name } = await parseRequestParams(request);
const [channel] = DiscussChannel.search([["id", "=", channel_id]]);
if (!channel) {
return false;
}
const [expertise] = ImLivechatExpertise.search([["name", "=", expertise_name]]);
let expertiseId = expertise?.id;
if (!expertise) {
expertiseId = ImLivechatExpertise.create({ name: expertise_name });
}
DiscussChannel.write(channel_id, { livechat_expertise_ids: [Command.link(expertiseId)] });
}
patch(mailDataHelpers, {
_process_request_for_all(store, name, params) {
const ResPartner = this.env["res.partner"];
const ResUsers = this.env["res.users"];
super._process_request_for_all(...arguments);
store.add({ livechat_available: true });
if (name === "init_livechat") {
if (this.env.user && !ResUsers._is_public(this.env.uid)) {
store.add(
ResPartner.browse(this.env.user.partner_id),
makeKwArgs({ fields: ["email"] })
);
}
}
},
_process_request_for_internal_user(store, name, params) {
super._process_request_for_internal_user(...arguments);
if (name === "im_livechat.channel") {
const LivechatChannel = this.env["im_livechat.channel"];
store.add(
LivechatChannel.browse(LivechatChannel.search([])),
makeKwArgs({ fields: ["are_you_inside", "name"] })
);
return;
}
if (name === "/im_livechat/looking_for_help") {
const DiscussChannel = this.env["discuss.channel"];
store.add(
DiscussChannel.browse(
DiscussChannel.search([["livechat_status", "=", "need_help"]])
)
);
}
if (name === "/im_livechat/fetch_self_expertise") {
const ResUsers = this.env["res.users"];
store.add(ResUsers.browse(serverState.userId), ["livechat_expertise_ids"]);
}
},
});

View file

@ -0,0 +1,11 @@
declare module "mock_models" {
import { LivechatChannel as LivechatChannel2 } from "../im_livechat_channel";
import { RatingRating as RatingRating2 } from "../rating_rating";
export interface LivechatChannel extends LivechatChannel2 {}
export interface RatingRating extends RatingRating2 {}
export interface Models {
"im_livechat.channel": LivechatChannel,
}
}

View file

@ -0,0 +1,221 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { fields, getKwArgs, makeKwArgs, serverState } from "@web/../tests/web_test_helpers";
import { serializeDate } from "@web/core/l10n/dates";
import { ensureArray } from "@web/core/utils/arrays";
export class DiscussChannel extends mailModels.DiscussChannel {
livechat_channel_id = fields.Many2one({ relation: "im_livechat.channel", string: "Channel" }); // FIXME: somehow not fetched properly
livechat_lang_id = fields.Many2one({ relation: "res.lang", string: "Language" });
livechat_note = fields.Html({ sanitize: true });
livechat_status = fields.Selection({
selection: [
("in_progress", "In progress"),
("waiting", "Waiting for customer"),
("need_help", "Looking for help"),
],
});
livechat_expertise_ids = fields.Many2many({
relation: "im_livechat.expertise",
});
action_unfollow(idOrIds) {
/** @type {import("mock_models").BusBus} */
const BusBus = this.env["bus.bus"];
const ids = ensureArray(idOrIds);
for (const channel_id of ids) {
const [channel] = this.browse(channel_id);
if (channel.channel_type == "livechat" && channel.channel_member_ids.length <= 2) {
this.write([channel.id], { livechat_end_dt: serializeDate(luxon.DateTime.now()) });
BusBus._sendone(
channel,
"mail.record/insert",
new mailDataHelpers.Store()
.add(this.browse(channel_id), makeKwArgs({ fields: ["livechat_end_dt"] }))
.get_result()
);
}
}
return super.action_unfollow(...arguments);
}
/**
* @override
* @param {number[]} ids
* @param {number[]} partner_ids
* @param {boolean} [invite_to_rtc_call=undefined]
*/
add_members(ids, partner_ids, invite_to_rtc_call) {
const kwargs = getKwArgs(arguments, "ids", "partner_ids", "invite_to_rtc_call");
ids = kwargs.ids;
delete kwargs.ids;
partner_ids = kwargs.partner_ids || [];
const channels = this.browse(
Array.from(super.add_members(ids, partner_ids, invite_to_rtc_call)).map(
({ channel_id }) => channel_id
)
);
for (const channel of channels) {
if (channel.livechat_status == "need_help") {
this.write([channel.id], { livechat_status: "in_progress" });
}
}
}
_channel_basic_info_fields() {
return [
...super._channel_basic_info_fields(),
"livechat_lang_id",
"livechat_note",
"livechat_status",
"livechat_expertise_ids",
];
}
/**
* @override
* @type {typeof mailModels.DiscussChannel["prototype"]["_to_store"]}
*/
_to_store(store) {
/** @type {import("mock_models").ResCountry} */
const ResCountry = this.env["res.country"];
/** @type {import("mock_models").ResLang} */
const ResLang = this.env["res.lang"];
/** @type {import("mock_models").ResPartner} */
const ResPartner = this.env["res.partner"];
super._to_store(...arguments);
for (const channel of this) {
const channelInfo = {};
const [country] = ResCountry.browse(channel.country_id);
channelInfo["country_id"] = country
? {
code: country.code,
id: country.id,
name: country.name,
}
: false;
// add the last message date
if (channel.channel_type === "livechat") {
// add the operator id
if (channel.livechat_operator_id) {
// livechat_username ignored for simplicity
channelInfo.livechat_operator_id = mailDataHelpers.Store.one(
ResPartner.browse(channel.livechat_operator_id),
makeKwArgs({ fields: ["avatar_128", "user_livechat_username"] })
);
} else {
channelInfo.livechat_operator_id = false;
}
channelInfo.livechat_lang_id = channel.livechat_lang_id
? mailDataHelpers.Store.one(
ResLang.browse(channel.livechat_lang_id),
makeKwArgs({ fields: ["name"] })
)
: false;
channelInfo["livechat_end_dt"] = channel.livechat_end_dt;
channelInfo["livechat_note"] = ["markup", channel.livechat_note];
channelInfo["livechat_status"] = channel.livechat_status;
channelInfo["livechat_expertise_ids"] = mailDataHelpers.Store.many(
this.env["im_livechat.expertise"].browse(channel.livechat_expertise_ids),
makeKwArgs({ fields: ["name"] })
);
channelInfo.livechat_channel_id = mailDataHelpers.Store.one(
this.env["im_livechat.channel"].browse(channel.livechat_channel_id),
makeKwArgs({ fields: ["name"] })
);
}
store._add_record_fields(this.browse(channel.id), channelInfo);
}
}
_close_livechat_session(channel_id) {
/** @type {import("mock_models").BusBus} */
const BusBus = this.env["bus.bus"];
if (this.browse(channel_id)[0].livechat_end_dt) {
return;
}
this.write([channel_id], { livechat_end_dt: serializeDate(luxon.DateTime.now()) });
const [channel] = this.browse(channel_id);
BusBus._sendone(
channel,
"mail.record/insert",
new mailDataHelpers.Store()
.add(this.browse(channel_id), makeKwArgs({ fields: ["livechat_end_dt"] }))
.get_result()
);
if (channel.message_ids.length === 0) {
return;
}
this.message_post(
channel.id,
makeKwArgs({
body: this._get_visitor_leave_message(),
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
})
);
}
_get_visitor_leave_message() {
return "Visitor left the conversation.";
}
_email_livechat_transcript(channel_id, email) {
const [channel] = this.browse(channel_id);
this.message_post(
channel.id,
makeKwArgs({
body: `<div class="o_mail_notification o_hide_author">${this.env.user.name} sent the conversation to ${email}</div>`,
message_type: "notification",
subtype_xmlid: "mail.mt_comment",
})
);
}
/**
* @override
* @type {typeof mailModels.DiscussChannel["prototype"]["_types_allowing_seen_infos"]}
*/
_types_allowing_seen_infos() {
return super._types_allowing_seen_infos(...arguments).concat(["livechat"]);
}
livechat_join_channel_needing_help(idOrIds) {
const channel = this.browse(idOrIds)[0];
if (channel.livechat_status !== "need_help") {
return false;
}
this.add_members([channel.id], [this.env.user.partner_id]);
return true;
}
/** @type {typeof models.Model["prototype"]["write"]} */
write(idOrIds, values) {
const kwargs = getKwArgs(arguments, "ids", "vals");
({ ids: idOrIds, vals: values } = kwargs);
const needHelpBefore = [];
for (const channel of this._filter([["livechat_status", "=", "need_help"]])) {
needHelpBefore.push(channel.id);
}
const result = super.write(...arguments);
const needHelpAfter = [];
for (const channel of this._filter([["livechat_status", "=", "need_help"]])) {
needHelpAfter.push(channel.id);
}
const updatedChannelIds = [
...needHelpBefore.filter((id) => !needHelpAfter.includes(id)),
...needHelpAfter.filter((id) => !needHelpBefore.includes(id)),
];
if (updatedChannelIds.length) {
this.env["bus.bus"]._sendone(
[this.env["res.groups"].browse(serverState.groupLivechatId), "LOOKING_FOR_HELP"],
"mail.record/insert",
new mailDataHelpers.Store().add(this.browse(updatedChannelIds)).get_result()
);
}
return result;
}
}

View file

@ -0,0 +1,55 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { fields } from "@web/../tests/web_test_helpers";
export class DiscussChannelMember extends mailModels.DiscussChannelMember {
livechat_member_type = fields.Selection({
selection: [
["agent", "Agent"],
["visitor", "Visitor"],
["bot", "Chatbot"],
],
compute: false,
});
/**
* @override
* @type {typeof mailModels.DiscussChannelMember["prototype"]["_get_store_partner_fields"]}
*/
_get_store_partner_fields(fields) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
const member = this[0];
const [channel] = DiscussChannel.browse(member.channel_id);
if (channel.channel_type === "livechat") {
if (!fields) {
fields = [
"active",
"avatar_128",
"country_id",
"im_status",
"is_public",
"user_livechat_username",
];
if (member.livechat_member_type == "visitor") {
fields.push("offline_since", "email");
}
}
}
return super._get_store_partner_fields(fields);
}
/**
* @override
* @type {typeof mailModels.DiscussChannelMember["prototype"]["_to_store"]}
*/
_to_store(store, fields, extra_fields) {
super._to_store(...arguments);
for (const member of this) {
store._add_record_fields(this.browse(member.id), {
livechat_member_type: member.livechat_member_type,
});
}
}
get _to_store_defaults() {
return super._to_store_defaults.concat(["livechat_member_type"]);
}
}

View file

@ -0,0 +1,124 @@
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { Command, fields, getKwArgs, makeKwArgs, models } from "@web/../tests/web_test_helpers";
export class LivechatChannel extends models.ServerModel {
_name = "im_livechat.channel";
available_operator_ids = fields.Many2many({ relation: "res.users" }); // FIXME: somehow not fetched properly
user_ids = fields.Many2many({ relation: "res.users" }); // FIXME: somehow not fetched properly
/** @param {integer} id */
action_join(id) {
this.write([id], { user_ids: [Command.link(this.env.user.id)] });
const [partner] = this.env["res.partner"].read(this.env.user.partner_id);
this.env["bus.bus"]._sendone(
partner,
"mail.record/insert",
new mailDataHelpers.Store(
this.browse(id),
makeKwArgs({ fields: ["are_you_inside", "name"] })
).get_result()
);
}
/** @param {integer} id */
action_quit(id) {
this.write(id, { user_ids: [Command.unlink(this.env.user.id)] });
const [partner] = this.env["res.partner"].read(this.env.user.partner_id);
this.env["bus.bus"]._sendone(
partner,
"mail.record/insert",
new mailDataHelpers.Store(
this.browse(id),
makeKwArgs({ fields: ["are_you_inside", "name"] })
).get_result()
);
}
/** @param {integer} id */
_compute_available_operator_ids(id) {
/** @type {import("mock_models").ResUsers} */
const ResUsers = this.env["res.users"];
const [livechatChannel] = this.browse(id);
const users = ResUsers.browse(livechatChannel.user_ids);
return users.filter((user) => user.im_status === "online");
}
/** @param {integer} id */
_get_livechat_discuss_channel_vals(id, operator_info) {
/** @type {import("mock_models").MailGuest} */
const MailGuest = this.env["mail.guest"];
/** @type {import("mock_models").ResUsers} */
const ResUsers = this.env["res.users"];
const agent = operator_info["agent"];
const membersToAdd = [
Command.create({
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
partner_id: agent.partner_id,
unpin_dt: "2021-01-01 12:00:00",
}),
];
const guest = ResUsers._is_public(this.env.uid) && MailGuest._get_guest_from_context();
if (guest) {
membersToAdd.push(
Command.create({ guest_id: guest.id, livechat_member_type: "visitor" })
);
}
let visitorUser;
if (this.env.user && !ResUsers._is_public(this.env.uid) && this.env.user !== agent) {
visitorUser = this.env.user;
membersToAdd.push(
Command.create({
livechat_member_type: "visitor",
partner_id: visitorUser.partner_id,
})
);
}
const membersName = [
visitorUser ? visitorUser.display_name : guest.name,
agent.livechat_username ? agent.livechat_username : agent.name,
];
return {
channel_partner_ids: [agent.partner_id],
channel_member_ids: membersToAdd,
livechat_operator_id: agent.partner_id,
livechat_channel_id: id,
livechat_status: "in_progress",
channel_type: "livechat",
name: membersName.join(" "),
};
}
/**
* Simplified mock implementation: returns
* the previous operator if he is still available
* or the first available operator.
*
* @param {integer} id
*/
_get_operator(id, previous_operator_id) {
const availableUsers = this._compute_available_operator_ids(id);
return (
availableUsers.find((operator) => operator.partner_id === previous_operator_id) ??
availableUsers[0]
);
}
_to_store(store, fields) {
const kwargs = getKwArgs(arguments, "store", "fields");
fields = kwargs.fields;
store._add_record_fields(
this,
fields.filter((field) => field !== "are_you_inside")
);
for (const livechatChannel of this) {
if (fields.includes("are_you_inside")) {
store._add_record_fields(this.browse(livechatChannel.id), {
are_you_inside: livechatChannel.user_ids.includes(this.env.user.id),
});
}
}
}
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class Im_LivechatExpertise extends models.ServerModel {
_name = "im_livechat.expertise";
}

View file

@ -0,0 +1,23 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { serverState } from "@web/../tests/web_test_helpers";
export class IrWebSocket extends mailModels.IrWebSocket {
/**
* @override
* @type {typeof busModels.IrWebSocket["prototype"]["_build_bus_channel_list"]}
*/
_build_bus_channel_list(channels) {
channels = [...super._build_bus_channel_list(channels)];
const result = channels;
for (const channel of channels) {
if (channel === "im_livechat.looking_for_help") {
result.push([
this.env["res.groups"].browse(serverState.groupLivechatId)[0],
"LOOKING_FOR_HELP",
]);
}
}
return result.filter((channel) => channel !== "im_livechat.looking_for_help");
}
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class LivechatChannelRule extends models.ServerModel {
_name = "im_livechat.channel.rule";
}

View file

@ -0,0 +1,39 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { makeKwArgs } from "@web/../tests/web_test_helpers";
export class MailMessage extends mailModels.MailMessage {
_author_to_store(ids, store) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
/** @type {import("mock_models").MailMessage} */
const MailMessage = this.env["mail.message"];
/** @type {import("mock_models").ResPartner} */
const ResPartner = this.env["res.partner"];
const messages_w_author_livechat = MailMessage.browse(ids).filter((message) => {
if (!message.author_id || message.model !== "discuss.channel" || !message.res_id) {
return false;
}
const channel = DiscussChannel.browse(message.res_id);
return channel.channel_type === "livechat";
});
super._author_to_store(
ids.filter(
(id) => !messages_w_author_livechat.map((message) => message.id).includes(id)
),
store
);
for (const message of messages_w_author_livechat) {
store.add(this.browse(message.id), {
author_id: mailDataHelpers.Store.one(
ResPartner.browse(message.author_id),
makeKwArgs({
fields: ["avatar_128", "is_company", "user_livechat_username", "user"],
})
),
});
}
}
}

View file

@ -0,0 +1,18 @@
import { serverState } from "@web/../tests/web_test_helpers";
import { mailModels } from "@mail/../tests/mail_test_helpers";
export class ResGroups extends mailModels.ResGroups {
_records = [
...this._records,
{
id: serverState.groupLivechatId,
name: "Livechat User",
privilege_id: false,
},
{
id: serverState.groupLivechatManagerId,
name: "Livechat Manager",
privilege_id: false,
},
];
}

View file

@ -0,0 +1,3 @@
import { webModels } from "@web/../tests/web_test_helpers";
export class ResGroupsPrivilege extends webModels.ResGroupsPrivilege {}

View file

@ -0,0 +1,79 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { getKwArgs, makeKwArgs, serverState } from "@web/../tests/web_test_helpers";
export class ResPartner extends mailModels.ResPartner {
/**
* @override
* @type {typeof mailModels.ResPartner["prototype"]["_search_for_channel_invite_to_store"]}
*/
_search_for_channel_invite_to_store(ids, store, channel_id) {
/** @type {import("mock_models").DiscussChannel} */
const DiscussChannel = this.env["discuss.channel"];
/** @type {import("mock_models").DiscussChannelMember} */
const DiscussChannelMember = this.env["discuss.channel.member"];
/** @type {import("mock_models").LivechatChannel} */
const LivechatChannel = this.env["im_livechat.channel"];
/** @type {import("mock_models").Im_LivechatExpertise} */
const Im_LivechatExpertise = this.env["im_livechat.expertise"];
/** @type {import("mock_models").ResLang} */
const ResLang = this.env["res.lang"];
/** @type {import("mock_models").ResPartner} */
const ResPartner = this.env["res.partner"];
/** @type {import("mock_models").ResUsers} */
const ResUsers = this.env["res.users"];
super._search_for_channel_invite_to_store(ids, store, channel_id);
const [channel] = DiscussChannel.browse(channel_id);
if (channel?.channel_type !== "livechat") {
return;
}
const activeLivechatPartners = LivechatChannel._filter([])
.map(({ available_operator_ids }) => available_operator_ids)
.flat()
.map((userId) => ResUsers.browse(userId)[0].partner_id);
for (const partner of ResPartner.browse(ids)) {
const data = {
invite_by_self_count: DiscussChannelMember.search_count([
["partner_id", "=", partner.id],
["create_uid", "=", serverState.userId],
]),
is_available: activeLivechatPartners.includes(partner.id),
};
if (partner.lang) {
data.lang_name = ResLang.search_read([["code", "=", partner.lang]])[0].name;
}
if (partner.user_ids.length) {
const [user] = ResUsers.browse(partner.user_ids[0]);
if (user) {
const userLangs = user.livechat_lang_ids
.map((langId) => ResLang.browse(langId)[0])
.filter((lang) => lang.name !== data.lang_name);
data.livechat_languages = userLangs.map((lang) => lang.name);
data.livechat_expertise = user.livechat_expertise_ids.map(
(expId) => Im_LivechatExpertise.browse(expId)[0].name
);
}
}
store.add(this.browse(partner.id), makeKwArgs({ fields: ["user_livechat_username"] }));
store.add(this.browse(partner.id), data);
store.add(this.browse(partner.id), makeKwArgs({ extra_fields: ["is_in_call"] }));
}
}
/**
* @override
* @type {typeof mailModels.ResPartner["prototype"]["_to_store"]}
*/
_to_store(store, fields) {
const kwargs = getKwArgs(arguments, "store", "fields");
fields = kwargs.fields;
super._to_store(...arguments);
if (fields && fields.includes("user_livechat_username")) {
store._add_record_fields(
this.filter((partner) => !partner.user_livechat_username),
["name"]
);
}
}
}

View file

@ -0,0 +1,19 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { serverState } from "@web/../tests/web_test_helpers";
export class ResUsers extends mailModels.ResUsers {
/**
* @override
*/
_init_store_data(store) {
super._init_store_data(...arguments);
store.add({
has_access_livechat: this.env.user?.group_ids.includes(serverState.groupLivechatId),
});
store.add(this.browse(this.env.uid), {
is_livechat_manager: this.env.user?.group_ids.includes(
serverState.groupLivechatManagerId
),
});
}
}

View file

@ -1,52 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('chat_window_manager', {}, function () {
QUnit.module('chat_window_manager_tests.js');
QUnit.test('closing a chat window with no message from admin side unpins it', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" });
pyEnv['res.users'].create({ partner_id: resPartnerId1 });
const mailChannelId1 = pyEnv['mail.channel'].create(
{
channel_member_ids: [
[0, 0, {
is_pinned: true,
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: "livechat",
uuid: 'channel-10-uuid',
},
);
const { messaging } = await start();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click());
await afterNextRender(() => document.querySelector(`.o_ChatWindowHeader_commandClose`).click());
const channels = await messaging.rpc({
model: 'mail.channel',
method: 'channel_info',
args: [mailChannelId1],
}, { shadow: true });
assert.strictEqual(
channels[0].is_pinned,
false,
'Livechat channel should not be pinned',
);
});
});
});
});

View file

@ -1,50 +0,0 @@
/** @odoo-module **/
import { start, startServer } from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('composer_tests.js');
QUnit.test('livechat: no add attachment button', async function (assert) {
// Attachments are not yet supported in livechat, especially from livechat
// visitor PoV. This may likely change in the future with task-2029065.
assert.expect(2);
const pyEnv = await startServer();
const livechatId = pyEnv['mail.channel'].create({ channel_type: 'livechat' });
const { openDiscuss } = await start({
discuss: {
context: { active_id: livechatId },
},
});
await openDiscuss();
assert.containsOnce(document.body, '.o_Composer', "should have a composer");
assert.containsNone(
document.body,
'.o_Composer_buttonAttachment',
"composer linked to livechat should not have a 'Add attachment' button"
);
});
QUnit.test('livechat: disable attachment upload via drag and drop', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const livechatId = pyEnv['mail.channel'].create({ channel_type: 'livechat' });
const { openDiscuss } = await start({
discuss: {
context: { active_id: livechatId },
},
});
await openDiscuss();
assert.containsOnce(document.body, '.o_Composer', "should have a composer");
assert.containsNone(
document.body,
'.o_Composer_dropZone',
"composer linked to livechat should not have a dropzone"
);
});
});
});

View file

@ -1,77 +0,0 @@
/** @odoo-module **/
import {
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_sidebar_category_item_tests.js');
QUnit.test('livechat - avatar: should have a smiley face avatar for an anonymous livechat item', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
const livechatItem = document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
`);
assert.containsOnce(
livechatItem,
`.o_DiscussSidebarCategoryItem_image`,
"should have an avatar"
);
assert.strictEqual(
livechatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src,
'/mail/static/src/img/smiley/avatar.jpg',
'should have the smiley face as the avatar for anonymous users'
);
});
QUnit.test('livechat - avatar: should have a partner profile picture for a livechat item linked with a partner', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const resPartnerId1 = pyEnv['res.partner'].create({
name: "Jean",
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
const livechatItem = document.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
`);
assert.containsOnce(
livechatItem,
`.o_DiscussSidebarCategoryItem_image`,
"should have an avatar"
);
assert.strictEqual(
livechatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src,
`/web/image/res.partner/${resPartnerId1}/avatar_128`,
'should have the partner profile picture as the avatar for partners'
);
});
});
});

View file

@ -1,449 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_sidebar_category_tests.js');
QUnit.test('livechat - counter: should not have a counter if the category is unfolded and without unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
`.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`,
"should not have a counter if the category is unfolded and without unread messages",
);
});
QUnit.test('livechat - counter: should not have a counter if the category is unfolded and with unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, {
message_unread_counter: 10,
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
`.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`,
"should not have a counter if the category is unfolded and with unread messages",
);
});
QUnit.test('livechat - counter: should not have a counter if category is folded and without unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
`.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`,
"should not have a counter if the category is folded and without unread messages"
);
});
QUnit.test('livechat - counter: should have correct value of unread threads if category is folded and with unread messages', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, {
message_unread_counter: 10,
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.strictEqual(
document.querySelector(`.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_counter`).textContent,
"1",
"should have correct value of unread threads if category is folded and with unread messages"
);
});
QUnit.test('livechat - states: close manually by clicking the title', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: true,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
// fold the livechat category
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category livechat should be closed and the content should be invisible"
);
});
QUnit.test('livechat - states: open manually by clicking the title', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: false,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
// open the livechat category
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category livechat should be open and the content should be visible"
);
});
QUnit.test('livechat - states: close should update the value on the server', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: true,
});
const currentUserId = pyEnv.currentUserId;
const { messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_livechat_open,
true,
"the value in server side should be true"
);
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_livechat_open,
false,
"the value in server side should be false"
);
});
QUnit.test('livechat - states: open should update the value on the server', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: false,
});
const currentUserId = pyEnv.currentUserId;
const { messaging, openDiscuss } = await start();
await openDiscuss();
const initalSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
initalSettings.is_discuss_sidebar_category_livechat_open,
false,
"the value in server side should be false"
);
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
const newSettings = await messaging.rpc({
model: 'res.users.settings',
method: '_find_or_create_for_user',
args: [[currentUserId]],
});
assert.strictEqual(
newSettings.is_discuss_sidebar_category_livechat_open,
true,
"the value in server side should be true"
);
});
QUnit.test('livechat - states: close from the bus', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: true,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_livechat_open': false,
});
});
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category livechat should be closed and the content should be invisible"
);
});
QUnit.test('livechat - states: open from the bus', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const resUsersSettingsId1 = pyEnv['res.users.settings'].create({
user_id: pyEnv.currentUserId,
is_discuss_sidebar_category_livechat_open: false,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
await afterNextRender(() => {
pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'res.users.settings/insert', {
id: resUsersSettingsId1,
'is_discuss_sidebar_category_livechat_open': true,
});
});
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"Category livechat should be open and the content should be visible"
);
});
QUnit.test('livechat - states: category item should be invisible if the category is closed', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
assert.containsNone(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
"inactive item should be invisible if the category is folded"
);
});
QUnit.test('livechat - states: the active category item should be visble even if the category is closed', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`
);
const livechat = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`);
await afterNextRender(() => {
livechat.click();
});
assert.ok(livechat.classList.contains('o-active'));
await afterNextRender(() =>
document.querySelector(`.o_DiscussSidebarCategory[data-category-local-id="${
messaging.discuss.categoryLivechat.localId}"]
.o_DiscussSidebarCategory_title
`).click()
);
assert.containsOnce(
document.body,
`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`,
'the active livechat item should remain open even if the category is folded'
);
});
});
});

View file

@ -1,435 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
nextAnimationFrame,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
import { datetime_to_str } from 'web.time';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss_tests.js');
QUnit.test('livechat in the sidebar: basic rendering', async function (assert) {
assert.expect(5);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(document.body, '.o_Discuss_sidebar',
"should have a sidebar section"
);
const groupLivechat = document.querySelector('.o_DiscussSidebar_categoryLivechat');
assert.ok(groupLivechat,
"should have a channel group livechat"
);
const titleText = groupLivechat.querySelector('.o_DiscussSidebarCategory_titleText');
assert.strictEqual(
titleText.textContent.trim(),
"Livechat",
"should have a channel group named 'Livechat'"
);
const livechat = groupLivechat.querySelector(`
.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]
`);
assert.ok(
livechat,
"should have a livechat in sidebar"
);
assert.strictEqual(
livechat.textContent,
"Visitor 11",
"should have 'Visitor 11' as livechat name"
);
});
QUnit.test('livechat in the sidebar: existing user with country', async function (assert) {
assert.expect(3);
const pyEnv = await startServer();
const resCountryId1 = pyEnv['res.country'].create({
code: 'be',
name: "Belgium",
});
const resPartnerId1 = pyEnv['res.partner'].create({
country_id: resCountryId1,
name: "Jean",
});
pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: resPartnerId1 }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { openDiscuss } = await start();
await openDiscuss();
assert.containsOnce(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should have a channel group livechat in the side bar"
);
const livechat = document.querySelector('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem');
assert.ok(
livechat,
"should have a livechat in sidebar"
);
assert.strictEqual(
livechat.textContent,
"Jean (Belgium)",
"should have user name and country as livechat name"
);
});
QUnit.test('do not add livechat in the sidebar on visitor opening his chat', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' });
const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({
user_ids: [pyEnv.currentUserId],
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should not have any livechat in the sidebar initially"
);
// simulate livechat visitor opening his chat
await messaging.rpc({
route: '/im_livechat/get_session',
params: {
context: {
mockedUserId: false,
},
channel_id: imLivechatChannelId1,
},
});
await nextAnimationFrame();
assert.containsNone(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should still not have any livechat in the sidebar after visitor opened his chat"
);
});
QUnit.test('do not add livechat in the sidebar on visitor typing', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' });
const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({
user_ids: [pyEnv.currentUserId],
});
const mailChannelId1 = pyEnv['mail.channel'].create({
channel_member_ids: [
[0, 0, {
is_pinned: false,
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_channel_id: imLivechatChannelId1,
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should not have any livechat in the sidebar initially"
);
// simulate livechat visitor typing
const channel = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0];
await messaging.rpc({
route: '/im_livechat/notify_typing',
params: {
context: {
mockedPartnerId: pyEnv.publicPartnerId,
},
is_typing: true,
uuid: channel.uuid,
},
});
await nextAnimationFrame();
assert.containsNone(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should still not have any livechat in the sidebar after visitor started typing"
);
});
QUnit.test('add livechat in the sidebar on visitor sending first message', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
pyEnv['res.users'].write([pyEnv.currentUserId], { im_status: 'online' });
const resCountryId1 = pyEnv['res.country'].create({
code: 'be',
name: "Belgium",
});
const imLivechatChannelId1 = pyEnv['im_livechat.channel'].create({
user_ids: [pyEnv.currentUserId],
});
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor (Belgium)",
channel_member_ids: [
[0, 0, {
is_pinned: false,
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
country_id: resCountryId1,
livechat_channel_id: imLivechatChannelId1,
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start();
await openDiscuss();
assert.containsNone(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should not have any livechat in the sidebar initially"
);
// simulate livechat visitor sending a message
const channel = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0];
await afterNextRender(async () => messaging.rpc({
route: '/mail/chat_post',
params: {
context: {
mockedUserId: false,
},
uuid: channel.uuid,
message_content: "new message",
},
}));
assert.containsOnce(
document.body,
'.o_DiscussSidebar_categoryLivechat',
"should have a channel group livechat in the side bar after receiving first message"
);
assert.containsOnce(
document.body,
'.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem',
"should have a livechat in the sidebar after receiving first message"
);
assert.strictEqual(
document.querySelector('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategoryItem .o_DiscussSidebarCategoryItem_name').textContent,
"Visitor (Belgium)",
"should have visitor name and country as livechat name"
);
});
QUnit.test('livechats are sorted by last activity time in the sidebar: most recent at the top', async function (assert) {
assert.expect(6);
const pyEnv = await startServer();
const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([
{
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, {
last_interest_dt: datetime_to_str(new Date(2021, 0, 1)),
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
},
{
anonymous_name: "Visitor 12",
channel_member_ids: [
[0, 0, {
last_interest_dt: datetime_to_str(new Date(2021, 0, 2)),
partner_id: pyEnv.currentPartnerId,
}],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
},
]);
const { openDiscuss } = await start();
await openDiscuss();
const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_item');
assert.strictEqual(
initialLivechats.length,
2,
"should have 2 livechat items"
);
assert.strictEqual(
Number(initialLivechats[0].dataset.channelId),
mailChannelId2,
"first livechat should be the one with the more recent last activity time"
);
assert.strictEqual(
Number(initialLivechats[1].dataset.channelId),
mailChannelId1,
"second livechat should be the one with the less recent last activity time"
);
// post a new message on the last channel
await afterNextRender(() => initialLivechats[1].click());
await afterNextRender(() => document.execCommand('insertText', false, "Blabla"));
await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
const newLivechats = document.querySelectorAll('.o_DiscussSidebar_categoryLivechat .o_DiscussSidebarCategory_item');
assert.strictEqual(
newLivechats.length,
2,
"should have 2 livechat items"
);
assert.strictEqual(
Number(newLivechats[0].dataset.channelId),
mailChannelId1,
"first livechat should be the one with the more recent last activity time"
);
assert.strictEqual(
Number(newLivechats[1].dataset.channelId),
mailChannelId2,
"second livechat should be the one with the less recent last activity time"
);
});
QUnit.test('invite button should be present on livechat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create(
{
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
},
);
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_ThreadViewTopbar_inviteButton',
"Invite button should be visible in top bar when livechat is active thread"
);
});
QUnit.test('call buttons should not be present on livechat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create(
{
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
},
);
const { openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
assert.containsNone(
document.body,
'.o_ThreadViewTopbar_callButton',
"Call buttons should not be visible in top bar when livechat is active thread"
);
});
QUnit.test('reaction button should not be present on livechat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
channel_partner_ids: [pyEnv.currentPartnerId, pyEnv.publicPartnerId],
});
const { click, insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "Test");
await click('.o_Composer_buttonSend');
await click('.o_Message');
assert.containsNone(
document.body,
'.o_MessageActionView_actionReaction',
"should not have action to add a reaction"
);
});
QUnit.test('reply button should not be present on livechat', async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
channel_partner_ids: [pyEnv.currentPartnerId, pyEnv.publicPartnerId],
});
const { click, insertText, openDiscuss } = await start({
discuss: {
params: {
default_active_id: `mail.channel_${mailChannelId1}`,
},
},
});
await openDiscuss();
await insertText('.o_ComposerTextInput_textarea', "Test");
await click('.o_Composer_buttonSend');
await click('.o_Message');
assert.containsNone(
document.body,
'.o_MessageActionView_actionReplyTo',
"should not have reply action"
);
});
});
});

View file

@ -1,71 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('messaging_menu_tests.js');
QUnit.test('livechats should be in "chat" filter', async function (assert) {
assert.expect(7);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 11",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
await start();
assert.containsOnce(
document.body,
'.o_MessagingMenu',
"should have messaging menu"
);
await afterNextRender(() => document.querySelector('.o_MessagingMenu_toggler').click());
assert.containsOnce(
document.body,
'.o_MessagingMenuTab[data-tab-id="all"]',
"should have a tab/filter 'all' in messaging menu"
);
assert.containsOnce(
document.body,
'.o_MessagingMenuTab[data-tab-id="chat"]',
"should have a tab/filter 'chat' in messaging menu"
);
assert.hasClass(
document.querySelector('.o_MessagingMenuTab[data-tab-id="all"]'),
'o-active',
"tab/filter 'all' of messaging menu should be active initially"
);
assert.containsOnce(
document.body,
`.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`,
"livechat should be listed in 'all' tab/filter of messaging menu"
);
await afterNextRender(() =>
document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]').click()
);
assert.hasClass(
document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]'),
'o-active',
"tab/filter 'chat' of messaging menu should become active after click"
);
assert.containsOnce(
document.body,
`.o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`,
"livechat should be listed in 'chat' tab/filter of messaging menu"
);
});
});
});

View file

@ -1,68 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('thread_icon_tests.js');
QUnit.test('livechat: public website visitor is typing', async function (assert) {
assert.expect(4);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 20",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
});
await openDiscuss();
assert.containsOnce(
document.body,
'.o_ThreadViewTopbar .o_ThreadIcon',
"should have thread icon"
);
assert.containsOnce(
document.body,
'.o_ThreadIcon .fa.fa-comments',
"should have default livechat icon"
);
const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0];
// simulate receive typing notification from livechat visitor "is typing"
await afterNextRender(() => messaging.rpc({
route: '/im_livechat/notify_typing',
params: {
context: {
mockedPartnerId: pyEnv.publicPartnerId,
},
is_typing: true,
uuid: mailChannel1.uuid,
},
}));
assert.containsOnce(
document.body,
'.o_ThreadIcon_typing',
"should have thread icon with visitor currently typing"
);
assert.strictEqual(
document.querySelector('.o_ThreadIcon_typing').title,
"Visitor 20 is typing...",
"title of icon should tell visitor is currently typing"
);
});
});
});

View file

@ -1,59 +0,0 @@
/** @odoo-module **/
import {
afterNextRender,
start,
startServer,
} from '@mail/../tests/helpers/test_utils';
QUnit.module('im_livechat', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('thread_textual_typing_status_tests.js');
QUnit.test('receive visitor typing status "is typing"', async function (assert) {
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create({
anonymous_name: "Visitor 20",
channel_member_ids: [
[0, 0, { partner_id: pyEnv.currentPartnerId }],
[0, 0, { partner_id: pyEnv.publicPartnerId }],
],
channel_type: 'livechat',
livechat_operator_id: pyEnv.currentPartnerId,
});
const { messaging, openDiscuss } = await start({
discuss: {
context: { active_id: mailChannelId1 },
},
});
await openDiscuss();
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"",
"Should display no one is currently typing"
);
const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0];
// simulate receive typing notification from livechat visitor "is typing"
await afterNextRender(() => messaging.rpc({
route: '/im_livechat/notify_typing',
params: {
context: {
mockedPartnerId: pyEnv.publicPartnerId,
},
is_typing: true,
uuid: mailChannel1.uuid,
},
}));
assert.strictEqual(
document.querySelector('.o_ThreadTextualTypingStatus').textContent,
"Visitor 20 is typing...",
"Should display that visitor is typing"
);
});
});
});

View file

@ -0,0 +1,469 @@
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { mockDate, tick } from "@odoo/hoot-mock";
import { asyncStep, Command, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
import { deserializeDateTime } from "@web/core/l10n/dates";
import { rpc } from "@web/core/network/rpc";
import { url } from "@web/core/utils/urls";
import { defineLivechatModels } from "./livechat_test_helpers";
import { press } from "@odoo/hoot-dom";
import { browser } from "@web/core/browser/browser";
describe.current.tags("desktop");
defineLivechatModels();
test("Unknown visitor", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebar .o-mail-DiscussSidebarCategory-livechat");
await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" });
});
test("Do not show channel when visitor is typing", 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();
pyEnv["res.users"].write([serverState.userId], { im_status: "online" });
const livechatChannelId = pyEnv["im_livechat.channel"].create({
user_ids: [serverState.userId],
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
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",
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory", { count: 2 });
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container",
{
count: 0,
}
);
// simulate livechat visitor typing
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
await withGuest(guestId, () =>
rpc("/discuss/channel/notify_typing", {
is_typing: true,
channel_id: channel.id,
})
);
// weak test, no guaranteed that we waited long enough for the livechat to potentially appear
await tick();
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container",
{
count: 0,
}
);
});
test("Smiley face avatar for livechat item linked to a guest", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
const guest = pyEnv["mail.guest"].search_read([["id", "=", guestId]])[0];
await contains(
`.o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container img[data-src='${url(
`/web/image/mail.guest/${guestId}/avatar_128?unique=${
deserializeDateTime(guest.write_date).ts
}`
)}']`
);
});
test("Partner profile picture for livechat item linked to a partner", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jean" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ partner_id: partnerId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
const partner = pyEnv["res.partner"].search_read([["id", "=", partnerId]])[0];
await contains(
`.o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container img[data-src='${url(
`/web/image/res.partner/${partnerId}/avatar_128?unique=${
deserializeDateTime(partner.write_date).ts
}`
)}']`
);
});
test("No counter if the category is unfolded and with unread messages", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
message_unread_counter: 10,
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory-livechat");
await contains(".o-mail-DiscussSidebarCategory-livechat .o-mail-Discuss-category-counter", {
count: 0,
});
});
test("No counter if category is folded and without unread messages", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarCategory-livechat");
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(".o-mail-DiscussSidebarCategory-livechat .o-discuss-badge", { count: 0 });
});
test("Counter should have correct value of unread threads if category is folded and with unread messages", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
livechat_member_type: "agent",
partner_id: serverState.partnerId,
}),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
pyEnv["mail.message"].create({
author_guest_id: guestId,
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss();
// first, close the live chat category
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(".o-mail-DiscussSidebarCategory-livechat .o-discuss-badge", { text: "1" });
});
test("Close manually by clicking the title", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container"
);
// fold the livechat category
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
});
test("Open manually by clicking the title", async () => {
mockDate("2023-01-03 12:00:00");
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
// first, close the live chat category
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(".o-mail-DiscussSidebarCategory-livechat");
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container",
{
count: 0,
}
);
// open the livechat category
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container"
);
});
test("Category item should be invisible if the category is closed", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container"
);
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(
".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel-container",
{
count: 0,
}
);
});
test("Active category item should be visible even if the category is closed", async () => {
mockDate("2023-01-03 12:00:00");
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" });
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Visitor 11" });
await click(".o-mail-DiscussSidebarCategory-livechat .btn");
await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" });
});
test("Clicking on leave button leaves the channel", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Visitor 11" }),
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel", { text: "Visitor 11" });
await click("[title='Chat Actions']");
await click(".o-dropdown-item:contains('Leave Channel')");
await click("button:contains(Leave Conversation)");
await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Visitor 11" });
});
test("Message unread counter", async () => {
mockDate("2023-01-03 12:00:00");
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 11" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-03 10:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: guestId,
last_interest_dt: "2021-01-03 10:00:00",
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
withGuest(guestId, () =>
rpc("/mail/message/post", {
post_data: {
body: "hu",
message_type: "comment",
subtype_xmlid: "mail.mt_comment",
},
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-DiscussSidebarChannel .badge", { text: "1" });
});
test("unknown livechat can be displayed and interacted with", async () => {
mockDate("2023-01-03 12:00:00");
const pyEnv = await startServer();
pyEnv["res.users"].write(serverState.userId, { notification_type: "inbox" });
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({
partner_id: partnerId,
last_interest_dt: "2021-01-01 10:00:00",
livechat_member_type: "agent",
}),
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Jane" }),
livechat_member_type: "visitor",
}),
],
channel_type: "livechat",
livechat_operator_id: partnerId,
create_uid: serverState.publicUserId,
});
const env = await start();
env.services.bus_service.subscribe("discuss.channel/new_message", () =>
asyncStep("discuss.channel/new_message")
);
await openDiscuss("mail.box_inbox");
await contains("button.o-active", { text: "Inbox" });
await contains(".o-mail-DiscussSidebarCategory-livechat", { count: 0 });
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
await openDiscuss(channelId);
await waitForChannels([`discuss.channel_${channelId}`]);
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Jane" });
await insertText(".o-mail-Composer-input", "Hello", { replace: true });
await press("Enter");
await contains(".o-mail-Message", { text: "Hello" });
await waitForSteps(["discuss.channel/new_message"]);
await click("button", { text: "Inbox" });
await contains(".o-mail-DiscussSidebarChannel:not(.o-active)", { text: "Jane" });
await click("[title='Chat Actions']");
await click(".o-dropdown-item:contains('Leave Channel')");
await contains(".o-mail-DiscussSidebarCategory-livechat", { count: 0 });
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
});
test("Local sidebar category state is shared between tabs", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Visitor #12" }),
livechat_member_type: "visitor",
}),
],
livechat_operator_id: serverState.user,
});
const env1 = await start({ asTab: true });
const env2 = await start({ asTab: true });
await openDiscuss(undefined, { target: env1 });
await openDiscuss(undefined, { target: env2 });
await contains(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-down`);
await contains(`${env2.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-down`);
await click(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .btn`);
await contains(`${env1.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-right`);
await contains(`${env2.selector} .o-mail-DiscussSidebarCategory-livechat .oi-chevron-right`);
});
test("live chat is displayed below its category", async () => {
const pyEnv = await startServer();
const livechatChannelId = pyEnv["im_livechat.channel"].create({ name: "Helpdesk" });
browser.localStorage.setItem(
`discuss_sidebar_category_im_livechat.category_${livechatChannelId}_open`,
false
);
pyEnv["discuss.channel"].create({
channel_type: "livechat",
livechat_channel_id: livechatChannelId,
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Visitor #12" }),
livechat_member_type: "visitor",
}),
],
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarCategory .btn", { text: "Helpdesk" });
await contains(
".o-mail-DiscussSidebarCategory:contains(Helpdesk) + .o-mail-DiscussSidebarChannel-container:contains(Visitor #12)"
);
});

View file

@ -0,0 +1,91 @@
import { describe, test } from "@odoo/hoot";
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("Suggestions are shown after delimiter was used in text (::)", async () => {
const pyEnv = await startServer();
pyEnv["mail.canned.response"].create({
source: "hello",
substitution: "Hello dear customer, how may I help you?",
});
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
partner_id: serverState.publicPartnerId,
livechat_member_type: "visitor",
}),
],
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "::");
await contains(".o-mail-Composer-suggestion strong", { text: "hello" });
await insertText(".o-mail-Composer-input", ")");
await contains(".o-mail-Composer-suggestion strong", { count: 0 });
await insertText(".o-mail-Composer-input", " ::");
await contains(".o-mail-Composer-suggestion strong", { text: "hello" });
});
test("Cannot mention other channels in a livechat", async () => {
const pyEnv = await startServer();
const [channelId] = pyEnv["discuss.channel"].create([
{
channel_type: "livechat",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
livechat_member_type: "agent",
}),
Command.create({
partner_id: serverState.publicPartnerId,
livechat_member_type: "visitor",
}),
],
},
{
channel_type: "channel",
group_public_id: false,
name: "Link and Zelda",
},
]);
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "#");
await contains(".o-mail-Composer-suggestion", { count: 0 });
});
test("Internal user mention shows their live chat username", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].write([serverState.partnerId], { user_livechat_username: "Batman" });
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
partner_id: serverState.publicPartnerId,
livechat_member_type: "visitor",
}),
],
});
pyEnv["res.users"]._applyComputesAndValidate();
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await click('.o-mail-Composer-suggestion:contains(Mitchell Admin "Batman")');
await contains(".o-mail-Composer-input:value(@Batman)");
await click(".o-mail-Composer button[title='Send']:enabled");
await contains(".o-mail-Message a.o_mail_redirect", { text: "@Batman" });
});

View file

@ -0,0 +1,38 @@
import { contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
import { withGuest } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("Public website visitor is typing", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor 20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussContent-header .o-mail-ThreadIcon .fa.fa-circle-o");
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
// simulate receive typing notification from livechat visitor "is typing"
withGuest(guestId, () =>
rpc("/discuss/channel/notify_typing", {
is_typing: true,
channel_id: channel.id,
})
);
await contains(".o-mail-DiscussContent-header .o-discuss-Typing-icon");
await contains(
".o-mail-DiscussContent-header .o-discuss-Typing-icon[title='Visitor 20 is typing...']"
);
});

View file

@ -0,0 +1,117 @@
import {
click,
contains,
insertText,
openDiscuss,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
import { rpc } from "@web/core/network/rpc";
import { press } from "@odoo/hoot-dom";
describe.current.tags("desktop");
defineLivechatModels();
test("Thread name unchanged when inviting new users", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']");
await click("button[title='Invite People']");
await click("input", {
parent: [".o-discuss-ChannelInvitation-selectable", { text: "James" }],
});
await click("button:enabled", { text: "Invite" });
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await click("button[title='Members']");
await contains(".o-discuss-ChannelMember", { text: "James" });
await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']");
});
test("Can set a custom name to livechat conversation", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await click(".o-mail-DiscussSidebar-item:contains('Visitor #20')");
await contains(".o-mail-DiscussContent-threadName[title='Visitor #20']");
await insertText(".o-mail-DiscussContent-threadName", "New Name", { replace: true });
await triggerHotkey("Enter");
await contains(".o-mail-DiscussContent-threadName[title='New Name']");
await contains(".o-mail-DiscussSidebar-item:contains('New Name')");
});
test("Display livechat custom username if defined", async () => {
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, {
user_livechat_username: "livechat custom username",
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor #20" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "hello");
await press("Enter");
await contains(".o-mail-Message-author", { text: "livechat custom username" });
});
test("Display livechat custom name in typing status", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "James" });
const partnerId = pyEnv["res.partner"].create({
name: "James",
user_ids: [userId],
user_livechat_username: "livechat custom username",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: partnerId, livechat_member_type: "agent" }),
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_operator_id: partnerId,
});
await start();
await openDiscuss(channelId);
await withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "livechat custom username is typing..." });
});

View file

@ -0,0 +1,40 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("change_chatbot_step_type", {
steps: () => [
{
content: "Open an existing script",
trigger: ".o_field_cell[data-tooltip='Clear Answer Test Bot']",
run: "click",
},
{
content: "Open first step",
trigger: '.o_row_draggable .o_field_cell:contains("Question")',
run: "click",
},
{
content: "Change step type to 'text'",
trigger: 'div[name="step_type"] input',
run: "click",
},
{
trigger: '.dropdown-item:contains("Text")',
run: "click",
},
{
content: "Verify answers cleared",
trigger: ".btn-primary:contains('Save')",
run: "click",
},
{
trigger: ".o_form_button_save",
run: "click",
},
{
// Ensure form is properly saved, in which case the save button is hidden.
trigger: ".o_form_button_save:not(:visible)",
},
],
});

View file

@ -1,32 +1,32 @@
/** @odoo-module */
import tour from "web_tour.tour";
import { registry } from "@web/core/registry";
const requestChatSteps = [
{
trigger: ".o_livechat_button",
trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton",
run: "click",
},
{
trigger: ".o_thread_window",
trigger: ".o-livechat-root:shadow .o-mail-ChatWindow",
},
];
tour.register("im_livechat_request_chat", { test: true }, requestChatSteps);
registry.category("web_tour.tours").add("im_livechat_request_chat", {
steps: () => requestChatSteps,
});
tour.register("im_livechat_request_chat_and_send_message", { test: true }, [
...requestChatSteps,
{
trigger: ".o_composer_text_field",
run: "text Hello, I need help please !",
},
{
trigger: '.o_composer_text_field',
run() {
$(".o_composer_text_field").trigger($.Event("keydown", { which: 13 }));
registry.category("web_tour.tours").add("im_livechat_request_chat_and_send_message", {
steps: () => [
...requestChatSteps,
{
trigger: ".o-livechat-root:shadow .o-mail-Composer-input",
run: "edit Hello, I need help please !",
},
},
{
trigger: ".o_thread_message:contains('Hello, I need help')",
},
]);
{
trigger: ".o-livechat-root:shadow .o-mail-Composer-input",
run: "press Enter",
},
{
trigger: ".o-livechat-root:shadow .o-mail-Message:contains('Hello, I need help')",
},
],
});

View file

@ -1,130 +1,99 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from "web_tour.tour";
const commonSteps = [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="im_livechat.menu_livechat_root"]',
}, {
trigger: 'button[data-menu-xmlid="im_livechat.livechat_config"]',
}, {
trigger: 'a[data-menu-xmlid="im_livechat.chatbot_config"]',
}, {
trigger: '.o_list_button_add',
}, {
trigger: 'input[id="title"]',
run: 'text Test Chatbot Sequence'
}, {
trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a'
}, {
trigger: 'textarea#message',
run: 'text Step 1'
}, {
trigger: 'button:contains("Save & New")'
}, {
trigger: 'tr:contains("Step 1")',
in_modal: false,
run: () => {}
}, {
trigger: 'textarea#message',
run: 'text Step 2'
}, {
trigger: 'button:contains("Save & New")'
}, {
trigger: 'tr:contains("Step 2")',
in_modal: false,
run: () => {}
}, {
trigger: 'textarea#message',
run: 'text Step 3'
}];
function createChatbotSteps(...stepMessages) {
return [
{
trigger: "div[name='script_step_ids'] .o_field_x2many_list_row_add a",
run: "click",
},
...stepMessages
.map((message) => [
{
trigger: ".modal .odoo-editor-editable",
run: `editor ${message}`,
},
{
trigger: `.modal .odoo-editor-editable:contains(${message})`,
},
{
trigger: ".modal button:contains(Save & New)",
run: "click",
},
{
trigger: `tr:contains(${message})`,
},
{
trigger: ".modal .odoo-editor-editable:empty",
},
])
.flat(),
{
trigger: ".modal-footer button:contains(Discard)",
run: "click",
},
];
}
const commonSteps = [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="im_livechat.menu_livechat_root"]',
run: "click",
},
{
trigger: 'button[data-menu-xmlid="im_livechat.livechat_config"]',
run: "click",
},
{
trigger: 'a[data-menu-xmlid="im_livechat.chatbot_config"]',
run: "click",
},
{
trigger: ".o_list_button_add",
run: "click",
},
{
trigger: 'input[id="title_0"]',
run: "edit Test Chatbot Sequence",
},
...createChatbotSteps("Step 1", "Step 2", "Step 3"),
];
/**
* Simply create a few steps in order to check the sequences.
*/
tour.register('im_livechat_chatbot_steps_sequence_tour', {
test: true,
url: '/web',
}, [
...commonSteps, {
trigger: 'button:contains("Save & Close")'
}, {
trigger: 'body.o_web_client:not(.modal-open)',
run() {},
}, ...tour.stepUtils.discardForm()
]);
registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_tour", {
url: "/odoo",
steps: () => [
...commonSteps,
{
trigger: "body.o_web_client:not(.modal-open)",
},
],
});
/**
* Same as above, with an extra drag&drop at the end.
*/
tour.register('im_livechat_chatbot_steps_sequence_with_move_tour', {
test: true,
url: '/web',
}, [
...commonSteps, {
trigger: 'button:contains("Save & New")'
}, {
trigger: 'tr:contains("Step 3")',
in_modal: false,
run: () => {}
}, {
trigger: 'textarea#message',
run: 'text Step 4'
}, {
trigger: 'button:contains("Save & New")'
}, {
trigger: 'tr:contains("Step 4")',
in_modal: false,
run: () => {}
}, {
trigger: 'textarea#message',
run: 'text Step 5'
}, {
trigger: 'button:contains("Save & Close")'
}, {
trigger: 'body.o_web_client:not(.modal-open)',
run: () => {}
}, {
trigger: 'tr:contains("Step 5") .o_row_handle',
run: () => {
// move 'step 5' between 'step 1' and 'step 2'
const from = document.querySelector('div[name="script_step_ids"] tr:nth-child(5) .o_row_handle');
const fromPosition = from.getBoundingClientRect();
fromPosition.x += from.offsetWidth / 2;
fromPosition.y += from.offsetHeight / 2;
const to = document.querySelector('div[name="script_step_ids"] tr:nth-child(2) .o_row_handle');
from.dispatchEvent(new Event("mouseenter", { bubbles: true }));
from.dispatchEvent(new MouseEvent("mousedown", {
bubbles: true,
which: 1,
button: 0,
clientX: fromPosition.x,
clientY: fromPosition.y}));
from.dispatchEvent(new MouseEvent("mousemove", {
bubbles: true,
which: 1,
button: 0,
// dragging is only enabled when the mouse have moved from at least 10 pixels from the original position
clientX: fromPosition.x + 20,
clientY: fromPosition.y + 20,
}));
to.dispatchEvent(new Event("mouseenter", { bubbles: true }));
from.dispatchEvent(new Event("mouseup", { bubbles: true }));
}
}, {
trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a'
}, {
trigger: 'textarea#message',
run: 'text Step 6'
}, {
trigger: 'button:contains("Save & Close")'
}, {
trigger: 'body.o_web_client:not(.modal-open)',
run: () => {}
}, {
trigger: 'tr:contains("Step 6")',
in_modal: false,
run: () => {}
}, ...tour.stepUtils.discardForm(),
]);
registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with_move_tour", {
url: "/odoo",
steps: () => [
...commonSteps,
...createChatbotSteps("Step 4", "Step 5"),
{
trigger: "body.o_web_client:not(.modal-open)",
},
{
trigger: 'div[name="script_step_ids"] tr:nth-child(5) .o_row_handle',
run: 'drag_and_drop(div[name="script_step_ids"] tr:nth-child(2))',
},
...createChatbotSteps("Step 6"),
{
trigger: "body.o_web_client:not(.modal-open)",
},
{
trigger: 'tr:contains("Step 6")',
},
],
});

View file

@ -0,0 +1,39 @@
import { delay } from "@web/core/utils/concurrency";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("im_livechat_history_back_and_forth_tour", {
steps: () => [
{
trigger: "button.o_switch_view.o_list",
run: "click",
},
{
trigger: ".o_data_cell:contains(Visitor)",
run: "click",
},
{
trigger: ".o-mail-DiscussContent-threadName[title='Visitor']",
async run() {
await delay(1000);
history.back();
},
},
{
trigger: ".o_data_cell:contains(Visitor)",
async run() {
await delay(0);
history.forward();
},
},
{
trigger: ".o-mail-DiscussContent-threadName[title='Visitor']",
async run() {
await delay(1000);
history.back();
},
},
{
trigger: ".o_data_cell:contains(Visitor)",
},
],
});

View file

@ -0,0 +1,56 @@
import { registry } from "@web/core/registry";
/**
* @param {"list" | "kanban"} viewType
* @returns {import("@web_tour/tour_service/tour_service").TourStep[]}
*/
function getSteps(viewType) {
let bobChatId;
return [
{
trigger: ".o_control_panel .active:contains(Looking for Help)",
},
{
trigger:
viewType === "list"
? ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))"
: ".o_kanban_renderer:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help])",
async run() {
const { orm } = odoo.__WOWL_DEBUG__.root.env.services;
[bobChatId] = await orm.search("discuss.channel", [
["livechat_status", "=", "need_help"],
["livechat_agent_partner_ids.name", "like", "bob_looking_for_help%"],
]);
await orm.write("discuss.channel", [bobChatId], {
livechat_status: "in_progress",
});
},
},
{
trigger:
viewType === "list"
? ".o_list_table:not(:has(.o_data_row:contains(bob_looking_for_help)))"
: ".o_kanban_renderer:not(:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help]))",
async run() {
const { orm } = odoo.__WOWL_DEBUG__.root.env.services;
await orm.write("discuss.channel", [bobChatId], {
livechat_status: "need_help",
});
},
},
{
trigger:
viewType === "list"
? ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))"
: ".o_kanban_renderer:has(.o_kanban_record [name=livechat_agent_partner_ids] [aria-label^=bob_looking_for_help])",
},
];
}
registry.category("web_tour.tours").add("im_livechat.looking_for_help_list_real_time_update_tour", {
steps: () => getSteps("list"),
});
registry
.category("web_tour.tours")
.add("im_livechat.looking_for_help_kanban_real_time_update_tour", {
steps: () => getSteps("kanban"),
});

View file

@ -0,0 +1,57 @@
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
let bobChatId;
let tagId;
registry.category("web_tour.tours").add("im_livechat.looking_for_help_tags_real_time_update_tour", {
steps: () => [
{
trigger: ".o_control_panel .active:contains(Looking for Help)",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: '.o-dropdown-item input[name="livechat_conversation_tag_ids"]',
run: "click",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: ".o_list_table:has(.o_data_row:contains(bob_looking_for_help))",
},
{
trigger: '.o_data_cell[name="livechat_conversation_tag_ids"]:not(:has(.o_tag))',
async run() {
const { orm } = odoo.__WOWL_DEBUG__.root.env.services;
[bobChatId] = await orm.search("discuss.channel", [
["livechat_status", "=", "need_help"],
["livechat_agent_partner_ids.name", "like", "bob_looking_for_help%"],
]);
[tagId] = await orm.create("im_livechat.conversation.tag", [{ name: "Discuss" }]);
// Simulate other user adding a tag
await rpc("/im_livechat/conversation/update_tags", {
channel_id: bobChatId,
tag_ids: [tagId],
method: "ADD",
});
},
},
{
trigger:
'.o_data_cell[name="livechat_conversation_tag_ids"]:has(.o_tag:contains(Discuss))',
async run() {
// Simulate other user removing a tag
await rpc("/im_livechat/conversation/update_tags", {
channel_id: bobChatId,
tag_ids: [tagId],
method: "DELETE",
});
},
},
{ trigger: '.o_data_cell[name="livechat_conversation_tag_ids"]:not(:has(.o_tag))' },
],
});

View file

@ -0,0 +1,37 @@
import { registry } from "@web/core/registry";
function makePivotRedirectTourSteps(singleRecordName, multiRecordName) {
return [
{
content: "Click on a cell with a single related record",
trigger: `.o_pivot table tbody tr:has(th:contains(${singleRecordName})) td:eq(0)`,
run: "click",
},
{
trigger: ".o-mail-Discuss",
content: "Verify redirection to the single record view",
},
{
content: "Go back to the pivot view",
trigger: ".o_back_button",
run: "click",
},
{
content: "Click on a cell with a multiple related records",
trigger: `.o_pivot table tbody tr:has(th:contains(${multiRecordName})) td:eq(0)`,
run: "click",
},
{
trigger: ".o_list_view",
content: "Verify redirection to the list view for multiple records",
},
];
}
registry.category("web_tour.tours").add("im_livechat_agents_report_pivot_redirect_tour", {
steps: () => makePivotRedirectTourSteps("test 1", "test 2"),
});
registry.category("web_tour.tours").add("im_livechat_sessions_report_pivot_redirect_tour", {
steps: () => makePivotRedirectTourSteps("operator_1", "operator_2"),
});

View file

@ -0,0 +1,62 @@
import { whenReady } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
let firstChannelId;
registry.category("web_tour.tours").add("im_livechat_session_history_open", {
steps: () => [
{
trigger: "body",
async run() {
await whenReady();
const busService = odoo.__WOWL_DEBUG__.root.env.services.bus_service;
patchWithCleanup(busService, {
addChannel(channel) {
document.body.classList.add(`o-bus-channel-${channel}`);
return super.addChannel(...arguments);
},
deleteChannel(channel) {
document.body.classList.remove(`o-bus-channel-${channel}`);
return super.deleteChannel(...arguments);
},
});
},
},
{
trigger: ".o_switch_view[data-tooltip='List']",
run: "click",
},
{
trigger: ".o_data_cell:contains('test 2')",
run: "click",
},
{
trigger: ".o-mail-Message-content:contains('Test Channel 2 Msg')",
async run({ waitFor }) {
firstChannelId =
odoo.__WOWL_DEBUG__.root.env.services.action.currentController.state.resId;
await waitFor(`body.o-bus-channel-discuss\\.channel_${firstChannelId}`, {
timeout: 3000,
});
},
},
{
trigger: ".oi-chevron-right",
run: "click",
},
{
trigger: ".o-mail-Message-content:contains('Test Channel 1 Msg')",
async run({ waitFor }) {
await waitFor(`body:not(.o-bus-channel-discuss\\.channel_${firstChannelId})`, {
timeout: 3000,
});
const channelId =
odoo.__WOWL_DEBUG__.root.env.services.action.currentController.state.resId;
await waitFor(`body.o-bus-channel-discuss\\.channel_${channelId}`, {
trimeout: 3000,
});
},
},
],
});

View file

@ -0,0 +1,38 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("im_livechat.looking_for_help_discuss_category_tour", {
steps: () => [
{
// Two live chats are looking for help, they are both in the "Looking for help" category.
trigger:
".o-mail-DiscussSidebarCategory-livechatNeedHelp + .o-mail-DiscussSidebarChannel-container:contains(Visitor Accounting) + .o-mail-DiscussSidebarChannel-container:contains(Visitor Sales)",
},
{
trigger: ".o-mail-DiscussSidebarChannel:contains(Sales) .o-mail-starred",
},
{
trigger:
".o-mail-DiscussSidebarChannel:contains(Accounting):not(:has(.o-mail-starred))",
},
{
trigger: ".o-mail-DiscussSidebarChannel:contains(Accounting)",
run: "hover && click [title='Chat Actions']",
},
{
trigger:
".o-mail-DiscussSidebar:has(.o-mail-DiscussSidebarChannel:contains(Accounting))",
},
{
trigger: "button[name='livechat-status']",
run: "hover",
},
{
trigger: ".o-livechat-LivechatStatusSelection-Label:contains(In progress)",
run: "click",
},
{
trigger:
".o-mail-DiscussSidebar:not(:has(.o-mail-DiscussSidebarChannel:contains(Accounting)))",
},
],
});

View file

@ -0,0 +1,16 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("im_livechat.basic_tour", {
steps: () => [
{
trigger: ".channel_name:contains(Support Channel)",
},
{
trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton",
run: "click",
},
{
trigger: ".o-livechat-root:shadow .o-mail-ChatWindow",
},
],
});

View file

@ -0,0 +1,44 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("im_livechat.meeting_view_tour", {
steps: () => [
{
trigger: ".o-livechat-root:shadow .o-livechat-LivechatButton",
run: "click",
},
{
trigger: ".o-livechat-root:shadow .o-mail-Thread[data-transient]",
},
{
trigger: ".o-livechat-root:shadow .o-mail-Composer-input",
run: "edit Hello!",
},
{
trigger: ".o-livechat-root:shadow .o-mail-Composer-input",
run: "press Enter",
},
{
trigger: ".o-livechat-root:shadow [title='Join Call']",
run: "click",
},
{
trigger: ".o-livechat-root:shadow .o-discuss-Call [title='Fullscreen']",
run: "click",
},
{
trigger: ".o-livechat-root:shadow .o-mail-Meeting",
},
{
trigger: ".o-livechat-root:shadow .o-mail-MeetingSideActions [name^='more-action:'] ",
run: "click",
},
{
trigger: ".o-livechat-root:shadow [name='call-settings']",
run: "click",
},
{
trigger:
".o-livechat-root:shadow .o-mail-DiscussContent-panelContainer .o-mail-ActionPanel-header:contains('voice settings')",
},
],
});

View file

@ -0,0 +1,31 @@
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import { contains, focus, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { serializeDate, today } from "@web/core/l10n/dates";
describe.current.tags("desktop");
defineLivechatModels();
test("agent can send conversation after livechat ends", async () => {
const pyEnv = await startServer();
const demoPartnerId = pyEnv["res.partner"].create({
name: "Awesome partner",
email: "awesome@example.com",
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ partner_id: demoPartnerId, livechat_member_type: "visitor" }),
],
channel_type: "livechat",
livechat_end_dt: serializeDate(today()),
livechat_operator_id: serverState.partnerId,
});
await start();
await openDiscuss(channelId);
await focus("input[placeholder='mail@example.com']", { value: "awesome@example.com" });
await press("Enter");
await contains(".form-text", { text: "The conversation was sent." });
});

View file

@ -0,0 +1,54 @@
import { describe, test } from "@odoo/hoot";
import { click, contains, openDiscuss, start, startServer } from "@mail/../tests/mail_test_helpers";
import { Command, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "./livechat_test_helpers";
describe.current.tags("desktop");
defineLivechatModels();
test("message translation in livechat (agent is member)", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Mario" }),
livechat_member_type: "visitor",
}),
],
});
pyEnv["mail.message"].create({
body: "Mai mettere l'ananas sulla pizza!",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await click("[title='Expand']");
await contains(".o-dropdown-item:contains('Translate')");
});
test("message translation in livechat (agent is not member)", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({
guest_id: pyEnv["mail.guest"].create({ name: "Mario" }),
livechat_member_type: "visitor",
}),
],
});
pyEnv["mail.message"].create({
body: "Mai mettere l'ananas sulla pizza!",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await click("[title='Expand']");
await contains(".o-dropdown-item:contains('Translate')");
});

View file

@ -0,0 +1,76 @@
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { defineLivechatModels } from "@im_livechat/../tests/livechat_test_helpers";
import {
contains,
setupChatHub,
start,
startServer,
click,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { Store } from "@mail/core/common/store_service";
import { Thread } from "@mail/core/common/thread";
describe.current.tags("desktop");
defineLivechatModels();
test("Visitor going offline shows disconnection banner to operator", async () => {
patchWithCleanup(Store, { IM_STATUS_DEBOUNCE_DELAY: 0 });
patchWithCleanup(Thread.prototype, {
setup() {
super.setup();
this.IM_STATUS_DELAY = 0;
},
});
const pyEnv = await startServer();
pyEnv["res.users"].write([serverState.userId], {
group_ids: pyEnv["res.groups"]
.search_read([["id", "=", serverState.groupLivechatId]])
.map(({ id }) => id),
});
const guestId = pyEnv["mail.guest"].create({ name: "Visitor", im_status: "online" });
const livechatChannelId = pyEnv["im_livechat.channel"].create({
name: "HR",
user_ids: [serverState.userId],
});
const channel_id = pyEnv["discuss.channel"].create({
channel_type: "livechat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, livechat_member_type: "agent" }),
Command.create({ guest_id: guestId, livechat_member_type: "visitor" }),
],
livechat_channel_id: livechatChannelId,
livechat_operator_id: serverState.partnerId,
create_uid: serverState.publicUserId,
});
setupChatHub({ opened: [channel_id] });
await start();
await contains(".o-mail-ChatWindow");
mockDate("2025-01-01 12:00:00", +1);
pyEnv["mail.guest"].write(guestId, { im_status: "offline" });
pyEnv["bus.bus"]._sendone(guestId, "bus.bus/im_status_updated", {
partner_id: false,
guest_id: guestId,
im_status: "offline",
});
await contains(".o-livechat-VisitorDisconnected", {
text: "Visitor is disconnected since 1:00 PM",
});
mockDate("2025-01-02 12:00:00", +1);
await click("button[title*='Fold']");
await click(".o-mail-ChatBubble");
await contains(".o-livechat-VisitorDisconnected", {
text: "Visitor is disconnected since yesterday at 1:00 PM",
});
mockDate("2025-01-05 12:00:00", +1);
await click("button[title*='Fold']");
await click(".o-mail-ChatBubble");
await contains(".o-livechat-VisitorDisconnected", { text: `Visitor is disconnected` });
pyEnv["bus.bus"]._sendone(guestId, "bus.bus/im_status_updated", {
partner_id: false,
guest_id: guestId,
im_status: "online",
});
await contains(".o-livechat-VisitorDisconnected", { count: 0 });
});