19.0 vanilla

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
import {
click,
contains,
start,
startServer,
openDiscuss,
mockGetMedia,
onlineTest,
defineMailModels,
} from "@mail/../tests/mail_test_helpers";
import { onRpc } from "@web/../tests/web_test_helpers";
import { PeerToPeer, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
defineMailModels();
function connectionReady(p2p) {
return new Promise((resolve) => {
p2p.addEventListener("update", ({ detail }) => {
if (
detail.name === UPDATE_EVENT.CONNECTION_CHANGE &&
detail.payload.state === "connected"
) {
resolve();
}
});
});
}
async function mockPeerToPeerCallEnvironment({ channelId, remoteSessionId }) {
const env = await start();
const rtc = env.services["discuss.rtc"];
const localUserP2P = env.services["discuss.p2p"];
const remoteUserP2P = new PeerToPeer({
notificationRoute: "/mail/rtc/session/notify_call_members",
});
remoteUserP2P.connect(remoteSessionId, channelId);
onRpc("/mail/rtc/session/notify_call_members", async (req) => {
const {
params: { peer_notifications },
} = await req.json();
for (const [sender, , message] of peer_notifications) {
/**
* This is a simplification, if more than 2 users we should check notification.target to know which user
* should get the notification.
*/
if (sender === rtc.selfSession.id) {
await remoteUserP2P.handleNotification(sender, message);
} else {
await localUserP2P.handleNotification(sender, message);
}
}
});
const localUserConnected = connectionReady(localUserP2P);
const remoteUserConnected = connectionReady(remoteUserP2P);
return { localUserConnected, remoteUserConnected };
}
onlineTest("Can join a call in p2p", async (assert) => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const remoteSessionId = pyEnv["discuss.channel.rtc.session"].create({
channel_member_id: pyEnv["discuss.channel.member"].create({
channel_id: channelId,
partner_id: pyEnv["res.partner"].create({ name: "Remote" }),
}),
channel_id: channelId,
});
const { localUserConnected, remoteUserConnected } = await mockPeerToPeerCallEnvironment({
channelId,
remoteSessionId,
});
await openDiscuss(channelId);
await click("[title='Join Call']");
await contains(".o-discuss-Call");
await contains(".o-discuss-CallParticipantCard[title='Remote']");
await Promise.all([localUserConnected, remoteUserConnected]);
await contains("span[data-connection-state='connected']");
});

View file

@ -0,0 +1,103 @@
import {
click,
contains,
defineMailModels,
mockGetMedia,
mockPermissionsPrompt,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
describe.current.tags("desktop");
defineMailModels();
test("Starting a video call asks for permissions", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
mockGetMedia();
mockPermissionsPrompt();
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
await click("[title='Start Video Call']");
await contains(".modal[role='dialog']", { count: 1 });
rtc.cameraPermission = "granted";
await click(".modal-footer button", { text: "Use Camera" });
await contains(".o-discuss-CallActionList button[title='Stop camera']");
});
test("Turning on the microphone asks for permissions", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
mockGetMedia();
mockPermissionsPrompt();
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
await click("[title='Start Call']");
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
await click(".o-discuss-CallActionList button[title='Unmute']");
await contains(".modal[role='dialog']", { count: 1 });
rtc.microphonePermission = "granted";
await click(".modal-footer button", { text: "Use Microphone" });
await contains(".o-discuss-CallActionList button[title='Mute']");
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
});
test("Turning on the camera asks for permissions", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
mockGetMedia();
mockPermissionsPrompt();
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
await click("[title='Start Call']");
await click(".o-discuss-CallActionList button[title='Turn camera on']");
await contains(".modal[role='dialog']", { count: 1 });
rtc.cameraPermission = "granted";
await click(".modal-footer button", { text: "Use Camera" });
await contains(".o-discuss-CallActionList button[title='Stop camera']");
});
test("Turn on both microphone and camera from permission dialog", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
mockGetMedia();
mockPermissionsPrompt();
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
await click("[title='Start Call']");
await contains(".o-discuss-CallActionList button[title='Turn camera on']");
await click(".o-discuss-CallActionList button[title='Turn camera on']");
await contains(".modal[role='dialog']", { count: 1 });
rtc.microphonePermission = "granted";
rtc.cameraPermission = "granted";
await click(".modal-footer button", { text: "Use microphone and camera" });
await contains(".o-discuss-CallActionList button[title='Stop camera']");
await contains(".o-discuss-CallActionList button[title='Mute']");
});
test("Combined mic+camera button only shown when both permissions not granted", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
mockGetMedia();
mockPermissionsPrompt();
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
await click("[title='Start Call']");
await click(".o-discuss-CallActionList button[title='Turn camera on']");
await contains(".modal-footer button", { count: 2 });
await contains(".modal-footer button", { text: "Use microphone and camera" });
await contains(".modal-footer button", { text: "Use Camera" });
rtc.cameraPermission = "granted";
await click(".modal-footer button", { text: "Use Camera" });
await click(".o-discuss-CallActionList button[title='Unmute']");
await contains(".modal-footer button");
await contains(".modal-footer button", { text: "Use Microphone" });
});

View file

@ -0,0 +1,22 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
describe.current.tags("desktop");
defineMailModels();
test("Call has Picture-in-picture feature", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await click("[title='Start Call']");
await contains(".o-discuss-Call");
await contains(".o-discuss-Call-layoutActions button[title='Picture in Picture']");
});

View file

@ -0,0 +1,131 @@
import {
click,
contains,
defineMailModels,
editInput,
openDiscuss,
patchUiSize,
SIZES,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test, expect } from "@odoo/hoot";
import { advanceTime } from "@odoo/hoot-mock";
import { asyncStep, patchWithCleanup, waitForSteps } from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
describe.current.tags("desktop");
defineMailModels();
test("Renders the call settings", async () => {
patchWithCleanup(browser.navigator.mediaDevices, {
enumerateDevices: () =>
Promise.resolve([
{
deviceId: "mockAudioDeviceId",
kind: "audioinput",
label: "mockAudioDeviceLabel",
},
{
deviceId: "mockVideoDeviceId",
kind: "videoinput",
label: "mockVideoDeviceLabel",
},
]),
});
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
patchUiSize({ size: SIZES.SM });
const env = await start();
const rtc = env.services["discuss.rtc"];
await openDiscuss(channelId);
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await contains(".o-discuss-CallSettings");
await contains("label[aria-label='Camera']");
await contains("label[aria-label='Microphone']");
await contains("label[aria-label='Audio Output']");
await contains("option", { textContent: "Permission Needed", count: 3 });
rtc.microphonePermission = "granted";
await contains("option[value=mockAudioDeviceId]");
rtc.cameraPermission = "granted";
await contains("option[value=mockVideoDeviceId]");
await contains("button", { text: "Voice Detection" });
await contains("button", { text: "Push to Talk" });
await contains("span", { text: "Voice detection sensitivity" });
await contains("button", { text: "Test" });
await contains("label", { text: "Show video participants only" });
await contains("label", { text: "Blur video background" });
});
test("activate push to talk", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss(channelId);
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await click("button", { text: "Push to Talk" });
await contains("i[aria-label='Register new key']");
await contains("label", { text: "Delay after releasing push-to-talk" });
await contains("label", { text: "Voice detection sensitivity", count: 0 });
});
test("activate blur", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss(channelId);
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await click("input[title='Blur video background']");
await contains("label", { text: "Blur video background" });
await contains("label", { text: "Edge blur intensity" });
});
test("local storage for call settings", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "test" });
localStorage.setItem("mail_user_setting_background_blur_amount", "3");
localStorage.setItem("mail_user_setting_edge_blur_amount", "5");
localStorage.setItem("mail_user_setting_show_only_video", "true");
localStorage.setItem("mail_user_setting_use_blur", "true");
patchWithCleanup(localStorage, {
setItem(key, value) {
if (key.startsWith("mail_user_setting")) {
asyncStep(`${key}: ${value}`);
}
return super.setItem(key, value);
},
});
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss(channelId);
// testing load from local storage
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await contains("input[title='Show video participants only']:checked");
await contains("input[title='Blur video background']:checked");
await contains("label[title='Background blur intensity']", { text: "15%" });
await contains("label[title='Edge blur intensity']", { text: "25%" });
// testing save to local storage
await click("input[title='Show video participants only']");
await waitForSteps(["mail_user_setting_show_only_video: false"]);
await click("input[title='Blur video background']");
expect(localStorage.getItem("mail_user_setting_use_blur")).toBe(null);
await editInput(document.body, ".o-Discuss-CallSettings-thresholdInput", 0.3);
await advanceTime(2000); // threshold setting debounce timer
await waitForSteps(["mail_user_setting_voice_threshold: 0.3"]);
});

View file

@ -0,0 +1,211 @@
import { describe, expect } from "@odoo/hoot";
import { advanceTime } from "@odoo/hoot-mock";
import { browser } from "@web/core/browser/browser";
import { asyncStep, onRpc, mountWebClient, waitForSteps } from "@web/../tests/web_test_helpers";
import { defineMailModels, mockGetMedia, onlineTest } from "@mail/../tests/mail_test_helpers";
import { PeerToPeer, STREAM_TYPE, UPDATE_EVENT } from "@mail/discuss/call/common/peer_to_peer";
describe.current.tags("desktop");
defineMailModels();
class Network {
_peerToPeerInstances = new Map();
_notificationRoute;
constructor(route) {
this._notificationRoute = route || "/any/mock/notification";
onRpc(this._notificationRoute, async (req) => {
const {
params: { peer_notifications },
} = await req.json();
for (const notification of peer_notifications) {
const [sender_session_id, target_session_ids, content] = notification;
for (const id of target_session_ids) {
const p2p = this._peerToPeerInstances.get(id);
p2p.handleNotification(sender_session_id, content);
}
}
});
}
/**
* @param id
* @return {{id, p2p: PeerToPeer}}
*/
register(id) {
const p2p = new PeerToPeer({ notificationRoute: this._notificationRoute });
this._peerToPeerInstances.set(id, p2p);
return { id, p2p };
}
close() {
for (const p2p of this._peerToPeerInstances.values()) {
p2p.disconnect();
}
}
}
onlineTest("basic peer to peer connection", async () => {
await mountWebClient();
const channelId = 1;
const network = new Network();
const user1 = network.register(1);
const user2 = network.register(2);
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
asyncStep(payload.state);
}
});
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
await user1.p2p.addPeer(user2.id);
await waitForSteps(["connected"]);
network.close();
});
onlineTest("mesh peer to peer connections", async () => {
await mountWebClient();
const channelId = 2;
const network = new Network();
const userCount = 10;
const users = Array.from({ length: userCount }, (_, i) => network.register(i));
const promises = [];
for (const user of users) {
user.p2p.connect(user.id, channelId);
for (let i = 0; i < user.id; i++) {
promises.push(user.p2p.addPeer(i));
}
}
await Promise.all(promises);
let connectionsCount = 0;
for (const user of users) {
connectionsCount += user.p2p.peers.size;
}
expect(connectionsCount).toBe(userCount * (userCount - 1));
connectionsCount = 0;
network.close();
for (const user of users) {
connectionsCount += user.p2p.peers.size;
}
expect(connectionsCount).toBe(0);
});
onlineTest("connection recovery", async () => {
await mountWebClient();
const channelId = 1;
const network = new Network();
const user1 = network.register(1);
const user2 = network.register(2);
user2.remoteStates = new Map();
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.CONNECTION_CHANGE && payload.state === "connected") {
asyncStep(payload.state);
}
});
user1.p2p.connect(user1.id, channelId);
user1.p2p.addPeer(user2.id);
// only connecting user2 after user1 has called addPeer so that user2 ignores notifications
// from user1, which simulates a connection drop that should be recovered.
user2.p2p.connect(user2.id, channelId);
const openPromise = new Promise((resolve) => {
user1.p2p.peers.get(2).dataChannel.onopen = resolve;
});
advanceTime(5_000); // recovery timeout
await openPromise;
await waitForSteps(["connected"]);
network.close();
});
onlineTest("can broadcast a stream and control download", async () => {
mockGetMedia();
await mountWebClient();
const channelId = 3;
const network = new Network();
const user1 = network.register(1);
const user2 = network.register(2);
user2.remoteMedia = new Map();
const trackPromise = new Promise((resolve) => {
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.TRACK) {
user2.remoteMedia.set(payload.sessionId, {
[payload.type]: {
track: payload.track,
active: payload.active,
},
});
resolve();
}
});
});
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
await user1.p2p.addPeer(user2.id);
const videoStream = await browser.navigator.mediaDevices.getUserMedia({
video: true,
});
const videoTrack = videoStream.getVideoTracks()[0];
await user1.p2p.updateUpload(STREAM_TYPE.CAMERA, videoTrack);
await trackPromise;
const user2RemoteMedia = user2.remoteMedia.get(user1.id);
const user2CameraTransceiver = user2.p2p.peers.get(user1.id).getTransceiver(STREAM_TYPE.CAMERA);
expect(user2CameraTransceiver.direction).toBe("recvonly");
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].track.kind).toBe("video");
expect(user2RemoteMedia[STREAM_TYPE.CAMERA].active).toBe(true);
user2.p2p.updateDownload(user1.id, { camera: false });
expect(user2CameraTransceiver.direction).toBe("inactive");
network.close();
});
onlineTest("can broadcast arbitrary messages (dataChannel)", async () => {
await mountWebClient();
const channelId = 4;
const network = new Network();
const user1 = network.register(1);
const user2 = network.register(2);
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
await user1.p2p.addPeer(user2.id);
user1.inbox = [];
const pongPromise = new Promise((resolve) => {
user1.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.BROADCAST) {
user1.inbox.push(payload);
resolve();
}
});
});
user2.inbox = [];
user2.p2p.addEventListener("update", ({ detail: { name, payload } }) => {
if (name === UPDATE_EVENT.BROADCAST && payload.message === "ping") {
user2.inbox.push(payload);
user2.p2p.broadcast("pong");
}
});
user1.p2p.broadcast("ping");
await pongPromise;
expect(user2.inbox[0].senderId).toBe(user1.id);
expect(user2.inbox[0].message).toBe("ping");
expect(user1.inbox[0].senderId).toBe(user2.id);
expect(user1.inbox[0].message).toBe("pong");
network.close();
});
onlineTest("can reject arbitrary offers", async () => {
await mountWebClient();
const channelId = 1;
const network = new Network();
const user1 = network.register(1);
const user2 = network.register(2);
user2.p2p.connect(user2.id, channelId);
user1.p2p.connect(user1.id, channelId);
user2.p2p._emitLog = (id, message) => {
if (message === "offer rejected") {
asyncStep("offer rejected");
}
};
user2.p2p.acceptOffer = (id, sequence) => id !== user1.id || sequence > 20;
user1.p2p.addPeer(user2.id, { sequence: 19 });
await waitForSteps(["offer rejected"]);
network.close();
});

View file

@ -0,0 +1,47 @@
import {
click,
contains,
defineMailModels,
mockGetMedia,
openDiscuss,
patchUiSize,
SIZES,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
import { describe, test } from "@odoo/hoot";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("display banner when ptt extension is not enabled", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
patchWithCleanup(pttExtensionServiceInternal, {
onAnswerIsEnabled(pttService) {
pttService.isEnabled = false;
},
});
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss(channelId);
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await click("button", { text: "Push to Talk" });
await click("[title*='Close Chat Window']");
await click("button[title='New Meeting']");
await click("button[title='Close panel']"); // invitation panel automatically open
await contains(".o-discuss-PttAdBanner");
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await click("button", { text: "Voice Detection" });
await click("[title*='Close Chat Window']");
await contains(".o-discuss-PttAdBanner", { count: 0 });
});

