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,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 });
});