View file

@ -0,0 +1,123 @@
import {
click,
contains,
defineMailModels,
insertText,
mockGetMedia,
openDiscuss,
patchUiSize,
SIZES,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { pttExtensionServiceInternal } from "@mail/discuss/call/common/ptt_extension_service";
import { PTT_RELEASE_DURATION } from "@mail/discuss/call/common/rtc_service";
import { advanceTime, freezeTime, keyDown, mockTouch, mockUserAgent, test } from "@odoo/hoot";
import { patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
defineMailModels();
test.tags("desktop");
test("no auto-call on joining chat", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
pyEnv["res.users"].create({ partner_id: partnerId });
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await contains(".o_command_name", { count: 5 });
await insertText("input[placeholder='Search a conversation']", "mario");
await contains(".o_command_name", { count: 3 });
await click(".o_command_name", { text: "Mario" });
await contains(".o-mail-DiscussSidebar-item", { text: "Mario" });
await contains(".o-mail-Message", { count: 0 });
await contains(".o-discuss-Call", { count: 0 });
});
test.tags("desktop");
test("no auto-call on joining group chat", async () => {
const pyEnv = await startServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ name: "Mario" },
{ name: "Luigi" },
]);
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await click("a", { text: "Create Chat" });
await click("li", { text: "Mario" });
await click("li", { text: "Luigi" });
await click("button", { text: "Create Group Chat" });
await contains(".o-mail-DiscussSidebar-item:contains('Mario, and Luigi')");
await contains(".o-mail-Message", { count: 0 });
await contains(".o-discuss-Call", { count: 0 });
});
test.tags("mobile");
test("show Push-to-Talk button on mobile", async () => {
mockGetMedia();
mockTouch(true);
mockUserAgent("android");
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
patchWithCleanup(pttExtensionServiceInternal, {
onAnswerIsEnabled(pttService) {
pttService.isEnabled = false;
},
});
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss(channelId);
await click(".o-mail-ChatWindow-moreActions", { text: "General" });
await click(".o-dropdown-item:text('Start Call')");
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await click("button", { text: "Push to Talk" });
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Call Settings" });
await contains("button", { text: "Push to talk" });
});
test.tags("desktop");
test("Can push-to-talk", async () => {
mockGetMedia();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["res.users.settings"].create({
use_push_to_talk: true,
user_id: serverState.userId,
push_to_talk_key: "...f",
});
patchWithCleanup(pttExtensionServiceInternal, {
onAnswerIsEnabled(pttService) {
pttService.isEnabled = false;
},
});
freezeTime();
await start();
await openDiscuss(channelId);
await advanceTime(1000);
await click("[title='Start Call']");
await advanceTime(1000);
await contains(".o-discuss-Call");
await click(".o-discuss-Call");
await advanceTime(1000);
await keyDown("f");
await advanceTime(PTT_RELEASE_DURATION);
await contains(".o-discuss-CallParticipantCard .o-isTalking");
// switching tab while PTT key still pressed then released on other tab should eventually release PTT
browser.dispatchEvent(new Event("blur"));
await advanceTime(PTT_RELEASE_DURATION + 1000);
await contains(".o-discuss-CallParticipantCard:not(:has(.o-isTalking))");
await click(".o-discuss-Call");
await advanceTime(1000);
await keyDown("f");
await advanceTime(PTT_RELEASE_DURATION);
await contains(".o-discuss-CallParticipantCard .o-isTalking");
});

View file

@ -0,0 +1,56 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
describe.current.tags("desktop");
defineMailModels();
test("Empty attachment panel", async () => {
const pyEnv = await startServer();
const channelId = await pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Attachments']");
await contains(".o-mail-ActionPanel", {
text: "This channel doesn't have any attachments.",
});
});
test("Attachment panel sort by date", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["ir.attachment"].create([
{
res_id: channelId,
res_model: "discuss.channel",
name: "file1.pdf",
create_date: "2023-08-20 10:00:00",
},
{
res_id: channelId,
res_model: "discuss.channel",
name: "file2.pdf",
create_date: "2023-09-21 10:00:00",
},
]);
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Attachments']");
await contains(".o-mail-AttachmentList", {
text: "file2.pdf",
after: [".o-mail-DateSection", { text: "September, 2023" }],
before: [".o-mail-DateSection", { text: "August, 2023" }],
});
await contains(".o-mail-AttachmentList", {
text: "file1.pdf",
after: [".o-mail-DateSection", { text: "August, 2023" }],
});
});

View file

@ -0,0 +1,112 @@
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
import { onWebsocketEvent } from "@bus/../tests/mock_websocket";
import {
click,
contains,
defineMailModels,
insertText,
openDiscuss,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, edit, expect, mockDate, press, test } from "@odoo/hoot";
import { Command } from "@web/../tests/web_test_helpers";
defineMailModels();
describe.current.tags("desktop");
test("bus subscription updated when joining/leaving thread as non member", async () => {
const pyEnv = await startServer();
const johnUser = pyEnv["res.users"].create({ name: "John" });
const johnPartner = pyEnv["res.partner"].create({ name: "John", user_ids: [johnUser] });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [Command.create({ partner_id: johnPartner })],
name: "General",
});
await start();
await openDiscuss(channelId);
await waitForChannels([`discuss.channel_${channelId}`]);
await click("[title='Channel Actions']");
await click(".o-dropdown-item:contains('Leave Channel')");
await click("button", { text: "Leave Conversation" });
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
});
test("bus subscription updated when opening/closing chat window as a non member", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [],
name: "Sales",
});
setupChatHub({ opened: [channelId] });
await start();
await contains(".o-mail-ChatWindow", { text: "Sales" });
await waitForChannels([`discuss.channel_${channelId}`]);
await click("[title*='Close Chat Window']", {
parent: [".o-mail-ChatWindow", { text: "Sales" }],
});
await contains(".o-mail-ChatWindow", { count: 0, text: "Sales" });
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
await press(["control", "k"]);
await click(".o_command_palette_search input");
await edit("@");
await click(".o-mail-DiscussCommand", { text: "Sales" });
await waitForChannels([`discuss.channel_${channelId}`]);
});
test("bus subscription updated when joining locally pinned thread", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [],
name: "General",
});
await start();
await openDiscuss(channelId);
await waitForChannels([`discuss.channel_${channelId}`]);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", {
text: "Mitchell Admin",
});
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
await waitForChannels([`discuss.channel_${channelId}`], { operation: "delete" });
});
test("bus subscription is refreshed when channel is joined", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create([{ name: "General" }, { name: "Sales" }]);
onWebsocketEvent("subscribe", () => expect.step("subscribe"));
const later = luxon.DateTime.now().plus({ seconds: 2 });
mockDate(
`${later.year}-${later.month}-${later.day} ${later.hour}:${later.minute}:${later.second}`
);
await start();
await expect.waitForSteps(["subscribe"]);
await openDiscuss();
await expect.waitForSteps([]);
await click("input[placeholder='Search conversations']");
await insertText("input[placeholder='Search a conversation']", "new channel");
await expect.waitForSteps(["subscribe"]);
});
test("bus subscription is refreshed when channel is left", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "General" });
onWebsocketEvent("subscribe", () => expect.step("subscribe"));
const later = luxon.DateTime.now().plus({ seconds: 2 });
mockDate(
`${later.year}-${later.month}-${later.day} ${later.hour}:${later.minute}:${later.second}`
);
await start();
await expect.waitForSteps(["subscribe"]);
await openDiscuss();
await expect.waitForSteps([]);
await click("[title='Channel Actions']");
await click(".o-dropdown-item:contains('Leave Channel')");
await expect.waitForSteps(["subscribe"]);
});

View file

@ -0,0 +1,260 @@
import {
click,
contains,
defineMailModels,
insertText,
mockPermissionsPrompt,
openDiscuss,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-dom";
import { mockDate } from "@odoo/hoot-mock";
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("should display the channel invitation form after clicking on the invite button of a chat", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv["res.users"].create({ partner_id: partnerId });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Invite People']");
await contains(".o-discuss-ChannelInvitation");
});
test("can invite users in channel from chat window", async () => {
mockDate("2025-01-01 12:00:00", +1);
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv["res.users"].create({ partner_id: partnerId });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_type: "channel",
});
setupChatHub({ opened: [channelId] });
await start();
// dropdown requires an extra delay before click (because handler is registered in useEffect)
await contains("[title='Open Actions Menu']");
await click("[title='Open Actions Menu']");
await click(".o-dropdown-item", { text: "Invite People" });
await contains(".o-discuss-ChannelInvitation");
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner" });
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await contains(".o-mail-Thread .o-mail-NotificationMessage", {
text: "Mitchell Admin invited TestPartner to the channel1:00 PM",
});
});
test("should be able to search for a new user to invite from an existing chat", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
const partnerId_2 = pyEnv["res.partner"].create({
email: "testpartner2@odoo.com",
name: "TestPartner2",
});
pyEnv["res.users"].create({ partner_id: partnerId_1 });
pyEnv["res.users"].create({ partner_id: partnerId_2 });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Invite People']");
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner2");
await contains(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner2" });
});
test("Invitation form should display channel group restriction", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv["res.users"].create({ partner_id: partnerId });
const groupId = pyEnv["res.groups"].create({
name: "testGroup",
});
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_type: "channel",
group_public_id: groupId,
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Invite People']");
await contains(".o-discuss-ChannelInvitation div", {
text: 'Access restricted to group "testGroup"',
after: ["button .fa.fa-copy"],
});
});
test("should be able to create a new group chat from an existing chat", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
const partnerId_2 = pyEnv["res.partner"].create({
email: "testpartner2@odoo.com",
name: "TestPartner2",
});
pyEnv["res.users"].create({ partner_id: partnerId_1 });
pyEnv["res.users"].create({ partner_id: partnerId_2 });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
],
channel_type: "chat",
});
await start();
await openDiscuss(channelId);
await click(".o-mail-DiscussContent-header button[title='Invite People']");
await contains(".o-discuss-ChannelInvitation");
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner2");
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner2" });
await click("button[title='Create Group Chat']:enabled");
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await contains(".o-mail-DiscussSidebarChannel", {
text: "Mitchell Admin, TestPartner, and TestPartner2",
});
});
test("unnamed group chat should display correct name just after being invited", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({
email: "jane@example.com",
name: "Jane",
});
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
const [, channelId] = pyEnv["discuss.channel"].create([
{ name: "General" },
{
channel_member_ids: [Command.create({ partner_id: partnerId })],
channel_type: "group",
},
]);
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel", { text: "General" });
await contains(".o-mail-DiscussSidebarChannel", { count: 0, text: "Jane and Mitchell Admin" });
const currentPartnerId = serverState.partnerId;
await withUser(userId, async () => {
await getService("orm").call("discuss.channel", "add_members", [[channelId]], {
partner_ids: [currentPartnerId],
});
});
await contains(".o-mail-DiscussSidebarChannel", { text: "Jane and Mitchell Admin" });
await contains(".o_notification", {
text: "You have been invited to #Jane and Mitchell Admin",
});
});
test("invite user to self chat opens DM chat with user", async () => {
const pyEnv = await startServer();
const guestId = pyEnv["mail.guest"].create({ name: "TestGuest" });
const partnerId_1 = pyEnv["res.partner"].create({
email: "testpartner@odoo.com",
name: "TestPartner",
});
pyEnv["res.users"].create({ partner_id: partnerId_1 });
const [selfChatId] = pyEnv["discuss.channel"].create([
{
channel_member_ids: [Command.create({ partner_id: serverState.partnerId })],
channel_type: "chat",
},
{
channel_member_ids: [
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: serverState.partnerId }),
],
channel_type: "group",
},
{
// group chat with guest as correspondent for coverage of no crash
channel_member_ids: [
Command.create({ guest_id: guestId }),
Command.create({ partner_id: serverState.partnerId }),
],
channel_type: "group",
},
{
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
],
channel_type: "chat",
},
]);
await start();
await openDiscuss(selfChatId);
await contains(".o-mail-DiscussSidebarChannel", { text: "Mitchell Admin" }); // self-chat
await contains(".o-mail-DiscussSidebarChannel", { text: "TestPartner and Mitchell Admin" });
await contains(".o-mail-DiscussSidebarChannel", { text: "TestGuest and Mitchell Admin" });
await contains(".o-mail-DiscussSidebarChannel", { text: "TestPartner" });
await click(".o-mail-DiscussContent-header button[title='Invite People']");
await insertText(".o-discuss-ChannelInvitation-search", "TestPartner");
await click(".o-discuss-ChannelInvitation-selectable", { text: "TestPartner" });
await click("button:contains('Go to Conversation'):enabled");
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "TestPartner" });
});
test("Invite sidebar action has the correct title for group chats", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "group",
});
await start();
await openDiscuss(channelId);
await click("button[title='Chat Actions']");
await click(".o-dropdown-item", { text: "Invite People" });
await contains(".modal-title", { text: "Mitchell Admin and Demo" });
});
test("Active dialog retains focus over invite input", async () => {
await startServer();
mockPermissionsPrompt();
await start();
await openDiscuss();
await click("button[title='New Meeting']");
await animationFrame();
await contains(".o-discuss-ChannelInvitation");
await contains("button:focus", { text: "Use Camera" });
});

View file

@ -0,0 +1,233 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("there should be a button to show member list in the thread view topbar initially", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains("[title='Members']");
});
test("should show member list when clicking on member list button in thread view topbar", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // open by default
await click("[title='Members']");
await contains(".o-discuss-ChannelMemberList", { count: 0 });
await click("[title='Members']");
await contains(".o-discuss-ChannelMemberList");
});
test("should have correct members in member list", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { count: 2 });
await contains(".o-discuss-ChannelMember", { text: serverState.partnerName });
await contains(".o-discuss-ChannelMember", { text: "Demo" });
});
test("members should be correctly categorised into online/offline", async () => {
const pyEnv = await startServer();
const [onlinePartnerId, idlePartnerId] = pyEnv["res.partner"].create([
{ name: "Online Partner", im_status: "online" },
{ name: "Idle Partner", im_status: "away" },
]);
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "im_partner" });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChanel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: onlinePartnerId }),
Command.create({ partner_id: idlePartnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList h6", { text: "Online - 2" });
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 1" });
});
test("chat with member should be opened after clicking on channel member", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
pyEnv["res.users"].create({ partner_id: partnerId });
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await click(".o-discuss-ChannelMember.cursor-pointer", { text: "Demo" });
await contains(".o_avatar_card .o_card_user_infos", { text: "Demo" });
await click(".o_avatar_card button", { text: "Send message" });
await contains(".o-mail-AutoresizeInput[title='Demo']");
});
test("should show a button to load more members if they are not all loaded", async () => {
// Test assumes at most 100 members are loaded at once.
const pyEnv = await startServer();
const channel_member_ids = [];
for (let i = 0; i < 101; i++) {
const partnerId = pyEnv["res.partner"].create({ name: "name" + i });
channel_member_ids.push(Command.create({ partner_id: partnerId }));
}
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
pyEnv["discuss.channel"].write([channelId], { channel_member_ids });
await contains(
".o-mail-ActionPanel:has(.o-mail-ActionPanel-header:contains('Members')) button",
{ text: "Load more" }
);
});
test("Load more button should load more members", async () => {
// Test assumes at most 100 members are loaded at once.
const pyEnv = await startServer();
const channel_member_ids = [];
for (let i = 0; i < 101; i++) {
const partnerId = pyEnv["res.partner"].create({ name: "name" + i });
channel_member_ids.push(Command.create({ partner_id: partnerId }));
}
const channelId = pyEnv["discuss.channel"].create({
name: "TestChannel",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
pyEnv["discuss.channel"].write([channelId], { channel_member_ids });
await click(
".o-mail-ActionPanel:has(.o-mail-ActionPanel-header:contains('Members')) [title='Load more']"
);
await contains(".o-discuss-ChannelMember", { count: 102 });
});
test("Channel member count update after user joined", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const userId = pyEnv["res.users"].create({ name: "Harry" });
pyEnv["res.partner"].create({ name: "Harry", user_ids: [userId] });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 1" });
await click("[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Harry" });
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await click("[title='Members']");
await contains(".o-discuss-ChannelMemberList h6", { text: "Offline - 2" });
});
test("Channel member count update after user left", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Dobby" });
const partnerId = pyEnv["res.partner"].create({ name: "Dobby", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { count: 2 });
await withUser(userId, () =>
getService("orm").call("discuss.channel", "action_unfollow", [channelId])
);
await contains(".o-discuss-ChannelMember", { count: 1 });
});
test("Members are partitioned by online/offline", async () => {
const pyEnv = await startServer();
const [userId_1, userId_2] = pyEnv["res.users"].create([{ name: "Dobby" }, { name: "John" }]);
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{
name: "Dobby",
user_ids: [userId_1],
im_status: "offline",
},
{
name: "John",
user_ids: [userId_2],
im_status: "online",
},
]);
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
});
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "online" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { count: 3 });
await contains("h6", { text: "Online - 2" });
await contains("h6", { text: "Offline - 1" });
await contains(".o-discuss-ChannelMember", {
text: "John",
after: ["h6", { text: "Online - 2" }],
before: ["h6", { text: "Offline - 1" }],
});
await contains(".o-discuss-ChannelMember", {
text: "Mitchell Admin",
after: ["h6", { text: "Online - 2" }],
before: ["h6", { text: "Offline - 1" }],
});
await contains(".o-discuss-ChannelMember", {
text: "Dobby",
after: ["h6", { text: "Offline - 1" }],
});
});

View file

@ -0,0 +1,61 @@
import {
click,
contains,
defineMailModels,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
defineMailModels();
describe.current.tags("desktop");
test("Group name is based on channel members when name is not set", async () => {
const pyEnv = await startServer();
pyEnv["res.users"].create(
["Alice", "Bob", "Eve", "John", "Sam"].map((name) => ({
name,
partner_id: pyEnv["res.partner"].create({ name }),
}))
);
const channelId = pyEnv["discuss.channel"].create({ channel_type: "group" });
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin']");
await click("button[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Alice" });
await click("button", { text: "Invite to Group Chat" });
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin and Alice']");
await click("button[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Bob" });
await click("button", { text: "Invite to Group Chat" });
await contains(".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, and Bob']");
await click("button[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Eve" });
await click("button", { text: "Invite to Group Chat" });
await contains(
".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, Bob, and 1 other']"
);
await click("button[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "John" });
await click("button", { text: "Invite to Group Chat" });
await contains(
".o-mail-DiscussContent-threadName[title='Mitchell Admin, Alice, Bob, and 2 others']"
);
await click(".o-mail-DiscussContent-threadName");
await insertText(".o-mail-DiscussContent-threadName.o-focused", "Custom name", {
replace: true,
});
await contains(".o-mail-DiscussContent-threadName[title='Custom name']");
await press("Enter");
// Ensure that after setting the name, members are not taken into account for the group name.
await click("button[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Sam" });
await click("button", { text: "Invite to Group Chat" });
await contains(".o_mail_notification", { text: "invited Sam to the channel" });
await contains(".o-mail-DiscussContent-threadName[title='Custom name']");
});

View file

@ -0,0 +1,51 @@
import {
click,
contains,
defineMailModels,
setupChatHub,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { getOrigin } from "@web/core/utils/urls";
describe.current.tags("desktop");
defineMailModels();
test("clicking message link does not swap open chat window", async () => {
const pyEnv = await startServer();
const [rdId, supportId] = pyEnv["discuss.channel"].create([
{ name: "R&D" },
{ name: "Support" },
]);
const messageRdId = pyEnv["mail.message"].create({
body: "Hello R&D",
model: "discuss.channel",
res_id: rdId,
});
const urlRd = `${getOrigin()}/mail/message/${messageRdId}`;
const messageSupportId = pyEnv["mail.message"].create({
body: `Hello from there <a class="o_message_redirect" href="${urlRd}" data-oe-model="mail.message" data-oe-id="${messageRdId}">${urlRd}</a>`,
model: "discuss.channel",
res_id: supportId,
});
const urlSupport = `${getOrigin()}/mail/message/${messageSupportId}`;
pyEnv["mail.message"].create({
body: `Hello back <a class="o_message_redirect" href="${urlSupport}" data-oe-model="mail.message" data-oe-id="${messageSupportId}">${urlSupport}</a>`,
model: "discuss.channel",
res_id: rdId,
});
setupChatHub({ opened: [rdId, supportId] });
await start();
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
await click("a.o_message_redirect:contains(R&D)");
await contains(".o-mail-Message.o-highlighted:contains(Hello R&D)");
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
await click("a.o_message_redirect:contains(Support)");
await contains(".o-mail-Message.o-highlighted:contains(Hello from there)");
await contains(".o-mail-ChatWindow:eq(0) .o-mail-ChatWindow-header:contains(R&D)");
await contains(".o-mail-ChatWindow:eq(1) .o-mail-ChatWindow-header:contains(Support)");
});

View file

@ -0,0 +1,661 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
import { describe, test } from "@odoo/hoot";
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineMailModels();
test("rendering when just one has received the message", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
channel_type: "group",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const [memberId_1] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId_1],
]);
pyEnv["discuss.channel.member"].write([memberId_1], {
fetched_message_id: messageId,
seen_message_id: false,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator[title='Sent']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
});
test("rendering when everyone have received the message", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
channel_type: "group",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: false,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator[title='Sent']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
});
test("rendering when just one has seen the message", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
channel_type: "group",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: false,
});
const [memberId_1] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId_1],
]);
pyEnv["discuss.channel.member"].write([memberId_1], {
seen_message_id: messageId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator[title='Seen by Demo User']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
});
test("rendering when just one has seen & received the message", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
channel_type: "group",
});
const mesageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const [memberId_1] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId_1],
]);
pyEnv["discuss.channel.member"].write([memberId_1], {
seen_message_id: mesageId,
fetched_message_id: mesageId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator[title='Seen by Demo User']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 0 });
});
test("rendering when just everyone has seen the message", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
],
channel_type: "group",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: messageId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator[title='Seen by everyone']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen", { count: 1 });
});
test("'channel_fetch' notification received is correctly handled", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "test" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
// Simulate received channel fetched notification
pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", {
id: pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId],
])[0],
channel_id: channelId,
last_message_id: 100,
partner_id: partnerId,
});
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
});
test("mark channel as seen from the bus", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "test" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
// Simulate received channel seen notification
const DiscussChannelMember = pyEnv["discuss.channel.member"];
pyEnv["bus.bus"]._sendone(
channel,
"mail.record/insert",
new mailDataHelpers.Store(
DiscussChannelMember.browse(
DiscussChannelMember.search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId],
])
),
{ seen_message_id: messageId }
).get_result()
);
await contains(".o-mail-MessageSeenIndicator[title='Seen by test']");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
});
test("should display message indicator when message is fetched/seen", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Recipient" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 });
const channel = pyEnv["discuss.channel"].search_read([["id", "=", channelId]])[0];
// Simulate received channel fetched notification
pyEnv["bus.bus"]._sendone(channel, "discuss.channel.member/fetched", {
id: pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId],
])[0],
channel_id: channelId,
last_message_id: messageId,
partner_id: partnerId,
});
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
// Simulate received channel seen notification
const DiscussChannelMember = pyEnv["discuss.channel.member"];
pyEnv["bus.bus"]._sendone(
channel,
"mail.record/insert",
new mailDataHelpers.Store(
DiscussChannelMember.browse(
DiscussChannelMember.search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId],
])
),
{ seen_message_id: messageId }
).get_result()
);
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
});
test("do not show message seen indicator on the last message seen by everyone when the current user is not author of the message", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, { seen_message_id: messageId });
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await contains(".o-mail-MessageSeenIndicator", { count: 0 });
});
test("do not show message seen indicator on all the messages of the current user that are older than the last message seen by everyone", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
const [, messageId_2] = pyEnv["mail.message"].create([
{
author_id: serverState.partnerId,
body: "<p>Message before last seen</p>",
model: "discuss.channel",
res_id: channelId,
},
{
author_id: serverState.partnerId,
body: "<p>Last seen by everyone</p>",
model: "discuss.channel",
res_id: channelId,
},
]);
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, { seen_message_id: messageId_2 });
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", {
text: "Message before last seen",
contains: [".o-mail-MessageSeenIndicator", { contains: [".fa-check", { count: 0 }] }],
});
});
test("only show messaging seen indicator if authored by me, after last seen by all message", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
res_id: channelId,
model: "discuss.channel",
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: messageId - 1,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 });
});
test("all seen indicator in chat displayed only once (chat created by correspondent)", async () => {
const pyEnv = await startServer();
const demoPid = pyEnv["res.partner"].create({ name: "Demo User" });
const demoUid = pyEnv["res.users"].create({ partner_id: demoPid });
const selfPid = serverState.partnerId;
const channelId = await withUser(demoUid, () =>
pyEnv["discuss.channel"].create({
name: "test",
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: demoPid }),
Command.create({ partner_id: selfPid }),
],
})
);
const [, messageId] = pyEnv["mail.message"].create([
{
author_id: selfPid,
body: "<p>Test1</p>",
res_id: channelId,
model: "discuss.channel",
},
{
author_id: selfPid,
body: "<p>Test2</p>",
res_id: channelId,
model: "discuss.channel",
},
]);
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: messageId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 2 });
await contains(".o-mail-MessageSeenIndicator.o-hasEveryoneSeen .fa-check", { count: 2 });
});
test("no seen indicator in 'channel' channels (with is_typing)", async () => {
// is_typing info contains fetched / seen message so this could mistakenly show seen indicators
const pyEnv = await startServer();
const demoId = pyEnv["res.partner"].create({ name: "Demo User" });
const demoUserId = pyEnv["res.users"].create({ partner_id: demoId });
const channelId = pyEnv["discuss.channel"].create({
name: "test-channel",
channel_type: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: demoId }),
],
});
const chatId = pyEnv["discuss.channel"].create({
name: "test-chat",
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: demoId }),
],
});
const [channelMsgId, chatMsgId] = pyEnv["mail.message"].create([
{
author_id: serverState.partnerId,
body: "<p>channel-msg</p>",
res_id: channelId,
model: "discuss.channel",
},
{
author_id: serverState.partnerId,
body: "<p>chat-msg</p>",
res_id: chatId,
model: "discuss.channel",
},
]);
const channelMemberIds = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
]);
const chatMemberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", chatId]]);
pyEnv["discuss.channel.member"].write(channelMemberIds, {
fetched_message_id: channelMsgId,
seen_message_id: 0,
});
pyEnv["discuss.channel.member"].write(chatMemberIds, {
fetched_message_id: chatMsgId,
seen_message_id: 0,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { text: "channel-msg" });
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 }); // none in channel
await click(".o-mail-DiscussSidebar-item", { text: "Demo User" });
await contains(".o-mail-Message", { text: "chat-msg" });
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 1 }); // received in chat
// simulate channel read by Demo User in both threads
await withUser(demoUserId, () =>
rpc("/discuss/channel/mark_as_read", {
channel_id: channelId,
last_message_id: channelMsgId,
})
);
await withUser(demoUserId, () =>
rpc("/discuss/channel/mark_as_read", {
channel_id: chatId,
last_message_id: chatMsgId,
})
);
// simulate typing by Demo User in both threads
await withUser(demoUserId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await withUser(demoUserId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: chatId,
is_typing: true,
})
);
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 }); // seen in chat
await click(".o-mail-DiscussSidebar-item", { text: "test-channel" });
await contains(".o-mail-Message", { text: "channel-msg" });
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 0 }); // none in channel
});
test("Show everyone seen title on message seen indicator", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const partnerId_2 = pyEnv["res.partner"].create({ name: "Other User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId, last_seen_dt: "2024-06-01 12:00" }),
Command.create({ partner_id: partnerId_1, last_seen_dt: "2024-06-01 12:00" }),
Command.create({ partner_id: partnerId_2, last_seen_dt: "2024-06-01 13:00" }),
],
channel_type: "group",
});
const mesageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const [memberId_1, memberId_2] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "in", [partnerId_1, partnerId_2]],
]);
pyEnv["discuss.channel.member"].write([memberId_1], {
seen_message_id: mesageId,
fetched_message_id: mesageId,
});
pyEnv["discuss.channel.member"].write([memberId_2], {
seen_message_id: mesageId,
fetched_message_id: mesageId,
});
await start();
await openDiscuss(channelId);
await contains("[title='Seen by everyone']");
});
test("Title show some member seen info (partial seen), click show dialog with full info", async () => {
// last member flagged as not seen so that it doesn't show "Seen by everyone" but list names instead
const pyEnv = await startServer();
const partners = [];
for (let i = 0; i < 12; i++) {
partners.push({ name: `User ${i}` });
}
const partnerIds = pyEnv["res.partner"].create(partners);
const channelMemberIds = [];
for (const partner_id of partnerIds) {
channelMemberIds.push(
Command.create({
partner_id,
last_seen_dt: partner_id === partnerIds.at(-1) ? false : "2024-06-01 12:00",
})
);
}
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_seen_dt: "2024-06-01 12:00",
}),
...channelMemberIds,
],
channel_type: "group",
});
const mesageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "<p>Test</p>",
model: "discuss.channel",
res_id: channelId,
});
const members = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "in", partnerIds.filter((p) => p !== partnerIds.at(-1))],
]);
pyEnv["discuss.channel.member"].write(members, {
seen_message_id: mesageId,
fetched_message_id: mesageId,
});
await start();
await openDiscuss(channelId);
await contains("[title='Seen by User 0, User 1, User 2 and 8 others']");
await click(".o-mail-MessageSeenIndicator");
await contains("li", { count: 11 });
for (let i = 0; i < 11; i++) {
await contains("li", { text: `User ${i}` }); // Not checking datetime because HOOT mocking of tz do not work
}
});
test("Show seen indicator on message with only attachment", async () => {
const pyEnv = await startServer();
const partnerId_1 = pyEnv["res.partner"].create({ name: "Demo User" });
const channelId = pyEnv["discuss.channel"].create({
name: "test",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
],
channel_type: "group",
});
const attachmentId = pyEnv["ir.attachment"].create({
name: "test.txt",
mimetype: "text/plain",
});
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "",
model: "discuss.channel",
res_id: channelId,
attachment_ids: [attachmentId],
});
const memberIds = pyEnv["discuss.channel.member"].search([["channel_id", "=", channelId]]);
pyEnv["discuss.channel.member"].write(memberIds, {
fetched_message_id: messageId,
seen_message_id: false,
});
const [memberId_1] = pyEnv["discuss.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", partnerId_1],
]);
pyEnv["discuss.channel.member"].write([memberId_1], {
seen_message_id: messageId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageSeenIndicator");
await contains(".o-mail-MessageSeenIndicator .fa-check", { count: 2 });
});

View file

@ -0,0 +1,128 @@
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
import {
click,
contains,
defineMailModels,
focus,
insertText,
onRpcBefore,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { Composer } from "@mail/core/common/composer";
import { beforeEach, describe, test } from "@odoo/hoot";
import {
asyncStep,
getService,
patchWithCleanup,
waitForSteps,
} from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
beforeEach(() => {
// Simulate real user interactions
patchWithCleanup(Composer.prototype, {
isEventTrusted() {
return true;
},
});
});
test('do not send typing notification on typing "/" command', async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", () => {
if (!testEnded) {
asyncStep("notify_typing");
}
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/");
await contains(".o-mail-Composer button[title='Send']:enabled");
await waitForSteps([]); // No rpc done
testEnded = true;
});
test('do not send typing notification on typing after selecting suggestion from "/" command', async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", () => {
if (!testEnded) {
asyncStep("notify_typing");
}
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/");
await click(":nth-child(1 of .o-mail-Composer-suggestion)");
await contains(".o-mail-Composer-suggestion strong", { count: 0 });
await insertText(".o-mail-Composer-input", " is user?");
await waitForSteps([]); // No rpc done"
testEnded = true;
});
test("send is_typing on adding emoji", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "channel" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", () => {
if (!testEnded) {
asyncStep("notify_typing");
}
});
await start();
await openDiscuss(channelId);
await click("button[title='Add Emojis']");
await insertText("input[placeholder='Search emoji']", "Santa Claus");
await click(".o-Emoji", { text: "🎅" });
await waitForSteps(["notify_typing"]);
testEnded = true;
});
test("add an emoji after a command", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Composer-input", { value: "" });
await insertText(".o-mail-Composer-input", "/");
await click(":nth-child(1 of .o-mail-Composer-suggestion)");
await contains(".o-mail-Composer-input", { value: "/who " });
await click("button[title='Add Emojis']");
await click(".o-Emoji", { text: "😊" });
await contains(".o-mail-Composer-input", { value: "/who 😊" });
});
test.tags("html composer");
test("html composer: send a message in a channel", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Composer-input", { value: "" });
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await focus(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "Hello");
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "Hello" });
await click(".o-mail-Composer button[title='Send']:enabled");
await click(".o-mail-Message[data-persistent]:contains(Hello)");
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "" });
});

View file

@ -0,0 +1,53 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, getService, serverState, withUser } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("Add member to channel", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const userId = pyEnv["res.users"].create({ name: "Harry" });
pyEnv["res.partner"].create({ name: "Harry", user_ids: [userId] });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await contains(".o-discuss-ChannelMember", { text: "Mitchell Admin" });
await click("[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Harry" });
await click(".o-discuss-ChannelInvitation [title='Invite']:enabled");
await contains(".o-discuss-ChannelInvitation", { count: 0 });
await click("[title='Members']");
await contains(".o-discuss-ChannelMember", { text: "Harry" });
});
test("Remove member from channel", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Harry" });
const partnerId = pyEnv["res.partner"].create({
name: "Harry",
user_ids: [userId],
});
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { text: "Harry" });
withUser(userId, () =>
getService("orm").call("discuss.channel", "action_unfollow", [channelId])
);
await contains(".o-discuss-ChannelMember", { count: 0, text: "Harry" });
});

View file

@ -0,0 +1,53 @@
import { onWebsocketEvent } from "@bus/../tests/mock_websocket";
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { tick } from "@odoo/hoot-dom";
import { makeMockEnv } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("Member list and Pinned Messages Panel menu are exclusive", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // member list open by default
await click("[title='Pinned Messages']");
await contains(".o-discuss-PinnedMessagesPanel");
await contains(".o-discuss-ChannelMemberList", { count: 0 });
});
test("subscribe to presence channels according to store data", async () => {
const env = await makeMockEnv();
const store = env.services["mail.store"];
onWebsocketEvent("subscribe", (data) => expect.step(`subscribe - [${data.channels}]`));
expect(env.services.bus_service.isActive).toBe(false);
// Should not subscribe to presences as bus service is not started.
store["res.partner"].insert({ id: 1, name: "Partner 1" });
store["res.partner"].insert({ id: 2, name: "Partner 2" });
await tick();
expect.waitForSteps([]);
// Starting the bus should subscribe to known presence channels.
env.services.bus_service.start();
await expect.waitForSteps([
"subscribe - [odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
]);
// Discovering new presence channels should refresh the subscription.
store["mail.guest"].insert({ id: 1 });
await expect.waitForSteps([
"subscribe - [odoo-presence-mail.guest_1,odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
]);
// Updating "im_status_access_token" should refresh the subscription.
store["mail.guest"].insert({ id: 1, im_status_access_token: "token" });
await expect.waitForSteps([
"subscribe - [odoo-presence-mail.guest_1-token,odoo-presence-res.partner_1,odoo-presence-res.partner_2]",
]);
});

View file

@ -0,0 +1,111 @@
import {
click,
contains,
defineMailModels,
insertText,
observeRenders,
openDiscuss,
prepareObserveRenders,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { Composer } from "@mail/core/common/composer";
import { Message } from "@mail/core/common/message";
import { describe, expect, test } from "@odoo/hoot";
import { onMounted, onPatched } from "@odoo/owl";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
import { range } from "@web/core/utils/numbers";
describe.current.tags("desktop");
defineMailModels();
test("posting new message should only render relevant part", async () => {
// For example, it should not render old messages again
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
const messageIds = [];
for (let i = 0; i < 10; i++) {
messageIds.push(
pyEnv["mail.message"].create({
body: `not_empty_${i}`,
model: "discuss.channel",
res_id: channelId,
})
);
}
messageIds.pop(); // remove last as it might need re-render (it was the newest message before)
let posting = false;
prepareObserveRenders();
patchWithCleanup(Message.prototype, {
setup() {
const cb = () => {
if (posting) {
if (messageIds.includes(this.message.id)) {
throw new Error(
"Should not re-render old messages again on posting a new message"
);
}
}
};
onMounted(cb);
onPatched(cb);
return super.setup();
},
});
await start();
const stopObserve1 = observeRenders();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 10 });
await insertText(".o-mail-Composer-input", "Test");
const result1 = stopObserve1();
// LessThan because renders could be batched
expect(result1.get(Message)).toBeLessThan(11); // 10: all messages initially
const stopObserve2 = observeRenders();
posting = true;
triggerHotkey("Enter");
await contains(".o-mail-Message", { count: 11 });
posting = false;
const result2 = stopObserve2();
expect(result2.get(Composer)).toBeLessThan(3); // 2: temp disabling + clear content
expect(result2.get(Message)).toBeLessThan(4); // 3: new temp msg + new genuine msg + prev msg
});
test("replying to message should only render relevant part", async () => {
// For example, it should not render all messages when selecting message to reply
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
const messageIds = range(0, 10).map((i) =>
pyEnv["mail.message"].create({ body: `${i}`, model: "discuss.channel", res_id: channelId })
);
messageIds.pop(); // remove last as this is the one to be replied to
let replying = false;
prepareObserveRenders();
patchWithCleanup(Message.prototype, {
setup() {
const cb = () => {
if (replying) {
if (messageIds.includes(this.message.id)) {
throw new Error(
"Should not re-render other messages on replying to a message"
);
}
}
};
onMounted(cb);
onPatched(cb);
return super.setup();
},
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 10 });
const stopObserve = observeRenders();
replying = true;
await click(".o-mail-Message:last [title='Reply']");
await contains(".o-mail-Composer:has(:text('Replying to Mitchell Admin'))");
replying = false;
const result = stopObserve();
expect(result.get(Composer)).toBeLessThan(2);
expect(result.get(Message)).toBeLessThan(2);
});

View file

@ -0,0 +1,62 @@
import { waitUntilSubscribe } from "@bus/../tests/bus_test_helpers";
import {
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { Command, mockService, patchWithCleanup, withUser } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineMailModels();
test("open channel in discuss from push notification", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss("mail.box_inbox");
await contains(".o-mail-DiscussContent-threadName[title='Inbox']");
navigator.serviceWorker.dispatchEvent(
new MessageEvent("message", {
data: { action: "OPEN_CHANNEL", data: { id: channelId } },
})
);
await contains(".o-mail-DiscussContent-threadName[title='General']");
});
test("notify message to user as non member", async () => {
patchWithCleanup(window, {
Notification: class Notification {
static get permission() {
return "granted";
}
constructor() {
expect.step("push notification");
}
addEventListener() {}
},
});
mockService("multi_tab", { isOnMainTab: () => true });
const pyEnv = await startServer();
const johnUser = pyEnv["res.users"].create({ name: "John" });
const johnPartner = pyEnv["res.partner"].create({ name: "John", user_ids: [johnUser] });
const channelId = pyEnv["discuss.channel"].create({
channel_type: "chat",
channel_member_ids: [Command.create({ partner_id: johnPartner })],
});
await start();
await Promise.all([openDiscuss(channelId), waitUntilSubscribe(`discuss.channel_${channelId}`)]);
await withUser(johnUser, () =>
rpc("/mail/message/post", {
post_data: { body: "Hello!", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-Message", { text: "Hello!" });
expect.verifySteps(["push notification"]);
});

View file

@ -0,0 +1,376 @@
import {
click,
contains,
defineMailModels,
insertText,
onRpcAfter,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Deferred, mockDate, animationFrame } from "@odoo/hoot-mock";
import { Command, serverState, withUser } from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineMailModels();
test("navigate to sub channel", async () => {
mockDate("2025-01-01 12:00:00", +1);
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
// Should access sub-thread after its creation.
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
await click("button[title='Threads']");
await click("button[aria-label='Create Thread']");
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
// Should access sub-thread when clicking on the menu.
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
await click("button[title='Threads']");
await click(".o-mail-SubChannelPreview", { text: "New Thread" });
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
// Should access sub-thread when clicking on the notification.
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
await contains(".o-mail-NotificationMessage", {
text: `${serverState.partnerName} started a thread: New Thread.1:00 PM`,
});
await click(".o-mail-NotificationMessage a", { text: "New Thread" });
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
});
test("can manually unpin a sub-thread", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
// Open thread so this is pinned
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
await click("button[title='Threads']");
await click("button[aria-label='Create Thread']");
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
await click("[title='Thread Actions']");
await click(".o-dropdown-item:contains('Unpin Conversation')");
await contains(".o-mail-DiscussSidebar-item", { text: "New Thread", count: 0 });
});
test("create sub thread from existing message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
model: "discuss.channel",
res_id: channelId,
body: "<p>Selling a training session and selling the products after the training session is more efficient.</p>",
});
await start();
await openDiscuss(channelId);
await click(".o-mail-Message-actions [title='Expand']");
await click(".o-dropdown-item:contains('Create Thread')");
await contains(".o-mail-DiscussContent-threadName", {
value: "Selling a training session and",
});
await contains(".o-mail-Message", {
text: "Selling a training session and selling the products after the training session is more efficient.",
});
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
await click(".o-mail-Message-actions [title='Expand']");
await contains(".o-dropdown-item:contains('Create Thread')", { count: 0 });
await contains(".o-mail-SubChannelPreview:contains('Selling a training session and')");
await click(".o-mail-SubChannelPreview:contains('Selling a training session and')");
await contains(".o-mail-DiscussContent-threadName", {
value: "Selling a training session and",
});
await contains(".o-mail-SubChannelPreview:contains('Selling a training session and')", {
count: 0,
});
});
test("should allow creating a thread from an existing thread", async () => {
mockDate("2025-01-01 12:00:00", +1);
const pyEnv = await startServer();
const parent_channel_id = pyEnv["discuss.channel"].create({ name: "General" });
const sub_channel_id = pyEnv["discuss.channel"].create({
name: "sub channel",
parent_channel_id: parent_channel_id,
});
pyEnv["mail.message"].create({
model: "discuss.channel",
res_id: sub_channel_id,
body: "<p>hello alex</p>",
});
await start();
await openDiscuss(sub_channel_id);
await click(".o-mail-Message-actions [title='Expand']");
await click(".o-dropdown-item:contains('Create Thread')");
await contains(".o-mail-DiscussContent-threadName", { value: "hello alex" });
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
await contains(".o-mail-NotificationMessage", {
text: `${serverState.partnerName} started a thread: hello alex.1:00 PM`,
});
});
test("create sub thread from existing message (slow network)", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
model: "discuss.channel",
res_id: channelId,
body: "<p>Selling a training session and selling the products after the training session is more efficient.</p>",
});
const createSubChannelDef = new Deferred();
onRpcAfter("/discuss/channel/sub_channel/create", async () => await createSubChannelDef);
await start();
await openDiscuss(channelId);
await click(".o-mail-Message-actions [title='Expand']");
await click(".o-dropdown-item:contains('Create Thread')");
await animationFrame();
createSubChannelDef.resolve();
await contains(".o-mail-DiscussContent-threadName", {
value: "Selling a training session and",
});
await contains(".o-mail-Message", {
text: "Selling a training session and selling the products after the training session is more efficient.",
});
});
test("create sub thread from sub-thread list", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("button[title='Threads']");
await contains(".o-mail-SubChannelList", { text: "This channel has no thread yet." });
await click("button[aria-label='Create Thread']");
await contains(".o-mail-DiscussContent-threadName", { value: "New Thread" });
await click(".o-mail-DiscussSidebarChannel", { name: "General" });
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
await click(".o-mail-DiscussContent-header button[title='Threads']");
await insertText(
".o-mail-ActionPanel:has(.o-mail-SubChannelList) .o_searchview_input",
"MyEpicThread"
);
await click("button[aria-label='Search button']");
await contains(".o-mail-SubChannelList", { text: 'No thread named "MyEpicThread"' });
await click("button[aria-label='Create Thread']");
await contains(".o-mail-DiscussContent-threadName", { value: "MyEpicThread" });
});
test("'Thread' menu available in threads", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
});
const subChannelID = pyEnv["discuss.channel"].create({
name: "ThreadOne",
parent_channel_id: channelId,
});
await start();
await openDiscuss(subChannelID);
await click(".o-mail-DiscussSidebar-item", { text: "ThreadOne" });
await contains(".o-mail-DiscussContent-threadName", { value: "ThreadOne" });
await click("button[title='Threads']");
await insertText(".o-mail-ActionPanel input[placeholder='Search by name']", "ThreadTwo");
await click(".o-mail-ActionPanel button", { text: "Create" });
await click(".o-mail-DiscussSidebar-item", { text: "ThreadTwo" });
});
test("sub thread is available for channel and group, not for chat", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
});
pyEnv["discuss.channel"].create({
name: "Group",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "group",
});
pyEnv["discuss.channel"].create({
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("button[title='Threads']");
await insertText(
".o-mail-ActionPanel input[placeholder='Search by name']",
"Sub thread for channel"
);
await click(".o-mail-ActionPanel button", { text: "Create" });
await click(".o-mail-DiscussSidebar-item", { text: "Sub thread for channel" });
await click(".o-mail-DiscussSidebarChannel", { text: "Group" });
await contains(".o-mail-DiscussContent-threadName", { value: "Group" });
await click("button[title='Threads']");
await insertText(
".o-mail-ActionPanel input[placeholder='Search by name']",
"Sub thread for group"
);
await click(".o-mail-ActionPanel button", { text: "Create" });
await click(".o-mail-DiscussSidebar-item", { text: "Sub thread for group" });
await click(".o-mail-DiscussSidebarChannel", { text: "Demo" });
await contains("button[title='Threads']", { count: 0 });
});
test("mention suggestions in thread match channel restrictions", async () => {
const pyEnv = await startServer();
const groupId = pyEnv["res.groups"].create({ name: "testGroup" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
group_public_id: groupId,
});
pyEnv["discuss.channel"].create({
name: "Thread",
parent_channel_id: channelId,
});
pyEnv["res.users"].write(serverState.userId, { group_ids: [Command.link(groupId)] });
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ email: "p1@odoo.com", name: "p1" },
{ email: "p2@odoo.com", name: "p2" },
]);
pyEnv["res.users"].create([
{ partner_id: partnerId_1, group_ids: [Command.link(groupId)] },
{ partner_id: partnerId_2 },
]);
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussSidebar-item.o-active:contains('General')");
await insertText(".o-mail-Composer-input", "@");
await contains(".o-mail-Composer-suggestion", { count: 2 });
await contains(".o-mail-Composer-suggestion", { text: "Mitchell Admin" });
await contains(".o-mail-Composer-suggestion", { text: "p1" });
await click(".o-mail-DiscussSidebar-item:contains('Thread')");
await contains(".o-mail-DiscussSidebar-item.o-active:contains('Thread')");
await insertText(".o-mail-Composer-input", "@");
await contains(".o-mail-Composer-suggestion", { count: 2 });
await contains(".o-mail-Composer-suggestion", { text: "Mitchell Admin" });
await contains(".o-mail-Composer-suggestion", { text: "p1" });
});
test("sub-thread is visually muted when mute is active", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-mail-DiscussContent-threadName", { value: "General" });
await click("button[title='Threads']");
await click("button[aria-label='Create Thread']");
await contains(".opacity-50.o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
await click(".o-mail-DiscussSidebar-item:contains('New Thread')");
await click("button[title='Notification Settings']");
await click("button:contains('Mute Conversation')");
await click("button:contains('Until I turn it back on')");
await contains(".opacity-50.o-mail-DiscussSidebar-item:contains('New Thread')");
});
test("muted channel hides sub-thread unless channel is selected or thread has unread messages", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const partnerId2 = pyEnv["res.partner"].create({ email: "p1@odoo.com", name: "p1" });
const userId2 = pyEnv["res.users"].create({ name: "User 2", partner_id: partnerId2 });
const partnerId = serverState.partnerId;
const subChannelId = pyEnv["discuss.channel"].create({
name: "New Thread",
parent_channel_id: channelId,
channel_member_ids: [
Command.create({ partner_id: partnerId }),
Command.create({ partner_id: partnerId2 }),
],
});
pyEnv["discuss.channel"].create({ name: "Other" });
await start();
await openDiscuss(channelId);
await click(".o-mail-DiscussSidebar-item:contains('General')");
await click("button[title='Notification Settings']");
await click("button:contains('Mute Conversation')");
await click("button:contains('Until I turn it back on')");
await click(".o-mail-DiscussSidebar-item:contains('Other')");
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
await click(".o-mail-DiscussSidebar-item:contains('General')");
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')");
await click(".o-mail-DiscussSidebar-item:contains('Other')");
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')", { count: 0 });
withUser(userId2, () =>
rpc("/mail/message/post", {
post_data: { body: "Some message", message_type: "comment" },
thread_id: subChannelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-DiscussSidebar-item:contains('New Thread')");
});
test("show notification when clicking on deleted thread", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "Test Channel" });
const activeThreadId = pyEnv["discuss.channel"].create({
name: "Message 1",
parent_channel_id: channelId,
});
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: `<div class="o_mail_notification"> started a thread:<a href="#" class="o_channel_redirect" data-oe-id="${activeThreadId}" data-oe-model="discuss.channel">Message 1</a></div>`,
message_type: "notification",
model: "discuss.channel",
res_id: channelId,
});
pyEnv["discuss.channel"].unlink(activeThreadId);
await start();
await openDiscuss(channelId);
await click(".o-mail-NotificationMessage a", { text: "Message 1" });
await contains(".o_notification:has(.o_notification_bar.bg-danger)", {
text: "This thread is no longer available.",
});
});
test("Can delete channel thread as author of thread", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const subChannelID = pyEnv["discuss.channel"].create({
name: "test thread",
parent_channel_id: channelId,
});
await start();
await openDiscuss(subChannelID);
await contains(".o-mail-DiscussContent-threadName[title='test thread']");
await click(".o-mail-DiscussSidebar-item:contains('test thread') [title='Thread Actions']");
await click(".o-dropdown-item:contains('Delete Thread')");
await click(".modal button:contains('Delete Thread')");
await contains(".o-mail-DiscussContent-threadName[title='General']");
await contains(
`.o-mail-NotificationMessage :text(Mitchell Admin deleted the thread "test thread")`
);
});
test("can mention all group chat members inside its sub-thread", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Lilibeth" });
const groupChannelId = pyEnv["discuss.channel"].create({
name: "Our channel",
channel_type: "group",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
const groupSubChannelId = pyEnv["discuss.channel"].create({
name: "New Thread",
parent_channel_id: groupChannelId,
channel_member_ids: [Command.create({ partner_id: serverState.partnerId })],
});
await start();
await openDiscuss(groupSubChannelId);
await insertText(".o-mail-Composer-input", "@");
await contains(".o-mail-Composer-suggestion", { count: 2 });
});

View file

@ -0,0 +1,319 @@
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
import {
click,
contains,
defineMailModels,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { beforeEach, describe, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { Command, getService, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { Composer } from "@mail/core/common/composer";
import { press } from "@odoo/hoot-dom";
describe.current.tags("desktop");
defineMailModels();
beforeEach(() => {
// Simulate real user interactions
patchWithCleanup(Composer.prototype, {
isEventTrusted() {
return true;
},
});
});
test('[text composer] display command suggestions on typing "/"', async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await insertText(".o-mail-Composer-input", "/");
await contains(".o-mail-Composer-suggestionList .o-open");
});
test.tags("html composer");
test("display command suggestions on typing '/'", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await focus(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "/");
await contains(".o-mail-Composer-suggestionList .o-open");
});
test("[text composer] use a command for a specific channel type", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat" });
await start();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await contains(".o-mail-Composer-input", { value: "" });
await insertText(".o-mail-Composer-input", "/");
await click(".o-mail-Composer-suggestion strong", { text: "who" });
await contains(".o-mail-Composer-input", { value: "/who " });
});
test.tags("html composer");
test("use a command for a specific channel type", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ channel_type: "chat" });
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await focus(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "/");
await click(".o-mail-Composer-suggestion strong", { text: "who" });
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "/who" });
});
test("[text composer] command suggestion should only open if command is the first character", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await contains(".o-mail-Composer-input", { value: "" });
await insertText(".o-mail-Composer-input", "bluhbluh ");
await contains(".o-mail-Composer-input", { value: "bluhbluh " });
await insertText(".o-mail-Composer-input", "/");
// weak test, no guarantee that we waited long enough for the potential list to open
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
});
test.tags("html composer");
test("command suggestion should only open if command is the first character", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await focus(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "bluhbluh");
await contains(".o-mail-Composer-html.odoo-editor-editable", { text: "bluhbluh" });
await htmlInsertText(editor, "/");
// weak test, no guarantee that we waited long enough for the potential list to open
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
});
test("Sort partner suggestions by recent chats", 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 [partner_1, partner_2, partner_3] = pyEnv["res.partner"].create([
{ name: "User 1" },
{ name: "User 2" },
{ name: "User 3" },
]);
pyEnv["res.users"].create([
{ partner_id: partner_1 },
{ partner_id: partner_2 },
{ partner_id: partner_3 },
]);
pyEnv["discuss.channel"].create([
{
name: "General",
channel_type: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partner_1 }),
Command.create({ partner_id: partner_2 }),
Command.create({ partner_id: partner_3 }),
],
},
{
channel_member_ids: [
Command.create({
last_interest_dt: "2023-01-01 00:00:00",
partner_id: serverState.partnerId,
}),
Command.create({ partner_id: partner_1 }),
],
channel_type: "chat",
},
{
channel_member_ids: [
Command.create({
last_interest_dt: "2023-01-01 00:00:10",
partner_id: serverState.partnerId,
}),
Command.create({ partner_id: partner_2 }),
],
channel_type: "chat",
},
{
channel_member_ids: [
Command.create({
last_interest_dt: "2023-01-01 00:00:20",
partner_id: serverState.partnerId,
}),
Command.create({ partner_id: partner_3 }),
],
channel_type: "chat",
},
]);
await start();
await openDiscuss();
await click(".o-mail-DiscussSidebarChannel", { text: "User 2" });
await insertText(".o-mail-Composer-input", "This is a test");
await press("Enter");
await contains(".o-mail-Message-content", { text: "This is a test" });
await click(".o-mail-DiscussSidebarChannel", { text: "General" });
await contains(
".o-mail-DiscussSidebarCategory-chat + .o-mail-DiscussSidebarChannel-container:text(User 2)"
);
await insertText(".o-mail-Composer-input[placeholder='Message #General…']", "@");
await insertText(".o-mail-Composer-input", "User");
await contains(".o-mail-Composer-suggestion strong", { count: 3 });
await contains(":nth-child(1 of .o-mail-Composer-suggestion) strong", { text: "User 2" });
await contains(":nth-child(2 of .o-mail-Composer-suggestion) strong", { text: "User 3" });
await contains(":nth-child(3 of .o-mail-Composer-suggestion) strong", { text: "User 1" });
});
test("mention suggestion are shown after deleting a character", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@John D");
await contains(".o-mail-Composer-suggestion strong", { text: "John Doe" });
await insertText(".o-mail-Composer-input", "a");
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "John D" });
// Simulate pressing backspace
const textarea = document.querySelector(".o-mail-Composer-input");
textarea.value = textarea.value.slice(0, -1);
await contains(".o-mail-Composer-suggestion strong", { text: "John Doe" });
});
test("[text composer] command suggestion are shown after deleting a character", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/he");
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
await insertText(".o-mail-Composer-input", "e");
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "help" });
// Simulate pressing backspace
const textarea = document.querySelector(".o-mail-Composer-input");
textarea.value = textarea.value.slice(0, -1);
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
});
test.tags("html composer");
test("command suggestion are shown after deleting a character", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-suggestionList");
await contains(".o-mail-Composer-suggestionList .o-open", { count: 0 });
await focus(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "/he");
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
await htmlInsertText(editor, "e");
await contains(".o-mail-Composer-suggestion strong", { count: 0, text: "help" });
await press("Backspace");
await contains(".o-mail-Composer-suggestion strong", { text: "help" });
});
test("mention suggestion displays OdooBot before archived partners", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jane", active: false });
const channelId = pyEnv["discuss.channel"].create({
name: "Our channel",
channel_type: "group",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
Command.create({ partner_id: serverState.odoobotId }),
],
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "@");
await contains(".o-mail-Composer-suggestion", { count: 3 });
await contains(".o-mail-Composer-suggestion", {
text: "Mitchell Admin",
before: [
".o-mail-Composer-suggestion",
{
text: "OdooBot",
before: [".o-mail-Composer-suggestion", { text: "Jane" }],
},
],
});
});

View file

@ -0,0 +1,175 @@
import {
click,
contains,
defineMailModels,
insertText,
openDiscuss,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { Command, serverState } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("can open DM from @username in command palette", async () => {
const pyEnv = await startServer();
const marioUid = pyEnv["res.users"].create({ name: "Mario" });
pyEnv["res.partner"].create({ name: "Mario", user_ids: [marioUid] });
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@");
await insertText("input[placeholder='Search a conversation']", "Mario");
await click(".o_command.focused:has(.oi-user)", { text: "Mario" });
await contains(".o-mail-ChatWindow", { text: "Mario" });
});
test("can open channel from @channel_name in command palette", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({
name: "general",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-02 10:00:00", // same last interest to sort by id
}),
],
});
pyEnv["discuss.channel"].create({
name: "project",
channel_member_ids: [
Command.create({
partner_id: serverState.partnerId,
last_interest_dt: "2021-01-02 10:00:00", // same last interest to sort by id
}),
],
});
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@");
await contains(".o_command", { count: 6 });
await contains(".o_command:eq(0):has(.fa-hashtag)", { text: "project" });
await contains(".o_command:eq(1):has(.fa-hashtag)", { text: "general" });
await contains(".o_command:has(.oi-user)", { text: "OdooBot" });
await contains(".o_command:has(.oi-user)", { text: "Mitchell Admin" }); // self-conversation
await contains(".o_command", { text: "Create Channel" });
await contains(".o_command", { text: "Create Chat" });
await click(".o_command.focused:has(.fa-hashtag)", { text: "project" });
await contains(".o-mail-ChatWindow", { text: "project" });
});
test("Conversation mentions in the command palette with @", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "group",
});
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
model: "discuss.channel",
res_id: channelId,
body: "@Mitchell Admin",
needaction: true,
});
pyEnv["mail.notification"].create({
mail_message_id: messageId,
notification_type: "inbox",
res_partner_id: serverState.partnerId,
});
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@", { replace: true });
await contains(".o_command_palette .o_command_category", {
contains: [
["span.fw-bold", { text: "Mentions" }],
[".o_command.focused .o_command_name", { text: "Mitchell Admin and Mario" }],
],
});
// can also make self conversation
await contains(".o_command_palette .o_command_category", {
contains: [[".o_command_name", { text: "Mitchell Admin" }]],
});
await click(".o_command.focused");
await contains(".o-mail-ChatWindow", { text: "Mitchell Admin and Mario" });
});
test("Max 3 most recent conversations in command palette of Discuss", async () => {
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "channel_1" });
pyEnv["discuss.channel"].create({ name: "channel_2" });
pyEnv["discuss.channel"].create({ name: "channel_3" });
pyEnv["discuss.channel"].create({ name: "channel_4" });
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@", { replace: true });
await contains(".o_command_palette .o_command_category", {
contains: [
["span.fw-bold", { text: "Recent" }],
[".o_command", { count: 3 }],
],
});
});
test("only partners with dedicated users will be displayed in command palette", async () => {
const pyEnv = await startServer();
const demoUid = pyEnv["res.users"].create({ name: "Demo" });
pyEnv["res.partner"].create({ name: "Demo", user_ids: [demoUid] });
pyEnv["res.partner"].create({ name: "Portal" });
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@");
await contains(".o_command_name", { count: 5 });
await contains(".o_command_name", { text: "Demo" });
await contains(".o_command_name", { text: "OdooBot" });
await contains(".o_command_name", { text: "Mitchell Admin" }); // self-conversation
await contains(".o_command_name", { text: "Create Channel" });
await contains(".o_command_name", { text: "Create Chat" });
await contains(".o_command_name", { text: "Portal", count: 0 });
});
test("hide conversations in recent if they have mentions", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: serverState.odoobotId }),
],
channel_type: "chat",
});
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
model: "discuss.channel",
res_id: channelId,
body: "@OdooBot",
});
await start();
triggerHotkey("control+k");
await insertText(".o_command_palette_search input", "@", { replace: true });
await contains(".o_command_category span.fw-bold", { text: "Mentions" });
await contains(".o_command_palette .o_command_category .o_command_name", {
text: "OdooBot",
count: 1,
});
});
test("Ctrl-K opens @ command palette in discuss app", async () => {
await start();
await openDiscuss();
triggerHotkey("control+k");
await contains(".o_command_palette_search", { text: "@" });
});
test("Can create group chat from ctrl-k without any user selected", async () => {
await start();
await openDiscuss();
triggerHotkey("control+k");
await click(".o_command_name:contains(Create Chat)");
await click(".modal-footer > .btn:contains(Create Group Chat)");
await contains(".o-mail-DiscussSidebarChannel-itemName", { text: "Mitchell Admin" });
});

View file

@ -0,0 +1,30 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { asyncStep, mockService, waitForSteps } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("Channel subscription is renewed when channel is manually added", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General", channel_member_ids: [] });
await start();
mockService("bus_service", {
forceUpdateChannels() {
asyncStep("update-channels");
},
});
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("[title='Invite People']");
await click(".o-discuss-ChannelInvitation-selectable", { text: "Mitchell Admin" });
await click("[title='Invite']:enabled");
await waitForSteps(["update-channels"]);
});

View file

@ -0,0 +1,244 @@
import {
click,
contains,
defineMailModels,
insertText,
listenStoreFetch,
onRpcBefore,
openDiscuss,
patchUiSize,
SIZES,
start,
startServer,
STORE_FETCH_ROUTES,
triggerHotkey,
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";
import { pick } from "@web/core/utils/objects";
describe.current.tags("desktop");
defineMailModels();
test("can create a new channel", async () => {
const pyEnv = await startServer();
onRpcBefore((route, args) => {
if (
(route.startsWith("/mail") || route.startsWith("/discuss")) &&
!STORE_FETCH_ROUTES.includes(route)
) {
asyncStep(`${route} - ${JSON.stringify(args)}`);
}
});
listenStoreFetch(undefined, { logParams: ["/discuss/create_channel"] });
await start();
await openDiscuss();
await waitStoreFetch([
"failures",
"systray_get_activities",
"init_messaging",
"channels_as_member",
]);
await contains(".o-mail-Discuss");
await contains(".o-mail-DiscussSidebar-item", { text: "abc", count: 0 });
await click("input[placeholder='Search conversations']");
await insertText("input[placeholder='Search a conversation']", "abc");
await waitForSteps([`/discuss/search - {"term":""}`, `/discuss/search - {"term":"abc"}`]);
await click("a", { text: "Create Channel" });
await contains(".o-mail-DiscussSidebar-item", { text: "abc" });
await contains(".o-mail-Message", { count: 0 });
const [channelId] = pyEnv["discuss.channel"].search([["name", "=", "abc"]]);
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
["channel_id", "=", channelId],
["partner_id", "=", serverState.partnerId],
]);
await waitStoreFetch([["/discuss/create_channel", { name: "abc" }]], {
stepsAfter: [
`/discuss/channel/messages - ${JSON.stringify({
channel_id: channelId,
fetch_params: { limit: 60, around: selfMember.new_message_separator },
})}`,
`/discuss/channel/members - ${JSON.stringify({
channel_id: channelId,
known_member_ids: [selfMember.id],
})}`,
],
});
});
test("can make a DM chat", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Mario" });
pyEnv["res.users"].create({ partner_id: partnerId });
onRpcBefore((route, args) => {
if (
(route.startsWith("/mail") || route.startsWith("/discuss")) &&
!STORE_FETCH_ROUTES.includes(route)
) {
asyncStep(`${route} - ${JSON.stringify(args)}`);
}
});
onRpc((params) => {
if (params.model === "discuss.channel" && ["search_read"].includes(params.method)) {
asyncStep(
`${params.route} - ${JSON.stringify(
pick(params, "args", "kwargs", "method", "model")
)}`
);
}
});
listenStoreFetch(undefined, {
logParams: ["/discuss/get_or_create_chat"],
});
await start();
await waitStoreFetch(["failures", "systray_get_activities", "init_messaging"]);
await openDiscuss();
await waitStoreFetch(["channels_as_member"]);
await contains(".o-mail-Discuss");
await contains(".o-mail-DiscussSidebar-item", { text: "Mario", count: 0 });
await click("input[placeholder='Search conversations']");
await contains(".o_command_name", { count: 5 });
await insertText("input[placeholder='Search a conversation']", "mario");
await contains(".o_command_name", { count: 3 });
await click(".o_command_name", { text: "Mario" });
await contains(".o-mail-DiscussSidebar-item", { text: "Mario" });
await contains(".o-mail-Message", { count: 0 });
const [channelId] = pyEnv["discuss.channel"].search([["name", "=", "Mario, Mitchell Admin"]]);
await waitStoreFetch([["/discuss/get_or_create_chat", { partners_to: [partnerId] }]], {
stepsAfter: [
`/discuss/channel/messages - ${JSON.stringify({
channel_id: channelId,
fetch_params: { limit: 60, around: 0 },
})}`,
],
stepsBefore: [`/discuss/search - {"term":""}`, `/discuss/search - {"term":"mario"}`],
});
});
test("can create a group chat conversation", async () => {
const pyEnv = await startServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ name: "Mario" },
{ name: "Luigi" },
]);
pyEnv["res.users"].create([{ partner_id: partnerId_1 }, { partner_id: partnerId_2 }]);
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await click("a", { text: "Create Chat" });
await click("li", { text: "Mario" });
await click("li", { text: "Luigi" });
await click(".btn", { text: "Create Group Chat" });
await contains(".o-mail-DiscussSidebarChannel");
await contains(".o-mail-Message", { count: 0 });
});
test("mobile chat search should allow to create group chat", async () => {
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss();
await contains("button.active", { text: "Notifications" });
await click("button", { text: "Chats" });
await contains(".o-mail-DiscussSearch-inputContainer");
});
test("Chat is pinned on other tabs when joined", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jerry Golay" });
pyEnv["res.users"].create({ partner_id: partnerId });
const env1 = await start({ asTab: true });
const env2 = await start({ asTab: true });
await openDiscuss(undefined, { target: env1 });
await openDiscuss(undefined, { target: env2 });
await click(`${env1.selector} input[placeholder='Search conversations']`);
await contains(`${env1.selector} .o_command_name`, { count: 5 });
await insertText(`${env1.selector} input[placeholder='Search a conversation']`, "Jer");
await contains(`${env1.selector} .o_command_name`, { count: 3 });
await click(`${env1.selector} .o_command_name`, { text: "Jerry Golay" });
await contains(`${env1.selector} .o-mail-DiscussSidebar-item`, { text: "Jerry Golay" });
await contains(`${env2.selector} .o-mail-DiscussSidebar-item`, { text: "Jerry Golay" });
});
test("Auto-open OdooBot chat when opening discuss for the first time", async () => {
// Odoobot chat has onboarding for using Discuss app.
// We assume pinned odoobot chat without any message seen means user just started using Discuss app.
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: serverState.odoobotId }),
],
channel_type: "chat",
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussContent-threadName", { value: "OdooBot" });
});
test("no conversation selected when opening non-existing channel in discuss", async () => {
await startServer();
await start();
await openDiscuss(200); // non-existing id
await contains("h4", { text: "No conversation selected." });
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-down");
await click(".o-mail-DiscussSidebar .btn", { text: "Channels" }); // check no crash
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-right");
});
test("can access portal partner profile from avatar popover", async () => {
const pyEnv = await startServer();
const joelPartnerId = pyEnv["res.partner"].create({
name: "Joel",
user_ids: [Command.create({ name: "Joel", share: true })],
});
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: joelPartnerId }),
],
});
pyEnv["mail.message"].create({
author_id: joelPartnerId,
body: "Hello!",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await click(".o-mail-Message-avatar", {
parent: [".o-mail-Message", { text: "Joel" }],
});
await contains(".o_avatar_card", { text: "Joel" });
await click("button", { text: "View Profile" });
await contains(".o_form_view");
await contains(".o_field_widget[name='name'] .o_input", { value: "Joel" });
});
test("Preserve letter case and accents when creating channel from sidebar", async () => {
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await insertText("input[placeholder='Search a conversation']", "Crème brûlée Fan Club");
await click("a", { text: "Create Channel" });
await contains(".o-mail-DiscussContent-threadName", { value: "Crème brûlée Fan Club" });
});
test("Create channel must have a name", async () => {
await start();
await openDiscuss();
await click("input[placeholder='Search conversations']");
await click("a", { text: "Create Channel" });
await click("input[placeholder='Channel name']");
await triggerHotkey("Enter");
await contains(".invalid-feedback", { text: "Channel must have a name." });
});

View file

@ -0,0 +1,334 @@
import {
SIZES,
click,
contains,
defineMailModels,
insertText,
openDiscuss,
patchUiSize,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, disableAnimations, expect, mockPermission, mockTouch, test } from "@odoo/hoot";
import {
Command,
contains as webContains,
getService,
serverState,
swipeLeft,
swipeRight,
withUser,
} from "@web/../tests/web_test_helpers";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineMailModels();
test("can make DM chat in mobile", async () => {
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Gandalf" });
pyEnv["res.users"].create({ partner_id: partnerId });
await start();
await openDiscuss();
await contains("button.active", { text: "Notifications" });
await click("button", { text: "Chats" });
await click(".o-mail-DiscussSearch-inputContainer");
await contains(".o_command_name", { count: 5 });
await insertText("input[placeholder='Search a conversation']", "Gandalf");
await contains(".o_command_name", { count: 3 });
await click(".o_command_name", { text: "Gandalf" });
await contains(".o-mail-ChatWindow", { text: "Gandalf" });
});
test("can search channel in mobile", async () => {
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
pyEnv["discuss.channel"].create({ name: "Gryffindors" });
await start();
await openDiscuss();
await contains("button.active", { text: "Notifications" });
await click("button", { text: "Channels" });
await click(".o-mail-DiscussSearch-inputContainer");
await contains(".o_command_name", { count: 5 });
await insertText("input[placeholder='Search a conversation']", "Gryff");
await contains(".o_command_name", { count: 3 });
await click(".o_command_name", { text: "Gryffindors" });
await contains(".o-mail-ChatWindow div[title='Gryffindors']");
});
test("can make new channel in mobile", async () => {
patchUiSize({ size: SIZES.SM });
await start();
await openDiscuss();
await contains("button.active", { text: "Notifications" });
await click("button", { text: "Channels" });
await click(".o-mail-DiscussSearch-inputContainer");
await insertText("input[placeholder='Search a conversation']", "slytherins");
await click("a", { text: "Create Channel" });
await contains(".o-mail-ChatWindow", { text: "slytherins" });
});
test("new message opens the @ command palette", async () => {
await start();
await click(".o_menu_systray .dropdown-toggle i[aria-label='Messages']");
await click(".o-mail-MessagingMenu button", { text: "New Message" });
await contains(".o_command_palette_search .o_namespace", { text: "@" });
await contains(".o_command_palette input[placeholder='Search a conversation']");
});
test("channel preview show deleted messages", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "<p>before last</p>",
model: "discuss.channel",
res_id: channelId,
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "<p></p>",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { text: "before last" });
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
await contains(".o-mail-NotificationItem-text", {
text: "Demo: This message has been removed",
});
});
test("deleted message should not show parent message reference and mentions", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const messageId = pyEnv["mail.message"].create({
body: "<p>Parent Message</p>",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
pyEnv["mail.message"].create({
body: "<p>reply message</p>",
message_type: "comment",
model: "discuss.channel",
parent_id: messageId,
partner_ids: [serverState.partnerId],
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-MessageInReply", { text: "Parent Message" });
await webContains(
".o-mail-Message:has(.o-mail-Message-bubble.o-orange):contains('reply message')"
).hover();
await webContains(
".o-mail-Message:has(.o-mail-Message-bubble.o-orange):contains('reply message') [title='Expand']"
).click();
await click(".o-mail-Message-moreMenu .o-dropdown-item:contains(Delete)");
await click(".o_dialog button:contains(Delete)");
await contains(".o-mail-Message:not(:has(.o-mail-Message-bubble.o-orange))", {
text: "This message has been removed",
});
await contains(".o-mail-MessageInReply", { count: 0 });
});
test("channel preview ignores transient message", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "<p>test</p>",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "/who");
await click(".o-mail-Composer button[title='Send']:enabled");
await contains(".o_mail_notification", { text: "You are alone in this channel." });
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
await contains(".o-mail-NotificationItem-text", { text: "Demo: test" });
});
test("channel preview ignores messages from the past", async () => {
disableAnimations();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const messageId = pyEnv["mail.message"].create({
body: "first message",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
for (let i = 0; i < 100; i++) {
pyEnv["mail.message"].create({
body: `message ${i}`,
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
const newestMessageId = pyEnv["mail.message"].create({
body: "last message",
message_type: "comment",
model: "discuss.channel",
parent_id: messageId,
res_id: channelId,
});
const [selfMember] = pyEnv["discuss.channel.member"].search_read([
["partner_id", "=", serverState.partnerId],
["channel_id", "=", channelId],
]);
pyEnv["discuss.channel.member"].write([selfMember.id], {
new_message_separator: newestMessageId + 1,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 30 });
await contains(".o-mail-Message-content", { text: "last message" });
await contains(".o-mail-Thread", { scroll: "bottom" });
await click(".o-mail-MessageInReply-content", { text: "first message" });
await contains(".o-mail-Message", { count: 31 });
await contains(".o-mail-Message-content", { text: "first message" });
await contains(".o-mail-Message-content", { text: "last message", count: 0 });
await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Messages'])");
await contains(".o-mail-NotificationItem-text", { text: "You: last message" });
withUser(serverState.userId, () =>
rpc("/mail/message/post", {
post_data: { body: "it's a good idea", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-NotificationItem-text", { text: "You: it's a good idea" });
});
test("counter is taking into account non-fetched channels", async () => {
mockPermission("notifications", "denied");
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ message_unread_counter: 1, partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "first message",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await contains(".o-mail-MessagingMenu-counter", { text: "1" });
expect(
Boolean(getService("mail.store").Thread.get({ model: "discuss.channel", id: channelId }))
).toBe(false);
});
test("counter is updated on receiving message on non-fetched channels", async () => {
mockPermission("notifications", "denied");
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Jane" });
const userId = pyEnv["res.users"].create({ partner_id: partnerId });
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "first message",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await contains(".o_menu_systray .dropdown-toggle i[aria-label='Messages']");
await contains(".o-mail-MessagingMenu-counter", { count: 0 });
expect(
Boolean(getService("mail.store").Thread.get({ model: "discuss.channel", id: channelId }))
).toBe(false);
withUser(userId, () =>
rpc("/mail/message/post", {
post_data: { body: "good to know", message_type: "comment" },
thread_id: channelId,
thread_model: "discuss.channel",
})
);
await contains(".o-mail-MessagingMenu-counter", { text: "1" });
});
test("can use notification item swipe actions", async () => {
mockTouch(true);
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo", email: "demo@odoo.com" });
const channelId = pyEnv["discuss.channel"].create({
channel_type: "chat",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
pyEnv["mail.message"].create({
author_id: partnerId,
body: "A message",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss();
await contains("button.active", { text: "Notifications" });
await click("button", { text: "Chats" });
await contains(".o-mail-NotificationItem .o-mail-NotificationItem-badge:contains(1)");
await swipeRight(".o_actionswiper"); // marks as read
await contains(".o-mail-NotificationItem-badge", { count: 0 });
await swipeLeft(".o_actionswiper"); // unpins
await contains(".o-mail-NotificationItem", { count: 0 });
});
test("counter does not double count channel needaction messages", async () => {
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({
name: "General",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
body: "Hey @Mitchell Admin",
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
pyEnv["mail.notification"].create({
mail_message_id: messageId,
notification_status: "sent",
notification_type: "inbox",
res_partner_id: serverState.partnerId,
});
await start();
await click(".o_menu_systray i[aria-label='Messages']"); // fetch channels
await contains(".o-mail-NotificationItem", { text: "General" }); // ensure channels fetched
await contains(".o-mail-MessagingMenu-counter:text('1')");
});

View file

@ -0,0 +1,47 @@
import { waitForChannels } from "@bus/../tests/bus_test_helpers";
import {
click,
contains,
defineMailModels,
insertText,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { asyncStep, Command, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineMailModels();
test("unknown channel can be displayed and interacted with", async () => {
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 })],
channel_type: "channel",
name: "Not So Secret",
});
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-DiscussSidebarChannel", { count: 0 });
await openDiscuss(channelId);
await waitForChannels([`discuss.channel_${channelId}`]);
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Not So Secret" });
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: "Not So Secret" });
await click("[title='Channel Actions']");
await click(".o-dropdown-item:contains('Leave Channel')");
await click("button", { text: "Leave Conversation" });
await contains(".o-mail-DiscussSidebarChannel", { count: 0 });
});

View file

@ -0,0 +1,145 @@
import { AWAY_DELAY } from "@mail/core/common/im_status_service";
import { defineMailModels, start, startServer } from "@mail/../tests/mail_test_helpers";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { advanceTime, freezeTime } from "@odoo/hoot-dom";
import { registry } from "@web/core/registry";
import {
asyncStep,
makeMockEnv,
mockService,
patchWithCleanup,
restoreRegistry,
serverState,
waitForSteps,
} from "@web/../tests/web_test_helpers";
defineMailModels();
beforeEach(freezeTime);
describe.current.tags("headless");
test("update presence if IM status changes to offline while this device is online", async () => {
mockService("bus_service", { send: (type) => asyncStep(type) });
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
await start();
await waitForSteps(["update_presence"]);
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
presence_status: "offline",
im_status: "offline",
partner_id: serverState.partnerId,
});
await waitForSteps(["update_presence"]);
});
test("update presence if IM status changes to away while this device is online", async () => {
mockService("bus_service", { send: (type) => asyncStep(type) });
localStorage.setItem("presence.lastPresence", Date.now());
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
await start();
await waitForSteps(["update_presence"]);
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
presence_status: "away",
im_status: "away",
partner_id: serverState.partnerId,
});
await waitForSteps(["update_presence"]);
});
test("do not update presence if IM status changes to away while this device is away", async () => {
mockService("bus_service", { send: (type) => asyncStep(type) });
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "away" });
await start();
await waitForSteps(["update_presence"]);
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
presence_status: "away",
im_status: "away",
partner_id: serverState.partnerId,
});
await waitForSteps([]);
});
test("do not update presence if other user's IM status changes to away", async () => {
mockService("bus_service", { send: (type) => asyncStep(type) });
localStorage.setItem("presence.lastPresence", Date.now());
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
await start();
await waitForSteps(["update_presence"]);
pyEnv["bus.bus"]._sendone(serverState.partnerId, "bus.bus/im_status_updated", {
presence_status: "away",
im_status: "away",
partner_id: serverState.publicPartnerId,
});
await waitForSteps([]);
});
test("update presence when user comes back from away", async () => {
mockService("bus_service", {
send: (type, payload) => {
if (type === "update_presence") {
asyncStep(payload.inactivity_period);
}
},
});
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "away" });
await start();
await waitForSteps([AWAY_DELAY]);
localStorage.setItem("presence.lastPresence", Date.now());
await waitForSteps([0]);
});
test("update presence when user status changes to away", async () => {
mockService("bus_service", {
send: (type, payload) => {
if (type === "update_presence") {
asyncStep(payload.inactivity_period);
}
},
});
localStorage.setItem("presence.lastPresence", Date.now());
const pyEnv = await startServer();
pyEnv["res.partner"].write(serverState.partnerId, { im_status: "online" });
await start();
await waitForSteps([0]);
await advanceTime(AWAY_DELAY);
await waitForSteps([AWAY_DELAY]);
});
test("new tab update presence when user comes back from away", async () => {
// Tabs notify presence with a debounced update, and the status service skips
// duplicates. This test ensures a new tab that never sent presence still issues
// its first update (important when old tabs close and new ones replace them).
localStorage.setItem("presence.lastPresence", Date.now() - AWAY_DELAY);
const pyEnv = await startServer();
pyEnv["res.partner"].write([serverState.partnerId], { im_status: "offline" });
const tabEnv_1 = await makeMockEnv();
patchWithCleanup(tabEnv_1.services.bus_service, {
send: (type) => {
if (type === "update_presence") {
expect.step("update_presence");
}
},
});
tabEnv_1.services.bus_service.start();
await expect.waitForSteps(["update_presence"]);
restoreRegistry(registry);
const tabEnv_2 = await makeMockEnv(null, { makeNew: true });
patchWithCleanup(tabEnv_2.services.bus_service, {
send: (type) => {
if (type === "update_presence") {
expect.step("update_presence");
}
},
});
tabEnv_2.services.bus_service.start();
await expect.waitForSteps([]);
localStorage.setItem("presence.lastPresence", Date.now()); // Simulate user presence.
await expect.waitForSteps(["update_presence", "update_presence"]);
});

View file

@ -0,0 +1,191 @@
import {
click,
contains,
defineMailModels,
openDiscuss,
scroll,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test, expect } from "@odoo/hoot";
import { disableAnimations } from "@odoo/hoot-mock";
describe.current.tags("desktop");
defineMailModels();
async function assertPinnedPanelUnpinCount(expectedCount) {
await contains(".dropdown-item", { text: "Unpin", count: expectedCount });
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", {
text: "Test pinned message",
});
expect(".o-discuss-PinnedMessagesPanel button[title='Unpin']").toHaveCount(expectedCount);
}
test("Pin message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
await contains(".o-discuss-PinnedMessagesPanel p", {
text: "This channel doesn't have any pinned messages.",
});
await click(".o-mail-Message [title='Expand']");
await click(".dropdown-item", { text: "Pin" });
await click(".modal-footer button", { text: "Yeah, pin it!" });
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", { text: "Hello world!" });
});
test("Unpin message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
pinned_at: "2023-03-30 11:27:11",
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message");
await click(".o-mail-Message [title='Expand']");
await click(".dropdown-item", { text: "Unpin" });
await click(".modal-footer button", { text: "Yes, remove it please" });
await contains(".o-discuss-PinnedMessagesPanel .o-mail-Message", { count: 0 });
});
test("Open pinned panel from notification", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await click(":nth-child(1 of .o-mail-Message) [title='Expand']");
await click(".dropdown-item", { text: "Pin" });
await click(".modal-footer button", { text: "Yeah, pin it!" });
await contains(".o-discuss-PinnedMessagesPanel", { count: 0 });
await click(".o_mail_notification a", { text: "See all pinned messages" });
await contains(".o-discuss-PinnedMessagesPanel");
});
test("Jump to message", async () => {
disableAnimations();
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
pinned_at: "2023-04-03 08:15:04",
});
for (let i = 0; i < 20; i++) {
pyEnv["mail.message"].create({
body: "Non Empty Body ".repeat(25),
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
await click(".o-discuss-PinnedMessagesPanel a[role='button']", { text: "Jump" });
await contains(".o-mail-Thread .o-mail-Message-body", { text: "Hello world!", visible: true });
});
test("Jump to message from notification", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
});
for (let i = 0; i < 20; i++) {
pyEnv["mail.message"].create({
body: "Non Empty Body ".repeat(25),
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 21 });
await click(":nth-child(1 of .o-mail-Message) [title='Expand']");
await click(".dropdown-item", { text: "Pin" });
await click(".modal-footer button", { text: "Yeah, pin it!" });
await contains(".o_mail_notification");
await scroll(".o-mail-Thread", "bottom");
await contains(".o-mail-Thread", { scroll: "bottom" });
await click(".o_mail_notification a", { text: "message" });
await contains(".o-mail-Thread", { count: 0, scroll: "bottom" });
});
test("can add reactions from pinned panel", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Hello world!",
model: "discuss.channel",
res_id: channelId,
pinned_at: "2025-10-09 11:15:04",
});
await start();
await openDiscuss(channelId);
await click(".o-mail-Message-actions [title='Add a Reaction']");
await click(".o-mail-QuickReactionMenu button", { text: "👍" });
await contains(".o-mail-MessageReaction", { text: "👍1" });
await click(".o-mail-DiscussContent-header button[title='Pinned Messages']");
await click(".o-discuss-PinnedMessagesPanel .o-mail-Message [title='Add a Reaction']");
await click(".o-mail-QuickReactionMenu button", { text: "👍" });
await contains(".o-mail-MessageReaction", { count: 0 });
});
test("Guest user cannot see unpin button", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
name: "General",
channel_type: "channel",
});
pyEnv["mail.message"].create({
body: "Test pinned message",
model: "discuss.channel",
res_id: channelId,
pinned_at: "2023-03-30 11:27:11",
});
await start({ authenticateAs: false });
await openDiscuss(channelId);
await contains(".o-mail-Message", { text: "Test pinned message" });
expect(".o-mail-Message [title='Expand']").toHaveCount(0);
await assertPinnedPanelUnpinCount(0);
});
test("Internal user can see unpin button", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
body: "Test pinned message",
model: "discuss.channel",
res_id: channelId,
pinned_at: "2023-03-30 11:27:11",
});
await start();
await openDiscuss(channelId);
await click(".o-mail-Message [title='Expand']");
await assertPinnedPanelUnpinCount(1);
});

View file

@ -0,0 +1,348 @@
import {
click,
contains,
defineMailModels,
insertText,
onRpcBefore,
openDiscuss,
patchUiSize,
scroll,
SIZES,
start,
startServer,
triggerHotkey,
} from "@mail/../tests/mail_test_helpers";
import { expect, mockTouch, mockUserAgent, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { tick } from "@odoo/hoot-mock";
import { serverState } from "@web/../tests/web_test_helpers";
import { HIGHLIGHT_CLASS } from "@mail/core/common/message_search_hook";
defineMailModels();
test.tags("desktop");
test("Should have a search button", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains("[title='Search Messages']");
});
test.tags("desktop");
test("Should open the search panel when search button is clicked", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("[title='Search Messages']");
await contains(".o-mail-SearchMessagesPanel");
await contains(".o_searchview");
await contains(".o_searchview_input");
});
test.tags("desktop");
test("Should open the search panel with hotkey 'f'", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await press("alt+f");
await contains(".o-mail-SearchMessagesPanel");
});
test.tags("desktop");
test("Search a message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await click("button[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
});
test.tags("desktop");
test("Search should be hightlighted", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(`.o-mail-SearchMessagesPanel .o-mail-Message .${HIGHLIGHT_CLASS}`);
});
test.tags("desktop");
test("Search a starred message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
starred_partner_ids: [serverState.partnerId],
});
await start();
await openDiscuss("mail.box_starred");
await contains(".o-mail-Message");
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
});
test.tags("desktop");
test("Search a message in inbox", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
needaction: true,
});
await start();
await openDiscuss("mail.box_inbox");
await contains(".o-mail-Message");
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
});
test.tags("desktop");
test("Search a message in history (desktop)", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
needaction: false,
});
pyEnv["mail.notification"].create({
is_read: true,
mail_message_id: messageId,
notification_status: "sent",
notification_type: "inbox",
res_partner_id: serverState.partnerId,
});
await start();
await openDiscuss("mail.box_history");
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
await click(".o-mail-SearchMessagesPanel .o-mail-MessageCard-jump");
await contains(".o-mail-Thread .o-mail-Message.o-highlighted");
});
test.tags("mobile");
test("Search a message in history (mobile)", async () => {
mockTouch(true);
mockUserAgent("android");
patchUiSize({ size: SIZES.SM });
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const messageId = pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
needaction: false,
});
pyEnv["mail.notification"].create({
is_read: true,
mail_message_id: messageId,
notification_status: "sent",
notification_type: "inbox",
res_partner_id: serverState.partnerId,
});
await start();
await openDiscuss("mail.box_history");
await contains(".o-mail-Thread");
await click("[title='Search Messages']");
await contains(".o-mail-SearchMessagesPanel");
await contains(".o-mail-Thread", { count: 0 });
await insertText(".o_searchview_input", "message");
await triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
await click(".o-mail-MessageCard-jump");
await contains(".o-mail-Thread");
await contains(".o-mail-SearchMessagesPanel", { count: 0 });
});
test.tags("desktop");
test("Should close the search panel when search button is clicked again", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("[title='Search Messages']");
await click("[title='Close Search']");
await contains(".o-mail-SearchMessagesPanel");
});
test.tags("desktop");
test("Search a message in 60 messages should return 30 message first", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
for (let i = 0; i < 60; i++) {
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 30 });
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
// give enough time to useVisible to potentially load more (unexpected) messages
await tick();
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
});
test.tags("desktop");
test("Scrolling to the bottom should load more searched message", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
for (let i = 0; i < 90; i++) {
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message", { count: 30 });
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 30 });
await scroll(".o-mail-SearchMessagesPanel .o-mail-ActionPanel", "bottom");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 60 });
// give enough time to useVisible to potentially load more (unexpected) messages
await tick();
await contains(".o-mail-SearchMessagesPanel .o-mail-Message", { count: 60 });
});
test.tags("desktop");
test("Editing the searched term should not edit the current searched term", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
for (let i = 0; i < 60; i++) {
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a message",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
}
onRpcBefore("/discuss/channel/messages", (args) => {
if (args.search_term) {
const { search_term } = args;
expect(search_term).toBe("message");
}
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMemberList"); // wait for auto-open of this panel
await click("[title='Search Messages']");
await insertText(".o_searchview_input", "message");
triggerHotkey("Enter");
await insertText(".o_searchview_input", "test");
await scroll(".o-mail-SearchMessagesPanel .o-mail-ActionPanel", "bottom");
});
test.tags("desktop");
test("Search a message containing round brackets", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: serverState.partnerId,
body: "This is a (message)",
attachment_ids: [],
message_type: "comment",
model: "discuss.channel",
res_id: channelId,
});
await start();
await openDiscuss(channelId);
await contains(".o-mail-Message");
await click("button[title='Search Messages']");
await insertText(".o_searchview_input", "(message");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
});
test.tags("desktop");
test("Search a message containing single quotes", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "I can't do it");
await click(".o-sendMessageActive:enabled");
await contains(".o-mail-Message");
await click("button[title='Search Messages']");
await insertText(".o_searchview_input", "can't");
triggerHotkey("Enter");
await contains(".o-mail-SearchMessagesPanel .o-mail-Message");
});

View file

@ -0,0 +1,906 @@
import { insertText as htmlInsertText } from "@html_editor/../tests/_helpers/user_actions";
import {
click,
contains,
defineMailModels,
insertText,
onRpcBefore,
openDiscuss,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, test } from "@odoo/hoot";
import { advanceTime, mockDate } from "@odoo/hoot-mock";
import {
asyncStep,
Command,
getService,
serverState,
waitForSteps,
withUser,
} from "@web/../tests/web_test_helpers";
import { Store } from "@mail/core/common/store_service";
import { LONG_TYPING, SHORT_TYPING } from "@mail/discuss/typing/common/composer_patch";
import { rpc } from "@web/core/network/rpc";
describe.current.tags("desktop");
defineMailModels();
test('[text composer] receive other member typing status "is typing"', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
// simulate receive typing notification from demo
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
});
test.tags("html composer");
test('receive other member typing status "is typing"', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
});
test('[text composer] receive other member typing status "is typing" then "no longer is typing"', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
// simulate receive typing notification from demo "is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
// simulate receive typing notification from demo "is no longer typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test.tags("html composer");
test('receive other member typing status "is typing" then "no longer is typing"', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test('[text composer] assume other member typing status becomes "no longer is typing" after long without any updated typing status', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
// simulate receive typing notification from demo "is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(Store.OTHER_LONG_TYPING);
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test.tags("html composer");
test('assume other member typing status becomes "no longer is typing" after long without any updated typing status', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(Store.OTHER_LONG_TYPING);
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test('"is typing" timeout should work even when 2 notify_typing happen at the exact same time', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
mockDate("2024-01-01 12:00:00");
await withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(Store.OTHER_LONG_TYPING);
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing..." });
});
test('[text composer] other member typing status "is typing" refreshes of assuming no longer typing', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
// simulate receive typing notification from demo "is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
// simulate receive typing notification from demo "is typing" again after long time.
await advanceTime(LONG_TYPING);
await withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await advanceTime(LONG_TYPING);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(Store.OTHER_LONG_TYPING - LONG_TYPING);
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test.tags("html composer");
test('other member typing status "is typing" refreshes of assuming no longer typing', async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(LONG_TYPING);
await withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await advanceTime(LONG_TYPING);
await contains(".o-discuss-Typing", { text: "Demo is typing..." });
await advanceTime(Store.OTHER_LONG_TYPING - LONG_TYPING);
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
});
test('[text composer] receive several other members typing status "is typing"', async () => {
const pyEnv = await startServer();
const [userId_1, userId_2, userId_3] = pyEnv["res.users"].create([
{ name: "Other 10" },
{ name: "Other 11" },
{ name: "Other 12" },
]);
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
{ name: "Other 10", user_ids: [userId_1] },
{ name: "Other 11", user_ids: [userId_2] },
{ name: "Other 12", user_ids: [userId_3] },
]);
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
Command.create({ partner_id: partnerId_3 }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
// simulate receive typing notification from other 10 (is typing)
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10 is typing..." });
// simulate receive typing notification from other 11 (is typing)
withUser(userId_2, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10 and Other 11 are typing..." });
// simulate receive typing notification from other 12 (is typing)
withUser(userId_3, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10, Other 11 and more are typing..." });
// simulate receive typing notification from other 10 (no longer is typing)
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".o-discuss-Typing", { text: "Other 11 and Other 12 are typing..." });
// simulate receive typing notification from other 10 (is typing again)
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 11, Other 12 and more are typing..." });
});
test.tags("html composer");
test('receive several other members typing status "is typing"', async () => {
const pyEnv = await startServer();
const [userId_1, userId_2, userId_3] = pyEnv["res.users"].create([
{ name: "Other 10" },
{ name: "Other 11" },
{ name: "Other 12" },
]);
const [partnerId_1, partnerId_2, partnerId_3] = pyEnv["res.partner"].create([
{ name: "Other 10", user_ids: [userId_1] },
{ name: "Other 11", user_ids: [userId_2] },
{ name: "Other 12", user_ids: [userId_3] },
]);
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId_1 }),
Command.create({ partner_id: partnerId_2 }),
Command.create({ partner_id: partnerId_3 }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10 is typing..." });
withUser(userId_2, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10 and Other 11 are typing..." });
withUser(userId_3, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 10, Other 11 and more are typing..." });
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".o-discuss-Typing", { text: "Other 11 and Other 12 are typing..." });
withUser(userId_1, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing", { text: "Other 11, Other 12 and more are typing..." });
});
test("[text composer] current partner notify is typing to other thread members", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "a");
await waitForSteps(["notify_typing:true"]);
testEnded = true;
});
test.tags("html composer");
test("current partner notify is typing to other thread members", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "a");
await waitForSteps(["notify_typing:true"]);
testEnded = true;
});
test("[text composer] current partner notify is typing again to other members for long continuous typing", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await insertText(".o-mail-Composer-input", "a");
await waitForSteps(["notify_typing:true"]);
// simulate current partner typing a character for a long time.
const elapseTickTime = SHORT_TYPING / 2;
for (let i = 0; i <= LONG_TYPING / elapseTickTime; i++) {
await insertText(".o-mail-Composer-input", "a");
await advanceTime(elapseTickTime);
}
await waitForSteps(["notify_typing:true"]);
testEnded = true;
});
test.tags("html composer");
test("current partner notify is typing again to other members for long continuous typing", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-html.odoo-editor-editable");
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "a");
await waitForSteps(["notify_typing:true"]);
const elapseTickTime = SHORT_TYPING / 2;
for (let i = 0; i <= LONG_TYPING / elapseTickTime; i++) {
await htmlInsertText(editor, "a");
await advanceTime(elapseTickTime);
}
await waitForSteps(["notify_typing:true"]);
testEnded = true;
});
test("[text composer] current partner notify no longer is typing to thread members after 5 seconds inactivity", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
onRpcBefore("/discuss/channel/notify_typing", (args) =>
asyncStep(`notify_typing:${args.is_typing}`)
);
await start();
await openDiscuss(channelId);
await advanceTime(Store.FETCH_DATA_DEBOUNCE_DELAY);
await insertText(".o-mail-Composer-input", "a");
await waitForSteps(["notify_typing:true"]);
await advanceTime(SHORT_TYPING);
await waitForSteps(["notify_typing:false"]);
});
test.tags("html composer");
test("current partner notify no longer is typing to thread members after 5 seconds inactivity", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
onRpcBefore("/discuss/channel/notify_typing", (args) =>
asyncStep(`notify_typing:${args.is_typing}`)
);
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "a");
await waitForSteps(["notify_typing:true"]);
await advanceTime(SHORT_TYPING);
await waitForSteps(["notify_typing:false"]);
});
test("[text composer] current partner is typing should not translate on textual typing status", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
await openDiscuss(channelId);
await insertText(".o-mail-Composer-input", "a");
await waitForSteps(["notify_typing:true"]);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
testEnded = true;
});
test.tags("html composer");
test("current partner is typing should not translate on textual typing status", async () => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "general" });
let testEnded = false;
onRpcBefore("/discuss/channel/notify_typing", (args) => {
if (!testEnded) {
asyncStep(`notify_typing:${args.is_typing}`);
}
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "a");
await waitForSteps(["notify_typing:true"]);
await contains(".o-discuss-Typing");
await contains(".o-discuss-Typing", { count: 0, text: "Demo is typing...)" });
testEnded = true;
});
test("[text composer] chat: correspondent is typing", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
await start();
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel .o-mail-DiscussSidebarChannel-threadIcon");
await contains(".fa-circle.text-success");
// simulate receive typing notification from demo "is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing-icon[title='Demo is typing...']");
// simulate receive typing notification from demo "no longer is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".fa-circle.text-success");
});
test.tags("html composer");
test("chat: correspondent is typing", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss();
await contains(".o-mail-DiscussSidebarChannel .o-mail-DiscussSidebarChannel-threadIcon");
await contains(".fa-circle.text-success");
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-Typing-icon[title='Demo is typing...']");
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains(".fa-circle.text-success");
});
test("[text composer] chat: correspondent is typing in chat window", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
await start();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem");
await contains("[title='Demo is typing...']", { count: 0 });
// simulate receive typing notification from demo "is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains("[title='Demo is typing...']", { count: 2 }); // icon in header & text above composer
// simulate receive typing notification from demo "no longer is typing"
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains("[title='Demo is typing...']", { count: 0 });
});
test.tags("html composer");
test("chat: correspondent is typing in chat window", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await click(".o_menu_systray i[aria-label='Messages']");
await click(".o-mail-NotificationItem");
await contains("[title='Demo is typing...']", { count: 0 });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains("[title='Demo is typing...']", { count: 2 });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: false,
})
);
await contains("[title='Demo is typing...']", { count: 0 });
});
test("[text composer] show typing in member list", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Other 10" });
const partnerId = pyEnv["res.partner"].create({ name: "Other 10", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { count: 2 });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-ChannelMemberList [title='Other 10 is typing...']");
withUser(serverState.userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(
`.o-discuss-ChannelMemberList [title='${serverState.partnerName} is typing...']`
);
});
test.tags("html composer");
test("show typing in member list", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Other 10" });
const partnerId = pyEnv["res.partner"].create({ name: "Other 10", user_ids: [userId] });
const channelId = pyEnv["discuss.channel"].create({
name: "channel",
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
});
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(channelId);
await contains(".o-discuss-ChannelMember", { count: 2 });
withUser(userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(".o-discuss-ChannelMemberList [title='Other 10 is typing...']");
withUser(serverState.userId, () =>
rpc("/discuss/channel/notify_typing", {
channel_id: channelId,
is_typing: true,
})
);
await contains(
`.o-discuss-ChannelMemberList [title='${serverState.partnerName} is typing...']`
);
});
test("[text composer] switching to another channel triggers notify_typing to stop", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const chatId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
pyEnv["discuss.channel"].create({ name: "general" });
onRpcBefore("/discuss/channel/notify_typing", (args) =>
asyncStep(`notify_typing:${args.is_typing}`)
);
await start();
await openDiscuss(chatId);
await insertText(".o-mail-Composer-input", "a");
await waitForSteps(["notify_typing:true"]);
await click(".o-mail-DiscussSidebar-item", { text: "general" });
await advanceTime(SHORT_TYPING / 2);
await waitForSteps(["notify_typing:false"]);
});
test.tags("html composer");
test("switching to another channel triggers notify_typing to stop", async () => {
const pyEnv = await startServer();
const userId = pyEnv["res.users"].create({ name: "Demo" });
const partnerId = pyEnv["res.partner"].create({
im_status: "online",
name: "Demo",
user_ids: [userId],
});
const chatId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
pyEnv["discuss.channel"].create({ name: "general" });
onRpcBefore("/discuss/channel/notify_typing", (args) =>
asyncStep(`notify_typing:${args.is_typing}`)
);
await start();
const composerService = getService("mail.composer");
composerService.setHtmlComposer();
await openDiscuss(chatId);
await contains(".o-mail-Composer-html.odoo-editor-editable");
const editor = {
document,
editable: document.querySelector(".o-mail-Composer-html.odoo-editor-editable"),
};
await htmlInsertText(editor, "a");
await waitForSteps(["notify_typing:true"]);
await click(".o-mail-DiscussSidebar-item", { text: "general" });
await advanceTime(SHORT_TYPING / 2);
await waitForSteps(["notify_typing:false"]);
});

View file

@ -0,0 +1,96 @@
import {
click,
contains,
defineMailModels,
mockGetMedia,
openDiscuss,
patchVoiceMessageAudio,
start,
startServer,
} from "@mail/../tests/mail_test_helpers";
import { describe, globals, test } from "@odoo/hoot";
import { Deferred, mockDate } from "@odoo/hoot-mock";
import { Command, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { loadLamejs } from "@mail/discuss/voice_message/common/voice_message_service";
import { VoicePlayer } from "@mail/discuss/voice_message/common/voice_player";
import { patchable } from "@mail/discuss/voice_message/common/voice_recorder";
import { Mp3Encoder } from "@mail/discuss/voice_message/common/mp3_encoder";
describe.current.tags("desktop");
defineMailModels();
test("make voice message in chat", async () => {
const file = new File([new Uint8Array(25000)], "test.mp3", { type: "audio/mp3" });
const voicePlayerDrawing = new Deferred();
patchWithCleanup(Mp3Encoder.prototype, {
encode() {},
finish() {
return Array(500).map(() => new Int8Array());
},
});
patchWithCleanup(patchable, { makeFile: () => file });
patchWithCleanup(VoicePlayer.prototype, {
async drawWave(...args) {
voicePlayerDrawing.resolve();
return super.drawWave(...args);
},
async fetchFile() {
return super.fetchFile("/mail/static/src/audio/call-invitation.mp3");
},
_fetch(url) {
if (url.includes("call-invitation.mp3")) {
const realFetch = globals.fetch;
return realFetch(...arguments);
}
return super._fetch(...arguments);
},
});
mockGetMedia();
const resources = patchVoiceMessageAudio();
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: serverState.partnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
await start();
await openDiscuss(channelId);
await loadLamejs(); // simulated AudioProcess.process() requires lamejs fully loaded
await click(".o-mail-Composer button[title='More Actions']");
await contains(".dropdown-item:contains('Voice Message')");
mockDate("2023-07-31 13:00:00");
await click(".dropdown-item:contains('Voice Message')");
await contains(".o-mail-VoiceRecorder", { text: "00 : 00" });
/**
* Simulate 10 sec elapsed.
* `patchDate` does not freeze the time, it merely changes the value of "now" at the time it was
* called. The code of click following the first `patchDate` doesn't actually happen at the time
* that was specified, but few miliseconds later (8 ms on my machine).
* The process following the next `patchDate` is intended to be between 10s and 11s later than
* the click, because the test wants to assert a 10 sec counter, and the two dates are
* substracted and then rounded down in the code (it means absolute values are irrelevant here).
* The problem with aiming too close to a 10s difference is that if the click is longer than
* the following process, it will round down to 9s.
* The problem with aiming too close to a 11s difference is that if the click is shorter than
* the following process, it will round down to 11s.
* The best bet is therefore to use 10s + 500ms difference.
*/
mockDate("2023-07-31 13:00:10.500");
// simulate some microphone data
resources.audioProcessor.process([[new Float32Array(128)]]);
await contains(".o-mail-VoiceRecorder", { text: "00 : 10" });
await click(".o-mail-Composer button[title='Stop Recording']");
await contains(".o-mail-VoicePlayer");
// wait for audio stream decode + drawing of waves
await voicePlayerDrawing;
await contains(".o-mail-VoicePlayer button[title='Play']");
await contains(".o-mail-VoicePlayer canvas", { count: 2 }); // 1 for global waveforms, 1 for played waveforms
await contains(".o-mail-VoicePlayer", { text: "00 : 03" }); // duration of call-invitation_.mp3
await click(".o-mail-Composer button[title='More Actions']");
await contains(".dropdown-item:contains('Attach Files')"); // check menu loaded
await contains(".dropdown-item:contains('Voice Message')", { count: 0 }); // only 1 voice message at a time
});